From b32544e76c6e2d7f2b0b6d88d70b421b75353e99 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Thu, 30 Oct 2025 18:13:47 -0700 Subject: [PATCH] feat: complete project identities --- .../ee/services/permission/default-roles.ts | 6 +- .../services/permission/project-permission.ts | 6 +- backend/src/lib/api-docs/constants.ts | 8 +- backend/src/server/app.ts | 1 + backend/src/server/routes/index.ts | 3 +- .../v1/identity-org-membership-router.ts | 6 +- ... => identity-project-membership-router.ts} | 14 +- backend/src/server/routes/v1/index.ts | 2 +- .../routes/v1/project-identity-router.ts | 2 + .../identity-alicloud-auth-service.ts | 213 ++++-- .../identity-aws-auth-service.ts | 213 ++++-- .../identity-azure-auth-service.ts | 208 +++-- .../identity-gcp-auth-service.ts | 207 +++-- .../identity-jwt-auth-service.ts | 223 ++++-- .../identity-kubernetes-auth-service.ts | 211 +++-- .../identity-ldap-auth-service.ts | 234 ++++-- .../identity-oci-auth-service.ts | 219 ++++-- .../identity-oidc-auth-service.ts | 228 ++++-- .../identity-project/identity-project-dal.ts | 17 +- .../identity-tls-cert-auth-service.ts | 218 ++++-- .../identity-token-auth-service.ts | 471 ++++++++---- .../identity-ua/identity-ua-service.ts | 592 ++++++++------ .../src/services/identity-v2/identity-fns.ts | 24 + .../services/identity-v2/identity-service.ts | 10 +- .../project/project-identity-factory.ts | 8 +- .../src/services/identity/identity-org-dal.ts | 2 + .../src/services/identity/identity-service.ts | 30 +- .../membership-identity-dal.ts | 7 +- .../membership-identity-service.ts | 3 +- .../project-membership-identity-factory.ts | 8 +- .../context/ProjectPermissionContext/types.ts | 6 +- .../src/hooks/api/identities/mutations.tsx | 721 ++++++++++++------ frontend/src/hooks/api/identities/queries.tsx | 8 +- frontend/src/hooks/api/identities/types.ts | 194 +++-- frontend/src/hooks/api/index.tsx | 4 + frontend/src/hooks/api/orgIdentity/index.ts | 3 + .../src/hooks/api/orgIdentity/mutations.tsx | 116 +++ .../src/hooks/api/orgIdentity/queries.tsx | 40 + frontend/src/hooks/api/orgIdentity/types.ts | 31 + .../api/orgIdentityMembership/queries.tsx | 31 + .../hooks/api/orgIdentityMembership/types.ts | 33 +- frontend/src/hooks/api/organization/index.ts | 1 - .../src/hooks/api/organization/queries.tsx | 13 - .../hooks/api/organizationIdentity/index.tsx | 15 - .../api/organizationIdentity/mutations.tsx | 58 -- .../api/organizationIdentity/queries.tsx | 44 -- .../hooks/api/organizationIdentity/types.ts | 43 -- .../hooks/api/projectIdentity/mutations.tsx | 28 +- .../src/hooks/api/projectIdentity/queries.tsx | 6 +- .../src/hooks/api/projectIdentity/types.ts | 23 +- .../api/projectIdentityMembership/index.ts | 3 + .../projectIdentityMembership/mutations.tsx | 93 +++ .../api/projectIdentityMembership/queries.ts | 101 +++ .../api/projectIdentityMembership/types.ts | 36 + frontend/src/hooks/api/projects/index.tsx | 5 - frontend/src/hooks/api/projects/queries.tsx | 156 +--- frontend/src/hooks/api/projects/types.ts | 18 - frontend/src/hooks/api/shared/index.ts | 1 + frontend/src/hooks/api/shared/types.ts | 37 + .../src/hooks/api/subscriptions/queries.tsx | 3 +- .../IdentityAliCloudAuthForm.tsx | 9 +- .../IdentitySection/IdentityAwsAuthForm.tsx | 13 +- .../IdentitySection/IdentityAzureAuthForm.tsx | 9 +- .../IdentitySection/IdentityGcpAuthForm.tsx | 10 +- .../IdentitySection/IdentityJwtAuthForm.tsx | 10 +- .../IdentityKubernetesAuthForm.tsx | 9 +- .../IdentitySection/IdentityLdapAuthForm.tsx | 7 +- .../IdentitySection/IdentityOciAuthForm.tsx | 10 +- .../IdentitySection/IdentityOidcAuthForm.tsx | 10 +- .../IdentitySection/IdentitySection.tsx | 109 ++- .../IdentitySection/IdentityTable.tsx | 27 +- .../IdentityTlsCertAuthForm.tsx | 10 +- .../IdentitySection/IdentityTokenAuthForm.tsx | 9 +- .../IdentityUniversalAuthForm.tsx | 9 +- ...tyLinkForm.tsx => OrgIdentityLinkForm.tsx} | 21 +- ...IdentityModal.tsx => OrgIdentityModal.tsx} | 8 +- .../IdentityDetailsByIDPage.tsx | 12 +- .../IdentityAuthenticationSection.tsx | 8 +- .../IdentityClientSecrets.tsx | 6 +- .../IdentityTokens.tsx | 4 +- .../components/IdentityDetailsSection.tsx | 4 +- .../IdentityAddToProjectModal.tsx | 4 +- .../IdentityProjectRow.tsx | 4 +- .../IdentityProjectsSection.tsx | 4 +- .../IdentityAuthLockoutFields.tsx | 33 +- .../IdentityTokenAuthTokensTable.tsx | 67 +- ...dentityUniversalAuthClientSecretsTable.tsx | 48 +- .../ViewIdentityAliCloudAuthContent.tsx | 1 + .../ViewIdentityAuthModal.tsx | 15 +- .../ViewIdentityAwsAuthContent.tsx | 1 + .../ViewIdentityAzureAuthContent.tsx | 1 + .../ViewIdentityContentWrapper.tsx | 56 +- .../ViewIdentityGcpAuthContent.tsx | 1 + .../ViewIdentityJwtAuthContent.tsx | 1 + .../ViewIdentityKubernetesAuthContent.tsx | 1 + .../ViewIdentityLdapAuthContent.tsx | 1 + .../ViewIdentityOciAuthContent.tsx | 1 + .../ViewIdentityOidcAuthContent.tsx | 1 + .../ViewIdentityTlsCertAuthContent.tsx | 1 + .../ViewIdentityTokenAuthContent.tsx | 1 + .../ViewIdentityUniversalAuthContent.tsx | 1 + .../components/IdentityTab/IdentityTab.tsx | 179 +++-- .../components/ProjectIdentityModal.tsx | 310 ++++++++ ...Modal.tsx => ProjectLinkIdentityModal.tsx} | 118 ++- .../IdentityDetailsByIDPage.tsx | 108 ++- ...ntityProjectAdditionalPrivilegeSection.tsx | 4 +- .../IdentityRoleDetailsSection.tsx | 10 +- .../IdentityRoleModify.tsx | 16 +- .../components/ProjectIdentityAuthSection.tsx | 119 +++ .../ProjectIdentityDetailsSection.tsx | 227 ++++++ .../ProjectRoleModifySection.utils.tsx | 20 +- 111 files changed, 4820 insertions(+), 2113 deletions(-) rename backend/src/server/routes/v1/{identity-project-router.ts => identity-project-membership-router.ts} (97%) create mode 100644 backend/src/services/identity-v2/identity-fns.ts create mode 100644 frontend/src/hooks/api/orgIdentity/index.ts create mode 100644 frontend/src/hooks/api/orgIdentity/mutations.tsx create mode 100644 frontend/src/hooks/api/orgIdentity/queries.tsx create mode 100644 frontend/src/hooks/api/orgIdentity/types.ts create mode 100644 frontend/src/hooks/api/orgIdentityMembership/queries.tsx delete mode 100644 frontend/src/hooks/api/organizationIdentity/index.tsx delete mode 100644 frontend/src/hooks/api/organizationIdentity/mutations.tsx delete mode 100644 frontend/src/hooks/api/organizationIdentity/queries.tsx delete mode 100644 frontend/src/hooks/api/organizationIdentity/types.ts create mode 100644 frontend/src/hooks/api/projectIdentityMembership/index.ts create mode 100644 frontend/src/hooks/api/projectIdentityMembership/mutations.tsx create mode 100644 frontend/src/hooks/api/projectIdentityMembership/queries.ts create mode 100644 frontend/src/hooks/api/projectIdentityMembership/types.ts create mode 100644 frontend/src/hooks/api/shared/index.ts create mode 100644 frontend/src/hooks/api/shared/types.ts rename frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/{IdentityLinkForm.tsx => OrgIdentityLinkForm.tsx} (83%) rename frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/{IdentityModal.tsx => OrgIdentityModal.tsx} (97%) create mode 100644 frontend/src/pages/project/AccessControlPage/components/IdentityTab/components/ProjectIdentityModal.tsx rename frontend/src/pages/project/AccessControlPage/components/IdentityTab/components/{IdentityModal.tsx => ProjectLinkIdentityModal.tsx} (55%) create mode 100644 frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityAuthSection.tsx create mode 100644 frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityDetailsSection.tsx diff --git a/backend/src/ee/services/permission/default-roles.ts b/backend/src/ee/services/permission/default-roles.ts index 5e7025f05..81814a67c 100644 --- a/backend/src/ee/services/permission/default-roles.ts +++ b/backend/src/ee/services/permission/default-roles.ts @@ -171,7 +171,11 @@ const buildAdminPermissionRules = () => { ProjectPermissionIdentityActions.Delete, ProjectPermissionIdentityActions.Read, ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionIdentityActions.AssumePrivileges + ProjectPermissionIdentityActions.AssumePrivileges, + ProjectPermissionIdentityActions.GetToken, + ProjectPermissionIdentityActions.CreateToken, + ProjectPermissionIdentityActions.DeleteToken, + ProjectPermissionIdentityActions.RevokeAuth ], ProjectPermissionSub.Identity ); diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 74e4554ed..19340644a 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -65,7 +65,11 @@ export enum ProjectPermissionIdentityActions { Edit = "edit", Delete = "delete", GrantPrivileges = "grant-privileges", - AssumePrivileges = "assume-privileges" + AssumePrivileges = "assume-privileges", + RevokeAuth = "revoke-auth", + CreateToken = "create-token", + GetToken = "get-token", + DeleteToken = "delete-token" } export enum ProjectPermissionMemberActions { diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index b837026e6..15ec747ef 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -757,7 +757,7 @@ export const ORG_IDENTITY_MEMBERSHIP = { LIST_IDENTITY_MEMBERSHIPS: { offset: "The offset to start from. If you enter 10, it will start from the 10th identity membership.", limit: "The number of identity memberships to return.", - identityName: "The text string that identity membership names will be filtered by.", + identityName: "", roles: "The role slugs to filter identity memberships by." }, GET_IDENTITY_MEMBERSHIP_BY_ID: { @@ -765,7 +765,8 @@ export const ORG_IDENTITY_MEMBERSHIP = { }, LIST_AVAILABLE_IDENTITIES: { offset: "The offset to start from. If you enter 10, it will start from the 10th identity.", - limit: "The number of identities to return." + limit: "The number of identities to return.", + identityName: "The text string that identity membership names will be filtered by." } } as const; @@ -1002,7 +1003,8 @@ export const PROJECT_IDENTITY_MEMBERSHIP = { LIST_AVAILABLE_IDENTITIES: { projectId: "The ID of the project to list available identities for.", offset: "The offset to start from. If you enter 10, it will start from the 10th identity.", - limit: "The number of identities to return." + limit: "The number of identities to return.", + identityName: "The text string that identity membership names will be filtered by." } } as const; diff --git a/backend/src/server/app.ts b/backend/src/server/app.ts index 60b678f63..c6aaca367 100644 --- a/backend/src/server/app.ts +++ b/backend/src/server/app.ts @@ -14,6 +14,7 @@ import { fastifyRequestContext } from "@fastify/request-context"; import fastify from "fastify"; import { Cluster, Redis } from "ioredis"; import { Knex } from "knex"; +import { monitorEventLoopDelay } from "perf_hooks"; import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; import { TKeyStoreFactory } from "@app/keystore/keystore"; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 3bfaa82ef..fab7ebde3 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1666,7 +1666,8 @@ export const registerRoutes = async ( identityMetadataDAL, licenseService, permissionService, - identityDAL: identityV2DAL + identityDAL: identityV2DAL, + keyStore }); const identityProjectService = identityProjectServiceFactory({ diff --git a/backend/src/server/routes/v1/identity-org-membership-router.ts b/backend/src/server/routes/v1/identity-org-membership-router.ts index de57280a0..275ed5475 100644 --- a/backend/src/server/routes/v1/identity-org-membership-router.ts +++ b/backend/src/server/routes/v1/identity-org-membership-router.ts @@ -430,7 +430,8 @@ export const registerIdentityOrgMembershipRouter = async (server: FastifyZodProv .max(100) .default(20) .describe(ORG_IDENTITY_MEMBERSHIP.LIST_AVAILABLE_IDENTITIES.limit) - .optional() + .optional(), + identityName: z.string().describe(ORG_IDENTITY_MEMBERSHIP.LIST_AVAILABLE_IDENTITIES.identityName).optional() }), response: { 200: z.object({ @@ -447,7 +448,8 @@ export const registerIdentityOrgMembershipRouter = async (server: FastifyZodProv }, data: { offset: req.query.offset, - limit: req.query.limit + limit: req.query.limit, + identityName: req.query.identityName } }); diff --git a/backend/src/server/routes/v1/identity-project-router.ts b/backend/src/server/routes/v1/identity-project-membership-router.ts similarity index 97% rename from backend/src/server/routes/v1/identity-project-router.ts rename to backend/src/server/routes/v1/identity-project-membership-router.ts index 06fe6a14a..473a27f22 100644 --- a/backend/src/server/routes/v1/identity-project-router.ts +++ b/backend/src/server/routes/v1/identity-project-membership-router.ts @@ -293,7 +293,7 @@ export const registerIdentityProjectMembershipRouter = async (server: FastifyZod temporaryAccessEndTime: z.date().nullable().optional() }) ), - identity: IdentitiesSchema.pick({ name: true, id: true }).extend({ + identity: IdentitiesSchema.pick({ name: true, id: true, projectId: true, orgId: true }).extend({ authMethods: z.array(z.string()) }), project: SanitizedProjectSchema.pick({ name: true, id: true }) @@ -362,7 +362,9 @@ export const registerIdentityProjectMembershipRouter = async (server: FastifyZod temporaryAccessEndTime: z.date().nullable().optional() }) ), - identity: IdentitiesSchema.pick({ name: true, id: true }).extend({ + lastLoginAuthMethod: z.string().nullable().optional(), + lastLoginTime: z.date().nullable().optional(), + identity: IdentitiesSchema.pick({ name: true, id: true, projectId: true, orgId: true }).extend({ authMethods: z.array(z.string()) }), project: SanitizedProjectSchema.pick({ name: true, id: true }) @@ -469,6 +471,11 @@ export const registerIdentityProjectMembershipRouter = async (server: FastifyZod .max(100) .default(20) .describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_AVAILABLE_IDENTITIES.limit) + .optional(), + identityName: z + .string() + .trim() + .describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_AVAILABLE_IDENTITIES.identityName) .optional() }), response: { @@ -487,7 +494,8 @@ export const registerIdentityProjectMembershipRouter = async (server: FastifyZod }, data: { offset: req.query.offset, - limit: req.query.limit + limit: req.query.limit, + identityName: req.query.identityName } }); diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 4088c1490..29327d767 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -34,7 +34,7 @@ import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router"; import { registerIdentityOciAuthRouter } from "./identity-oci-auth-router"; import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router"; import { registerIdentityOrgMembershipRouter } from "./identity-org-membership-router"; -import { registerIdentityProjectMembershipRouter } from "./identity-project-router"; +import { registerIdentityProjectMembershipRouter } from "./identity-project-membership-router"; import { registerIdentityRouter } from "./identity-router"; import { registerIdentityTlsCertAuthRouter } from "./identity-tls-cert-auth-router"; import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router"; diff --git a/backend/src/server/routes/v1/project-identity-router.ts b/backend/src/server/routes/v1/project-identity-router.ts index 2a8a6c7d8..5d8696545 100644 --- a/backend/src/server/routes/v1/project-identity-router.ts +++ b/backend/src/server/routes/v1/project-identity-router.ts @@ -21,6 +21,8 @@ const sanitizedIdentitySchema = IdentitiesSchema.pick({ updatedAt: true, hasDeleteProtection: true }).extend({ + activeLockoutAuthMethods: z.string().array().optional(), + authMethods: z.string().array().optional(), metadata: z .object({ key: z.string(), diff --git a/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts index 0bbc6f480..fba7fee98 100644 --- a/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts +++ b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import { requestContext } from "@fastify/request-context"; import { AxiosError } from "axios"; -import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; +import { AccessScope, ActionProjectType, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -11,6 +11,7 @@ import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; import { crypto } from "@app/lib/crypto"; @@ -51,7 +52,7 @@ type TIdentityAliCloudAuthServiceFactoryDep = { >; membershipIdentityDAL: Pick; licenseService: Pick; - permissionService: Pick; + permissionService: Pick; orgDAL: Pick; }; @@ -105,7 +106,18 @@ export const identityAliCloudAuthServiceFactory = ({ // Generate the token const identityAccessToken = await identityAliCloudAuthDAL.transaction(async (tx) => { await membershipIdentityDAL.update( - { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + identity.projectId + ? { + scope: AccessScope.Project, + scopeOrgId: identity.orgId, + scopeProjectId: identity.projectId, + actorIdentityId: identity.id + } + : { + scope: AccessScope.Organization, + scopeOrgId: identity.orgId, + actorIdentityId: identity.id + }, { lastLoginAuthMethod: IdentityAuthMethod.ALICLOUD_AUTH, lastLoginTime: new Date() @@ -214,16 +226,34 @@ export const identityAliCloudAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Create, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionIdentityActions.Create, + OrgPermissionSubjects.Identity + ); + } const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { if ( @@ -300,16 +330,31 @@ export const identityAliCloudAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Edit, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + } const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { if ( @@ -362,15 +407,31 @@ export const identityAliCloudAuthServiceFactory = ({ const alicloudIdentityAuth = await identityAliCloudAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + } return { ...alicloudIdentityAuth, orgId: identityMembershipOrg.scopeOrgId }; }; @@ -397,45 +458,61 @@ export const identityAliCloudAuthServiceFactory = ({ message: "The identity does not have Alibaba Cloud auth" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - - const { permission: rolePermission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor: ActorType.IDENTITY, - actorId: identityMembershipOrg.identity.id, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - - const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); - const permissionBoundary = validatePrivilegeChangeOperation( - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.RevokeAuth, - OrgPermissionSubjects.Identity, - permission, - rolePermission - ); - - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to revoke Alibaba Cloud auth of identity with more privileged role", - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.RevokeAuth, - OrgPermissionSubjects.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.RevokeAuth, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to revoke Alibaba Cloud auth of identity with more privileged role", + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + const revokedIdentityAliCloudAuth = await identityAliCloudAuthDAL.transaction(async (tx) => { const deletedAliCloudAuth = await identityAliCloudAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.ALICLOUD_AUTH }, tx); 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 2252263d1..b3b6bbfce 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 @@ -1,10 +1,11 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import { requestContext } from "@fastify/request-context"; +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import axios from "axios"; import RE2 from "re2"; -import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; +import { AccessScope, ActionProjectType, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -12,6 +13,7 @@ import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; import { @@ -50,7 +52,7 @@ type TIdentityAwsAuthServiceFactoryDep = { identityAwsAuthDAL: Pick; membershipIdentityDAL: Pick; licenseService: Pick; - permissionService: Pick; + permissionService: Pick; orgDAL: Pick; }; @@ -179,7 +181,18 @@ export const identityAwsAuthServiceFactory = ({ const identityAccessToken = await identityAwsAuthDAL.transaction(async (tx) => { await membershipIdentityDAL.update( - { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + identity.projectId + ? { + scope: AccessScope.Project, + scopeOrgId: identity.orgId, + scopeProjectId: identity.projectId, + actorIdentityId: identity.id + } + : { + scope: AccessScope.Organization, + scopeOrgId: identity.orgId, + actorIdentityId: identity.id + }, { lastLoginAuthMethod: IdentityAuthMethod.AWS_AUTH, lastLoginTime: new Date() @@ -300,16 +313,34 @@ export const identityAwsAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission({ - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId, - scope: OrganizationActionScope.Any - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Create, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.Any + }); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionIdentityActions.Create, + OrgPermissionSubjects.Identity + ); + } const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { if ( @@ -389,16 +420,31 @@ export const identityAwsAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Edit, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + } const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { if ( @@ -453,15 +499,31 @@ export const identityAwsAuthServiceFactory = ({ const awsIdentityAuth = await identityAwsAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + } return { ...awsIdentityAuth, orgId: identityMembershipOrg.scopeOrgId }; }; @@ -488,45 +550,60 @@ export const identityAwsAuthServiceFactory = ({ message: "The identity does not have aws auth" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - - const { permission: rolePermission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor: ActorType.IDENTITY, - actorId: identityMembershipOrg.identity.id, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - - const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); - const permissionBoundary = validatePrivilegeChangeOperation( - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.RevokeAuth, - OrgPermissionSubjects.Identity, - permission, - rolePermission - ); - - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to revoke aws auth of identity with more privileged role", - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.RevokeAuth, - OrgPermissionSubjects.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.RevokeAuth, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to revoke aws auth of identity with more privileged role", + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } const revokedIdentityAwsAuth = await identityAwsAuthDAL.transaction(async (tx) => { const deletedAwsAuth = await identityAwsAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.AWS_AUTH }, tx); 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 0d6f8c3ee..3c05511d1 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 @@ -1,7 +1,7 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import { requestContext } from "@fastify/request-context"; -import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; +import { AccessScope, ActionProjectType, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -9,6 +9,7 @@ import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; import { @@ -46,7 +47,7 @@ type TIdentityAzureAuthServiceFactoryDep = { >; membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick; - permissionService: Pick; + permissionService: Pick; licenseService: Pick; orgDAL: Pick; }; @@ -99,7 +100,18 @@ export const identityAzureAuthServiceFactory = ({ const identityAccessToken = await identityAzureAuthDAL.transaction(async (tx) => { await membershipIdentityDAL.update( - { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + identity.projectId + ? { + scope: AccessScope.Project, + scopeOrgId: identity.orgId, + scopeProjectId: identity.projectId, + actorIdentityId: identity.id + } + : { + scope: AccessScope.Organization, + scopeOrgId: identity.orgId, + actorIdentityId: identity.id + }, { lastLoginAuthMethod: IdentityAuthMethod.AZURE_AUTH, lastLoginTime: new Date() @@ -205,16 +217,34 @@ export const identityAzureAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Create, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionIdentityActions.Create, + OrgPermissionSubjects.Identity + ); + } const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { if ( @@ -293,16 +323,31 @@ export const identityAzureAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Edit, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + } const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { if ( @@ -359,16 +404,31 @@ export const identityAzureAuthServiceFactory = ({ const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + } return { ...identityAzureAuth, orgId: identityMembershipOrg.scopeOrgId }; }; @@ -395,43 +455,59 @@ export const identityAzureAuthServiceFactory = ({ message: "The identity does not have azure auth" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - - const { permission: rolePermission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor: ActorType.IDENTITY, - actorId: identityMembershipOrg.identity.id, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); - const permissionBoundary = validatePrivilegeChangeOperation( - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.RevokeAuth, - OrgPermissionSubjects.Identity, - permission, - rolePermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to revoke azure auth of identity with more privileged role", - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.RevokeAuth, - OrgPermissionSubjects.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.RevokeAuth, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to revoke azure auth of identity with more privileged role", + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + const revokedIdentityAzureAuth = await identityAzureAuthDAL.transaction(async (tx) => { const deletedAzureAuth = await identityAzureAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.AZURE_AUTH }, tx); 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 d5f71b84c..847abd81f 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 @@ -1,7 +1,7 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import { requestContext } from "@fastify/request-context"; -import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; +import { AccessScope, ActionProjectType, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -9,6 +9,7 @@ import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; import { @@ -44,7 +45,7 @@ type TIdentityGcpAuthServiceFactoryDep = { identityGcpAuthDAL: Pick; membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick; - permissionService: Pick; + permissionService: Pick; licenseService: Pick; orgDAL: Pick; }; @@ -139,7 +140,18 @@ export const identityGcpAuthServiceFactory = ({ const identityAccessToken = await identityGcpAuthDAL.transaction(async (tx) => { await membershipIdentityDAL.update( - { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + identity.projectId + ? { + scope: AccessScope.Project, + scopeOrgId: identity.orgId, + scopeProjectId: identity.projectId, + actorIdentityId: identity.id + } + : { + scope: AccessScope.Organization, + scopeOrgId: identity.orgId, + actorIdentityId: identity.id + }, { lastLoginAuthMethod: IdentityAuthMethod.GCP_AUTH, lastLoginTime: new Date() @@ -246,16 +258,34 @@ export const identityGcpAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Create, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionIdentityActions.Create, + OrgPermissionSubjects.Identity + ); + } const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { if ( @@ -336,16 +366,31 @@ export const identityGcpAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Edit, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + } const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { if ( @@ -404,16 +449,31 @@ export const identityGcpAuthServiceFactory = ({ const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + } return { ...identityGcpAuth, orgId: identityMembershipOrg.scopeOrgId }; }; @@ -441,43 +501,58 @@ export const identityGcpAuthServiceFactory = ({ message: "The identity does not have gcp auth" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - - const { permission: rolePermission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor: ActorType.IDENTITY, - actorId: identityMembershipOrg.identity.id, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); - const permissionBoundary = validatePrivilegeChangeOperation( - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.RevokeAuth, - OrgPermissionSubjects.Identity, - permission, - rolePermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to revoke gcp auth of identity with more privileged role", - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.RevokeAuth, - OrgPermissionSubjects.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.RevokeAuth, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + const { permission: rolePermission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to revoke gcp auth of identity with more privileged role", + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + const revokedIdentityGcpAuth = await identityGcpAuthDAL.transaction(async (tx) => { const deletedGcpAuth = await identityGcpAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.GCP_AUTH }, tx); 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 bfa5654b8..82935e4a4 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 @@ -1,10 +1,16 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import { requestContext } from "@fastify/request-context"; import https from "https"; import jwt from "jsonwebtoken"; import { JwksClient } from "jwks-rsa"; -import { AccessScope, IdentityAuthMethod, OrganizationActionScope, TIdentityJwtAuthsUpdate } from "@app/db/schemas"; +import { + AccessScope, + ActionProjectType, + IdentityAuthMethod, + OrganizationActionScope, + 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 { @@ -12,6 +18,7 @@ import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; import { @@ -50,7 +57,7 @@ type TIdentityJwtAuthServiceFactoryDep = { identityJwtAuthDAL: TIdentityJwtAuthDALFactory; membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick; - permissionService: Pick; + permissionService: Pick; licenseService: Pick; kmsService: Pick; orgDAL: Pick; @@ -213,8 +220,22 @@ export const identityJwtAuthServiceFactory = ({ const identityAccessToken = await identityJwtAuthDAL.transaction(async (tx) => { await membershipIdentityDAL.update( - { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, - { lastLoginAuthMethod: IdentityAuthMethod.JWT_AUTH, lastLoginTime: new Date() }, + identity.projectId + ? { + scope: AccessScope.Project, + scopeOrgId: identity.orgId, + scopeProjectId: identity.projectId, + actorIdentityId: identity.id + } + : { + scope: AccessScope.Organization, + scopeOrgId: identity.orgId, + actorIdentityId: identity.id + }, + { + lastLoginAuthMethod: IdentityAuthMethod.JWT_AUTH, + lastLoginTime: new Date() + }, tx ); const newToken = await identityAccessTokenDAL.create( @@ -322,16 +343,35 @@ export const identityJwtAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Create, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionIdentityActions.Create, + OrgPermissionSubjects.Identity + ); + } const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { @@ -435,17 +475,32 @@ export const identityJwtAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Edit, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + } const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { if ( @@ -534,17 +589,32 @@ export const identityJwtAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + } const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId }); const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ @@ -586,45 +656,60 @@ export const identityJwtAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - - const { permission: rolePermission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor: ActorType.IDENTITY, - actorId: identityMembershipOrg.identity.id, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - - const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); - const permissionBoundary = validatePrivilegeChangeOperation( - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.RevokeAuth, - OrgPermissionSubjects.Identity, - permission, - rolePermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to revoke jwt auth of identity with more privileged role", - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.RevokeAuth, - OrgPermissionSubjects.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.RevokeAuth, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to revoke jwt auth of identity with more privileged role", + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } const revokedIdentityJwtAuth = await identityJwtAuthDAL.transaction(async (tx) => { const deletedJwtAuth = await identityJwtAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.JWT_AUTH }, tx); 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 e278853b3..9322e48cb 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 @@ -1,4 +1,4 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import { requestContext } from "@fastify/request-context"; import axios, { AxiosError } from "axios"; import https from "https"; @@ -6,6 +6,7 @@ import RE2 from "re2"; import { AccessScope, + ActionProjectType, IdentityAuthMethod, OrganizationActionScope, TIdentityKubernetesAuthsUpdate @@ -25,6 +26,7 @@ import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; import { @@ -69,7 +71,7 @@ type TIdentityKubernetesAuthServiceFactoryDep = { >; identityAccessTokenDAL: Pick; membershipIdentityDAL: Pick; - permissionService: Pick; + permissionService: Pick; licenseService: Pick; kmsService: Pick; gatewayService: TGatewayServiceFactory; @@ -448,8 +450,22 @@ export const identityKubernetesAuthServiceFactory = ({ const identityAccessToken = await identityKubernetesAuthDAL.transaction(async (tx) => { await membershipIdentityDAL.update( - { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, - { lastLoginAuthMethod: IdentityAuthMethod.KUBERNETES_AUTH, lastLoginTime: new Date() }, + identity.projectId + ? { + scope: AccessScope.Project, + scopeOrgId: identity.orgId, + scopeProjectId: identity.projectId, + actorIdentityId: identity.id + } + : { + scope: AccessScope.Organization, + scopeOrgId: identity.orgId, + actorIdentityId: identity.id + }, + { + lastLoginAuthMethod: IdentityAuthMethod.KUBERNETES_AUTH, + lastLoginTime: new Date() + }, tx ); const newToken = await identityAccessTokenDAL.create( @@ -563,16 +579,34 @@ export const identityKubernetesAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Create, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionIdentityActions.Create, + OrgPermissionSubjects.Identity + ); + } const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { if ( @@ -699,16 +733,31 @@ export const identityKubernetesAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Edit, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + } const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { if ( @@ -846,16 +895,31 @@ export const identityKubernetesAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + } const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, orgId: identityMembershipOrg.scopeOrgId @@ -906,43 +970,58 @@ export const identityKubernetesAuthServiceFactory = ({ message: "The identity does not have kubernetes auth" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - - const { permission: rolePermission } = await permissionService.getOrgPermission({ - actor: ActorType.IDENTITY, - actorId: identityMembershipOrg.identity.id, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId, - scope: OrganizationActionScope.Any - }); - const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); - const permissionBoundary = validatePrivilegeChangeOperation( - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.RevokeAuth, - OrgPermissionSubjects.Identity, - permission, - rolePermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to revoke kubernetes auth of identity with more privileged role", - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.RevokeAuth, - OrgPermissionSubjects.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.RevokeAuth, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.Any + }); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to revoke kubernetes auth of identity with more privileged role", + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } const revokedIdentityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => { const deletedKubernetesAuth = await identityKubernetesAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.KUBERNETES_AUTH }, tx); diff --git a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts index 921aa3273..455b52412 100644 --- a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts +++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import { requestContext } from "@fastify/request-context"; import slugify from "@sindresorhus/slugify"; -import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; +import { AccessScope, ActionProjectType, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { TIdentityAuthTemplateDALFactory } from "@app/ee/services/identity-auth-template"; import { testLDAPConfig } from "@app/ee/services/ldap-config/ldap-fns"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; @@ -17,6 +17,7 @@ import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; @@ -61,7 +62,7 @@ type TIdentityLdapAuthServiceFactoryDep = { >; membershipIdentityDAL: Pick; licenseService: Pick; - permissionService: Pick; + permissionService: Pick; kmsService: TKmsServiceFactory; identityDAL: Pick; identityAuthTemplateDAL: TIdentityAuthTemplateDALFactory; @@ -177,8 +178,22 @@ export const identityLdapAuthServiceFactory = ({ try { const identityAccessToken = await identityLdapAuthDAL.transaction(async (tx) => { await membershipIdentityDAL.update( - { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, - { lastLoginAuthMethod: IdentityAuthMethod.LDAP_AUTH, lastLoginTime: new Date() }, + identity.projectId + ? { + scope: AccessScope.Project, + scopeOrgId: identity.orgId, + scopeProjectId: identity.projectId, + actorIdentityId: identity.id + } + : { + scope: AccessScope.Organization, + scopeOrgId: identity.orgId, + actorIdentityId: identity.id + }, + { + lastLoginAuthMethod: IdentityAuthMethod.LDAP_AUTH, + lastLoginTime: new Date() + }, tx ); const newToken = await identityAccessTokenDAL.create( @@ -290,7 +305,7 @@ export const identityLdapAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission({ + const { permission: orgPermission } = await permissionService.getOrgPermission({ scope: OrganizationActionScope.Any, actor, actorId, @@ -298,10 +313,30 @@ export const identityLdapAuthServiceFactory = ({ actorAuthMethod, actorOrgId }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + + if (identityMembershipOrg.identity.projectId) { + const { permission: projectPermission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + + ForbiddenError.from(projectPermission).throwUnlessCan( + ProjectPermissionIdentityActions.Create, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + ForbiddenError.from(orgPermission).throwUnlessCan( + OrgPermissionIdentityActions.Create, + OrgPermissionSubjects.Identity + ); + } if (templateId) { - ForbiddenError.from(permission).throwUnlessCan( + ForbiddenError.from(orgPermission).throwUnlessCan( OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate ); @@ -470,7 +505,7 @@ export const identityLdapAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission({ + const { permission: orgPermission } = await permissionService.getOrgPermission({ scope: OrganizationActionScope.Any, actor, actorId, @@ -478,10 +513,30 @@ export const identityLdapAuthServiceFactory = ({ actorAuthMethod, actorOrgId }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + + if (identityMembershipOrg.identity.projectId) { + const { permission: projectPermission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + + ForbiddenError.from(projectPermission).throwUnlessCan( + ProjectPermissionIdentityActions.Create, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + ForbiddenError.from(orgPermission).throwUnlessCan( + OrgPermissionIdentityActions.Edit, + OrgPermissionSubjects.Identity + ); + } if (templateId) { - ForbiddenError.from(permission).throwUnlessCan( + ForbiddenError.from(orgPermission).throwUnlessCan( OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate ); @@ -630,14 +685,31 @@ export const identityLdapAuthServiceFactory = ({ const ldapIdentityAuth = await identityLdapAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + } const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, @@ -650,7 +722,6 @@ export const identityLdapAuthServiceFactory = ({ ? decryptor({ cipherTextBlob: ldapIdentityAuth.encryptedLdapCaCertificate }).toString() : undefined; - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...ldapIdentityAuth, orgId: identityMembershipOrg.scopeOrgId, bindDN, bindPass, ldapCaCertificate }; }; @@ -677,45 +748,62 @@ export const identityLdapAuthServiceFactory = ({ message: "The identity does not have LDAP Auth attached" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission({ - actor: ActorType.IDENTITY, - actorId: identityMembershipOrg.identity.id, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId, - scope: OrganizationActionScope.Any - }); - - const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); - const permissionBoundary = validatePrivilegeChangeOperation( - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.RevokeAuth, - OrgPermissionSubjects.Identity, - permission, - rolePermission - ); - - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to revoke LDAP auth of identity with more privileged role", - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.RevokeAuth, - OrgPermissionSubjects.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.RevokeAuth, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.Any + }); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to revoke LDAP auth of identity with more privileged role", + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + const revokedIdentityLdapAuth = await identityLdapAuthDAL.transaction(async (tx) => { const [deletedLdapAuth] = await identityLdapAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.LDAP_AUTH }, tx); @@ -824,15 +912,31 @@ export const identityLdapAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Edit, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + } const deleted = await keyStore.deleteItems({ pattern: `lockout:identity:${identityId}:${IdentityAuthMethod.LDAP_AUTH}:*` diff --git a/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts b/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts index 5d7042b9f..05e56c77d 100644 --- a/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts +++ b/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts @@ -1,10 +1,10 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import { requestContext } from "@fastify/request-context"; import { AxiosError } from "axios"; import RE2 from "re2"; -import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; +import { AccessScope, ActionProjectType, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -12,6 +12,7 @@ import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; import { crypto } from "@app/lib/crypto"; @@ -49,7 +50,7 @@ type TIdentityOciAuthServiceFactoryDep = { identityOciAuthDAL: Pick; membershipIdentityDAL: Pick; licenseService: Pick; - permissionService: Pick; + permissionService: Pick; orgDAL: Pick; }; @@ -110,8 +111,22 @@ export const identityOciAuthServiceFactory = ({ // Generate the token const identityAccessToken = await identityOciAuthDAL.transaction(async (tx) => { await membershipIdentityDAL.update( - { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, - { lastLoginAuthMethod: IdentityAuthMethod.OCI_AUTH, lastLoginTime: new Date() }, + identity.projectId + ? { + scope: AccessScope.Project, + scopeOrgId: identity.orgId, + scopeProjectId: identity.projectId, + actorIdentityId: identity.id + } + : { + scope: AccessScope.Organization, + scopeOrgId: identity.orgId, + actorIdentityId: identity.id + }, + { + lastLoginAuthMethod: IdentityAuthMethod.OCI_AUTH, + lastLoginTime: new Date() + }, tx ); const newToken = await identityAccessTokenDAL.create( @@ -217,15 +232,34 @@ export const identityOciAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Create, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionIdentityActions.Create, + OrgPermissionSubjects.Identity + ); + } const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { @@ -304,15 +338,31 @@ export const identityOciAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Edit, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + } const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { @@ -367,15 +417,31 @@ export const identityOciAuthServiceFactory = ({ const ociIdentityAuth = await identityOciAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + } return { ...ociIdentityAuth, orgId: identityMembershipOrg.scopeOrgId }; }; @@ -402,45 +468,62 @@ export const identityOciAuthServiceFactory = ({ message: "The identity does not have OCI auth" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission({ - actor: ActorType.IDENTITY, - actorId: identityMembershipOrg.identity.id, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId, - scope: OrganizationActionScope.Any - }); - - const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId); - const permissionBoundary = validatePrivilegeChangeOperation( - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.RevokeAuth, - OrgPermissionSubjects.Identity, - permission, - rolePermission - ); - - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to revoke OCI auth of identity with more privileged role", - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.RevokeAuth, - OrgPermissionSubjects.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.RevokeAuth, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.Any + }); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId); + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to revoke OCI auth of identity with more privileged role", + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + const revokedIdentityOciAuth = await identityOciAuthDAL.transaction(async (tx) => { const deletedOciAuth = await identityOciAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.OCI_AUTH }, tx); 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 36ca09b3f..a253c1e95 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 @@ -1,11 +1,17 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import { requestContext } from "@fastify/request-context"; import axios from "axios"; import https from "https"; import jwt from "jsonwebtoken"; import { JwksClient } from "jwks-rsa"; -import { AccessScope, IdentityAuthMethod, OrganizationActionScope, TIdentityOidcAuthsUpdate } from "@app/db/schemas"; +import { + AccessScope, + ActionProjectType, + IdentityAuthMethod, + OrganizationActionScope, + 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 { @@ -13,6 +19,7 @@ import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; import { @@ -51,7 +58,7 @@ type TIdentityOidcAuthServiceFactoryDep = { identityOidcAuthDAL: TIdentityOidcAuthDALFactory; membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick; - permissionService: Pick; + permissionService: Pick; licenseService: Pick; kmsService: Pick; orgDAL: Pick; @@ -266,8 +273,22 @@ export const identityOidcAuthServiceFactory = ({ const identityAccessToken = await identityOidcAuthDAL.transaction(async (tx) => { await membershipIdentityDAL.update( - { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, - { lastLoginAuthMethod: IdentityAuthMethod.OIDC_AUTH, lastLoginTime: new Date() }, + identity.projectId + ? { + scope: AccessScope.Project, + scopeOrgId: identity.orgId, + scopeProjectId: identity.projectId, + actorIdentityId: identity.id + } + : { + scope: AccessScope.Organization, + scopeOrgId: identity.orgId, + actorIdentityId: identity.id + }, + { + lastLoginAuthMethod: IdentityAuthMethod.OIDC_AUTH, + lastLoginTime: new Date() + }, tx ); const newToken = await identityAccessTokenDAL.create( @@ -379,16 +400,35 @@ export const identityOidcAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Create, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionIdentityActions.Create, + OrgPermissionSubjects.Identity + ); + } const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { @@ -481,16 +521,32 @@ export const identityOidcAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Edit, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + } const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { @@ -565,15 +621,31 @@ export const identityOidcAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + } const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId }); @@ -610,46 +682,62 @@ export const identityOidcAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - - const { permission: rolePermission } = await permissionService.getOrgPermission({ - actor: ActorType.IDENTITY, - actorId: identityMembershipOrg.identity.id, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId, - scope: OrganizationActionScope.Any - }); - - const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); - const permissionBoundary = validatePrivilegeChangeOperation( - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.RevokeAuth, - OrgPermissionSubjects.Identity, - permission, - rolePermission - ); - - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to revoke oidc auth of identity with more privileged role", - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.RevokeAuth, - OrgPermissionSubjects.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.RevokeAuth, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.Any + }); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to revoke oidc auth of identity with more privileged role", + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + const revokedIdentityOidcAuth = await identityOidcAuthDAL.transaction(async (tx) => { const deletedOidcAuth = await identityOidcAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.OIDC_AUTH }, tx); diff --git a/backend/src/services/identity-project/identity-project-dal.ts b/backend/src/services/identity-project/identity-project-dal.ts index adcdd8be8..c264717d0 100644 --- a/backend/src/services/identity-project/identity-project-dal.ts +++ b/backend/src/services/identity-project/identity-project-dal.ts @@ -293,7 +293,11 @@ export const identityProjectDALFactory = (db: TDbClient) => { db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity), db.ref("id").as("identityId").withSchema(TableName.Identity), db.ref("name").as("identityName").withSchema(TableName.Identity), + db.ref("orgId").as("identityOrgId").withSchema(TableName.Identity), + db.ref("projectId").as("identityProjectId").withSchema(TableName.Identity), db.ref("id").withSchema(TableName.Membership), + db.ref("lastLoginAuthMethod").withSchema(TableName.Membership), + db.ref("lastLoginTime").withSchema(TableName.Membership), db.ref("role").withSchema(TableName.MembershipRole), db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), db.ref("customRoleId").withSchema(TableName.MembershipRole), @@ -334,6 +338,8 @@ export const identityProjectDALFactory = (db: TDbClient) => { parentMapper: ({ identityId, identityName, + identityOrgId, + identityProjectId, uaId, alicloudId, awsId, @@ -346,7 +352,9 @@ export const identityProjectDALFactory = (db: TDbClient) => { id, createdAt, updatedAt, - projectName + projectName, + lastLoginAuthMethod, + lastLoginTime }) => ({ id, identityId, @@ -355,6 +363,8 @@ export const identityProjectDALFactory = (db: TDbClient) => { identity: { id: identityId, name: identityName, + projectId: identityProjectId, + orgId: identityOrgId, authMethods: buildAuthMethods({ uaId, alicloudId, @@ -367,6 +377,11 @@ export const identityProjectDALFactory = (db: TDbClient) => { tokenId }) }, + // TODO: scott - not sure why these aren't properly typed? + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + lastLoginAuthMethod, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + lastLoginTime, project: { id: projectId, name: projectName diff --git a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts index aa4940d64..630638a06 100644 --- a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts +++ b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts @@ -1,7 +1,7 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import { requestContext } from "@fastify/request-context"; -import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; +import { AccessScope, ActionProjectType, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -9,6 +9,7 @@ import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; @@ -43,7 +44,7 @@ type TIdentityTlsCertAuthServiceFactoryDep = { >; membershipIdentityDAL: Pick; licenseService: Pick; - permissionService: Pick; + permissionService: Pick; kmsService: Pick; orgDAL: Pick; }; @@ -130,8 +131,22 @@ export const identityTlsCertAuthServiceFactory = ({ // Generate the token const identityAccessToken = await identityTlsCertAuthDAL.transaction(async (tx) => { await membershipIdentityDAL.update( - { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, - { lastLoginAuthMethod: IdentityAuthMethod.TLS_CERT_AUTH, lastLoginTime: new Date() }, + identity.projectId + ? { + scope: AccessScope.Project, + scopeOrgId: identity.orgId, + scopeProjectId: identity.projectId, + actorIdentityId: identity.id + } + : { + scope: AccessScope.Organization, + scopeOrgId: identity.orgId, + actorIdentityId: identity.id + }, + { + lastLoginAuthMethod: IdentityAuthMethod.TLS_CERT_AUTH, + lastLoginTime: new Date() + }, tx ); const newToken = await identityAccessTokenDAL.create( @@ -237,15 +252,34 @@ export const identityTlsCertAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Create, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionIdentityActions.Create, + OrgPermissionSubjects.Identity + ); + } const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { @@ -329,15 +363,31 @@ export const identityTlsCertAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Edit, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + } const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { @@ -404,15 +454,32 @@ export const identityTlsCertAuthServiceFactory = ({ const identityAuth = await identityTlsCertAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + } + const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, orgId: identityMembershipOrg.scopeOrgId @@ -448,44 +515,61 @@ export const identityTlsCertAuthServiceFactory = ({ message: "The identity does not have TLS Certificate auth" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission, memberships } = await permissionService.getOrgPermission({ - actor: ActorType.IDENTITY, - actorId: identityMembershipOrg.identity.id, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId, - scope: OrganizationActionScope.Any - }); - const shouldUseNewPrivilegeSystem = Boolean(memberships?.[0]?.shouldUseNewPrivilegeSystem); - const permissionBoundary = validatePrivilegeChangeOperation( - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.RevokeAuth, - OrgPermissionSubjects.Identity, - permission, - rolePermission - ); - - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to revoke TLS Certificate auth of identity with more privileged role", - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.RevokeAuth, - OrgPermissionSubjects.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.RevokeAuth, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission, memberships } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.Any + }); + const shouldUseNewPrivilegeSystem = Boolean(memberships?.[0]?.shouldUseNewPrivilegeSystem); + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to revoke TLS Certificate auth of identity with more privileged role", + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + const revokedIdentityTlsCertAuth = await identityTlsCertAuthDAL.transaction(async (tx) => { const deletedTlsCertAuth = await identityTlsCertAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.TLS_CERT_AUTH }, tx); 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 338a7838a..20c692134 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 @@ -1,6 +1,12 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; -import { AccessScope, IdentityAuthMethod, OrganizationActionScope, TableName } from "@app/db/schemas"; +import { + AccessScope, + ActionProjectType, + IdentityAuthMethod, + OrganizationActionScope, + 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 { @@ -8,6 +14,7 @@ import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; import { @@ -49,7 +56,7 @@ type TIdentityTokenAuthServiceFactoryDep = { TIdentityAccessTokenDALFactory, "create" | "find" | "update" | "findById" | "findOne" | "updateById" | "delete" >; - permissionService: Pick; + permissionService: Pick; licenseService: Pick; orgDAL: Pick; }; @@ -101,15 +108,34 @@ export const identityTokenAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Create, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionIdentityActions.Create, + OrgPermissionSubjects.Identity + ); + } const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { @@ -187,15 +213,31 @@ export const identityTokenAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Edit, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + } const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { @@ -251,15 +293,31 @@ export const identityTokenAuthServiceFactory = ({ const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + } return { ...identityTokenAuth, orgId: identityMembershipOrg.scopeOrgId }; }; @@ -291,44 +349,61 @@ export const identityTokenAuthServiceFactory = ({ message: "The identity does not have Token Auth" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission({ - actor: ActorType.IDENTITY, - actorId: identityMembershipOrg.identity.id, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId, - scope: OrganizationActionScope.Any - }); - - const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); - const permissionBoundary = validatePrivilegeChangeOperation( - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.RevokeAuth, - OrgPermissionSubjects.Identity, - permission, - rolePermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to revoke token auth of identity with more privileged role", - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.RevokeAuth, - OrgPermissionSubjects.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.RevokeAuth, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.Any + }); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to revoke token auth of identity with more privileged role", + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + const revokedIdentityTokenAuth = await identityTokenAuthDAL.transaction(async (tx) => { const deletedTokenAuth = await identityTokenAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ @@ -367,45 +442,61 @@ export const identityTokenAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - - const { permission: rolePermission } = await permissionService.getOrgPermission({ - actor: ActorType.IDENTITY, - actorId: identityMembershipOrg.identity.id, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId, - scope: OrganizationActionScope.Any - }); - - const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); - const permissionBoundary = validatePrivilegeChangeOperation( - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.CreateToken, - OrgPermissionSubjects.Identity, - permission, - rolePermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to create token for identity with more privileged role", - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.CreateToken, - OrgPermissionSubjects.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.CreateToken, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.Any + }); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.CreateToken, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to create token for identity with more privileged role", + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.CreateToken, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId }); const identity = await identityDAL.findById(identityTokenAuth.identityId); @@ -413,7 +504,18 @@ export const identityTokenAuthServiceFactory = ({ const identityAccessToken = await identityTokenAuthDAL.transaction(async (tx) => { await membershipIdentityDAL.update( - { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + identity.projectId + ? { + scope: AccessScope.Project, + scopeOrgId: identity.orgId, + scopeProjectId: identity.projectId, + actorIdentityId: identity.id + } + : { + scope: AccessScope.Organization, + scopeOrgId: identity.orgId, + actorIdentityId: identity.id + }, { lastLoginAuthMethod: IdentityAuthMethod.TOKEN_AUTH, lastLoginTime: new Date() }, tx ); @@ -478,15 +580,32 @@ export const identityTokenAuthServiceFactory = ({ message: "The identity does not have Token Auth" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + } const tokens = await identityAccessTokenDAL.find( { @@ -531,43 +650,60 @@ export const identityTokenAuthServiceFactory = ({ message: "The identity does not have Token Auth" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission({ - actor: ActorType.IDENTITY, - actorId: identityMembershipOrg.identity.id, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId, - scope: OrganizationActionScope.Any - }); - const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); - const permissionBoundary = validatePrivilegeChangeOperation( - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.CreateToken, - OrgPermissionSubjects.Identity, - permission, - rolePermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to update token for identity with more privileged role", - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.CreateToken, - OrgPermissionSubjects.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.CreateToken, + subject(ProjectPermissionSub.Identity, { identityId: identityMembershipOrg.identity.id }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.Any + }); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.CreateToken, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to update token for identity with more privileged role", + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.CreateToken, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + const [token] = await identityAccessTokenDAL.update( { authMethod: IdentityAuthMethod.TOKEN_AUTH, @@ -603,24 +739,43 @@ export const identityTokenAuthServiceFactory = ({ await validateIdentityUpdateForSuperAdminPrivileges(identityAccessToken.identityId, isActorSuperAdmin); - const identityOrgMembership = await membershipIdentityDAL.findOne({ - actorIdentityId: identityAccessToken.identityId, - scope: AccessScope.Organization + const identityOrgMembership = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId: identityAccessToken.identityId }); if (!identityOrgMembership) { throw new NotFoundError({ message: `Failed to find identity with ID ${identityAccessToken.identityId}` }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityOrgMembership.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + if (identityOrgMembership.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityOrgMembership.identity.projectId, + actorAuthMethod, + actorOrgId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Edit, + subject(ProjectPermissionSub.Identity, { identityId: identityOrgMembership.identity.id }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityOrgMembership.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + 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 c8409585e..5ea5c4a6e 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -1,7 +1,7 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import { requestContext } from "@fastify/request-context"; -import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; +import { AccessScope, ActionProjectType, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -9,6 +9,7 @@ import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; @@ -51,7 +52,7 @@ type TIdentityUaServiceFactoryDep = { identityUaClientSecretDAL: TIdentityUaClientSecretDALFactory; identityAccessTokenDAL: TIdentityAccessTokenDALFactory; membershipIdentityDAL: TMembershipIdentityDALFactory; - permissionService: Pick; + permissionService: Pick; licenseService: Pick; orgDAL: Pick; keyStore: Pick< @@ -231,7 +232,18 @@ export const identityUaServiceFactory = ({ const identityAccessToken = await identityUaDAL.transaction(async (tx) => { const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx); await membershipIdentityDAL.update( - { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + identity.projectId + ? { + scope: AccessScope.Project, + scopeOrgId: identity.orgId, + scopeProjectId: identity.projectId, + actorIdentityId: identity.id + } + : { + scope: AccessScope.Organization, + scopeOrgId: identity.orgId, + actorIdentityId: identity.id + }, { lastLoginAuthMethod: IdentityAuthMethod.UNIVERSAL_AUTH, lastLoginTime: new Date() @@ -351,16 +363,35 @@ export const identityUaServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Create, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionIdentityActions.Create, + OrgPermissionSubjects.Identity + ); + } const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedClientSecretTrustedIps = clientSecretTrustedIps.map((clientSecretTrustedIp) => { @@ -467,15 +498,31 @@ export const identityUaServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Edit, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + } const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedClientSecretTrustedIps = clientSecretTrustedIps?.map((clientSecretTrustedIp) => { @@ -554,15 +601,31 @@ export const identityUaServiceFactory = ({ throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + } return { ...uaIdentityAuth, orgId: identityMembershipOrg.scopeOrgId }; }; @@ -590,43 +653,59 @@ export const identityUaServiceFactory = ({ if (identityMembershipOrg.identity.orgId !== actorOrgId) { throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission({ - actor: ActorType.IDENTITY, - actorId: identityMembershipOrg.identity.id, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId, - scope: OrganizationActionScope.Any - }); - const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); - const permissionBoundary = validatePrivilegeChangeOperation( - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.RevokeAuth, - OrgPermissionSubjects.Identity, - permission, - rolePermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to revoke universal auth of identity with more privileged role", - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.RevokeAuth, - OrgPermissionSubjects.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.RevokeAuth, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.Any + }); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to revoke universal auth of identity with more privileged role", + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } const revokedIdentityUniversalAuth = await identityUaDAL.transaction(async (tx) => { const deletedUniversalAuth = await identityUaDAL.delete({ identityId }, tx); return { ...deletedUniversalAuth?.[0], orgId: identityMembershipOrg.scopeOrgId }; @@ -662,43 +741,61 @@ export const identityUaServiceFactory = ({ throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - - const { permission: rolePermission } = await permissionService.getOrgPermission({ - actor: ActorType.IDENTITY, - actorId: identityMembershipOrg.identity.id, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId, - scope: OrganizationActionScope.Any - }); - const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); - const permissionBoundary = validatePrivilegeChangeOperation( - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.CreateToken, - OrgPermissionSubjects.Identity, - permission, - rolePermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to create client secret for identity.", - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.CreateToken, - OrgPermissionSubjects.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.CreateToken, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionIdentityActions.Create, + OrgPermissionSubjects.Identity + ); + + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.Any + }); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.CreateToken, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to create client secret for identity.", + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.CreateToken, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } const appCfg = getConfig(); const clientSecret = crypto.randomBytes(32).toString("hex"); const clientSecretHash = await crypto.hashing().createHash(clientSecret, appCfg.SALT_ROUNDS); @@ -748,43 +845,59 @@ export const identityUaServiceFactory = ({ throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - - const { permission: rolePermission } = await permissionService.getOrgPermission({ - actor: ActorType.IDENTITY, - actorId: identityMembershipOrg.identity.id, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId, - scope: OrganizationActionScope.Any - }); - const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); - const permissionBoundary = validatePrivilegeChangeOperation( - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.GetToken, - OrgPermissionSubjects.Identity, - permission, - rolePermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to get identity client secret with more privileged role", - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.GetToken, - OrgPermissionSubjects.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.GetToken, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.Any + }); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.GetToken, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to get identity client secret with more privileged role", + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.GetToken, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + const identityUniversalAuth = await identityUaDAL.findOne({ identityId }); @@ -828,43 +941,57 @@ export const identityUaServiceFactory = ({ const clientSecret = await identityUaClientSecretDAL.findOne({ id: clientSecretId, identityUAId: identityUa.id }); if (!clientSecret) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - - const { permission: rolePermission } = await permissionService.getOrgPermission({ - actor: ActorType.IDENTITY, - actorId: identityMembershipOrg.identity.id, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId, - scope: OrganizationActionScope.Any - }); - const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); - const permissionBoundary = validatePrivilegeChangeOperation( - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.GetToken, - OrgPermissionSubjects.Identity, - permission, - rolePermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to read identity client secret of identity with more privileged role", - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.GetToken, - OrgPermissionSubjects.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.GetToken, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.Any + }); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.GetToken, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to read identity client secret of identity with more privileged role", + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.GetToken, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } return { ...clientSecret, identityId, orgId: identityMembershipOrg.scopeOrgId }; }; @@ -900,45 +1027,63 @@ export const identityUaServiceFactory = ({ const clientSecret = await identityUaClientSecretDAL.findOne({ id: clientSecretId, identityUAId: identityUa.id }); if (!clientSecret) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity); - - const { permission: rolePermission } = await permissionService.getOrgPermission({ - actor: ActorType.IDENTITY, - actorId: identityMembershipOrg.identity.id, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId, - scope: OrganizationActionScope.Any - }); - - const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); - const permissionBoundary = validatePrivilegeChangeOperation( - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.DeleteToken, - OrgPermissionSubjects.Identity, - permission, - rolePermission - ); - if (!permissionBoundary.isValid) { - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to revoke identity client secret with more privileged role", - shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.DeleteToken, - OrgPermissionSubjects.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId }); - } + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.DeleteToken, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionIdentityActions.Delete, + OrgPermissionSubjects.Identity + ); + + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.Any + }); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.DeleteToken, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) { + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to revoke identity client secret with more privileged role", + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.DeleteToken, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + } const updatedClientSecret = await identityUaClientSecretDAL.updateById(clientSecretId, { isClientSecretRevoked: true }); @@ -971,16 +1116,31 @@ export const identityUaServiceFactory = ({ throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Edit, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + } const deleted = await keyStore.deleteItems({ pattern: `lockout:identity:${identityId}:${IdentityAuthMethod.UNIVERSAL_AUTH}:*` }); diff --git a/backend/src/services/identity-v2/identity-fns.ts b/backend/src/services/identity-v2/identity-fns.ts new file mode 100644 index 000000000..fde9ef2d3 --- /dev/null +++ b/backend/src/services/identity-v2/identity-fns.ts @@ -0,0 +1,24 @@ +import { TKeyStoreFactory } from "@app/keystore/keystore"; + +export const getIdentityActiveLockoutAuthMethods = async ( + identityId: string, + keyStore: Pick +) => { + const activeLockouts = await keyStore.getKeysByPattern(`lockout:identity:${identityId}:*`); + + const activeLockoutAuthMethods = new Set(); + for await (const key of activeLockouts) { + const parts = key.split(":"); + if (parts.length > 3) { + const lockoutRaw = await keyStore.getItem(key); + if (lockoutRaw) { + const lockout = JSON.parse(lockoutRaw) as { lockedOut: boolean }; + if (lockout.lockedOut) { + activeLockoutAuthMethods.add(parts[3]); + } + } + } + } + + return Array.from(activeLockoutAuthMethods); +}; diff --git a/backend/src/services/identity-v2/identity-service.ts b/backend/src/services/identity-v2/identity-service.ts index 0de1b8a5c..d3c72bc29 100644 --- a/backend/src/services/identity-v2/identity-service.ts +++ b/backend/src/services/identity-v2/identity-service.ts @@ -1,7 +1,9 @@ import { AccessScope, OrgMembershipRole } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { TKeyStoreFactory } from "@app/keystore/keystore"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { getIdentityActiveLockoutAuthMethods } from "@app/services/identity-v2/identity-fns"; import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal"; import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; @@ -24,6 +26,7 @@ type TScopedIdentityV2ServiceFactoryDep = { membershipIdentityDAL: TMembershipIdentityDALFactory; membershipRoleDAL: TMembershipRoleDALFactory; identityMetadataDAL: TIdentityMetadataDALFactory; + keyStore: Pick; }; export type TScopedIdentityV2ServiceFactory = ReturnType; @@ -34,7 +37,8 @@ export const identityV2ServiceFactory = ({ licenseService, membershipIdentityDAL, membershipRoleDAL, - identityMetadataDAL + identityMetadataDAL, + keyStore }: TScopedIdentityV2ServiceFactoryDep) => { const orgFactory = newOrgIdentityFactory({ permissionService @@ -217,7 +221,9 @@ export const identityV2ServiceFactory = ({ const identity = await identityDAL.getIdentityById(dto.scopeData, dto.selector.identityId); if (!identity) throw new NotFoundError({ message: `Identity with id ${dto.selector.identityId} not found` }); - return { identity }; + const activeLockoutAuthMethods = await getIdentityActiveLockoutAuthMethods(identity.id, keyStore); + + return { identity: { ...identity, activeLockoutAuthMethods } }; }; const listIdentities = async (dto: TListIdentityV2DTO) => { diff --git a/backend/src/services/identity-v2/project/project-identity-factory.ts b/backend/src/services/identity-v2/project/project-identity-factory.ts index ab3ee3b94..a87f80d2b 100644 --- a/backend/src/services/identity-v2/project/project-identity-factory.ts +++ b/backend/src/services/identity-v2/project/project-identity-factory.ts @@ -1,4 +1,4 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import { AccessScope, ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; @@ -47,7 +47,7 @@ export const newProjectIdentityFactory = ({ permissionService }: TProjectIdentit }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionIdentityActions.Edit, - ProjectPermissionSub.Identity + subject(ProjectPermissionSub.Identity, { identityId: dto.selector.identityId }) ); }; @@ -63,7 +63,7 @@ export const newProjectIdentityFactory = ({ permissionService }: TProjectIdentit }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionIdentityActions.Delete, - ProjectPermissionSub.Identity + subject(ProjectPermissionSub.Identity, { identityId: dto.selector.identityId }) ); }; @@ -95,7 +95,7 @@ export const newProjectIdentityFactory = ({ permissionService }: TProjectIdentit }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionIdentityActions.Read, - ProjectPermissionSub.Identity + subject(ProjectPermissionSub.Identity, { identityId: dto.selector.identityId }) ); }; diff --git a/backend/src/services/identity/identity-org-dal.ts b/backend/src/services/identity/identity-org-dal.ts index 66556f5fa..2da536c36 100644 --- a/backend/src/services/identity/identity-org-dal.ts +++ b/backend/src/services/identity/identity-org-dal.ts @@ -159,6 +159,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { .join(TableName.Membership, `${TableName.Membership}.actorIdentityId`, `${TableName.Identity}.id`) .where(`${TableName.Membership}.scope`, AccessScope.Organization) .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .whereNull(`${TableName.Identity}.projectId`) .orderBy(`${TableName.Identity}.${orderBy}`, orderDirection) .select( selectAllTableCols(TableName.Membership), @@ -404,6 +405,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { .whereNotNull(`${TableName.Membership}.actorIdentityId`) .where(`${TableName.Membership}.scopeOrgId`, orgId) .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Membership}.actorIdentityId`) + .whereNull(`${TableName.Identity}.projectId`) .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .orderBy( diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index 72b81bac2..f59930ece 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -11,6 +11,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { TKeyStoreFactory } from "@app/keystore/keystore"; import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; +import { getIdentityActiveLockoutAuthMethods } from "@app/services/identity-v2/identity-fns"; import { TAdditionalPrivilegeDALFactory } from "../additional-privilege/additional-privilege-dal"; import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; @@ -220,6 +221,13 @@ export const identityServiceFactory = ({ } const identityDetails = await identityDAL.findById(id); + + console.log("has project id", identityDetails); + + if (identityDetails.projectId) { + throw new BadRequestError({ message: `Identity is managed by project` }); + } + const identity = await identityDAL.transaction(async (tx) => { const newIdentity = identityDetails.orgId === actorOrgId && (name || hasDeleteProtection) @@ -286,25 +294,11 @@ export const identityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - const activeLockouts = await keyStore.getKeysByPattern(`lockout:identity:${id}:*`); - - const activeLockoutAuthMethods = new Set(); - for await (const key of activeLockouts) { - const parts = key.split(":"); - if (parts.length > 3) { - const lockoutRaw = await keyStore.getItem(key); - if (lockoutRaw) { - const lockout = JSON.parse(lockoutRaw) as { lockedOut: boolean }; - if (lockout.lockedOut) { - activeLockoutAuthMethods.add(parts[3]); - } - } - } - } + const activeLockoutAuthMethods = await getIdentityActiveLockoutAuthMethods(id, keyStore); return { ...identity, - identity: { ...identity.identity, activeLockoutAuthMethods: Array.from(activeLockoutAuthMethods) } + identity: { ...identity.identity, activeLockoutAuthMethods } }; }; @@ -340,6 +334,10 @@ export const identityServiceFactory = ({ if (identityOrgMembership.identity.hasDeleteProtection) throw new BadRequestError({ message: "Identity has delete protection" }); + if (identityOrgMembership.identity.projectId) { + throw new BadRequestError({ message: `Identity is managed by project` }); + } + if (identityOrgMembership.identity.orgId === actorOrgId) { const deletedIdentity = await identityDAL.deleteById(id); await licenseService.updateSubscriptionOrgMemberCount(identityOrgMembership.scopeOrgId); diff --git a/backend/src/services/membership-identity/membership-identity-dal.ts b/backend/src/services/membership-identity/membership-identity-dal.ts index 4ff52ec0f..f4c83643b 100644 --- a/backend/src/services/membership-identity/membership-identity-dal.ts +++ b/backend/src/services/membership-identity/membership-identity-dal.ts @@ -371,8 +371,10 @@ export const membershipIdentityDALFactory = (db: TDbClient) => { }; const listAvailableIdentities = async (scopeData: AccessScopeData, rootOrgId: string) => { + // TODO (akhil/scott): need to implement filters + try { - const identitesConnectedToOrg = db + const identitiesConnectedToOrg = db .replicaNode()(TableName.Membership) .whereNotNull(`${TableName.Membership}.actorIdentityId`) .where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId) @@ -389,6 +391,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => { .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Membership}.actorIdentityId`) .where(`${TableName.Membership}.scope`, AccessScope.Organization) .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .whereNull(`${TableName.Identity}.projectId`) .where((qb) => { // if sub org pick from root and if project pick from org of project if (scopeData.scope === AccessScope.Organization) { @@ -397,7 +400,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => { void qb.where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId); } }) - .whereNotIn(`${TableName.Membership}.actorIdentityId`, identitesConnectedToOrg) + .whereNotIn(`${TableName.Membership}.actorIdentityId`, identitiesConnectedToOrg) .select( db.ref("id").withSchema(TableName.Identity), db.ref("name").withSchema(TableName.Identity), diff --git a/backend/src/services/membership-identity/membership-identity-service.ts b/backend/src/services/membership-identity/membership-identity-service.ts index 1cbc8aa01..224dff67b 100644 --- a/backend/src/services/membership-identity/membership-identity-service.ts +++ b/backend/src/services/membership-identity/membership-identity-service.ts @@ -340,7 +340,8 @@ export const membershipIdentityServiceFactory = ({ await factory.onListMembershipIdentityGuard(dto); - if (dto.permission.rootOrgId === dto.permission.orgId) return { identities: [] }; + if (scopeData.scope !== AccessScope.Project && dto.permission.rootOrgId === dto.permission.orgId) + return { identities: [] }; const identities = await membershipIdentityDAL.listAvailableIdentities(dto.scopeData, dto.permission.rootOrgId); diff --git a/backend/src/services/membership-identity/project/project-membership-identity-factory.ts b/backend/src/services/membership-identity/project/project-membership-identity-factory.ts index 3639dff78..9be970f14 100644 --- a/backend/src/services/membership-identity/project/project-membership-identity-factory.ts +++ b/backend/src/services/membership-identity/project/project-membership-identity-factory.ts @@ -1,4 +1,4 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import { AccessScope, ActionProjectType, ProjectMembershipRole } from "@app/db/schemas"; import { @@ -119,7 +119,7 @@ export const newProjectMembershipIdentityFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionIdentityActions.Edit, - ProjectPermissionSub.Identity + subject(ProjectPermissionSub.Identity, { identityId: dto.selector.identityId }) ); const identityDetails = await identityDAL.findById(dto.selector.identityId); @@ -168,7 +168,7 @@ export const newProjectMembershipIdentityFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionIdentityActions.Delete, - ProjectPermissionSub.Identity + subject(ProjectPermissionSub.Identity, { identityId: dto.selector.identityId }) ); const identityDetails = await identityDAL.findById(dto.selector.identityId); @@ -210,7 +210,7 @@ export const newProjectMembershipIdentityFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionIdentityActions.Read, - ProjectPermissionSub.Identity + subject(ProjectPermissionSub.Identity, { identityId: dto.selector.identityId }) ); }; diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index b6fd85f44..a35451cd3 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -78,7 +78,11 @@ export enum ProjectPermissionIdentityActions { Edit = "edit", Delete = "delete", GrantPrivileges = "grant-privileges", - AssumePrivileges = "assume-privileges" + AssumePrivileges = "assume-privileges", + RevokeAuth = "revoke-auth", + CreateToken = "create-token", + GetToken = "get-token", + DeleteToken = "delete-token" } export enum ProjectPermissionMemberActions { diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx index 4ada1fb9b..14903eaef 100644 --- a/frontend/src/hooks/api/identities/mutations.tsx +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -1,9 +1,9 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { projectIdentityQuery, projectKeys } from "@app/hooks/api"; import { organizationKeys } from "../organization/queries"; -import { subscriptionQueryKeys } from "../subscriptions/queries"; import { identitiesKeys } from "./queries"; import { AddIdentityAliCloudAuthDTO, @@ -21,7 +21,6 @@ import { ClearIdentityLdapAuthLockoutsDTO, ClearIdentityUniversalAuthLockoutsDTO, ClientSecretData, - CreateIdentityDTO, CreateIdentityUniversalAuthClientSecretDTO, CreateIdentityUniversalAuthClientSecretRes, CreateTokenIdentityTokenAuthDTO, @@ -29,7 +28,6 @@ import { DeleteIdentityAliCloudAuthDTO, DeleteIdentityAwsAuthDTO, DeleteIdentityAzureAuthDTO, - DeleteIdentityDTO, DeleteIdentityGcpAuthDTO, DeleteIdentityJwtAuthDTO, DeleteIdentityKubernetesAuthDTO, @@ -40,7 +38,6 @@ import { DeleteIdentityTokenAuthDTO, DeleteIdentityUniversalAuthClientSecretDTO, DeleteIdentityUniversalAuthDTO, - Identity, IdentityAccessToken, IdentityAliCloudAuth, IdentityAwsAuth, @@ -59,7 +56,6 @@ import { UpdateIdentityAliCloudAuthDTO, UpdateIdentityAwsAuthDTO, UpdateIdentityAzureAuthDTO, - UpdateIdentityDTO, UpdateIdentityGcpAuthDTO, UpdateIdentityJwtAuthDTO, UpdateIdentityKubernetesAuthDTO, @@ -72,73 +68,6 @@ import { UpdateTokenIdentityTokenAuthDTO } from "./types"; -export const useCreateIdentity = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async (body) => { - const { - data: { identity } - } = await apiRequest.post("/api/v1/identities/", body); - return identity; - }, - onSuccess: (_, { organizationId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); - queryClient.invalidateQueries({ - queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId) - }); - queryClient.invalidateQueries({ queryKey: identitiesKeys.searchIdentitiesRoot }); - } - }); -}; - -export const useUpdateIdentity = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async ({ identityId, name, role, hasDeleteProtection, metadata }) => { - const { - data: { identity } - } = await apiRequest.patch(`/api/v1/identities/${identityId}`, { - name, - role, - hasDeleteProtection, - metadata - }); - - return identity; - }, - onSuccess: (_, { organizationId, identityId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); - queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); - queryClient.invalidateQueries({ queryKey: identitiesKeys.searchIdentitiesRoot }); - } - }); -}; - -export const useDeleteIdentity = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async ({ identityId }) => { - const { - data: { identity } - } = await apiRequest.delete(`/api/v1/identities/${identityId}`); - return identity; - }, - onSuccess: (_, { organizationId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); - queryClient.invalidateQueries({ - queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId) - }); - queryClient.invalidateQueries({ queryKey: identitiesKeys.searchIdentitiesRoot }); - } - }); -}; - // TODO: move these to /auth export const useAddIdentityUniversalAuth = () => { @@ -171,10 +100,20 @@ export const useAddIdentityUniversalAuth = () => { }); return identityUniversalAuth; }, - onSuccess: (_, { identityId, organizationId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { identityId, organizationId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityUniversalAuth(identityId) @@ -215,10 +154,20 @@ export const useUpdateIdentityUniversalAuth = () => { }); return identityUniversalAuth; }, - onSuccess: (_, { identityId, organizationId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { identityId, organizationId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityUniversalAuth(identityId) @@ -236,10 +185,20 @@ export const useDeleteIdentityUniversalAuth = () => { } = await apiRequest.delete(`/api/v1/auth/universal-auth/identities/${identityId}`); return identityUniversalAuth; }, - onSuccess: (_, { organizationId, identityId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { organizationId, identityId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityUniversalAuth(identityId) @@ -344,10 +303,20 @@ export const useAddIdentityGcpAuth = () => { return identityGcpAuth; }, - onSuccess: (_, { identityId, organizationId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { identityId, organizationId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityGcpAuth(identityId) }); } @@ -386,10 +355,20 @@ export const useUpdateIdentityGcpAuth = () => { return identityGcpAuth; }, - onSuccess: (_, { identityId, organizationId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { identityId, organizationId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityGcpAuth(identityId) }); } @@ -405,10 +384,20 @@ export const useDeleteIdentityGcpAuth = () => { } = await apiRequest.delete(`/api/v1/auth/gcp-auth/identities/${identityId}`); return identityGcpAuth; }, - onSuccess: (_, { organizationId, identityId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { organizationId, identityId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityGcpAuth(identityId) }); } @@ -445,10 +434,20 @@ export const useAddIdentityAwsAuth = () => { return identityAwsAuth; }, - onSuccess: (_, { identityId, organizationId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { identityId, organizationId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityAwsAuth(identityId) }); } @@ -485,10 +484,20 @@ export const useUpdateIdentityAwsAuth = () => { return identityAwsAuth; }, - onSuccess: (_, { identityId, organizationId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { identityId, organizationId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityAwsAuth(identityId) }); } @@ -504,10 +513,20 @@ export const useDeleteIdentityAwsAuth = () => { } = await apiRequest.delete(`/api/v1/auth/aws-auth/identities/${identityId}`); return identityAwsAuth; }, - onSuccess: (_, { organizationId, identityId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { organizationId, identityId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityAwsAuth(identityId) }); } @@ -542,10 +561,20 @@ export const useAddIdentityOciAuth = () => { return identityOciAuth; }, - onSuccess: (_, { identityId, organizationId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { identityId, organizationId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityOciAuth(identityId) }); } @@ -580,10 +609,20 @@ export const useUpdateIdentityOciAuth = () => { return identityOciAuth; }, - onSuccess: (_, { identityId, organizationId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { identityId, organizationId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityOciAuth(identityId) }); } @@ -599,10 +638,20 @@ export const useDeleteIdentityOciAuth = () => { } = await apiRequest.delete(`/api/v1/auth/oci-auth/identities/${identityId}`); return identityOciAuth; }, - onSuccess: (_, { organizationId, identityId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { organizationId, identityId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityOciAuth(identityId) }); } @@ -635,10 +684,20 @@ export const useAddIdentityAliCloudAuth = () => { return identityAliCloudAuth; }, - onSuccess: (_, { identityId, organizationId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { identityId, organizationId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityAliCloudAuth(identityId) @@ -673,10 +732,20 @@ export const useUpdateIdentityAliCloudAuth = () => { return identityAliCloudAuth; }, - onSuccess: (_, { identityId, organizationId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { identityId, organizationId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityAliCloudAuth(identityId) @@ -694,10 +763,20 @@ export const useDeleteIdentityAliCloudAuth = () => { } = await apiRequest.delete(`/api/v1/auth/alicloud-auth/identities/${identityId}`); return identityAliCloudAuth; }, - onSuccess: (_, { organizationId, identityId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { organizationId, identityId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityAliCloudAuth(identityId) @@ -734,10 +813,20 @@ export const useAddIdentityTlsCertAuth = () => { return identityTlsCertAuth; }, - onSuccess: (_, { identityId, organizationId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { identityId, organizationId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityTlsCertAuth(identityId) @@ -774,10 +863,20 @@ export const useUpdateIdentityTlsCertAuth = () => { return identityTlsCertAuth; }, - onSuccess: (_, { identityId, organizationId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { identityId, organizationId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityTlsCertAuth(identityId) @@ -795,10 +894,20 @@ export const useDeleteIdentityTlsCertAuth = () => { } = await apiRequest.delete(`/api/v1/auth/tls-cert-auth/identities/${identityId}`); return identityTlsCertAuth; }, - onSuccess: (_, { organizationId, identityId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { organizationId, identityId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityTlsCertAuth(identityId) @@ -845,10 +954,20 @@ export const useUpdateIdentityOidcAuth = () => { return identityOidcAuth; }, - onSuccess: (_, { identityId, organizationId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { identityId, organizationId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityOidcAuth(identityId) }); } @@ -893,10 +1012,20 @@ export const useAddIdentityOidcAuth = () => { return identityOidcAuth; }, - onSuccess: (_, { identityId, organizationId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { identityId, organizationId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityOidcAuth(identityId) }); } @@ -912,10 +1041,20 @@ export const useDeleteIdentityOidcAuth = () => { } = await apiRequest.delete(`/api/v1/auth/oidc-auth/identities/${identityId}`); return identityOidcAuth; }, - onSuccess: (_, { organizationId, identityId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { organizationId, identityId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityOidcAuth(identityId) }); } @@ -961,10 +1100,20 @@ export const useUpdateIdentityJwtAuth = () => { return identityJwtAuth; }, - onSuccess: (_, { identityId, organizationId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { identityId, organizationId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityJwtAuth(identityId) }); } @@ -1011,10 +1160,20 @@ export const useAddIdentityJwtAuth = () => { return identityJwtAuth; }, - onSuccess: (_, { identityId, organizationId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { identityId, organizationId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityJwtAuth(identityId) }); } @@ -1030,10 +1189,20 @@ export const useDeleteIdentityJwtAuth = () => { } = await apiRequest.delete(`/api/v1/auth/jwt-auth/identities/${identityId}`); return identityJwtAuth; }, - onSuccess: (_, { organizationId, identityId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { organizationId, identityId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityJwtAuth(identityId) }); } @@ -1070,10 +1239,20 @@ export const useAddIdentityAzureAuth = () => { return identityAzureAuth; }, - onSuccess: (_, { identityId, organizationId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { identityId, organizationId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityKubernetesAuth(identityId) @@ -1122,10 +1301,20 @@ export const useAddIdentityKubernetesAuth = () => { return identityKubernetesAuth; }, - onSuccess: (_, { identityId, organizationId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { identityId, organizationId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityAzureAuth(identityId) }); } @@ -1162,10 +1351,20 @@ export const useUpdateIdentityAzureAuth = () => { return identityAzureAuth; }, - onSuccess: (_, { identityId, organizationId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { identityId, organizationId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityAzureAuth(identityId) }); } @@ -1181,10 +1380,20 @@ export const useDeleteIdentityAzureAuth = () => { } = await apiRequest.delete(`/api/v1/auth/azure-auth/identities/${identityId}`); return identityAzureAuth; }, - onSuccess: (_, { organizationId, identityId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { organizationId, identityId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityAzureAuth(identityId) }); } @@ -1231,10 +1440,20 @@ export const useUpdateIdentityKubernetesAuth = () => { return identityKubernetesAuth; }, - onSuccess: (_, { identityId, organizationId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { identityId, organizationId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityKubernetesAuth(identityId) @@ -1252,10 +1471,20 @@ export const useDeleteIdentityKubernetesAuth = () => { } = await apiRequest.delete(`/api/v1/auth/kubernetes-auth/identities/${identityId}`); return identityKubernetesAuth; }, - onSuccess: (_, { organizationId, identityId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { organizationId, identityId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityKubernetesAuth(identityId) @@ -1288,10 +1517,20 @@ export const useAddIdentityTokenAuth = () => { return identityTokenAuth; }, - onSuccess: (_, { identityId, organizationId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { identityId, organizationId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityTokenAuth(identityId) @@ -1324,10 +1563,20 @@ export const useUpdateIdentityTokenAuth = () => { return identityTokenAuth; }, - onSuccess: (_, { identityId, organizationId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { identityId, organizationId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityTokenAuth(identityId) @@ -1345,10 +1594,20 @@ export const useDeleteIdentityTokenAuth = () => { } = await apiRequest.delete(`/api/v1/auth/token-auth/identities/${identityId}`); return identityTokenAuth; }, - onSuccess: (_, { organizationId, identityId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { organizationId, identityId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityTokenAuth(identityId) }); } @@ -1462,10 +1721,20 @@ export const useAddIdentityLdapAuth = () => { ); return data.identityLdapAuth; }, - onSuccess: (_, { identityId, organizationId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { identityId, organizationId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityLdapAuth(identityId) @@ -1519,10 +1788,20 @@ export const useUpdateIdentityLdapAuth = () => { ); return data.identityLdapAuth; }, - onSuccess: (_, { identityId, organizationId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { identityId, organizationId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityLdapAuth(identityId) @@ -1538,10 +1817,20 @@ export const useDeleteIdentityLdapAuth = () => { const { data } = await apiRequest.delete(`/api/v1/auth/ldap-auth/identities/${identityId}`); return data.identityLdapAuth; }, - onSuccess: (_, { organizationId, identityId }) => { - queryClient.invalidateQueries({ - queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) - }); + onSuccess: (_, { organizationId, identityId, projectId }) => { + if (organizationId) { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + } + if (projectId) { + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId }) + }); + } queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityLdapAuth(identityId) diff --git a/frontend/src/hooks/api/identities/queries.tsx b/frontend/src/hooks/api/identities/queries.tsx index cf7f4be87..dae668624 100644 --- a/frontend/src/hooks/api/identities/queries.tsx +++ b/frontend/src/hooks/api/identities/queries.tsx @@ -13,10 +13,10 @@ import { IdentityJwtAuth, IdentityKubernetesAuth, IdentityLdapAuth, - IdentityMembership, IdentityMembershipOrg, IdentityOciAuth, IdentityOidcAuth, + IdentityProjectMembership, IdentityTlsCertAuth, IdentityTokenAuth, IdentityUniversalAuth, @@ -52,7 +52,7 @@ export const identitiesKeys = { [{ identityId }, "identity-project-memberships"] as const }; -export const useGetIdentityById = (identityId: string) => { +export const useGetOrgIdentityMembershipById = (identityId: string) => { return useQuery({ enabled: Boolean(identityId), queryKey: identitiesKeys.getIdentityById(identityId), @@ -67,7 +67,7 @@ export const useGetIdentityById = (identityId: string) => { }); }; -export const useSearchIdentities = (dto: TSearchIdentitiesDTO) => { +export const useSearchOrgIdentityMemberships = (dto: TSearchIdentitiesDTO) => { const { limit, search, offset, orderBy, orderDirection } = dto; return useQuery({ queryKey: identitiesKeys.searchIdentities(dto), @@ -95,7 +95,7 @@ export const useGetIdentityProjectMemberships = (identityId: string) => { queryFn: async () => { const { data: { identityMemberships } - } = await apiRequest.get<{ identityMemberships: IdentityMembership[] }>( + } = await apiRequest.get<{ identityMemberships: IdentityProjectMembership[] }>( `/api/v1/identities/${identityId}/identity-memberships` ); return identityMemberships; diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index a0eb828e8..cdf05e621 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -1,6 +1,8 @@ +import { TemporaryPermissionMode } from "@app/hooks/api/shared"; + import { OrderByDirection } from "../generic/types"; import { OrgIdentityOrderBy } from "../organization/types"; -import { Project, ProjectUserMembershipTemporaryMode } from "../projects/types"; +import { Project } from "../projects/types"; import { TOrgRole } from "../roles/types"; import { IdentityAuthMethod, IdentityJwtConfigurationType } from "./enums"; @@ -21,6 +23,8 @@ export type Identity = { updatedAt: string; isInstanceAdmin?: boolean; orgId: string; + projectId?: string | null; + metadata?: { key: string; value: string; id: string }[]; }; export type IdentityAccessToken = { @@ -52,7 +56,7 @@ export type IdentityMembershipOrg = { updatedAt: string; }; -export type IdentityMembership = { +export type IdentityProjectMembership = { id: string; identity: Identity; project: Pick; @@ -74,7 +78,7 @@ export type IdentityMembership = { | { isTemporary: true; temporaryRange: string; - temporaryMode: ProjectUserMembershipTemporaryMode; + temporaryMode: TemporaryPermissionMode; temporaryAccessEndTime: string; temporaryAccessStartTime: string; } @@ -82,6 +86,8 @@ export type IdentityMembership = { >; createdAt: string; updatedAt: string; + lastLoginTime?: string; + lastLoginAuthMethod?: IdentityAuthMethod; }; export type CreateIdentityDTO = { @@ -122,7 +128,8 @@ export type IdentityUniversalAuth = { }; export type AddIdentityUniversalAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; clientSecretTrustedIps: { ipAddress: string; @@ -138,10 +145,11 @@ export type AddIdentityUniversalAuthDTO = { lockoutThreshold: number; lockoutDurationSeconds: number; lockoutCounterResetSeconds: number; -}; +} & ({ organizationId: string } | { projectId: string }); export type UpdateIdentityUniversalAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; clientSecretTrustedIps?: { ipAddress: string; @@ -157,12 +165,13 @@ export type UpdateIdentityUniversalAuthDTO = { lockoutThreshold?: number; lockoutDurationSeconds?: number; lockoutCounterResetSeconds?: number; -}; +} & ({ organizationId: string } | { projectId: string }); export type DeleteIdentityUniversalAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; -}; +} & ({ organizationId: string } | { projectId: string }); export type IdentityGcpAuth = { identityId: string; @@ -177,7 +186,8 @@ export type IdentityGcpAuth = { }; export type AddIdentityGcpAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; type: "iam" | "gce"; allowedServiceAccounts: string; @@ -189,10 +199,11 @@ export type AddIdentityGcpAuthDTO = { accessTokenTrustedIps: { ipAddress: string; }[]; -}; +} & ({ organizationId: string } | { projectId: string }); export type UpdateIdentityGcpAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; type?: "iam" | "gce"; allowedServiceAccounts?: string; @@ -204,12 +215,13 @@ export type UpdateIdentityGcpAuthDTO = { accessTokenTrustedIps?: { ipAddress: string; }[]; -}; +} & ({ organizationId: string } | { projectId: string }); export type DeleteIdentityGcpAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; -}; +} & ({ organizationId: string } | { projectId: string }); export type IdentityOidcAuth = { identityId: string; @@ -227,7 +239,8 @@ export type IdentityOidcAuth = { }; export type AddIdentityOidcAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; oidcDiscoveryUrl: string; caCert: string; @@ -242,10 +255,11 @@ export type AddIdentityOidcAuthDTO = { accessTokenTrustedIps: { ipAddress: string; }[]; -}; +} & ({ organizationId: string } | { projectId: string }); export type UpdateIdentityOidcAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; oidcDiscoveryUrl?: string; caCert?: string; @@ -260,12 +274,13 @@ export type UpdateIdentityOidcAuthDTO = { accessTokenTrustedIps?: { ipAddress: string; }[]; -}; +} & ({ organizationId: string } | { projectId: string }); export type DeleteIdentityOidcAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; -}; +} & ({ organizationId: string } | { projectId: string }); export type IdentityAwsAuth = { identityId: string; @@ -280,7 +295,8 @@ export type IdentityAwsAuth = { }; export type AddIdentityAwsAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; stsEndpoint: string; allowedPrincipalArns: string; @@ -291,10 +307,11 @@ export type AddIdentityAwsAuthDTO = { accessTokenTrustedIps: { ipAddress: string; }[]; -}; +} & ({ organizationId: string } | { projectId: string }); export type UpdateIdentityAwsAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; stsEndpoint?: string; allowedPrincipalArns?: string; @@ -308,9 +325,10 @@ export type UpdateIdentityAwsAuthDTO = { }; export type DeleteIdentityAwsAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; -}; +} & ({ organizationId: string } | { projectId: string }); export type IdentityAliCloudAuth = { identityId: string; @@ -323,7 +341,8 @@ export type IdentityAliCloudAuth = { }; export type AddIdentityAliCloudAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; allowedArns: string; accessTokenTTL: number; @@ -332,10 +351,11 @@ export type AddIdentityAliCloudAuthDTO = { accessTokenTrustedIps: { ipAddress: string; }[]; -}; +} & ({ organizationId: string } | { projectId: string }); export type UpdateIdentityAliCloudAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; allowedArns: string; accessTokenTTL?: number; @@ -344,12 +364,13 @@ export type UpdateIdentityAliCloudAuthDTO = { accessTokenTrustedIps?: { ipAddress: string; }[]; -}; +} & ({ organizationId: string } | { projectId: string }); export type DeleteIdentityAliCloudAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; -}; +} & ({ organizationId: string } | { projectId: string }); export type IdentityOciAuth = { identityId: string; @@ -363,7 +384,8 @@ export type IdentityOciAuth = { }; export type AddIdentityOciAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; tenancyOcid: string; allowedUsernames?: string | null; @@ -373,10 +395,11 @@ export type AddIdentityOciAuthDTO = { accessTokenTrustedIps: { ipAddress: string; }[]; -}; +} & ({ organizationId: string } | { projectId: string }); export type UpdateIdentityOciAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; tenancyOcid?: string; allowedUsernames?: string | null; @@ -386,12 +409,13 @@ export type UpdateIdentityOciAuthDTO = { accessTokenTrustedIps?: { ipAddress: string; }[]; -}; +} & ({ organizationId: string } | { projectId: string }); export type DeleteIdentityOciAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; -}; +} & ({ organizationId: string } | { projectId: string }); export type IdentityAzureAuth = { identityId: string; @@ -405,7 +429,8 @@ export type IdentityAzureAuth = { }; export type AddIdentityAzureAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; tenantId: string; resource: string; @@ -416,10 +441,11 @@ export type AddIdentityAzureAuthDTO = { accessTokenTrustedIps: { ipAddress: string; }[]; -}; +} & ({ organizationId: string } | { projectId: string }); export type UpdateIdentityAzureAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; tenantId?: string; resource?: string; @@ -430,12 +456,13 @@ export type UpdateIdentityAzureAuthDTO = { accessTokenTrustedIps?: { ipAddress: string; }[]; -}; +} & ({ organizationId: string } | { projectId: string }); export type DeleteIdentityAzureAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; -}; +} & ({ organizationId: string } | { projectId: string }); export enum IdentityKubernetesAuthTokenReviewMode { Api = "api", @@ -459,7 +486,8 @@ export type IdentityKubernetesAuth = { }; export type AddIdentityKubernetesAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; kubernetesHost: string | null; tokenReviewerJwt?: string; @@ -475,10 +503,11 @@ export type AddIdentityKubernetesAuthDTO = { accessTokenTrustedIps: { ipAddress: string; }[]; -}; +} & ({ organizationId: string } | { projectId: string }); export type UpdateIdentityKubernetesAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; kubernetesHost?: string | null; tokenReviewerJwt?: string | null; @@ -494,12 +523,13 @@ export type UpdateIdentityKubernetesAuthDTO = { accessTokenTrustedIps?: { ipAddress: string; }[]; -}; +} & ({ organizationId: string } | { projectId: string }); export type DeleteIdentityKubernetesAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; -}; +} & ({ organizationId: string } | { projectId: string }); export type IdentityTlsCertAuth = { identityId: string; @@ -512,7 +542,8 @@ export type IdentityTlsCertAuth = { }; export type AddIdentityTlsCertAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; caCertificate: string; allowedCommonNames?: string; @@ -522,10 +553,11 @@ export type AddIdentityTlsCertAuthDTO = { accessTokenTrustedIps: { ipAddress: string; }[]; -}; +} & ({ organizationId: string } | { projectId: string }); export type UpdateIdentityTlsCertAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; caCertificate: string; allowedCommonNames?: string | null; @@ -535,12 +567,13 @@ export type UpdateIdentityTlsCertAuthDTO = { accessTokenTrustedIps?: { ipAddress: string; }[]; -}; +} & ({ organizationId: string } | { projectId: string }); export type DeleteIdentityTlsCertAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; -}; +} & ({ organizationId: string } | { projectId: string }); export type CreateIdentityUniversalAuthClientSecretDTO = { identityId: string; @@ -585,7 +618,8 @@ export type IdentityTokenAuth = { }; export type AddIdentityLdapAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; templateId?: string; url?: string; @@ -609,11 +643,12 @@ export type AddIdentityLdapAuthDTO = { lockoutThreshold: number; lockoutDurationSeconds: number; lockoutCounterResetSeconds: number; -}; +} & ({ organizationId: string } | { projectId: string }); export type UpdateIdentityLdapAuthDTO = { identityId: string; - organizationId: string; + organizationId?: string; + projectId?: string; templateId?: string; url?: string; bindDN?: string; @@ -636,12 +671,13 @@ export type UpdateIdentityLdapAuthDTO = { lockoutThreshold?: number; lockoutDurationSeconds?: number; lockoutCounterResetSeconds?: number; -}; +} & ({ organizationId: string } | { projectId: string }); export type DeleteIdentityLdapAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; -}; +} & ({ organizationId: string } | { projectId: string }); export type IdentityLdapAuth = { url?: string; @@ -673,7 +709,8 @@ export type ClearIdentityLdapAuthLockoutsDTO = { }; export type AddIdentityTokenAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; accessTokenTTL: number; accessTokenMaxTTL: number; @@ -681,10 +718,11 @@ export type AddIdentityTokenAuthDTO = { accessTokenTrustedIps: { ipAddress: string; }[]; -}; +} & ({ organizationId: string } | { projectId: string }); export type UpdateIdentityTokenAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; accessTokenTTL?: number; accessTokenMaxTTL?: number; @@ -692,12 +730,13 @@ export type UpdateIdentityTokenAuthDTO = { accessTokenTrustedIps?: { ipAddress: string; }[]; -}; +} & ({ organizationId: string } | { projectId: string }); export type DeleteIdentityTokenAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; -}; +} & ({ organizationId: string } | { projectId: string }); export type IdentityJwtAuth = { identityId: string; @@ -716,7 +755,8 @@ export type IdentityJwtAuth = { }; export type AddIdentityJwtAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; configurationType: string; jwksUrl?: string; @@ -732,10 +772,11 @@ export type AddIdentityJwtAuthDTO = { accessTokenTrustedIps: { ipAddress: string; }[]; -}; +} & ({ organizationId: string } | { projectId: string }); export type UpdateIdentityJwtAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; configurationType?: string; jwksUrl?: string; @@ -751,12 +792,13 @@ export type UpdateIdentityJwtAuthDTO = { accessTokenTrustedIps?: { ipAddress: string; }[]; -}; +} & ({ organizationId: string } | { projectId: string }); export type DeleteIdentityJwtAuthDTO = { - organizationId: string; + organizationId?: string; + projectId?: string; identityId: string; -}; +} & ({ organizationId: string } | { projectId: string }); export type CreateTokenIdentityTokenAuthDTO = { identityId: string; @@ -785,8 +827,8 @@ export type RevokeTokenRes = { message: string; }; -export type TProjectIdentitiesList = { - identityMemberships: IdentityMembership[]; +export type TProjectIdentityMembershipsList = { + identityMemberships: IdentityProjectMembership[]; totalCount: number; }; diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 0216ae030..33eacd18d 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -25,10 +25,14 @@ export * from "./ldapConfig"; export * from "./oidcConfig"; export * from "./orgAdmin"; export * from "./organization"; +export * from "./orgIdentity"; +export * from "./orgIdentityMembership"; export * from "./pkiAlerts"; export * from "./pkiCollections"; export * from "./pkiSubscriber"; export * from "./pkiSyncs"; +export * from "./projectIdentity"; +export * from "./projectIdentityMembership"; export * from "./projects"; export * from "./projectUserAdditionalPrivilege"; export * from "./rateLimit"; diff --git a/frontend/src/hooks/api/orgIdentity/index.ts b/frontend/src/hooks/api/orgIdentity/index.ts new file mode 100644 index 000000000..60fb072cc --- /dev/null +++ b/frontend/src/hooks/api/orgIdentity/index.ts @@ -0,0 +1,3 @@ +export * from "./mutations"; +export * from "./queries"; +export type * from "./types"; diff --git a/frontend/src/hooks/api/orgIdentity/mutations.tsx b/frontend/src/hooks/api/orgIdentity/mutations.tsx new file mode 100644 index 000000000..acc863279 --- /dev/null +++ b/frontend/src/hooks/api/orgIdentity/mutations.tsx @@ -0,0 +1,116 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; +import { identitiesKeys } from "@app/hooks/api"; +import { CreateIdentityDTO, Identity, UpdateIdentityDTO } from "@app/hooks/api/identities/types"; +import { organizationKeys } from "@app/hooks/api/organization/queries"; +import { subscriptionQueryKeys } from "@app/hooks/api/subscriptions/queries"; + +import { orgIdentityQuery } from "./queries"; +import { TDeleteOrgIdentityDTO, TOrgIdentity } from "./types"; + +// TODO (scott/akhi): eventually move to the new api commented out below; the current ones use old api + +export const useCreateOrgIdentity = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (body) => { + const { + data: { identity } + } = await apiRequest.post("/api/v1/identities/", body); + return identity; + }, + onSuccess: (_, { organizationId }) => { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + queryClient.invalidateQueries({ + queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId) + }); + queryClient.invalidateQueries({ queryKey: identitiesKeys.searchIdentitiesRoot }); + } + }); +}; + +export const useUpdateOrgIdentity = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ identityId, name, role, hasDeleteProtection, metadata }) => { + const { + data: { identity } + } = await apiRequest.patch(`/api/v1/identities/${identityId}`, { + name, + role, + hasDeleteProtection, + metadata + }); + + return identity; + }, + onSuccess: (_, { organizationId, identityId }) => { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); + queryClient.invalidateQueries({ queryKey: identitiesKeys.searchIdentitiesRoot }); + } + }); +}; + +// export const useCreateOrgIdentity = () => { +// const queryClient = useQueryClient(); +// return useMutation({ +// mutationFn: async (dto: TCreateOrgIdentityDTO) => { +// const { data } = await apiRequest.post<{ identity: TOrgIdentity }>( +// "/api/v1/organization/identities", +// dto +// ); +// return data; +// }, +// onSuccess: () => { +// queryClient.invalidateQueries({ queryKey: orgIdentityQuery.allKey() }); +// queryClient.invalidateQueries({ +// queryKey: subscriptionQueryKeys.all() +// }); +// } +// }); +// }; +// +// export const useUpdateOrgIdentity = () => { +// const queryClient = useQueryClient(); +// return useMutation({ +// mutationFn: async ({ identityId, ...updates }: TUpdateOrgIdentityDTO) => { +// const { data } = await apiRequest.patch<{ identity: TOrgIdentity }>( +// `/api/v1/organization/identities/${identityId}`, +// updates +// ); +// return data; +// }, +// onSuccess: () => { +// queryClient.invalidateQueries({ queryKey: orgIdentityQuery.allKey() }); +// } +// }); +// }; + +export const useDeleteOrgIdentity = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ identityId }: TDeleteOrgIdentityDTO) => { + const { data } = await apiRequest.delete<{ identity: TOrgIdentity }>( + `/api/v1/identities/${identityId}` + ); + return data; + }, + onSuccess: (_, { orgId }) => { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(orgId) + }); + queryClient.invalidateQueries({ queryKey: identitiesKeys.searchIdentitiesRoot }); + queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(orgId) }); + queryClient.invalidateQueries({ queryKey: orgIdentityQuery.allKey() }); + queryClient.invalidateQueries({ + queryKey: subscriptionQueryKeys.all() + }); + } + }); +}; diff --git a/frontend/src/hooks/api/orgIdentity/queries.tsx b/frontend/src/hooks/api/orgIdentity/queries.tsx new file mode 100644 index 000000000..0f0a74992 --- /dev/null +++ b/frontend/src/hooks/api/orgIdentity/queries.tsx @@ -0,0 +1,40 @@ +import { queryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TGetOrgIdentityByIdDTO, TListOrgIdentitiesDTO, TOrgIdentity } from "./types"; + +export const orgIdentityQuery = { + allKey: () => ["organization-identities"] as const, + getByIdKey: (params: TGetOrgIdentityByIdDTO) => + [...orgIdentityQuery.allKey(), "by-id", params] as const, + listKey: (params?: TListOrgIdentitiesDTO) => + [...orgIdentityQuery.allKey(), "list", params] as const, + getById: (params: TGetOrgIdentityByIdDTO) => + queryOptions({ + queryKey: orgIdentityQuery.getByIdKey(params), + queryFn: async () => { + const { data } = await apiRequest.get<{ identity: TOrgIdentity }>( + `/api/v1/organization/identities/${params.identityId}` + ); + return data.identity; + } + }), + list: (params: TListOrgIdentitiesDTO = {}) => + queryOptions({ + queryKey: orgIdentityQuery.listKey(params), + queryFn: async () => { + const { data } = await apiRequest.get<{ + identities: TOrgIdentity[]; + totalCount: number; + }>("/api/v1/organization/identities", { + params: { + offset: params.offset, + limit: params.limit, + search: params.search + } + }); + return data; + } + }) +}; diff --git a/frontend/src/hooks/api/orgIdentity/types.ts b/frontend/src/hooks/api/orgIdentity/types.ts new file mode 100644 index 000000000..03f74a23c --- /dev/null +++ b/frontend/src/hooks/api/orgIdentity/types.ts @@ -0,0 +1,31 @@ +import { TIdentity, TMetadata } from "@app/hooks/api/shared"; + +export type TOrgIdentity = TIdentity; + +export type TCreateOrgIdentityDTO = { + name: string; + hasDeleteProtection?: boolean; + metadata?: TMetadata; +}; + +export type TUpdateOrgIdentityDTO = { + identityId: string; + name?: string; + hasDeleteProtection?: boolean; + metadata?: TMetadata; +}; + +export type TGetOrgIdentityByIdDTO = { + identityId: string; +}; + +export type TListOrgIdentitiesDTO = { + offset?: number; + limit?: number; + search?: string; +}; + +export type TDeleteOrgIdentityDTO = { + identityId: string; + orgId: string; +}; diff --git a/frontend/src/hooks/api/orgIdentityMembership/queries.tsx b/frontend/src/hooks/api/orgIdentityMembership/queries.tsx new file mode 100644 index 000000000..d0cad6993 --- /dev/null +++ b/frontend/src/hooks/api/orgIdentityMembership/queries.tsx @@ -0,0 +1,31 @@ +import { queryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { + TAvailableOrganizationIdentities, + TListAvailableOrganizationIdentitiesDTO, + TListOrgIdentityMembershipsDTO +} from "./types"; + +export const orgIdentityMembershipQuery = { + allKey: () => ["organization-identity-memberships"] as const, + listAvailableKey: (params?: TListOrgIdentityMembershipsDTO) => + [...orgIdentityMembershipQuery.allKey(), "list-available", params] as const, + listAvailable: (params: TListAvailableOrganizationIdentitiesDTO = {}) => + queryOptions({ + queryKey: orgIdentityMembershipQuery.listAvailableKey(params), + queryFn: async () => { + const { data } = await apiRequest.get<{ + identities: TAvailableOrganizationIdentities; + }>("/api/v1/organization/available-identities", { + params: { + offset: params.offset, + limit: params.limit, + identityName: params.identityName + } + }); + return data.identities; + } + }) +}; diff --git a/frontend/src/hooks/api/orgIdentityMembership/types.ts b/frontend/src/hooks/api/orgIdentityMembership/types.ts index 95fa06b82..2ec8dc3d4 100644 --- a/frontend/src/hooks/api/orgIdentityMembership/types.ts +++ b/frontend/src/hooks/api/orgIdentityMembership/types.ts @@ -1,6 +1,4 @@ -export enum TemporaryPermissionMode { - Relative = "relative" -} +import { TRoles } from "@app/hooks/api/shared"; export type TOrgIdentityMembership = { id: string; @@ -12,21 +10,24 @@ export type TOrgIdentityMembership = { export type TCreateOrgIdentityMembershipDTO = { identityId: string; - roles: Array< - | { - role: string; - isTemporary?: false; - } - | { - role: string; - isTemporary: true; - temporaryMode: TemporaryPermissionMode; - temporaryRange: string; - temporaryAccessStartTime: string; - } - >; + roles: TRoles; }; export type TDeleteOrgIdentityMembershipDTO = { identityId: string; }; + +export type TListOrgIdentityMembershipsDTO = { + offset?: number; + limit?: number; + identityName?: string; + roles?: string[]; +}; + +export type TListAvailableOrganizationIdentitiesDTO = { + offset?: number; + limit?: number; + identityName?: string; +}; + +export type TAvailableOrganizationIdentities = Array<{ id: string; name: string }>; diff --git a/frontend/src/hooks/api/organization/index.ts b/frontend/src/hooks/api/organization/index.ts index 7c283691e..f4627a614 100644 --- a/frontend/src/hooks/api/organization/index.ts +++ b/frontend/src/hooks/api/organization/index.ts @@ -6,7 +6,6 @@ export { useDeleteOrgById, useDeleteOrgPmtMethod, useDeleteOrgTaxId, - useGetAvailableOrgIdentities, useGetIdentityMembershipOrgs, useGetOrganizationGroups, useGetOrganizations, diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index 4340f9718..9e2413b28 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -579,19 +579,6 @@ export const useGetOrgIntegrationAuths = ( }); }; -export const useGetAvailableOrgIdentities = (enabled = true) => - useQuery({ - queryKey: organizationKeys.getAvailableIdentities(), - queryFn: async () => { - const { data } = await apiRequest.get<{ identities: { name: string; id: string }[] }>( - "/api/v1/organization/identities/available" - ); - - return data.identities; - }, - enabled - }); - export const useGetAvailableOrgUsers = (enabled = true) => useQuery({ queryKey: organizationKeys.getAvailableUsers(), diff --git a/frontend/src/hooks/api/organizationIdentity/index.tsx b/frontend/src/hooks/api/organizationIdentity/index.tsx deleted file mode 100644 index 9da529579..000000000 --- a/frontend/src/hooks/api/organizationIdentity/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -export { - useCreateOrganizationIdentity, - useDeleteOrganizationIdentity, - useUpdateOrganizationIdentity -} from "./mutations"; -export { organizationIdentityQuery } from "./queries"; -export type { - TCreateOrganizationIdentityDTO, - TDeleteOrganizationIdentityDTO, - TGetOrganizationIdentityByIdDTO, - TListOrganizationIdentitiesDTO, - TMetadata, - TOrganizationIdentity, - TUpdateOrganizationIdentityDTO -} from "./types"; diff --git a/frontend/src/hooks/api/organizationIdentity/mutations.tsx b/frontend/src/hooks/api/organizationIdentity/mutations.tsx deleted file mode 100644 index 510f440fb..000000000 --- a/frontend/src/hooks/api/organizationIdentity/mutations.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -import { apiRequest } from "@app/config/request"; - -import { organizationIdentityQuery } from "./queries"; -import { - TCreateOrganizationIdentityDTO, - TDeleteOrganizationIdentityDTO, - TOrganizationIdentity, - TUpdateOrganizationIdentityDTO -} from "./types"; - -export const useCreateOrganizationIdentity = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async (dto: TCreateOrganizationIdentityDTO) => { - const { data } = await apiRequest.post<{ identity: TOrganizationIdentity }>( - "/api/v1/organization/identities", - dto - ); - return data; - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: organizationIdentityQuery.allKey() }); - } - }); -}; - -export const useUpdateOrganizationIdentity = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async ({ identityId, ...updates }: TUpdateOrganizationIdentityDTO) => { - const { data } = await apiRequest.patch<{ identity: TOrganizationIdentity }>( - `/api/v1/organization/identities/${identityId}`, - updates - ); - return data; - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: organizationIdentityQuery.allKey() }); - } - }); -}; - -export const useDeleteOrganizationIdentity = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async ({ identityId }: TDeleteOrganizationIdentityDTO) => { - const { data } = await apiRequest.delete<{ identity: TOrganizationIdentity }>( - `/api/v1/organization/identities/${identityId}` - ); - return data; - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: organizationIdentityQuery.allKey() }); - } - }); -}; diff --git a/frontend/src/hooks/api/organizationIdentity/queries.tsx b/frontend/src/hooks/api/organizationIdentity/queries.tsx deleted file mode 100644 index 17bfc5816..000000000 --- a/frontend/src/hooks/api/organizationIdentity/queries.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { queryOptions } from "@tanstack/react-query"; - -import { apiRequest } from "@app/config/request"; - -import { - TGetOrganizationIdentityByIdDTO, - TListOrganizationIdentitiesDTO, - TOrganizationIdentity -} from "./types"; - -export const organizationIdentityQuery = { - allKey: () => ["organization-identities"] as const, - getByIdKey: (params: TGetOrganizationIdentityByIdDTO) => - [...organizationIdentityQuery.allKey(), "by-id", params] as const, - listKey: (params?: TListOrganizationIdentitiesDTO) => - [...organizationIdentityQuery.allKey(), "list", params] as const, - getById: (params: TGetOrganizationIdentityByIdDTO) => - queryOptions({ - queryKey: organizationIdentityQuery.getByIdKey(params), - queryFn: async () => { - const { data } = await apiRequest.get<{ identity: TOrganizationIdentity }>( - `/api/v1/organization/identities/${params.identityId}` - ); - return data.identity; - } - }), - list: (params: TListOrganizationIdentitiesDTO = {}) => - queryOptions({ - queryKey: organizationIdentityQuery.listKey(params), - queryFn: async () => { - const { data } = await apiRequest.get<{ - identities: TOrganizationIdentity[]; - totalCount: number; - }>("/api/v1/organization/identities", { - params: { - offset: params.offset, - limit: params.limit, - search: params.search - } - }); - return data; - } - }) -}; diff --git a/frontend/src/hooks/api/organizationIdentity/types.ts b/frontend/src/hooks/api/organizationIdentity/types.ts deleted file mode 100644 index 7ae60feb7..000000000 --- a/frontend/src/hooks/api/organizationIdentity/types.ts +++ /dev/null @@ -1,43 +0,0 @@ -export type TMetadata = { - key: string; - value: string; -}; - -export type TOrganizationIdentity = { - id: string; - name: string; - orgId: string; - projectId: string | null; - createdAt: string; - updatedAt: string; - hasDeleteProtection: boolean; - authMethods?: string[]; - metadata?: TMetadata[]; -}; - -export type TCreateOrganizationIdentityDTO = { - name: string; - hasDeleteProtection?: boolean; - metadata?: TMetadata[]; -}; - -export type TUpdateOrganizationIdentityDTO = { - identityId: string; - name?: string; - hasDeleteProtection?: boolean; - metadata?: TMetadata[]; -}; - -export type TGetOrganizationIdentityByIdDTO = { - identityId: string; -}; - -export type TListOrganizationIdentitiesDTO = { - offset?: number; - limit?: number; - search?: string; -}; - -export type TDeleteOrganizationIdentityDTO = { - identityId: string; -}; diff --git a/frontend/src/hooks/api/projectIdentity/mutations.tsx b/frontend/src/hooks/api/projectIdentity/mutations.tsx index 9df4d9256..277ed76c9 100644 --- a/frontend/src/hooks/api/projectIdentity/mutations.tsx +++ b/frontend/src/hooks/api/projectIdentity/mutations.tsx @@ -1,6 +1,8 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { identitiesKeys, projectKeys } from "@app/hooks/api"; +import { subscriptionQueryKeys } from "@app/hooks/api/subscriptions/queries"; import { projectIdentityQuery } from "./queries"; import { @@ -18,10 +20,13 @@ export const useCreateProjectIdentity = () => { `/api/v1/projects/${projectId}/identities`, dto ); - return data; + return data.identity; }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: projectIdentityQuery.allKey() }); + queryClient.invalidateQueries({ + queryKey: subscriptionQueryKeys.all() + }); } }); }; @@ -34,10 +39,13 @@ export const useUpdateProjectIdentity = () => { `/api/v1/projects/${projectId}/identities/${identityId}`, updates ); - return data; + return data.identity; }, - onSuccess: () => { + onSuccess: (_, { projectId, identityId }) => { queryClient.invalidateQueries({ queryKey: projectIdentityQuery.allKey() }); + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMembershipDetails(projectId, identityId) + }); } }); }; @@ -49,10 +57,20 @@ export const useDeleteProjectIdentity = () => { const { data } = await apiRequest.delete<{ identity: TProjectIdentity }>( `/api/v1/projects/${projectId}/identities/${identityId}` ); - return data; + return data.identity; }, - onSuccess: () => { + onSuccess: (_, { projectId, identityId }) => { queryClient.invalidateQueries({ queryKey: projectIdentityQuery.allKey() }); + queryClient.invalidateQueries({ queryKey: projectIdentityQuery.allKey() }); + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: identitiesKeys.getIdentityProjectMemberships(identityId) + }); + queryClient.invalidateQueries({ + queryKey: subscriptionQueryKeys.all() + }); } }); }; diff --git a/frontend/src/hooks/api/projectIdentity/queries.tsx b/frontend/src/hooks/api/projectIdentity/queries.tsx index 16fafbc55..163cd8d74 100644 --- a/frontend/src/hooks/api/projectIdentity/queries.tsx +++ b/frontend/src/hooks/api/projectIdentity/queries.tsx @@ -2,11 +2,7 @@ import { queryOptions } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { - TGetProjectIdentityByIdDTO, - TListProjectIdentitiesDTO, - TProjectIdentity -} from "./types"; +import { TGetProjectIdentityByIdDTO, TListProjectIdentitiesDTO, TProjectIdentity } from "./types"; export const projectIdentityQuery = { allKey: () => ["project-identities"] as const, diff --git a/frontend/src/hooks/api/projectIdentity/types.ts b/frontend/src/hooks/api/projectIdentity/types.ts index 278b85e8a..3e6700391 100644 --- a/frontend/src/hooks/api/projectIdentity/types.ts +++ b/frontend/src/hooks/api/projectIdentity/types.ts @@ -1,28 +1,18 @@ +import { TIdentity, TMetadata } from "@app/hooks/api/shared"; + export type TProjectIdentityMetadata = { key: string; value: string; id: string; }; -export type TProjectIdentity = { - id: string; - name: string; - orgId: string; - projectId: string | null; - createdAt: string; - updatedAt: string; - hasDeleteProtection: boolean; - metadata?: TProjectIdentityMetadata[]; -}; +export type TProjectIdentity = TIdentity; export type TCreateProjectIdentityDTO = { projectId: string; name: string; hasDeleteProtection?: boolean; - metadata?: Array<{ - key: string; - value: string; - }>; + metadata?: TMetadata[]; }; export type TUpdateProjectIdentityDTO = { @@ -30,10 +20,7 @@ export type TUpdateProjectIdentityDTO = { identityId: string; name?: string; hasDeleteProtection?: boolean; - metadata?: Array<{ - key: string; - value: string; - }>; + metadata?: TMetadata[]; }; export type TGetProjectIdentityByIdDTO = { diff --git a/frontend/src/hooks/api/projectIdentityMembership/index.ts b/frontend/src/hooks/api/projectIdentityMembership/index.ts new file mode 100644 index 000000000..177955438 --- /dev/null +++ b/frontend/src/hooks/api/projectIdentityMembership/index.ts @@ -0,0 +1,3 @@ +export * from "./mutations"; +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/projectIdentityMembership/mutations.tsx b/frontend/src/hooks/api/projectIdentityMembership/mutations.tsx new file mode 100644 index 000000000..053d44a54 --- /dev/null +++ b/frontend/src/hooks/api/projectIdentityMembership/mutations.tsx @@ -0,0 +1,93 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; +import { identitiesKeys, projectKeys } from "@app/hooks/api"; +import { projectIdentityQuery } from "@app/hooks/api/projectIdentity"; + +import { + TCreateProjectIdentityMembershipDTO, + TDeleteProjectIdentityMembershipDTO, + TProjectIdentityMembership, + TUpdateProjectIdentityMembershipDTO +} from "./types"; + +export const useCreateProjectIdentityMembership = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ identityId, projectId, role }: TCreateProjectIdentityMembershipDTO) => { + const { + data: { identityMembership } + } = await apiRequest.post<{ identityMembership: TProjectIdentityMembership }>( + `/api/v1/projects/${projectId}/identity-memberships/${identityId}`, + { + role + } + ); + + return identityMembership; + }, + onSuccess: (_, { identityId, projectId }) => { + queryClient.invalidateQueries({ queryKey: projectIdentityQuery.allKey() }); + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: identitiesKeys.getIdentityProjectMemberships(identityId) + }); + } + }); +}; + +export const useUpdateProjectIdentityMembership = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + projectId, + identityId, + ...updates + }: TUpdateProjectIdentityMembershipDTO) => { + const { + data: { identityMembership } + } = await apiRequest.patch<{ identityMembership: TProjectIdentityMembership }>( + `/api/v1/projects/${projectId}/identity-memberships/${identityId}`, + updates + ); + return identityMembership; + }, + onSuccess: (_, { projectId, identityId }) => { + queryClient.invalidateQueries({ queryKey: projectIdentityQuery.allKey() }); + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: identitiesKeys.getIdentityProjectMemberships(identityId) + }); + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMembershipDetails(projectId, identityId) + }); + } + }); +}; + +export const useDeleteProjectIdentityMembership = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ identityId, projectId }: TDeleteProjectIdentityMembershipDTO) => { + const { + data: { identityMembership } + } = await apiRequest.delete<{ identityMembership: TProjectIdentityMembership }>( + `/api/v1/projects/${projectId}/identity-memberships/${identityId}` + ); + return identityMembership; + }, + onSuccess: (_, { identityId, projectId }) => { + queryClient.invalidateQueries({ queryKey: projectIdentityQuery.allKey() }); + queryClient.invalidateQueries({ + queryKey: projectKeys.getProjectIdentityMemberships(projectId) + }); + queryClient.invalidateQueries({ + queryKey: identitiesKeys.getIdentityProjectMemberships(identityId) + }); + } + }); +}; diff --git a/frontend/src/hooks/api/projectIdentityMembership/queries.ts b/frontend/src/hooks/api/projectIdentityMembership/queries.ts new file mode 100644 index 000000000..bf86bd53c --- /dev/null +++ b/frontend/src/hooks/api/projectIdentityMembership/queries.ts @@ -0,0 +1,101 @@ +import { queryOptions, useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; +import { + projectKeys, + TAvailableProjectIdentities, + TListAvailableProjectIdentitiesDTO +} from "@app/hooks/api"; +import { OrderByDirection } from "@app/hooks/api/generic/types"; +import { + IdentityProjectMembership, + TProjectIdentityMembershipsList +} from "@app/hooks/api/identities/types"; +import { ProjectIdentityOrderBy, TListProjectIdentitiesDTO } from "@app/hooks/api/projects/types"; + +export const projectIdentityMembershipQuery = { + allKey: () => ["project-identity-memberships"] as const, + listAvailableKey: (params?: TListAvailableProjectIdentitiesDTO) => + [...projectIdentityMembershipQuery.allKey(), "list-available", params] as const, + listAvailable: (params: TListAvailableProjectIdentitiesDTO) => + queryOptions({ + queryKey: projectIdentityMembershipQuery.listAvailableKey(params), + queryFn: async () => { + const { data } = await apiRequest.get<{ + identities: TAvailableProjectIdentities; + }>(`/api/v1/projects/${params.projectId}/available-identities`, { + params: { + offset: params.offset, + limit: params.limit, + identityName: params.identityName + } + }); + return data.identities; + } + }) +}; + +// TODO (scott/akhi): move to new projectIdentityMembershipQuery structure + +export const useListProjectIdentityMemberships = ( + { + projectId, + offset = 0, + limit = 100, + orderBy = ProjectIdentityOrderBy.Name, + orderDirection = OrderByDirection.ASC, + search = "" + }: TListProjectIdentitiesDTO, + options?: Omit< + UseQueryOptions< + TProjectIdentityMembershipsList, + unknown, + TProjectIdentityMembershipsList, + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: projectKeys.getProjectIdentityMembershipsWithParams({ + projectId, + offset, + limit, + orderBy, + orderDirection, + search + }), + queryFn: async () => { + const params = new URLSearchParams({ + offset: String(offset), + limit: String(limit), + orderBy: String(orderBy), + orderDirection: String(orderDirection), + search: String(search) + }); + + const { data } = await apiRequest.get( + `/api/v1/projects/${projectId}/identity-memberships`, + { params } + ); + return data; + }, + enabled: true, + ...options + }); +}; + +export const useGetProjectIdentityMembership = (projectId: string, identityId: string) => { + return useQuery({ + enabled: Boolean(projectId && identityId), + queryKey: projectKeys.getProjectIdentityMembershipDetails(projectId, identityId), + queryFn: async () => { + const { + data: { identityMembership } + } = await apiRequest.get<{ identityMembership: IdentityProjectMembership }>( + `/api/v1/projects/${projectId}/identity-memberships/${identityId}` + ); + return identityMembership; + } + }); +}; diff --git a/frontend/src/hooks/api/projectIdentityMembership/types.ts b/frontend/src/hooks/api/projectIdentityMembership/types.ts new file mode 100644 index 000000000..0c0186759 --- /dev/null +++ b/frontend/src/hooks/api/projectIdentityMembership/types.ts @@ -0,0 +1,36 @@ +import { TRoles } from "@app/hooks/api/shared"; + +export type TProjectIdentityMembership = { + id: string; + projectId: string; + identityId: string; + createdAt: string; + updatedAt: string; + // TODO +}; + +export type TCreateProjectIdentityMembershipDTO = { + identityId: string; + projectId: string; + role?: string; +}; + +export type TUpdateProjectIdentityMembershipDTO = { + identityId: string; + projectId: string; + roles: TRoles; +}; + +export type TDeleteProjectIdentityMembershipDTO = { + identityId: string; + projectId: string; +}; + +export type TListAvailableProjectIdentitiesDTO = { + projectId: string; + offset?: number; + limit?: number; + identityName?: string; +}; + +export type TAvailableProjectIdentities = Array<{ id: string; name: string }>; diff --git a/frontend/src/hooks/api/projects/index.tsx b/frontend/src/hooks/api/projects/index.tsx index ed8eba815..d4a3cbc83 100644 --- a/frontend/src/hooks/api/projects/index.tsx +++ b/frontend/src/hooks/api/projects/index.tsx @@ -8,10 +8,8 @@ export { useUpdateProjectSshConfig } from "./mutations"; export { - useAddIdentityToWorkspace, useCreateWorkspace, useCreateWsEnvironment, - useDeleteIdentityFromWorkspace, useDeleteUserFromWorkspace, useDeleteWorkspace, useDeleteWsEnvironment, @@ -21,8 +19,6 @@ export { useGetUserWorkspaceMemberships, useGetWorkspaceAuthorizations, useGetWorkspaceById, - useGetWorkspaceIdentityMembershipDetails, - useGetWorkspaceIdentityMemberships, useGetWorkspaceIndexStatus, useGetWorkspaceIntegrations, useGetWorkspaceUserDetails, @@ -41,7 +37,6 @@ export { useListWorkspaceSshHostGroups, useListWorkspaceSshHosts, useSearchProjects, - useUpdateIdentityWorkspaceRole, useUpdateProject, useUpdateUserWorkspaceRole, useUpdateWsEnvironment, diff --git a/frontend/src/hooks/api/projects/queries.tsx b/frontend/src/hooks/api/projects/queries.tsx index 1613e95a9..8c07ca691 100644 --- a/frontend/src/hooks/api/projects/queries.tsx +++ b/frontend/src/hooks/api/projects/queries.tsx @@ -1,15 +1,12 @@ -import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { OrderByDirection } from "@app/hooks/api/generic/types"; import { CaStatus } from "../ca/enums"; import { TCertificateAuthority } from "../ca/types"; import { TCertificate } from "../certificates/types"; import { TCertificateTemplate } from "../certificateTemplates/types"; import { TGroupMembership } from "../groups/types"; -import { identitiesKeys } from "../identities/queries"; -import { IdentityMembership, TProjectIdentitiesList } from "../identities/types"; import { IntegrationAuth } from "../integrationAuth/types"; import { TIntegration } from "../integrations/types"; import { TPkiAlert } from "../pkiAlerts/types"; @@ -33,13 +30,10 @@ import { DeleteWorkspaceDTO, Project, ProjectEnv, - ProjectIdentityOrderBy, ProjectType, TGetUpgradeProjectStatusDTO, - TListProjectIdentitiesDTO, TProjectSshConfig, TSearchProjectsDTO, - TUpdateWorkspaceIdentityRoleDTO, TUpdateWorkspaceUserRoleDTO, UpdateAuditLogsRetentionDTO, UpdateEnvironmentDTO, @@ -452,154 +446,6 @@ export const useUpdateUserWorkspaceRole = () => { }); }; -export const useAddIdentityToWorkspace = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async ({ - identityId, - projectId, - role - }: { - identityId: string; - projectId: string; - role?: string; - }) => { - const { - data: { identityMembership } - } = await apiRequest.post( - `/api/v1/projects/${projectId}/identity-memberships/${identityId}`, - { - role - } - ); - - return identityMembership; - }, - onSuccess: (_, { identityId, projectId }) => { - queryClient.invalidateQueries({ - queryKey: projectKeys.getProjectIdentityMemberships(projectId) - }); - queryClient.invalidateQueries({ - queryKey: identitiesKeys.getIdentityProjectMemberships(identityId) - }); - } - }); -}; - -export const useUpdateIdentityWorkspaceRole = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async ({ identityId, projectId, roles }: TUpdateWorkspaceIdentityRoleDTO) => { - const { - data: { identityMembership } - } = await apiRequest.patch( - `/api/v1/projects/${projectId}/identity-memberships/${identityId}`, - { - roles - } - ); - - return identityMembership; - }, - onSuccess: (_, { identityId, projectId }) => { - queryClient.invalidateQueries({ - queryKey: projectKeys.getProjectIdentityMemberships(projectId) - }); - queryClient.invalidateQueries({ - queryKey: identitiesKeys.getIdentityProjectMemberships(identityId) - }); - queryClient.invalidateQueries({ - queryKey: projectKeys.getProjectIdentityMembershipDetails(projectId, identityId) - }); - } - }); -}; - -export const useDeleteIdentityFromWorkspace = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async ({ identityId, projectId }: { identityId: string; projectId: string }) => { - const { - data: { identityMembership } - } = await apiRequest.delete( - `/api/v1/projects/${projectId}/identity-memberships/${identityId}` - ); - return identityMembership; - }, - onSuccess: (_, { identityId, projectId }) => { - queryClient.invalidateQueries({ - queryKey: projectKeys.getProjectIdentityMemberships(projectId) - }); - queryClient.invalidateQueries({ - queryKey: identitiesKeys.getIdentityProjectMemberships(identityId) - }); - } - }); -}; - -export const useGetWorkspaceIdentityMemberships = ( - { - projectId, - offset = 0, - limit = 100, - orderBy = ProjectIdentityOrderBy.Name, - orderDirection = OrderByDirection.ASC, - search = "" - }: TListProjectIdentitiesDTO, - options?: Omit< - UseQueryOptions< - TProjectIdentitiesList, - unknown, - TProjectIdentitiesList, - ReturnType - >, - "queryKey" | "queryFn" - > -) => { - return useQuery({ - queryKey: projectKeys.getProjectIdentityMembershipsWithParams({ - projectId, - offset, - limit, - orderBy, - orderDirection, - search - }), - queryFn: async () => { - const params = new URLSearchParams({ - offset: String(offset), - limit: String(limit), - orderBy: String(orderBy), - orderDirection: String(orderDirection), - search: String(search) - }); - - const { data } = await apiRequest.get( - `/api/v1/projects/${projectId}/identity-memberships`, - { params } - ); - return data; - }, - enabled: true, - ...options - }); -}; - -export const useGetWorkspaceIdentityMembershipDetails = (projectId: string, identityId: string) => { - return useQuery({ - enabled: Boolean(projectId && identityId), - queryKey: projectKeys.getProjectIdentityMembershipDetails(projectId, identityId), - queryFn: async () => { - const { - data: { identityMembership } - } = await apiRequest.get<{ identityMembership: IdentityMembership }>( - `/api/v1/projects/${projectId}/identity-memberships/${identityId}` - ); - return identityMembership; - } - }); -}; - export const useGetWorkspaceGroupMembershipDetails = (projectId: string, groupId: string) => { return useQuery({ enabled: Boolean(projectId && groupId), diff --git a/frontend/src/hooks/api/projects/types.ts b/frontend/src/hooks/api/projects/types.ts index 977afd179..b47720106 100644 --- a/frontend/src/hooks/api/projects/types.ts +++ b/frontend/src/hooks/api/projects/types.ts @@ -138,24 +138,6 @@ export type TUpdateWorkspaceUserRoleDTO = { )[]; }; -export type TUpdateWorkspaceIdentityRoleDTO = { - identityId: string; - projectId: string; - roles: ( - | { - role: string; - isTemporary?: false; - } - | { - role: string; - isTemporary: true; - temporaryMode: ProjectUserMembershipTemporaryMode; - temporaryRange: string; - temporaryAccessStartTime: string; - } - )[]; -}; - export type TUpdateWorkspaceGroupRoleDTO = { groupId: string; projectId: string; diff --git a/frontend/src/hooks/api/shared/index.ts b/frontend/src/hooks/api/shared/index.ts new file mode 100644 index 000000000..eea524d65 --- /dev/null +++ b/frontend/src/hooks/api/shared/index.ts @@ -0,0 +1 @@ +export * from "./types"; diff --git a/frontend/src/hooks/api/shared/types.ts b/frontend/src/hooks/api/shared/types.ts new file mode 100644 index 000000000..c57daf3fd --- /dev/null +++ b/frontend/src/hooks/api/shared/types.ts @@ -0,0 +1,37 @@ +import { IdentityAuthMethod } from "@app/hooks/api"; + +export enum TemporaryPermissionMode { + Relative = "relative" +} + +export type TMetadata = { + key: string; + value: string; +}; + +export type TIdentity = { + id: string; + name: string; + orgId: string; + projectId: string | null; + createdAt: string; + updatedAt: string; + hasDeleteProtection: boolean; + authMethods: IdentityAuthMethod[]; + activeLockoutAuthMethods: string[]; + metadata?: Array; +}; + +export type TRoles = Array< + | { + role: string; + isTemporary?: false; + } + | { + role: string; + isTemporary: true; + temporaryMode: TemporaryPermissionMode; + temporaryRange: string; + temporaryAccessStartTime: string; + } +>; diff --git a/frontend/src/hooks/api/subscriptions/queries.tsx b/frontend/src/hooks/api/subscriptions/queries.tsx index f545565eb..325e62e5f 100644 --- a/frontend/src/hooks/api/subscriptions/queries.tsx +++ b/frontend/src/hooks/api/subscriptions/queries.tsx @@ -7,7 +7,8 @@ import { SubscriptionPlan } from "./types"; // import { Workspace } from './types'; export const subscriptionQueryKeys = { - getOrgSubsription: (orgID: string) => ["plan", { orgID }] as const + all: () => ["plan"] as const, + getOrgSubsription: (orgID: string) => [...subscriptionQueryKeys.all(), { orgID }] as const }; export const fetchOrgSubscription = async (orgID: string, refreshCache: boolean = false) => { diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAliCloudAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAliCloudAuthForm.tsx index 94c9c49a3..1aca4dc5f 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAliCloudAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAliCloudAuthForm.tsx @@ -3,6 +3,7 @@ import { Controller, useFieldArray, useForm } from "react-hook-form"; import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useParams } from "@tanstack/react-router"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; @@ -76,7 +77,9 @@ export const IdentityAliCloudAuthForm = ({ const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; const { subscription } = useSubscription(); - + const { projectId } = useParams({ + strict: false + }); const { mutateAsync: addMutateAsync } = useAddIdentityAliCloudAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityAliCloudAuth(); const [tabValue, setTabValue] = useState(IdentityFormTab.Configuration); @@ -144,7 +147,7 @@ export const IdentityAliCloudAuthForm = ({ if (data) { await updateMutateAsync({ - organizationId: orgId, + ...(projectId ? { projectId } : { organizationId: orgId }), allowedArns, identityId, accessTokenTTL: Number(accessTokenTTL), @@ -154,7 +157,7 @@ export const IdentityAliCloudAuthForm = ({ }); } else { await addMutateAsync({ - organizationId: orgId, + ...(projectId ? { projectId } : { organizationId: orgId }), identityId, allowedArns, accessTokenTTL: Number(accessTokenTTL), diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsAuthForm.tsx index a05dcf8df..1737d250b 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsAuthForm.tsx @@ -3,6 +3,7 @@ import { Controller, useFieldArray, useForm } from "react-hook-form"; import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useParams } from "@tanstack/react-router"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; @@ -78,7 +79,9 @@ export const IdentityAwsAuthForm = ({ const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; const { subscription } = useSubscription(); - + const { projectId } = useParams({ + strict: false + }); const { mutateAsync: addMutateAsync } = useAddIdentityAwsAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityAwsAuth(); const [tabValue, setTabValue] = useState(IdentityFormTab.Configuration); @@ -154,7 +157,7 @@ export const IdentityAwsAuthForm = ({ if (data) { await updateMutateAsync({ - organizationId: orgId, + ...(projectId ? { projectId } : { organizationId: orgId }), stsEndpoint, allowedPrincipalArns, allowedAccountIds, @@ -166,7 +169,7 @@ export const IdentityAwsAuthForm = ({ }); } else { await addMutateAsync({ - organizationId: orgId, + ...(projectId ? { projectId } : { organizationId: orgId }), identityId, stsEndpoint: stsEndpoint || "", allowedPrincipalArns: allowedPrincipalArns || "", @@ -176,10 +179,8 @@ export const IdentityAwsAuthForm = ({ accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), accessTokenTrustedIps }); + handlePopUpToggle("identityAuthMethod", false); } - - handlePopUpToggle("identityAuthMethod", false); - createNotification({ text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`, type: "success" diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm.tsx index ada799d13..9315768c9 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm.tsx @@ -3,6 +3,7 @@ import { Controller, useFieldArray, useForm } from "react-hook-form"; import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useParams } from "@tanstack/react-router"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; @@ -73,7 +74,9 @@ export const IdentityAzureAuthForm = ({ const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; const { subscription } = useSubscription(); - + const { projectId } = useParams({ + strict: false + }); const { mutateAsync: addMutateAsync } = useAddIdentityAzureAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityAzureAuth(); const [tabValue, setTabValue] = useState(IdentityFormTab.Configuration); @@ -150,7 +153,7 @@ export const IdentityAzureAuthForm = ({ if (data) { await updateMutateAsync({ - organizationId: orgId, + ...(projectId ? { projectId } : { organizationId: orgId }), identityId, tenantId, resource, @@ -162,7 +165,7 @@ export const IdentityAzureAuthForm = ({ }); } else { await addMutateAsync({ - organizationId: orgId, + ...(projectId ? { projectId } : { organizationId: orgId }), identityId, tenantId: tenantId || "", resource: resource || "", diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm.tsx index 960d4b561..b110f5a7d 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm.tsx @@ -3,6 +3,7 @@ import { Controller, useFieldArray, useForm } from "react-hook-form"; import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useParams } from "@tanstack/react-router"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; @@ -76,7 +77,9 @@ export const IdentityGcpAuthForm = ({ const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; const { subscription } = useSubscription(); - + const { projectId } = useParams({ + strict: false + }); const { mutateAsync: addMutateAsync } = useAddIdentityGcpAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityGcpAuth(); const [tabValue, setTabValue] = useState(IdentityFormTab.Configuration); @@ -160,7 +163,7 @@ export const IdentityGcpAuthForm = ({ if (data) { await updateMutateAsync({ identityId, - organizationId: orgId, + ...(projectId ? { projectId } : { organizationId: orgId }), type, allowedServiceAccounts, allowedProjects, @@ -173,7 +176,7 @@ export const IdentityGcpAuthForm = ({ } else { await addMutateAsync({ identityId, - organizationId: orgId, + ...(projectId ? { projectId } : { organizationId: orgId }), type, allowedServiceAccounts: allowedServiceAccounts || "", allowedProjects: allowedProjects || "", @@ -191,7 +194,6 @@ export const IdentityGcpAuthForm = ({ text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`, type: "success" }); - reset(); }; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm.tsx index 10eab486d..8435bf6f4 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm.tsx @@ -4,6 +4,7 @@ import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons"; import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useParams } from "@tanstack/react-router"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; @@ -109,7 +110,9 @@ export const IdentityJwtAuthForm = ({ const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; const { subscription } = useSubscription(); - + const { projectId } = useParams({ + strict: false + }); const { mutateAsync: addMutateAsync } = useAddIdentityJwtAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityJwtAuth(); const [tabValue, setTabValue] = useState(IdentityFormTab.Configuration); @@ -227,7 +230,7 @@ export const IdentityJwtAuthForm = ({ if (data) { await updateMutateAsync({ identityId, - organizationId: orgId, + ...(projectId ? { projectId } : { organizationId: orgId }), configurationType, jwksUrl, jwksCaCert, @@ -252,7 +255,7 @@ export const IdentityJwtAuthForm = ({ boundAudiences, boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])), boundSubject, - organizationId: orgId, + ...(projectId ? { projectId } : { organizationId: orgId }), accessTokenTTL: Number(accessTokenTTL), accessTokenMaxTTL: Number(accessTokenMaxTTL), accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), @@ -266,7 +269,6 @@ export const IdentityJwtAuthForm = ({ text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`, type: "success" }); - reset(); }; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx index c4ae8bac0..4d13469c8 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx @@ -4,6 +4,7 @@ import { faInfoCircle, faPlus, faXmark } from "@fortawesome/free-solid-svg-icons import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { useQuery } from "@tanstack/react-query"; +import { useParams } from "@tanstack/react-router"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; @@ -117,7 +118,9 @@ export const IdentityKubernetesAuthForm = ({ const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; const { subscription } = useSubscription(); - + const { projectId } = useParams({ + strict: false + }); const { mutateAsync: addMutateAsync } = useAddIdentityKubernetesAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityKubernetesAuth(); const [tabValue, setTabValue] = useState(IdentityFormTab.Configuration); @@ -318,7 +321,7 @@ export const IdentityKubernetesAuthForm = ({ if (data) { await updateMutateAsync({ - organizationId: orgId, + ...(projectId ? { projectId } : { organizationId: orgId }), ...(tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Api ? { kubernetesHost: kubernetesHost || "" @@ -341,7 +344,7 @@ export const IdentityKubernetesAuthForm = ({ }); } else { await addMutateAsync({ - organizationId: orgId, + ...(projectId ? { projectId } : { organizationId: orgId }), identityId, ...(tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Api ? { diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm.tsx index a3ab70de0..213d3e2a6 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm.tsx @@ -3,6 +3,7 @@ import { Controller, useFieldArray, useForm } from "react-hook-form"; import { faPlus, faQuestionCircle, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useParams } from "@tanstack/react-router"; import ms from "ms"; import { z } from "zod"; @@ -168,7 +169,9 @@ export const IdentityLdapAuthForm = ({ const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; const { subscription } = useSubscription(); - + const { projectId } = useParams({ + strict: false + }); const { mutateAsync: addMutateAsync } = useAddIdentityLdapAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityLdapAuth(); const [tabValue, setTabValue] = useState(IdentityFormTab.Configuration); @@ -345,7 +348,7 @@ export const IdentityLdapAuthForm = ({ ms(`${lockoutCounterResetValue}${lockoutCounterResetUnit}`) / 1000; const basePayload = { - organizationId: orgId, + ...(projectId ? { projectId } : { organizationId: orgId }), identityId, searchFilter, ldapCaCertificate, diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOciAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOciAuthForm.tsx index 3ebfceb4a..ee6160d41 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOciAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOciAuthForm.tsx @@ -3,6 +3,7 @@ import { Controller, useFieldArray, useForm } from "react-hook-form"; import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useParams } from "@tanstack/react-router"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; @@ -84,7 +85,9 @@ export const IdentityOciAuthForm = ({ const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; const { subscription } = useSubscription(); - + const { projectId } = useParams({ + strict: false + }); const { mutateAsync: addMutateAsync } = useAddIdentityOciAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityOciAuth(); const [tabValue, setTabValue] = useState(IdentityFormTab.Configuration); @@ -156,7 +159,7 @@ export const IdentityOciAuthForm = ({ if (data) { await updateMutateAsync({ - organizationId: orgId, + ...(projectId ? { projectId } : { organizationId: orgId }), tenancyOcid, allowedUsernames, identityId, @@ -167,7 +170,7 @@ export const IdentityOciAuthForm = ({ }); } else { await addMutateAsync({ - organizationId: orgId, + ...(projectId ? { projectId } : { organizationId: orgId }), identityId, tenancyOcid, allowedUsernames: allowedUsernames || undefined, @@ -184,7 +187,6 @@ export const IdentityOciAuthForm = ({ text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`, type: "success" }); - reset(); }; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx index 7503e6f45..e30e2b37f 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx @@ -4,6 +4,7 @@ import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons"; import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useParams } from "@tanstack/react-router"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; @@ -96,7 +97,9 @@ export const IdentityOidcAuthForm = ({ const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; const { subscription } = useSubscription(); - + const { projectId } = useParams({ + strict: false + }); const { mutateAsync: addMutateAsync } = useAddIdentityOidcAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityOidcAuth(); const [tabValue, setTabValue] = useState(IdentityFormTab.Configuration); @@ -211,7 +214,7 @@ export const IdentityOidcAuthForm = ({ if (data) { await updateMutateAsync({ identityId, - organizationId: orgId, + ...(projectId ? { projectId } : { organizationId: orgId }), oidcDiscoveryUrl, caCert, boundIssuer, @@ -238,7 +241,7 @@ export const IdentityOidcAuthForm = ({ ? Object.fromEntries(claimMetadataMapping.map((entry) => [entry.key, entry.value])) : undefined, boundSubject, - organizationId: orgId, + ...(projectId ? { projectId } : { organizationId: orgId }), accessTokenTTL: Number(accessTokenTTL), accessTokenMaxTTL: Number(accessTokenMaxTTL), accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), @@ -252,7 +255,6 @@ export const IdentityOidcAuthForm = ({ text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`, type: "success" }); - reset(); }; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx index 8380836cb..674302459 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx @@ -1,10 +1,18 @@ -import { faLink, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { faChevronDown, faLink, faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; -import { Button, DeleteActionModal, Modal, ModalContent } from "@app/components/v2"; +import { + Button, + DeleteActionModal, + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger, + Modal, + ModalContent +} from "@app/components/v2"; import { DocumentationLinkBadge } from "@app/components/v3"; import { OrgPermissionIdentityActions, @@ -14,17 +22,17 @@ import { } from "@app/context"; import { OrgPermissionMachineIdentityAuthTemplateActions } from "@app/context/OrgPermissionContext/types"; import { withPermission } from "@app/hoc"; -import { useDeleteIdentity } from "@app/hooks/api"; +import { useDeleteOrgIdentity } from "@app/hooks/api"; import { useDeleteIdentityAuthTemplate } from "@app/hooks/api/identityAuthTemplates"; import { usePopUp } from "@app/hooks/usePopUp"; import { IdentityAuthTemplateModal } from "./IdentityAuthTemplateModal"; import { IdentityAuthTemplatesTable } from "./IdentityAuthTemplatesTable"; -import { IdentityLinkForm } from "./IdentityLinkForm"; -import { IdentityModal } from "./IdentityModal"; import { IdentityTable } from "./IdentityTable"; import { IdentityTokenAuthTokenModal } from "./IdentityTokenAuthTokenModal"; import { MachineAuthTemplateUsagesModal } from "./MachineAuthTemplateUsagesModal"; +import { OrgIdentityLinkForm } from "./OrgIdentityLinkForm"; +import { OrgIdentityModal } from "./OrgIdentityModal"; export const IdentitySection = withPermission( () => { @@ -32,7 +40,7 @@ export const IdentitySection = withPermission( const { currentOrg, isSubOrganization } = useOrganization(); const orgId = currentOrg?.id || ""; - const { mutateAsync: deleteMutateAsync } = useDeleteIdentity(); + const { mutateAsync: deleteMutateAsync } = useDeleteOrgIdentity(); const { mutateAsync: deleteTemplateMutateAsync } = useDeleteIdentityAuthTemplate(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "identity", @@ -46,7 +54,8 @@ export const IdentitySection = withPermission( "editTemplate", "deleteTemplate", "viewUsages", - "linkIdentity" + "linkIdentity", + "addOptions" ] as const); const isMoreIdentitiesAllowed = subscription?.identityLimit @@ -58,7 +67,7 @@ export const IdentitySection = withPermission( const onDeleteIdentitySubmit = async (identityId: string) => { await deleteMutateAsync({ identityId, - organizationId: orgId + orgId }); createNotification({ @@ -91,50 +100,70 @@ export const IdentitySection = withPermission(

Identities

- {isSubOrganization && ( +
{(isAllowed) => ( )} - )} - - {(isAllowed) => ( - + + + + + + {(isAllowed) => ( + + )} + + + )} - +
@@ -173,7 +202,7 @@ export const IdentitySection = withPermission( - + - handlePopUpClose("linkIdentity")} /> + handlePopUpClose("linkIdentity")} /> diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx index d202b909a..9d590bb3a 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx @@ -2,7 +2,6 @@ import { useCallback, useState } from "react"; import { faArrowDown, faArrowUp, - faBuilding, faCheckCircle, faChevronRight, faEdit, @@ -46,6 +45,7 @@ import { Tooltip, Tr } from "@app/components/v2"; +import { Badge, OrgIcon, SubOrgIcon } from "@app/components/v3"; import { OrgPermissionIdentityActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { getUserTablePreference, @@ -56,8 +56,8 @@ import { usePagination, useResetPageHelper } from "@app/hooks"; import { identityAuthToNameMap, useGetOrgRoles, - useSearchIdentities, - useUpdateIdentity + useSearchOrgIdentityMemberships, + useUpdateOrgIdentity } from "@app/hooks/api"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrgIdentityOrderBy } from "@app/hooks/api/organization/types"; @@ -110,9 +110,9 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { const organizationId = currentOrg?.id || ""; - const { mutateAsync: updateMutateAsync } = useUpdateIdentity(); + const { mutateAsync: updateMutateAsync } = useUpdateOrgIdentity(); - const { data, isPending, isFetching } = useSearchIdentities({ + const { data, isPending, isFetching } = useSearchOrgIdentityMemberships({ offset, limit, orderDirection, @@ -357,10 +357,19 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { {isSubOrganization && ( -

- - {currentOrg.id === orgId ? "Sub Organization" : "Root Organization"} -

+ + {currentOrg.id === orgId ? ( + <> + + Sub-Organization + + ) : ( + <> + + Root Organization + + )} + )} diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTlsCertAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTlsCertAuthForm.tsx index 5666be39f..dc615b111 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTlsCertAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTlsCertAuthForm.tsx @@ -3,6 +3,7 @@ import { Controller, useFieldArray, useForm } from "react-hook-form"; import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useParams } from "@tanstack/react-router"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; @@ -71,7 +72,9 @@ export const IdentityTlsCertAuthForm = ({ const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; const { subscription } = useSubscription(); - + const { projectId } = useParams({ + strict: false + }); const { mutateAsync: addMutateAsync } = useAddIdentityTlsCertAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityTlsCertAuth(); const [tabValue, setTabValue] = useState(IdentityFormTab.Configuration); @@ -141,7 +144,7 @@ export const IdentityTlsCertAuthForm = ({ if (data) { await updateMutateAsync({ - organizationId: orgId, + ...(projectId ? { projectId } : { organizationId: orgId }), caCertificate, allowedCommonNames: allowedCommonNames || null, identityId, @@ -152,7 +155,7 @@ export const IdentityTlsCertAuthForm = ({ }); } else { await addMutateAsync({ - organizationId: orgId, + ...(projectId ? { projectId } : { organizationId: orgId }), identityId, caCertificate, allowedCommonNames: allowedCommonNames || undefined, @@ -169,7 +172,6 @@ export const IdentityTlsCertAuthForm = ({ text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`, type: "success" }); - reset(); }; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthForm.tsx index af2404c5c..34f5116d8 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthForm.tsx @@ -3,6 +3,7 @@ import { Controller, useFieldArray, useForm } from "react-hook-form"; import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useParams } from "@tanstack/react-router"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; @@ -70,7 +71,9 @@ export const IdentityTokenAuthForm = ({ const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; const { subscription } = useSubscription(); - + const { projectId } = useParams({ + strict: false + }); const { mutateAsync: addMutateAsync } = useAddIdentityTokenAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityTokenAuth(); const [tabValue, setTabValue] = useState(IdentityFormTab.Configuration); @@ -134,7 +137,7 @@ export const IdentityTokenAuthForm = ({ if (data) { await updateMutateAsync({ - organizationId: orgId, + ...(projectId ? { projectId } : { organizationId: orgId }), identityId, accessTokenTTL: Number(accessTokenTTL), accessTokenMaxTTL: Number(accessTokenMaxTTL), @@ -143,7 +146,7 @@ export const IdentityTokenAuthForm = ({ }); } else { await addMutateAsync({ - organizationId: orgId, + ...(projectId ? { projectId } : { organizationId: orgId }), identityId, accessTokenTTL: Number(accessTokenTTL), accessTokenMaxTTL: Number(accessTokenMaxTTL), diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx index d4de4bd0e..c7990f55a 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx @@ -3,6 +3,7 @@ import { Controller, useFieldArray, useForm } from "react-hook-form"; import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useParams } from "@tanstack/react-router"; import ms from "ms"; import { z } from "zod"; @@ -105,6 +106,9 @@ export const IdentityUniversalAuthForm = ({ identityId, isUpdate }: Props) => { + const { projectId } = useParams({ + strict: false + }); const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; const { subscription } = useSubscription(); @@ -232,7 +236,7 @@ export const IdentityUniversalAuthForm = ({ if (data) { // update universal auth configuration await updateMutateAsync({ - organizationId: orgId, + ...(projectId ? { projectId } : { organizationId: orgId }), identityId, clientSecretTrustedIps, accessTokenTTL: Number(accessTokenTTL), @@ -249,7 +253,7 @@ export const IdentityUniversalAuthForm = ({ // create new universal auth configuration await addMutateAsync({ - organizationId: orgId, + ...(projectId ? { projectId } : { organizationId: orgId }), identityId, clientSecretTrustedIps, accessTokenTTL: Number(accessTokenTTL), @@ -270,7 +274,6 @@ export const IdentityUniversalAuthForm = ({ text: `Successfully ${isUpdate ? "updated" : "created"} auth method`, type: "success" }); - reset(); }; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLinkForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/OrgIdentityLinkForm.tsx similarity index 83% rename from frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLinkForm.tsx rename to frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/OrgIdentityLinkForm.tsx index 545aea5aa..24406382e 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLinkForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/OrgIdentityLinkForm.tsx @@ -1,13 +1,15 @@ import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; import { useNavigate } from "@tanstack/react-router"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FilterableSelect, FormControl } from "@app/components/v2"; import { useOrganization } from "@app/context"; -import { useGetAvailableOrgIdentities, useGetOrgRoles } from "@app/hooks/api"; +import { useGetOrgRoles } from "@app/hooks/api"; import { useCreateOrgIdentityMembership } from "@app/hooks/api/orgIdentityMembership"; +import { orgIdentityMembershipQuery } from "@app/hooks/api/orgIdentityMembership/queries"; const schema = z .object({ @@ -22,15 +24,26 @@ type Props = { onClose: () => void; }; -export const IdentityLinkForm = ({ onClose }: Props) => { +export const OrgIdentityLinkForm = ({ onClose }: Props) => { const navigate = useNavigate(); const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; const { data: roles } = useGetOrgRoles(orgId); + // const [searchValue, setSearchValue] = useState(""); + // + // const [debouncedSearchValue] = useDebounce(searchValue); + const { mutateAsync: createMutateAsync } = useCreateOrgIdentityMembership(); - const { data: rootOrgIdentities, isPending: isRootOrgLoading } = useGetAvailableOrgIdentities(); + + // TODO: name filter needs to be implemented on backend + const { data: rootOrgIdentities, isPending: isRootOrgLoading } = useQuery({ + ...orgIdentityMembershipQuery.listAvailable({ + // identityName: debouncedSearchValue + }), + placeholderData: (prev) => prev + }); const { control, @@ -69,6 +82,7 @@ export const IdentityLinkForm = ({ onClose }: Props) => { value={value} onChange={onChange} placeholder="Select identity..." + // onInputChange={setSearchValue} options={rootOrgIdentities} getOptionValue={(option) => option.id} getOptionLabel={(option) => option.name} @@ -94,7 +108,6 @@ export const IdentityLinkForm = ({ onClose }: Props) => { placeholder="Select role..." getOptionValue={(option) => option.slug} getOptionLabel={(option) => option.name} - // menuPortalTarget={document.body} /> )} diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/OrgIdentityModal.tsx similarity index 97% rename from frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx rename to frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/OrgIdentityModal.tsx index 09e779fa9..02101c191 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/OrgIdentityModal.tsx @@ -20,7 +20,7 @@ import { } from "@app/components/v2"; import { useOrganization } from "@app/context"; import { findOrgMembershipRole } from "@app/helpers/roles"; -import { useCreateIdentity, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api"; +import { useCreateOrgIdentity, useGetOrgRoles, useUpdateOrgIdentity } from "@app/hooks/api"; import { useAddIdentityUniversalAuth } from "@app/hooks/api/identities"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -47,7 +47,7 @@ type Props = { handlePopUpToggle: (popUpName: keyof UsePopUpState<["identity"]>, state?: boolean) => void; }; -export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { +export const OrgIdentityModal = ({ popUp, handlePopUpToggle }: Props) => { const navigate = useNavigate(); const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; @@ -55,8 +55,8 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { const { data: roles } = useGetOrgRoles(orgId); const isOrgIdentity = popUp?.identity?.data ? orgId === popUp?.identity?.data?.orgId : true; - const { mutateAsync: createMutateAsync } = useCreateIdentity(); - const { mutateAsync: updateMutateAsync } = useUpdateIdentity(); + const { mutateAsync: createMutateAsync } = useCreateOrgIdentity(); + const { mutateAsync: updateMutateAsync } = useUpdateOrgIdentity(); const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth(); const { diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx index 33753da39..db682e5ab 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx @@ -10,13 +10,13 @@ import { OrgPermissionCan } from "@app/components/permissions"; import { DeleteActionModal, PageHeader } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; import { OrgPermissionIdentityActions, OrgPermissionSubjects, useOrganization } from "@app/context"; -import { useDeleteIdentity, useGetIdentityById } from "@app/hooks/api"; +import { useDeleteOrgIdentity, useGetOrgIdentityMembershipById } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; import { ViewIdentityAuthModal } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal"; import { OrgAccessControlTabSections } from "@app/types/org"; import { IdentityAuthMethodModal } from "../AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal"; -import { IdentityModal } from "../AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal"; +import { OrgIdentityModal } from "../AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/OrgIdentityModal"; import { IdentityAuthenticationSection, IdentityDetailsSection, @@ -31,8 +31,8 @@ const Page = () => { const identityId = params.identityId as string; const { currentOrg, isSubOrganization } = useOrganization(); const orgId = currentOrg?.id || ""; - const { data } = useGetIdentityById(identityId); - const { mutateAsync: deleteIdentity } = useDeleteIdentity(); + const { data } = useGetOrgIdentityMembershipById(identityId); + const { mutateAsync: deleteIdentity } = useDeleteOrgIdentity(); const isAuthHidden = orgId !== data?.identity?.orgId; const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ @@ -46,7 +46,7 @@ const Page = () => { const onDeleteIdentitySubmit = async (id: string) => { await deleteIdentity({ identityId: id, - organizationId: orgId + orgId }); createNotification({ @@ -100,7 +100,7 @@ const Page = () => { )} - + { - const { data, refetch } = useGetIdentityById(identityId); + const { data, refetch } = useGetOrgIdentityMembershipById(identityId); return data ? (
diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityAuthenticationSection/IdentityClientSecrets.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityAuthenticationSection/IdentityClientSecrets.tsx index c1bd0adf6..10129be26 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityAuthenticationSection/IdentityClientSecrets.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityAuthenticationSection/IdentityClientSecrets.tsx @@ -7,9 +7,9 @@ import { Button, IconButton, Tooltip } from "@app/components/v2"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context"; import { useTimedReset } from "@app/hooks"; import { - useGetIdentityById, useGetIdentityUniversalAuth, - useGetIdentityUniversalAuthClientSecrets + useGetIdentityUniversalAuthClientSecrets, + useGetOrgIdentityMembershipById } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -30,7 +30,7 @@ export const IdentityClientSecrets = ({ identityId, handlePopUpOpen }: Props) => initialState: "Copy Client ID to clipboard" }); - const { data } = useGetIdentityById(identityId); + const { data } = useGetOrgIdentityMembershipById(identityId); const { data: identityUniversalAuth } = useGetIdentityUniversalAuth(identityId); const { data: clientSecrets } = useGetIdentityUniversalAuthClientSecrets(identityId); return ( diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityAuthenticationSection/IdentityTokens.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityAuthenticationSection/IdentityTokens.tsx index 118fea015..d0c562bc9 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityAuthenticationSection/IdentityTokens.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityAuthenticationSection/IdentityTokens.tsx @@ -11,7 +11,7 @@ import { IconButton, Tooltip } from "@app/components/v2"; -import { useGetIdentityById, useGetIdentityTokensTokenAuth } from "@app/hooks/api"; +import { useGetIdentityTokensTokenAuth, useGetOrgIdentityMembershipById } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; type Props = { @@ -23,7 +23,7 @@ type Props = { }; export const IdentityTokens = ({ identityId, handlePopUpOpen }: Props) => { - const { data } = useGetIdentityById(identityId); + const { data } = useGetOrgIdentityMembershipById(identityId); const { data: tokens } = useGetIdentityTokensTokenAuth(identityId); return (
diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx index 30429d48d..e3241d222 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx @@ -23,7 +23,7 @@ import { } from "@app/components/v2"; import { OrgPermissionIdentityActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { useTimedReset } from "@app/hooks"; -import { identityAuthToNameMap, useGetIdentityById } from "@app/hooks/api"; +import { identityAuthToNameMap, useGetOrgIdentityMembershipById } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; type Props = { @@ -41,7 +41,7 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen, isOrgIdent }); const { isSubOrganization } = useOrganization(); - const { data } = useGetIdentityById(identityId); + const { data } = useGetOrgIdentityMembershipById(identityId); return data ? (
diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityAddToProjectModal.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityAddToProjectModal.tsx index 2f407142a..5c4dcd6dd 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityAddToProjectModal.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityAddToProjectModal.tsx @@ -14,7 +14,7 @@ import { } from "@app/components/v2"; import { useOrganization } from "@app/context"; import { - useAddIdentityToWorkspace, + useCreateProjectIdentityMembership, useGetIdentityProjectMemberships, useGetProjectRoles, useGetUserProjects, @@ -45,7 +45,7 @@ type Props = { const Content = ({ identityId, handlePopUpToggle }: Omit) => { const { currentOrg } = useOrganization(); const { data: workspaces = [] } = useGetUserProjects(); - const { mutateAsync: addIdentityToWorkspace } = useAddIdentityToWorkspace(); + const { mutateAsync: addIdentityToWorkspace } = useCreateProjectIdentityMembership(); const { control, diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectRow.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectRow.tsx index 1622962ea..431986990 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectRow.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectRow.tsx @@ -9,7 +9,7 @@ import { IconButton, Td, Tooltip, Tr } from "@app/components/v2"; import { getProjectBaseURL } from "@app/helpers/project"; import { formatProjectRoleName } from "@app/helpers/roles"; import { useGetUserProjects } from "@app/hooks/api"; -import { IdentityMembership } from "@app/hooks/api/identities/types"; +import { IdentityProjectMembership } from "@app/hooks/api/identities/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; export enum TabSections { @@ -20,7 +20,7 @@ export enum TabSections { } type Props = { - membership: IdentityMembership; + membership: IdentityProjectMembership; handlePopUpOpen: ( popUpName: keyof UsePopUpState<["removeIdentityFromProject"]>, data?: object diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectsSection.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectsSection.tsx index 8d67ef6a4..542ed1486 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectsSection.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectsSection.tsx @@ -3,7 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createNotification } from "@app/components/notifications"; import { DeleteActionModal, IconButton } from "@app/components/v2"; -import { useDeleteIdentityFromWorkspace } from "@app/hooks/api"; +import { useDeleteProjectIdentityMembership } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; import { IdentityAddToProjectModal } from "./IdentityAddToProjectModal"; @@ -14,7 +14,7 @@ type Props = { }; export const IdentityProjectsSection = ({ identityId }: Props) => { - const { mutateAsync: deleteMutateAsync } = useDeleteIdentityFromWorkspace(); + const { mutateAsync: deleteMutateAsync } = useDeleteProjectIdentityMembership(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "addIdentityToProject", diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityAuthLockoutFields.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityAuthLockoutFields.tsx index 4ec5871a2..d1bca3787 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityAuthLockoutFields.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityAuthLockoutFields.tsx @@ -1,11 +1,18 @@ import { useState } from "react"; +import { subject } from "@casl/ability"; import { UseMutationResult } from "@tanstack/react-query"; +import { useParams } from "@tanstack/react-router"; import ms from "ms"; import { createNotification } from "@app/components/notifications"; -import { OrgPermissionCan } from "@app/components/permissions"; +import { VariablePermissionCan } from "@app/components/permissions"; import { Button } from "@app/components/v2"; -import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context"; +import { + OrgPermissionIdentityActions, + OrgPermissionSubjects, + ProjectPermissionIdentityActions, + ProjectPermissionSub +} from "@app/context"; import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay"; @@ -31,7 +38,11 @@ export const LockoutFields = ({ const [lockedOutState, setLockedOutState] = useState(lockedOut); - const clearLockouts = async () => { + const { projectId } = useParams({ + strict: false + }); + + async function clearLockouts() { const deleted = await mutateAsync({ identityId }); createNotification({ text: `Successfully cleared ${deleted} lockout${deleted === 1 ? "" : "s"}`, @@ -39,13 +50,23 @@ export const LockoutFields = ({ }); setLockedOutState(false); onResetAllLockouts(); - }; + } return ( <>
Lockout Options - + {(isAllowed) => (
{data.lockoutThreshold} diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityTokenAuthTokensTable.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityTokenAuthTokensTable.tsx index cc11d9d10..d0d0b0fcd 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityTokenAuthTokensTable.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityTokenAuthTokensTable.tsx @@ -1,10 +1,12 @@ import { useState } from "react"; +import { subject } from "@casl/ability"; import { faBan, faEdit, faKey, faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useParams } from "@tanstack/react-router"; import { format } from "date-fns"; import { createNotification } from "@app/components/notifications"; -import { OrgPermissionCan } from "@app/components/permissions"; +import { VariablePermissionCan } from "@app/components/permissions"; import { Button, DeleteActionModal, @@ -20,7 +22,12 @@ import { Tooltip, Tr } from "@app/components/v2"; -import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context"; +import { + OrgPermissionIdentityActions, + OrgPermissionSubjects, + ProjectPermissionIdentityActions, + ProjectPermissionSub +} from "@app/context"; import { usePopUp } from "@app/hooks"; import { useRevokeIdentityTokenAuthToken } from "@app/hooks/api"; import { IdentityAccessToken } from "@app/hooks/api/identities/types"; @@ -37,6 +44,10 @@ export const IdentityTokenAuthTokensTable = ({ tokens, identityId }: Props) => { "revokeToken" ] as const); + const { projectId } = useParams({ + strict: false + }); + const [page, setPage] = useState(1); const [perPage, setPerPage] = useState(5); @@ -68,7 +79,17 @@ export const IdentityTokenAuthTokensTable = ({ tokens, identityId }: Props) => {
Access Tokens - + {(isAllowed) => ( )} - +
@@ -132,9 +153,20 @@ export const IdentityTokenAuthTokensTable = ({ tokens, identityId }: Props) => { diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityUniversalAuthClientSecretsTable.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityUniversalAuthClientSecretsTable.tsx index 017f30719..cfecb5f3c 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityUniversalAuthClientSecretsTable.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityUniversalAuthClientSecretsTable.tsx @@ -1,10 +1,12 @@ import { useState } from "react"; +import { subject } from "@casl/ability"; import { faKey, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useParams } from "@tanstack/react-router"; import { format } from "date-fns"; import { createNotification } from "@app/components/notifications"; -import { OrgPermissionCan } from "@app/components/permissions"; +import { VariablePermissionCan } from "@app/components/permissions"; import { Button, DeleteActionModal, @@ -20,7 +22,12 @@ import { Tooltip, Tr } from "@app/components/v2"; -import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context"; +import { + OrgPermissionIdentityActions, + OrgPermissionSubjects, + ProjectPermissionIdentityActions, + ProjectPermissionSub +} from "@app/context"; import { usePopUp } from "@app/hooks"; import { useRevokeIdentityUniversalAuthClientSecret } from "@app/hooks/api"; import { ClientSecretData } from "@app/hooks/api/identities/types"; @@ -37,6 +44,10 @@ export const IdentityUniversalAuthClientSecretsTable = ({ clientSecrets, identit "clientSecret" ] as const); + const { projectId } = useParams({ + strict: false + }); + const [page, setPage] = useState(1); const [perPage, setPerPage] = useState(5); @@ -60,7 +71,17 @@ export const IdentityUniversalAuthClientSecretsTable = ({ clientSecrets, identit
Client Secrets - + {(isAllowed) => (
- {(isAllowed) => ( @@ -155,11 +187,22 @@ export const IdentityTokenAuthTokensTable = ({ tokens, identityId }: Props) => { )} - + {!isAccessTokenRevoked && ( - {(isAllowed) => ( @@ -181,7 +224,7 @@ export const IdentityTokenAuthTokensTable = ({ tokens, identityId }: Props) => { )} - + )}
@@ -120,9 +141,20 @@ export const IdentityUniversalAuthClientSecretsTable = ({ clientSecrets, identit {expiresAt ? format(expiresAt, "yyyy-MM-dd") : "-"} ); diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAliCloudAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAliCloudAuthContent.tsx index 6d6ca6cdc..5249d7f65 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAliCloudAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAliCloudAuthContent.tsx @@ -49,6 +49,7 @@ export const ViewIdentityAliCloudAuthContent = ({ handlePopUpOpen("identityAuthMethod")} onDelete={onDelete} + identityId={identityId} > {data.accessTokenTTL} diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx index be5ae2cc9..5c12ddd4c 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx @@ -1,3 +1,5 @@ +import { useParams } from "@tanstack/react-router"; + import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { createNotification } from "@app/components/notifications"; import { DeleteActionModal, Modal, ModalContent } from "@app/components/v2"; @@ -46,8 +48,7 @@ type Props = { type TRevokeOptions = { identityId: string; - organizationId: string; -}; +} & ({ projectId: string } | { organizationId: string }); export const Content = ({ identityId, @@ -61,7 +62,9 @@ export const Content = ({ >) => { const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; - + const { projectId } = useParams({ + strict: false + }); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ "revokeAuthMethod", "upgradePlan", @@ -142,7 +145,11 @@ export const Content = ({ const handleDeleteAuthMethod = async () => { await revokeMethod({ identityId, - organizationId: orgId + ...(projectId + ? { projectId } + : { + organizationId: orgId + }) }); createNotification({ diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAwsAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAwsAuthContent.tsx index e7130d8a2..dde9f1d2a 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAwsAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAwsAuthContent.tsx @@ -46,6 +46,7 @@ export const ViewIdentityAwsAuthContent = ({ handlePopUpOpen("identityAuthMethod")} onDelete={onDelete} + identityId={identityId} > {data.accessTokenTTL} diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAzureAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAzureAuthContent.tsx index ea9ee09ea..82e4c69e0 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAzureAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAzureAuthContent.tsx @@ -46,6 +46,7 @@ export const ViewIdentityAzureAuthContent = ({ handlePopUpOpen("identityAuthMethod")} onDelete={onDelete} + identityId={identityId} > {data.accessTokenTTL} diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper.tsx index 7ea2c8ba3..f3a4cb369 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper.tsx @@ -1,8 +1,10 @@ import { ReactNode } from "react"; +import { subject } from "@casl/ability"; import { faChevronDown, faEdit, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useParams } from "@tanstack/react-router"; -import { OrgPermissionCan } from "@app/components/permissions"; +import { VariablePermissionCan } from "@app/components/permissions"; import { Button, DropdownMenu, @@ -10,15 +12,25 @@ import { DropdownMenuItem, DropdownMenuTrigger } from "@app/components/v2"; -import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context"; +import { + OrgPermissionIdentityActions, + OrgPermissionSubjects, + ProjectPermissionIdentityActions, + ProjectPermissionSub +} from "@app/context"; type Props = { children: ReactNode; onEdit: VoidFunction; onDelete: VoidFunction; + identityId: string; }; -export const ViewIdentityContentWrapper = ({ children, onDelete, onEdit }: Props) => { +export const ViewIdentityContentWrapper = ({ children, onDelete, onEdit, identityId }: Props) => { + const { projectId } = useParams({ + strict: false + }); + return (
@@ -36,9 +48,20 @@ export const ViewIdentityContentWrapper = ({ children, onDelete, onEdit }: Props - {(isAllowed) => ( )} - - + {(isAllowed) => ( )} - +
diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityGcpAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityGcpAuthContent.tsx index 4008850e9..296b4d764 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityGcpAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityGcpAuthContent.tsx @@ -46,6 +46,7 @@ export const ViewIdentityGcpAuthContent = ({ handlePopUpOpen("identityAuthMethod")} onDelete={onDelete} + identityId={identityId} > {data.accessTokenTTL} diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityJwtAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityJwtAuthContent.tsx index f068a2b7f..7fb81d2e1 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityJwtAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityJwtAuthContent.tsx @@ -49,6 +49,7 @@ export const ViewIdentityJwtAuthContent = ({ handlePopUpOpen("identityAuthMethod")} onDelete={onDelete} + identityId={identityId} > {data.accessTokenTTL} diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityKubernetesAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityKubernetesAuthContent.tsx index 62c2b210e..02b0e0c88 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityKubernetesAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityKubernetesAuthContent.tsx @@ -59,6 +59,7 @@ export const ViewIdentityKubernetesAuthContent = ({ handlePopUpOpen("identityAuthMethod")} onDelete={onDelete} + identityId={identityId} > {data.accessTokenTTL} diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityLdapAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityLdapAuthContent.tsx index c3ea58db4..b6db96c41 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityLdapAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityLdapAuthContent.tsx @@ -52,6 +52,7 @@ export const ViewIdentityLdapAuthContent = ({ handlePopUpOpen("identityAuthMethod")} onDelete={onDelete} + identityId={identityId} > {data.accessTokenTTL} diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOciAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOciAuthContent.tsx index fb3bd4fa8..c577c01e0 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOciAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOciAuthContent.tsx @@ -46,6 +46,7 @@ export const ViewIdentityOciAuthContent = ({ handlePopUpOpen("identityAuthMethod")} onDelete={onDelete} + identityId={identityId} > {data.accessTokenTTL} diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOidcAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOidcAuthContent.tsx index 19130f914..dc7e5b985 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOidcAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOidcAuthContent.tsx @@ -48,6 +48,7 @@ export const ViewIdentityOidcAuthContent = ({ handlePopUpOpen("identityAuthMethod")} onDelete={onDelete} + identityId={identityId} > {data.accessTokenTTL} diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityTlsCertAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityTlsCertAuthContent.tsx index 1e0878add..03c4989a6 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityTlsCertAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityTlsCertAuthContent.tsx @@ -51,6 +51,7 @@ export const ViewIdentityTlsCertAuthContent = ({ handlePopUpOpen("identityAuthMethod")} onDelete={onDelete} + identityId={identityId} > {data.accessTokenTTL} diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityTokenAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityTokenAuthContent.tsx index 516289f74..d42f9d2ca 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityTokenAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityTokenAuthContent.tsx @@ -49,6 +49,7 @@ export const ViewIdentityTokenAuthContent = ({ handlePopUpOpen("identityAuthMethod")} onDelete={onDelete} + identityId={identityId} > {data.accessTokenTTL} diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityUniversalAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityUniversalAuthContent.tsx index 4d7b95087..e9840a28c 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityUniversalAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityUniversalAuthContent.tsx @@ -66,6 +66,7 @@ export const ViewIdentityUniversalAuthContent = ({ handlePopUpOpen("identityAuthMethod")} onDelete={onDelete} + identityId={identityId} > {Number(data.accessTokenPeriod) > 0 ? ( diff --git a/frontend/src/pages/project/AccessControlPage/components/IdentityTab/IdentityTab.tsx b/frontend/src/pages/project/AccessControlPage/components/IdentityTab/IdentityTab.tsx index 48e01e2aa..a8e3e640d 100644 --- a/frontend/src/pages/project/AccessControlPage/components/IdentityTab/IdentityTab.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/IdentityTab/IdentityTab.tsx @@ -2,16 +2,17 @@ import { subject } from "@casl/ability"; import { faArrowDown, faArrowUp, + faChevronDown, faCircleXmark, faClock, faEllipsisV, + faLink, faMagnifyingGlass, faPlus, faServer } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNavigate } from "@tanstack/react-router"; -import { format } from "date-fns"; import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; @@ -42,8 +43,19 @@ import { Tooltip, Tr } from "@app/components/v2"; -import { DocumentationLinkBadge } from "@app/components/v3"; -import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; +import { + Badge, + DocumentationLinkBadge, + OrgIcon, + ProjectIcon, + SubOrgIcon +} from "@app/components/v3"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useOrganization, + useProject +} from "@app/context"; import { getProjectBaseURL } from "@app/helpers/project"; import { formatProjectRoleName } from "@app/helpers/roles"; import { @@ -53,12 +65,17 @@ import { } from "@app/helpers/userTablePreferences"; import { withProjectPermission } from "@app/hoc"; import { usePagination, useResetPageHelper } from "@app/hooks"; -import { useDeleteIdentityFromWorkspace, useGetWorkspaceIdentityMemberships } from "@app/hooks/api"; +import { + useDeleteProjectIdentity, + useDeleteProjectIdentityMembership, + useListProjectIdentityMemberships +} from "@app/hooks/api"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { ProjectIdentityOrderBy } from "@app/hooks/api/projects/types"; import { usePopUp } from "@app/hooks/usePopUp"; +import { ProjectIdentityModal } from "@app/pages/project/AccessControlPage/components/IdentityTab/components/ProjectIdentityModal"; -import { IdentityModal } from "./components/IdentityModal"; +import { ProjectLinkIdentityModal } from "./components/ProjectLinkIdentityModal"; const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2; @@ -66,6 +83,7 @@ export const IdentityTab = withProjectPermission( () => { const { currentProject, projectId } = useProject(); const navigate = useNavigate(); + const { isSubOrganization } = useOrganization(); const { offset, @@ -90,7 +108,7 @@ export const IdentityTab = withProjectPermission( setUserTablePreference("projectIdentityTable", PreferenceKey.PerPage, newPerPage); }; - const { data, isPending, isFetching } = useGetWorkspaceIdentityMemberships( + const { data, isPending, isFetching } = useListProjectIdentityMemberships( { projectId, offset, @@ -110,24 +128,39 @@ export const IdentityTab = withProjectPermission( setPage }); - const { mutateAsync: deleteMutateAsync } = useDeleteIdentityFromWorkspace(); + const { mutateAsync: deleteMembershipMutateAsync } = useDeleteProjectIdentityMembership(); + const { mutateAsync: deleteProjectIdentity } = useDeleteProjectIdentity(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ - "identity", + "createIdentity", + "linkIdentity", "deleteIdentity", - "upgradePlan" + "upgradePlan", + "addOptions" ] as const); - const onRemoveIdentitySubmit = async (identityId: string) => { - await deleteMutateAsync({ - identityId, - projectId - }); + const onRemoveIdentitySubmit = async (identityId: string, isProjectIdentity: boolean) => { + if (isProjectIdentity) { + await deleteProjectIdentity({ + identityId, + projectId + }); - createNotification({ - text: "Successfully removed identity from project", - type: "success" - }); + createNotification({ + text: "Successfully deleted project identity", + type: "success" + }); + } else { + await deleteMembershipMutateAsync({ + identityId, + projectId + }); + + createNotification({ + text: "Successfully removed identity from project", + type: "success" + }); + } handlePopUpClose("deleteIdentity"); }; @@ -151,22 +184,55 @@ export const IdentityTab = withProjectPermission(

Identities

- - {(isAllowed) => ( - - )} - +
+ + {(isAllowed) => ( + + )} + + handlePopUpToggle("addOptions", isOpen)} + > + + + + + + {(isAllowed) => ( + + )} + + + +
- + @@ -213,9 +279,8 @@ export const IdentityTab = withProjectPermission( data.identityMemberships.length > 0 && data.identityMemberships.map((identityMember) => { const { - identity: { id, name }, - roles, - createdAt + identity: { id, name, projectId: identityProjectId }, + roles } = identityMember; return ( - +
- {(isAllowed) => ( @@ -143,7 +175,7 @@ export const IdentityUniversalAuthClientSecretsTable = ({ clientSecrets, identit )} - +
RoleAdded onManaged by {isFetching ? : null}
{format(new Date(createdAt), "yyyy-MM-dd")} + + {/* eslint-disable-next-line no-nested-ternary */} + {identityProjectId ? ( + <> + + Project + + ) : isSubOrganization ? ( + <> + + Sub-Organization + + ) : ( + <> + + Organization + + )} + + @@ -369,11 +454,14 @@ export const IdentityTab = withProjectPermission( evt.preventDefault(); handlePopUpOpen("deleteIdentity", { identityId: id, - name + name, + isProjectIdentity: Boolean(identityProjectId) }); }} > - Remove Identity From Project + {identityProjectId + ? "Delete Project Identity" + : "Remove Identity From Project"} )} @@ -406,7 +494,11 @@ export const IdentityTab = withProjectPermission( /> )} - + handlePopUpToggle("createIdentity", isOpen)} + /> + onRemoveIdentitySubmit( - (popUp?.deleteIdentity?.data as { identityId: string })?.identityId + popUp?.deleteIdentity?.data?.identityId, + popUp?.deleteIdentity?.data?.isProjectIdentity ) } /> diff --git a/frontend/src/pages/project/AccessControlPage/components/IdentityTab/components/ProjectIdentityModal.tsx b/frontend/src/pages/project/AccessControlPage/components/IdentityTab/components/ProjectIdentityModal.tsx new file mode 100644 index 000000000..003623f56 --- /dev/null +++ b/frontend/src/pages/project/AccessControlPage/components/IdentityTab/components/ProjectIdentityModal.tsx @@ -0,0 +1,310 @@ +import { Controller, useFieldArray, useForm } from "react-hook-form"; +import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useNavigate } from "@tanstack/react-router"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + FilterableSelect, + FormControl, + FormLabel, + IconButton, + Input, + Modal, + ModalContent, + Switch +} from "@app/components/v2"; +import { useProject } from "@app/context"; +import { getProjectBaseURL } from "@app/helpers/project"; +import { + TProjectIdentity, + useCreateProjectIdentity, + useGetProjectRoles, + useUpdateProjectIdentity, + useUpdateProjectIdentityMembership +} from "@app/hooks/api"; +import { useAddIdentityUniversalAuth } from "@app/hooks/api/identities"; +import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; + +const schema = z.object({ + name: z.string().min(1, "Required"), + hasDeleteProtection: z.boolean(), + role: z.object({ slug: z.string(), name: z.string() }).optional(), + metadata: z + .object({ + key: z.string().trim().min(1), + value: z.string().trim().min(1) + }) + .array() + .default([]) + .optional() +}); +export type FormData = z.infer; + +type ContentProps = { + onClose: () => void; + identity?: TProjectIdentity; +}; + +const Content = ({ onClose, identity }: ContentProps) => { + const navigate = useNavigate(); + + const { currentProject } = useProject(); + + const isUpdate = Boolean(identity); + + const { data: roles } = useGetProjectRoles(currentProject.id); + + const { mutateAsync: createMutateAsync } = useCreateProjectIdentity(); + const { mutateAsync: updateMutateAsync } = useUpdateProjectIdentity(); + const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth(); + const { mutateAsync: updateMembershipMutateAsync } = useUpdateProjectIdentityMembership(); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + name: identity?.name ?? "", + hasDeleteProtection: identity?.hasDeleteProtection ?? false, + metadata: identity?.metadata ?? [], + role: isUpdate ? undefined : { slug: ProjectMembershipRole.NoAccess, name: "No Access" } + } + }); + + const metadataFormFields = useFieldArray({ + control, + name: "metadata" + }); + + const onFormSubmit = async ({ name, role, metadata, hasDeleteProtection }: FormData) => { + try { + if (identity) { + // update + await updateMutateAsync({ + identityId: identity.id, + name, + hasDeleteProtection, + projectId: currentProject.id, + metadata + }); + + onClose(); + } else { + // create + + const { id: createdId } = await createMutateAsync({ + name, + projectId: currentProject.id, + hasDeleteProtection, + metadata + }); + + if (role) { + await updateMembershipMutateAsync({ + roles: [{ role: role.slug }], + identityId: createdId, + projectId: currentProject.id + }); + } + + await addMutateAsync({ + projectId: currentProject.id, + identityId: createdId, + clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }], + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }], + accessTokenTTL: 2592000, + accessTokenMaxTTL: 2592000, + accessTokenNumUsesLimit: 0, + accessTokenPeriod: 0, + lockoutEnabled: true, + lockoutThreshold: 3, + lockoutDurationSeconds: 300, + lockoutCounterResetSeconds: 30 + }); + + onClose(); + navigate({ + to: `${getProjectBaseURL(currentProject.type)}/identities/$identityId`, + params: { + identityId: createdId + } + }); + } + + createNotification({ + text: `Successfully ${isUpdate ? "updated" : "created"} project identity`, + type: "success" + }); + + reset(); + } catch (err) { + console.error(err); + const error = err as any; + const text = + error?.response?.data?.message ?? + `Failed to ${isUpdate ? "update" : "create"} project identity`; + + createNotification({ + text, + type: "error" + }); + } + }; + + return ( +
+ ( + + + + )} + /> + {!isUpdate && ( + ( + + option.slug} + getOptionLabel={(option) => option.name} + /> + + )} + /> + )} + ( + + +

Delete Protection {value ? "Enabled" : "Disabled"}

+
+
+ )} + /> +
+ +
+
+ {metadataFormFields.fields.map(({ id: metadataFieldId }, i) => ( +
+
+ {i === 0 && Key} + ( + + + + )} + /> +
+
+ {i === 0 && ( + + )} + ( + + + + )} + /> +
+ metadataFormFields.remove(i)} + > + + +
+ ))} +
+ +
+
+
+ + +
+ + ); +}; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + identity?: TProjectIdentity; +}; + +export const ProjectIdentityModal = ({ isOpen, onOpenChange, identity }: Props) => { + return ( + onOpenChange(open)}> + + onOpenChange(false)} /> + + + ); +}; diff --git a/frontend/src/pages/project/AccessControlPage/components/IdentityTab/components/IdentityModal.tsx b/frontend/src/pages/project/AccessControlPage/components/IdentityTab/components/ProjectLinkIdentityModal.tsx similarity index 55% rename from frontend/src/pages/project/AccessControlPage/components/IdentityTab/components/IdentityModal.tsx rename to frontend/src/pages/project/AccessControlPage/components/IdentityTab/components/ProjectLinkIdentityModal.tsx index 3274185b1..1738c1ec3 100644 --- a/frontend/src/pages/project/AccessControlPage/components/IdentityTab/components/IdentityModal.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/IdentityTab/components/ProjectLinkIdentityModal.tsx @@ -1,10 +1,7 @@ import { useMemo } from "react"; import { Controller, useForm } from "react-hook-form"; -import { components, OptionProps } from "react-select"; -import { faCheckCircle } from "@fortawesome/free-regular-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Link } from "@tanstack/react-router"; +import { useQuery } from "@tanstack/react-query"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; @@ -17,64 +14,47 @@ import { ModalContent, Spinner } from "@app/components/v2"; -import { Badge, OrgIcon } from "@app/components/v3"; -import { useOrganization, useProject } from "@app/context"; +import { useProject } from "@app/context"; import { - useAddIdentityToWorkspace, - useGetIdentityMembershipOrgs, + projectIdentityMembershipQuery, + useCreateProjectIdentityMembership, useGetProjectRoles, - useGetWorkspaceIdentityMemberships + useListProjectIdentityMemberships } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; const schema = z.object({ - identity: z.object({ name: z.string(), id: z.string(), isManagedByRootOrg: z.boolean() }), + identity: z.object({ + name: z.string(), + id: z.string() + }), role: z.object({ name: z.string(), slug: z.string() }) }); export type FormData = z.infer; type Props = { - popUp: UsePopUpState<["identity"]>; - handlePopUpToggle: (popUpName: keyof UsePopUpState<["identity"]>, state?: boolean) => void; -}; - -const Option = ({ - isSelected, - children, - ...props -}: OptionProps<{ name: string; id: string; isManagedByRootOrg: boolean }>) => { - return ( - -
-

{children}

- {props.data.isManagedByRootOrg && ( - - - Organization - - )} - {isSelected && ( - - )} -
-
- ); + popUp: UsePopUpState<["linkIdentity"]>; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["linkIdentity"]>, state?: boolean) => void; }; const Content = ({ popUp, handlePopUpToggle }: Props) => { - const { currentOrg } = useOrganization(); const { projectId } = useProject(); - const organizationId = currentOrg?.id || ""; + // const [searchValue, setSearchValue] = useState(""); - const { data: identityMembershipOrgsData, isPending: isMembershipsLoading } = - useGetIdentityMembershipOrgs({ - organizationId, - limit: 20000 // TODO: this is temp to preserve functionality for larger projects, will replace with combobox in separate PR - }); - const identityMembershipOrgs = identityMembershipOrgsData?.identityMemberships; - const { data: identityMembershipsData } = useGetWorkspaceIdentityMemberships({ + // const [debouncedSearchValue] = useDebounce(searchValue); + + // TODO: name search needs to be implemented on the backend + const { data: identityMembershipOrgs, isPending: isMembershipsLoading } = useQuery({ + ...projectIdentityMembershipQuery.listAvailable({ + projectId + // identityName: debouncedSearchValue + }), + placeholderData: (prev) => prev + }); + + const { data: identityMembershipsData } = useListProjectIdentityMemberships({ projectId, limit: 20000 // TODO: this is temp to preserve functionality for larger projects, will optimize in PR referenced above }); @@ -82,7 +62,8 @@ const Content = ({ popUp, handlePopUpToggle }: Props) => { const { data: roles, isPending: isRolesLoading } = useGetProjectRoles(projectId); - const { mutateAsync: addIdentityToWorkspaceMutateAsync } = useAddIdentityToWorkspace(); + const { mutateAsync: createProjectIdentityMembershipMutateAsync } = + useCreateProjectIdentityMembership(); const filteredIdentityMembershipOrgs = useMemo(() => { const wsIdentityIds = new Map(); @@ -91,7 +72,7 @@ const Content = ({ popUp, handlePopUpToggle }: Props) => { wsIdentityIds.set(identityMembership.identity.id, true); }); - return (identityMembershipOrgs || []).filter(({ identity: i }) => !wsIdentityIds.has(i.id)); + return (identityMembershipOrgs || []).filter((i) => !wsIdentityIds.has(i.id)); }, [identityMembershipOrgs, identityMemberships]); const { @@ -104,7 +85,7 @@ const Content = ({ popUp, handlePopUpToggle }: Props) => { }); const onFormSubmit = async ({ identity, role }: FormData) => { - await addIdentityToWorkspaceMutateAsync({ + await createProjectIdentityMembershipMutateAsync({ projectId, identityId: identity.id, role: role.slug || undefined @@ -116,17 +97,17 @@ const Content = ({ popUp, handlePopUpToggle }: Props) => { }); const nextAvailableMembership = filteredIdentityMembershipOrgs.filter( - (membership) => membership.identity.id !== identity.id + (membership) => membership.id !== identity.id )[0]; // prevents combobox from displaying previously added identity reset({ identity: { - name: nextAvailableMembership?.identity.name, - id: nextAvailableMembership?.identity.id + name: nextAvailableMembership?.name, + id: nextAvailableMembership?.id } }); - handlePopUpToggle("identity", false); + handlePopUpToggle("linkIdentity", false); }; if (isMembershipsLoading || isRolesLoading) @@ -136,7 +117,7 @@ const Content = ({ popUp, handlePopUpToggle }: Props) => { ); - return filteredIdentityMembershipOrgs.length ? ( + return (
{ value={value} onChange={onChange} placeholder="Select identity..." + // onInputChange={setSearchValue} options={filteredIdentityMembershipOrgs.map((membership) => ({ - ...membership.identity, - isManagedByRootOrg: membership.identity.orgId !== currentOrg.id + name: membership.name, + id: membership.id }))} getOptionValue={(option) => option.id} getOptionLabel={(option) => option.name} - components={{ - Option - }} /> )} @@ -189,7 +168,7 @@ const Content = ({ popUp, handlePopUpToggle }: Props) => { isLoading={isSubmitting} isDisabled={isSubmitting} > - {popUp?.identity?.data ? "Update" : "Add"} + {popUp?.linkIdentity?.data ? "Update" : "Link"} - - ); }; -export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { +export const ProjectLinkIdentityModal = ({ popUp, handlePopUpToggle }: Props) => { return ( { - handlePopUpToggle("identity", isOpen); + handlePopUpToggle("linkIdentity", isOpen); }} > - + diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx index 65a2a5710..097f28f3b 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; import { subject } from "@casl/ability"; import { faChevronLeft } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useQuery } from "@tanstack/react-query"; import { Link, useNavigate, useParams } from "@tanstack/react-router"; import { formatRelative } from "date-fns"; @@ -26,10 +27,13 @@ import { getProjectBaseURL, getProjectHomePage } from "@app/helpers/project"; import { usePopUp } from "@app/hooks"; import { useAssumeProjectPrivileges, - useDeleteIdentityFromWorkspace, - useGetWorkspaceIdentityMembershipDetails + useDeleteProjectIdentityMembership, + useGetProjectIdentityMembership } from "@app/hooks/api"; import { ActorType } from "@app/hooks/api/auditLogs/enums"; +import { projectIdentityQuery } from "@app/hooks/api/projectIdentity"; +import { ProjectIdentityAuthenticationSection } from "@app/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityAuthSection"; +import { ProjectIdentityDetailsSection } from "@app/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityDetailsSection"; import { ProjectAccessControlTabs } from "@app/types/project"; import { IdentityProjectAdditionalPrivilegeSection } from "./components/IdentityProjectAdditionalPrivilegeSection"; @@ -44,10 +48,24 @@ const Page = () => { const { currentProject, projectId } = useProject(); const { data: identityMembershipDetails, isPending: isMembershipDetailsLoading } = - useGetWorkspaceIdentityMembershipDetails(projectId, identityId); + useGetProjectIdentityMembership(projectId, identityId); const { mutateAsync: deleteMutateAsync, isPending: isDeletingIdentity } = - useDeleteIdentityFromWorkspace(); + useDeleteProjectIdentityMembership(); + + const isProjectIdentity = Boolean(identityMembershipDetails?.identity.projectId); + + const { + data: identity, + isPending: isProjectIdentityPending, + refetch: refetchIdentity + } = useQuery({ + ...projectIdentityQuery.getById({ + identityId: identityMembershipDetails?.identity.id as string, + projectId: identityMembershipDetails?.identity.projectId as string + }), + enabled: isProjectIdentity + }); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "deleteIdentity", @@ -96,7 +114,7 @@ const Page = () => { }); }; - if (isMembershipDetailsLoading) { + if (isMembershipDetailsLoading || (isProjectIdentity && isProjectIdentityPending)) { return (
@@ -124,7 +142,7 @@ const Page = () => {
{ )} - - {(isAllowed) => ( - - )} - + {!isProjectIdentity && ( + + {(isAllowed) => ( + + )} + + )}
- - +
+ {identity && ( +
+ + refetchIdentity()} + /> +
+ )} +
+ + +
+
{ diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx index dce632121..f96218bb5 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx @@ -26,14 +26,14 @@ import { import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { formatProjectRoleName } from "@app/helpers/roles"; import { usePopUp } from "@app/hooks"; -import { useUpdateIdentityWorkspaceRole } from "@app/hooks/api"; -import { IdentityMembership } from "@app/hooks/api/identities/types"; +import { useUpdateProjectIdentityMembership } from "@app/hooks/api"; +import { IdentityProjectMembership } from "@app/hooks/api/identities/types"; import { TProjectRole } from "@app/hooks/api/roles/types"; import { IdentityRoleModify } from "./IdentityRoleModify"; type Props = { - identityMembershipDetails: IdentityMembership; + identityMembershipDetails: IdentityProjectMembership; isMembershipDetailsLoading?: boolean; }; @@ -46,12 +46,12 @@ export const IdentityRoleDetailsSection = ({ "deleteRole", "modifyRole" ] as const); - const { mutateAsync: updateIdentityWorkspaceRole } = useUpdateIdentityWorkspaceRole(); + const { mutateAsync: updateIdentityProjectMembership } = useUpdateProjectIdentityMembership(); const handleRoleDelete = async () => { const { id } = popUp?.deleteRole?.data as TProjectRole; const updatedRoles = identityMembershipDetails?.roles?.filter((el) => el.id !== id); - await updateIdentityWorkspaceRole({ + await updateIdentityProjectMembership({ projectId: currentProject?.id || "", identityId: identityMembershipDetails.identity.id, roles: updatedRoles.map( diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleModify.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleModify.tsx index 8bc451151..38a1c8c5a 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleModify.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleModify.tsx @@ -32,10 +32,10 @@ import { useProject, useProjectPermission } from "@app/context"; -import { useGetProjectRoles, useUpdateIdentityWorkspaceRole } from "@app/hooks/api"; -import { IdentityMembership } from "@app/hooks/api/identities/types"; -import { ProjectUserMembershipTemporaryMode } from "@app/hooks/api/projects/types"; +import { useGetProjectRoles, useUpdateProjectIdentityMembership } from "@app/hooks/api"; +import { IdentityProjectMembership } from "@app/hooks/api/identities/types"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; +import { TemporaryPermissionMode } from "@app/hooks/api/shared"; const roleFormSchema = z.object({ roles: z @@ -58,7 +58,7 @@ const roleFormSchema = z.object({ type TRoleForm = z.infer; type Props = { - identityProjectMembership: IdentityMembership; + identityProjectMembership: IdentityProjectMembership; }; export const IdentityRoleModify = ({ identityProjectMembership }: Props) => { @@ -95,10 +95,10 @@ export const IdentityRoleModify = ({ identityProjectMembership }: Props) => { const formRoleField = roleForm.watch("roles"); - const updateIdentityWorkspaceRole = useUpdateIdentityWorkspaceRole(); + const updateProjectIdentityMembership = useUpdateProjectIdentityMembership(); const handleRoleUpdate = async (data: TRoleForm) => { - if (updateIdentityWorkspaceRole.isPending) return; + if (updateProjectIdentityMembership.isPending) return; const sanitizedRoles = data.roles.map((el) => { const { isTemporary } = el.temporaryAccess; @@ -108,13 +108,13 @@ export const IdentityRoleModify = ({ identityProjectMembership }: Props) => { return { role: el.slug, isTemporary: true as const, - temporaryMode: ProjectUserMembershipTemporaryMode.Relative, + temporaryMode: TemporaryPermissionMode.Relative, temporaryRange: el.temporaryAccess.temporaryRange, temporaryAccessStartTime: el.temporaryAccess.temporaryAccessStartTime }; }); - await updateIdentityWorkspaceRole.mutateAsync({ + await updateProjectIdentityMembership.mutateAsync({ projectId, identityId: identityProjectMembership.identity.id, roles: sanitizedRoles diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityAuthSection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityAuthSection.tsx new file mode 100644 index 000000000..f471a3d24 --- /dev/null +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityAuthSection.tsx @@ -0,0 +1,119 @@ +import { subject } from "@casl/ability"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { LockIcon, SettingsIcon } from "lucide-react"; + +import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { Button, Tooltip } from "@app/components/v2"; +import { Badge } from "@app/components/v3"; +import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/context"; +import { IdentityAuthMethod, identityAuthToNameMap, TProjectIdentity } from "@app/hooks/api"; +import { usePopUp } from "@app/hooks/usePopUp"; +import { IdentityAuthMethodModal } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal"; +import { ViewIdentityAuthModal } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal"; + +type Props = { + identity: TProjectIdentity; + refetchIdentity: () => void; +}; + +export const ProjectIdentityAuthenticationSection = ({ identity, refetchIdentity }: Props) => { + const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp([ + "viewAuthMethod", + "identityAuthMethod", + "upgradePlan" + ]); + + return ( +
+
+

Authentication

+
+ {identity.authMethods.length > 0 ? ( +
+ {identity.authMethods.map((authMethod) => ( + + ))} +
+ ) : ( +
+

+ No authentication methods configured. Get started by creating a new auth method. +

+
+ )} + {!Object.values(IdentityAuthMethod).every((method) => + identity.authMethods.includes(method) + ) && ( + + {(isAllowed) => ( + + )} + + )} + + handlePopUpToggle("upgradePlan", isOpen)} + text={(popUp.upgradePlan?.data as { description: string })?.description} + isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature} + /> + handlePopUpToggle("viewAuthMethod", isOpen)} + authMethod={popUp.viewAuthMethod.data?.authMethod} + lockedOut={popUp.viewAuthMethod.data?.lockedOut || false} + identityId={identity.id} + onResetAllLockouts={popUp.viewAuthMethod.data?.refetchIdentity} + /> +
+ ); +}; diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityDetailsSection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityDetailsSection.tsx new file mode 100644 index 000000000..bab3208ce --- /dev/null +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityDetailsSection.tsx @@ -0,0 +1,227 @@ +import { subject } from "@casl/ability"; +import { + faCheck, + faChevronDown, + faCopy, + faEdit, + faKey, + faTrash +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useNavigate } from "@tanstack/react-router"; +import { format } from "date-fns"; +import { twMerge } from "tailwind-merge"; + +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + Button, + DeleteActionModal, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + IconButton, + Tag, + Tooltip +} from "@app/components/v2"; +import { ProjectPermissionIdentityActions, ProjectPermissionSub, useProject } from "@app/context"; +import { getProjectBaseURL } from "@app/helpers/project"; +import { usePopUp, useTimedReset } from "@app/hooks"; +import { identityAuthToNameMap, TProjectIdentity, useDeleteProjectIdentity } from "@app/hooks/api"; +import { IdentityProjectMembership } from "@app/hooks/api/identities/types"; +import { ProjectIdentityModal } from "@app/pages/project/AccessControlPage/components/IdentityTab/components/ProjectIdentityModal"; + +type Props = { + identity: TProjectIdentity; + membership: IdentityProjectMembership; +}; + +export const ProjectIdentityDetailsSection = ({ identity, membership }: Props) => { + const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset({ + initialState: "Copy ID to clipboard" + }); + + const { currentProject } = useProject(); + const { mutateAsync: deleteIdentity } = useDeleteProjectIdentity(); + const navigate = useNavigate(); + const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp([ + "editIdentity", + "deleteIdentity" + ] as const); + + const handleDeleteIdentity = async () => { + try { + await deleteIdentity({ + identityId: identity.id, + projectId: identity.projectId! + }); + + navigate({ + to: `${getProjectBaseURL(currentProject.type)}/access-management`, + search: { + selectedTab: "identities" + } + }); + } catch { + createNotification({ + type: "error", + text: "Failed to delete project identity" + }); + } + }; + + return ( +
+
+

Identity Details

+ + + + + + + {(isAllowed) => ( + } + onClick={async () => { + handlePopUpOpen("editIdentity"); + }} + disabled={!isAllowed} + > + Edit Identity + + )} + + + {(isAllowed) => ( + { + handlePopUpOpen("deleteIdentity"); + }} + icon={} + disabled={!isAllowed} + > + Delete Identity + + )} + + + +
+
+
+

Identity ID

+
+

{identity.id}

+
+ + { + navigator.clipboard.writeText(identity.id); + setCopyTextId("Copied"); + }} + > + + + +
+
+
+
+

Last Login Auth Method

+

+ {membership.lastLoginAuthMethod + ? identityAuthToNameMap[membership.lastLoginAuthMethod] + : "-"} +

+
+
+

Last Login Time

+

+ {membership.lastLoginTime ? format(membership.lastLoginTime, "PPpp") : "-"} +

+
+
+

Delete Protection

+

+ {identity.hasDeleteProtection ? "On" : "Off"} +

+
+
+

Metadata

+ {identity?.metadata?.length ? ( +
+ {identity.metadata?.map((el) => ( +
+ + +
{el.key}
+
+ +
+ {el.value} +
+
+
+ ))} +
+ ) : ( +

-

+ )} +
+
+ handlePopUpToggle("editIdentity", isOpen)} + /> + handlePopUpToggle("deleteIdentity", isOpen)} + deleteKey="confirm" + onDeleteApproved={handleDeleteIdentity} + /> +
+ ); +}; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index efd7b123b..ca89f9e60 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -180,7 +180,11 @@ const IdentityPolicyActionSchema = z.object({ [ProjectPermissionIdentityActions.Edit]: z.boolean().optional(), [ProjectPermissionIdentityActions.Delete]: z.boolean().optional(), [ProjectPermissionIdentityActions.GrantPrivileges]: z.boolean().optional(), - [ProjectPermissionIdentityActions.AssumePrivileges]: z.boolean().optional() + [ProjectPermissionIdentityActions.AssumePrivileges]: z.boolean().optional(), + [ProjectPermissionIdentityActions.RevokeAuth]: z.boolean().optional(), + [ProjectPermissionIdentityActions.GetToken]: z.boolean().optional(), + [ProjectPermissionIdentityActions.CreateToken]: z.boolean().optional(), + [ProjectPermissionIdentityActions.DeleteToken]: z.boolean().optional() }); const GroupPolicyActionSchema = z.object({ @@ -962,6 +966,10 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { const canAssumePrivileges = action.includes( ProjectPermissionIdentityActions.AssumePrivileges ); + const canRevokeAuth = action.includes(ProjectPermissionIdentityActions.RevokeAuth); + const canCreateToken = action.includes(ProjectPermissionIdentityActions.CreateToken); + const canGetToken = action.includes(ProjectPermissionIdentityActions.GetToken); + const canDeleteToken = action.includes(ProjectPermissionIdentityActions.DeleteToken); if (!formVal[subject]) formVal[subject] = [{ conditions: [] }]; @@ -974,6 +982,10 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { formVal[subject]![0][ProjectPermissionIdentityActions.GrantPrivileges] = true; if (canAssumePrivileges) formVal[subject]![0][ProjectPermissionIdentityActions.AssumePrivileges] = true; + if (canRevokeAuth) formVal[subject]![0][ProjectPermissionIdentityActions.RevokeAuth] = true; + if (canCreateToken) formVal[subject]![0][ProjectPermissionIdentityActions.CreateToken] = true; + if (canGetToken) formVal[subject]![0][ProjectPermissionIdentityActions.GetToken] = true; + if (canDeleteToken) formVal[subject]![0][ProjectPermissionIdentityActions.DeleteToken] = true; return; } @@ -1453,7 +1465,11 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { { label: "Modify", value: ProjectPermissionIdentityActions.Edit }, { label: "Remove", value: ProjectPermissionIdentityActions.Delete }, { label: "Grant Privileges", value: ProjectPermissionIdentityActions.GrantPrivileges }, - { label: "Assume Privileges", value: ProjectPermissionIdentityActions.AssumePrivileges } + { label: "Assume Privileges", value: ProjectPermissionIdentityActions.AssumePrivileges }, + { label: "Revoke Auth", value: ProjectPermissionIdentityActions.RevokeAuth }, + { label: "Create Token", value: ProjectPermissionIdentityActions.CreateToken }, + { label: "Get Token", value: ProjectPermissionIdentityActions.GetToken }, + { label: "Delete Token", value: ProjectPermissionIdentityActions.DeleteToken } ] }, [ProjectPermissionSub.Groups]: {