From ef6282968b7f59c23354943a3b3d589c90fcf6de Mon Sep 17 00:00:00 2001 From: = Date: Sat, 18 Oct 2025 15:30:21 +0530 Subject: [PATCH 01/44] feat: first iteration changes for sub org routers --- backend/src/@types/fastify.d.ts | 3 + .../db/migrations/20251018061215_sub-org.ts | 36 ++++++ backend/src/db/schemas/identities.ts | 3 +- backend/src/db/schemas/models.ts | 6 + backend/src/db/schemas/organizations.ts | 3 +- backend/src/ee/routes/v1/sub-org-router.ts | 114 ++++++++++++++++++ .../ee/services/audit-log/audit-log-types.ts | 11 ++ .../src/ee/services/license/license-fns.ts | 1 + .../src/ee/services/license/license-types.ts | 1 + .../ee/services/permission/org-permission.ts | 19 ++- .../ee/services/permission/permission-dal.ts | 5 +- .../permission/permission-service-types.ts | 26 +--- .../services/permission/permission-service.ts | 30 +++-- .../ee/services/sub-org/sub-org-service.ts | 109 +++++++++++++++++ .../src/ee/services/sub-org/sub-org-types.ts | 16 +++ backend/src/lib/api-docs/constants.ts | 12 ++ backend/src/lib/types/index.ts | 1 + .../server/plugins/auth/inject-identity.ts | 48 +++++--- .../server/plugins/auth/inject-permission.ts | 8 +- .../services/auth-token/auth-token-service.ts | 61 +++++++--- .../identity-access-token-dal.ts | 1 + .../identity-access-token-service.ts | 48 ++++++-- backend/src/services/org/org-dal.ts | 44 +++++++ 23 files changed, 529 insertions(+), 77 deletions(-) create mode 100644 backend/src/db/migrations/20251018061215_sub-org.ts create mode 100644 backend/src/ee/routes/v1/sub-org-router.ts create mode 100644 backend/src/ee/services/sub-org/sub-org-service.ts create mode 100644 backend/src/ee/services/sub-org/sub-org-types.ts diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index e3e8733f0..59ca1cf55 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -126,6 +126,7 @@ import { TUserServiceFactory } from "@app/services/user/user-service"; import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service"; import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service"; import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service"; +import { TSubOrgServiceFactory } from "@app/ee/services/sub-org/sub-org-service"; declare module "@fastify/request-context" { interface RequestContextData { @@ -178,6 +179,7 @@ declare module "fastify" { type: ActorType; id: string; orgId: string; + parentOrgId: string; }; rateLimits: RateLimitConfiguration; // passport data @@ -327,6 +329,7 @@ declare module "fastify" { additionalPrivilege: TAdditionalPrivilegeServiceFactory; role: TRoleServiceFactory; convertor: TConvertorServiceFactory; + subOrganization: TSubOrgServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer diff --git a/backend/src/db/migrations/20251018061215_sub-org.ts b/backend/src/db/migrations/20251018061215_sub-org.ts new file mode 100644 index 000000000..3824bf68c --- /dev/null +++ b/backend/src/db/migrations/20251018061215_sub-org.ts @@ -0,0 +1,36 @@ +import { Knex } from "knex"; +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasParentOrgId = await knex.schema.hasColumn(TableName.Organization, "parentOrgId"); + if (!hasParentOrgId) { + await knex.schema.alterTable(TableName.Organization, (t) => { + t.uuid("parentOrgId"); + t.foreign("parentOrgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + }); + } + + const hasIdentityOrgCol = await knex.schema.hasColumn(TableName.Identity, "orgId"); + if (!hasIdentityOrgCol) { + await knex.schema.alterTable(TableName.Identity, (t) => { + t.uuid("orgId").notNullable(); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasParentOrgId = await knex.schema.hasColumn(TableName.Organization, "parentOrgId"); + if (hasParentOrgId) { + await knex.schema.alterTable(TableName.Organization, (t) => { + t.dropColumn("parentOrgId"); + }); + } + + const hasIdentityOrgCol = await knex.schema.hasColumn(TableName.Identity, "orgId"); + if (hasIdentityOrgCol) { + await knex.schema.alterTable(TableName.Identity, (t) => { + t.dropColumn("orgId"); + }); + } +} diff --git a/backend/src/db/schemas/identities.ts b/backend/src/db/schemas/identities.ts index a592e2480..06c37ff22 100644 --- a/backend/src/db/schemas/identities.ts +++ b/backend/src/db/schemas/identities.ts @@ -13,7 +13,8 @@ export const IdentitiesSchema = z.object({ authMethod: z.string().nullable().optional(), createdAt: z.date(), updatedAt: z.date(), - hasDeleteProtection: z.boolean().default(false) + hasDeleteProtection: z.boolean().default(false), + orgId: z.string().uuid() }); export type TIdentities = z.infer; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index f7291a70f..4705df2ce 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -312,6 +312,12 @@ export enum ActionProjectType { Any = "any" } +export enum OrganizationActionScope { + ChildOrganization = "child-organization-only", + ParentOrganization = "parent-organization-only", + Any = "any" +} + export enum TemporaryPermissionMode { Relative = "relative" } diff --git a/backend/src/db/schemas/organizations.ts b/backend/src/db/schemas/organizations.ts index afc9e2b73..38c6f797d 100644 --- a/backend/src/db/schemas/organizations.ts +++ b/backend/src/db/schemas/organizations.ts @@ -38,7 +38,8 @@ export const OrganizationsSchema = z.object({ maxSharedSecretLifetime: z.number().default(2592000).nullable().optional(), maxSharedSecretViewLimit: z.number().nullable().optional(), googleSsoAuthEnforced: z.boolean().default(false), - googleSsoAuthLastUsed: z.date().nullable().optional() + googleSsoAuthLastUsed: z.date().nullable().optional(), + parentOrgId: z.string().uuid().nullable().optional() }); export type TOrganizations = z.infer; diff --git a/backend/src/ee/routes/v1/sub-org-router.ts b/backend/src/ee/routes/v1/sub-org-router.ts new file mode 100644 index 000000000..9b9ecfea2 --- /dev/null +++ b/backend/src/ee/routes/v1/sub-org-router.ts @@ -0,0 +1,114 @@ +import { z } from "zod"; + +import { OrganizationsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags, SUB_ORGANIZATIONS } from "@app/lib/api-docs"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerSubOrgRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SubOrganizations], + description: "Create a child organization", + security: [ + { + bearerAuth: [] + } + ], + body: z.object({ + name: z.string().trim().describe(SUB_ORGANIZATIONS.CREATE.name) + }), + response: { + 200: z.object({ + organization: OrganizationsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { organization } = await server.services.subOrganization.createSubOrg({ + name: req.body.name, + permissionActor: { + id: req.permission.id, + type: req.permission.type, + authMethod: req.permission.authMethod, + orgId: req.permission.orgId, + parentOrgId: req.permission.parentOrgId + } + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.CREATE_CHILD_ORGANIZATION, + metadata: { + name: req.body.name, + organizationId: organization.id + } + } + }); + + return { organization }; + } + }); + + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SubOrganizations], + description: "List child organizations", + security: [ + { + bearerAuth: [] + } + ], + querystring: z.object({ + limit: z.coerce.number().min(1).max(100).default(25).describe(SUB_ORGANIZATIONS.LIST.limit), + offset: z.coerce.number().min(0).default(0).describe(SUB_ORGANIZATIONS.LIST.offset), + isAccessible: z + .enum(["true", "false"]) + .optional() + .transform((value) => value === "true") + .describe(SUB_ORGANIZATIONS.LIST.isAccessible) + }), + response: { + 200: z.object({ + organizations: OrganizationsSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { organizations } = await server.services.subOrganization.listSubOrgs({ + permissionActor: { + id: req.permission.id, + type: req.permission.type, + authMethod: req.permission.authMethod, + orgId: req.permission.orgId, + parentOrgId: req.permission.orgId + }, + data: { + limit: req.query.limit, + offset: req.query.offset, + isAccessible: req.query.isAccessible + } + }); + + return { organizations }; + } + }); +}; diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index bc50283d4..b5d49b7e4 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -173,6 +173,8 @@ export enum EventType { UPDATE_TOKEN_IDENTITY_TOKEN_AUTH = "update-token-identity-token-auth", GET_TOKENS_IDENTITY_TOKEN_AUTH = "get-tokens-identity-token-auth", + CREATE_CHILD_ORGANIZATION = "create-child-organization", + ADD_IDENTITY_TOKEN_AUTH = "add-identity-token-auth", UPDATE_IDENTITY_TOKEN_AUTH = "update-identity-token-auth", GET_IDENTITY_TOKEN_AUTH = "get-identity-token-auth", @@ -607,6 +609,14 @@ interface GetSecretsEvent { }; } +interface CreateChildOrganizationEvent { + type: EventType.CREATE_CHILD_ORGANIZATION; + metadata: { + name: string; + organizationId: string; + }; +} + type TSecretMetadata = { key: string; value: string }[]; interface GetSecretEvent { @@ -3863,6 +3873,7 @@ interface PamResourceDeleteEvent { } export type Event = + | CreateChildOrganizationEvent | GetSecretsEvent | GetSecretEvent | CreateSecretEvent diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index 2a3cf82cc..e5c775f07 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -28,6 +28,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ rbac: false, githubOrgSync: false, customRateLimits: false, + childOrganization: true, customAlerts: false, secretAccessInsights: false, auditLogs: false, diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 9cdcfcc3d..8090d789e 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -33,6 +33,7 @@ export type TFeatureSet = { membersUsed: number; identityLimit: null; identitiesUsed: number; + childOrganization: true; environmentLimit: null; environmentsUsed: 0; secretVersioning: true; diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index d4da8c98f..45b867398 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -15,6 +15,11 @@ export enum OrgPermissionActions { Delete = "delete" } +export enum OrgPermissionChildOrgActions { + Create = "create", + DirectAccess = "direct-access" +} + export enum OrgPermissionAppConnectionActions { Read = "read", Create = "create", @@ -117,7 +122,8 @@ export enum OrgPermissionSubjects { Kmip = "kmip", Gateway = "gateway", Relay = "relay", - SecretShare = "secret-share" + SecretShare = "secret-share", + ChildOrganization = "child-organization" } export type AppConnectionSubjectFields = { @@ -128,6 +134,7 @@ export type OrgPermissionSet = | [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace] | [OrgPermissionActions.Create, OrgPermissionSubjects.Project] | [OrgPermissionActions, OrgPermissionSubjects.Role] + | [OrgPermissionChildOrgActions, OrgPermissionSubjects.ChildOrganization] | [OrgPermissionActions, OrgPermissionSubjects.Member] | [OrgPermissionActions, OrgPermissionSubjects.Settings] | [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount] @@ -185,6 +192,12 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [ subject: z.literal(OrgPermissionSubjects.Role).describe("The entity this permission pertains to."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") }), + z.object({ + subject: z.literal(OrgPermissionSubjects.ChildOrganization).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionChildOrgActions).describe( + "Describe what action an entity can take." + ) + }), z.object({ subject: z.literal(OrgPermissionSubjects.Member).describe("The entity this permission pertains to."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") @@ -308,6 +321,10 @@ const buildAdminPermission = () => { // ws permissions can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); can(OrgPermissionActions.Create, OrgPermissionSubjects.Project); + + can(OrgPermissionChildOrgActions.Create, OrgPermissionSubjects.ChildOrganization); + can(OrgPermissionChildOrgActions.DirectAccess, OrgPermissionSubjects.ChildOrganization); + // role permission can(OrgPermissionActions.Read, OrgPermissionSubjects.Role); can(OrgPermissionActions.Create, OrgPermissionSubjects.Role); diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index 49a375f8f..eb45158ab 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -19,6 +19,7 @@ interface TPermissionDataReturn extends TMemberships { orgAuthEnforced?: boolean | null; orgGoogleSsoAuthEnforced?: boolean | null; shouldUseNewPrivilegeSystem?: boolean | null; + parentOrgId?: boolean | null; bypassOrgAuthEnabled?: boolean | null; roles: { id: string; @@ -273,7 +274,8 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), db.ref("googleSsoAuthEnforced").withSchema(TableName.Organization).as("orgGoogleSsoAuthEnforced"), - db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled") + db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"), + db.ref("parentOrgId").withSchema(TableName.Organization).as("parentOrgId") ); const data = sqlNestRelationships({ @@ -283,6 +285,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { MembershipsSchema.extend({ orgAuthEnforced: z.boolean().optional().nullable(), shouldUseNewPrivilegeSystem: z.boolean().optional().nullable(), + parentOrgId: z.boolean().optional().nullable(), orgGoogleSsoAuthEnforced: z.boolean(), bypassOrgAuthEnabled: z.boolean() }).parse(el), diff --git a/backend/src/ee/services/permission/permission-service-types.ts b/backend/src/ee/services/permission/permission-service-types.ts index 1f0e00470..34660d17d 100644 --- a/backend/src/ee/services/permission/permission-service-types.ts +++ b/backend/src/ee/services/permission/permission-service-types.ts @@ -2,7 +2,7 @@ import { MongoAbility } from "@casl/ability"; import { MongoQuery } from "@ucast/mongo2js"; import { Knex } from "knex"; -import { ActionProjectType, TMemberships } from "@app/db/schemas"; +import { ActionProjectType, OrganizationActionScope, TMemberships } from "@app/db/schemas"; import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; import { OrgPermissionSet } from "./org-permission"; @@ -18,21 +18,6 @@ export type TBuildOrgPermissionDTO = { role: string; }[]; -export type TGetUserProjectPermissionArg = { - userId: string; - projectId: string; - authMethod: ActorAuthMethod; - actionProjectType: ActionProjectType; - userOrgId?: string; -}; - -export type TGetIdentityProjectPermissionArg = { - identityId: string; - projectId: string; - identityOrgId?: string; - actionProjectType: ActionProjectType; -}; - export type TGetServiceTokenProjectPermissionArg = { serviceTokenId: string; projectId: string; @@ -55,16 +40,11 @@ export type TGetOrgPermissionArg = { orgId: string; actorAuthMethod: ActorAuthMethod; actorOrgId?: string; + scope: OrganizationActionScope; }; export type TPermissionServiceFactory = { - getOrgPermission: ( - type: ActorType, - id: string, - orgId: string, - authMethod: ActorAuthMethod, - actorOrgId: string | undefined - ) => Promise<{ + getOrgPermission: (arg: TGetOrgPermissionArg) => Promise<{ permission: MongoAbility; memberships: Array< TMemberships & { diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index 2d879b4fd..dc4874b10 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -7,6 +7,7 @@ import { Knex } from "knex"; import { AccessScope, ActionProjectType, + OrganizationActionScope, OrgMembershipRole, ProjectMembershipRole, ServiceTokenScopes @@ -179,14 +180,15 @@ export const permissionServiceFactory = ({ // return minTtl; // }; - const getOrgPermission: TPermissionServiceFactory["getOrgPermission"] = async ( - type, - id, + const getOrgPermission: TPermissionServiceFactory["getOrgPermission"] = async ({ + actor, + actorId, orgId, - authMethod, - actorOrgId - ) => { - if (type !== ActorType.USER && type !== ActorType.IDENTITY) { + actorOrgId, + scope, + actorAuthMethod + }) => { + if (actor !== ActorType.USER && actor !== ActorType.IDENTITY) { throw new BadRequestError({ message: "Invalid actor provided", name: "Get org permission" @@ -202,11 +204,19 @@ export const permissionServiceFactory = ({ scope: AccessScope.Organization, orgId }, - actorId: id, - actorType: type + actorId, + actorType: actor }); if (!permissionData?.length) throw new ForbiddenRequestError({ name: "You are not member of this organization" }); + const parentOrgId = permissionData?.[0]?.parentOrgId; + const isChild = Boolean(parentOrgId); + if (scope === OrganizationActionScope.ParentOrganization && isChild) { + throw new BadRequestError({ message: `Child organization cannot do this operation` }); + } else if (scope === OrganizationActionScope.ChildOrganization && !isChild) { + throw new BadRequestError({ message: `Parent organization cannot do this operation` }); + } + const permissionFromRoles = permissionData.flatMap((membership) => { const activeRoles = membership?.roles .filter( @@ -227,7 +237,7 @@ export const permissionServiceFactory = ({ permissionData.some((memberships) => memberships.roles.some((el) => role === (el.customRoleSlug || el.role))); validateOrgSSO( - authMethod, + actorAuthMethod, permissionData?.[0].orgAuthEnforced, Boolean(permissionData?.[0].orgGoogleSsoAuthEnforced), Boolean(permissionData?.[0].bypassOrgAuthEnabled), diff --git a/backend/src/ee/services/sub-org/sub-org-service.ts b/backend/src/ee/services/sub-org/sub-org-service.ts new file mode 100644 index 000000000..37c5545c0 --- /dev/null +++ b/backend/src/ee/services/sub-org/sub-org-service.ts @@ -0,0 +1,109 @@ +import { ForbiddenError } from "@casl/ability"; + +import { AccessScope, OrganizationActionScope, OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas"; +import { BadRequestError } from "@app/lib/errors"; +import { ActorType } from "@app/services/auth/auth-type"; +import { TMembershipDALFactory } from "@app/services/membership/membership-dal"; +import { TMembershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; +import { TOrgDALFactory } from "@app/services/org/org-dal"; + +import { TLicenseServiceFactory } from "../license/license-service"; +import { OrgPermissionChildOrgActions, OrgPermissionSubjects } from "../permission/org-permission"; +import { TPermissionServiceFactory } from "../permission/permission-service-types"; +import { TCreateSubOrgDTO, TListSubOrgDTO } from "./sub-org-types"; + +type TSubOrgServiceFactoryDep = { + orgDAL: Pick; + permissionService: Pick; + licenseService: Pick; + membershipDAL: Pick; + membershipRoleDAL: Pick; +}; + +export type TSubOrgServiceFactory = ReturnType; + +export const subOrgServiceFactory = ({ + orgDAL, + permissionService, + licenseService, + membershipDAL, + membershipRoleDAL +}: TSubOrgServiceFactoryDep) => { + const createSubOrg = async ({ name, permissionActor }: TCreateSubOrgDTO) => { + const { permission } = await permissionService.getOrgPermission({ + actorId: permissionActor.id, + actor: permissionActor.type, + orgId: permissionActor.orgId, + actorOrgId: permissionActor.orgId, + actorAuthMethod: permissionActor.authMethod, + scope: OrganizationActionScope.ParentOrganization + }); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionChildOrgActions.Create, + OrgPermissionSubjects.ChildOrganization + ); + + const orgLicensePlan = await licenseService.getPlan(permissionActor.parentOrgId); + if (!orgLicensePlan.gateway) { + throw new BadRequestError({ + message: "Child organization creation failed. Please upgrade your instance to Infisical's Enterprise plan." + }); + } + + const organization = await orgDAL.transaction(async (tx) => { + const org = await orgDAL.create({ name, slug: name, parentOrgId: permissionActor.orgId }, tx); + const membership = await membershipDAL.create( + { + scope: AccessScope.Organization, + [permissionActor.type === ActorType.IDENTITY ? "actorIdentityId" : "actorUserId"]: permissionActor.id, + scopeOrgId: org.id, + status: OrgMembershipStatus.Accepted, + isActive: true + }, + tx + ); + await membershipRoleDAL.create( + { + membershipId: membership.id, + role: OrgMembershipRole.Admin + }, + tx + ); + return org; + }); + + return { + organization + }; + }; + + const listSubOrgs = async ({ permissionActor, data }: TListSubOrgDTO) => { + await permissionService.getOrgPermission({ + actorId: permissionActor.id, + actor: permissionActor.type, + orgId: permissionActor.parentOrgId, + actorOrgId: permissionActor.parentOrgId, + actorAuthMethod: permissionActor.authMethod, + scope: OrganizationActionScope.ParentOrganization + }); + + const organizations = await orgDAL.listSubOrganizations({ + actorId: permissionActor.id, + actorType: permissionActor.type, + orgId: permissionActor.parentOrgId, + isAccessible: data?.isAccessible, + limit: data?.limit, + offset: data?.offset + }); + + return { + organizations + }; + }; + + return { + createSubOrg, + listSubOrgs + }; +}; diff --git a/backend/src/ee/services/sub-org/sub-org-types.ts b/backend/src/ee/services/sub-org/sub-org-types.ts new file mode 100644 index 000000000..fc2a47b59 --- /dev/null +++ b/backend/src/ee/services/sub-org/sub-org-types.ts @@ -0,0 +1,16 @@ +import { OrgServiceActor } from "@app/lib/types"; + +export type TCreateSubOrgDTO = { + name: string; + permissionActor: OrgServiceActor; +}; + +export type TListSubOrgDTO = { + permissionActor: OrgServiceActor; + data: Partial<{ + limit?: number; + offset?: number; + search?: string; + isAccessible?: boolean; + }>; +}; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 8d1ae45bf..77ce47cd5 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -33,6 +33,7 @@ export enum ApiDocsTags { LdapAuth = "LDAP Auth", Groups = "Groups", Organizations = "Organizations", + SubOrganizations = "Sub Organizations", Projects = "Projects", ProjectUsers = "Project Users", ProjectGroups = "Project Groups", @@ -716,6 +717,17 @@ export const ORGANIZATIONS = { } } as const; +export const SUB_ORGANIZATIONS = { + CREATE: { + name: "The name of the child organization to create." + }, + LIST: { + limit: "The number of child organizations to return.", + offset: "The offset to start from. If you enter 10, it will start from the 10th child organization.", + isAccessible: "Filter to only return child organizations that the actor has access to." + } +} as const; + export const PROJECTS = { CREATE: { organizationSlug: "The slug of the organization to create the project in.", diff --git a/backend/src/lib/types/index.ts b/backend/src/lib/types/index.ts index a7a60349f..1a2262bc1 100644 --- a/backend/src/lib/types/index.ts +++ b/backend/src/lib/types/index.ts @@ -78,6 +78,7 @@ export type OrgServiceActor = { id: string; authMethod: ActorAuthMethod; orgId: string; + parentOrgId: string; }; export enum QueueWorkerProfile { diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 1bff11879..f91d56f32 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -20,6 +20,7 @@ export type TAuthMode = tokenVersionId: string; // the session id of token used user: TUsers; orgId: string; + parentOrgId: string; authMethod: AuthMethod; isMfaVerified?: boolean; token: AuthModeJwtTokenPayload; @@ -31,6 +32,7 @@ export type TAuthMode = userId: string; user: TUsers; orgId: string; + parentOrgId: string; token: string; } | { @@ -39,6 +41,7 @@ export type TAuthMode = actor: ActorType.SERVICE; serviceTokenId: string; orgId: string; + parentOrgId: string; authMethod: null; token: string; } @@ -48,6 +51,7 @@ export type TAuthMode = identityId: string; identityName: string; orgId: string; + parentOrgId: string; authMethod: null; isInstanceAdmin?: boolean; token: TIdentityAccessTokenJwtPayload; @@ -57,6 +61,7 @@ export type TAuthMode = actor: ActorType.SCIM_CLIENT; scimTokenId: string; orgId: string; + parentOrgId: string; authMethod: null; }; @@ -136,17 +141,24 @@ export const injectIdentity = fp( if (!authMode) return; + const subOrganizationSelector = req.headers?.["x-infisical-org"] as string | undefined; + switch (authMode) { case AuthMode.JWT: { - const { user, tokenVersionId, orgId } = await server.services.authToken.fnValidateJwtIdentity(token); + const { user, tokenVersionId, orgId, parentOrgId } = await server.services.authToken.fnValidateJwtIdentity( + token, + subOrganizationSelector + ); requestContext.set("orgId", orgId); + req.auth = { authMode: AuthMode.JWT, user, userId: user.id, tokenVersionId, actor, - orgId: orgId as string, + orgId, + parentOrgId, authMethod: token.authMethod, isMfaVerified: token.isMfaVerified, token @@ -154,13 +166,18 @@ export const injectIdentity = fp( break; } case AuthMode.IDENTITY_ACCESS_TOKEN: { - const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken(token, req.realIp); + const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken( + token, + subOrganizationSelector, + req.realIp + ); const serverCfg = await getServerCfg(); requestContext.set("orgId", identity.orgId); req.auth = { authMode: AuthMode.IDENTITY_ACCESS_TOKEN, actor, orgId: identity.orgId, + parentOrgId: identity.parentOrgId, identityId: identity.identityId, identityName: identity.name, authMethod: null, @@ -190,8 +207,13 @@ export const injectIdentity = fp( case AuthMode.SERVICE_TOKEN: { const serviceToken = await server.services.serviceToken.fnValidateServiceToken(token); requestContext.set("orgId", serviceToken.orgId); + + if (subOrganizationSelector) + throw new BadRequestError({ message: `Service token doesn't support sub organization selector` }); + req.auth = { orgId: serviceToken.orgId, + parentOrgId: serviceToken.orgId, authMode: AuthMode.SERVICE_TOKEN as const, serviceToken, serviceTokenId: serviceToken.id, @@ -202,22 +224,18 @@ export const injectIdentity = fp( break; } case AuthMode.API_KEY: { - const user = await server.services.apiKey.fnValidateApiKey(token as string); - req.auth = { - authMode: AuthMode.API_KEY as const, - userId: user.id, - actor, - user, - orgId: "API_KEY", // We set the orgId to an arbitrary value, since we can't link an API key to a specific org. We have to deprecate API keys soon! - authMethod: null, - token: token as string - }; - break; + throw new BadRequestError({ + message: "API key authentication is not supported anymore. Please switch to identity authentication." + }); } case AuthMode.SCIM_TOKEN: { const { orgId, scimTokenId } = await server.services.scim.fnValidateScimToken(token); requestContext.set("orgId", orgId); - req.auth = { authMode: AuthMode.SCIM_TOKEN, actor, scimTokenId, orgId, authMethod: null }; + + if (subOrganizationSelector) + throw new BadRequestError({ message: `Service token doesn't support sub organization selector` }); + + req.auth = { authMode: AuthMode.SCIM_TOKEN, actor, scimTokenId, orgId, authMethod: null, parentOrgId: orgId }; break; } default: diff --git a/backend/src/server/plugins/auth/inject-permission.ts b/backend/src/server/plugins/auth/inject-permission.ts index 11a94657b..da5e7e54e 100644 --- a/backend/src/server/plugins/auth/inject-permission.ts +++ b/backend/src/server/plugins/auth/inject-permission.ts @@ -14,7 +14,8 @@ export const injectPermission = fp(async (server) => { type: ActorType.USER, id: req.auth.userId, orgId: req.auth.orgId, // if the req.auth.authMode is AuthMode.API_KEY, the orgId will be "API_KEY" - authMethod: req.auth.authMethod // if the req.auth.authMode is AuthMode.API_KEY, the authMethod will be null + authMethod: req.auth.authMethod, // if the req.auth.authMode is AuthMode.API_KEY, the authMethod will be null + parentOrgId: req.auth.parentOrgId }; logger.info( @@ -25,7 +26,8 @@ export const injectPermission = fp(async (server) => { type: ActorType.IDENTITY, id: req.auth.identityId, orgId: req.auth.orgId, - authMethod: null + authMethod: null, + parentOrgId: req.auth.parentOrgId }; logger.info( @@ -36,6 +38,7 @@ export const injectPermission = fp(async (server) => { type: ActorType.SERVICE, id: req.auth.serviceTokenId, orgId: req.auth.orgId, + parentOrgId: req.auth.orgId, authMethod: null }; @@ -47,6 +50,7 @@ export const injectPermission = fp(async (server) => { type: ActorType.SCIM_CLIENT, id: req.auth.scimTokenId, orgId: req.auth.orgId, + parentOrgId: req.auth.orgId, authMethod: null }; diff --git a/backend/src/services/auth-token/auth-token-service.ts b/backend/src/services/auth-token/auth-token-service.ts index 82df0dcb1..b30e3beaf 100644 --- a/backend/src/services/auth-token/auth-token-service.ts +++ b/backend/src/services/auth-token/auth-token-service.ts @@ -3,17 +3,19 @@ import { Knex } from "knex"; import { AccessScope, TAuthTokens, TAuthTokenSessions } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; -import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { AuthModeJwtTokenPayload, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "../auth/auth-type"; import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; import { TUserDALFactory } from "../user/user-dal"; import { TTokenDALFactory } from "./auth-token-dal"; import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types"; +import { TOrgDALFactory } from "../org/org-dal"; type TAuthTokenServiceFactoryDep = { tokenDAL: TTokenDALFactory; userDAL: Pick; + orgDAL: Pick; membershipUserDAL: Pick; }; @@ -80,7 +82,7 @@ export const getTokenConfig = (tokenType: TokenType) => { } }; -export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL }: TAuthTokenServiceFactoryDep) => { +export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgDAL }: TAuthTokenServiceFactoryDep) => { const createTokenForUser = async ({ type, userId, orgId, aliasId, payload }: TCreateTokenForUserDTO) => { const { token, ...tkCfg } = getTokenConfig(type); const appCfg = getConfig(); @@ -194,7 +196,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL }: TA }; // to parse jwt identity in inject identity plugin - const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload) => { + const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload, subOrganizationSelector?: string) => { const session = await tokenDAL.findOneTokenSession({ id: token.tokenVersionId, userId: token.userId @@ -207,22 +209,53 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL }: TA const user = await userDAL.findById(session.userId); if (!user || !user.isAccepted) throw new NotFoundError({ message: `User with ID '${session.userId}' not found` }); + let orgId = ""; + let parentOrgId = ""; if (token.organizationId) { - const orgMembership = await membershipUserDAL.findOne({ - actorUserId: user.id, - scopeOrgId: token.organizationId, - scope: AccessScope.Organization - }); + if (subOrganizationSelector) { + const subOrganization = await orgDAL.findOne({ + parentOrgId: token.organizationId, + slug: subOrganizationSelector + }); + if (!subOrganization) + throw new BadRequestError({ message: `Sub organization ${subOrganizationSelector} not found` }); - if (!orgMembership) { - throw new ForbiddenRequestError({ message: "User not member of organization" }); - } - if (!orgMembership.isActive) { - throw new ForbiddenRequestError({ message: "User organization membership is inactive" }); + const orgMembership = await membershipUserDAL.findOne({ + actorUserId: user.id, + scopeOrgId: subOrganization.id, + scope: AccessScope.Organization + }); + + if (!orgMembership) { + throw new ForbiddenRequestError({ message: "User not member of organization" }); + } + + if (!orgMembership.isActive) { + throw new ForbiddenRequestError({ message: "User organization membership is inactive" }); + } + orgId = subOrganization.id; + parentOrgId = token.organizationId; + } else { + const orgMembership = await membershipUserDAL.findOne({ + actorUserId: user.id, + scopeOrgId: token.organizationId, + scope: AccessScope.Organization + }); + + if (!orgMembership) { + throw new ForbiddenRequestError({ message: "User not member of organization" }); + } + + if (!orgMembership.isActive) { + throw new ForbiddenRequestError({ message: "User organization membership is inactive" }); + } + + orgId = token.organizationId; + parentOrgId = token.organizationId; } } - return { user, tokenVersionId: token.tokenVersionId, orgId: token.organizationId }; + return { user, tokenVersionId: token.tokenVersionId, orgId, parentOrgId }; }; return { diff --git a/backend/src/services/identity-access-token/identity-access-token-dal.ts b/backend/src/services/identity-access-token/identity-access-token-dal.ts index 19de362d8..ffdb78645 100644 --- a/backend/src/services/identity-access-token/identity-access-token-dal.ts +++ b/backend/src/services/identity-access-token/identity-access-token-dal.ts @@ -19,6 +19,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityAccessToken}.identityId`) .select(selectAllTableCols(TableName.IdentityAccessToken)) .select(db.ref("name").withSchema(TableName.Identity)) + .select(db.ref("orgId").withSchema(TableName.Identity).as("identityScopeOrgId")) .first(); return doc; diff --git a/backend/src/services/identity-access-token/identity-access-token-service.ts b/backend/src/services/identity-access-token/identity-access-token-service.ts index 1f6e4616b..0bca28903 100644 --- a/backend/src/services/identity-access-token/identity-access-token-service.ts +++ b/backend/src/services/identity-access-token/identity-access-token-service.ts @@ -10,6 +10,7 @@ import { TIdentityDALFactory } from "../identity/identity-dal"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; import { TIdentityAccessTokenDALFactory } from "./identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload, TRenewAccessTokenDTO } from "./identity-access-token-types"; +import { TOrgDALFactory } from "../org/org-dal"; type TIdentityAccessTokenServiceFactoryDep = { identityAccessTokenDAL: TIdentityAccessTokenDALFactory; @@ -19,6 +20,7 @@ type TIdentityAccessTokenServiceFactoryDep = { "updateIdentityAccessTokenStatus" | "getIdentityTokenDetailsInCache" >; membershipIdentityDAL: Pick; + orgDAL: Pick; }; export type TIdentityAccessTokenServiceFactory = ReturnType; @@ -27,7 +29,8 @@ export const identityAccessTokenServiceFactory = ({ identityAccessTokenDAL, accessTokenQueue, identityDAL, - membershipIdentityDAL + membershipIdentityDAL, + orgDAL }: TIdentityAccessTokenServiceFactoryDep) => { const validateAccessTokenExp = async (identityAccessToken: TIdentityAccessTokens) => { const { @@ -181,7 +184,11 @@ export const identityAccessTokenServiceFactory = ({ return { revokedToken }; }; - const fnValidateIdentityAccessToken = async (token: TIdentityAccessTokenJwtPayload, ipAddress?: string) => { + const fnValidateIdentityAccessToken = async ( + token: TIdentityAccessTokenJwtPayload, + subOrganizationSelector?: string, + ipAddress?: string + ) => { const identityAccessToken = await identityAccessTokenDAL.findOne({ [`${TableName.IdentityAccessToken}.id` as "id"]: token.identityAccessTokenId, isAccessTokenRevoked: false @@ -202,13 +209,36 @@ export const identityAccessTokenServiceFactory = ({ trustedIps: trustedIps as TIp[] }); } - const identityOrgMembership = await membershipIdentityDAL.findOne({ - scope: AccessScope.Organization, - actorIdentityId: identityAccessToken.identityId - }); + let orgId = ""; + const parentOrgId = identityAccessToken.identityScopeOrgId; - if (!identityOrgMembership) { - throw new BadRequestError({ message: "Identity does not belong to any organization" }); + if (subOrganizationSelector) { + const subOrganization = await orgDAL.findOne({ parentOrgId, slug: subOrganizationSelector }); + if (!subOrganizationSelector) + throw new BadRequestError({ message: `Sub organization ${subOrganizationSelector} not found` }); + + const identityOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identityAccessToken.identityId, + scopeOrgId: subOrganization.id + }); + + if (!identityOrgMembership) { + throw new BadRequestError({ message: "Identity does not belong to any organization" }); + } + orgId = subOrganization.id; + } else { + const identityOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identityAccessToken.identityId, + scopeOrgId: parentOrgId + }); + + if (!identityOrgMembership) { + throw new BadRequestError({ message: "Identity does not belong to any organization" }); + } + + orgId = parentOrgId; } let { accessTokenNumUses } = identityAccessToken; @@ -219,7 +249,7 @@ export const identityAccessTokenServiceFactory = ({ await validateAccessTokenExp({ ...identityAccessToken, accessTokenNumUses }); await accessTokenQueue.updateIdentityAccessTokenStatus(identityAccessToken.id, Number(accessTokenNumUses) + 1); - return { ...identityAccessToken, orgId: identityOrgMembership.scopeOrgId }; + return { ...identityAccessToken, orgId, parentOrgId }; }; return { renewAccessToken, revokeAccessToken, fnValidateIdentityAccessToken }; diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index d987c890c..077ddcdfb 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -27,6 +27,7 @@ import { import { generateKnexQueryFromScim } from "@app/lib/knex/scim"; import { OrgAuthMethod } from "./org-types"; +import { ActorType } from "../auth/auth-type"; export type TOrgDALFactory = ReturnType; @@ -64,6 +65,7 @@ export const orgDALFactory = (db: TDbClient) => { const buildBaseQuery = (orgIdSubquery: Knex.QueryBuilder) => { return db .replicaNode()(TableName.Organization) + .whereNull(`${TableName.Organization}.parentOrgId`) .whereIn(`${TableName.Organization}.id`, orgIdSubquery) .leftJoin(TableName.Project, `${TableName.Organization}.id`, `${TableName.Project}.orgId`) .leftJoin(TableName.Membership, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`) @@ -154,11 +156,47 @@ export const orgDALFactory = (db: TDbClient) => { } }; + const listSubOrganizations = async (dto: { + actorId: string; + actorType: ActorType; + orgId: string; + isAccessible?: boolean; + limit?: number; + offset?: number; + }) => { + try { + // TODO(sub-org:group): check this when implement group support + const query = db + .replicaNode()(TableName.Organization) + .where(`${TableName.Organization}.parentOrgId`, dto.orgId) + .select(selectAllTableCols(TableName.Organization)); + + if (dto.isAccessible) { + void query.leftJoin(`${TableName.Membership}`, (qb) => { + void qb.on(`${TableName.Membership}.scope`, AccessScope.Organization); + if (dto.actorType === ActorType.IDENTITY) { + void qb.andOn(`${TableName.Membership}.actorIdentityId`, dto.actorId); + } else { + void qb.andOn(`${TableName.Membership}.actorUserId`, dto.actorId); + } + }); + } + if (dto.limit) void query.limit(dto.limit); + if (dto.offset) void query.offset(dto.offset); + + const orgs = await query; + return orgs; + } catch (error) { + throw new DatabaseError({ error, name: "List sub organization" }); + } + }; + const findOrgById = async (orgId: string) => { try { const org = (await db .replicaNode()(TableName.Organization) .where({ [`${TableName.Organization}.id` as "id"]: orgId }) + .whereNull(`${TableName.Organization}.parentOrgId`) .leftJoin(TableName.SamlConfig, (qb) => { qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn( `${TableName.SamlConfig}.isActive`, @@ -195,6 +233,7 @@ export const orgDALFactory = (db: TDbClient) => { try { const org = (await db .replicaNode()(TableName.Organization) + .whereNull(`${TableName.Organization}.parentOrgId`) .where({ [`${TableName.Organization}.slug` as "slug"]: orgSlug }) .leftJoin(TableName.SamlConfig, (qb) => { qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn( @@ -240,6 +279,7 @@ export const orgDALFactory = (db: TDbClient) => { .whereNotNull(`${TableName.Membership}.actorUserId`) .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) .join(TableName.Organization, `${TableName.Membership}.scopeOrgId`, `${TableName.Organization}.id`) + .whereNull(`${TableName.Organization}.parentOrgId`) .leftJoin(TableName.SamlConfig, (qb) => { qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn( `${TableName.SamlConfig}.isActive`, @@ -337,6 +377,7 @@ export const orgDALFactory = (db: TDbClient) => { } }; + // TODO(sub-org): updated this logic later const countAllOrgMembers = async (orgId: string) => { try { interface CountResult { @@ -610,6 +651,7 @@ export const orgDALFactory = (db: TDbClient) => { }) .join(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`) .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`) + .whereNull(`${TableName.Organization}.parentOrgId`) .leftJoin(TableName.UserAliases, function joinUserAlias() { this.on(`${TableName.UserAliases}.userId`, "=", `${TableName.Membership}.actorUserId`) .andOn(`${TableName.UserAliases}.orgId`, "=", `${TableName.Membership}.scopeOrgId`) @@ -648,6 +690,7 @@ export const orgDALFactory = (db: TDbClient) => { .replicaNode()(TableName.Membership) .where({ actorIdentityId: identityId }) .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNull(`${TableName.Organization}.parentOrgId`) .whereNotNull(`${TableName.Membership}.actorIdentityId`) .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) .join(TableName.Organization, `${TableName.Membership}.scopeOrgId`, `${TableName.Organization}.id`) @@ -667,6 +710,7 @@ export const orgDALFactory = (db: TDbClient) => { findOrgByProjectId, findAllOrgMembers, countAllOrgMembers, + listSubOrganizations, findOrgById, findOrgBySlug, findAllOrgsByUserId, From 9a7849c12b4d765dcbd340d5d7a6b579411d50e7 Mon Sep 17 00:00:00 2001 From: = Date: Sat, 18 Oct 2025 22:10:03 +0530 Subject: [PATCH 02/44] feat: changed all org permission signature --- backend/src/@types/fastify.d.ts | 2 +- .../db/migrations/20251018061215_sub-org.ts | 1 + .../audit-log-stream-service.ts | 78 +++---- .../services/audit-log/audit-log-service.ts | 9 +- .../dynamic-secret/dynamic-secret-service.ts | 16 +- .../external-kms/external-kms-service.ts | 43 ++-- .../services/gateway-v2/gateway-v2-service.ts | 56 ++--- .../ee/services/gateway/gateway-service.ts | 70 ++++--- .../github-org-sync-service.ts | 92 +++++---- .../src/ee/services/group/group-service.ts | 53 ++--- .../identity-auth-template-service.ts | 57 ++--- .../services/kmip/kmip-operation-service.ts | 57 ++--- backend/src/ee/services/kmip/kmip-service.ts | 25 ++- .../ldap-config/ldap-config-service.ts | 72 ++++++- .../ee/services/license/license-service.ts | 145 +++++++++++-- .../ee/services/oidc/oidc-config-service.ts | 48 +++-- .../pam-account/pam-account-service.ts | 17 +- .../pam-session/pam-session-service.ts | 32 +-- .../project-template-service.ts | 92 +++++---- .../src/ee/services/relay/relay-service.ts | 46 +++-- .../saml-config/saml-config-service.ts | 34 ++- backend/src/ee/services/scim/scim-service.ts | 26 ++- .../secret-scanning-service.ts | 53 ++++- backend/src/lib/types/index.ts | 7 + backend/src/server/routes/index.ts | 16 +- .../app-connection/app-connection-service.ts | 137 +++++++------ .../services/auth-token/auth-token-service.ts | 2 +- ...external-group-org-role-mapping-service.ts | 31 +-- .../external-migration-service.ts | 194 ++++++++++-------- .../identity-access-token-service.ts | 2 +- .../identity-alicloud-auth-service.ts | 41 ++-- .../identity-aws-auth-service.ts | 43 ++-- .../identity-azure-auth-service.ts | 41 ++-- .../identity-gcp-auth-service.ts | 41 ++-- .../identity-jwt-auth-service.ts | 41 ++-- .../identity-kubernetes-auth-service.ts | 62 +++--- .../identity-ldap-auth-service.ts | 50 +++-- .../identity-oci-auth-service.ts | 43 ++-- .../identity-oidc-auth-service.ts | 43 ++-- .../identity-tls-cert-auth-service.ts | 44 ++-- .../identity-token-auth-service.ts | 99 +++++---- .../identity-ua/identity-ua-service.ts | 132 ++++++------ .../src/services/identity/identity-service.ts | 59 ++++-- .../org/org-membership-group-factory.ts | 47 +++-- .../org/org-membership-identity-factory.ts | 48 +++-- .../org/org-membership-user-factory.ts | 77 +++---- .../microsoft-teams-service.ts | 73 ++++--- .../services/org-admin/org-admin-service.ts | 20 +- backend/src/services/org/org-dal.ts | 2 +- backend/src/services/org/org-service.ts | 161 ++++++++++----- .../src/services/project/project-service.ts | 44 ++-- .../src/services/role/org/org-role-factory.ts | 92 +++++---- backend/src/services/role/role-service.ts | 17 +- .../secret-sharing/secret-sharing-service.ts | 59 ++++-- backend/src/services/slack/slack-service.ts | 64 +++--- .../super-admin/super-admin-service.ts | 2 +- backend/src/services/user/user-service.ts | 11 +- .../workflow-integration-service.ts | 10 +- 58 files changed, 1797 insertions(+), 1182 deletions(-) diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 59ca1cf55..1a28a6879 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -48,6 +48,7 @@ import { TSshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh import { TSshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service"; import { TSshHostServiceFactory } from "@app/ee/services/ssh-host/ssh-host-service"; import { TSshHostGroupServiceFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-service"; +import { TSubOrgServiceFactory } from "@app/ee/services/sub-org/sub-org-service"; import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-types"; import { TAuthMode } from "@app/server/plugins/auth/inject-identity"; import { TAdditionalPrivilegeServiceFactory } from "@app/services/additional-privilege/additional-privilege-service"; @@ -126,7 +127,6 @@ import { TUserServiceFactory } from "@app/services/user/user-service"; import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service"; import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service"; import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service"; -import { TSubOrgServiceFactory } from "@app/ee/services/sub-org/sub-org-service"; declare module "@fastify/request-context" { interface RequestContextData { diff --git a/backend/src/db/migrations/20251018061215_sub-org.ts b/backend/src/db/migrations/20251018061215_sub-org.ts index 3824bf68c..0b2f72abf 100644 --- a/backend/src/db/migrations/20251018061215_sub-org.ts +++ b/backend/src/db/migrations/20251018061215_sub-org.ts @@ -1,4 +1,5 @@ import { Knex } from "knex"; + import { TableName } from "../schemas"; export async function up(knex: Knex): Promise { diff --git a/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts b/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts index 5dd0fd4ba..b3cd34ac9 100644 --- a/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts +++ b/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { AxiosError } from "axios"; -import { TAuditLogs } from "@app/db/schemas"; +import { OrganizationActionScope, TAuditLogs } from "@app/db/schemas"; import { decryptLogStream, decryptLogStreamCredentials, @@ -45,13 +45,14 @@ export const auditLogStreamServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: actor.type, + actorId: actor.id, + orgId: actor.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); @@ -94,13 +95,14 @@ export const auditLogStreamServiceFactory = ({ const logStream = await auditLogStreamDAL.findById(logStreamId); if (!logStream) throw new NotFoundError({ message: `Audit Log Stream with ID '${logStreamId}' not found` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - logStream.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: actor.type, + actorId: actor.id, + orgId: actor.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); @@ -160,13 +162,14 @@ export const auditLogStreamServiceFactory = ({ const logStream = await auditLogStreamDAL.findById(logStreamId); if (!logStream) throw new NotFoundError({ message: `Audit Log Stream with ID '${logStreamId}' not found` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - logStream.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: actor.type, + actorId: actor.id, + orgId: actor.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings); @@ -185,14 +188,14 @@ export const auditLogStreamServiceFactory = ({ const logStream = await auditLogStreamDAL.findById(logStreamId); if (!logStream) throw new NotFoundError({ message: `Audit log stream with ID '${logStreamId}' not found` }); - - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - logStream.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: actor.type, + actorId: actor.id, + orgId: actor.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); @@ -206,13 +209,14 @@ export const auditLogStreamServiceFactory = ({ }; const list = async (actor: OrgServiceActor) => { - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: actor.type, + actorId: actor.id, + orgId: actor.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); diff --git a/backend/src/ee/services/audit-log/audit-log-service.ts b/backend/src/ee/services/audit-log/audit-log-service.ts index ece5edaf9..eab7792f8 100644 --- a/backend/src/ee/services/audit-log/audit-log-service.ts +++ b/backend/src/ee/services/audit-log/audit-log-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { requestContext } from "@fastify/request-context"; -import { ActionProjectType } from "@app/db/schemas"; +import { ActionProjectType, OrganizationActionScope } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; import { ActorType } from "@app/services/auth/auth-type"; @@ -47,13 +47,14 @@ export const auditLogServiceFactory = ({ ); } else { // Organization-wide logs - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionAuditLogsActions.Read, diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts index 659e07bca..974f5dd35 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError, subject } from "@casl/ability"; -import { ActionProjectType } from "@app/db/schemas"; +import { ActionProjectType, OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { @@ -134,13 +134,14 @@ export const dynamicSecretServiceFactory = ({ isGatewayV1 = false; } - const { permission: orgPermission } = await permissionService.getOrgPermission( + const { permission: orgPermission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - gateway?.orgId ?? gatewayv2?.orgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(orgPermission).throwUnlessCan( OrgPermissionGatewayActions.AttachGateways, @@ -297,13 +298,14 @@ export const dynamicSecretServiceFactory = ({ isGatewayV1 = false; } - const { permission: orgPermission } = await permissionService.getOrgPermission( + const { permission: orgPermission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(orgPermission).throwUnlessCan( OrgPermissionGatewayActions.AttachGateways, diff --git a/backend/src/ee/services/external-kms/external-kms-service.ts b/backend/src/ee/services/external-kms/external-kms-service.ts index d515c5973..9614f3298 100644 --- a/backend/src/ee/services/external-kms/external-kms-service.ts +++ b/backend/src/ee/services/external-kms/external-kms-service.ts @@ -3,6 +3,7 @@ import { STSServiceException } from "@aws-sdk/client-sts"; import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; +import { OrganizationActionScope } from "@app/db/schemas"; import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; @@ -51,13 +52,14 @@ export const externalKmsServiceFactory = ({ actorOrgId, actorAuthMethod }: TCreateExternalKmsDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Kms); const plan = await licenseService.getPlan(actorOrgId); @@ -154,13 +156,14 @@ export const externalKmsServiceFactory = ({ actorAuthMethod }: TUpdateExternalKmsDTO) => { const kmsDoc = await kmsDAL.findById(kmsId); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - kmsDoc.orgId, + orgId: kmsDoc.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms); const plan = await licenseService.getPlan(kmsDoc.orgId); @@ -257,13 +260,14 @@ export const externalKmsServiceFactory = ({ const deleteById = async ({ actor, id: kmsId, actorId, actorOrgId, actorAuthMethod }: TDeleteExternalKmsDTO) => { const kmsDoc = await kmsDAL.findById(kmsId); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - kmsDoc.orgId, + orgId: kmsDoc.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms); const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); @@ -278,13 +282,14 @@ export const externalKmsServiceFactory = ({ }; const list = async ({ actor, actorId, actorOrgId, actorAuthMethod }: TListExternalKmsDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms); const externalKmsDocs = await externalKmsDAL.find({ orgId: actorOrgId }); @@ -294,13 +299,14 @@ export const externalKmsServiceFactory = ({ const findById = async ({ actor, actorId, actorOrgId, actorAuthMethod, id: kmsId }: TGetExternalKmsByIdDTO) => { const kmsDoc = await kmsDAL.findById(kmsId); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - kmsDoc.orgId, + orgId: kmsDoc.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms); @@ -342,13 +348,14 @@ export const externalKmsServiceFactory = ({ name: kmsName }: TGetExternalKmsBySlugDTO) => { const kmsDoc = await kmsDAL.findOne({ name: kmsName, orgId: actorOrgId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - kmsDoc.orgId, + orgId: kmsDoc.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms); const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); diff --git a/backend/src/ee/services/gateway-v2/gateway-v2-service.ts b/backend/src/ee/services/gateway-v2/gateway-v2-service.ts index a2d323790..fd4954a00 100644 --- a/backend/src/ee/services/gateway-v2/gateway-v2-service.ts +++ b/backend/src/ee/services/gateway-v2/gateway-v2-service.ts @@ -3,7 +3,7 @@ import net from "node:net"; import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; -import { OrgMembershipRole, TRelays } from "@app/db/schemas"; +import { OrganizationActionScope, OrgMembershipRole, TRelays } from "@app/db/schemas"; import { PgSqlLock } from "@app/keystore/keystore"; import { crypto } from "@app/lib/crypto"; import { DatabaseErrorCode } from "@app/lib/error-codes"; @@ -73,13 +73,14 @@ export const gatewayV2ServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: ActorType.IDENTITY, actorId, orgId, actorAuthMethod, - orgId - ); + actorOrgId: orgId + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.CreateGateways, @@ -258,13 +259,14 @@ export const gatewayV2ServiceFactory = ({ }; const listGateways = async ({ orgPermission }: { orgPermission: OrgServiceActor }) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: orgPermission.type, + actorId: orgPermission.id, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.ListGateways, @@ -815,13 +817,14 @@ export const gatewayV2ServiceFactory = ({ throw new NotFoundError({ message: `Gateway ${id} not found` }); } - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - gateway.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: orgPermission.type, + actorId: orgPermission.id, + orgId: gateway.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.DeleteGateways, @@ -845,13 +848,14 @@ export const gatewayV2ServiceFactory = ({ }; const getPamSessionKey = async ({ orgPermission }: { orgPermission: OrgServiceActor }) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: orgPermission.type, + actorId: orgPermission.id, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.CreateGateways, diff --git a/backend/src/ee/services/gateway/gateway-service.ts b/backend/src/ee/services/gateway/gateway-service.ts index 5c8ad80bf..261640cc9 100644 --- a/backend/src/ee/services/gateway/gateway-service.ts +++ b/backend/src/ee/services/gateway/gateway-service.ts @@ -2,6 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; import { z } from "zod"; +import { OrganizationActionScope } from "@app/db/schemas"; import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; @@ -68,13 +69,14 @@ export const gatewayServiceFactory = ({ "Gateway handshake failed due to organization plan restrictions. Please upgrade your instance to Infisical's Enterprise plan." }); } - const { permission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, + const { permission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, actorId, orgId, actorAuthMethod, - orgId - ); + actorOrgId: orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway @@ -480,13 +482,14 @@ export const gatewayServiceFactory = ({ }; const listGateways = async ({ orgPermission }: TListGatewaysDTO) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: orgPermission.type, + actorId: orgPermission.id, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway @@ -501,13 +504,14 @@ export const gatewayServiceFactory = ({ }; const getGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: orgPermission.type, + actorId: orgPermission.id, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway @@ -521,13 +525,14 @@ export const gatewayServiceFactory = ({ }; const updateGatewayById = async ({ orgPermission, id, name }: TUpdateGatewayByIdDTO) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: orgPermission.type, + actorId: orgPermission.id, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.EditGateways, OrgPermissionSubjects.Gateway @@ -542,13 +547,14 @@ export const gatewayServiceFactory = ({ }; const deleteGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: orgPermission.type, + actorId: orgPermission.id, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway diff --git a/backend/src/ee/services/github-org-sync/github-org-sync-service.ts b/backend/src/ee/services/github-org-sync/github-org-sync-service.ts index b2bcb4ef3..d2713f269 100644 --- a/backend/src/ee/services/github-org-sync/github-org-sync-service.ts +++ b/backend/src/ee/services/github-org-sync/github-org-sync-service.ts @@ -6,7 +6,7 @@ import { paginateGraphql } from "@octokit/plugin-paginate-graphql"; import { Octokit as OctokitRest } from "@octokit/rest"; import RE2 from "re2"; -import { AccessScope, OrgMembershipRole } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope, OrgMembershipRole } from "@app/db/schemas"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; @@ -104,13 +104,14 @@ export const githubOrgSyncServiceFactory = ({ githubOrgAccessToken, isActive }: TCreateGithubOrgSyncDTO) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor: orgPermission.type, + actorId: orgPermission.id, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.GithubOrgSync); const plan = await licenseService.getPlan(orgPermission.orgId); @@ -162,13 +163,14 @@ export const githubOrgSyncServiceFactory = ({ githubOrgAccessToken, isActive }: TUpdateGithubOrgSyncDTO) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: orgPermission.type, + scope: OrganizationActionScope.ParentOrganization, + actorId: orgPermission.id, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.GithubOrgSync); const plan = await licenseService.getPlan(orgPermission.orgId); @@ -226,13 +228,14 @@ export const githubOrgSyncServiceFactory = ({ }; const deleteGithubOrgSync = async ({ orgPermission }: TDeleteGithubOrgSyncDTO) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: orgPermission.type, + actorId: orgPermission.id, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.GithubOrgSync); @@ -256,13 +259,14 @@ export const githubOrgSyncServiceFactory = ({ }; const getGithubOrgSync = async ({ orgPermission }: TDeleteGithubOrgSyncDTO) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actorId: orgPermission.id, + actor: orgPermission.type, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.GithubOrgSync); @@ -422,13 +426,14 @@ export const githubOrgSyncServiceFactory = ({ }; const validateGithubToken = async ({ orgPermission, githubOrgAccessToken }: TValidateGithubTokenDTO) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actorId: orgPermission.id, + actor: orgPermission.type, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.GithubOrgSync); @@ -509,13 +514,14 @@ export const githubOrgSyncServiceFactory = ({ }; const syncAllTeams = async ({ orgPermission }: TSyncAllTeamsDTO): Promise => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor: orgPermission.type, + orgId: orgPermission.orgId, + actorId: orgPermission.id, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionActions.Edit, diff --git a/backend/src/ee/services/group/group-service.ts b/backend/src/ee/services/group/group-service.ts index 075488488..0ffd77f0d 100644 --- a/backend/src/ee/services/group/group-service.ts +++ b/backend/src/ee/services/group/group-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; -import { AccessScope, OrgMembershipRole, TRoles } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope, OrgMembershipRole, TRoles } from "@app/db/schemas"; import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; @@ -73,13 +73,14 @@ export const groupServiceFactory = ({ const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Create, OrgPermissionSubjects.Groups); const plan = await licenseService.getPlan(actorOrgId); @@ -167,13 +168,14 @@ export const groupServiceFactory = ({ }: TUpdateGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups); @@ -270,13 +272,14 @@ export const groupServiceFactory = ({ const deleteGroup = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Delete, OrgPermissionSubjects.Groups); const plan = await licenseService.getPlan(actorOrgId); @@ -297,17 +300,18 @@ export const groupServiceFactory = ({ const getGroupById = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TGetGroupByIdDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); const group = await groupDAL.findById(id); - if (!group) { + if (!group || group.orgId !== actorOrgId) { throw new NotFoundError({ message: `Cannot find group with ID ${id}` }); @@ -330,13 +334,14 @@ export const groupServiceFactory = ({ }: TListGroupUsersDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); const group = await groupDAL.findOne({ @@ -365,13 +370,14 @@ export const groupServiceFactory = ({ const addUserToGroup = async ({ id, username, actor, actorId, actorAuthMethod, actorOrgId }: TAddUserToGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups); // check if group with slug exists @@ -451,13 +457,14 @@ export const groupServiceFactory = ({ }: TRemoveUserFromGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups); // check if group with slug exists diff --git a/backend/src/ee/services/identity-auth-template/identity-auth-template-service.ts b/backend/src/ee/services/identity-auth-template/identity-auth-template-service.ts index ef071742d..10aa3b190 100644 --- a/backend/src/ee/services/identity-auth-template/identity-auth-template-service.ts +++ b/backend/src/ee/services/identity-auth-template/identity-auth-template-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { OrganizationActionScope } from "@app/db/schemas"; import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { @@ -68,13 +69,14 @@ export const identityAuthTemplateServiceFactory = ({ templateFields: Record; } & Omit) => { await $checkPlan(actorOrgId); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate @@ -113,13 +115,14 @@ export const identityAuthTemplateServiceFactory = ({ throw new NotFoundError({ message: "Template not found" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - template.orgId, + orgId: template.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate @@ -227,13 +230,14 @@ export const identityAuthTemplateServiceFactory = ({ throw new NotFoundError({ message: "Template not found" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - template.orgId, + orgId: template.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate @@ -282,13 +286,14 @@ export const identityAuthTemplateServiceFactory = ({ throw new NotFoundError({ message: "Template not found" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - template.orgId, + orgId: template.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate @@ -316,13 +321,14 @@ export const identityAuthTemplateServiceFactory = ({ actorOrgId }: TListIdentityAuthTemplatesDTO) => { await $checkPlan(actorOrgId); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate @@ -352,13 +358,14 @@ export const identityAuthTemplateServiceFactory = ({ actorOrgId }: TGetTemplatesByAuthMethodDTO) => { await $checkPlan(actorOrgId); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate @@ -385,13 +392,14 @@ export const identityAuthTemplateServiceFactory = ({ actorOrgId }: TFindTemplateUsagesDTO) => { await $checkPlan(actorOrgId); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate @@ -415,13 +423,14 @@ export const identityAuthTemplateServiceFactory = ({ actorOrgId }: TUnlinkTemplateUsageDTO) => { await $checkPlan(actorOrgId); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate diff --git a/backend/src/ee/services/kmip/kmip-operation-service.ts b/backend/src/ee/services/kmip/kmip-operation-service.ts index 27f59a99f..b3eace6bc 100644 --- a/backend/src/ee/services/kmip/kmip-operation-service.ts +++ b/backend/src/ee/services/kmip/kmip-operation-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { OrganizationActionScope } from "@app/db/schemas"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; @@ -46,13 +47,14 @@ export const kmipOperationServiceFactory = ({ actorAuthMethod, actorOrgId }: TKmipCreateDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); @@ -78,13 +80,14 @@ export const kmipOperationServiceFactory = ({ }; const destroy = async ({ projectId, id, clientId, actor, actorId, actorOrgId, actorAuthMethod }: TKmipDestroyDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); @@ -131,13 +134,14 @@ export const kmipOperationServiceFactory = ({ }; const get = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipGetDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); @@ -189,13 +193,14 @@ export const kmipOperationServiceFactory = ({ }; const activate = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipGetDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); @@ -226,13 +231,14 @@ export const kmipOperationServiceFactory = ({ }; const revoke = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipRevokeDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); @@ -287,13 +293,14 @@ export const kmipOperationServiceFactory = ({ actorAuthMethod, actorOrgId }: TKmipGetAttributesDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); @@ -339,13 +346,14 @@ export const kmipOperationServiceFactory = ({ }; const locate = async ({ projectId, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipLocateDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); @@ -377,13 +385,14 @@ export const kmipOperationServiceFactory = ({ actorOrgId, kmipMetadata }: TKmipRegisterDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); diff --git a/backend/src/ee/services/kmip/kmip-service.ts b/backend/src/ee/services/kmip/kmip-service.ts index 8daa5a37a..482eb41be 100644 --- a/backend/src/ee/services/kmip/kmip-service.ts +++ b/backend/src/ee/services/kmip/kmip-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; -import { ActionProjectType } from "@app/db/schemas"; +import { ActionProjectType, OrganizationActionScope } from "@app/db/schemas"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; import { isValidIp } from "@app/lib/ip"; @@ -401,13 +401,14 @@ export const kmipServiceFactory = ({ }; const setupOrgKmip = async ({ caKeyAlgorithm, actorOrgId, actor, actorId, actorAuthMethod }: TSetupOrgKmipDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip); const kmipConfig = await kmipOrgConfigDAL.findOne({ @@ -566,7 +567,14 @@ export const kmipServiceFactory = ({ }; const getOrgKmip = async ({ actorOrgId, actor, actorId, actorAuthMethod }: TGetOrgKmipDTO) => { - await permissionService.getOrgPermission(actor, actorId, actorOrgId, actorAuthMethod, actorOrgId); + await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); const kmipConfig = await kmipOrgConfigDAL.findOne({ orgId: actorOrgId @@ -759,13 +767,14 @@ export const kmipServiceFactory = ({ keyAlgorithm, hostnamesOrIps }: TRegisterServerDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts index 43ca5ab3d..86bfcc687 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -1,7 +1,14 @@ import { ForbiddenError } from "@casl/ability"; import { Knex } from "knex"; -import { AccessScope, OrgMembershipStatus, TableName, TLdapConfigsUpdate, TUsers } from "@app/db/schemas"; +import { + AccessScope, + OrganizationActionScope, + OrgMembershipStatus, + TableName, + TLdapConfigsUpdate, + TUsers +} from "@app/db/schemas"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; @@ -119,7 +126,14 @@ export const ldapConfigServiceFactory = ({ groupSearchFilter, caCert }: TCreateLdapCfgDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap); const plan = await licenseService.getPlan(orgId); @@ -238,7 +252,14 @@ export const ldapConfigServiceFactory = ({ groupSearchFilter, caCert }: TUpdateLdapCfgDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap); const plan = await licenseService.getPlan(orgId); @@ -316,7 +337,14 @@ export const ldapConfigServiceFactory = ({ actorAuthMethod, actorOrgId }: TGetLdapCfgDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap); return getLdapCfg({ orgId @@ -649,7 +677,14 @@ export const ldapConfigServiceFactory = ({ actorAuthMethod, actorOrgId }: TGetLdapGroupMapsDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap); const ldapConfig = await ldapConfigDAL.findOne({ @@ -678,7 +713,14 @@ export const ldapConfigServiceFactory = ({ actorAuthMethod, actorOrgId }: TCreateLdapGroupMapDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap); const plan = await licenseService.getPlan(orgId); @@ -732,7 +774,14 @@ export const ldapConfigServiceFactory = ({ actorAuthMethod, actorOrgId }: TDeleteLdapGroupMapDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Ldap); const plan = await licenseService.getPlan(orgId); @@ -771,7 +820,14 @@ export const ldapConfigServiceFactory = ({ caCert, url }: TTestLdapConnectionDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap); const plan = await licenseService.getPlan(orgId); diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index fba9d0cca..2d539dbf0 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -9,6 +9,7 @@ import { AxiosError } from "axios"; import { CronJob } from "cron"; import { Knex } from "knex"; +import { OrganizationActionScope } from "@app/db/schemas"; import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { verifyOfflineLicense } from "@app/lib/crypto"; @@ -319,7 +320,14 @@ export const licenseServiceFactory = ({ actorAuthMethod, billingCycle }: TOrgPlansTableDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); const { data } = await licenseServerCloudApi.request.get( `/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}` @@ -336,7 +344,14 @@ export const licenseServiceFactory = ({ projectId, refreshCache }: TOrgPlanDTO) => { - await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); if (refreshCache) { await refreshPlan(orgId); } @@ -352,7 +367,14 @@ export const licenseServiceFactory = ({ actorAuthMethod, success_url }: TStartOrgTrialDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionBillingActions.ManageBilling, OrgPermissionSubjects.Billing @@ -384,7 +406,14 @@ export const licenseServiceFactory = ({ actorAuthMethod, actorOrgId }: TCreateOrgPortalSession) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionBillingActions.ManageBilling, OrgPermissionSubjects.Billing @@ -433,7 +462,14 @@ export const licenseServiceFactory = ({ }; const getOrgBillingInfo = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); const organization = await orgDAL.findOrgById(orgId); @@ -516,7 +552,14 @@ export const licenseServiceFactory = ({ // returns org current plan feature table const getOrgPlanTable = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); const organization = await orgDAL.findOrgById(orgId); @@ -553,7 +596,14 @@ export const licenseServiceFactory = ({ }; const getOrgBillingDetails = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); const organization = await orgDAL.findOrgById(orgId); @@ -578,7 +628,14 @@ export const licenseServiceFactory = ({ name, email }: TUpdateOrgBillingDetailsDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionBillingActions.ManageBilling, OrgPermissionSubjects.Billing @@ -601,7 +658,14 @@ export const licenseServiceFactory = ({ }; const getOrgPmtMethods = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TOrgPmtMethodsDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); const organization = await orgDAL.findOrgById(orgId); @@ -628,7 +692,14 @@ export const licenseServiceFactory = ({ success_url, cancel_url }: TAddOrgPmtMethodDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionBillingActions.ManageBilling, OrgPermissionSubjects.Billing @@ -660,7 +731,14 @@ export const licenseServiceFactory = ({ orgId, pmtMethodId }: TDelOrgPmtMethodDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionBillingActions.ManageBilling, OrgPermissionSubjects.Billing @@ -692,7 +770,14 @@ export const licenseServiceFactory = ({ }; const getOrgTaxIds = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgTaxIdDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); const organization = await orgDAL.findOrgById(orgId); @@ -710,7 +795,14 @@ export const licenseServiceFactory = ({ }; const addOrgTaxId = async ({ actorId, actor, actorAuthMethod, actorOrgId, orgId, type, value }: TAddOrgTaxIdDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionBillingActions.ManageBilling, OrgPermissionSubjects.Billing @@ -734,7 +826,14 @@ export const licenseServiceFactory = ({ }; const delOrgTaxId = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId, taxId }: TDelOrgTaxIdDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionBillingActions.ManageBilling, OrgPermissionSubjects.Billing @@ -754,7 +853,14 @@ export const licenseServiceFactory = ({ }; const getOrgTaxInvoices = async ({ actorId, actor, actorOrgId, actorAuthMethod, orgId }: TOrgInvoiceDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); const organization = await orgDAL.findOrgById(orgId); @@ -771,7 +877,14 @@ export const licenseServiceFactory = ({ }; const getOrgLicenses = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TOrgLicensesDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); const organization = await orgDAL.findOrgById(orgId); diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index c2672a94e..e80ec7cf5 100644 --- a/backend/src/ee/services/oidc/oidc-config-service.ts +++ b/backend/src/ee/services/oidc/oidc-config-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client"; -import { AccessScope, OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope, OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas"; import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs"; import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; @@ -118,13 +118,14 @@ export const oidcConfigServiceFactory = ({ } if (dto.type === "external") { - const { permission } = await permissionService.getOrgPermission( - dto.actor, - dto.actorId, - dto.organizationId, - dto.actorAuthMethod, - dto.actorOrgId - ); + const { permission } = await permissionService.getOrgPermission({ + actorId: dto.actorId, + actor: dto.actor, + orgId: dto.organizationId, + actorOrgId: dto.actorOrgId, + actorAuthMethod: dto.actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso); } @@ -508,13 +509,14 @@ export const oidcConfigServiceFactory = ({ "Failed to update OIDC SSO configuration due to plan restriction. Upgrade plan to update SSO configuration." }); - const { permission } = await permissionService.getOrgPermission( - actor, + const { permission } = await permissionService.getOrgPermission({ actorId, - org.id, + actor, + orgId: org.id, + actorOrgId, actorAuthMethod, - actorOrgId - ); + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); if (org.googleSsoAuthEnforced && isActive) { @@ -602,13 +604,14 @@ export const oidcConfigServiceFactory = ({ "Failed to create OIDC SSO configuration due to plan restriction. Upgrade plan to update SSO configuration." }); - const { permission } = await permissionService.getOrgPermission( - actor, + const { permission } = await permissionService.getOrgPermission({ actorId, - org.id, + actor, + orgId: org.id, + actorOrgId, actorAuthMethod, - actorOrgId - ); + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso); if (org.googleSsoAuthEnforced && isActive) { @@ -764,7 +767,14 @@ export const oidcConfigServiceFactory = ({ }; const isOidcManageGroupMembershipsEnabled = async (orgId: string, actor: OrgServiceActor) => { - await permissionService.getOrgPermission(ActorType.USER, actor.id, orgId, actor.authMethod, actor.orgId); + await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: actor.id, + orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.ParentOrganization + }); const oidcConfig = await oidcConfigDAL.findOne({ orgId, diff --git a/backend/src/ee/services/pam-account/pam-account-service.ts b/backend/src/ee/services/pam-account/pam-account-service.ts index e9ea76e8c..b8dad991a 100644 --- a/backend/src/ee/services/pam-account/pam-account-service.ts +++ b/backend/src/ee/services/pam-account/pam-account-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError, subject } from "@casl/ability"; -import { ActionProjectType, TPamAccounts, TPamResources } from "@app/db/schemas"; +import { ActionProjectType, OrganizationActionScope, TPamAccounts, TPamResources } from "@app/db/schemas"; import { PAM_RESOURCE_FACTORY_MAP } from "@app/ee/services/pam-resource/pam-resource-factory"; import { decryptResource, decryptResourceConnectionDetails } from "@app/ee/services/pam-resource/pam-resource-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; @@ -459,13 +459,14 @@ export const pamAccountServiceFactory = ({ const project = await projectDAL.findById(session.projectId); if (!project) throw new NotFoundError({ message: `Project with ID '${session.projectId}' not found` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - project.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: project.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.CreateGateways, diff --git a/backend/src/ee/services/pam-session/pam-session-service.ts b/backend/src/ee/services/pam-session/pam-session-service.ts index 713383306..26ff7daa6 100644 --- a/backend/src/ee/services/pam-session/pam-session-service.ts +++ b/backend/src/ee/services/pam-session/pam-session-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { ActionProjectType } from "@app/db/schemas"; +import { ActionProjectType, OrganizationActionScope } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; @@ -102,13 +102,14 @@ export const pamSessionServiceFactory = ({ const project = await projectDAL.findById(session.projectId); if (!project) throw new NotFoundError({ message: `Project with ID '${session.projectId}' not found` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - project.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: project.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.CreateGateways, @@ -142,13 +143,14 @@ export const pamSessionServiceFactory = ({ const project = await projectDAL.findById(session.projectId); if (!project) throw new NotFoundError({ message: `Project with ID '${session.projectId}' not found` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - project.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: project.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.Any + }); if (actor.type === ActorType.IDENTITY) { ForbiddenError.from(permission).throwUnlessCan( diff --git a/backend/src/ee/services/project-template/project-template-service.ts b/backend/src/ee/services/project-template/project-template-service.ts index f3fe07aa8..5a9f04d8d 100644 --- a/backend/src/ee/services/project-template/project-template-service.ts +++ b/backend/src/ee/services/project-template/project-template-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { packRules } from "@casl/ability/extra"; -import { ProjectType, TProjectTemplates } from "@app/db/schemas"; +import { OrganizationActionScope, ProjectType, TProjectTemplates } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; @@ -59,13 +59,14 @@ export const projectTemplateServiceFactory = ({ message: "Failed to access project templates due to plan restriction. Upgrade plan to access project templates." }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: actor.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates); @@ -97,13 +98,14 @@ export const projectTemplateServiceFactory = ({ if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with Name "${name}"` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - projectTemplate.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: projectTemplate.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates); @@ -125,13 +127,14 @@ export const projectTemplateServiceFactory = ({ if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - projectTemplate.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: projectTemplate.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates); @@ -152,13 +155,14 @@ export const projectTemplateServiceFactory = ({ message: "Failed to create project template due to plan restriction. Upgrade plan to access project templates." }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: actor.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.ProjectTemplates); @@ -213,13 +217,14 @@ export const projectTemplateServiceFactory = ({ if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - projectTemplate.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: projectTemplate.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates); if (projectTemplate.type !== ProjectType.SecretManager && environments) @@ -272,13 +277,14 @@ export const projectTemplateServiceFactory = ({ if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - projectTemplate.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: projectTemplate.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates); diff --git a/backend/src/ee/services/relay/relay-service.ts b/backend/src/ee/services/relay/relay-service.ts index d791e9919..b2eb932ed 100644 --- a/backend/src/ee/services/relay/relay-service.ts +++ b/backend/src/ee/services/relay/relay-service.ts @@ -3,7 +3,7 @@ import { isIP } from "node:net"; import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; -import { OrgMembershipRole, TRelays } from "@app/db/schemas"; +import { OrganizationActionScope, OrgMembershipRole, TRelays } from "@app/db/schemas"; import { PgSqlLock } from "@app/keystore/keystore"; import { crypto } from "@app/lib/crypto"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; @@ -126,8 +126,8 @@ export const relayServiceFactory = ({ // generate instance relay CA const instanceRelayCaSerialNumber = createSerialNumber(); - const instanceRelayCaIssuedAt = new Date(); const instanceRelayCaExpiration = new Date(new Date().setFullYear(2045)); + const instanceRelayCaIssuedAt = new Date(); const instanceRelayCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); const instanceRelayCaSkObj = crypto.nativeCrypto.KeyObject.from(instanceRelayCaKeys.privateKey); const instanceRelayCaCert = await x509.X509CertificateGenerator.create({ @@ -972,13 +972,14 @@ export const relayServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityId, + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: ActorType.IDENTITY, + actorId: identityId, orgId, - actorAuthMethod!, - orgId - ); + actorAuthMethod: actorAuthMethod!, + actorOrgId: orgId + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionRelayActions.CreateRelays, @@ -1102,13 +1103,14 @@ export const relayServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityId, + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: ActorType.IDENTITY, + actorId: identityId, orgId, - actorAuthMethod!, - orgId - ); + actorAuthMethod: actorAuthMethod!, + actorOrgId: orgId + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionRelayActions.CreateRelays, OrgPermissionSubjects.Relay @@ -1155,13 +1157,14 @@ export const relayServiceFactory = ({ actorAuthMethod: ActorAuthMethod; actorOrgId: string; }) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, - actorAuthMethod, + orgId: actorOrgId, + actorAuthMethod: actorAuthMethod!, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionRelayActions.ListRelays, OrgPermissionSubjects.Relay); @@ -1189,13 +1192,14 @@ export const relayServiceFactory = ({ actorAuthMethod: ActorAuthMethod; actorOrgId: string; }) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionRelayActions.DeleteRelays, OrgPermissionSubjects.Relay); diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index ab84ebd39..abe8c3d2e 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -5,6 +5,7 @@ import RE2 from "re2"; import { AccessScope, + OrganizationActionScope, OrgMembershipRole, OrgMembershipStatus, TableName, @@ -251,7 +252,14 @@ export const samlConfigServiceFactory = ({ authProvider, enableGroupSync }) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso); const plan = await licenseService.getPlan(orgId); @@ -317,7 +325,14 @@ export const samlConfigServiceFactory = ({ authProvider, enableGroupSync }) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); const plan = await licenseService.getPlan(orgId); if (!plan.samlSSO) @@ -424,13 +439,14 @@ export const samlConfigServiceFactory = ({ // when dto is type id means it's internally used if (dto.type === "org") { - const { permission } = await permissionService.getOrgPermission( - dto.actor, - dto.actorId, - samlConfig.orgId, - dto.actorAuthMethod, - dto.actorOrgId - ); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor: dto.actor, + actorId: dto.actorId, + orgId: samlConfig.orgId, + actorAuthMethod: dto.actorAuthMethod, + actorOrgId: dto.actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso); } const { decryptor } = await kmsService.createCipherPairWithDataKey({ diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts index a08cd5dbf..e3a6d5e7e 100644 --- a/backend/src/ee/services/scim/scim-service.ts +++ b/backend/src/ee/services/scim/scim-service.ts @@ -4,6 +4,7 @@ import { scimPatch } from "scim-patch"; import { AccessScope, + OrganizationActionScope, OrgMembershipRole, OrgMembershipStatus, TableName, @@ -125,7 +126,14 @@ export const scimServiceFactory = ({ description, ttlDays }) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Scim); const plan = await licenseService.getPlan(orgId); @@ -160,7 +168,14 @@ export const scimServiceFactory = ({ actorAuthMethod, orgId }) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Scim); const plan = await licenseService.getPlan(orgId); @@ -183,13 +198,14 @@ export const scimServiceFactory = ({ let scimToken = await scimDAL.findById(scimTokenId); if (!scimToken) throw new NotFoundError({ message: `SCIM token with ID '${scimTokenId}' not found` }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, actor, actorId, - scimToken.orgId, + orgId: scimToken.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Scim); const plan = await licenseService.getPlan(scimToken.orgId); diff --git a/backend/src/ee/services/secret-scanning/secret-scanning-service.ts b/backend/src/ee/services/secret-scanning/secret-scanning-service.ts index 85a3cd5f2..a5fbe37a7 100644 --- a/backend/src/ee/services/secret-scanning/secret-scanning-service.ts +++ b/backend/src/ee/services/secret-scanning/secret-scanning-service.ts @@ -2,6 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import { WebhookEventMap } from "@octokit/webhooks-types"; import { ProbotOctokit } from "probot"; +import { OrganizationActionScope } from "@app/db/schemas"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; @@ -49,7 +50,14 @@ export const secretScanningServiceFactory = ({ }: TInstallAppSessionDTO) => { const appCfg = getConfig(); - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning); const sessionId = crypto.randomBytes(16).toString("hex"); @@ -68,13 +76,14 @@ export const secretScanningServiceFactory = ({ const session = await gitAppInstallSessionDAL.findOne({ sessionId }); if (!session) throw new NotFoundError({ message: "Session was not found" }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - session.orgId, + orgId: session.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning); const installatedApp = await gitAppOrgDAL.transaction(async (tx) => { await gitAppInstallSessionDAL.deleteById(session.id, tx); @@ -117,7 +126,14 @@ export const secretScanningServiceFactory = ({ actorAuthMethod, actorOrgId }: TGetOrgInstallStatusDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning); const appInstallation = await gitAppOrgDAL.findOne({ orgId }); @@ -125,7 +141,14 @@ export const secretScanningServiceFactory = ({ }; const getRisksByOrg = async ({ actor, orgId, actorId, actorAuthMethod, actorOrgId, filter }: TGetOrgRisksDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning); const results = await secretScanningDAL.findByOrgId(orgId, filter); @@ -134,7 +157,14 @@ export const secretScanningServiceFactory = ({ }; const getAllRisksByOrg = async ({ actor, orgId, actorId, actorAuthMethod, actorOrgId }: TGetAllOrgRisksDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning); const risks = await secretScanningDAL.find({ orgId }, { sort: [["createdAt", "desc"]] }); @@ -150,7 +180,14 @@ export const secretScanningServiceFactory = ({ riskId, status }: TUpdateRiskStatusDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning); const isRiskResolved = Boolean( diff --git a/backend/src/lib/types/index.ts b/backend/src/lib/types/index.ts index 1a2262bc1..ff6013b7f 100644 --- a/backend/src/lib/types/index.ts +++ b/backend/src/lib/types/index.ts @@ -81,6 +81,13 @@ export type OrgServiceActor = { parentOrgId: string; }; +export type ProjectServiceActor = { + type: ActorType; + id: string; + authMethod: ActorAuthMethod; + orgId: string; +}; + export enum QueueWorkerProfile { All = "all", Standard = "standard", diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 4a73a650c..bcf508835 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -131,6 +131,7 @@ import { sshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login- import { sshHostGroupDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-dal"; import { sshHostGroupMembershipDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-membership-dal"; import { sshHostGroupServiceFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-service"; +import { subOrgServiceFactory } from "@app/ee/services/sub-org/sub-org-service"; import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal"; import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service"; import { keyValueStoreDALFactory } from "@app/keystore/key-value-store-dal"; @@ -564,7 +565,7 @@ export const registerRoutes = async ( projectDAL }); - const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, membershipUserDAL }); + const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, membershipUserDAL, orgDAL }); const membershipUserService = membershipUserServiceFactory({ licenseService, @@ -904,6 +905,15 @@ export const registerRoutes = async ( userGroupMembershipDAL, additionalPrivilegeDAL }); + + const subOrgService = subOrgServiceFactory({ + licenseService, + membershipDAL, + membershipRoleDAL, + orgDAL, + permissionService + }); + const signupService = authSignupServiceFactory({ tokenService, smtpService, @@ -1601,7 +1611,8 @@ export const registerRoutes = async ( identityAccessTokenDAL, accessTokenQueue, identityDAL, - membershipIdentityDAL + membershipIdentityDAL, + orgDAL }); const identityTokenAuthService = identityTokenAuthServiceFactory({ @@ -2248,6 +2259,7 @@ export const registerRoutes = async ( groupProject: groupProjectService, permission: permissionService, org: orgService, + subOrganization: subOrgService, oidc: oidcService, apiKey: apiKeyService, authToken: tokenService, diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index 5e26ebdaf..d599568b3 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError, subject } from "@casl/ability"; -import { ActionProjectType, TAppConnections } from "@app/db/schemas"; +import { ActionProjectType, OrganizationActionScope, TAppConnections } from "@app/db/schemas"; import { ValidateOCIConnectionCredentialsSchema } from "@app/ee/services/app-connections/oci"; import { ociConnectionService } from "@app/ee/services/app-connections/oci/oci-connection-service"; import { ValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb"; @@ -215,13 +215,14 @@ export const appConnectionServiceFactory = ({ ) ); } else { - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionAppConnectionActions.Read, @@ -268,13 +269,14 @@ export const appConnectionServiceFactory = ({ subject(ProjectPermissionSub.AppConnections, { connectionId }) ); } else { - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - appConnection.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: appConnection.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionAppConnectionActions.Read, @@ -318,13 +320,14 @@ export const appConnectionServiceFactory = ({ subject(ProjectPermissionSub.AppConnections, { connectionId: appConnection.id }) ); } else { - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - appConnection.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: appConnection.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionAppConnectionActions.Read, @@ -342,13 +345,14 @@ export const appConnectionServiceFactory = ({ { method, app, credentials, gatewayId, projectId, ...params }: TCreateAppConnectionDTO, actor: OrgServiceActor ) => { - const { permission: orgPermission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { permission: orgPermission } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (projectId) { const project = await projectDAL.findProjectById(projectId); @@ -477,13 +481,14 @@ export const appConnectionServiceFactory = ({ "Failed to update app connection due to plan restriction. Upgrade plan to access enterprise app connections." ); - const { permission: orgPermission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - appConnection.orgId, - actor.authMethod, - actor.orgId - ); + const { permission: orgPermission } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: appConnection.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (appConnection.projectId) { const { permission } = await permissionService.getProjectPermission({ @@ -635,13 +640,14 @@ export const appConnectionServiceFactory = ({ subject(ProjectPermissionSub.AppConnections, { connectionId }) ); } else { - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - appConnection.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: appConnection.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionAppConnectionActions.Delete, @@ -704,13 +710,14 @@ export const appConnectionServiceFactory = ({ subject(ProjectPermissionSub.AppConnections, { connectionId }) ); } else { - const { permission: orgPermission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - appConnection.orgId, - actor.authMethod, - actor.orgId - ); + const { permission: orgPermission } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: appConnection.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(orgPermission).throwUnlessCan( OrgPermissionAppConnectionActions.Connect, @@ -747,13 +754,14 @@ export const appConnectionServiceFactory = ({ }; const listAvailableAppConnectionsForUser = async (app: AppConnection, actor: OrgServiceActor, projectId?: string) => { - const { permission: orgPermission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { permission: orgPermission } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); let availableProjectConnections: TAppConnections[] = []; @@ -805,13 +813,14 @@ export const appConnectionServiceFactory = ({ if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - appConnection.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: appConnection.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionAppConnectionActions.Read, diff --git a/backend/src/services/auth-token/auth-token-service.ts b/backend/src/services/auth-token/auth-token-service.ts index b30e3beaf..6d0dfcd4c 100644 --- a/backend/src/services/auth-token/auth-token-service.ts +++ b/backend/src/services/auth-token/auth-token-service.ts @@ -7,10 +7,10 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedErro import { AuthModeJwtTokenPayload, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "../auth/auth-type"; import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { TUserDALFactory } from "../user/user-dal"; import { TTokenDALFactory } from "./auth-token-dal"; import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types"; -import { TOrgDALFactory } from "../org/org-dal"; type TAuthTokenServiceFactoryDep = { tokenDAL: TTokenDALFactory; diff --git a/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts index a072544ca..e51d25ce3 100644 --- a/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts +++ b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; @@ -26,13 +27,14 @@ export const externalGroupOrgRoleMappingServiceFactory = ({ roleDAL }: TExternalGroupOrgRoleMappingServiceFactoryDep) => { const listExternalGroupOrgRoleMappings = async (actor: OrgServiceActor) => { - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: actor.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.ParentOrganization + }); // TODO: will need to change if we add support for ldap, oidc, etc. ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Scim); @@ -48,13 +50,14 @@ export const externalGroupOrgRoleMappingServiceFactory = ({ dto: TSyncExternalGroupOrgMembershipRoleMappingsDTO, actor: OrgServiceActor ) => { - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: actor.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.ParentOrganization + }); // TODO: will need to change if we add support for ldap, oidc, etc. ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim); diff --git a/backend/src/services/external-migration/external-migration-service.ts b/backend/src/services/external-migration/external-migration-service.ts index 4192ffcd5..3cc128768 100644 --- a/backend/src/services/external-migration/external-migration-service.ts +++ b/backend/src/services/external-migration/external-migration-service.ts @@ -1,4 +1,4 @@ -import { OrgMembershipRole } from "@app/db/schemas"; +import { OrganizationActionScope, OrgMembershipRole } from "@app/db/schemas"; import { AuditLogInfo, EventType, @@ -89,13 +89,14 @@ export const externalMigrationServiceFactory = ({ throw new BadRequestError({ message: "EnvKey migration is not supported when running in FIPS mode." }); } - const { hasRole } = await permissionService.getOrgPermission( - actor, + const { hasRole } = await permissionService.getOrgPermission({ actorId, + actor, + orgId: actorOrgId, actorOrgId, actorAuthMethod, - actorOrgId - ); + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can import data" }); } @@ -136,13 +137,14 @@ export const externalMigrationServiceFactory = ({ actorOrgId, actorAuthMethod }: TImportVaultDataDTO) => { - const { hasRole } = await permissionService.getOrgPermission( - actor, + const { hasRole } = await permissionService.getOrgPermission({ actorId, + actor, + orgId: actorOrgId, actorOrgId, actorAuthMethod, - actorOrgId - ); + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can import data" }); @@ -192,13 +194,14 @@ export const externalMigrationServiceFactory = ({ actorAuthMethod, provider }: THasCustomVaultMigrationDTO) => { - const { hasRole } = await permissionService.getOrgPermission( - actor, + const { hasRole } = await permissionService.getOrgPermission({ actorId, + actor, + orgId: actorOrgId, actorOrgId, actorAuthMethod, - actorOrgId - ); + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can check custom migration status" }); @@ -247,13 +250,14 @@ export const externalMigrationServiceFactory = ({ }; const createVaultExternalMigration = async ({ namespace, connectionId, actor }: TCreateVaultExternalMigrationDTO) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can configure vault external migration" }); @@ -298,13 +302,14 @@ export const externalMigrationServiceFactory = ({ connectionId, actor }: TUpdateVaultExternalMigrationDTO) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can update vault external migration" }); @@ -332,13 +337,14 @@ export const externalMigrationServiceFactory = ({ }; const getVaultExternalMigrationConfigs = async ({ actor }: { actor: OrgServiceActor }) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can view vault external migration configs" }); @@ -352,13 +358,14 @@ export const externalMigrationServiceFactory = ({ }; const getVaultNamespaces = async ({ actor }: { actor: OrgServiceActor }) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can view vault namespaces" }); @@ -380,13 +387,14 @@ export const externalMigrationServiceFactory = ({ }; const getVaultPolicies = async ({ actor, namespace }: { actor: OrgServiceActor; namespace: string }) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can view vault policies" }); @@ -422,13 +430,14 @@ export const externalMigrationServiceFactory = ({ }; const getVaultMounts = async ({ actor, namespace }: { actor: OrgServiceActor; namespace: string }) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can view vault mounts" }); @@ -472,13 +481,14 @@ export const externalMigrationServiceFactory = ({ namespace: string; mountPath: string; }) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can view vault secret paths" }); @@ -531,13 +541,14 @@ export const externalMigrationServiceFactory = ({ vaultSecretPath: string; auditLogInfo: AuditLogInfo; }) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can import vault secrets" }); @@ -617,13 +628,14 @@ export const externalMigrationServiceFactory = ({ }; const deleteVaultExternalMigration = async ({ id, actor }: TDeleteVaultExternalMigrationDTO) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can delete vault external migration configs" }); @@ -653,13 +665,14 @@ export const externalMigrationServiceFactory = ({ namespace: string; authType?: string; }) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can view vault auth mounts" }); @@ -704,13 +717,14 @@ export const externalMigrationServiceFactory = ({ namespace: string; mountPath: string; }) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can view vault Kubernetes auth roles" }); diff --git a/backend/src/services/identity-access-token/identity-access-token-service.ts b/backend/src/services/identity-access-token/identity-access-token-service.ts index 0bca28903..f565ebf65 100644 --- a/backend/src/services/identity-access-token/identity-access-token-service.ts +++ b/backend/src/services/identity-access-token/identity-access-token-service.ts @@ -8,9 +8,9 @@ import { TAccessTokenQueueServiceFactory } from "../access-token-queue/access-to import { AuthTokenType } from "../auth/auth-type"; import { TIdentityDALFactory } from "../identity/identity-dal"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { TIdentityAccessTokenDALFactory } from "./identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload, TRenewAccessTokenDTO } from "./identity-access-token-types"; -import { TOrgDALFactory } from "../org/org-dal"; type TIdentityAccessTokenServiceFactoryDep = { identityAccessTokenDAL: TIdentityAccessTokenDALFactory; 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 43584a1af..274703c60 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 @@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import { AxiosError } from "axios"; -import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, 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 { @@ -173,13 +173,14 @@ export const identityAliCloudAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -255,13 +256,14 @@ export const identityAliCloudAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -313,13 +315,14 @@ export const identityAliCloudAuthServiceFactory = ({ const alicloudIdentityAuth = await identityAliCloudAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...alicloudIdentityAuth, orgId: identityMembershipOrg.scopeOrgId }; }; @@ -344,22 +347,24 @@ export const identityAliCloudAuthServiceFactory = ({ message: "The identity does not have Alibaba Cloud auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + 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( 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 8793c3a00..74912635e 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 @@ -3,7 +3,7 @@ import { ForbiddenError } from "@casl/ability"; import axios from "axios"; import RE2 from "re2"; -import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, 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 { @@ -251,13 +251,14 @@ export const identityAwsAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -336,13 +337,14 @@ export const identityAwsAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -396,13 +398,14 @@ export const identityAwsAuthServiceFactory = ({ const awsIdentityAuth = await identityAwsAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...awsIdentityAuth, orgId: identityMembershipOrg.scopeOrgId }; }; @@ -427,22 +430,24 @@ export const identityAwsAuthServiceFactory = ({ message: "The identity does not have aws auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + 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( 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 b3250ed56..98e7b14a4 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,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, 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 { @@ -163,13 +163,14 @@ export const identityAzureAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -247,13 +248,14 @@ export const identityAzureAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -309,13 +311,14 @@ export const identityAzureAuthServiceFactory = ({ const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...identityAzureAuth, orgId: identityMembershipOrg.scopeOrgId }; @@ -341,22 +344,24 @@ export const identityAzureAuthServiceFactory = ({ message: "The identity does not have azure auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + 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, 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 fe7b9b6d7..05a2c94f8 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,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, 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 { @@ -204,13 +204,14 @@ export const identityGcpAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -290,13 +291,14 @@ export const identityGcpAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -354,13 +356,14 @@ export const identityGcpAuthServiceFactory = ({ const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...identityGcpAuth, orgId: identityMembershipOrg.scopeOrgId }; @@ -387,22 +390,24 @@ export const identityGcpAuthServiceFactory = ({ message: "The identity does not have gcp auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + 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, 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 a99c8ad78..b50655286 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 @@ -3,7 +3,7 @@ import https from "https"; import jwt from "jsonwebtoken"; import { JwksClient } from "jwks-rsa"; -import { AccessScope, IdentityAuthMethod, TIdentityJwtAuthsUpdate } from "@app/db/schemas"; +import { AccessScope, 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 { @@ -294,13 +294,14 @@ export const identityJwtAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); @@ -403,13 +404,14 @@ export const identityJwtAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); @@ -498,13 +500,14 @@ export const identityJwtAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); @@ -546,23 +549,25 @@ export const identityJwtAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + 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( 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 952b1e31d..d2eefcda1 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 @@ -3,7 +3,12 @@ import axios, { AxiosError } from "axios"; import https from "https"; import RE2 from "re2"; -import { AccessScope, IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas"; +import { + AccessScope, + IdentityAuthMethod, + OrganizationActionScope, + TIdentityKubernetesAuthsUpdate +} from "@app/db/schemas"; import { TGatewayDALFactory } from "@app/ee/services/gateway/gateway-dal"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TGatewayV2DALFactory } from "@app/ee/services/gateway-v2/gateway-v2-dal"; @@ -519,13 +524,14 @@ export const identityKubernetesAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -560,13 +566,14 @@ export const identityKubernetesAuthServiceFactory = ({ isGatewayV1 = false; } - const { permission: orgPermission } = await permissionService.getOrgPermission( + const { permission: orgPermission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(orgPermission).throwUnlessCan( OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway @@ -650,13 +657,14 @@ export const identityKubernetesAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -692,13 +700,14 @@ export const identityKubernetesAuthServiceFactory = ({ isGatewayV1 = false; } - const { permission: orgPermission } = await permissionService.getOrgPermission( + const { permission: orgPermission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(orgPermission).throwUnlessCan( OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway @@ -791,13 +800,14 @@ export const identityKubernetesAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const { decryptor } = await kmsService.createCipherPairWithDataKey({ @@ -847,22 +857,24 @@ export const identityKubernetesAuthServiceFactory = ({ message: "The identity does not have kubernetes auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( shouldUseNewPrivilegeSystem, 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 1a8ea3ed6..f925ff245 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 @@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; -import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, 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"; @@ -265,13 +265,14 @@ export const identityLdapAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); if (templateId) { @@ -441,13 +442,14 @@ export const identityLdapAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); if (templateId) { @@ -597,13 +599,14 @@ export const identityLdapAuthServiceFactory = ({ const ldapIdentityAuth = await identityLdapAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, @@ -640,22 +643,24 @@ export const identityLdapAuthServiceFactory = ({ message: "The identity does not have LDAP Auth attached" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( @@ -785,13 +790,14 @@ export const identityLdapAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const deleted = await keyStore.deleteItems({ 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 bfac3d158..b8991477d 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 @@ -3,7 +3,7 @@ import { ForbiddenError } from "@casl/ability"; import { AxiosError } from "axios"; import RE2 from "re2"; -import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, 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 { @@ -179,13 +179,14 @@ export const identityOciAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -262,13 +263,14 @@ export const identityOciAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -321,13 +323,14 @@ export const identityOciAuthServiceFactory = ({ const ociIdentityAuth = await identityOciAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...ociIdentityAuth, orgId: identityMembershipOrg.scopeOrgId }; }; @@ -352,22 +355,24 @@ export const identityOciAuthServiceFactory = ({ message: "The identity does not have OCI auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId); const permissionBoundary = validatePrivilegeChangeOperation( 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 1218d8e1c..9372c940c 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 @@ -4,7 +4,7 @@ import https from "https"; import jwt from "jsonwebtoken"; import { JwksClient } from "jwks-rsa"; -import { AccessScope, IdentityAuthMethod, TIdentityOidcAuthsUpdate } from "@app/db/schemas"; +import { AccessScope, 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 { @@ -269,13 +269,14 @@ export const identityOidcAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); @@ -367,13 +368,14 @@ export const identityOidcAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); @@ -447,13 +449,14 @@ export const identityOidcAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId }); @@ -488,23 +491,25 @@ export const identityOidcAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( 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 625b9b328..9d59ababf 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,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, 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 { @@ -200,13 +200,14 @@ export const identityTlsCertAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -288,13 +289,14 @@ export const identityTlsCertAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -359,13 +361,14 @@ export const identityTlsCertAuthServiceFactory = ({ const identityAuth = await identityTlsCertAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, @@ -399,23 +402,24 @@ export const identityTlsCertAuthServiceFactory = ({ message: "The identity does not have TLS Certificate auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission, memberships } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission, memberships } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); - + actorOrgId, + scope: OrganizationActionScope.Any + }); const shouldUseNewPrivilegeSystem = Boolean(memberships?.[0]?.shouldUseNewPrivilegeSystem); const permissionBoundary = validatePrivilegeChangeOperation( shouldUseNewPrivilegeSystem, 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 2ae05cb97..87ae18fd5 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,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope, IdentityAuthMethod, TableName } from "@app/db/schemas"; +import { AccessScope, 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 { @@ -90,13 +90,14 @@ export const identityTokenAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -172,13 +173,14 @@ export const identityTokenAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -232,13 +234,14 @@ export const identityTokenAuthServiceFactory = ({ const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...identityTokenAuth, orgId: identityMembershipOrg.scopeOrgId }; @@ -268,22 +271,24 @@ export const identityTokenAuthServiceFactory = ({ message: "The identity does not have Token Auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( @@ -341,22 +346,26 @@ export const identityTokenAuthServiceFactory = ({ message: "The identity does not have Token Auth" }); } - const { permission } = await permissionService.getOrgPermission( + + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( @@ -449,13 +458,14 @@ export const identityTokenAuthServiceFactory = ({ message: "The identity does not have Token Auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const tokens = await identityAccessTokenDAL.find( @@ -501,22 +511,24 @@ export const identityTokenAuthServiceFactory = ({ message: "The identity does not have Token Auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( shouldUseNewPrivilegeSystem, @@ -580,13 +592,14 @@ export const identityTokenAuthServiceFactory = ({ throw new NotFoundError({ message: `Failed to find identity with ID ${identityAccessToken.identityId}` }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityOrgMembership.scopeOrgId, + 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 563a3f897..f63c8a6e5 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, 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 { @@ -320,13 +320,15 @@ export const identityUaServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -430,13 +432,14 @@ export const identityUaServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -513,13 +516,14 @@ export const identityUaServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...uaIdentityAuth, orgId: identityMembershipOrg.scopeOrgId }; }; @@ -545,22 +549,24 @@ export const identityUaServiceFactory = ({ message: "The identity does not have universal auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( shouldUseNewPrivilegeSystem, @@ -612,22 +618,24 @@ export const identityUaServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( shouldUseNewPrivilegeSystem, @@ -692,23 +700,24 @@ export const identityUaServiceFactory = ({ message: "The identity does not have universal auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); - + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( shouldUseNewPrivilegeSystem, @@ -768,22 +777,24 @@ 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( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( shouldUseNewPrivilegeSystem, @@ -835,22 +846,24 @@ 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( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( @@ -901,13 +914,14 @@ export const identityUaServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const deleted = await keyStore.deleteItems({ diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index 721844070..e83f2f369 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope, OrgMembershipRole, TableName, TRoles } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope, OrgMembershipRole, TableName, TRoles } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -67,7 +67,14 @@ export const identityServiceFactory = ({ actorOrgId, metadata }: TCreateIdentityDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const [rolePermissionDetails] = await permissionService.getOrgPermissionByRoles([role], orgId); @@ -104,7 +111,7 @@ export const identityServiceFactory = ({ } const identity = await identityDAL.transaction(async (tx) => { - const newIdentity = await identityDAL.create({ name, hasDeleteProtection }, tx); + const newIdentity = await identityDAL.create({ name, hasDeleteProtection, orgId }, tx); const membership = await membershipIdentityDAL.create( { scope: AccessScope.Organization, @@ -172,13 +179,14 @@ export const identityServiceFactory = ({ }); if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityOrgMembership.scopeOrgId, + orgId: identityOrgMembership.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); let customRole: TRoles | undefined; @@ -264,13 +272,14 @@ export const identityServiceFactory = ({ const identity = doc[0]; if (!identity) throw new NotFoundError({ message: `Failed to find identity with id ${id}` }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identity.orgId, + orgId: identity.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); // TODO(namespace): check this in identity service @@ -314,13 +323,14 @@ export const identityServiceFactory = ({ }); if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityOrgMembership.scopeOrgId, + orgId: identityOrgMembership.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity); @@ -346,7 +356,14 @@ export const identityServiceFactory = ({ orderDirection, search }: TListOrgIdentitiesByOrgIdDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const identityMemberships = await identityOrgMembershipDAL.find({ @@ -379,7 +396,14 @@ export const identityServiceFactory = ({ orderDirection, searchFilter = {} }: TSearchOrgIdentitiesByOrgIdDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const { totalCount, docs } = await identityOrgMembershipDAL.searchIdentities({ @@ -408,13 +432,14 @@ export const identityServiceFactory = ({ }); if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityOrgMembership.scopeOrgId, + orgId: identityOrgMembership.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const identityMemberships = await identityProjectDAL.findByIdentityId(identityId); diff --git a/backend/src/services/membership-group/org/org-membership-group-factory.ts b/backend/src/services/membership-group/org/org-membership-group-factory.ts index 1e87ee3ca..d69db9c08 100644 --- a/backend/src/services/membership-group/org/org-membership-group-factory.ts +++ b/backend/src/services/membership-group/org/org-membership-group-factory.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope, OrgMembershipRole } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope, OrgMembershipRole } from "@app/db/schemas"; import { OrgPermissionGroupActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { constructPermissionErrorMessage, @@ -45,13 +45,14 @@ export const newOrgMembershipGroupFactory = ({ }; const onUpdateMembershipGroupGuard: TMembershipGroupScopeFactory["onUpdateMembershipGroupGuard"] = async (dto) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups); const permissionRoles = await permissionService.getOrgPermissionByRoles( dto.data.roles.map((el) => el.role), @@ -89,26 +90,28 @@ export const newOrgMembershipGroupFactory = ({ }; const onListMembershipGroupGuard: TMembershipGroupScopeFactory["onListMembershipGroupGuard"] = async (dto) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); }; const onGetMembershipGroupByGroupIdGuard: TMembershipGroupScopeFactory["onGetMembershipGroupByGroupIdGuard"] = async ( dto ) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); }; diff --git a/backend/src/services/membership-identity/org/org-membership-identity-factory.ts b/backend/src/services/membership-identity/org/org-membership-identity-factory.ts index 06789e274..fce31b0fe 100644 --- a/backend/src/services/membership-identity/org/org-membership-identity-factory.ts +++ b/backend/src/services/membership-identity/org/org-membership-identity-factory.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope, OrgMembershipRole } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope, OrgMembershipRole } from "@app/db/schemas"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { constructPermissionErrorMessage, @@ -48,13 +48,15 @@ export const newOrgMembershipIdentityFactory = ({ const onUpdateMembershipIdentityGuard: TMembershipIdentityScopeFactory["onUpdateMembershipIdentityGuard"] = async ( dto ) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const permissionRoles = await permissionService.getOrgPermissionByRoles( dto.data.roles.map((el) => el.role), @@ -95,25 +97,27 @@ export const newOrgMembershipIdentityFactory = ({ const onListMembershipIdentityGuard: TMembershipIdentityScopeFactory["onListMembershipIdentityGuard"] = async ( dto ) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); }; const onGetMembershipIdentityByIdentityIdGuard: TMembershipIdentityScopeFactory["onGetMembershipIdentityByIdentityIdGuard"] = async (dto) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); }; diff --git a/backend/src/services/membership-user/org/org-membership-user-factory.ts b/backend/src/services/membership-user/org/org-membership-user-factory.ts index 523e85bae..5caf77c2c 100644 --- a/backend/src/services/membership-user/org/org-membership-user-factory.ts +++ b/backend/src/services/membership-user/org/org-membership-user-factory.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope } from "@app/db/schemas"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; @@ -52,13 +52,14 @@ export const newOrgMembershipUserFactory = ({ const isCustomRole: TMembershipUserScopeFactory["isCustomRole"] = (role: string) => isCustomOrgRole(role); const onCreateMembershipUserGuard: TMembershipUserScopeFactory["onCreateMembershipUserGuard"] = async (dto) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member); const plan = await licenseService.getPlan(dto.permission.orgId); @@ -134,48 +135,52 @@ export const newOrgMembershipUserFactory = ({ }; const onUpdateMembershipUserGuard: TMembershipUserScopeFactory["onUpdateMembershipUserGuard"] = async (dto) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Member); }; const onDeleteMembershipUserGuard: TMembershipUserScopeFactory["onDeleteMembershipUserGuard"] = async (dto) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member); }; const onListMembershipUserGuard: TMembershipUserScopeFactory["onListMembershipUserGuard"] = async (dto) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); }; const onGetMembershipUserByUserIdGuard: TMembershipUserScopeFactory["onGetMembershipUserByUserIdGuard"] = async ( dto ) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); }; diff --git a/backend/src/services/microsoft-teams/microsoft-teams-service.ts b/backend/src/services/microsoft-teams/microsoft-teams-service.ts index 23ed61402..ff17daa75 100644 --- a/backend/src/services/microsoft-teams/microsoft-teams-service.ts +++ b/backend/src/services/microsoft-teams/microsoft-teams-service.ts @@ -9,6 +9,7 @@ import { import { CronJob } from "cron"; import { FastifyReply, FastifyRequest } from "fastify"; +import { OrganizationActionScope } from "@app/db/schemas"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; @@ -208,13 +209,14 @@ export const microsoftTeamsServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - microsoftTeamsIntegration.orgId, + orgId: microsoftTeamsIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); @@ -282,13 +284,14 @@ export const microsoftTeamsServiceFactory = ({ description, redirectUri }: TCreateMicrosoftTeamsIntegrationDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); @@ -393,13 +396,14 @@ export const microsoftTeamsServiceFactory = ({ }); }; const getClientId = async ({ actorId, actor, actorOrgId, actorAuthMethod }: TGetClientIdDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); @@ -427,13 +431,14 @@ export const microsoftTeamsServiceFactory = ({ actorOrgId, actorAuthMethod }: TGetMicrosoftTeamsIntegrationByOrgDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); @@ -463,13 +468,14 @@ export const microsoftTeamsServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - microsoftTeamsIntegration.orgId, + orgId: microsoftTeamsIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); @@ -495,13 +501,14 @@ export const microsoftTeamsServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - microsoftTeamsIntegration.orgId, + orgId: microsoftTeamsIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); @@ -549,13 +556,14 @@ export const microsoftTeamsServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - microsoftTeamsIntegration.orgId, + orgId: microsoftTeamsIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings); @@ -577,13 +585,14 @@ export const microsoftTeamsServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - microsoftTeamsIntegration.orgId, + orgId: microsoftTeamsIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); diff --git a/backend/src/services/org-admin/org-admin-service.ts b/backend/src/services/org-admin/org-admin-service.ts index 4c080717d..4f4a9b08c 100644 --- a/backend/src/services/org-admin/org-admin-service.ts +++ b/backend/src/services/org-admin/org-admin-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope, ProjectMembershipRole, ProjectVersion } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope, ProjectMembershipRole, ProjectVersion } from "@app/db/schemas"; import { OrgPermissionAdminConsoleAction, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -44,13 +44,14 @@ export const orgAdminServiceFactory = ({ actorOrgId, actorAuthMethod }: TListOrgProjectsDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole @@ -76,13 +77,14 @@ export const orgAdminServiceFactory = ({ actorAuthMethod, projectId }: TAccessProjectDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index 077ddcdfb..d7efdc463 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -26,8 +26,8 @@ import { } from "@app/lib/knex"; import { generateKnexQueryFromScim } from "@app/lib/knex/scim"; -import { OrgAuthMethod } from "./org-types"; import { ActorType } from "../auth/auth-type"; +import { OrgAuthMethod } from "./org-types"; export type TOrgDALFactory = ReturnType; diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 5b98b44b1..b053349d3 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -4,6 +4,7 @@ import { Knex } from "knex"; import { AccessScope, + OrganizationActionScope, OrgMembershipRole, OrgMembershipStatus, TableName, @@ -158,7 +159,14 @@ export const orgServiceFactory = ({ actorAuthMethod: ActorAuthMethod, actorOrgId: string | undefined ) => { - await permissionService.getOrgPermission(ActorType.USER, userId, orgId, actorAuthMethod, actorOrgId); + await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: userId, + orgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.Any + }); const appCfg = getConfig(); const org = await orgDAL.findOrgById(orgId); if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); @@ -194,13 +202,14 @@ export const orgServiceFactory = ({ actorAuthMethod: ActorAuthMethod, actorOrgId: string | undefined ) => { - const { permission } = await permissionService.getOrgPermission( - ActorType.USER, - userId, + const { permission } = await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: userId, orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); const members = await orgDAL.findAllOrgMembers(orgId); @@ -208,7 +217,14 @@ export const orgServiceFactory = ({ }; const getOrgGroups = async ({ actor, actorId, orgId, actorAuthMethod, actorOrgId }: TGetOrgGroupsDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); const groups = await groupDAL.findByOrgId(orgId); return groups; @@ -222,7 +238,14 @@ export const orgServiceFactory = ({ orgId, emails }: TFindOrgMembersByEmailDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); const members = await orgDAL.findOrgMembersByUsername(orgId, emails); @@ -309,13 +332,14 @@ export const orgServiceFactory = ({ actorAuthMethod, orgId }: TUpgradePrivilegeSystemDTO) => { - const { hasRole } = await permissionService.getOrgPermission( - ActorType.USER, + const { hasRole } = await permissionService.getOrgPermission({ + actor: ActorType.USER, actorId, orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ @@ -380,7 +404,14 @@ export const orgServiceFactory = ({ } }: TUpdateOrgDTO) => { const appCfg = getConfig(); - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); if (allowSecretSharingOutsideOrganization !== undefined) { @@ -658,13 +689,14 @@ export const orgServiceFactory = ({ actorAuthMethod: ActorAuthMethod; actorOrgId: string | undefined; }) => { - const { hasRole } = await permissionService.getOrgPermission( - ActorType.USER, - userId, + const { hasRole } = await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: userId, orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ name: "DeleteOrganizationById", @@ -744,13 +776,14 @@ export const orgServiceFactory = ({ actorOrgId, metadata }: TUpdateOrgMembershipDTO) => { - const { permission } = await permissionService.getOrgPermission( - ActorType.USER, - userId, + const { permission } = await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: userId, orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Member); const foundMembership = await membershipUserDAL.findOne({ @@ -831,7 +864,14 @@ export const orgServiceFactory = ({ membershipId }: TResendOrgMemberInvitationDTO) => { const appCfg = getConfig(); - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member); @@ -967,7 +1007,14 @@ export const orgServiceFactory = ({ actorAuthMethod, actorOrgId }: TGetOrgMembershipDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); const membership = await orgMembershipDAL.findOrgMembershipById(membershipId); @@ -988,13 +1035,14 @@ export const orgServiceFactory = ({ actorAuthMethod, actorOrgId }: TDeleteOrgMembershipDTO) => { - const { permission } = await permissionService.getOrgPermission( - ActorType.USER, - userId, + const { permission } = await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: userId, orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member); const [deletedMembership] = await deleteOrgMembershipsFn({ @@ -1021,13 +1069,14 @@ export const orgServiceFactory = ({ actorAuthMethod, actorOrgId }: TDeleteOrgMembershipsDTO) => { - const { permission } = await permissionService.getOrgPermission( - ActorType.USER, - userId, + const { permission } = await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: userId, orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member); if (membershipIds.includes(userId)) { @@ -1059,7 +1108,14 @@ export const orgServiceFactory = ({ actorAuthMethod, actorOrgId }: TListProjectMembershipsByOrgMembershipIdDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); const membership = await orgMembershipDAL.findOrgMembershipById(orgMembershipId); @@ -1082,13 +1138,14 @@ export const orgServiceFactory = ({ actorAuthMethod: ActorAuthMethod, actorOrgId: string | undefined ) => { - const { permission } = await permissionService.getOrgPermission( - ActorType.USER, - userId, + const { permission } = await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: userId, orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount); const incidentContacts = await incidentContactDAL.findByOrgId(orgId); return incidentContacts; @@ -1101,13 +1158,14 @@ export const orgServiceFactory = ({ actorAuthMethod: ActorAuthMethod, actorOrgId: string | undefined ) => { - const { permission } = await permissionService.getOrgPermission( - ActorType.USER, - userId, + const { permission } = await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: userId, orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.IncidentAccount); const doesIncidentContactExist = await incidentContactDAL.findOne(orgId, { email }); if (doesIncidentContactExist) { @@ -1128,13 +1186,14 @@ export const orgServiceFactory = ({ actorAuthMethod: ActorAuthMethod, actorOrgId: string | undefined ) => { - const { permission } = await permissionService.getOrgPermission( - ActorType.USER, - userId, + const { permission } = await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: userId, orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.IncidentAccount); const incidentContact = await incidentContactDAL.deleteById(id, orgId); diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 58b3c5395..e17787351 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -5,6 +5,7 @@ import slugify from "@sindresorhus/slugify"; import { AccessScope, ActionProjectType, + OrganizationActionScope, ProjectMembershipRole, ProjectType, ProjectVersion, @@ -245,13 +246,14 @@ export const projectServiceFactory = ({ type = ProjectType.SecretManager }: TCreateProjectDTO) => { const organization = await orgDAL.findOne({ id: actorOrgId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - organization.id, + orgId: organization.id, actorAuthMethod, actorOrgId - ); + }); if ( permission.cannot(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace) && @@ -296,6 +298,7 @@ export const projectServiceFactory = ({ projectTemplate = await projectTemplateService.findProjectTemplateByName(template, { id: actorId, orgId: organization.id, + parentOrgId: organization.id, type: actor, authMethod: actorAuthMethod }); @@ -513,13 +516,14 @@ export const projectServiceFactory = ({ : await projectDAL.findUserProjects(actorId, actorOrgId, type); if (includeRoles) { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); // `includeRoles` is specifically used by organization admins when inviting new users to the organizations to avoid looping redundant api calls. ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member); @@ -1822,13 +1826,13 @@ export const projectServiceFactory = ({ projectIds }: TSearchProjectsDTO) => { // check user belong to org - await permissionService.getOrgPermission( - permission.type, - permission.id, - permission.orgId, - permission.authMethod, - permission.orgId - ); + await permissionService.getOrgPermission({ + actor: permission.type, + actorId: permission.id, + orgId: permission.orgId, + actorAuthMethod: permission.authMethod, + scope: OrganizationActionScope.Any + }); return projectDAL.searchProjects({ limit, @@ -1846,13 +1850,13 @@ export const projectServiceFactory = ({ const requestProjectAccess = async ({ permission, comment, projectId }: TProjectAccessRequestDTO) => { // check user belong to org - await permissionService.getOrgPermission( - permission.type, - permission.id, - permission.orgId, - permission.authMethod, - permission.orgId - ); + await permissionService.getOrgPermission({ + actor: permission.type, + actorId: permission.id, + orgId: permission.orgId, + actorAuthMethod: permission.authMethod, + scope: OrganizationActionScope.Any + }); const projectMember = await permissionService .getProjectPermission({ diff --git a/backend/src/services/role/org/org-role-factory.ts b/backend/src/services/role/org/org-role-factory.ts index 50ffa5e43..f91dabccb 100644 --- a/backend/src/services/role/org/org-role-factory.ts +++ b/backend/src/services/role/org/org-role-factory.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope } from "@app/db/schemas"; import { orgAdminPermissions, orgMemberPermissions, @@ -34,35 +34,38 @@ export const newOrgRoleFactory = ({ const isCustomRole: TRoleScopeFactory["isCustomRole"] = (role: string) => isCustomOrgRole(role); const onCreateRoleGuard: TRoleScopeFactory["onCreateRoleGuard"] = async (dto) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Role); }; const onUpdateRoleGuard: TRoleScopeFactory["onUpdateRoleGuard"] = async (dto) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Role); }; const onDeleteRoleGuard: TRoleScopeFactory["onDeleteRoleGuard"] = async (dto) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Role); const externalGroupMapping = await externalGroupOrgRoleMappingDAL.findOne({ @@ -78,35 +81,38 @@ export const newOrgRoleFactory = ({ }; const onListRoleGuard: TRoleScopeFactory["onListRoleGuard"] = async (dto) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role); }; const onGetRoleByIdGuard: TRoleScopeFactory["onGetRoleByIdGuard"] = async (dto) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role); }; const onGetRoleBySlugGuard: TRoleScopeFactory["onGetRoleBySlugGuard"] = async (dto) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role); }; diff --git a/backend/src/services/role/role-service.ts b/backend/src/services/role/role-service.ts index 41c825b2e..3387dc96b 100644 --- a/backend/src/services/role/role-service.ts +++ b/backend/src/services/role/role-service.ts @@ -1,7 +1,7 @@ import { packRules } from "@casl/ability/extra"; import { requestContext } from "@fastify/request-context"; -import { AccessScope, ActionProjectType, TableName } from "@app/db/schemas"; +import { AccessScope, ActionProjectType, OrganizationActionScope, TableName } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; @@ -214,13 +214,14 @@ export const roleServiceFactory = ({ const getUserPermission = async (dto: TGetUserPermissionDTO) => { if (dto.scopeData.scope === AccessScope.Organization) { - const { permission, memberships } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission, memberships } = await permissionService.getOrgPermission({ + actorId: dto.permission.id, + actor: dto.permission.type, + orgId: dto.permission.orgId, + actorOrgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + scope: OrganizationActionScope.Any + }); return { permissions: packRules(permission.rules), memberships, assumedPrivilegeDetails: undefined }; } diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts index 4cbfcdc7f..3f5df049c 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -1,4 +1,4 @@ -import { TSecretSharing } from "@app/db/schemas"; +import { OrganizationActionScope, TSecretSharing } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; @@ -81,7 +81,14 @@ export const secretSharingServiceFactory = ({ }: TCreateSharedSecretDTO) => { const appCfg = getConfig(); - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.Any + }); if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" }); $validateSharedSecretExpiry(expiresAt); @@ -196,7 +203,14 @@ export const secretSharingServiceFactory = ({ actorAuthMethod, actorOrgId }: TCreateSecretRequestDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" }); $validateSharedSecretExpiry(expiresAt); @@ -228,7 +242,14 @@ export const secretSharingServiceFactory = ({ throw new NotFoundError({ message: `Secret request with ID '${id}' not found` }); } - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" }); if (secretRequest.userId !== actorId || secretRequest.orgId !== orgId) { @@ -267,13 +288,14 @@ export const secretSharingServiceFactory = ({ throw new UnauthorizedError(); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - secretRequest.orgId, + orgId: secretRequest.orgId, actorAuthMethod, actorOrgId - ); + }); if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" }); } @@ -316,13 +338,14 @@ export const secretSharingServiceFactory = ({ throw new UnauthorizedError(); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - secretRequest.orgId, + orgId: secretRequest.orgId, actorAuthMethod, actorOrgId - ); + }); if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" }); const user = await userDAL.findById(actorId); @@ -415,13 +438,14 @@ export const secretSharingServiceFactory = ({ }: TGetSharedSecretsDTO) => { if (!actorOrgId) throw new ForbiddenRequestError(); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); if (!permission) throw new ForbiddenRequestError({ name: "User does not belong to the specified organization" }); const secrets = await secretSharingDAL.find( @@ -563,7 +587,14 @@ export const secretSharingServiceFactory = ({ const deleteSharedSecretById = async (deleteSharedSecretInput: TDeleteSharedSecretDTO) => { const { actor, actorId, orgId, actorAuthMethod, actorOrgId, sharedSecretId } = deleteSharedSecretInput; - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); if (!permission) throw new ForbiddenRequestError({ name: "User does not belong to the specified organization" }); const sharedSecret = isUuidV4(sharedSecretId) diff --git a/backend/src/services/slack/slack-service.ts b/backend/src/services/slack/slack-service.ts index c8aa8aaf6..e4110ac11 100644 --- a/backend/src/services/slack/slack-service.ts +++ b/backend/src/services/slack/slack-service.ts @@ -1,6 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { InstallProvider } from "@slack/oauth"; +import { OrganizationActionScope } from "@app/db/schemas"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; @@ -230,13 +231,14 @@ export const slackServiceFactory = ({ }: TGetSlackInstallUrlDTO) => { const appCfg = getConfig(); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); @@ -264,13 +266,14 @@ export const slackServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - slackIntegration.orgId, + orgId: slackIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); @@ -293,13 +296,14 @@ export const slackServiceFactory = ({ actorOrgId, actorAuthMethod }: TGetSlackIntegrationByOrgDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); @@ -324,13 +328,14 @@ export const slackServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - slackIntegration.orgId, + orgId: slackIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); @@ -351,13 +356,14 @@ export const slackServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - slackIntegration.orgId, + orgId: slackIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); @@ -389,13 +395,14 @@ export const slackServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - slackIntegration.orgId, + orgId: slackIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); @@ -432,13 +439,14 @@ export const slackServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - slackIntegration.orgId, + orgId: slackIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings); diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 84a53f407..63bab2666 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -592,7 +592,7 @@ export const superAdminServiceFactory = ({ }); const { identity, credentials } = await identityDAL.transaction(async (tx) => { - const newIdentity = await identityDAL.create({ name: "Instance Admin Identity" }, tx); + const newIdentity = await identityDAL.create({ name: "Instance Admin Identity", orgId: organization.id }, tx); const membership = await membershipIdentityDAL.create( { actorIdentityId: newIdentity.id, diff --git a/backend/src/services/user/user-service.ts b/backend/src/services/user/user-service.ts index b54eab8ef..56d7ee635 100644 --- a/backend/src/services/user/user-service.ts +++ b/backend/src/services/user/user-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { Knex } from "knex"; -import { AccessScope } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope } from "@app/db/schemas"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { crypto } from "@app/lib/crypto"; @@ -458,13 +458,14 @@ export const userServiceFactory = ({ // This makes it so the user can always read information about themselves, but no one else if they don't have the Members Read permission. if (user.id !== actorId) { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); } diff --git a/backend/src/services/workflow-integration/workflow-integration-service.ts b/backend/src/services/workflow-integration/workflow-integration-service.ts index cb7f7a325..8fea0e240 100644 --- a/backend/src/services/workflow-integration/workflow-integration-service.ts +++ b/backend/src/services/workflow-integration/workflow-integration-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { OrganizationActionScope } from "@app/db/schemas"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; @@ -23,13 +24,14 @@ export const workflowIntegrationServiceFactory = ({ actorOrgId, actorAuthMethod }: TGetWorkflowIntegrationsByOrg) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); From 6fca30f2cb6cbde86b139bf7ad88b1b8d0aad2f0 Mon Sep 17 00:00:00 2001 From: = Date: Sun, 19 Oct 2025 01:38:34 +0530 Subject: [PATCH 03/44] feat: completed base setup --- backend/src/ee/routes/v1/index.ts | 2 + backend/src/ee/routes/v1/license-router.ts | 2 +- backend/src/ee/routes/v1/sub-org-router.ts | 14 +++- .../src/ee/services/license/license-fns.ts | 2 +- .../src/ee/services/license/license-types.ts | 2 +- .../ee/services/permission/permission-dal.ts | 2 +- .../ee/services/sub-org/sub-org-service.ts | 2 +- backend/src/server/routes/v1/auth-router.ts | 1 + .../server/routes/v1/organization-router.ts | 10 ++- backend/src/services/org/org-service.ts | 18 +++-- frontend/src/config/request.ts | 6 ++ .../OrganizationContext.tsx | 9 ++- frontend/src/hooks/api/index.tsx | 1 + .../src/hooks/api/organization/queries.tsx | 4 +- .../src/hooks/api/subOrganizations/index.tsx | 7 ++ .../hooks/api/subOrganizations/mutations.tsx | 22 +++++ .../hooks/api/subOrganizations/queries.tsx | 28 +++++++ .../src/hooks/api/subOrganizations/types.ts | 17 ++++ frontend/src/hooks/api/subscriptions/types.ts | 1 + .../components/NavBar/Navbar.tsx | 29 ++++++- .../NavBar/NewSubOrganizationForm.tsx | 80 +++++++++++++++++++ frontend/src/pages/organization/layout.tsx | 11 ++- 22 files changed, 249 insertions(+), 21 deletions(-) create mode 100644 frontend/src/hooks/api/subOrganizations/index.tsx create mode 100644 frontend/src/hooks/api/subOrganizations/mutations.tsx create mode 100644 frontend/src/hooks/api/subOrganizations/queries.tsx create mode 100644 frontend/src/hooks/api/subOrganizations/types.ts create mode 100644 frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 42392ba55..8d4671e50 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -50,10 +50,12 @@ import { registerSshHostGroupRouter } from "./ssh-host-group-router"; import { registerSshHostRouter } from "./ssh-host-router"; import { registerTrustedIpRouter } from "./trusted-ip-router"; import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router"; +import { registerSubOrgRouter } from "./sub-org-router"; export const registerV1EERoutes = async (server: FastifyZodProvider) => { // org role starts with organization await server.register(registerOrgRoleRouter, { prefix: "/organization" }); + await server.register(registerSubOrgRouter, { prefix: "/sub-organizations" }); await server.register(registerLicenseRouter, { prefix: "/organizations" }); // depreciated in favour of infisical workspace diff --git a/backend/src/ee/routes/v1/license-router.ts b/backend/src/ee/routes/v1/license-router.ts index 17923975d..8ab31dbe4 100644 --- a/backend/src/ee/routes/v1/license-router.ts +++ b/backend/src/ee/routes/v1/license-router.ts @@ -58,7 +58,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { const plan = await server.services.license.getOrgPlan({ actorId: req.permission.id, actor: req.permission.type, - actorOrgId: req.permission.orgId, + actorOrgId: req.permission.parentOrgId, actorAuthMethod: req.permission.authMethod, orgId: req.params.organizationId, refreshCache: req.query.refreshCache diff --git a/backend/src/ee/routes/v1/sub-org-router.ts b/backend/src/ee/routes/v1/sub-org-router.ts index 9b9ecfea2..281b54e35 100644 --- a/backend/src/ee/routes/v1/sub-org-router.ts +++ b/backend/src/ee/routes/v1/sub-org-router.ts @@ -7,6 +7,14 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +const sanitiziedSubOrganizationSchema = OrganizationsSchema.pick({ + id: true, + name: true, + slug: true, + createdAt: true, + updatedAt: true +}); + export const registerSubOrgRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", @@ -28,7 +36,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - organization: OrganizationsSchema + organization: sanitiziedSubOrganizationSchema }) } }, @@ -77,7 +85,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => { } ], querystring: z.object({ - limit: z.coerce.number().min(1).max(100).default(25).describe(SUB_ORGANIZATIONS.LIST.limit), + limit: z.coerce.number().min(1).max(1000).default(25).describe(SUB_ORGANIZATIONS.LIST.limit), offset: z.coerce.number().min(0).default(0).describe(SUB_ORGANIZATIONS.LIST.offset), isAccessible: z .enum(["true", "false"]) @@ -87,7 +95,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - organizations: OrganizationsSchema.array() + organizations: sanitiziedSubOrganizationSchema.array() }) } }, diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index e5c775f07..8d0d9d74b 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -28,7 +28,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ rbac: false, githubOrgSync: false, customRateLimits: false, - childOrganization: true, + subOrganization: true, customAlerts: false, secretAccessInsights: false, auditLogs: false, diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 8090d789e..88c1edb2e 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -33,7 +33,7 @@ export type TFeatureSet = { membersUsed: number; identityLimit: null; identitiesUsed: number; - childOrganization: true; + subOrganization: true; environmentLimit: null; environmentsUsed: 0; secretVersioning: true; diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index eb45158ab..d35abe665 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -285,7 +285,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { MembershipsSchema.extend({ orgAuthEnforced: z.boolean().optional().nullable(), shouldUseNewPrivilegeSystem: z.boolean().optional().nullable(), - parentOrgId: z.boolean().optional().nullable(), + parentOrgId: z.string().optional().nullable(), orgGoogleSsoAuthEnforced: z.boolean(), bypassOrgAuthEnabled: z.boolean() }).parse(el), diff --git a/backend/src/ee/services/sub-org/sub-org-service.ts b/backend/src/ee/services/sub-org/sub-org-service.ts index 37c5545c0..86dd03abd 100644 --- a/backend/src/ee/services/sub-org/sub-org-service.ts +++ b/backend/src/ee/services/sub-org/sub-org-service.ts @@ -45,7 +45,7 @@ export const subOrgServiceFactory = ({ ); const orgLicensePlan = await licenseService.getPlan(permissionActor.parentOrgId); - if (!orgLicensePlan.gateway) { + if (!orgLicensePlan.subOrganization) { throw new BadRequestError({ message: "Child organization creation failed. Please upgrade your instance to Infisical's Enterprise plan." }); diff --git a/backend/src/server/routes/v1/auth-router.ts b/backend/src/server/routes/v1/auth-router.ts index 911979b60..48939844e 100644 --- a/backend/src/server/routes/v1/auth-router.ts +++ b/backend/src/server/routes/v1/auth-router.ts @@ -94,6 +94,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => { decodedToken.userId, decodedToken.organizationId, decodedToken.authMethod, + decodedToken.organizationId, decodedToken.organizationId ); if (org && org.userTokenExpiration) { diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index 872b7b157..b640fba1a 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -59,7 +59,14 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - organization: sanitizedOrganizationSchema + organization: sanitizedOrganizationSchema.extend({ + subOrganization: z + .object({ + id: z.string(), + name: z.string() + }) + .optional() + }) }) } }, @@ -69,6 +76,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { req.permission.id, req.params.organizationId, req.permission.authMethod, + req.permission.parentOrgId, req.permission.orgId ); return { organization }; diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index b053349d3..011af3520 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -157,23 +157,31 @@ export const orgServiceFactory = ({ userId: string, orgId: string, actorAuthMethod: ActorAuthMethod, - actorOrgId: string | undefined + parentOrgId: string, + actorOrgId: string ) => { await permissionService.getOrgPermission({ actor: ActorType.USER, actorId: userId, orgId, actorAuthMethod, - actorOrgId, + actorOrgId: parentOrgId, scope: OrganizationActionScope.Any }); const appCfg = getConfig(); const org = await orgDAL.findOrgById(orgId); if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); - if (!org.userTokenExpiration) { - return { ...org, userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME }; + + const hasSubOrg = actorOrgId !== parentOrgId; + let subOrg; + if (hasSubOrg) { + subOrg = await orgDAL.findOne({ parentOrgId, id: actorOrgId }); } - return org; + + if (!org.userTokenExpiration) { + return { ...org, userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME, subOrganization: subOrg }; + } + return { ...org, subOrganization: subOrg }; }; /* * Get all organization a user part of diff --git a/frontend/src/config/request.ts b/frontend/src/config/request.ts index a37b38cb6..3fc01ccac 100644 --- a/frontend/src/config/request.ts +++ b/frontend/src/config/request.ts @@ -24,6 +24,8 @@ apiRequest.interceptors.request.use((config) => { const token = getAuthToken(); const providerAuthToken = SecurityClient.getProviderAuthToken(); + const params = new URLSearchParams(window.location.search); + if (config.headers) { if (signupTempToken) { // eslint-disable-next-line no-param-reassign @@ -38,6 +40,10 @@ apiRequest.interceptors.request.use((config) => { // eslint-disable-next-line no-param-reassign config.headers.Authorization = `Bearer ${providerAuthToken}`; } + const subOrganization = params.get("subOrganization"); + if (subOrganization) { + config.headers.set("x-infisical-org", subOrganization); + } } return config; diff --git a/frontend/src/context/OrganizationContext/OrganizationContext.tsx b/frontend/src/context/OrganizationContext/OrganizationContext.tsx index 26865b5bf..b7726e6a4 100644 --- a/frontend/src/context/OrganizationContext/OrganizationContext.tsx +++ b/frontend/src/context/OrganizationContext/OrganizationContext.tsx @@ -15,5 +15,12 @@ export const useOrganization = () => { staleTime: Infinity }); - return { currentOrg }; + return { + currentOrg: { + ...currentOrg, + id: currentOrg?.subOrganization?.id || currentOrg?.id, + parentOrgId: currentOrg.id + }, + isSubOrganization: Boolean(currentOrg.subOrganization) + }; }; diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index d65c7e72c..6167f6e07 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -48,6 +48,7 @@ export * from "./sshCertificateTemplates"; export * from "./sshHost"; export * from "./sshHostGroup"; export * from "./ssoConfig"; +export * from "./subOrganizations"; export * from "./subscriptions"; export * from "./tags"; export * from "./trustedIps"; diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index 15e1b861c..ce3071584 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -64,7 +64,9 @@ export const useGetOrganizations = () => { export const fetchOrganizationById = async (id: string) => { const { data: { organization } - } = await apiRequest.get<{ organization: Organization }>(`/api/v1/organization/${id}`); + } = await apiRequest.get<{ + organization: Organization & { subOrganization?: { id: string; name: string } }; + }>(`/api/v1/organization/${id}`); return organization; }; diff --git a/frontend/src/hooks/api/subOrganizations/index.tsx b/frontend/src/hooks/api/subOrganizations/index.tsx new file mode 100644 index 000000000..85095fcc6 --- /dev/null +++ b/frontend/src/hooks/api/subOrganizations/index.tsx @@ -0,0 +1,7 @@ +export { useCreateSubOrganization } from "./mutations"; +export { subOrganizationsQuery } from "./queries"; +export type { + TCreateSubOrganizationDTO, + TListSubOrganizationsDTO, + TSubOrganization +} from "./types"; diff --git a/frontend/src/hooks/api/subOrganizations/mutations.tsx b/frontend/src/hooks/api/subOrganizations/mutations.tsx new file mode 100644 index 000000000..828aea6f4 --- /dev/null +++ b/frontend/src/hooks/api/subOrganizations/mutations.tsx @@ -0,0 +1,22 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { subOrganizationsQuery } from "./queries"; +import { TCreateSubOrganizationDTO, TSubOrganization } from "./types"; + +export const useCreateSubOrganization = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (dto: TCreateSubOrganizationDTO) => { + const { data } = await apiRequest.post<{ organization: TSubOrganization }>( + "/api/v1/sub-organizations", + dto + ); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: subOrganizationsQuery.allKey() }); + } + }); +}; diff --git a/frontend/src/hooks/api/subOrganizations/queries.tsx b/frontend/src/hooks/api/subOrganizations/queries.tsx new file mode 100644 index 000000000..99ccb7770 --- /dev/null +++ b/frontend/src/hooks/api/subOrganizations/queries.tsx @@ -0,0 +1,28 @@ +import { queryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TListSubOrganizationsDTO, TSubOrganization } from "./types"; + +export const subOrganizationsQuery = { + allKey: () => ["sub-organizations"] as const, + listKey: (params?: TListSubOrganizationsDTO) => + [...subOrganizationsQuery.allKey(), "list", params] as const, + list: (params: TListSubOrganizationsDTO) => + queryOptions({ + queryKey: subOrganizationsQuery.listKey(params), + queryFn: async () => { + const { data } = await apiRequest.get<{ organizations: TSubOrganization[] }>( + "/api/v1/sub-organizations", + { + params: { + limit: params.limit, + offset: params.offset, + isAccessible: params.isAccessible + } + } + ); + return data.organizations; + } + }) +}; diff --git a/frontend/src/hooks/api/subOrganizations/types.ts b/frontend/src/hooks/api/subOrganizations/types.ts new file mode 100644 index 000000000..e6fa39e1e --- /dev/null +++ b/frontend/src/hooks/api/subOrganizations/types.ts @@ -0,0 +1,17 @@ +export type TSubOrganization = { + id: string; + name: string; + slug: string; + createdAt: string; + updatedAt: string; +}; + +export type TCreateSubOrganizationDTO = { + name: string; +}; + +export type TListSubOrganizationsDTO = { + limit?: number; + offset?: number; + isAccessible?: boolean; +}; diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index ede2f8cf1..2a1e078fc 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -13,6 +13,7 @@ export type SubscriptionPlan = { customRateLimits: boolean; pitRecovery: boolean; githubOrgSync: boolean; + subOrganization?: boolean; ipAllowlisting: boolean; rbac: boolean; secretVersioning: boolean; diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx index f3182ab83..ffaf5b068 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx @@ -6,12 +6,14 @@ import { faBook, faCaretDown, faCheck, + faCubes, faEnvelope, faExclamationTriangle, faGlobe, faInfinity, faInfo, faInfoCircle, + faPlus, faServer, faSignOut, faToolbox, @@ -19,7 +21,7 @@ import { faUsers } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useQueryClient } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Link, useLocation, useNavigate, useRouter, useRouterState } from "@tanstack/react-router"; import { twMerge } from "tailwind-merge"; @@ -34,6 +36,9 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, + DropdownSubMenu, + DropdownSubMenuContent, + DropdownSubMenuTrigger, IconButton, Modal, ModalContent, @@ -44,7 +49,7 @@ import { envConfig } from "@app/config/env"; import { useOrganization, useSubscription, useUser } from "@app/context"; import { isInfisicalCloud } from "@app/helpers/platform"; import { useToggle } from "@app/hooks"; -import { projectKeys, useGetOrganizations, useGetOrgTrialUrl, useLogoutUser } from "@app/hooks/api"; +import { projectKeys, subOrganizationsQuery, useGetOrganizations, useGetOrgTrialUrl, useLogoutUser } from "@app/hooks/api"; import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries"; import { MfaMethod } from "@app/hooks/api/auth/types"; import { getAuthToken } from "@app/hooks/api/reactQuery"; @@ -54,6 +59,7 @@ import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils"; import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel"; import { NotificationDropdown } from "./NotificationDropdown"; +import { NewSubOrganizationForm } from "./NewSubOrganizationForm"; const getPlan = (subscription: SubscriptionPlan) => { if (subscription.groups) return "Enterprise"; @@ -119,9 +125,15 @@ export const INFISICAL_SUPPORT_OPTIONS = [ export const Navbar = () => { const { user } = useUser(); const { subscription } = useSubscription(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); + const [showAdminsModal, setShowAdminsModal] = useState(false); + const [showSubOrgForm, setShowSubOrgForm] = useState(false); const [showCardDeclinedModal, setShowCardDeclinedModal] = useState(false); + const { data: subOrganizations = [] } = useQuery({ + ...subOrganizationsQuery.list({ limit: 500 }), + enabled: Boolean(subscription.subOrganization) && !isSubOrganization + }); useEffect(() => { if (subscription?.cardDeclined && !sessionStorage.getItem("paymentFailed")) { @@ -517,6 +529,7 @@ export const Navbar = () => { + { + + +
+ setShowSubOrgForm(true)} /> +
+
+
diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx new file mode 100644 index 000000000..81946de8d --- /dev/null +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx @@ -0,0 +1,80 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Input } from "@app/components/v2"; +import { GenericResourceNameSchema } from "@app/lib/schemas"; +import { useCreateSubOrganization } from "@app/hooks/api"; + +type ContentProps = { + onClose: () => void; +}; + +const AddOrgSchema = z.object({ + name: GenericResourceNameSchema.nonempty("Suborganization name required") +}); + +type FormData = z.infer; + +export const NewSubOrganizationForm = ({ onClose }: ContentProps) => { + const createSubOrg = useCreateSubOrganization(); + + const { + handleSubmit, + control, + formState: { isSubmitting } + } = useForm({ + defaultValues: { + name: "", + invitees: [] + }, + resolver: zodResolver(AddOrgSchema) + }); + + const onSubmit = async ({ name }: FormData) => { + try { + await createSubOrg.mutateAsync({ + name + }); + + createNotification({ + type: "success", + text: "Successfully created sub organization" + }); + onClose(); + } catch { + createNotification({ + text: "Failed to create sub organization", + type: "error" + }); + } + }; + + return ( +
+ ( + + + + )} + control={control} + name="name" + /> +
+ + +
+ + ); +}; diff --git a/frontend/src/pages/organization/layout.tsx b/frontend/src/pages/organization/layout.tsx index 79bdbf216..233b490b3 100644 --- a/frontend/src/pages/organization/layout.tsx +++ b/frontend/src/pages/organization/layout.tsx @@ -1,7 +1,14 @@ -import { createFileRoute } from "@tanstack/react-router"; +import { createFileRoute, retainSearchParams } from "@tanstack/react-router"; import { OrganizationLayout } from "@app/layouts/OrganizationLayout"; +import { z } from "zod"; export const Route = createFileRoute("/_authenticate/_inject-org-details/_org-layout")({ - component: OrganizationLayout + component: OrganizationLayout, + validateSearch: z.object({ + subOrganization: z.string().optional() + }), + search: { + middlewares: [retainSearchParams(["subOrganization"])] + } }); From 545ea4e27c28cc0dc98bf3340b532ca93ed81934 Mon Sep 17 00:00:00 2001 From: = Date: Sun, 19 Oct 2025 15:23:21 +0530 Subject: [PATCH 04/44] feat: switched to root org id pattern --- backend/src/@types/fastify.d.ts | 1 + .../db/migrations/20251018061215_sub-org.ts | 10 +++++-- backend/src/db/schemas/organizations.ts | 3 ++- .../src/ee/services/group/group-service.ts | 2 +- .../ee/services/permission/permission-dal.ts | 6 ++--- .../services/permission/permission-service.ts | 4 +-- .../project-template-types.ts | 14 +++++----- .../ee/services/sub-org/sub-org-service.ts | 11 +++++--- backend/src/lib/types/index.ts | 1 + .../server/plugins/auth/inject-identity.ts | 27 ++++++++++++++----- .../server/plugins/auth/inject-permission.ts | 8 ++++-- .../server/routes/v1/organization-router.ts | 2 +- .../services/auth-token/auth-token-service.ts | 9 ++++--- .../src/services/auth/auth-signup-service.ts | 8 +++++- .../identity-access-token-service.ts | 14 ++++++---- .../membership-user/membership-user-dal.ts | 8 ++++++ backend/src/services/org/org-dal.ts | 14 +++++----- backend/src/services/org/org-service.ts | 8 +++--- .../src/services/project/project-service.ts | 1 - .../service-token/service-token-service.ts | 15 +++++++++-- 20 files changed, 114 insertions(+), 52 deletions(-) diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 1a28a6879..5480f6dde 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -180,6 +180,7 @@ declare module "fastify" { id: string; orgId: string; parentOrgId: string; + rootOrgId: string; }; rateLimits: RateLimitConfiguration; // passport data diff --git a/backend/src/db/migrations/20251018061215_sub-org.ts b/backend/src/db/migrations/20251018061215_sub-org.ts index 0b2f72abf..ec14e5fc5 100644 --- a/backend/src/db/migrations/20251018061215_sub-org.ts +++ b/backend/src/db/migrations/20251018061215_sub-org.ts @@ -6,8 +6,12 @@ export async function up(knex: Knex): Promise { const hasParentOrgId = await knex.schema.hasColumn(TableName.Organization, "parentOrgId"); if (!hasParentOrgId) { await knex.schema.alterTable(TableName.Organization, (t) => { + // the one just above the chain t.uuid("parentOrgId"); t.foreign("parentOrgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + // this would root organization containing various informations like billing etc + t.uuid("rootOrgId"); + t.foreign("rootOrgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); }); } @@ -22,9 +26,11 @@ export async function up(knex: Knex): Promise { export async function down(knex: Knex): Promise { const hasParentOrgId = await knex.schema.hasColumn(TableName.Organization, "parentOrgId"); - if (hasParentOrgId) { + const hasRootOrgId = await knex.schema.hasColumn(TableName.Organization, "rootOrgId"); + if (hasParentOrgId || hasRootOrgId) { await knex.schema.alterTable(TableName.Organization, (t) => { - t.dropColumn("parentOrgId"); + if (hasParentOrgId) t.dropColumn("parentOrgId"); + if (hasRootOrgId) t.dropColumn("rootOrgId"); }); } diff --git a/backend/src/db/schemas/organizations.ts b/backend/src/db/schemas/organizations.ts index 38c6f797d..a1c01151f 100644 --- a/backend/src/db/schemas/organizations.ts +++ b/backend/src/db/schemas/organizations.ts @@ -39,7 +39,8 @@ export const OrganizationsSchema = z.object({ maxSharedSecretViewLimit: z.number().nullable().optional(), googleSsoAuthEnforced: z.boolean().default(false), googleSsoAuthLastUsed: z.date().nullable().optional(), - parentOrgId: z.string().uuid().nullable().optional() + parentOrgId: z.string().uuid().nullable().optional(), + rootOrgId: z.string().uuid().nullable().optional() }); export type TOrganizations = z.infer; diff --git a/backend/src/ee/services/group/group-service.ts b/backend/src/ee/services/group/group-service.ts index 0ffd77f0d..956d7853a 100644 --- a/backend/src/ee/services/group/group-service.ts +++ b/backend/src/ee/services/group/group-service.ts @@ -460,7 +460,7 @@ export const groupServiceFactory = ({ const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId, scope: OrganizationActionScope.Any diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index d35abe665..95480a54a 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -19,7 +19,7 @@ interface TPermissionDataReturn extends TMemberships { orgAuthEnforced?: boolean | null; orgGoogleSsoAuthEnforced?: boolean | null; shouldUseNewPrivilegeSystem?: boolean | null; - parentOrgId?: boolean | null; + rootOrgId?: string | null; bypassOrgAuthEnabled?: boolean | null; roles: { id: string; @@ -275,7 +275,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), db.ref("googleSsoAuthEnforced").withSchema(TableName.Organization).as("orgGoogleSsoAuthEnforced"), db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"), - db.ref("parentOrgId").withSchema(TableName.Organization).as("parentOrgId") + db.ref("rootOrgId").withSchema(TableName.Organization).as("rootOrgId") ); const data = sqlNestRelationships({ @@ -285,7 +285,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { MembershipsSchema.extend({ orgAuthEnforced: z.boolean().optional().nullable(), shouldUseNewPrivilegeSystem: z.boolean().optional().nullable(), - parentOrgId: z.string().optional().nullable(), + rootOrgId: z.string().optional().nullable(), orgGoogleSsoAuthEnforced: z.boolean(), bypassOrgAuthEnabled: z.boolean() }).parse(el), diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index dc4874b10..ec2a21352 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -209,8 +209,8 @@ export const permissionServiceFactory = ({ }); if (!permissionData?.length) throw new ForbiddenRequestError({ name: "You are not member of this organization" }); - const parentOrgId = permissionData?.[0]?.parentOrgId; - const isChild = Boolean(parentOrgId); + const rootOrgId = permissionData?.[0]?.rootOrgId; + const isChild = Boolean(rootOrgId); if (scope === OrganizationActionScope.ParentOrganization && isChild) { throw new BadRequestError({ message: `Child organization cannot do this operation` }); } else if (scope === OrganizationActionScope.ChildOrganization && !isChild) { diff --git a/backend/src/ee/services/project-template/project-template-types.ts b/backend/src/ee/services/project-template/project-template-types.ts index 8d9e952a7..1815344a7 100644 --- a/backend/src/ee/services/project-template/project-template-types.ts +++ b/backend/src/ee/services/project-template/project-template-types.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { ProjectMembershipRole, ProjectType, TProjectEnvironments } from "@app/db/schemas"; import { TProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; -import { OrgServiceActor } from "@app/lib/types"; +import { ProjectServiceActor } from "@app/lib/types"; import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; export type TProjectTemplateEnvironment = Pick; @@ -31,7 +31,7 @@ export enum InfisicalProjectTemplate { export type TProjectTemplateServiceFactory = { listProjectTemplatesByOrg: ( - actor: OrgServiceActor, + actor: ProjectServiceActor, type?: ProjectType ) => Promise< ( @@ -85,7 +85,7 @@ export type TProjectTemplateServiceFactory = { >; createProjectTemplate: ( arg: TCreateProjectTemplateDTO, - actor: OrgServiceActor + actor: ProjectServiceActor ) => Promise<{ environments: TProjectTemplateEnvironment[]; roles: { @@ -109,7 +109,7 @@ export type TProjectTemplateServiceFactory = { updateProjectTemplateById: ( id: string, { roles, environments, ...params }: TUpdateProjectTemplateDTO, - actor: OrgServiceActor + actor: ProjectServiceActor ) => Promise<{ environments: TProjectTemplateEnvironment[]; roles: { @@ -132,7 +132,7 @@ export type TProjectTemplateServiceFactory = { }>; deleteProjectTemplateById: ( id: string, - actor: OrgServiceActor + actor: ProjectServiceActor ) => Promise<{ environments: TProjectTemplateEnvironment[]; roles: { @@ -155,7 +155,7 @@ export type TProjectTemplateServiceFactory = { }>; findProjectTemplateById: ( id: string, - actor: OrgServiceActor + actor: ProjectServiceActor ) => Promise<{ packedRoles: TProjectTemplateRole[]; environments: TProjectTemplateEnvironment[]; @@ -179,7 +179,7 @@ export type TProjectTemplateServiceFactory = { }>; findProjectTemplateByName: ( name: string, - actor: OrgServiceActor + actor: ProjectServiceActor ) => Promise<{ packedRoles: TProjectTemplateRole[]; environments: TProjectTemplateEnvironment[]; diff --git a/backend/src/ee/services/sub-org/sub-org-service.ts b/backend/src/ee/services/sub-org/sub-org-service.ts index 86dd03abd..8bb6da13d 100644 --- a/backend/src/ee/services/sub-org/sub-org-service.ts +++ b/backend/src/ee/services/sub-org/sub-org-service.ts @@ -44,7 +44,7 @@ export const subOrgServiceFactory = ({ OrgPermissionSubjects.ChildOrganization ); - const orgLicensePlan = await licenseService.getPlan(permissionActor.parentOrgId); + const orgLicensePlan = await licenseService.getPlan(permissionActor.rootOrgId); if (!orgLicensePlan.subOrganization) { throw new BadRequestError({ message: "Child organization creation failed. Please upgrade your instance to Infisical's Enterprise plan." @@ -52,7 +52,10 @@ export const subOrgServiceFactory = ({ } const organization = await orgDAL.transaction(async (tx) => { - const org = await orgDAL.create({ name, slug: name, parentOrgId: permissionActor.orgId }, tx); + const org = await orgDAL.create( + { name, slug: name, rootOrgId: permissionActor.orgId, parentOrgId: permissionActor.orgId }, + tx + ); const membership = await membershipDAL.create( { scope: AccessScope.Organization, @@ -83,7 +86,7 @@ export const subOrgServiceFactory = ({ actorId: permissionActor.id, actor: permissionActor.type, orgId: permissionActor.parentOrgId, - actorOrgId: permissionActor.parentOrgId, + actorOrgId: permissionActor.rootOrgId, actorAuthMethod: permissionActor.authMethod, scope: OrganizationActionScope.ParentOrganization }); @@ -91,7 +94,7 @@ export const subOrgServiceFactory = ({ const organizations = await orgDAL.listSubOrganizations({ actorId: permissionActor.id, actorType: permissionActor.type, - orgId: permissionActor.parentOrgId, + orgId: permissionActor.rootOrgId, isAccessible: data?.isAccessible, limit: data?.limit, offset: data?.offset diff --git a/backend/src/lib/types/index.ts b/backend/src/lib/types/index.ts index ff6013b7f..f29de20f3 100644 --- a/backend/src/lib/types/index.ts +++ b/backend/src/lib/types/index.ts @@ -78,6 +78,7 @@ export type OrgServiceActor = { id: string; authMethod: ActorAuthMethod; orgId: string; + rootOrgId: string; parentOrgId: string; }; diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index f91d56f32..bde9be050 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -20,6 +20,7 @@ export type TAuthMode = tokenVersionId: string; // the session id of token used user: TUsers; orgId: string; + rootOrgId: string; parentOrgId: string; authMethod: AuthMethod; isMfaVerified?: boolean; @@ -32,6 +33,7 @@ export type TAuthMode = userId: string; user: TUsers; orgId: string; + rootOrgId: string; parentOrgId: string; token: string; } @@ -41,6 +43,7 @@ export type TAuthMode = actor: ActorType.SERVICE; serviceTokenId: string; orgId: string; + rootOrgId: string; parentOrgId: string; authMethod: null; token: string; @@ -51,6 +54,7 @@ export type TAuthMode = identityId: string; identityName: string; orgId: string; + rootOrgId: string; parentOrgId: string; authMethod: null; isInstanceAdmin?: boolean; @@ -61,6 +65,7 @@ export type TAuthMode = actor: ActorType.SCIM_CLIENT; scimTokenId: string; orgId: string; + rootOrgId: string; parentOrgId: string; authMethod: null; }; @@ -145,10 +150,8 @@ export const injectIdentity = fp( switch (authMode) { case AuthMode.JWT: { - const { user, tokenVersionId, orgId, parentOrgId } = await server.services.authToken.fnValidateJwtIdentity( - token, - subOrganizationSelector - ); + const { user, tokenVersionId, orgId, rootOrgId, parentOrgId } = + await server.services.authToken.fnValidateJwtIdentity(token, subOrganizationSelector); requestContext.set("orgId", orgId); req.auth = { @@ -158,6 +161,7 @@ export const injectIdentity = fp( tokenVersionId, actor, orgId, + rootOrgId, parentOrgId, authMethod: token.authMethod, isMfaVerified: token.isMfaVerified, @@ -177,6 +181,7 @@ export const injectIdentity = fp( authMode: AuthMode.IDENTITY_ACCESS_TOKEN, actor, orgId: identity.orgId, + rootOrgId: identity.rootOrgId, parentOrgId: identity.parentOrgId, identityId: identity.identityId, identityName: identity.name, @@ -213,7 +218,8 @@ export const injectIdentity = fp( req.auth = { orgId: serviceToken.orgId, - parentOrgId: serviceToken.orgId, + rootOrgId: serviceToken.rootOrgId, + parentOrgId: serviceToken.parentOrgId, authMode: AuthMode.SERVICE_TOKEN as const, serviceToken, serviceTokenId: serviceToken.id, @@ -235,7 +241,16 @@ export const injectIdentity = fp( if (subOrganizationSelector) throw new BadRequestError({ message: `Service token doesn't support sub organization selector` }); - req.auth = { authMode: AuthMode.SCIM_TOKEN, actor, scimTokenId, orgId, authMethod: null, parentOrgId: orgId }; + req.auth = { + authMode: AuthMode.SCIM_TOKEN, + actor, + scimTokenId, + orgId, + authMethod: null, + // scim cannot be done for sub organization + rootOrgId: orgId, + parentOrgId: orgId + }; break; } default: diff --git a/backend/src/server/plugins/auth/inject-permission.ts b/backend/src/server/plugins/auth/inject-permission.ts index da5e7e54e..827a055d3 100644 --- a/backend/src/server/plugins/auth/inject-permission.ts +++ b/backend/src/server/plugins/auth/inject-permission.ts @@ -15,6 +15,7 @@ export const injectPermission = fp(async (server) => { id: req.auth.userId, orgId: req.auth.orgId, // if the req.auth.authMode is AuthMode.API_KEY, the orgId will be "API_KEY" authMethod: req.auth.authMethod, // if the req.auth.authMode is AuthMode.API_KEY, the authMethod will be null + rootOrgId: req.auth.rootOrgId, parentOrgId: req.auth.parentOrgId }; @@ -27,6 +28,7 @@ export const injectPermission = fp(async (server) => { id: req.auth.identityId, orgId: req.auth.orgId, authMethod: null, + rootOrgId: req.auth.rootOrgId, parentOrgId: req.auth.parentOrgId }; @@ -38,7 +40,8 @@ export const injectPermission = fp(async (server) => { type: ActorType.SERVICE, id: req.auth.serviceTokenId, orgId: req.auth.orgId, - parentOrgId: req.auth.orgId, + rootOrgId: req.auth.rootOrgId, + parentOrgId: req.auth.parentOrgId, authMethod: null }; @@ -50,7 +53,8 @@ export const injectPermission = fp(async (server) => { type: ActorType.SCIM_CLIENT, id: req.auth.scimTokenId, orgId: req.auth.orgId, - parentOrgId: req.auth.orgId, + rootOrgId: req.auth.rootOrgId, + parentOrgId: req.auth.parentOrgId, authMethod: null }; diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index b640fba1a..81165f1e7 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -76,7 +76,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { req.permission.id, req.params.organizationId, req.permission.authMethod, - req.permission.parentOrgId, + req.permission.rootOrgId, req.permission.orgId ); return { organization }; diff --git a/backend/src/services/auth-token/auth-token-service.ts b/backend/src/services/auth-token/auth-token-service.ts index 6d0dfcd4c..984fb4c31 100644 --- a/backend/src/services/auth-token/auth-token-service.ts +++ b/backend/src/services/auth-token/auth-token-service.ts @@ -210,11 +210,12 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgD if (!user || !user.isAccepted) throw new NotFoundError({ message: `User with ID '${session.userId}' not found` }); let orgId = ""; + let rootOrgId = ""; let parentOrgId = ""; if (token.organizationId) { if (subOrganizationSelector) { const subOrganization = await orgDAL.findOne({ - parentOrgId: token.organizationId, + rootOrgId: token.organizationId, slug: subOrganizationSelector }); if (!subOrganization) @@ -234,7 +235,8 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgD throw new ForbiddenRequestError({ message: "User organization membership is inactive" }); } orgId = subOrganization.id; - parentOrgId = token.organizationId; + rootOrgId = token.organizationId; + parentOrgId = subOrganization.parentOrgId; } else { const orgMembership = await membershipUserDAL.findOne({ actorUserId: user.id, @@ -251,11 +253,12 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgD } orgId = token.organizationId; + rootOrgId = token.organizationId; parentOrgId = token.organizationId; } } - return { user, tokenVersionId: token.tokenVersionId, orgId, parentOrgId }; + return { user, tokenVersionId: token.tokenVersionId, orgId, rootOrgId, parentOrgId }; }; return { diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts index a2e426a2e..14f4387b9 100644 --- a/backend/src/services/auth/auth-signup-service.ts +++ b/backend/src/services/auth/auth-signup-service.ts @@ -258,7 +258,13 @@ export const authSignupServiceFactory = ({ let refreshTokenExpiresIn: string | number = appCfg.JWT_REFRESH_LIFETIME; if (organizationId) { - const org = await orgService.findOrganizationById(user.id, organizationId, authMethod, organizationId); + const org = await orgService.findOrganizationById( + user.id, + organizationId, + authMethod, + organizationId, + organizationId + ); if (org && org.userTokenExpiration) { tokenSessionExpiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration); refreshTokenExpiresIn = org.userTokenExpiration; diff --git a/backend/src/services/identity-access-token/identity-access-token-service.ts b/backend/src/services/identity-access-token/identity-access-token-service.ts index f565ebf65..bbefe923c 100644 --- a/backend/src/services/identity-access-token/identity-access-token-service.ts +++ b/backend/src/services/identity-access-token/identity-access-token-service.ts @@ -210,10 +210,12 @@ export const identityAccessTokenServiceFactory = ({ }); } let orgId = ""; - const parentOrgId = identityAccessToken.identityScopeOrgId; + let parentOrgId = ""; + const identityOrgDetails = await orgDAL.findOne({ id: identityAccessToken.identityScopeOrgId }); + const rootOrgId = identityOrgDetails.rootOrgId || identityOrgDetails.id; if (subOrganizationSelector) { - const subOrganization = await orgDAL.findOne({ parentOrgId, slug: subOrganizationSelector }); + const subOrganization = await orgDAL.findOne({ rootOrgId, slug: subOrganizationSelector }); if (!subOrganizationSelector) throw new BadRequestError({ message: `Sub organization ${subOrganizationSelector} not found` }); @@ -227,18 +229,20 @@ export const identityAccessTokenServiceFactory = ({ throw new BadRequestError({ message: "Identity does not belong to any organization" }); } orgId = subOrganization.id; + parentOrgId = subOrganization.parentOrgId as string; } else { const identityOrgMembership = await membershipIdentityDAL.findOne({ scope: AccessScope.Organization, actorIdentityId: identityAccessToken.identityId, - scopeOrgId: parentOrgId + scopeOrgId: rootOrgId }); if (!identityOrgMembership) { throw new BadRequestError({ message: "Identity does not belong to any organization" }); } - orgId = parentOrgId; + orgId = rootOrgId; + parentOrgId = rootOrgId; } let { accessTokenNumUses } = identityAccessToken; @@ -249,7 +253,7 @@ export const identityAccessTokenServiceFactory = ({ await validateAccessTokenExp({ ...identityAccessToken, accessTokenNumUses }); await accessTokenQueue.updateIdentityAccessTokenStatus(identityAccessToken.id, Number(accessTokenNumUses) + 1); - return { ...identityAccessToken, orgId, parentOrgId }; + return { ...identityAccessToken, orgId, rootOrgId, parentOrgId }; }; return { renewAccessToken, revokeAccessToken, fnValidateIdentityAccessToken }; diff --git a/backend/src/services/membership-user/membership-user-dal.ts b/backend/src/services/membership-user/membership-user-dal.ts index 7882b9639..69b9585ed 100644 --- a/backend/src/services/membership-user/membership-user-dal.ts +++ b/backend/src/services/membership-user/membership-user-dal.ts @@ -291,5 +291,13 @@ export const membershipUserDALFactory = (db: TDbClient) => { } }; + // const listAvailableUsers = async (scopeData: AccessScopeData) => { + // try { + // const query = await db.replicaNode()(TableName.Membership).where(`${TableName.Membership}.scopeOrgId`); + // } catch (error) { + // throw new DatabaseError({ error, name: "ListAvailableUsers" }); + // } + // }; + return { ...orm, findUsers, getUserById }; }; diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index d7efdc463..fd1a361f0 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -65,7 +65,7 @@ export const orgDALFactory = (db: TDbClient) => { const buildBaseQuery = (orgIdSubquery: Knex.QueryBuilder) => { return db .replicaNode()(TableName.Organization) - .whereNull(`${TableName.Organization}.parentOrgId`) + .whereNull(`${TableName.Organization}.rootOrgId`) .whereIn(`${TableName.Organization}.id`, orgIdSubquery) .leftJoin(TableName.Project, `${TableName.Organization}.id`, `${TableName.Project}.orgId`) .leftJoin(TableName.Membership, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`) @@ -168,7 +168,7 @@ export const orgDALFactory = (db: TDbClient) => { // TODO(sub-org:group): check this when implement group support const query = db .replicaNode()(TableName.Organization) - .where(`${TableName.Organization}.parentOrgId`, dto.orgId) + .where(`${TableName.Organization}.rootOrgId`, dto.orgId) .select(selectAllTableCols(TableName.Organization)); if (dto.isAccessible) { @@ -196,7 +196,7 @@ export const orgDALFactory = (db: TDbClient) => { const org = (await db .replicaNode()(TableName.Organization) .where({ [`${TableName.Organization}.id` as "id"]: orgId }) - .whereNull(`${TableName.Organization}.parentOrgId`) + .whereNull(`${TableName.Organization}.rootOrgId`) .leftJoin(TableName.SamlConfig, (qb) => { qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn( `${TableName.SamlConfig}.isActive`, @@ -233,7 +233,7 @@ export const orgDALFactory = (db: TDbClient) => { try { const org = (await db .replicaNode()(TableName.Organization) - .whereNull(`${TableName.Organization}.parentOrgId`) + .whereNull(`${TableName.Organization}.rootOrgId`) .where({ [`${TableName.Organization}.slug` as "slug"]: orgSlug }) .leftJoin(TableName.SamlConfig, (qb) => { qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn( @@ -279,7 +279,7 @@ export const orgDALFactory = (db: TDbClient) => { .whereNotNull(`${TableName.Membership}.actorUserId`) .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) .join(TableName.Organization, `${TableName.Membership}.scopeOrgId`, `${TableName.Organization}.id`) - .whereNull(`${TableName.Organization}.parentOrgId`) + .whereNull(`${TableName.Organization}.rootOrgId`) .leftJoin(TableName.SamlConfig, (qb) => { qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn( `${TableName.SamlConfig}.isActive`, @@ -651,7 +651,7 @@ export const orgDALFactory = (db: TDbClient) => { }) .join(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`) .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`) - .whereNull(`${TableName.Organization}.parentOrgId`) + .whereNull(`${TableName.Organization}.rootOrgId`) .leftJoin(TableName.UserAliases, function joinUserAlias() { this.on(`${TableName.UserAliases}.userId`, "=", `${TableName.Membership}.actorUserId`) .andOn(`${TableName.UserAliases}.orgId`, "=", `${TableName.Membership}.scopeOrgId`) @@ -690,7 +690,7 @@ export const orgDALFactory = (db: TDbClient) => { .replicaNode()(TableName.Membership) .where({ actorIdentityId: identityId }) .where(`${TableName.Membership}.scope`, AccessScope.Organization) - .whereNull(`${TableName.Organization}.parentOrgId`) + .whereNull(`${TableName.Organization}.rootOrgId`) .whereNotNull(`${TableName.Membership}.actorIdentityId`) .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) .join(TableName.Organization, `${TableName.Membership}.scopeOrgId`, `${TableName.Organization}.id`) diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 011af3520..76c1fb801 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -157,7 +157,7 @@ export const orgServiceFactory = ({ userId: string, orgId: string, actorAuthMethod: ActorAuthMethod, - parentOrgId: string, + rootOrgId: string, actorOrgId: string ) => { await permissionService.getOrgPermission({ @@ -165,17 +165,17 @@ export const orgServiceFactory = ({ actorId: userId, orgId, actorAuthMethod, - actorOrgId: parentOrgId, + actorOrgId: rootOrgId, scope: OrganizationActionScope.Any }); const appCfg = getConfig(); const org = await orgDAL.findOrgById(orgId); if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); - const hasSubOrg = actorOrgId !== parentOrgId; + const hasSubOrg = actorOrgId !== rootOrgId; let subOrg; if (hasSubOrg) { - subOrg = await orgDAL.findOne({ parentOrgId, id: actorOrgId }); + subOrg = await orgDAL.findOne({ rootOrgId, id: actorOrgId }); } if (!org.userTokenExpiration) { diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index e17787351..628e8891c 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -298,7 +298,6 @@ export const projectServiceFactory = ({ projectTemplate = await projectTemplateService.findProjectTemplateByName(template, { id: actorId, orgId: organization.id, - parentOrgId: organization.id, type: actor, authMethod: actorAuthMethod }); diff --git a/backend/src/services/service-token/service-token-service.ts b/backend/src/services/service-token/service-token-service.ts index 2aa495673..8b50e9970 100644 --- a/backend/src/services/service-token/service-token-service.ts +++ b/backend/src/services/service-token/service-token-service.ts @@ -25,10 +25,12 @@ import { TGetServiceTokenInfoDTO, TProjectServiceTokensDTO } from "./service-token-types"; +import { TOrgDALFactory } from "../org/org-dal"; type TServiceTokenServiceFactoryDep = { serviceTokenDAL: TServiceTokenDALFactory; userDAL: TUserDALFactory; + orgDAL: Pick; permissionService: Pick; projectEnvDAL: Pick; projectDAL: Pick; @@ -45,7 +47,8 @@ export const serviceTokenServiceFactory = ({ projectEnvDAL, projectDAL, accessTokenQueue, - smtpService + smtpService, + orgDAL }: TServiceTokenServiceFactoryDep) => { const createServiceToken = async ({ iv, @@ -184,7 +187,15 @@ export const serviceTokenServiceFactory = ({ if (!isMatch) throw new UnauthorizedError({ message: "Invalid service token" }); await accessTokenQueue.updateServiceTokenStatus(serviceToken.id); - return { ...serviceToken, lastUsed: new Date(), orgId: project.orgId }; + const serviceTokenOrgDetails = await orgDAL.findById(project.orgId); + + return { + ...serviceToken, + lastUsed: new Date(), + orgId: project.orgId, + parentOrgId: serviceTokenOrgDetails.parentOrgId || serviceTokenOrgDetails.id, + rootOrgId: serviceTokenOrgDetails.rootOrgId || serviceTokenOrgDetails.id + }; }; const notifyExpiringTokens = async () => { From b7644c929419470658b72a9876b9c8fe93ed419d Mon Sep 17 00:00:00 2001 From: = Date: Sun, 19 Oct 2025 21:41:21 +0530 Subject: [PATCH 05/44] feat: added root org identity link functionality --- backend/src/ee/routes/v1/sub-org-router.ts | 6 +- .../ee/services/license/license-service.ts | 15 +- backend/src/server/routes/index.ts | 4 +- .../v1/identity-org-membership-router.ts | 137 ++++++++++++++++++ backend/src/server/routes/v1/index.ts | 2 + .../server/routes/v1/organization-router.ts | 65 +++++++++ .../services/auth-token/auth-token-service.ts | 2 +- .../src/services/identity/identity-service.ts | 6 +- .../membership-identity-dal.ts | 34 ++++- .../membership-identity-service.ts | 30 +++- .../membership-identity-types.ts | 5 +- .../org/org-membership-identity-factory.ts | 87 +++++++++-- .../membership-user/membership-user-dal.ts | 40 ++++- .../membership-user-service.ts | 20 ++- .../membership-user/membership-user-types.ts | 5 + .../org/org-membership-user-factory.ts | 23 ++- .../OrganizationContext.tsx | 3 +- .../hooks/api/orgIdentityMembership/index.tsx | 2 + .../api/orgIdentityMembership/mutation.tsx | 42 ++++++ .../hooks/api/orgIdentityMembership/types.ts | 30 ++++ frontend/src/hooks/api/organization/index.ts | 1 + .../src/hooks/api/organization/queries.tsx | 30 +++- .../IdentitySection/IdentityLinkForm.tsx | 129 +++++++++++++++++ .../IdentitySection/IdentitySection.tsx | 60 ++++++-- .../pages/organization/BillingPage/route.tsx | 11 +- .../components/OrgTabGroup/OrgTabGroup.tsx | 46 ++++-- 26 files changed, 753 insertions(+), 82 deletions(-) create mode 100644 backend/src/server/routes/v1/identity-org-membership-router.ts create mode 100644 frontend/src/hooks/api/orgIdentityMembership/index.tsx create mode 100644 frontend/src/hooks/api/orgIdentityMembership/mutation.tsx create mode 100644 frontend/src/hooks/api/orgIdentityMembership/types.ts create mode 100644 frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLinkForm.tsx diff --git a/backend/src/ee/routes/v1/sub-org-router.ts b/backend/src/ee/routes/v1/sub-org-router.ts index 281b54e35..aed2b63d6 100644 --- a/backend/src/ee/routes/v1/sub-org-router.ts +++ b/backend/src/ee/routes/v1/sub-org-router.ts @@ -49,7 +49,8 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => { type: req.permission.type, authMethod: req.permission.authMethod, orgId: req.permission.orgId, - parentOrgId: req.permission.parentOrgId + parentOrgId: req.permission.parentOrgId, + rootOrgId: req.permission.rootOrgId } }); @@ -107,7 +108,8 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => { type: req.permission.type, authMethod: req.permission.authMethod, orgId: req.permission.orgId, - parentOrgId: req.permission.orgId + parentOrgId: req.permission.orgId, + rootOrgId: req.permission.rootOrgId }, data: { limit: req.query.limit, diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index 2d539dbf0..8fc4987f4 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -285,19 +285,20 @@ export const licenseServiceFactory = ({ }; const updateSubscriptionOrgMemberCount = async (orgId: string, tx?: Knex) => { - if (instanceType === InstanceType.Cloud) { - const org = await orgDAL.findOrgById(orgId); - if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); + const org = await orgDAL.findOrgById(orgId); + if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); - const quantity = await licenseDAL.countOfOrgMembers(orgId, tx); - const quantityIdentities = await licenseDAL.countOrgUsersAndIdentities(orgId, tx); + const rootOrgId = org.rootOrgId || org.id; + if (instanceType === InstanceType.Cloud) { + const quantity = await licenseDAL.countOfOrgMembers(rootOrgId, tx); + const quantityIdentities = await licenseDAL.countOrgUsersAndIdentities(rootOrgId, tx); if (org?.customerId) { await licenseServerCloudApi.request.patch(`/api/license-server/v1/customers/${org.customerId}/cloud-plan`, { quantity, quantityIdentities }); } - await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId)); + await keyStore.deleteItem(FEATURE_CACHE_KEY(rootOrgId)); } else if (instanceType === InstanceType.EnterpriseOnPrem) { const usedSeats = await licenseDAL.countOfOrgMembers(null, tx); const usedIdentitySeats = await licenseDAL.countOrgUsersAndIdentities(null, tx); @@ -308,7 +309,7 @@ export const licenseServiceFactory = ({ usedIdentitySeats }); } - await refreshPlan(orgId); + await refreshPlan(rootOrgId); }; // below all are api calls diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index bcf508835..f23fcb7f4 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -585,6 +585,7 @@ export const registerRoutes = async ( }); const membershipIdentityService = membershipIdentityServiceFactory({ + identityDAL, membershipIdentityDAL, membershipRoleDAL, orgDAL, @@ -1577,7 +1578,8 @@ export const registerRoutes = async ( permissionService, projectDAL, accessTokenQueue, - smtpService + smtpService, + orgDAL }); const identityService = identityServiceFactory({ diff --git a/backend/src/server/routes/v1/identity-org-membership-router.ts b/backend/src/server/routes/v1/identity-org-membership-router.ts new file mode 100644 index 000000000..c9b93965a --- /dev/null +++ b/backend/src/server/routes/v1/identity-org-membership-router.ts @@ -0,0 +1,137 @@ +import { z } from "zod"; + +import { AccessScope, TemporaryPermissionMode } from "@app/db/schemas"; +import { ApiDocsTags, PROJECT_IDENTITIES } from "@app/lib/api-docs"; +import { ms } from "@app/lib/ms"; +import { writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +const sanitizedOrgIdentityMembershipSchema = z.object({ + id: z.string().uuid(), + orgId: z.string(), + identityId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export const registerOrgIdentityMembershipRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/identity-memberships/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + hide: true, + // this is hidden so not updating tags + tags: [ApiDocsTags.ProjectIdentities], + description: "Create org identity membership", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().trim() + }), + body: z.object({ + roles: z + .array( + z.union([ + z.object({ + role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), + isTemporary: z + .literal(false) + .default(false) + .describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role) + }), + z.object({ + role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), + isTemporary: z.literal(true).describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), + temporaryMode: z + .nativeEnum(TemporaryPermissionMode) + .describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), + temporaryRange: z + .string() + .refine((val) => ms(val) > 0, "Temporary range must be a positive number") + .describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), + temporaryAccessStartTime: z + .string() + .datetime() + .describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role) + }) + ]) + ) + .describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.description) + .max(1) + }), + response: { + 200: z.object({ + identityMembership: sanitizedOrgIdentityMembershipSchema + }) + } + }, + handler: async (req) => { + const { membership } = await server.services.membershipIdentity.createMembership({ + permission: req.permission, + scopeData: { + scope: AccessScope.Organization, + orgId: req.permission.orgId + }, + data: { + identityId: req.params.identityId, + roles: req.body.roles + } + }); + + return { + identityMembership: { ...membership, identityId: req.params.identityId, orgId: req.permission.orgId } + }; + } + }); + + server.route({ + method: "DELETE", + url: "/identity-memberships/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + hide: true, + tags: [ApiDocsTags.ProjectIdentities], + description: "Delete org identity memberships", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().trim().describe(PROJECT_IDENTITIES.DELETE_IDENTITY_MEMBERSHIP.identityId) + }), + response: { + 200: z.object({ + identityMembership: sanitizedOrgIdentityMembershipSchema + }) + } + }, + handler: async (req) => { + const { membership } = await server.services.membershipIdentity.deleteMembership({ + permission: req.permission, + scopeData: { + scope: AccessScope.Organization, + orgId: req.permission.orgId + }, + selector: { + identityId: req.params.identityId + } + }); + + return { + identityMembership: { ...membership, identityId: req.params.identityId, orgId: req.permission.orgId } + }; + } + }); +}; diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 89865b1a1..307b922d3 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -65,6 +65,7 @@ import { registerUserEngagementRouter } from "./user-engagement-router"; import { registerUserRouter } from "./user-router"; import { registerWebhookRouter } from "./webhook-router"; import { registerWorkflowIntegrationRouter } from "./workflow-integration-router"; +import { registerOrgIdentityMembershipRouter } from "./identity-org-membership-router"; export const registerV1Routes = async (server: FastifyZodProvider) => { await server.register(registerSsoRouter, { prefix: "/sso" }); @@ -89,6 +90,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { ); await server.register(registerPasswordRouter, { prefix: "/password" }); await server.register(registerOrgRouter, { prefix: "/organization" }); + await server.register(registerOrgIdentityMembershipRouter, { prefix: "/organization" }); await server.register(registerAdminRouter, { prefix: "/admin" }); await server.register(registerOrgAdminRouter, { prefix: "/organization-admin" }); await server.register(registerUserRouter, { prefix: "/user" }); diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index 81165f1e7..76b3eae51 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -2,6 +2,7 @@ import RE2 from "re2"; import { z } from "zod"; import { + AccessScope, AuditLogsSchema, GroupsSchema, IncidentContactsSchema, @@ -475,4 +476,68 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { return { groups }; } }); + + server.route({ + method: "GET", + url: "/users/available", + schema: { + response: { + 200: z.object({ + users: z + .object({ + id: z.string().uuid(), + username: z.string(), + email: z.string().nullable().optional(), + firstName: z.string().nullable().optional(), + lastName: z.string().nullable().optional() + }) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { users } = await server.services.membershipUser.listAvailableUsers({ + permission: req.permission, + scopeData: { + orgId: req.permission.orgId, + scope: AccessScope.Organization + }, + data: {} + }); + + return { users }; + } + }); + + server.route({ + method: "GET", + url: "/identities/available", + schema: { + response: { + 200: z.object({ + identities: z + .object({ + id: z.string().uuid(), + name: z.string(), + hasDeleteProtection: z.boolean() + }) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { identities } = await server.services.membershipIdentity.listAvailableIdentities({ + permission: req.permission, + scopeData: { + orgId: req.permission.orgId, + scope: AccessScope.Organization + }, + data: {} + }); + + return { identities }; + } + }); }; diff --git a/backend/src/services/auth-token/auth-token-service.ts b/backend/src/services/auth-token/auth-token-service.ts index 984fb4c31..28a986fe8 100644 --- a/backend/src/services/auth-token/auth-token-service.ts +++ b/backend/src/services/auth-token/auth-token-service.ts @@ -236,7 +236,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgD } orgId = subOrganization.id; rootOrgId = token.organizationId; - parentOrgId = subOrganization.parentOrgId; + parentOrgId = subOrganization.parentOrgId as string; } else { const orgMembership = await membershipUserDAL.findOne({ actorUserId: user.id, diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index e83f2f369..f35c80032 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -216,11 +216,12 @@ export const identityServiceFactory = ({ if (isCustomRole) customRole = rolePermissionDetails?.role; } + const identityDetails = await identityDAL.findById(id); const identity = await identityDAL.transaction(async (tx) => { const newIdentity = - name || hasDeleteProtection + identityDetails.orgId === actorOrgId && (name || hasDeleteProtection) ? await identityDAL.updateById(id, { name, hasDeleteProtection }, tx) - : await identityDAL.findById(id, tx); + : identityDetails; if (role) { await membershipRoleDAL.delete({ membershipId: identityOrgMembership.id }, tx); @@ -282,7 +283,6 @@ export const identityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - // TODO(namespace): check this in identity service const activeLockouts = await keyStore.getKeysByPattern(`lockout:identity:${id}:*`); const activeLockoutAuthMethods = new Set(); diff --git a/backend/src/services/membership-identity/membership-identity-dal.ts b/backend/src/services/membership-identity/membership-identity-dal.ts index 64e508fb8..78df6a757 100644 --- a/backend/src/services/membership-identity/membership-identity-dal.ts +++ b/backend/src/services/membership-identity/membership-identity-dal.ts @@ -91,6 +91,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => { .select( db.ref("name").withSchema(TableName.Identity).as("identityName"), db.ref("id").withSchema(TableName.Identity).as("identityId"), + db.ref("orgId").withSchema(TableName.Identity).as("identityOrgId"), db.ref("hasDeleteProtection").withSchema(TableName.Identity).as("identityHasDeleteProtection"), db.ref("slug").withSchema(TableName.Role).as("roleSlug"), @@ -132,6 +133,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => { parentMapper: (el) => { const { identityId: actorIdentityId, + identityOrgId, identityHasDeleteProtection, identityName, uaId, @@ -153,6 +155,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => { name: identityName, id: actorIdentityId, hasDeleteProtection: identityHasDeleteProtection, + identityOrgId, authMethods: buildAuthMethods({ uaId, awsId, @@ -353,5 +356,34 @@ export const membershipIdentityDALFactory = (db: TDbClient) => { } }; - return { ...orm, findIdentities, getIdentityById }; + // this right nwo only support sub organization + const listAvailableIdentities = async (orgId: string, rootOrgId: string) => { + try { + const usersConnectedToOrg = db + .replicaNode()(TableName.Membership) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .select("actorIdentityId"); + + const docs = await db + .replicaNode()(TableName.Membership) + .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Membership}.actorIdentityId`) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .where(`${TableName.Membership}.scopeOrgId`, rootOrgId) + .whereNotIn(`${TableName.Membership}.actorIdentityId`, usersConnectedToOrg) + .select( + db.ref("id").withSchema(TableName.Identity), + db.ref("name").withSchema(TableName.Identity), + db.ref("hasDeleteProtection").withSchema(TableName.Identity) + ); + + return docs; + } catch (error) { + throw new DatabaseError({ error, name: "ListAvailableUsers" }); + } + }; + + return { ...orm, findIdentities, getIdentityById, listAvailableIdentities }; }; diff --git a/backend/src/services/membership-identity/membership-identity-service.ts b/backend/src/services/membership-identity/membership-identity-service.ts index 4dd3da064..c1bb9cbbc 100644 --- a/backend/src/services/membership-identity/membership-identity-service.ts +++ b/backend/src/services/membership-identity/membership-identity-service.ts @@ -20,6 +20,7 @@ import { import { newNamespaceMembershipIdentityFactory } from "./namespace/namespace-membership-identity-factory"; import { newOrgMembershipIdentityFactory } from "./org/org-membership-identity-factory"; import { newProjectMembershipIdentityFactory } from "./project/project-membership-identity-factory"; +import { TIdentityDALFactory } from "../identity/identity-dal"; type TMembershipIdentityServiceFactoryDep = { membershipIdentityDAL: TMembershipIdentityDALFactory; @@ -31,6 +32,7 @@ type TMembershipIdentityServiceFactoryDep = { >; orgDAL: Pick; additionalPrivilegeDAL: Pick; + identityDAL: Pick; }; export type TMembershipIdentityServiceFactory = ReturnType; @@ -41,12 +43,14 @@ export const membershipIdentityServiceFactory = ({ membershipRoleDAL, permissionService, orgDAL, - additionalPrivilegeDAL + additionalPrivilegeDAL, + identityDAL }: TMembershipIdentityServiceFactoryDep) => { const scopeFactory = { [AccessScope.Organization]: newOrgMembershipIdentityFactory({ orgDAL, - permissionService + permissionService, + identityDAL }), [AccessScope.Project]: newProjectMembershipIdentityFactory({ membershipIdentityDAL, @@ -305,7 +309,7 @@ export const membershipIdentityServiceFactory = ({ [SearchResourceOperators.$contains]: dto.data.identityName } : undefined, - role: dto.data.roles.length + role: dto.data?.roles?.length ? { [SearchResourceOperators.$in]: dto.data.roles } @@ -329,11 +333,29 @@ export const membershipIdentityServiceFactory = ({ return membership; }; + const listAvailableIdentities = async (dto: TListMembershipIdentityDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onListMembershipIdentityGuard(dto); + + const organizationDetails = await orgDAL.findById(dto.scopeData.orgId); + if (!organizationDetails.rootOrgId) return { identities: [] }; + + const identities = await membershipIdentityDAL.listAvailableIdentities( + organizationDetails.id, + organizationDetails.rootOrgId + ); + + return { identities }; + }; + return { createMembership, updateMembership, deleteMembership, listMemberships, - getMembershipByIdentityId + getMembershipByIdentityId, + listAvailableIdentities }; }; diff --git a/backend/src/services/membership-identity/membership-identity-types.ts b/backend/src/services/membership-identity/membership-identity-types.ts index adce10237..78923cb14 100644 --- a/backend/src/services/membership-identity/membership-identity-types.ts +++ b/backend/src/services/membership-identity/membership-identity-types.ts @@ -54,14 +54,11 @@ export type TUpdateMembershipIdentityDTO = { export type TListMembershipIdentityDTO = { permission: OrgServiceActor; scopeData: AccessScopeData; - selector: { - identityId: string; - }; data: { limit?: number; offset?: number; identityName?: string; - roles: string[]; + roles?: string[]; }; }; diff --git a/backend/src/services/membership-identity/org/org-membership-identity-factory.ts b/backend/src/services/membership-identity/org/org-membership-identity-factory.ts index fce31b0fe..caffc984e 100644 --- a/backend/src/services/membership-identity/org/org-membership-identity-factory.ts +++ b/backend/src/services/membership-identity/org/org-membership-identity-factory.ts @@ -12,15 +12,18 @@ import { TOrgDALFactory } from "@app/services/org/org-dal"; import { isCustomOrgRole } from "@app/services/org/org-role-fns"; import { TMembershipIdentityScopeFactory } from "../membership-identity-types"; +import { TIdentityDALFactory } from "@app/services/identity/identity-dal"; type TOrgMembershipIdentityScopeFactoryDep = { permissionService: Pick; orgDAL: Pick; + identityDAL: Pick; }; export const newOrgMembershipIdentityFactory = ({ permissionService, - orgDAL + orgDAL, + identityDAL }: TOrgMembershipIdentityScopeFactoryDep): TMembershipIdentityScopeFactory => { const getScopeField: TMembershipIdentityScopeFactory["getScopeField"] = (dto) => { if (dto.scope === AccessScope.Organization) { @@ -38,12 +41,53 @@ export const newOrgMembershipIdentityFactory = ({ const isCustomRole: TMembershipIdentityScopeFactory["isCustomRole"] = (role: string) => isCustomOrgRole(role); - const onCreateMembershipIdentityGuard: TMembershipIdentityScopeFactory["onCreateMembershipIdentityGuard"] = - async () => { - throw new BadRequestError({ - message: "Organization membership cannot be created for organization scoped identity" - }); - }; + const onCreateMembershipIdentityGuard: TMembershipIdentityScopeFactory["onCreateMembershipIdentityGuard"] = async ( + dto + ) => { + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.ChildOrganization + }); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + + const identityDetails = await identityDAL.findById(dto.data.identityId); + if (identityDetails.orgId !== dto.permission.rootOrgId) { + throw new BadRequestError({ message: "Only identites from parent organization can be invited" }); + } + + const permissionRoles = await permissionService.getOrgPermissionByRoles( + dto.data.roles.map((el) => el.role), + dto.permission.orgId + ); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(dto.permission.orgId); + for (const permissionRole of permissionRoles) { + if (permissionRole?.role?.name !== OrgMembershipRole.NoAccess) { + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.GrantPrivileges, + OrgPermissionSubjects.Identity, + permission, + permissionRole.permission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to update identity org membership", + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.GrantPrivileges, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + } + }; const onUpdateMembershipIdentityGuard: TMembershipIdentityScopeFactory["onUpdateMembershipIdentityGuard"] = async ( dto @@ -87,12 +131,29 @@ export const newOrgMembershipIdentityFactory = ({ } }; - const onDeleteMembershipIdentityGuard: TMembershipIdentityScopeFactory["onDeleteMembershipIdentityGuard"] = - async () => { - throw new BadRequestError({ - message: "Organization membership cannot be deleted for organization scoped identity" - }); - }; + const onDeleteMembershipIdentityGuard: TMembershipIdentityScopeFactory["onDeleteMembershipIdentityGuard"] = async ( + dto + ) => { + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.ChildOrganization + }); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + + const identityDetails = await identityDAL.findById(dto.selector.identityId); + if (identityDetails.orgId !== dto.permission.rootOrgId) { + throw new BadRequestError({ message: "Only identites from parent organization can do this operation" }); + } + + if (identityDetails.orgId === dto.permission.orgId) { + throw new BadRequestError({ message: "Identity cannot exist as orphan" }); + } + }; const onListMembershipIdentityGuard: TMembershipIdentityScopeFactory["onListMembershipIdentityGuard"] = async ( dto diff --git a/backend/src/services/membership-user/membership-user-dal.ts b/backend/src/services/membership-user/membership-user-dal.ts index 69b9585ed..41fa09696 100644 --- a/backend/src/services/membership-user/membership-user-dal.ts +++ b/backend/src/services/membership-user/membership-user-dal.ts @@ -291,13 +291,37 @@ export const membershipUserDALFactory = (db: TDbClient) => { } }; - // const listAvailableUsers = async (scopeData: AccessScopeData) => { - // try { - // const query = await db.replicaNode()(TableName.Membership).where(`${TableName.Membership}.scopeOrgId`); - // } catch (error) { - // throw new DatabaseError({ error, name: "ListAvailableUsers" }); - // } - // }; + // this right nwo only support sub organization + const listAvailableUsers = async (orgId: string, rootOrgId: string) => { + try { + const usersConnectedToOrg = db + .replicaNode()(TableName.Membership) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .select("actorUserId"); - return { ...orm, findUsers, getUserById }; + const docs = await db + .replicaNode()(TableName.Membership) + .join(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .where(`${TableName.Users}.isGhost`, false) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .where(`${TableName.Membership}.scopeOrgId`, rootOrgId) + .whereNot(`${TableName.Membership}.actorUserId`, usersConnectedToOrg) + .select( + db.ref("id").withSchema(TableName.Users), + db.ref("email").withSchema(TableName.Users), + db.ref("username").withSchema(TableName.Users), + db.ref("firstName").withSchema(TableName.Users), + db.ref("lastName").withSchema(TableName.Users) + ); + + return docs; + } catch (error) { + throw new DatabaseError({ error, name: "ListAvailableUsers" }); + } + }; + + return { ...orm, findUsers, getUserById, listAvailableUsers }; }; diff --git a/backend/src/services/membership-user/membership-user-service.ts b/backend/src/services/membership-user/membership-user-service.ts index 82dca0159..f6265d2bb 100644 --- a/backend/src/services/membership-user/membership-user-service.ts +++ b/backend/src/services/membership-user/membership-user-service.ts @@ -83,7 +83,8 @@ export const membershipUserServiceFactory = ({ orgDAL, tokenService, userDAL, - userGroupMembershipDAL + userGroupMembershipDAL, + membershipUserDAL }), [AccessScope.Namespace]: newNamespaceMembershipUserFactory({}), [AccessScope.Project]: newProjectMembershipUserFactory({ @@ -471,11 +472,26 @@ export const membershipUserServiceFactory = ({ return membership; }; + // Should only be used for sub organization as of now + const listAvailableUsers = async (dto: TListMembershipUserDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onListMembershipUserGuard(dto); + + const organizationDetails = await orgDAL.findById(dto.scopeData.orgId); + if (!organizationDetails.rootOrgId) return { users: [] }; + + const users = await membershipUserDAL.listAvailableUsers(organizationDetails.id, organizationDetails.rootOrgId); + return { users }; + }; + return { createMembership, updateMembership, deleteMembership, listMemberships, - getMembershipByUserId + getMembershipByUserId, + listAvailableUsers }; }; diff --git a/backend/src/services/membership-user/membership-user-types.ts b/backend/src/services/membership-user/membership-user-types.ts index b8761671c..15982bb6a 100644 --- a/backend/src/services/membership-user/membership-user-types.ts +++ b/backend/src/services/membership-user/membership-user-types.ts @@ -93,3 +93,8 @@ export type TGetMembershipUserByUserIdDTO = { userId: string; }; }; + +export type TListAvailableUsersDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; +}; diff --git a/backend/src/services/membership-user/org/org-membership-user-factory.ts b/backend/src/services/membership-user/org/org-membership-user-factory.ts index 5caf77c2c..761a1397d 100644 --- a/backend/src/services/membership-user/org/org-membership-user-factory.ts +++ b/backend/src/services/membership-user/org/org-membership-user-factory.ts @@ -16,6 +16,7 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { TUserDALFactory } from "@app/services/user/user-dal"; import { TMembershipUserScopeFactory } from "../membership-user-types"; +import { TMembershipUserDALFactory } from "../membership-user-dal"; type TOrgMembershipUserScopeFactoryDep = { permissionService: Pick; @@ -25,6 +26,7 @@ type TOrgMembershipUserScopeFactoryDep = { orgDAL: Pick; userGroupMembershipDAL: Pick; licenseService: Pick; + membershipUserDAL: Pick; }; export const newOrgMembershipUserFactory = ({ @@ -33,7 +35,8 @@ export const newOrgMembershipUserFactory = ({ userDAL, orgDAL, smtpService, - licenseService + licenseService, + membershipUserDAL }: TOrgMembershipUserScopeFactoryDep): TMembershipUserScopeFactory => { const getScopeField: TMembershipUserScopeFactory["getScopeField"] = (dto) => { if (dto.scope === AccessScope.Organization) { @@ -51,7 +54,10 @@ export const newOrgMembershipUserFactory = ({ const isCustomRole: TMembershipUserScopeFactory["isCustomRole"] = (role: string) => isCustomOrgRole(role); - const onCreateMembershipUserGuard: TMembershipUserScopeFactory["onCreateMembershipUserGuard"] = async (dto) => { + const onCreateMembershipUserGuard: TMembershipUserScopeFactory["onCreateMembershipUserGuard"] = async ( + dto, + newMembers + ) => { const { permission } = await permissionService.getOrgPermission({ actor: dto.permission.type, actorId: dto.permission.id, @@ -78,6 +84,19 @@ export const newOrgMembershipUserFactory = ({ message: "Failed to invite user due to org-level auth enforced for organization" }); } + if (org.rootOrgId) { + const rootOrgMembership = await membershipUserDAL.find({ + scope: AccessScope.Organization, + $in: { + actorUserId: newMembers.map((el) => el.id) + }, + scopeOrgId: org.rootOrgId + }); + if (rootOrgMembership.length !== newMembers.length) + throw new BadRequestError({ + message: "User doesn't have membership in root organization" + }); + } }; const onCreateMembershipComplete: TMembershipUserScopeFactory["onCreateMembershipComplete"] = async ( diff --git a/frontend/src/context/OrganizationContext/OrganizationContext.tsx b/frontend/src/context/OrganizationContext/OrganizationContext.tsx index b7726e6a4..5a18ec741 100644 --- a/frontend/src/context/OrganizationContext/OrganizationContext.tsx +++ b/frontend/src/context/OrganizationContext/OrganizationContext.tsx @@ -21,6 +21,7 @@ export const useOrganization = () => { id: currentOrg?.subOrganization?.id || currentOrg?.id, parentOrgId: currentOrg.id }, - isSubOrganization: Boolean(currentOrg.subOrganization) + isSubOrganization: Boolean(currentOrg.subOrganization), + isRootOrganization: !currentOrg.subOrganization }; }; diff --git a/frontend/src/hooks/api/orgIdentityMembership/index.tsx b/frontend/src/hooks/api/orgIdentityMembership/index.tsx new file mode 100644 index 000000000..61d572e30 --- /dev/null +++ b/frontend/src/hooks/api/orgIdentityMembership/index.tsx @@ -0,0 +1,2 @@ +export { useCreateOrgIdentityMembership, useDeleteOrgIdentityMembership } from "./mutation"; +export type { TCreateOrgIdentityMembershipDTO, TDeleteOrgIdentityMembershipDTO, TOrgIdentityMembership } from "./types"; diff --git a/frontend/src/hooks/api/orgIdentityMembership/mutation.tsx b/frontend/src/hooks/api/orgIdentityMembership/mutation.tsx new file mode 100644 index 000000000..cd5787842 --- /dev/null +++ b/frontend/src/hooks/api/orgIdentityMembership/mutation.tsx @@ -0,0 +1,42 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { + TCreateOrgIdentityMembershipDTO, + TDeleteOrgIdentityMembershipDTO, + TOrgIdentityMembership +} from "./types"; + +export const useCreateOrgIdentityMembership = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ identityId, roles }: TCreateOrgIdentityMembershipDTO) => { + const { data } = await apiRequest.post<{ identityMembership: TOrgIdentityMembership }>( + `/api/v1/organization/identity-memberships/${identityId}`, + { roles } + ); + return data.identityMembership; + }, + onSuccess: () => { + // Invalidate relevant queries if needed + queryClient.invalidateQueries({ queryKey: ["organization"] }); + } + }); +}; + +export const useDeleteOrgIdentityMembership = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ identityId }: TDeleteOrgIdentityMembershipDTO) => { + const { data } = await apiRequest.delete<{ identityMembership: TOrgIdentityMembership }>( + `/api/v1/organization/identity-memberships/${identityId}` + ); + return data.identityMembership; + }, + onSuccess: () => { + // Invalidate relevant queries if needed + queryClient.invalidateQueries({ queryKey: ["organization"] }); + } + }); +}; diff --git a/frontend/src/hooks/api/orgIdentityMembership/types.ts b/frontend/src/hooks/api/orgIdentityMembership/types.ts new file mode 100644 index 000000000..a50ddc1d0 --- /dev/null +++ b/frontend/src/hooks/api/orgIdentityMembership/types.ts @@ -0,0 +1,30 @@ +import { TemporaryPermissionMode } from "@app/db/schemas"; + +export type TOrgIdentityMembership = { + id: string; + orgId: string; + identityId: string; + createdAt: string; + updatedAt: string; +}; + +export type TCreateOrgIdentityMembershipDTO = { + identityId: string; + roles: Array< + | { + role: string; + isTemporary?: false; + } + | { + role: string; + isTemporary: true; + temporaryMode: TemporaryPermissionMode; + temporaryRange: string; + temporaryAccessStartTime: string; + } + >; +}; + +export type TDeleteOrgIdentityMembershipDTO = { + identityId: string; +}; diff --git a/frontend/src/hooks/api/organization/index.ts b/frontend/src/hooks/api/organization/index.ts index f4627a614..5f2be9b76 100644 --- a/frontend/src/hooks/api/organization/index.ts +++ b/frontend/src/hooks/api/organization/index.ts @@ -1,5 +1,6 @@ export { useAddOrgPmtMethod, + useGetAvailableOrgIdentities, useAddOrgTaxId, useCreateCustomerPortalSession, useCreateOrg, diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index ce3071584..d76b8a54b 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -42,7 +42,9 @@ export const organizationKeys = { [...organizationKeys.getOrgIdentityMemberships(orgId), params] as const, getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const, getOrgIntegrationAuths: (orgId: string) => [{ orgId }, "integration-auths"] as const, - getOrgById: (orgId: string) => ["organization", { orgId }] + getOrgById: (orgId: string) => ["organization", { orgId }], + getAvailableIdentities: () => ["available-identities"], + getAvailableUsers: () => ["available-users"] }; export const fetchOrganizations = async () => { @@ -574,3 +576,29 @@ export const useGetOrgIntegrationAuths = ( select }); }; + +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(), + queryFn: async () => { + const { data } = await apiRequest.get<{ + users: { username: string; id: string; firstName: string; lastName: string }[]; + }>(`/api/v1/organization/users/available`); + + return data.users; + }, + enabled + }); diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLinkForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLinkForm.tsx new file mode 100644 index 000000000..96ad3eba4 --- /dev/null +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLinkForm.tsx @@ -0,0 +1,129 @@ +import { Controller, useForm } from "react-hook-form"; +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 } from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { useGetAvailableOrgIdentities, useGetOrgRoles } from "@app/hooks/api"; +import { useCreateOrgIdentityMembership } from "@app/hooks/api/orgIdentityMembership"; + +const schema = z + .object({ + identity: z.object({ name: z.string(), id: z.string() }), + role: z.object({ name: z.string(), slug: z.string() }) + }) + .required(); + +export type FormData = z.infer; + +type Props = { + onClose: () => void; +}; + +export const IdentityLinkForm = ({ onClose }: Props) => { + const navigate = useNavigate(); + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + + const { data: roles } = useGetOrgRoles(orgId); + + const { mutateAsync: createMutateAsync } = useCreateOrgIdentityMembership(); + const { data: rootOrgIdentities, isPending: isRootOrgLoading } = useGetAvailableOrgIdentities(); + + const { + control, + handleSubmit, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: {} + }); + + const onFormSubmit = async ({ identity, role }: FormData) => { + try { + await createMutateAsync({ + identityId: identity.id, + roles: [{ role: role.slug, isTemporary: false }] + }); + createNotification({ + text: "Successfully linked identity", + type: "success" + }); + navigate({ + to: "/organization/identities/$identityId", + params: { + identityId: identity.id + } + }); + } catch (err) { + console.error(err); + const error = err as any; + const text = error?.response?.data?.message ?? "Failed to link identity"; + + createNotification({ + text, + type: "error" + }); + } + }; + + return ( +
+ ( + + option.id} + getOptionLabel={(option) => option.name} + isLoading={isRootOrgLoading} + /> + + )} + /> + ( + + option.slug} + getOptionLabel={(option) => option.name} + menuPortalTarget={document.body} + /> + + )} + /> +
+ + +
+ + ); +}; 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 9280ab5d9..6627f87ce 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,15 @@ -import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { + faArrowUpRightFromSquare, + faBookOpen, + 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 } from "@app/components/v2"; +import { Button, DeleteActionModal, Modal, ModalContent } from "@app/components/v2"; import { OrgPermissionIdentityActions, OrgPermissionSubjects, @@ -23,11 +28,12 @@ import { IdentityModal } from "./IdentityModal"; import { IdentityTable } from "./IdentityTable"; import { IdentityTokenAuthTokenModal } from "./IdentityTokenAuthTokenModal"; import { MachineAuthTemplateUsagesModal } from "./MachineAuthTemplateUsagesModal"; +import { IdentityLinkForm } from "./IdentityLinkForm"; export const IdentitySection = withPermission( () => { const { subscription } = useSubscription(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const orgId = currentOrg?.id || ""; const { mutateAsync: deleteMutateAsync } = useDeleteIdentity(); @@ -43,7 +49,8 @@ export const IdentitySection = withPermission( "createTemplate", "editTemplate", "deleteTemplate", - "viewUsages" + "viewUsages", + "linkIdentity" ] as const); const isMoreIdentitiesAllowed = subscription?.identityLimit @@ -105,8 +112,8 @@ export const IdentitySection = withPermission( return (
-
-
+
+

Identities

+ {isSubOrganization && ( + + {(isAllowed) => ( + + )} + + )} - {/* */} - {/* */} + handlePopUpToggle("linkIdentity", isOpen)} + > + + handlePopUpClose("linkIdentity")} /> + + { + beforeLoad: ({ search }) => { + if (search.subOrganization) { + throw redirect({ + to: "/organization/projects", + search + }); + } + return { breadcrumbs: [ { diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx index 66af7df1b..f5b0f5a0c 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx @@ -14,25 +14,39 @@ import { OrgSecurityTab } from "../OrgSecurityTab"; import { OrgSsoTab } from "../OrgSsoTab"; import { OrgWorkflowIntegrationTab } from "../OrgWorkflowIntegrationTab"; import { ProjectTemplatesTab } from "../ProjectTemplatesTab"; +import { useOrganization } from "@app/context"; export const OrgTabGroup = () => { const search = useSearch({ from: ROUTE_PATHS.Organization.SettingsPage.id }); + const { isSubOrganization } = useOrganization(); + const tabs = [ { name: "General", key: "tab-org-general", component: OrgGeneralTab }, { name: "SSO", key: "sso-settings", - component: OrgSsoTab + component: OrgSsoTab, + isHidden: isSubOrganization }, { name: "Provisioning", key: "provisioning-settings", - component: OrgProvisioningTab + component: OrgProvisioningTab, + isHidden: isSubOrganization + }, + { + name: "Security", + key: "tab-org-security", + component: OrgSecurityTab, + isHidden: isSubOrganization + }, + { + name: "Encryption", + key: "tab-org-encryption", + component: OrgEncryptionTab }, - { name: "Security", key: "tab-org-security", component: OrgSecurityTab }, - { name: "Encryption", key: "tab-org-encryption", component: OrgEncryptionTab }, { name: "Workflow Integrations", key: "workflow-integrations", @@ -57,17 +71,21 @@ export const OrgTabGroup = () => { return ( - {tabs.map((tab) => ( - - {tab.name} - - ))} + {tabs + .filter((el) => !el.isHidden) + .map((tab) => ( + + {tab.name} + + ))} - {tabs.map(({ key, component: Component }) => ( - - - - ))} + {tabs + .filter((el) => !el.isHidden) + .map(({ key, component: Component }) => ( + + + + ))} ); }; From 03e49183629434f95471ab9fc06c0fd170b1d660 Mon Sep 17 00:00:00 2001 From: = Date: Sun, 19 Oct 2025 22:42:40 +0530 Subject: [PATCH 06/44] feat: completed conditional rendering of identity for sub org --- backend/src/server/routes/index.ts | 1 + .../src/server/routes/v1/identity-router.ts | 2 +- .../identity-alicloud-auth-service.ts | 20 +++++- .../identity-aws-auth-service.ts | 20 +++++- .../identity-azure-auth-service.ts | 20 +++++- .../identity-gcp-auth-service.ts | 20 +++++- .../identity-jwt-auth-service.ts | 12 ++++ .../identity-kubernetes-auth-service.ts | 20 +++++- .../identity-ldap-auth-service.ts | 13 ++++ .../identity-oci-auth-service.ts | 20 +++++- .../identity-oidc-auth-service.ts | 12 ++++ .../identity-tls-cert-auth-service.ts | 20 +++++- .../identity-token-auth-service.ts | 14 +++- .../identity-ua/identity-ua-service.ts | 34 ++++++++++ .../src/services/identity/identity-org-dal.ts | 6 +- .../src/services/identity/identity-service.ts | 34 +++++++++- frontend/src/hooks/api/identities/types.ts | 1 + .../IdentitySection/IdentityModal.tsx | 68 +++++++++++-------- .../IdentityDetailsByIDPage.tsx | 11 +-- .../components/IdentityDetailsSection.tsx | 46 +++++++------ 20 files changed, 329 insertions(+), 65 deletions(-) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index f23fcb7f4..fa5cd7a11 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1583,6 +1583,7 @@ export const registerRoutes = async ( }); const identityService = identityServiceFactory({ + additionalPrivilegeDAL, permissionService, identityDAL, identityOrgMembershipDAL, diff --git a/backend/src/server/routes/v1/identity-router.ts b/backend/src/server/routes/v1/identity-router.ts index d6a42c4a2..b1798692d 100644 --- a/backend/src/server/routes/v1/identity-router.ts +++ b/backend/src/server/routes/v1/identity-router.ts @@ -249,7 +249,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { permissions: true, description: true }).optional(), - identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({ + identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true, orgId: true }).extend({ authMethods: z.array(z.string()), activeLockoutAuthMethods: z.array(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 274703c60..3f3c6cdf6 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 @@ -13,7 +13,13 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; import { crypto } from "@app/lib/crypto"; -import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; @@ -162,6 +168,9 @@ export const identityAliCloudAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) { throw new BadRequestError({ @@ -239,6 +248,9 @@ export const identityAliCloudAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) { throw new NotFoundError({ @@ -306,6 +318,9 @@ export const identityAliCloudAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) { throw new BadRequestError({ @@ -342,6 +357,9 @@ export const identityAliCloudAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) { throw new BadRequestError({ message: "The identity does not have Alibaba Cloud auth" 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 74912635e..59f1b3d5b 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 @@ -13,7 +13,13 @@ import { import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; -import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; @@ -240,6 +246,9 @@ export const identityAwsAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) { throw new BadRequestError({ @@ -321,6 +330,9 @@ export const identityAwsAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) { throw new NotFoundError({ @@ -389,6 +401,9 @@ export const identityAwsAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) { throw new BadRequestError({ @@ -425,6 +440,9 @@ export const identityAwsAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) { throw new BadRequestError({ message: "The identity does not have aws auth" 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 98e7b14a4..c85fabc8f 100644 --- a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts +++ b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts @@ -10,7 +10,13 @@ import { import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; -import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; @@ -153,6 +159,9 @@ export const identityAzureAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) { throw new BadRequestError({ @@ -233,6 +242,9 @@ export const identityAzureAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) { throw new BadRequestError({ message: "Failed to update Azure Auth" @@ -303,6 +315,9 @@ export const identityAzureAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) { throw new BadRequestError({ message: "The identity does not have Azure Auth attached" @@ -339,6 +354,9 @@ export const identityAzureAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) { throw new BadRequestError({ message: "The identity does not have azure auth" 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 05a2c94f8..83d407fa7 100644 --- a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts @@ -10,7 +10,13 @@ import { import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; -import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; @@ -193,6 +199,9 @@ export const identityGcpAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) { throw new BadRequestError({ @@ -275,6 +284,9 @@ export const identityGcpAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) { throw new BadRequestError({ @@ -347,6 +359,9 @@ export const identityGcpAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) { throw new BadRequestError({ @@ -384,6 +399,9 @@ export const identityGcpAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) { throw new BadRequestError({ 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 b50655286..87b5e8ea7 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 @@ -284,6 +284,9 @@ export const identityJwtAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) { throw new BadRequestError({ message: "Failed to add JWT Auth to already configured identity" @@ -388,6 +391,9 @@ export const identityJwtAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) { throw new BadRequestError({ @@ -493,6 +499,9 @@ export const identityJwtAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) { throw new BadRequestError({ @@ -542,6 +551,9 @@ export const identityJwtAuthServiceFactory = ({ if (!identityMembershipOrg) { throw new NotFoundError({ message: "Failed to find identity" }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) { throw new BadRequestError({ 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 d2eefcda1..bdb6ecd67 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 @@ -26,7 +26,13 @@ import { import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; -import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; @@ -513,6 +519,9 @@ export const identityKubernetesAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) { throw new BadRequestError({ @@ -640,6 +649,9 @@ export const identityKubernetesAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) { throw new BadRequestError({ @@ -788,6 +800,9 @@ export const identityKubernetesAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId }); if (!identityKubernetesAuth) { @@ -851,6 +866,9 @@ export const identityKubernetesAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) { throw new BadRequestError({ 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 f925ff245..ad76a60a1 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 @@ -21,6 +21,7 @@ import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; import { BadRequestError, + ForbiddenRequestError, NotFoundError, PermissionBoundaryError, RateLimitError, @@ -254,6 +255,9 @@ export const identityLdapAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) { throw new BadRequestError({ @@ -426,6 +430,9 @@ export const identityLdapAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) { throw new NotFoundError({ @@ -590,6 +597,9 @@ export const identityLdapAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) { throw new BadRequestError({ @@ -638,6 +648,9 @@ export const identityLdapAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) { throw new BadRequestError({ message: "The identity does not have LDAP Auth attached" 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 b8991477d..2c5d59e2b 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 @@ -14,7 +14,13 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; import { crypto } from "@app/lib/crypto"; -import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; @@ -168,6 +174,9 @@ export const identityOciAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) { throw new BadRequestError({ @@ -247,6 +256,9 @@ export const identityOciAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) { throw new NotFoundError({ @@ -314,6 +326,9 @@ export const identityOciAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) { throw new BadRequestError({ @@ -350,6 +365,9 @@ export const identityOciAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) { throw new BadRequestError({ message: "The identity does not have OCI auth" 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 9372c940c..f3d17eb71 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 @@ -259,6 +259,9 @@ export const identityOidcAuthServiceFactory = ({ if (!identityMembershipOrg) { throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) { throw new BadRequestError({ message: "Failed to add OIDC Auth to already configured identity" @@ -352,6 +355,9 @@ export const identityOidcAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) { throw new BadRequestError({ @@ -442,6 +448,9 @@ export const identityOidcAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) { throw new BadRequestError({ @@ -484,6 +493,9 @@ export const identityOidcAuthServiceFactory = ({ if (!identityMembershipOrg) { throw new NotFoundError({ message: "Failed to find identity" }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) { throw new BadRequestError({ 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 9d59ababf..d547f7449 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 @@ -11,7 +11,13 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; -import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; @@ -189,6 +195,9 @@ export const identityTlsCertAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) { throw new BadRequestError({ @@ -272,6 +281,9 @@ export const identityTlsCertAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) { throw new NotFoundError({ @@ -352,6 +364,9 @@ export const identityTlsCertAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) { throw new BadRequestError({ @@ -397,6 +412,9 @@ export const identityTlsCertAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) { throw new BadRequestError({ message: "The identity does not have TLS Certificate auth" 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 87ae18fd5..e3c7a486e 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-service.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-service.ts @@ -10,7 +10,7 @@ import { import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; -import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; @@ -79,6 +79,9 @@ export const identityTokenAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { throw new BadRequestError({ @@ -156,6 +159,9 @@ export const identityTokenAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { throw new BadRequestError({ @@ -225,6 +231,9 @@ export const identityTokenAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { throw new BadRequestError({ @@ -265,6 +274,9 @@ export const identityTokenAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { throw new BadRequestError({ diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index f63c8a6e5..0de2503ff 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -13,6 +13,7 @@ import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, + ForbiddenRequestError, NotFoundError, PermissionBoundaryError, RateLimitError, @@ -315,6 +316,13 @@ export const identityUaServiceFactory = ({ message: "Failed to add universal auth to already configured identity" }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } + + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); @@ -425,6 +433,10 @@ export const identityUaServiceFactory = ({ }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } + if ( (accessTokenMaxTTL || uaIdentityAuth.accessTokenMaxTTL) > 0 && (accessTokenTTL || uaIdentityAuth.accessTokenMaxTTL) > (accessTokenMaxTTL || uaIdentityAuth.accessTokenMaxTTL) @@ -515,6 +527,9 @@ export const identityUaServiceFactory = ({ message: "The identity does not have universal auth" }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } const { permission } = await permissionService.getOrgPermission({ scope: OrganizationActionScope.Any, @@ -549,6 +564,9 @@ export const identityUaServiceFactory = ({ message: "The identity does not have universal auth" }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } const { permission } = await permissionService.getOrgPermission({ scope: OrganizationActionScope.Any, actor, @@ -617,6 +635,9 @@ export const identityUaServiceFactory = ({ message: "The identity does not have universal auth" }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } const { permission } = await permissionService.getOrgPermission({ scope: OrganizationActionScope.Any, @@ -700,6 +721,10 @@ export const identityUaServiceFactory = ({ message: "The identity does not have universal auth" }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } + const { permission } = await permissionService.getOrgPermission({ scope: OrganizationActionScope.Any, actor, @@ -770,6 +795,9 @@ export const identityUaServiceFactory = ({ message: "The identity does not have universal auth" }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } const identityUa = await identityUaDAL.findOne({ identityId }); if (!identityUa) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); @@ -839,6 +867,9 @@ export const identityUaServiceFactory = ({ message: "The identity does not have universal auth" }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } const identityUa = await identityUaDAL.findOne({ identityId }); if (!identityUa) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); @@ -913,6 +944,9 @@ export const identityUaServiceFactory = ({ message: "The identity does not have universal auth" }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } const { permission } = await permissionService.getOrgPermission({ scope: OrganizationActionScope.Any, diff --git a/backend/src/services/identity/identity-org-dal.ts b/backend/src/services/identity/identity-org-dal.ts index 65aee561c..3d1004608 100644 --- a/backend/src/services/identity/identity-org-dal.ts +++ b/backend/src/services/identity/identity-org-dal.ts @@ -163,7 +163,8 @@ export const identityOrgDALFactory = (db: TDbClient) => { .select( selectAllTableCols(TableName.Membership), db.ref("name").withSchema(TableName.Identity).as("identityName"), - db.ref("hasDeleteProtection").withSchema(TableName.Identity) + db.ref("hasDeleteProtection").withSchema(TableName.Identity), + db.ref("orgId").withSchema(TableName.Identity) ) .where(filter) .as("paginatedIdentity"); @@ -257,6 +258,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("customRoleId").withSchema(TableName.MembershipRole).as("roleId"), db.ref("scopeOrgId").withSchema("paginatedIdentity").as("orgId"), db.ref("lastLoginAuthMethod").withSchema("paginatedIdentity"), + db.ref("orgId").withSchema("paginatedIdentity").as("identityOrgId"), db.ref("lastLoginTime").withSchema("paginatedIdentity"), db.ref("createdAt").withSchema("paginatedIdentity"), db.ref("updatedAt").withSchema("paginatedIdentity"), @@ -309,6 +311,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { roleId, id, orgId, + identityOrgId, uaId, alicloudId, awsId, @@ -348,6 +351,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { id: identityId as string, name: identityName, hasDeleteProtection, + orgId: identityOrgId, authMethods: buildAuthMethods({ uaId, alicloudId, diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index f35c80032..2183ec826 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -28,6 +28,7 @@ import { TSearchOrgIdentitiesByOrgIdDTO, TUpdateIdentityDTO } from "./identity-types"; +import { TAdditionalPrivilegeDALFactory } from "../additional-privilege/additional-privilege-dal"; type TIdentityServiceFactoryDep = { identityDAL: TIdentityDALFactory; @@ -40,6 +41,7 @@ type TIdentityServiceFactoryDep = { licenseService: Pick; keyStore: Pick; orgDAL: Pick; + additionalPrivilegeDAL: Pick; }; export type TIdentityServiceFactory = ReturnType; @@ -54,7 +56,8 @@ export const identityServiceFactory = ({ keyStore, orgDAL, membershipIdentityDAL, - membershipRoleDAL + membershipRoleDAL, + additionalPrivilegeDAL }: TIdentityServiceFactoryDep) => { const createIdentity = async ({ name, @@ -337,10 +340,35 @@ export const identityServiceFactory = ({ if (identityOrgMembership.identity.hasDeleteProtection) throw new BadRequestError({ message: "Identity has delete protection" }); - const deletedIdentity = await identityDAL.deleteById(id); + if (identityOrgMembership.identity.identityOrgId === actorOrgId) { + const deletedIdentity = await identityDAL.deleteById(id); + await licenseService.updateSubscriptionOrgMemberCount(identityOrgMembership.scopeOrgId); + return { ...deletedIdentity, orgId: identityOrgMembership.scopeOrgId }; + } - await licenseService.updateSubscriptionOrgMemberCount(identityOrgMembership.scopeOrgId); + await membershipIdentityDAL.transaction(async (tx) => { + const identityProjectMembership = await membershipIdentityDAL.find( + { + actorIdentityId: id, + scope: AccessScope.Project, + scopeOrgId: actorOrgId + }, + { tx } + ); + await additionalPrivilegeDAL.delete( + { + actorIdentityId: id, + $in: { + projectId: identityProjectMembership.map((el) => el.scopeProjectId) + } + }, + tx + ); + const doc = await membershipIdentityDAL.delete({ actorIdentityId: id, scopeOrgId: actorOrgId }, tx); + return doc; + }); + const deletedIdentity = await identityDAL.findById(id); return { ...deletedIdentity, orgId: identityOrgMembership.scopeOrgId }; }; diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index 99e377143..a0eb828e8 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -20,6 +20,7 @@ export type Identity = { createdAt: string; updatedAt: string; isInstanceAdmin?: boolean; + orgId: string; }; export type IdentityAccessToken = { diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx index 224af9d5e..6b4d5c232 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx @@ -53,6 +53,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { const orgId = currentOrg?.id || ""; const { data: roles } = useGetOrgRoles(orgId); + const isOrgIdentity = orgId === popUp?.identity?.data?.orgId; const { mutateAsync: createMutateAsync } = useCreateIdentity(); const { mutateAsync: updateMutateAsync } = useUpdateIdentity(); @@ -113,6 +114,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { name: string; role: string; hasDeleteProtection: boolean; + orgId: string; }; if (identity) { @@ -196,16 +198,23 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { title={`${popUp?.identity?.data ? "Update" : "Create"} Identity`} >
- ( - - - - )} - /> + {isOrgIdentity && ( + ( + + + + )} + /> + )} { label={`${popUp?.identity?.data ? "Update" : ""} Role`} errorText={error?.message} isError={Boolean(error)} - className="mt-4" > { )} /> - ( - - -

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

-
-
- )} - /> + {isOrgIdentity && ( + ( + + +

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

+
+
+ )} + /> + )}
diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx index 985c8d27d..0a853070b 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx @@ -33,6 +33,7 @@ const Page = () => { const orgId = currentOrg?.id || ""; const { data } = useGetIdentityById(identityId); const { mutateAsync: deleteIdentity } = useDeleteIdentity(); + const isAuthHidden = orgId !== data?.identity?.orgId; const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "identity", @@ -91,10 +92,12 @@ const Page = () => {
- + {!isAuthHidden && ( + + )}
diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx index 8e0c1228c..c27fcb96c 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx @@ -28,13 +28,14 @@ import { UsePopUpState } from "@app/hooks/usePopUp"; type Props = { identityId: string; + isOrgIdentity?: boolean; handlePopUpOpen: ( popUpName: keyof UsePopUpState<["identity", "identityAuthMethod", "deleteIdentity"]>, data?: object ) => void; }; -export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) => { +export const IdentityDetailsSection = ({ identityId, handlePopUpOpen, isOrgIdentity }: Props) => { const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset({ initialState: "Copy ID to clipboard" }); @@ -75,6 +76,7 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) = handlePopUpOpen("identity", { identityId, name: data.identity.name, + orgId: data.identity.orgId, hasDeleteProtection: data.identity.hasDeleteProtection, role: data.role, customRole: data.customRole, @@ -140,24 +142,30 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =

Name

{data.identity.name}

-
-

Last Login Auth Method

-

- {data.lastLoginAuthMethod ? identityAuthToNameMap[data.lastLoginAuthMethod] : "-"} -

-
-
-

Last Login Time

-

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

-
-
-

Delete Protection

-

- {data.identity.hasDeleteProtection ? "On" : "Off"} -

-
+ {isOrgIdentity && ( +
+

Last Login Auth Method

+

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

+
+ )} + {isOrgIdentity && ( +
+

Last Login Time

+

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

+
+ )} + {isOrgIdentity && ( +
+

Delete Protection

+

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

+
+ )}

Organization Role

{data.role}

From 8afc391e97c495e60eaeb44049559f58fae3663b Mon Sep 17 00:00:00 2001 From: = Date: Mon, 20 Oct 2025 00:40:27 +0530 Subject: [PATCH 07/44] feat: billing fixes --- backend/src/ee/routes/v1/index.ts | 2 +- .../src/ee/services/license/license-dal.ts | 33 ++++++++++-- .../ee/services/license/license-service.ts | 50 +++++++++---------- backend/src/server/routes/index.ts | 1 - backend/src/server/routes/v1/index.ts | 2 +- .../src/services/identity/identity-org-dal.ts | 2 +- .../src/services/identity/identity-service.ts | 9 +++- .../membership-identity-service.ts | 2 +- .../org/org-membership-identity-factory.ts | 2 +- .../org/org-membership-user-factory.ts | 2 +- backend/src/services/org/org-dal.ts | 22 +++++++- .../service-token/service-token-service.ts | 2 +- .../hooks/api/orgIdentityMembership/index.tsx | 6 ++- frontend/src/hooks/api/organization/index.ts | 2 +- .../src/hooks/api/organization/queries.tsx | 4 +- .../components/NavBar/Navbar.tsx | 2 +- .../NavBar/NewSubOrganizationForm.tsx | 2 +- .../IdentitySection/IdentityLinkForm.tsx | 2 +- .../IdentitySection/IdentityModal.tsx | 2 +- .../IdentitySection/IdentitySection.tsx | 2 +- .../components/OrgTabGroup/OrgTabGroup.tsx | 2 +- frontend/src/pages/organization/layout.tsx | 2 +- 22 files changed, 103 insertions(+), 52 deletions(-) diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 8d4671e50..31847b503 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -48,9 +48,9 @@ import { registerSshCertRouter } from "./ssh-certificate-router"; import { registerSshCertificateTemplateRouter } from "./ssh-certificate-template-router"; import { registerSshHostGroupRouter } from "./ssh-host-group-router"; import { registerSshHostRouter } from "./ssh-host-router"; +import { registerSubOrgRouter } from "./sub-org-router"; import { registerTrustedIpRouter } from "./trusted-ip-router"; import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router"; -import { registerSubOrgRouter } from "./sub-org-router"; export const registerV1EERoutes = async (server: FastifyZodProvider) => { // org role starts with organization diff --git a/backend/src/ee/services/license/license-dal.ts b/backend/src/ee/services/license/license-dal.ts index a2bd7ec51..853f3a994 100644 --- a/backend/src/ee/services/license/license-dal.ts +++ b/backend/src/ee/services/license/license-dal.ts @@ -10,6 +10,7 @@ export const licenseDALFactory = (db: TDbClient) => { const countOfOrgMembers = async (orgId: string | null, tx?: Knex) => { try { const doc = await (tx || db.replicaNode())(TableName.Membership) + .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`) .where({ status: OrgMembershipStatus.Accepted, scope: AccessScope.Organization }) .andWhere((bd) => { if (orgId) { @@ -18,6 +19,7 @@ export const licenseDALFactory = (db: TDbClient) => { }) .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .where(`${TableName.Users}.isGhost`, false) + .whereNull(`${TableName.Organization}.rootOrgId`) .count(); return Number(doc?.[0]?.count ?? 0); } catch (error) { @@ -25,10 +27,31 @@ export const licenseDALFactory = (db: TDbClient) => { } }; + const countOfOrgIdentities = async (orgId: string | null, tx?: Knex) => { + try { + // count org identities + const identityDoc = await (tx || db.replicaNode())(TableName.Identity) + .join(TableName.Organization, `${TableName.Identity}.orgId`, `${TableName.Organization}.id`) + .where((bd) => { + if (orgId) { + void bd.where(`${TableName.Organization}.rootOrgId`, orgId).orWhere(`${TableName.Organization}.id`, orgId); + } + }) + .count(); + + const identityCount = Number(identityDoc?.[0].count); + + return identityCount; + } catch (error) { + throw new DatabaseError({ error, name: "Count of Org Users + Identities" }); + } + }; + const countOrgUsersAndIdentities = async (orgId: string | null, tx?: Knex) => { try { // count org users const userDoc = await (tx || db.replicaNode())(TableName.Membership) + .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`) .where({ status: OrgMembershipStatus.Accepted, scope: AccessScope.Organization }) .whereNotNull(`${TableName.Membership}.actorUserId`) .andWhere((bd) => { @@ -38,17 +61,17 @@ export const licenseDALFactory = (db: TDbClient) => { }) .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .where(`${TableName.Users}.isGhost`, false) + .whereNull(`${TableName.Organization}.rootOrgId`) .count(); const userCount = Number(userDoc?.[0].count); // count org identities - const identityDoc = await (tx || db.replicaNode())(TableName.Membership) - .where({ scope: AccessScope.Organization }) - .whereNotNull(`${TableName.Membership}.actorIdentityId`) + const identityDoc = await (tx || db.replicaNode())(TableName.Identity) + .join(TableName.Organization, `${TableName.Identity}.orgId`, `${TableName.Organization}.id`) .where((bd) => { if (orgId) { - void bd.where(`${TableName.Membership}.scopeOrgId`, orgId); + void bd.where(`${TableName.Organization}.rootOrgId`, orgId).orWhere(`${TableName.Organization}.id`, orgId); } }) .count(); @@ -61,5 +84,5 @@ export const licenseDALFactory = (db: TDbClient) => { } }; - return { countOfOrgMembers, countOrgUsersAndIdentities }; + return { countOfOrgMembers, countOrgUsersAndIdentities, countOfOrgIdentities }; }; diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index 8fc4987f4..835c80dd5 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -15,7 +15,6 @@ import { getConfig } from "@app/lib/config/env"; import { verifyOfflineLicense } from "@app/lib/crypto"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; -import { TIdentityOrgDALFactory } from "@app/services/identity/identity-org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; @@ -46,11 +45,10 @@ import { } from "./license-types"; type TLicenseServiceFactoryDep = { - orgDAL: Pick; + orgDAL: Pick; permissionService: Pick; licenseDAL: TLicenseDALFactory; keyStore: Pick; - identityOrgMembershipDAL: TIdentityOrgDALFactory; projectDAL: TProjectDALFactory; }; @@ -67,7 +65,6 @@ export const licenseServiceFactory = ({ permissionService, licenseDAL, keyStore, - identityOrgMembershipDAL, projectDAL }: TLicenseServiceFactoryDep) => { let isValidLicense = false; @@ -200,19 +197,21 @@ export const licenseServiceFactory = ({ return JSON.parse(cachedPlan) as TFeatureSet; } - const org = await orgDAL.findOrgById(orgId); + const org = await orgDAL.findRootOrgDetails(orgId); if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); + const rootOrgId = org.id; + const { data: { currentPlan } } = await licenseServerCloudApi.request.get<{ currentPlan: TFeatureSet }>( `/api/license-server/v1/customers/${org.customerId}/cloud-plan` ); - const workspacesUsed = await projectDAL.countOfOrgProjects(orgId); + const workspacesUsed = await projectDAL.countOfOrgProjects(rootOrgId); currentPlan.workspacesUsed = workspacesUsed; - const membersUsed = await licenseDAL.countOfOrgMembers(orgId); + const membersUsed = await licenseDAL.countOfOrgMembers(rootOrgId); currentPlan.membersUsed = membersUsed; - const identityUsed = await licenseDAL.countOrgUsersAndIdentities(orgId); + const identityUsed = await licenseDAL.countOrgUsersAndIdentities(rootOrgId); currentPlan.identitiesUsed = identityUsed; if (currentPlan.identityLimit && currentPlan.identityLimit !== identityUsed) { @@ -285,10 +284,10 @@ export const licenseServiceFactory = ({ }; const updateSubscriptionOrgMemberCount = async (orgId: string, tx?: Knex) => { - const org = await orgDAL.findOrgById(orgId); + const org = await orgDAL.findRootOrgDetails(orgId); if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); - const rootOrgId = org.rootOrgId || org.id; + const rootOrgId = org.id; if (instanceType === InstanceType.Cloud) { const quantity = await licenseDAL.countOfOrgMembers(rootOrgId, tx); const quantityIdentities = await licenseDAL.countOrgUsersAndIdentities(rootOrgId, tx); @@ -381,7 +380,7 @@ export const licenseServiceFactory = ({ OrgPermissionSubjects.Billing ); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -420,7 +419,7 @@ export const licenseServiceFactory = ({ OrgPermissionSubjects.Billing ); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: "Organization not found" @@ -473,7 +472,7 @@ export const licenseServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -539,7 +538,7 @@ export const licenseServiceFactory = ({ const getUsageMetrics = async (orgId: string) => { const [orgMembersUsed, identityUsed, projectCount] = await Promise.all([ orgDAL.countAllOrgMembers(orgId), - identityOrgMembershipDAL.countAllOrgIdentities({ scopeOrgId: orgId }), + licenseDAL.countOfOrgIdentities(orgId), projectDAL.countOfOrgProjects(orgId) ]); @@ -563,7 +562,7 @@ export const licenseServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -607,7 +606,7 @@ export const licenseServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -642,7 +641,7 @@ export const licenseServiceFactory = ({ OrgPermissionSubjects.Billing ); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -669,7 +668,7 @@ export const licenseServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -706,7 +705,7 @@ export const licenseServiceFactory = ({ OrgPermissionSubjects.Billing ); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -745,7 +744,7 @@ export const licenseServiceFactory = ({ OrgPermissionSubjects.Billing ); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -781,7 +780,7 @@ export const licenseServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -809,7 +808,7 @@ export const licenseServiceFactory = ({ OrgPermissionSubjects.Billing ); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -840,7 +839,7 @@ export const licenseServiceFactory = ({ OrgPermissionSubjects.Billing ); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -864,7 +863,7 @@ export const licenseServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -888,7 +887,7 @@ export const licenseServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -933,7 +932,6 @@ export const licenseServiceFactory = ({ getLicenseId, invalidateGetPlan, updateSubscriptionOrgMemberCount, - refreshPlan, getOrgPlan, getOrgPlansTableByBillCycle, startOrgTrial, diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index fa5cd7a11..ddc757e6f 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -561,7 +561,6 @@ export const registerRoutes = async ( orgDAL, licenseDAL, keyStore, - identityOrgMembershipDAL, projectDAL }); diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 307b922d3..710bf4240 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -32,6 +32,7 @@ import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-rou import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router"; import { registerIdentityOciAuthRouter } from "./identity-oci-auth-router"; import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router"; +import { registerOrgIdentityMembershipRouter } from "./identity-org-membership-router"; import { registerIdentityProjectRouter } from "./identity-project-router"; import { registerIdentityRouter } from "./identity-router"; import { registerIdentityTlsCertAuthRouter } from "./identity-tls-cert-auth-router"; @@ -65,7 +66,6 @@ import { registerUserEngagementRouter } from "./user-engagement-router"; import { registerUserRouter } from "./user-router"; import { registerWebhookRouter } from "./webhook-router"; import { registerWorkflowIntegrationRouter } from "./workflow-integration-router"; -import { registerOrgIdentityMembershipRouter } from "./identity-org-membership-router"; export const registerV1Routes = async (server: FastifyZodProvider) => { await server.register(registerSsoRouter, { prefix: "/sso" }); diff --git a/backend/src/services/identity/identity-org-dal.ts b/backend/src/services/identity/identity-org-dal.ts index 3d1004608..04c384843 100644 --- a/backend/src/services/identity/identity-org-dal.ts +++ b/backend/src/services/identity/identity-org-dal.ts @@ -654,7 +654,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { tx?: Knex ) => { try { - const query = (tx || db.replicaNode())(TableName.Membership) + const query = (tx || db.replicaNode())(TableName.Identity) .where(`${TableName.Membership}.scope`, AccessScope.Organization) .whereNotNull(`${TableName.Membership}.actorIdentityId`) .where(filter) diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index 2183ec826..f2caeb053 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -12,6 +12,7 @@ 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 { TAdditionalPrivilegeDALFactory } from "../additional-privilege/additional-privilege-dal"; import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; import { TOrgDALFactory } from "../org/org-dal"; @@ -28,7 +29,6 @@ import { TSearchOrgIdentitiesByOrgIdDTO, TUpdateIdentityDTO } from "./identity-types"; -import { TAdditionalPrivilegeDALFactory } from "../additional-privilege/additional-privilege-dal"; type TIdentityServiceFactoryDep = { identityDAL: TIdentityDALFactory; @@ -347,6 +347,13 @@ export const identityServiceFactory = ({ } await membershipIdentityDAL.transaction(async (tx) => { + await identityMetadataDAL.delete( + { + identityId: id, + orgId: actorOrgId + }, + tx + ); const identityProjectMembership = await membershipIdentityDAL.find( { actorIdentityId: id, diff --git a/backend/src/services/membership-identity/membership-identity-service.ts b/backend/src/services/membership-identity/membership-identity-service.ts index c1bb9cbbc..16292ea82 100644 --- a/backend/src/services/membership-identity/membership-identity-service.ts +++ b/backend/src/services/membership-identity/membership-identity-service.ts @@ -6,6 +6,7 @@ import { ms } from "@app/lib/ms"; import { SearchResourceOperators } from "@app/lib/search-resource/search"; import { TAdditionalPrivilegeDALFactory } from "../additional-privilege/additional-privilege-dal"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; import { TOrgDALFactory } from "../org/org-dal"; import { TRoleDALFactory } from "../role/role-dal"; @@ -20,7 +21,6 @@ import { import { newNamespaceMembershipIdentityFactory } from "./namespace/namespace-membership-identity-factory"; import { newOrgMembershipIdentityFactory } from "./org/org-membership-identity-factory"; import { newProjectMembershipIdentityFactory } from "./project/project-membership-identity-factory"; -import { TIdentityDALFactory } from "../identity/identity-dal"; type TMembershipIdentityServiceFactoryDep = { membershipIdentityDAL: TMembershipIdentityDALFactory; diff --git a/backend/src/services/membership-identity/org/org-membership-identity-factory.ts b/backend/src/services/membership-identity/org/org-membership-identity-factory.ts index caffc984e..1ad77dfbd 100644 --- a/backend/src/services/membership-identity/org/org-membership-identity-factory.ts +++ b/backend/src/services/membership-identity/org/org-membership-identity-factory.ts @@ -8,11 +8,11 @@ import { } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { BadRequestError, InternalServerError, PermissionBoundaryError } from "@app/lib/errors"; +import { TIdentityDALFactory } from "@app/services/identity/identity-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { isCustomOrgRole } from "@app/services/org/org-role-fns"; import { TMembershipIdentityScopeFactory } from "../membership-identity-types"; -import { TIdentityDALFactory } from "@app/services/identity/identity-dal"; type TOrgMembershipIdentityScopeFactoryDep = { permissionService: Pick; diff --git a/backend/src/services/membership-user/org/org-membership-user-factory.ts b/backend/src/services/membership-user/org/org-membership-user-factory.ts index 761a1397d..ca867286a 100644 --- a/backend/src/services/membership-user/org/org-membership-user-factory.ts +++ b/backend/src/services/membership-user/org/org-membership-user-factory.ts @@ -15,8 +15,8 @@ import { isCustomOrgRole } from "@app/services/org/org-role-fns"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { TUserDALFactory } from "@app/services/user/user-dal"; -import { TMembershipUserScopeFactory } from "../membership-user-types"; import { TMembershipUserDALFactory } from "../membership-user-dal"; +import { TMembershipUserScopeFactory } from "../membership-user-types"; type TOrgMembershipUserScopeFactoryDep = { permissionService: Pick; diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index fd1a361f0..ff625875a 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -705,6 +705,25 @@ export const orgDALFactory = (db: TDbClient) => { } }; + const findRootOrgDetails = async (orgId: string): Promise => { + try { + const org = await db + .replicaNode()(TableName.Organization) + .select(selectAllTableCols(TableName.Organization)) + .where( + "id", + db(TableName.Organization) + .select(db.raw(`CASE WHEN "rootOrgId" IS NULL THEN id ELSE "rootOrgId" END`)) + .where("id", orgId) + ) + .first(); + + return org; + } catch (error) { + throw new DatabaseError({ error, name: "FindRootOrgDetails" }); + } + }; + return withTransaction(db, { ...orgOrm, findOrgByProjectId, @@ -728,6 +747,7 @@ export const orgDALFactory = (db: TDbClient) => { deleteMembershipById, deleteMembershipsById, updateMembership, - findIdentityOrganization + findIdentityOrganization, + findRootOrgDetails }); }; diff --git a/backend/src/services/service-token/service-token-service.ts b/backend/src/services/service-token/service-token-service.ts index 8b50e9970..081b99208 100644 --- a/backend/src/services/service-token/service-token-service.ts +++ b/backend/src/services/service-token/service-token-service.ts @@ -14,6 +14,7 @@ import { logger } from "@app/lib/logger"; import { TAccessTokenQueueServiceFactory } from "../access-token-queue/access-token-queue"; import { ActorType } from "../auth/auth-type"; +import { TOrgDALFactory } from "../org/org-dal"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; @@ -25,7 +26,6 @@ import { TGetServiceTokenInfoDTO, TProjectServiceTokensDTO } from "./service-token-types"; -import { TOrgDALFactory } from "../org/org-dal"; type TServiceTokenServiceFactoryDep = { serviceTokenDAL: TServiceTokenDALFactory; diff --git a/frontend/src/hooks/api/orgIdentityMembership/index.tsx b/frontend/src/hooks/api/orgIdentityMembership/index.tsx index 61d572e30..a28824501 100644 --- a/frontend/src/hooks/api/orgIdentityMembership/index.tsx +++ b/frontend/src/hooks/api/orgIdentityMembership/index.tsx @@ -1,2 +1,6 @@ export { useCreateOrgIdentityMembership, useDeleteOrgIdentityMembership } from "./mutation"; -export type { TCreateOrgIdentityMembershipDTO, TDeleteOrgIdentityMembershipDTO, TOrgIdentityMembership } from "./types"; +export type { + TCreateOrgIdentityMembershipDTO, + TDeleteOrgIdentityMembershipDTO, + TOrgIdentityMembership +} from "./types"; diff --git a/frontend/src/hooks/api/organization/index.ts b/frontend/src/hooks/api/organization/index.ts index 5f2be9b76..7c283691e 100644 --- a/frontend/src/hooks/api/organization/index.ts +++ b/frontend/src/hooks/api/organization/index.ts @@ -1,12 +1,12 @@ export { useAddOrgPmtMethod, - useGetAvailableOrgIdentities, useAddOrgTaxId, useCreateCustomerPortalSession, useCreateOrg, 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 d76b8a54b..36c0fd1fb 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -582,7 +582,7 @@ export const useGetAvailableOrgIdentities = (enabled = true) => queryKey: organizationKeys.getAvailableIdentities(), queryFn: async () => { const { data } = await apiRequest.get<{ identities: { name: string; id: string }[] }>( - `/api/v1/organization/identities/available` + "/api/v1/organization/identities/available" ); return data.identities; @@ -596,7 +596,7 @@ export const useGetAvailableOrgUsers = (enabled = true) => queryFn: async () => { const { data } = await apiRequest.get<{ users: { username: string; id: string; firstName: string; lastName: string }[]; - }>(`/api/v1/organization/users/available`); + }>("/api/v1/organization/users/available"); return data.users; }, diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx index ffaf5b068..3b48e7763 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx @@ -58,8 +58,8 @@ import { AuthMethod } from "@app/hooks/api/users/types"; import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils"; import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel"; -import { NotificationDropdown } from "./NotificationDropdown"; import { NewSubOrganizationForm } from "./NewSubOrganizationForm"; +import { NotificationDropdown } from "./NotificationDropdown"; const getPlan = (subscription: SubscriptionPlan) => { if (subscription.groups) return "Enterprise"; diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx index 81946de8d..c05c5abaa 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx @@ -4,8 +4,8 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input } from "@app/components/v2"; -import { GenericResourceNameSchema } from "@app/lib/schemas"; import { useCreateSubOrganization } from "@app/hooks/api"; +import { GenericResourceNameSchema } from "@app/lib/schemas"; type ContentProps = { onClose: () => void; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLinkForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLinkForm.tsx index 96ad3eba4..b0977437b 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLinkForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLinkForm.tsx @@ -105,7 +105,7 @@ export const IdentityLinkForm = ({ onClose }: Props) => { placeholder="Select role..." getOptionValue={(option) => option.slug} getOptionLabel={(option) => option.name} - menuPortalTarget={document.body} + // 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/IdentityModal.tsx index 6b4d5c232..dacbba428 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx @@ -53,7 +53,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { const orgId = currentOrg?.id || ""; const { data: roles } = useGetOrgRoles(orgId); - const isOrgIdentity = orgId === popUp?.identity?.data?.orgId; + const isOrgIdentity = popUp?.identity?.data ? orgId === popUp?.identity?.data?.orgId : true; const { mutateAsync: createMutateAsync } = useCreateIdentity(); const { mutateAsync: updateMutateAsync } = useUpdateIdentity(); 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 6627f87ce..2f0551966 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 @@ -24,11 +24,11 @@ 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 { IdentityLinkForm } from "./IdentityLinkForm"; export const IdentitySection = withPermission( () => { diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx index f5b0f5a0c..ceb3d6565 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx @@ -3,6 +3,7 @@ import { useSearch } from "@tanstack/react-router"; import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; +import { useOrganization } from "@app/context"; import { AuditLogStreamsTab } from "../AuditLogStreamTab"; import { ExternalMigrationsTab } from "../ExternalMigrationsTab"; @@ -14,7 +15,6 @@ import { OrgSecurityTab } from "../OrgSecurityTab"; import { OrgSsoTab } from "../OrgSsoTab"; import { OrgWorkflowIntegrationTab } from "../OrgWorkflowIntegrationTab"; import { ProjectTemplatesTab } from "../ProjectTemplatesTab"; -import { useOrganization } from "@app/context"; export const OrgTabGroup = () => { const search = useSearch({ diff --git a/frontend/src/pages/organization/layout.tsx b/frontend/src/pages/organization/layout.tsx index 233b490b3..0caf8286c 100644 --- a/frontend/src/pages/organization/layout.tsx +++ b/frontend/src/pages/organization/layout.tsx @@ -1,7 +1,7 @@ import { createFileRoute, retainSearchParams } from "@tanstack/react-router"; +import { z } from "zod"; import { OrganizationLayout } from "@app/layouts/OrganizationLayout"; -import { z } from "zod"; export const Route = createFileRoute("/_authenticate/_inject-org-details/_org-layout")({ component: OrganizationLayout, From 8824be431fd8335d7de5b2796a1a1adee3fed1ff Mon Sep 17 00:00:00 2001 From: = Date: Mon, 20 Oct 2025 15:08:19 +0530 Subject: [PATCH 08/44] feat: added manageby and conditional rendering more items --- .../saml-config/saml-config-service.ts | 2 +- .../src/server/routes/v1/identity-router.ts | 2 +- .../identity-project/identity-project-dal.ts | 3 ++- .../src/services/identity/identity-org-dal.ts | 3 +++ .../src/services/identity/identity-service.ts | 2 +- .../OrganizationContext.tsx | 9 +++++++-- .../src/hooks/api/organization/queries.tsx | 2 +- .../IdentitySection/IdentityTable.tsx | 18 +++++++++++++++--- .../IdentityDetailsByIDPage.tsx | 6 +++++- .../components/IdentityDetailsSection.tsx | 11 ++++++++++- .../components/ShareSecretForm.tsx | 14 +++++++++++--- 11 files changed, 57 insertions(+), 15 deletions(-) diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index abe8c3d2e..13b862343 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -408,7 +408,7 @@ export const samlConfigServiceFactory = ({ }); } } else if (dto.type === "orgSlug") { - const org = await orgDAL.findOne({ slug: dto.orgSlug }); + const org = await orgDAL.findOne({ slug: dto.orgSlug, rootOrgId: null }); if (!org) { throw new NotFoundError({ message: `Organization with slug '${dto.orgSlug}' not found` diff --git a/backend/src/server/routes/v1/identity-router.ts b/backend/src/server/routes/v1/identity-router.ts index b1798692d..f8e6c78ee 100644 --- a/backend/src/server/routes/v1/identity-router.ts +++ b/backend/src/server/routes/v1/identity-router.ts @@ -393,7 +393,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { permissions: true, description: true }).optional(), - identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({ + identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true, orgId: true }).extend({ authMethods: z.array(z.string()) }) }).array(), diff --git a/backend/src/services/identity-project/identity-project-dal.ts b/backend/src/services/identity-project/identity-project-dal.ts index 3dba6210d..adcdd8be8 100644 --- a/backend/src/services/identity-project/identity-project-dal.ts +++ b/backend/src/services/identity-project/identity-project-dal.ts @@ -25,11 +25,12 @@ import { buildAuthMethods } from "../identity/identity-fns"; export type TIdentityProjectDALFactory = ReturnType; export const identityProjectDALFactory = (db: TDbClient) => { - const findByIdentityId = async (identityId: string, tx?: Knex) => { + const findByIdentityId = async (identityId: string, orgId: string, tx?: Knex) => { try { const docs = await (tx || db.replicaNode())(TableName.Membership) .where(`${TableName.Membership}.actorIdentityId`, identityId) .where(`${TableName.Membership}.scope`, AccessScope.Project) + .where(`${TableName.Membership}.scopeOrgId`, orgId) .whereNotNull(`${TableName.Membership}.actorIdentityId`) .join(TableName.Project, `${TableName.Membership}.scopeProjectId`, `${TableName.Project}.id`) .join(TableName.Identity, `${TableName.Membership}.actorIdentityId`, `${TableName.Identity}.id`) diff --git a/backend/src/services/identity/identity-org-dal.ts b/backend/src/services/identity/identity-org-dal.ts index 04c384843..898040458 100644 --- a/backend/src/services/identity/identity-org-dal.ts +++ b/backend/src/services/identity/identity-org-dal.ts @@ -519,6 +519,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("actorIdentityId").withSchema(TableName.Membership).as("identityId"), db.ref("name").withSchema(TableName.Identity).as("identityName"), db.ref("hasDeleteProtection").withSchema(TableName.Identity), + db.ref("orgId").withSchema(TableName.Identity).as("identityOrgId"), db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), @@ -570,6 +571,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { crPermission, crName, identityId, + identityOrgId, identityName, hasDeleteProtection, role, @@ -615,6 +617,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { id: identityId as string, name: identityName, hasDeleteProtection, + orgId: identityOrgId, authMethods: buildAuthMethods({ uaId, alicloudId, diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index f2caeb053..f6ec60e9e 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -477,7 +477,7 @@ export const identityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - const identityMemberships = await identityProjectDAL.findByIdentityId(identityId); + const identityMemberships = await identityProjectDAL.findByIdentityId(identityId, actorOrgId); return identityMemberships; }; diff --git a/frontend/src/context/OrganizationContext/OrganizationContext.tsx b/frontend/src/context/OrganizationContext/OrganizationContext.tsx index 5a18ec741..79c004e1f 100644 --- a/frontend/src/context/OrganizationContext/OrganizationContext.tsx +++ b/frontend/src/context/OrganizationContext/OrganizationContext.tsx @@ -1,5 +1,5 @@ import { useSuspenseQuery } from "@tanstack/react-query"; -import { useRouteContext } from "@tanstack/react-router"; +import { useRouteContext, useSearch } from "@tanstack/react-router"; import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries"; @@ -9,8 +9,13 @@ export const useOrganization = () => { select: (el) => el.organizationId }); + const subOrganization = useSearch({ + strict: false, + select: (el) => el?.subOrganization + }); + const { data: currentOrg } = useSuspenseQuery({ - queryKey: organizationKeys.getOrgById(organizationId), + queryKey: organizationKeys.getOrgById(organizationId, subOrganization), queryFn: () => fetchOrganizationById(organizationId), staleTime: Infinity }); diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index 36c0fd1fb..bbf73dd25 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -42,7 +42,7 @@ export const organizationKeys = { [...organizationKeys.getOrgIdentityMemberships(orgId), params] as const, getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const, getOrgIntegrationAuths: (orgId: string) => [{ orgId }, "integration-auths"] as const, - getOrgById: (orgId: string) => ["organization", { orgId }], + getOrgById: (orgId: string, subOrg?: string) => ["organization", { orgId, subOrg }], getAvailableIdentities: () => ["available-identities"], getAvailableUsers: () => ["available-users"] }; 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 b632318e6..2705593e5 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,6 +2,7 @@ import { useCallback, useState } from "react"; import { faArrowDown, faArrowUp, + faBuilding, faCheckCircle, faChevronRight, faEdit, @@ -78,7 +79,7 @@ type Filter = { export const IdentityTable = ({ handlePopUpOpen }: Props) => { const navigate = useNavigate(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const { offset, @@ -286,15 +287,18 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
+ {isSubOrganization && Managed By} {isFetching ? : null} - {isPending && } + {isPending && ( + + )} {!isPending && data?.identities?.map( ({ - identity: { id, name }, + identity: { id, name, orgId }, role, customRole, lastLoginAuthMethod, @@ -362,6 +366,14 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { }} + {isSubOrganization && ( + +

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

+ + )} diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx index 0a853070b..98668d6ca 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx @@ -91,7 +91,11 @@ const Page = () => {
- + {!isAuthHidden && ( ({ initialState: "Copy ID to clipboard" }); + const { isSubOrganization } = useOrganization(); const { data } = useGetIdentityById(identityId); return data ? ( @@ -142,6 +143,14 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen, isOrgIdent

Name

{data.identity.name}

+ {isSubOrganization && ( +
+

Manage By

+

+ {isOrgIdentity ? "Organization" : "Root Organization"} +

+
+ )} {isOrgIdentity && (

Last Login Auth Method

diff --git a/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx b/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx index 54627888b..109c02fbc 100644 --- a/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx +++ b/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx @@ -22,6 +22,7 @@ import { import { useTimedReset } from "@app/hooks"; import { useCreatePublicSharedSecret, useCreateSharedSecret } from "@app/hooks/api"; import { SecretSharingAccessType } from "@app/hooks/api/secretSharing"; +import { useSearch } from "@tanstack/react-router"; // values in ms const expiresInOptions = [ @@ -87,6 +88,10 @@ export const ShareSecretForm = ({ const [, isCopyingSecret, setCopyTextSecret] = useTimedReset({ initialState: "Copy to clipboard" }); + const subOrganization = useSearch({ + strict: false, + select: (el) => el?.subOrganization + }); const publicSharedSecretCreator = useCreatePublicSharedSecret(); const privateSharedSecretCreator = useCreateSharedSecret(); @@ -148,11 +153,14 @@ export const ShareSecretForm = ({ type: "success" }); } else { - const link = `${window.location.origin}/shared/secret/${id}`; + const link = new URL(`${window.location.origin}/shared/secret/${id}`); + if (subOrganization) { + link.searchParams.set("subOrganization", subOrganization); + } - setSecretLink(link); + setSecretLink(link.toString()); - navigator.clipboard.writeText(link); + navigator.clipboard.writeText(link.toString()); setCopyTextSecret("secret"); createNotification({ From 2ec851fd34c6fa7b7c41d36c271222735e7521c6 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 20 Oct 2025 15:25:42 +0530 Subject: [PATCH 09/44] feat: add slug validator for sub org creation --- backend/src/ee/routes/v1/sub-org-router.ts | 7 ++++--- backend/src/ee/services/sub-org/sub-org-service.ts | 12 ++++++++++-- backend/src/server/plugins/auth/inject-identity.ts | 4 ++++ .../components/NavBar/NewSubOrganizationForm.tsx | 3 +-- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/backend/src/ee/routes/v1/sub-org-router.ts b/backend/src/ee/routes/v1/sub-org-router.ts index aed2b63d6..0a03c2ade 100644 --- a/backend/src/ee/routes/v1/sub-org-router.ts +++ b/backend/src/ee/routes/v1/sub-org-router.ts @@ -6,6 +6,7 @@ import { ApiDocsTags, SUB_ORGANIZATIONS } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { GenericResourceNameSchema } from "@app/server/lib/schemas"; const sanitiziedSubOrganizationSchema = OrganizationsSchema.pick({ id: true, @@ -32,7 +33,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => { } ], body: z.object({ - name: z.string().trim().describe(SUB_ORGANIZATIONS.CREATE.name) + name: GenericResourceNameSchema.describe(SUB_ORGANIZATIONS.CREATE.name) }), response: { 200: z.object({ @@ -40,7 +41,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { const { organization } = await server.services.subOrganization.createSubOrg({ name: req.body.name, @@ -100,7 +101,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { const { organizations } = await server.services.subOrganization.listSubOrgs({ permissionActor: { diff --git a/backend/src/ee/services/sub-org/sub-org-service.ts b/backend/src/ee/services/sub-org/sub-org-service.ts index 8bb6da13d..e2433a644 100644 --- a/backend/src/ee/services/sub-org/sub-org-service.ts +++ b/backend/src/ee/services/sub-org/sub-org-service.ts @@ -47,13 +47,21 @@ export const subOrgServiceFactory = ({ const orgLicensePlan = await licenseService.getPlan(permissionActor.rootOrgId); if (!orgLicensePlan.subOrganization) { throw new BadRequestError({ - message: "Child organization creation failed. Please upgrade your instance to Infisical's Enterprise plan." + message: "Sub-organization creation failed. Please upgrade your instance to Infisical's Enterprise plan." }); } + const existingSubOrg = await orgDAL.find({ + parentOrgId: permissionActor.orgId, + name + }); + if (existingSubOrg) { + throw new BadRequestError({ message: `Sub-organization with name ${name} already exists` }); + } + const organization = await orgDAL.transaction(async (tx) => { const org = await orgDAL.create( - { name, slug: name, rootOrgId: permissionActor.orgId, parentOrgId: permissionActor.orgId }, + { name, slug: name, rootOrgId: permissionActor.rootOrgId, parentOrgId: permissionActor.orgId }, tx ); const membership = await membershipDAL.create( diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index bde9be050..9a959c5bc 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -11,6 +11,7 @@ import { BadRequestError } from "@app/lib/errors"; import { ActorType, AuthMethod, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type"; import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-token/identity-access-token-types"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; +import { GenericResourceNameSchema } from "@app/server/lib/schemas"; export type TAuthMode = | { @@ -147,6 +148,9 @@ export const injectIdentity = fp( if (!authMode) return; const subOrganizationSelector = req.headers?.["x-infisical-org"] as string | undefined; + if (subOrganizationSelector) { + await GenericResourceNameSchema.parseAsync(subOrganizationSelector); + } switch (authMode) { case AuthMode.JWT: { diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx index c05c5abaa..5a736491d 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx @@ -26,8 +26,7 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => { formState: { isSubmitting } } = useForm({ defaultValues: { - name: "", - invitees: [] + name: "" }, resolver: zodResolver(AddOrgSchema) }); From 554c87dc9b530523051ccaf6d5e866e4da9d9d68 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 20 Oct 2025 16:20:39 +0530 Subject: [PATCH 10/44] feat: added migration and identity check --- .../db/migrations/20251018061215_sub-org.ts | 20 ++++++++++-- backend/src/server/routes/index.ts | 11 +++++++ .../v1/identity-alicloud-auth-router.ts | 4 +-- .../routes/v1/identity-aws-iam-auth-router.ts | 4 +-- .../routes/v1/identity-azure-auth-router.ts | 4 +-- .../routes/v1/identity-gcp-auth-router.ts | 4 +-- .../routes/v1/identity-jwt-auth-router.ts | 4 +-- .../v1/identity-kubernetes-auth-router.ts | 4 +-- .../routes/v1/identity-ldap-auth-router.ts | 4 +-- .../routes/v1/identity-oci-auth-router.ts | 4 +-- .../routes/v1/identity-oidc-auth-router.ts | 4 +-- .../v1/identity-tls-cert-auth-router.ts | 4 +-- .../routes/v1/identity-token-auth-router.ts | 4 +-- .../v1/identity-universal-auth-router.ts | 4 +-- .../identity-alicloud-auth-service.ts | 19 ++++++------ .../identity-aws-auth-service.ts | 18 +++++------ .../identity-azure-auth-service.ts | 18 +++++------ .../identity-gcp-auth-service.ts | 20 ++++++------ .../identity-jwt-auth-service.ts | 29 +++++++---------- .../identity-kubernetes-auth-service.ts | 29 +++++++---------- .../identity-ldap-auth-service.ts | 31 ++++++------------- .../identity-oci-auth-service.ts | 23 ++++++-------- .../identity-oidc-auth-service.ts | 29 +++++++---------- .../identity-tls-cert-auth-service.ts | 30 +++++++----------- .../identity-tls-cert-auth-types.ts | 4 +-- .../identity-token-auth-service.ts | 28 +++++++++++------ .../identity-ua/identity-ua-service.ts | 22 +++++-------- 27 files changed, 181 insertions(+), 198 deletions(-) diff --git a/backend/src/db/migrations/20251018061215_sub-org.ts b/backend/src/db/migrations/20251018061215_sub-org.ts index ec14e5fc5..58b49fbcc 100644 --- a/backend/src/db/migrations/20251018061215_sub-org.ts +++ b/backend/src/db/migrations/20251018061215_sub-org.ts @@ -1,6 +1,6 @@ import { Knex } from "knex"; -import { TableName } from "../schemas"; +import { AccessScope, TableName } from "../schemas"; export async function up(knex: Knex): Promise { const hasParentOrgId = await knex.schema.hasColumn(TableName.Organization, "parentOrgId"); @@ -18,9 +18,25 @@ export async function up(knex: Knex): Promise { const hasIdentityOrgCol = await knex.schema.hasColumn(TableName.Identity, "orgId"); if (!hasIdentityOrgCol) { await knex.schema.alterTable(TableName.Identity, (t) => { - t.uuid("orgId").notNullable(); + t.uuid("orgId"); t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); }); + + await knex.raw( + ` + UPDATE ?? AS identity + SET "orgId" = membership."scopeOrgId" + FROM ?? AS membership + WHERE + membership."actorIdentityId" = identity."id" + AND membership."scope" = ? +`, + [TableName.Identity, TableName.Membership, AccessScope.Organization] + ); + + await knex.schema.alterTable(TableName.Identity, (t) => { + t.uuid("orgId").notNullable(); + }); } } diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index ddc757e6f..d3bbc4da0 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1618,6 +1618,7 @@ export const registerRoutes = async ( }); const identityTokenAuthService = identityTokenAuthServiceFactory({ + identityDAL, identityTokenAuthDAL, identityAccessTokenDAL, permissionService, @@ -1627,6 +1628,7 @@ export const registerRoutes = async ( }); const identityUaService = identityUaServiceFactory({ + identityDAL, permissionService, identityAccessTokenDAL, identityUaClientSecretDAL, @@ -1638,6 +1640,7 @@ export const registerRoutes = async ( }); const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({ + identityDAL, identityKubernetesAuthDAL, identityAccessTokenDAL, permissionService, @@ -1651,6 +1654,7 @@ export const registerRoutes = async ( membershipIdentityDAL }); const identityGcpAuthService = identityGcpAuthServiceFactory({ + identityDAL, identityGcpAuthDAL, orgDAL, identityAccessTokenDAL, @@ -1660,6 +1664,7 @@ export const registerRoutes = async ( }); const identityAliCloudAuthService = identityAliCloudAuthServiceFactory({ + identityDAL, identityAccessTokenDAL, orgDAL, identityAliCloudAuthDAL, @@ -1669,6 +1674,7 @@ export const registerRoutes = async ( }); const identityTlsCertAuthService = identityTlsCertAuthServiceFactory({ + identityDAL, identityAccessTokenDAL, identityTlsCertAuthDAL, licenseService, @@ -1678,6 +1684,7 @@ export const registerRoutes = async ( }); const identityAwsAuthService = identityAwsAuthServiceFactory({ + identityDAL, identityAccessTokenDAL, orgDAL, identityAwsAuthDAL, @@ -1687,6 +1694,7 @@ export const registerRoutes = async ( }); const identityAzureAuthService = identityAzureAuthServiceFactory({ + identityDAL, identityAzureAuthDAL, orgDAL, identityAccessTokenDAL, @@ -1696,6 +1704,7 @@ export const registerRoutes = async ( }); const identityOciAuthService = identityOciAuthServiceFactory({ + identityDAL, identityAccessTokenDAL, orgDAL, identityOciAuthDAL, @@ -1719,6 +1728,7 @@ export const registerRoutes = async ( }); const identityOidcAuthService = identityOidcAuthServiceFactory({ + identityDAL, identityOidcAuthDAL, orgDAL, identityAccessTokenDAL, @@ -1729,6 +1739,7 @@ export const registerRoutes = async ( }); const identityJwtAuthService = identityJwtAuthServiceFactory({ + identityDAL, identityJwtAuthDAL, orgDAL, permissionService, diff --git a/backend/src/server/routes/v1/identity-alicloud-auth-router.ts b/backend/src/server/routes/v1/identity-alicloud-auth-router.ts index 3645a8bb6..8f64d3b23 100644 --- a/backend/src/server/routes/v1/identity-alicloud-auth-router.ts +++ b/backend/src/server/routes/v1/identity-alicloud-auth-router.ts @@ -73,12 +73,12 @@ export const registerIdentityAliCloudAuthRouter = async (server: FastifyZodProvi } }, handler: async (req) => { - const { identityAliCloudAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityAliCloudAuth, accessToken, identityAccessToken, identity } = await server.services.identityAliCloudAuth.login(req.body); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, event: { type: EventType.LOGIN_IDENTITY_ALICLOUD_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts index 59526899c..3cfb19895 100644 --- a/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts +++ b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts @@ -40,12 +40,12 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) } }, handler: async (req) => { - const { identityAwsAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityAwsAuth, accessToken, identityAccessToken, identity } = await server.services.identityAwsAuth.login(req.body); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, event: { type: EventType.LOGIN_IDENTITY_AWS_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-azure-auth-router.ts b/backend/src/server/routes/v1/identity-azure-auth-router.ts index 2649655bd..cdab7af02 100644 --- a/backend/src/server/routes/v1/identity-azure-auth-router.ts +++ b/backend/src/server/routes/v1/identity-azure-auth-router.ts @@ -35,12 +35,12 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider } }, handler: async (req) => { - const { identityAzureAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityAzureAuth, accessToken, identityAccessToken, identity } = await server.services.identityAzureAuth.login(req.body); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, event: { type: EventType.LOGIN_IDENTITY_AZURE_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-gcp-auth-router.ts b/backend/src/server/routes/v1/identity-gcp-auth-router.ts index d65c46613..474999b2b 100644 --- a/backend/src/server/routes/v1/identity-gcp-auth-router.ts +++ b/backend/src/server/routes/v1/identity-gcp-auth-router.ts @@ -35,12 +35,12 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) } }, handler: async (req) => { - const { identityGcpAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityGcpAuth, accessToken, identityAccessToken, identity } = await server.services.identityGcpAuth.login(req.body); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, event: { type: EventType.LOGIN_IDENTITY_GCP_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-jwt-auth-router.ts b/backend/src/server/routes/v1/identity-jwt-auth-router.ts index 2a882471d..5d71b3781 100644 --- a/backend/src/server/routes/v1/identity-jwt-auth-router.ts +++ b/backend/src/server/routes/v1/identity-jwt-auth-router.ts @@ -111,7 +111,7 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider) } }, handler: async (req) => { - const { identityJwtAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityJwtAuth, accessToken, identityAccessToken, identity } = await server.services.identityJwtAuth.login({ identityId: req.body.identityId, jwt: req.body.jwt @@ -119,7 +119,7 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider) await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, event: { type: EventType.LOGIN_IDENTITY_JWT_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts index 0794cf00d..28f611aba 100644 --- a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts +++ b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts @@ -56,7 +56,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide } }, handler: async (req) => { - const { identityKubernetesAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityKubernetesAuth, accessToken, identityAccessToken, identity } = await server.services.identityKubernetesAuth.login({ identityId: req.body.identityId, jwt: req.body.jwt @@ -64,7 +64,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, event: { type: EventType.LOGIN_IDENTITY_KUBERNETES_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-ldap-auth-router.ts b/backend/src/server/routes/v1/identity-ldap-auth-router.ts index caf5708e3..dade20ea3 100644 --- a/backend/src/server/routes/v1/identity-ldap-auth-router.ts +++ b/backend/src/server/routes/v1/identity-ldap-auth-router.ts @@ -162,13 +162,13 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) const { identityId, user } = req.passportMachineIdentity; - const { accessToken, identityLdapAuth, identityMembershipOrg } = await server.services.identityLdapAuth.login({ + const { accessToken, identityLdapAuth, identity } = await server.services.identityLdapAuth.login({ identityId }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, event: { type: EventType.LOGIN_IDENTITY_LDAP_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-oci-auth-router.ts b/backend/src/server/routes/v1/identity-oci-auth-router.ts index 24d414286..003d9810b 100644 --- a/backend/src/server/routes/v1/identity-oci-auth-router.ts +++ b/backend/src/server/routes/v1/identity-oci-auth-router.ts @@ -52,12 +52,12 @@ export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider) } }, handler: async (req) => { - const { identityOciAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityOciAuth, accessToken, identityAccessToken, identity } = await server.services.identityOciAuth.login(req.body); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, event: { type: EventType.LOGIN_IDENTITY_OCI_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-oidc-auth-router.ts b/backend/src/server/routes/v1/identity-oidc-auth-router.ts index 48fa64bf4..6fad1f400 100644 --- a/backend/src/server/routes/v1/identity-oidc-auth-router.ts +++ b/backend/src/server/routes/v1/identity-oidc-auth-router.ts @@ -59,7 +59,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) } }, handler: async (req) => { - const { identityOidcAuth, accessToken, identityAccessToken, identityMembershipOrg, oidcTokenData } = + const { identityOidcAuth, accessToken, identityAccessToken, identity, oidcTokenData } = await server.services.identityOidcAuth.login({ identityId: req.body.identityId, jwt: req.body.jwt @@ -67,7 +67,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, event: { type: EventType.LOGIN_IDENTITY_OIDC_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts b/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts index d549160db..b7a44c62c 100644 --- a/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts +++ b/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts @@ -64,7 +64,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid throw new BadRequestError({ message: "Missing TLS certificate in header" }); } - const { identityTlsCertAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityTlsCertAuth, accessToken, identityAccessToken, identity } = await server.services.identityTlsCertAuth.login({ identityId: req.body.identityId, clientCertificate: clientCertificate as string @@ -72,7 +72,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, event: { type: EventType.LOGIN_IDENTITY_TLS_CERT_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-token-auth-router.ts b/backend/src/server/routes/v1/identity-token-auth-router.ts index 9040d8909..aafffdfdb 100644 --- a/backend/src/server/routes/v1/identity-token-auth-router.ts +++ b/backend/src/server/routes/v1/identity-token-auth-router.ts @@ -319,7 +319,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider } }, handler: async (req) => { - const { identityTokenAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityTokenAuth, accessToken, identityAccessToken, identity } = await server.services.identityTokenAuth.createTokenAuthToken({ actor: req.permission.type, actorId: req.permission.id, @@ -332,7 +332,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, event: { type: EventType.CREATE_TOKEN_IDENTITY_TOKEN_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-universal-auth-router.ts b/backend/src/server/routes/v1/identity-universal-auth-router.ts index 0443d35dd..88a4cb775 100644 --- a/backend/src/server/routes/v1/identity-universal-auth-router.ts +++ b/backend/src/server/routes/v1/identity-universal-auth-router.ts @@ -52,14 +52,14 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { accessToken, identityAccessToken, validClientSecretInfo, - identityMembershipOrg, + identity, accessTokenTTL, accessTokenMaxTTL } = await server.services.identityUa.login(req.body.clientId, req.body.clientSecret, req.realIp); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, event: { type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH, metadata: { 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 3f3c6cdf6..646dc72fc 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 @@ -26,6 +26,7 @@ import { logger } from "@app/lib/logger"; import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; @@ -40,12 +41,13 @@ import { } from "./identity-alicloud-auth-types"; type TIdentityAliCloudAuthServiceFactoryDep = { + identityDAL: Pick; identityAccessTokenDAL: Pick; identityAliCloudAuthDAL: Pick< TIdentityAliCloudAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete" >; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; orgDAL: Pick; @@ -54,6 +56,7 @@ type TIdentityAliCloudAuthServiceFactoryDep = { export type TIdentityAliCloudAuthServiceFactory = ReturnType; export const identityAliCloudAuthServiceFactory = ({ + identityDAL, identityAccessTokenDAL, identityAliCloudAuthDAL, membershipIdentityDAL, @@ -69,12 +72,8 @@ export const identityAliCloudAuthServiceFactory = ({ }); } - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityAliCloudAuth.identityId, - scope: AccessScope.Organization - }); - - if (!identityMembershipOrg) throw new UnauthorizedError({ message: "Identity not attached to a organization" }); + const identity = await identityDAL.findById(identityAliCloudAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const requestUrl = new URL("https://sts.aliyuncs.com"); @@ -99,8 +98,8 @@ export const identityAliCloudAuthServiceFactory = ({ // Generate the token const identityAccessToken = await identityAliCloudAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, { lastLoginAuthMethod: IdentityAuthMethod.ALICLOUD_AUTH, lastLoginTime: new Date() @@ -141,7 +140,7 @@ export const identityAliCloudAuthServiceFactory = ({ identityAliCloudAuth, accessToken, identityAccessToken, - identityMembershipOrg + identity }; }; 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 59f1b3d5b..d36ea64de 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 @@ -25,6 +25,7 @@ import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; @@ -41,9 +42,10 @@ import { } from "./identity-aws-auth-types"; type TIdentityAwsAuthServiceFactoryDep = { + identityDAL: Pick; identityAccessTokenDAL: Pick; identityAwsAuthDAL: Pick; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; orgDAL: Pick; @@ -86,6 +88,7 @@ function isValidAwsRegion(region: string | null): boolean { } export const identityAwsAuthServiceFactory = ({ + identityDAL, identityAccessTokenDAL, identityAwsAuthDAL, membershipIdentityDAL, @@ -99,11 +102,8 @@ export const identityAwsAuthServiceFactory = ({ throw new NotFoundError({ message: "AWS auth method not found for identity, did you configure AWS auth?" }); } - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityAwsAuth.identityId, - scope: AccessScope.Organization - }); - if (!identityMembershipOrg) throw new UnauthorizedError({ message: "Identity not attached to a organization" }); + const identity = await identityDAL.findById(identityAwsAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString()); const body: string = Buffer.from(iamRequestBody, "base64").toString(); @@ -165,8 +165,8 @@ export const identityAwsAuthServiceFactory = ({ } const identityAccessToken = await identityAwsAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, { lastLoginAuthMethod: IdentityAuthMethod.AWS_AUTH, lastLoginTime: new Date() @@ -218,7 +218,7 @@ export const identityAwsAuthServiceFactory = ({ } ); - return { accessToken, identityAwsAuth, identityAccessToken, identityMembershipOrg }; + return { accessToken, identityAwsAuth, identityAccessToken, identity }; }; const attachAwsAuth = async ({ 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 c85fabc8f..a9fb6e703 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 @@ -22,6 +22,7 @@ import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; @@ -36,11 +37,12 @@ import { } from "./identity-azure-auth-types"; type TIdentityAzureAuthServiceFactoryDep = { + identityDAL: Pick; identityAzureAuthDAL: Pick< TIdentityAzureAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete" >; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; @@ -50,6 +52,7 @@ type TIdentityAzureAuthServiceFactoryDep = { export type TIdentityAzureAuthServiceFactory = ReturnType; export const identityAzureAuthServiceFactory = ({ + identityDAL, identityAzureAuthDAL, membershipIdentityDAL, identityAccessTokenDAL, @@ -63,11 +66,8 @@ export const identityAzureAuthServiceFactory = ({ throw new NotFoundError({ message: "Azure auth method not found for identity, did you configure Azure Auth?" }); } - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityAzureAuth.identityId, - scope: AccessScope.Organization - }); - if (!identityMembershipOrg) throw new UnauthorizedError({ message: "Identity not attached to a organization" }); + const identity = await identityDAL.findById(identityAzureAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const azureIdentity = await validateAzureIdentity({ tenantId: identityAzureAuth.tenantId, @@ -92,8 +92,8 @@ export const identityAzureAuthServiceFactory = ({ } const identityAccessToken = await identityAzureAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, { lastLoginAuthMethod: IdentityAuthMethod.AZURE_AUTH, lastLoginTime: new Date() @@ -131,7 +131,7 @@ export const identityAzureAuthServiceFactory = ({ } ); - return { accessToken, identityAzureAuth, identityAccessToken, identityMembershipOrg }; + return { accessToken, identityAzureAuth, identityAccessToken, identity }; }; const attachAzureAuth = async ({ 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 83d407fa7..1865e0fb8 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 @@ -22,6 +22,7 @@ import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; @@ -37,8 +38,9 @@ import { } from "./identity-gcp-auth-types"; type TIdentityGcpAuthServiceFactoryDep = { + identityDAL: Pick; identityGcpAuthDAL: Pick; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; @@ -48,6 +50,7 @@ type TIdentityGcpAuthServiceFactoryDep = { export type TIdentityGcpAuthServiceFactory = ReturnType; export const identityGcpAuthServiceFactory = ({ + identityDAL, identityGcpAuthDAL, membershipIdentityDAL, identityAccessTokenDAL, @@ -61,13 +64,8 @@ export const identityGcpAuthServiceFactory = ({ throw new NotFoundError({ message: "GCP auth method not found for identity, did you configure GCP auth?" }); } - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityGcpAuth.identityId, - scope: AccessScope.Organization - }); - if (!identityMembershipOrg) { - throw new UnauthorizedError({ message: "Identity does not belong to any organization" }); - } + const identity = await identityDAL.findById(identityGcpAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); let gcpIdentityDetails: TGcpIdentityDetails; switch (identityGcpAuth.type) { @@ -131,8 +129,8 @@ export const identityGcpAuthServiceFactory = ({ } const identityAccessToken = await identityGcpAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, { lastLoginAuthMethod: IdentityAuthMethod.GCP_AUTH, lastLoginTime: new Date() @@ -170,7 +168,7 @@ export const identityGcpAuthServiceFactory = ({ } ); - return { accessToken, identityGcpAuth, identityAccessToken, identityMembershipOrg }; + return { accessToken, identityGcpAuth, identityAccessToken, identity }; }; const attachGcpAuth = async ({ 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 87b5e8ea7..debd90933 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 @@ -24,6 +24,7 @@ import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { getValueByDot } from "@app/lib/template/dot-access"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TKmsServiceFactory } from "../kms/kms-service"; @@ -43,8 +44,9 @@ import { } from "./identity-jwt-auth-types"; type TIdentityJwtAuthServiceFactoryDep = { + identityDAL: Pick; identityJwtAuthDAL: TIdentityJwtAuthDALFactory; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; @@ -55,6 +57,7 @@ type TIdentityJwtAuthServiceFactoryDep = { export type TIdentityJwtAuthServiceFactory = ReturnType; export const identityJwtAuthServiceFactory = ({ + identityDAL, identityJwtAuthDAL, membershipIdentityDAL, permissionService, @@ -69,19 +72,12 @@ export const identityJwtAuthServiceFactory = ({ throw new NotFoundError({ message: "JWT auth method not found for identity, did you configure JWT auth?" }); } - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityJwtAuth.identityId, - scope: AccessScope.Organization - }); - if (!identityMembershipOrg) { - throw new NotFoundError({ - message: `Identity organization membership for identity with ID '${identityJwtAuth.identityId}' not found` - }); - } + const identity = await identityDAL.findById(identityJwtAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.scopeOrgId + orgId: identity.orgId }); const decodedToken = crypto.jwt().decode(jwtValue, { complete: true }); @@ -211,12 +207,9 @@ export const identityJwtAuthServiceFactory = ({ } const identityAccessToken = await identityJwtAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, - { - lastLoginAuthMethod: IdentityAuthMethod.JWT_AUTH, - lastLoginTime: new Date() - }, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { lastLoginAuthMethod: IdentityAuthMethod.JWT_AUTH, lastLoginTime: new Date() }, tx ); const newToken = await identityAccessTokenDAL.create( @@ -251,7 +244,7 @@ export const identityJwtAuthServiceFactory = ({ } ); - return { accessToken, identityJwtAuth, identityAccessToken, identityMembershipOrg }; + return { accessToken, identityJwtAuth, identityAccessToken, identity }; }; const attachJwtAuth = async ({ 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 bdb6ecd67..49fb597f5 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 @@ -39,6 +39,7 @@ import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TKmsServiceFactory } from "../kms/kms-service"; @@ -59,12 +60,13 @@ import { } from "./identity-kubernetes-auth-types"; type TIdentityKubernetesAuthServiceFactoryDep = { + identityDAL: Pick; identityKubernetesAuthDAL: Pick< TIdentityKubernetesAuthDALFactory, "create" | "findOne" | "transaction" | "updateById" | "delete" >; identityAccessTokenDAL: Pick; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; permissionService: Pick; licenseService: Pick; kmsService: Pick; @@ -80,6 +82,7 @@ export type TIdentityKubernetesAuthServiceFactory = ReturnType { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, - { - lastLoginAuthMethod: IdentityAuthMethod.KUBERNETES_AUTH, - lastLoginTime: new Date() - }, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { lastLoginAuthMethod: IdentityAuthMethod.KUBERNETES_AUTH, lastLoginTime: new Date() }, tx ); const newToken = await identityAccessTokenDAL.create( @@ -486,7 +479,7 @@ export const identityKubernetesAuthServiceFactory = ({ } ); - return { accessToken, identityKubernetesAuth, identityAccessToken, identityMembershipOrg }; + return { accessToken, identityKubernetesAuth, identityAccessToken, identity }; }; const attachKubernetesAuth = async ({ 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 ad76a60a1..272e45c4e 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 @@ -57,11 +57,11 @@ type TIdentityLdapAuthServiceFactoryDep = { TIdentityLdapAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete" >; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; kmsService: TKmsServiceFactory; - identityDAL: TIdentityDALFactory; + identityDAL: Pick; identityAuthTemplateDAL: TIdentityAuthTemplateDALFactory; keyStore: Pick< TKeyStoreFactory, @@ -151,17 +151,6 @@ export const identityLdapAuthServiceFactory = ({ }; const login = async ({ identityId }: TLoginLdapAuthDTO) => { - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityId, - scope: AccessScope.Organization - }); - - if (!identityMembershipOrg) { - throw new UnauthorizedError({ - message: "Invalid credentials" - }); - } - const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId }); if (!identityLdapAuth) { @@ -170,7 +159,10 @@ export const identityLdapAuthServiceFactory = ({ }); } - const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); + const identity = await identityDAL.findById(identityLdapAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); + + const plan = await licenseService.getPlan(identity.orgId); if (!plan.ldap) { throw new BadRequestError({ message: @@ -179,12 +171,9 @@ export const identityLdapAuthServiceFactory = ({ } const identityAccessToken = await identityLdapAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, - { - lastLoginAuthMethod: IdentityAuthMethod.LDAP_AUTH, - lastLoginTime: new Date() - }, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { lastLoginAuthMethod: IdentityAuthMethod.LDAP_AUTH, lastLoginTime: new Date() }, tx ); const newToken = await identityAccessTokenDAL.create( @@ -218,7 +207,7 @@ export const identityLdapAuthServiceFactory = ({ } ); - return { accessToken, identityLdapAuth, identityAccessToken, identityMembershipOrg }; + return { accessToken, identityLdapAuth, identityAccessToken, identity }; }; const attachLdapAuth = async ({ 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 2c5d59e2b..6d7f0c4d3 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 @@ -25,6 +25,7 @@ import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; @@ -41,9 +42,10 @@ import { } from "./identity-oci-auth-types"; type TIdentityOciAuthServiceFactoryDep = { + identityDAL: Pick; identityAccessTokenDAL: Pick; identityOciAuthDAL: Pick; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; orgDAL: Pick; @@ -52,6 +54,7 @@ type TIdentityOciAuthServiceFactoryDep = { export type TIdentityOciAuthServiceFactory = ReturnType; export const identityOciAuthServiceFactory = ({ + identityDAL, identityAccessTokenDAL, identityOciAuthDAL, membershipIdentityDAL, @@ -65,11 +68,8 @@ export const identityOciAuthServiceFactory = ({ throw new NotFoundError({ message: "OCI auth method not found for identity, did you configure OCI auth?" }); } - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityOciAuth.identityId, - scope: AccessScope.Organization - }); - if (!identityMembershipOrg) throw new UnauthorizedError({ message: "Identity not attached to a organization" }); + const identity = await identityDAL.findById(identityOciAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); // Validate OCI host format. Ensures that the host is in "identity..oraclecloud.com" format. if (!headers.host || !new RE2("^identity\\.([a-z]{2}-[a-z]+-[1-9])\\.oraclecloud\\.com$").test(headers.host)) { @@ -104,12 +104,9 @@ export const identityOciAuthServiceFactory = ({ // Generate the token const identityAccessToken = await identityOciAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, - { - lastLoginAuthMethod: IdentityAuthMethod.OCI_AUTH, - lastLoginTime: new Date() - }, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { lastLoginAuthMethod: IdentityAuthMethod.OCI_AUTH, lastLoginTime: new Date() }, tx ); const newToken = await identityAccessTokenDAL.create( @@ -146,7 +143,7 @@ export const identityOciAuthServiceFactory = ({ identityOciAuth, accessToken, identityAccessToken, - identityMembershipOrg + identity }; }; 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 f3d17eb71..628b69f14 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 @@ -25,6 +25,7 @@ import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { getValueByDot } from "@app/lib/template/dot-access"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TKmsServiceFactory } from "../kms/kms-service"; @@ -43,8 +44,9 @@ import { } from "./identity-oidc-auth-types"; type TIdentityOidcAuthServiceFactoryDep = { + identityDAL: Pick; identityOidcAuthDAL: TIdentityOidcAuthDALFactory; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; @@ -55,6 +57,7 @@ type TIdentityOidcAuthServiceFactoryDep = { export type TIdentityOidcAuthServiceFactory = ReturnType; export const identityOidcAuthServiceFactory = ({ + identityDAL, identityOidcAuthDAL, membershipIdentityDAL, permissionService, @@ -69,19 +72,12 @@ export const identityOidcAuthServiceFactory = ({ throw new NotFoundError({ message: "OIDC auth method not found for identity, did you configure OIDC auth?" }); } - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityOidcAuth.identityId, - scope: AccessScope.Organization - }); - if (!identityMembershipOrg) { - throw new NotFoundError({ - message: `Identity organization membership for identity with ID '${identityOidcAuth.identityId}' not found` - }); - } + const identity = await identityDAL.findById(identityOidcAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.scopeOrgId + orgId: identity.orgId }); let caCert = ""; @@ -182,12 +178,9 @@ export const identityOidcAuthServiceFactory = ({ } const identityAccessToken = await identityOidcAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, - { - lastLoginAuthMethod: IdentityAuthMethod.OIDC_AUTH, - lastLoginTime: new Date() - }, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { lastLoginAuthMethod: IdentityAuthMethod.OIDC_AUTH, lastLoginTime: new Date() }, tx ); const newToken = await identityAccessTokenDAL.create( @@ -226,7 +219,7 @@ export const identityOidcAuthServiceFactory = ({ } ); - return { accessToken, identityOidcAuth, identityAccessToken, identityMembershipOrg, oidcTokenData: tokenData }; + return { accessToken, identityOidcAuth, identityAccessToken, identity, oidcTokenData: tokenData }; }; const attachOidcAuth = async ({ 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 d547f7449..24c82ccac 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 @@ -21,6 +21,7 @@ import { import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TKmsServiceFactory } from "../kms/kms-service"; @@ -31,12 +32,13 @@ import { TIdentityTlsCertAuthDALFactory } from "./identity-tls-cert-auth-dal"; import { TIdentityTlsCertAuthServiceFactory } from "./identity-tls-cert-auth-types"; type TIdentityTlsCertAuthServiceFactoryDep = { + identityDAL: Pick; identityAccessTokenDAL: Pick; identityTlsCertAuthDAL: Pick< TIdentityTlsCertAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete" >; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; kmsService: Pick; @@ -52,6 +54,7 @@ const parseSubjectDetails = (data: string) => { }; export const identityTlsCertAuthServiceFactory = ({ + identityDAL, identityAccessTokenDAL, identityTlsCertAuthDAL, membershipIdentityDAL, @@ -67,20 +70,12 @@ export const identityTlsCertAuthServiceFactory = ({ }); } - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityTlsCertAuth.identityId, - scope: AccessScope.Organization - }); - - if (!identityMembershipOrg) { - throw new NotFoundError({ - message: `Identity organization membership for identity with ID '${identityTlsCertAuth.identityId}' not found` - }); - } + const identity = await identityDAL.findById(identityTlsCertAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.scopeOrgId + orgId: identity.orgId }); const caCertificate = decryptor({ @@ -125,12 +120,9 @@ export const identityTlsCertAuthServiceFactory = ({ // Generate the token const identityAccessToken = await identityTlsCertAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, - { - lastLoginAuthMethod: IdentityAuthMethod.TLS_CERT_AUTH, - lastLoginTime: new Date() - }, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { lastLoginAuthMethod: IdentityAuthMethod.TLS_CERT_AUTH, lastLoginTime: new Date() }, tx ); const newToken = await identityAccessTokenDAL.create( @@ -167,7 +159,7 @@ export const identityTlsCertAuthServiceFactory = ({ identityTlsCertAuth, accessToken, identityAccessToken, - identityMembershipOrg + identity }; }; diff --git a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts index eb9f4ab5d..cf35bb5ee 100644 --- a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts +++ b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts @@ -1,4 +1,4 @@ -import { TIdentityAccessTokens, TIdentityTlsCertAuths, TMemberships } from "@app/db/schemas"; +import { TIdentities, TIdentityAccessTokens, TIdentityTlsCertAuths } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; export type TLoginTlsCertAuthDTO = { @@ -40,7 +40,7 @@ export type TIdentityTlsCertAuthServiceFactory = { identityTlsCertAuth: TIdentityTlsCertAuths; accessToken: string; identityAccessToken: TIdentityAccessTokens; - identityMembershipOrg: TMemberships; + identity: TIdentities; }>; attachTlsCertAuth: (dto: TAttachTlsCertAuthDTO) => Promise; updateTlsCertAuth: (dto: TUpdateTlsCertAuthDTO) => Promise; 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 e3c7a486e..2d3e11cd8 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-service.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-service.ts @@ -10,10 +10,17 @@ import { import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; -import { BadRequestError, ForbiddenRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; @@ -32,11 +39,12 @@ import { } from "./identity-token-auth-types"; type TIdentityTokenAuthServiceFactoryDep = { + identityDAL: Pick; identityTokenAuthDAL: Pick< TIdentityTokenAuthDALFactory, "transaction" | "create" | "findOne" | "updateById" | "delete" >; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick< TIdentityAccessTokenDALFactory, "create" | "find" | "update" | "findById" | "findOne" | "updateById" | "delete" @@ -49,8 +57,8 @@ type TIdentityTokenAuthServiceFactoryDep = { export type TIdentityTokenAuthServiceFactory = ReturnType; export const identityTokenAuthServiceFactory = ({ + identityDAL, identityTokenAuthDAL, - // identityDAL, membershipIdentityDAL, identityAccessTokenDAL, permissionService, @@ -400,13 +408,13 @@ export const identityTokenAuthServiceFactory = ({ const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId }); + const identity = await identityDAL.findById(identityTokenAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); + const identityAccessToken = await identityTokenAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, - { - lastLoginAuthMethod: IdentityAuthMethod.TOKEN_AUTH, - lastLoginTime: new Date() - }, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { lastLoginAuthMethod: IdentityAuthMethod.TOKEN_AUTH, lastLoginTime: new Date() }, tx ); const newToken = await identityAccessTokenDAL.create( @@ -441,7 +449,7 @@ export const identityTokenAuthServiceFactory = ({ } ); - return { accessToken, identityTokenAuth, identityAccessToken, identityMembershipOrg }; + return { accessToken, identityTokenAuth, identityAccessToken, identity }; }; const getTokenAuthTokens = async ({ diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index 0de2503ff..dfd7787ea 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -41,8 +41,10 @@ import { TRevokeUaDTO, TUpdateUaDTO } from "./identity-ua-types"; +import { TIdentityDALFactory } from "../identity/identity-dal"; type TIdentityUaServiceFactoryDep = { + identityDAL: Pick; identityUaDAL: TIdentityUaDALFactory; identityUaClientSecretDAL: TIdentityUaClientSecretDALFactory; identityAccessTokenDAL: TIdentityAccessTokenDALFactory; @@ -71,7 +73,8 @@ export const identityUaServiceFactory = ({ permissionService, licenseService, orgDAL, - keyStore + keyStore, + identityDAL }: TIdentityUaServiceFactoryDep) => { const login = async (clientId: string, clientSecret: string, ip: string) => { const identityUa = await identityUaDAL.findOne({ clientId }); @@ -101,16 +104,6 @@ export const identityUaServiceFactory = ({ }); } - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityUa.identityId, - scope: AccessScope.Organization - }); - if (!identityMembershipOrg) { - throw new UnauthorizedError({ - message: "Invalid credentials" - }); - } - const clientSecretPrefix = clientSecret.slice(0, 4); const clientSecretInfo = await identityUaClientSecretDAL.find({ identityUAId: identityUa.id, @@ -228,10 +221,11 @@ export const identityUaServiceFactory = ({ accessTokenMaxTTL: 1000000000 }; + const identity = await identityDAL.findById(identityUa.identityId); const identityAccessToken = await identityUaDAL.transaction(async (tx) => { const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx); - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, { lastLoginAuthMethod: IdentityAuthMethod.UNIVERSAL_AUTH, lastLoginTime: new Date() @@ -277,7 +271,7 @@ export const identityUaServiceFactory = ({ identityUa, validClientSecretInfo, identityAccessToken, - identityMembershipOrg, + identity, ...accessTokenTTLParams }; }; From 223f4982a969223498ec4ba06c03fe736dc27fce Mon Sep 17 00:00:00 2001 From: = Date: Mon, 20 Oct 2025 16:25:41 +0530 Subject: [PATCH 11/44] feat: swtiched uniqueness --- backend/src/db/migrations/20251018061215_sub-org.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/src/db/migrations/20251018061215_sub-org.ts b/backend/src/db/migrations/20251018061215_sub-org.ts index 58b49fbcc..d2342aaef 100644 --- a/backend/src/db/migrations/20251018061215_sub-org.ts +++ b/backend/src/db/migrations/20251018061215_sub-org.ts @@ -12,6 +12,9 @@ export async function up(knex: Knex): Promise { // this would root organization containing various informations like billing etc t.uuid("rootOrgId"); t.foreign("rootOrgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + + t.dropUnique(["slug"]); + t.unique(["rootOrgId", "parentOrgId", "slug"]); }); } From f42dd7f74cc112ebdc7dfd1d5c764da98097286b Mon Sep 17 00:00:00 2001 From: = Date: Mon, 20 Oct 2025 18:47:47 +0530 Subject: [PATCH 12/44] feat: added missing alter statement --- backend/src/db/migrations/20251018061215_sub-org.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/db/migrations/20251018061215_sub-org.ts b/backend/src/db/migrations/20251018061215_sub-org.ts index d2342aaef..94a220c5c 100644 --- a/backend/src/db/migrations/20251018061215_sub-org.ts +++ b/backend/src/db/migrations/20251018061215_sub-org.ts @@ -38,7 +38,7 @@ export async function up(knex: Knex): Promise { ); await knex.schema.alterTable(TableName.Identity, (t) => { - t.uuid("orgId").notNullable(); + t.uuid("orgId").notNullable().alter(); }); } } From 2fa762e56d93c48cab2765842e6c518282368096 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 20 Oct 2025 19:09:19 +0530 Subject: [PATCH 13/44] feat: resolving rebase conflicts --- .../ee/services/sub-org/sub-org-service.ts | 4 +- .../hooks/api/orgIdentityMembership/types.ts | 4 +- .../components/NavBar/Navbar.tsx | 77 ++++++++++++++++++- .../components/OrgNavBar/OrgNavBar.tsx | 18 +++-- 4 files changed, 92 insertions(+), 11 deletions(-) diff --git a/backend/src/ee/services/sub-org/sub-org-service.ts b/backend/src/ee/services/sub-org/sub-org-service.ts index e2433a644..db5b89244 100644 --- a/backend/src/ee/services/sub-org/sub-org-service.ts +++ b/backend/src/ee/services/sub-org/sub-org-service.ts @@ -13,7 +13,7 @@ import { TPermissionServiceFactory } from "../permission/permission-service-type import { TCreateSubOrgDTO, TListSubOrgDTO } from "./sub-org-types"; type TSubOrgServiceFactoryDep = { - orgDAL: Pick; + orgDAL: Pick; permissionService: Pick; licenseService: Pick; membershipDAL: Pick; @@ -51,7 +51,7 @@ export const subOrgServiceFactory = ({ }); } - const existingSubOrg = await orgDAL.find({ + const existingSubOrg = await orgDAL.findOne({ parentOrgId: permissionActor.orgId, name }); diff --git a/frontend/src/hooks/api/orgIdentityMembership/types.ts b/frontend/src/hooks/api/orgIdentityMembership/types.ts index a50ddc1d0..95fa06b82 100644 --- a/frontend/src/hooks/api/orgIdentityMembership/types.ts +++ b/frontend/src/hooks/api/orgIdentityMembership/types.ts @@ -1,4 +1,6 @@ -import { TemporaryPermissionMode } from "@app/db/schemas"; +export enum TemporaryPermissionMode { + Relative = "relative" +} export type TOrgIdentityMembership = { id: string; diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx index 3b48e7763..19a255ee9 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx @@ -49,7 +49,13 @@ import { envConfig } from "@app/config/env"; import { useOrganization, useSubscription, useUser } from "@app/context"; import { isInfisicalCloud } from "@app/helpers/platform"; import { useToggle } from "@app/hooks"; -import { projectKeys, subOrganizationsQuery, useGetOrganizations, useGetOrgTrialUrl, useLogoutUser } from "@app/hooks/api"; +import { + projectKeys, + subOrganizationsQuery, + useGetOrganizations, + useGetOrgTrialUrl, + useLogoutUser +} from "@app/hooks/api"; import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries"; import { MfaMethod } from "@app/hooks/api/auth/types"; import { getAuthToken } from "@app/hooks/api/reactQuery"; @@ -297,6 +303,75 @@ export const Navbar = () => { className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md" style={{ minWidth: "220px" }} > + {subscription?.subOrganization && ( + <> + + + + + + } + onClick={() => setShowSubOrgForm(true)} + > + New Sub Organization + + {Boolean(subOrganizations.length) && ( +
+ )} + {subOrganizations?.map((org) => { + return ( + + + + ); + })} + + +
+ + )}
organizations
diff --git a/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx b/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx index ba34877f9..57f8d99f4 100644 --- a/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx @@ -4,12 +4,14 @@ import { motion } from "framer-motion"; import { CreateOrgModal } from "@app/components/organization/CreateOrgModal"; import { Tab, TabList, Tabs } from "@app/components/v2"; import { usePopUp } from "@app/hooks"; +import { useOrganization } from "@app/context"; type Props = { isHidden?: boolean; }; export const OrgNavBar = ({ isHidden }: Props) => { + const { isRootOrganization } = useOrganization(); const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const); const { pathname } = useLocation(); @@ -80,13 +82,15 @@ export const OrgNavBar = ({ isHidden }: Props) => { )} - - {({ isActive }) => ( - - Usage & Billing - - )} - + {isRootOrganization && ( + + {({ isActive }) => ( + + Usage & Billing + + )} + + )} {({ isActive }) => ( From eec2b306662942c98d82078500873f68c4d75fb7 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 20 Oct 2025 22:05:02 +0530 Subject: [PATCH 14/44] feat: greepy review --- .../db/migrations/20251018061215_sub-org.ts | 8 ++++- backend/src/db/seeds/5-machine-identity.ts | 3 +- backend/src/ee/routes/v1/sub-org-router.ts | 32 ++++++------------- .../ee/services/audit-log/audit-log-types.ts | 8 ++--- .../services/permission/permission-service.ts | 4 +-- backend/src/lib/api-docs/constants.ts | 8 ++--- .../server/plugins/auth/inject-identity.ts | 4 +-- .../identity-access-token-service.ts | 2 +- .../identity-alicloud-auth-service.ts | 2 +- .../identity-aws-auth-service.ts | 2 +- .../identity-azure-auth-service.ts | 2 +- .../identity-gcp-auth-service.ts | 2 +- .../identity-ua/identity-ua-service.ts | 6 +--- .../membership-identity-dal.ts | 4 +-- .../org/org-membership-identity-factory.ts | 6 ++-- .../membership-user/membership-user-dal.ts | 2 +- .../org/org-membership-user-factory.ts | 9 ++++-- .../api/orgIdentityMembership/mutation.tsx | 7 ++-- .../components/NavBar/Navbar.tsx | 4 +-- .../components/OrgNavBar/OrgNavBar.tsx | 2 +- .../IdentitySection/IdentitySection.tsx | 2 +- .../IdentitySection/IdentityTable.tsx | 2 +- .../AppConnectionsPage/route.tsx | 6 ++++ .../components/IdentityDetailsSection.tsx | 2 +- .../components/ShareSecretForm.tsx | 2 +- 25 files changed, 65 insertions(+), 66 deletions(-) diff --git a/backend/src/db/migrations/20251018061215_sub-org.ts b/backend/src/db/migrations/20251018061215_sub-org.ts index 94a220c5c..bd577fe75 100644 --- a/backend/src/db/migrations/20251018061215_sub-org.ts +++ b/backend/src/db/migrations/20251018061215_sub-org.ts @@ -14,8 +14,14 @@ export async function up(knex: Knex): Promise { t.foreign("rootOrgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); t.dropUnique(["slug"]); - t.unique(["rootOrgId", "parentOrgId", "slug"]); }); + + // had to switch to raw for null not distinct + await knex.raw(` +ALTER TABLE "organization" +ADD CONSTRAINT "organization_root_parent_slug_unique" +UNIQUE ("rootOrgId", "parentOrgId", "slug") NULLS NOT DISTINCT; +`); } const hasIdentityOrgCol = await knex.schema.hasColumn(TableName.Identity, "orgId"); diff --git a/backend/src/db/seeds/5-machine-identity.ts b/backend/src/db/seeds/5-machine-identity.ts index 333fc7e3a..85507c890 100644 --- a/backend/src/db/seeds/5-machine-identity.ts +++ b/backend/src/db/seeds/5-machine-identity.ts @@ -24,7 +24,8 @@ export async function seed(knex: Knex): Promise { // @ts-ignore id: seedData1.machineIdentity.id, name: seedData1.machineIdentity.name, - authMethod: IdentityAuthMethod.UNIVERSAL_AUTH + authMethod: IdentityAuthMethod.UNIVERSAL_AUTH, + orgId: seedData1.organization.id } ]); const identityUa = await knex(TableName.IdentityUniversalAuth) diff --git a/backend/src/ee/routes/v1/sub-org-router.ts b/backend/src/ee/routes/v1/sub-org-router.ts index 0a03c2ade..185425cea 100644 --- a/backend/src/ee/routes/v1/sub-org-router.ts +++ b/backend/src/ee/routes/v1/sub-org-router.ts @@ -4,11 +4,11 @@ import { OrganizationsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApiDocsTags, SUB_ORGANIZATIONS } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { GenericResourceNameSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; -import { GenericResourceNameSchema } from "@app/server/lib/schemas"; -const sanitiziedSubOrganizationSchema = OrganizationsSchema.pick({ +const sanitizedSubOrganizationSchema = OrganizationsSchema.pick({ id: true, name: true, slug: true, @@ -26,7 +26,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => { schema: { hide: false, tags: [ApiDocsTags.SubOrganizations], - description: "Create a child organization", + description: "Create a sub organization", security: [ { bearerAuth: [] @@ -37,7 +37,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - organization: sanitiziedSubOrganizationSchema + organization: sanitizedSubOrganizationSchema }) } }, @@ -45,21 +45,14 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => { handler: async (req) => { const { organization } = await server.services.subOrganization.createSubOrg({ name: req.body.name, - permissionActor: { - id: req.permission.id, - type: req.permission.type, - authMethod: req.permission.authMethod, - orgId: req.permission.orgId, - parentOrgId: req.permission.parentOrgId, - rootOrgId: req.permission.rootOrgId - } + permissionActor: req.permission }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, event: { - type: EventType.CREATE_CHILD_ORGANIZATION, + type: EventType.CREATE_SUB_ORGANIZATION, metadata: { name: req.body.name, organizationId: organization.id @@ -80,7 +73,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => { schema: { hide: false, tags: [ApiDocsTags.SubOrganizations], - description: "List child organizations", + description: "List of sub organizations", security: [ { bearerAuth: [] @@ -97,21 +90,14 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - organizations: sanitiziedSubOrganizationSchema.array() + organizations: sanitizedSubOrganizationSchema.array() }) } }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { const { organizations } = await server.services.subOrganization.listSubOrgs({ - permissionActor: { - id: req.permission.id, - type: req.permission.type, - authMethod: req.permission.authMethod, - orgId: req.permission.orgId, - parentOrgId: req.permission.orgId, - rootOrgId: req.permission.rootOrgId - }, + permissionActor: req.permission, data: { limit: req.query.limit, offset: req.query.offset, diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index b5d49b7e4..a933485ae 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -173,7 +173,7 @@ export enum EventType { UPDATE_TOKEN_IDENTITY_TOKEN_AUTH = "update-token-identity-token-auth", GET_TOKENS_IDENTITY_TOKEN_AUTH = "get-tokens-identity-token-auth", - CREATE_CHILD_ORGANIZATION = "create-child-organization", + CREATE_SUB_ORGANIZATION = "create-child-organization", ADD_IDENTITY_TOKEN_AUTH = "add-identity-token-auth", UPDATE_IDENTITY_TOKEN_AUTH = "update-identity-token-auth", @@ -609,8 +609,8 @@ interface GetSecretsEvent { }; } -interface CreateChildOrganizationEvent { - type: EventType.CREATE_CHILD_ORGANIZATION; +interface CreateSubOrganizationEvent { + type: EventType.CREATE_SUB_ORGANIZATION; metadata: { name: string; organizationId: string; @@ -3873,7 +3873,7 @@ interface PamResourceDeleteEvent { } export type Event = - | CreateChildOrganizationEvent + | CreateSubOrganizationEvent | GetSecretsEvent | GetSecretEvent | CreateSecretEvent diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index ec2a21352..48b78d980 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -212,9 +212,9 @@ export const permissionServiceFactory = ({ const rootOrgId = permissionData?.[0]?.rootOrgId; const isChild = Boolean(rootOrgId); if (scope === OrganizationActionScope.ParentOrganization && isChild) { - throw new BadRequestError({ message: `Child organization cannot do this operation` }); + throw new ForbiddenRequestError({ message: `Child organization cannot do this operation` }); } else if (scope === OrganizationActionScope.ChildOrganization && !isChild) { - throw new BadRequestError({ message: `Parent organization cannot do this operation` }); + throw new ForbiddenRequestError({ message: `Parent organization cannot do this operation` }); } const permissionFromRoles = permissionData.flatMap((membership) => { diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 77ce47cd5..e42eb9eff 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -719,12 +719,12 @@ export const ORGANIZATIONS = { export const SUB_ORGANIZATIONS = { CREATE: { - name: "The name of the child organization to create." + name: "The name of the sub organization to create." }, LIST: { - limit: "The number of child organizations to return.", - offset: "The offset to start from. If you enter 10, it will start from the 10th child organization.", - isAccessible: "Filter to only return child organizations that the actor has access to." + limit: "The number of sub organizations to return.", + offset: "The offset to start from. If you enter 10, it will start from the 10th sub organization.", + isAccessible: "Filter to only return sub organizations that the actor has access to." } } as const; diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 9a959c5bc..2339d78be 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -8,10 +8,10 @@ import { TScimTokenJwtPayload } from "@app/ee/services/scim/scim-types"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; import { BadRequestError } from "@app/lib/errors"; +import { GenericResourceNameSchema } from "@app/server/lib/schemas"; import { ActorType, AuthMethod, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type"; import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-token/identity-access-token-types"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; -import { GenericResourceNameSchema } from "@app/server/lib/schemas"; export type TAuthMode = | { @@ -243,7 +243,7 @@ export const injectIdentity = fp( requestContext.set("orgId", orgId); if (subOrganizationSelector) - throw new BadRequestError({ message: `Service token doesn't support sub organization selector` }); + throw new BadRequestError({ message: `SCIM token doesn't support sub organization selector` }); req.auth = { authMode: AuthMode.SCIM_TOKEN, diff --git a/backend/src/services/identity-access-token/identity-access-token-service.ts b/backend/src/services/identity-access-token/identity-access-token-service.ts index bbefe923c..02660a0ae 100644 --- a/backend/src/services/identity-access-token/identity-access-token-service.ts +++ b/backend/src/services/identity-access-token/identity-access-token-service.ts @@ -216,7 +216,7 @@ export const identityAccessTokenServiceFactory = ({ if (subOrganizationSelector) { const subOrganization = await orgDAL.findOne({ rootOrgId, slug: subOrganizationSelector }); - if (!subOrganizationSelector) + if (!subOrganization) throw new BadRequestError({ message: `Sub organization ${subOrganizationSelector} not found` }); const identityOrgMembership = await membershipIdentityDAL.findOne({ 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 646dc72fc..c6f6f1376 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 @@ -24,9 +24,9 @@ import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; -import { TIdentityDALFactory } from "../identity/identity-dal"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; 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 d36ea64de..1814afb2e 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 @@ -23,9 +23,9 @@ import { import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; -import { TIdentityDALFactory } from "../identity/identity-dal"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; 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 a9fb6e703..f75aeba4f 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 @@ -20,9 +20,9 @@ import { import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; -import { TIdentityDALFactory } from "../identity/identity-dal"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; 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 1865e0fb8..67adb6c1e 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 @@ -20,9 +20,9 @@ import { import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; -import { TIdentityDALFactory } from "../identity/identity-dal"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index dfd7787ea..00ab1610d 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -23,6 +23,7 @@ import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from import { logger } from "@app/lib/logger"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; @@ -41,7 +42,6 @@ import { TRevokeUaDTO, TUpdateUaDTO } from "./identity-ua-types"; -import { TIdentityDALFactory } from "../identity/identity-dal"; type TIdentityUaServiceFactoryDep = { identityDAL: Pick; @@ -314,10 +314,6 @@ export const identityUaServiceFactory = ({ throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); } - if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { - throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); - } - if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } diff --git a/backend/src/services/membership-identity/membership-identity-dal.ts b/backend/src/services/membership-identity/membership-identity-dal.ts index 78df6a757..682bfef3e 100644 --- a/backend/src/services/membership-identity/membership-identity-dal.ts +++ b/backend/src/services/membership-identity/membership-identity-dal.ts @@ -356,7 +356,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => { } }; - // this right nwo only support sub organization + // this right now only support sub organization const listAvailableIdentities = async (orgId: string, rootOrgId: string) => { try { const usersConnectedToOrg = db @@ -381,7 +381,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => { return docs; } catch (error) { - throw new DatabaseError({ error, name: "ListAvailableUsers" }); + throw new DatabaseError({ error, name: "ListAvailableIdentities" }); } }; diff --git a/backend/src/services/membership-identity/org/org-membership-identity-factory.ts b/backend/src/services/membership-identity/org/org-membership-identity-factory.ts index 1ad77dfbd..8b3bdf6d5 100644 --- a/backend/src/services/membership-identity/org/org-membership-identity-factory.ts +++ b/backend/src/services/membership-identity/org/org-membership-identity-factory.ts @@ -57,7 +57,7 @@ export const newOrgMembershipIdentityFactory = ({ const identityDetails = await identityDAL.findById(dto.data.identityId); if (identityDetails.orgId !== dto.permission.rootOrgId) { - throw new BadRequestError({ message: "Only identites from parent organization can be invited" }); + throw new BadRequestError({ message: "Only identities from parent organization can be invited" }); } const permissionRoles = await permissionService.getOrgPermissionByRoles( @@ -143,11 +143,11 @@ export const newOrgMembershipIdentityFactory = ({ scope: OrganizationActionScope.ChildOrganization }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity); const identityDetails = await identityDAL.findById(dto.selector.identityId); if (identityDetails.orgId !== dto.permission.rootOrgId) { - throw new BadRequestError({ message: "Only identites from parent organization can do this operation" }); + throw new BadRequestError({ message: "Only identities from parent organization can do this operation" }); } if (identityDetails.orgId === dto.permission.orgId) { diff --git a/backend/src/services/membership-user/membership-user-dal.ts b/backend/src/services/membership-user/membership-user-dal.ts index 41fa09696..221228465 100644 --- a/backend/src/services/membership-user/membership-user-dal.ts +++ b/backend/src/services/membership-user/membership-user-dal.ts @@ -291,7 +291,7 @@ export const membershipUserDALFactory = (db: TDbClient) => { } }; - // this right nwo only support sub organization + // this right now only support sub organization const listAvailableUsers = async (orgId: string, rootOrgId: string) => { try { const usersConnectedToOrg = db diff --git a/backend/src/services/membership-user/org/org-membership-user-factory.ts b/backend/src/services/membership-user/org/org-membership-user-factory.ts index ca867286a..7aff05220 100644 --- a/backend/src/services/membership-user/org/org-membership-user-factory.ts +++ b/backend/src/services/membership-user/org/org-membership-user-factory.ts @@ -92,10 +92,15 @@ export const newOrgMembershipUserFactory = ({ }, scopeOrgId: org.rootOrgId }); - if (rootOrgMembership.length !== newMembers.length) + if (rootOrgMembership.length !== newMembers.length) { + const emails = newMembers + .filter((user) => !rootOrgMembership.find((i) => i.actorUserId === user.id)) + .map((el) => el.email) + .join(","); throw new BadRequestError({ - message: "User doesn't have membership in root organization" + message: `Users with email ${emails} doesn't have membership in root organization` }); + } } }; diff --git a/frontend/src/hooks/api/orgIdentityMembership/mutation.tsx b/frontend/src/hooks/api/orgIdentityMembership/mutation.tsx index cd5787842..3ba41905a 100644 --- a/frontend/src/hooks/api/orgIdentityMembership/mutation.tsx +++ b/frontend/src/hooks/api/orgIdentityMembership/mutation.tsx @@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { identitiesKeys } from "../identities"; import { TCreateOrgIdentityMembershipDTO, TDeleteOrgIdentityMembershipDTO, @@ -19,8 +20,7 @@ export const useCreateOrgIdentityMembership = () => { return data.identityMembership; }, onSuccess: () => { - // Invalidate relevant queries if needed - queryClient.invalidateQueries({ queryKey: ["organization"] }); + queryClient.invalidateQueries({ queryKey: identitiesKeys.searchIdentities({ search: {} }) }); } }); }; @@ -35,8 +35,7 @@ export const useDeleteOrgIdentityMembership = () => { return data.identityMembership; }, onSuccess: () => { - // Invalidate relevant queries if needed - queryClient.invalidateQueries({ queryKey: ["organization"] }); + queryClient.invalidateQueries({ queryKey: identitiesKeys.searchIdentities({ search: {} }) }); } }); }; diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx index 19a255ee9..09bf59794 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx @@ -344,7 +344,7 @@ export const Navbar = () => { size="xs" className="flex w-full items-center justify-start p-0 font-normal" leftIcon={ - currentOrg?.id === org.id && ( + currentOrg?.parentOrgId === org.id && ( { subTitle="Define a new sub-organization under your current organization." >
- setShowSubOrgForm(true)} /> + setShowSubOrgForm(false)} />
diff --git a/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx b/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx index 57f8d99f4..4e72f9b06 100644 --- a/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx @@ -3,8 +3,8 @@ import { motion } from "framer-motion"; import { CreateOrgModal } from "@app/components/organization/CreateOrgModal"; import { Tab, TabList, Tabs } from "@app/components/v2"; -import { usePopUp } from "@app/hooks"; import { useOrganization } from "@app/context"; +import { usePopUp } from "@app/hooks"; type Props = { isHidden?: boolean; 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 2f0551966..ceac67251 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 @@ -243,7 +243,7 @@ export const IdentitySection = withPermission( > 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 2705593e5..c4c1561e7 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 @@ -370,7 +370,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {

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

)} diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/route.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/route.tsx index ea24aa679..86dfe4530 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/route.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/route.tsx @@ -1,4 +1,5 @@ import { createFileRoute } from "@tanstack/react-router"; +import { z } from "zod"; import { AppConnectionsPage } from "./AppConnectionsPage"; @@ -6,6 +7,11 @@ export const Route = createFileRoute( "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/" )({ component: AppConnectionsPage, + validateSearch: z.object({ + error: z.string().optional(), + success: z.string().optional(), + connectionId: z.string().optional() + }), context: () => ({ breadcrumbs: [ { diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx index a9a6bf2f9..30429d48d 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx @@ -145,7 +145,7 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen, isOrgIdent
{isSubOrganization && (
-

Manage By

+

Managed By

{isOrgIdentity ? "Organization" : "Root Organization"}

diff --git a/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx b/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx index 109c02fbc..a34c08f9d 100644 --- a/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx +++ b/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx @@ -3,6 +3,7 @@ import { Controller, useForm } from "react-hook-form"; import { faCheck, faCopy, faRedo } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useSearch } from "@tanstack/react-router"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; @@ -22,7 +23,6 @@ import { import { useTimedReset } from "@app/hooks"; import { useCreatePublicSharedSecret, useCreateSharedSecret } from "@app/hooks/api"; import { SecretSharingAccessType } from "@app/hooks/api/secretSharing"; -import { useSearch } from "@tanstack/react-router"; // values in ms const expiresInOptions = [ From 0ecca6a312b15b3d4f028c1e35e5c3f54e74382b Mon Sep 17 00:00:00 2001 From: = Date: Mon, 20 Oct 2025 23:01:59 +0530 Subject: [PATCH 15/44] feat: added change in user invitation mail for suborg and updated list operation for sub org --- .../ee/services/sub-org/sub-org-service.ts | 4 +- .../org/org-membership-user-factory.ts | 73 +++++++++++-------- backend/src/services/org/org-service.ts | 10 +-- .../SubOrganizationInvitationTemplate.tsx | 50 +++++++++++++ backend/src/services/smtp/emails/index.ts | 1 + backend/src/services/smtp/smtp-service.ts | 5 +- .../OrganizationContext.tsx | 24 +++--- .../components/NavBar/Navbar.tsx | 4 +- .../OrgNameChangeSection.tsx | 28 ++++--- .../OrgProductSelectSection.tsx | 2 +- 10 files changed, 136 insertions(+), 65 deletions(-) create mode 100644 backend/src/services/smtp/emails/SubOrganizationInvitationTemplate.tsx diff --git a/backend/src/ee/services/sub-org/sub-org-service.ts b/backend/src/ee/services/sub-org/sub-org-service.ts index db5b89244..fca60bb9b 100644 --- a/backend/src/ee/services/sub-org/sub-org-service.ts +++ b/backend/src/ee/services/sub-org/sub-org-service.ts @@ -93,10 +93,10 @@ export const subOrgServiceFactory = ({ await permissionService.getOrgPermission({ actorId: permissionActor.id, actor: permissionActor.type, - orgId: permissionActor.parentOrgId, + orgId: permissionActor.rootOrgId, actorOrgId: permissionActor.rootOrgId, actorAuthMethod: permissionActor.authMethod, - scope: OrganizationActionScope.ParentOrganization + scope: OrganizationActionScope.Any }); const organizations = await orgDAL.listSubOrganizations({ diff --git a/backend/src/services/membership-user/org/org-membership-user-factory.ts b/backend/src/services/membership-user/org/org-membership-user-factory.ts index 7aff05220..2da263426 100644 --- a/backend/src/services/membership-user/org/org-membership-user-factory.ts +++ b/backend/src/services/membership-user/org/org-membership-user-factory.ts @@ -84,6 +84,7 @@ export const newOrgMembershipUserFactory = ({ message: "Failed to invite user due to org-level auth enforced for organization" }); } + if (org.rootOrgId) { const rootOrgMembership = await membershipUserDAL.find({ scope: AccessScope.Organization, @@ -120,40 +121,52 @@ export const newOrgMembershipUserFactory = ({ const signUpTokens: { email: string; link: string }[] = []; const orgDetails = await orgDAL.findById(dto.permission.orgId); + if (orgDetails.rootOrgId) { + const emails = newUsers.map((el) => el.email).filter(Boolean); + await smtpService.sendMail({ + template: SmtpTemplates.SubOrgInvite, + subjectLine: "Infisical sub-organization invitation", + recipients: emails as string[], + substitutions: { + subOrganizationName: orgDetails.slug, + callback_url: `${appCfg.SITE_URL}/organization/projects?${orgDetails.slug}` + } + }); + } else { + await Promise.allSettled( + newUsers.map(async (el) => { + const token = await tokenService.createTokenForUser({ + type: TokenType.TOKEN_EMAIL_ORG_INVITATION, + userId: el.id, + orgId: dto.permission.orgId + }); - await Promise.allSettled( - newUsers.map(async (el) => { - const token = await tokenService.createTokenForUser({ - type: TokenType.TOKEN_EMAIL_ORG_INVITATION, - userId: el.id, - orgId: dto.permission.orgId - }); + if (el.email) { + if (!appCfg.isSmtpConfigured) { + signUpTokens.push({ + email: el.email, + link: `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${el.email}&organization_id=${dto.permission.orgId}` + }); + } - if (el.email) { - if (!appCfg.isSmtpConfigured) { - signUpTokens.push({ - email: el.email, - link: `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${el.email}&organization_id=${dto.permission.orgId}` + await smtpService.sendMail({ + template: SmtpTemplates.OrgInvite, + subjectLine: "Infisical organization invitation", + recipients: [el.email], + substitutions: { + inviterFirstName: actorDetails?.firstName, + inviterUsername: actorDetails?.email, + organizationName: orgDetails?.name, + email: el.email, + organizationId: orgDetails?.id.toString(), + token, + callback_url: `${appCfg.SITE_URL}/signupinvite` + } }); } - - await smtpService.sendMail({ - template: SmtpTemplates.OrgInvite, - subjectLine: "Infisical organization invitation", - recipients: [el.email], - substitutions: { - inviterFirstName: actorDetails?.firstName, - inviterUsername: actorDetails?.email, - organizationName: orgDetails?.name, - email: el.email, - organizationId: orgDetails?.id.toString(), - token, - callback_url: `${appCfg.SITE_URL}/signupinvite` - } - }); - } - }) - ); + }) + ); + } return { signUpTokens }; }; diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 76c1fb801..6334322eb 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -346,7 +346,7 @@ export const orgServiceFactory = ({ orgId, actorAuthMethod, actorOrgId, - scope: OrganizationActionScope.Any + scope: OrganizationActionScope.ParentOrganization }); if (!hasRole(OrgMembershipRole.Admin)) { @@ -418,7 +418,7 @@ export const orgServiceFactory = ({ orgId, actorAuthMethod, actorOrgId, - scope: OrganizationActionScope.Any + scope: OrganizationActionScope.ParentOrganization }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); @@ -878,7 +878,7 @@ export const orgServiceFactory = ({ orgId, actorAuthMethod, actorOrgId, - scope: OrganizationActionScope.Any + scope: OrganizationActionScope.ParentOrganization }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member); @@ -1172,7 +1172,7 @@ export const orgServiceFactory = ({ orgId, actorAuthMethod, actorOrgId, - scope: OrganizationActionScope.Any + scope: OrganizationActionScope.ParentOrganization }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.IncidentAccount); const doesIncidentContactExist = await incidentContactDAL.findOne(orgId, { email }); @@ -1200,7 +1200,7 @@ export const orgServiceFactory = ({ orgId, actorAuthMethod, actorOrgId, - scope: OrganizationActionScope.Any + scope: OrganizationActionScope.ParentOrganization }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.IncidentAccount); diff --git a/backend/src/services/smtp/emails/SubOrganizationInvitationTemplate.tsx b/backend/src/services/smtp/emails/SubOrganizationInvitationTemplate.tsx new file mode 100644 index 000000000..e0b347dae --- /dev/null +++ b/backend/src/services/smtp/emails/SubOrganizationInvitationTemplate.tsx @@ -0,0 +1,50 @@ +import { Heading, Section, Text } from "@react-email/components"; +import React from "react"; + +import { BaseButton } from "./BaseButton"; +import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; + +interface SubOrganizationInvitationTemplateProps extends Omit { + callback_url: string; + subOrganizationName: string; +} + +export const SubOrganizationInvitationTemplate = ({ + callback_url, + subOrganizationName, + siteUrl +}: SubOrganizationInvitationTemplateProps) => { + return ( + + + You've been invited to join a suborganization on Infisical + +
+ + You've been invited to join the suborganization {subOrganizationName}. + +
+
+ Join Suborganization +
+
+ + About Infisical: Infisical is an all-in-one platform to securely manage application secrets, + certificates, SSH keys, and configurations across your team and infrastructure. + +
+
+ ); +}; + +export default SubOrganizationInvitationTemplate; + +SubOrganizationInvitationTemplate.PreviewProps = { + subOrganizationName: "Example Project", + siteUrl: "https://infisical.com", + callback_url: "https://app.infisical.com" +} as SubOrganizationInvitationTemplateProps; diff --git a/backend/src/services/smtp/emails/index.ts b/backend/src/services/smtp/emails/index.ts index 06ac31ab6..78e415832 100644 --- a/backend/src/services/smtp/emails/index.ts +++ b/backend/src/services/smtp/emails/index.ts @@ -32,3 +32,4 @@ export * from "./SecretSyncFailedTemplate"; export * from "./ServiceTokenExpiryNoticeTemplate"; export * from "./SignupEmailVerificationTemplate"; export * from "./UnlockAccountTemplate"; +export * from "./SubOrganizationInvitationTemplate"; diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index 652f56567..e5f83f66c 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -40,7 +40,8 @@ import { SecretSyncFailedTemplate, ServiceTokenExpiryNoticeTemplate, SignupEmailVerificationTemplate, - UnlockAccountTemplate + UnlockAccountTemplate, + SubOrganizationInvitationTemplate } from "./emails"; export type TSmtpConfig = SMTPTransport.Options; @@ -65,6 +66,7 @@ export enum SmtpTemplates { // HistoricalSecretList = "historicalSecretLeakIncident", not used anymore? NewDeviceJoin = "newDevice", OrgInvite = "organizationInvitation", + SubOrgInvite = "subOrganizationInvitation", OrgAssignment = "organizationAssignment", OAuthPasswordReset = "oAuthPasswordReset", ResetPassword = "passwordReset", @@ -102,6 +104,7 @@ export enum SmtpHost { // eslint-disable-next-line @typescript-eslint/no-explicit-any const EmailTemplateMap: Record> = { [SmtpTemplates.OrgInvite]: OrganizationInvitationTemplate, + [SmtpTemplates.SubOrgInvite]: SubOrganizationInvitationTemplate, [SmtpTemplates.OrgAssignment]: OrganizationAssignmentTemplate, [SmtpTemplates.NewDeviceJoin]: NewDeviceLoginTemplate, [SmtpTemplates.SignupEmailVerification]: SignupEmailVerificationTemplate, diff --git a/frontend/src/context/OrganizationContext/OrganizationContext.tsx b/frontend/src/context/OrganizationContext/OrganizationContext.tsx index 79c004e1f..b657a827d 100644 --- a/frontend/src/context/OrganizationContext/OrganizationContext.tsx +++ b/frontend/src/context/OrganizationContext/OrganizationContext.tsx @@ -2,6 +2,7 @@ import { useSuspenseQuery } from "@tanstack/react-query"; import { useRouteContext, useSearch } from "@tanstack/react-router"; import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries"; +import { useMemo } from "react"; export const useOrganization = () => { const organizationId = useRouteContext({ @@ -20,13 +21,18 @@ export const useOrganization = () => { staleTime: Infinity }); - return { - currentOrg: { - ...currentOrg, - id: currentOrg?.subOrganization?.id || currentOrg?.id, - parentOrgId: currentOrg.id - }, - isSubOrganization: Boolean(currentOrg.subOrganization), - isRootOrganization: !currentOrg.subOrganization - }; + const org = useMemo( + () => ({ + currentOrg: { + ...currentOrg, + id: currentOrg?.subOrganization?.id || currentOrg?.id, + parentOrgId: currentOrg.id + }, + isSubOrganization: Boolean(currentOrg.subOrganization), + isRootOrganization: !currentOrg.subOrganization + }), + [currentOrg] + ); + + return org; }; diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx index 09bf59794..0bc4439ae 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx @@ -131,14 +131,14 @@ export const INFISICAL_SUPPORT_OPTIONS = [ export const Navbar = () => { const { user } = useUser(); const { subscription } = useSubscription(); - const { currentOrg, isSubOrganization } = useOrganization(); + const { currentOrg } = useOrganization(); const [showAdminsModal, setShowAdminsModal] = useState(false); const [showSubOrgForm, setShowSubOrgForm] = useState(false); const [showCardDeclinedModal, setShowCardDeclinedModal] = useState(false); const { data: subOrganizations = [] } = useQuery({ ...subOrganizationsQuery.list({ limit: 500 }), - enabled: Boolean(subscription.subOrganization) && !isSubOrganization + enabled: Boolean(subscription.subOrganization) }); useEffect(() => { diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx index 9b6323c2e..766a22158 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx @@ -41,21 +41,19 @@ export const OrgNameChangeSection = (): JSX.Element => { const [isFormInitialized, setIsFormInitialized] = useState(false); useEffect(() => { - if (currentOrg) { - reset({ - name: currentOrg.name, - slug: currentOrg.slug, - ...(canReadOrgRoles && - roles?.length && { - // will always be present, can't remove role if default - defaultMembershipRole: isCustomOrgRole(currentOrg.defaultMembershipRole) - ? roles?.find((role) => currentOrg.defaultMembershipRole === role.id)?.slug || "" - : currentOrg.defaultMembershipRole - }) - }); - setIsFormInitialized(true); - } - }, [currentOrg, roles]); + reset({ + name: currentOrg.name, + slug: currentOrg.slug, + ...(canReadOrgRoles && + roles?.length && { + // will always be present, can't remove role if default + defaultMembershipRole: isCustomOrgRole(currentOrg.defaultMembershipRole) + ? roles?.find((role) => currentOrg.defaultMembershipRole === role.id)?.slug || "" + : currentOrg.defaultMembershipRole + }) + }); + setIsFormInitialized(true); + }, [roles]); const onFormSubmit = async ({ name, slug, defaultMembershipRole }: FormData) => { try { diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx index 61ee0c6e9..5be15edad 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx @@ -50,7 +50,7 @@ export const OrgProductSelectSection = () => { })); } }); - }, [currentOrg]); + }, [currentOrg?.id]); const onProductToggle = async (value: boolean, key: string) => { setIsLoading(true); From 9db8f2a87d09c37aa069ed4a3fbcc87ffc9960d8 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 21 Oct 2025 00:10:57 +0530 Subject: [PATCH 16/44] feat: updated settings page --- backend/src/ee/routes/v1/sub-org-router.ts | 55 +++++++++- .../ee/services/audit-log/audit-log-types.ts | 12 ++- .../ee/services/sub-org/sub-org-service.ts | 52 ++++++++- .../src/ee/services/sub-org/sub-org-types.ts | 6 ++ backend/src/lib/api-docs/constants.ts | 4 + .../server/plugins/auth/inject-identity.ts | 4 +- backend/src/services/smtp/emails/index.ts | 2 +- backend/src/services/smtp/smtp-service.ts | 4 +- .../OrganizationContext.tsx | 2 +- .../src/hooks/api/subOrganizations/index.tsx | 5 +- .../hooks/api/subOrganizations/mutations.tsx | 18 +++- .../src/hooks/api/subOrganizations/types.ts | 5 + .../ProjectsPage/components/MyProjectView.tsx | 2 +- .../OrgGeneralTab/OrgGeneralTab.tsx | 7 +- .../SubOrgNameChangeSection.tsx | 101 ++++++++++++++++++ .../components/OrgNameChangeSection/index.tsx | 1 + 16 files changed, 260 insertions(+), 20 deletions(-) create mode 100644 frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/SubOrgNameChangeSection.tsx diff --git a/backend/src/ee/routes/v1/sub-org-router.ts b/backend/src/ee/routes/v1/sub-org-router.ts index 185425cea..c89fa40a3 100644 --- a/backend/src/ee/routes/v1/sub-org-router.ts +++ b/backend/src/ee/routes/v1/sub-org-router.ts @@ -4,7 +4,7 @@ import { OrganizationsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApiDocsTags, SUB_ORGANIZATIONS } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; -import { GenericResourceNameSchema } from "@app/server/lib/schemas"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -33,7 +33,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => { } ], body: z.object({ - name: GenericResourceNameSchema.describe(SUB_ORGANIZATIONS.CREATE.name) + name: slugSchema().describe(SUB_ORGANIZATIONS.CREATE.name) }), response: { 200: z.object({ @@ -108,4 +108,55 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => { return { organizations }; } }); + + server.route({ + method: "PATCH", + url: "/:subOrgId", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SubOrganizations], + description: "Update a sub organization", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + subOrgId: z.string().trim().describe(SUB_ORGANIZATIONS.UPDATE.subOrgId) + }), + body: z.object({ + name: slugSchema().describe(SUB_ORGANIZATIONS.UPDATE.name) + }), + response: { + 200: z.object({ + organization: sanitizedSubOrganizationSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { organization } = await server.services.subOrganization.updateSubOrg({ + subOrgId: req.params.subOrgId, + name: req.body.name, + permissionActor: req.permission + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.UPDATE_SUB_ORGANIZATION, + metadata: { + name: req.body.name, + organizationId: organization.id + } + } + }); + + return { organization }; + } + }); }; diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index a933485ae..620e7b8fb 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -173,7 +173,8 @@ export enum EventType { UPDATE_TOKEN_IDENTITY_TOKEN_AUTH = "update-token-identity-token-auth", GET_TOKENS_IDENTITY_TOKEN_AUTH = "get-tokens-identity-token-auth", - CREATE_SUB_ORGANIZATION = "create-child-organization", + CREATE_SUB_ORGANIZATION = "create-sub-organization", + UPDATE_SUB_ORGANIZATION = "update-sub-organization", ADD_IDENTITY_TOKEN_AUTH = "add-identity-token-auth", UPDATE_IDENTITY_TOKEN_AUTH = "update-identity-token-auth", @@ -617,6 +618,14 @@ interface CreateSubOrganizationEvent { }; } +interface UpdateSubOrganizationEvent { + type: EventType.UPDATE_SUB_ORGANIZATION; + metadata: { + name: string; + organizationId: string; + }; +} + type TSecretMetadata = { key: string; value: string }[]; interface GetSecretEvent { @@ -3874,6 +3883,7 @@ interface PamResourceDeleteEvent { export type Event = | CreateSubOrganizationEvent + | UpdateSubOrganizationEvent | GetSecretsEvent | GetSecretEvent | CreateSecretEvent diff --git a/backend/src/ee/services/sub-org/sub-org-service.ts b/backend/src/ee/services/sub-org/sub-org-service.ts index fca60bb9b..d49a2036f 100644 --- a/backend/src/ee/services/sub-org/sub-org-service.ts +++ b/backend/src/ee/services/sub-org/sub-org-service.ts @@ -8,12 +8,19 @@ import { TMembershipRoleDALFactory } from "@app/services/membership/membership-r import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TLicenseServiceFactory } from "../license/license-service"; -import { OrgPermissionChildOrgActions, OrgPermissionSubjects } from "../permission/org-permission"; +import { + OrgPermissionActions, + OrgPermissionChildOrgActions, + OrgPermissionSubjects +} from "../permission/org-permission"; import { TPermissionServiceFactory } from "../permission/permission-service-types"; -import { TCreateSubOrgDTO, TListSubOrgDTO } from "./sub-org-types"; +import { TCreateSubOrgDTO, TListSubOrgDTO, TUpdateSubOrgDTO } from "./sub-org-types"; type TSubOrgServiceFactoryDep = { - orgDAL: Pick; + orgDAL: Pick< + TOrgDALFactory, + "findOne" | "create" | "transaction" | "listSubOrganizations" | "updateById" | "findById" + >; permissionService: Pick; licenseService: Pick; membershipDAL: Pick; @@ -113,8 +120,45 @@ export const subOrgServiceFactory = ({ }; }; + const updateSubOrg = async ({ subOrgId, name, permissionActor }: TUpdateSubOrgDTO) => { + const subOrg = await orgDAL.findOne({ + rootOrgId: permissionActor.rootOrgId, + id: subOrgId + }); + if (!subOrg) { + throw new BadRequestError({ message: "Sub-organization not found" }); + } + + const { permission } = await permissionService.getOrgPermission({ + actorId: permissionActor.id, + actor: permissionActor.type, + orgId: subOrgId, + actorOrgId: subOrgId, + actorAuthMethod: permissionActor.authMethod, + scope: OrganizationActionScope.ChildOrganization + }); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); + + const existingSubOrg = await orgDAL.findOne({ + parentOrgId: subOrg.parentOrgId, + slug: name + }); + + if (existingSubOrg && existingSubOrg.id !== subOrgId) { + throw new BadRequestError({ message: `Sub-organization with name ${name} already exists` }); + } + + const organization = await orgDAL.updateById(subOrgId, { name, slug: name }); + + return { + organization + }; + }; + return { createSubOrg, - listSubOrgs + listSubOrgs, + updateSubOrg }; }; diff --git a/backend/src/ee/services/sub-org/sub-org-types.ts b/backend/src/ee/services/sub-org/sub-org-types.ts index fc2a47b59..a1af9878e 100644 --- a/backend/src/ee/services/sub-org/sub-org-types.ts +++ b/backend/src/ee/services/sub-org/sub-org-types.ts @@ -14,3 +14,9 @@ export type TListSubOrgDTO = { isAccessible?: boolean; }>; }; + +export type TUpdateSubOrgDTO = { + subOrgId: string; + name: string; + permissionActor: OrgServiceActor; +}; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index e42eb9eff..19cf463f5 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -721,6 +721,10 @@ export const SUB_ORGANIZATIONS = { CREATE: { name: "The name of the sub organization to create." }, + UPDATE: { + name: "The name of the sub organization to update.", + subOrgId: "The id of the sub organization to update." + }, LIST: { limit: "The number of sub organizations to return.", offset: "The offset to start from. If you enter 10, it will start from the 10th sub organization.", diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 2339d78be..b33f2fbe6 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -8,7 +8,7 @@ import { TScimTokenJwtPayload } from "@app/ee/services/scim/scim-types"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; import { BadRequestError } from "@app/lib/errors"; -import { GenericResourceNameSchema } from "@app/server/lib/schemas"; +import { slugSchema } from "@app/server/lib/schemas"; import { ActorType, AuthMethod, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type"; import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-token/identity-access-token-types"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; @@ -149,7 +149,7 @@ export const injectIdentity = fp( const subOrganizationSelector = req.headers?.["x-infisical-org"] as string | undefined; if (subOrganizationSelector) { - await GenericResourceNameSchema.parseAsync(subOrganizationSelector); + await slugSchema().parseAsync(subOrganizationSelector); } switch (authMode) { diff --git a/backend/src/services/smtp/emails/index.ts b/backend/src/services/smtp/emails/index.ts index 78e415832..692cacbaf 100644 --- a/backend/src/services/smtp/emails/index.ts +++ b/backend/src/services/smtp/emails/index.ts @@ -31,5 +31,5 @@ export * from "./SecretScanningSecretsDetectedTemplate"; export * from "./SecretSyncFailedTemplate"; export * from "./ServiceTokenExpiryNoticeTemplate"; export * from "./SignupEmailVerificationTemplate"; -export * from "./UnlockAccountTemplate"; export * from "./SubOrganizationInvitationTemplate"; +export * from "./UnlockAccountTemplate"; diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index e5f83f66c..cef22009a 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -40,8 +40,8 @@ import { SecretSyncFailedTemplate, ServiceTokenExpiryNoticeTemplate, SignupEmailVerificationTemplate, - UnlockAccountTemplate, - SubOrganizationInvitationTemplate + SubOrganizationInvitationTemplate, + UnlockAccountTemplate } from "./emails"; export type TSmtpConfig = SMTPTransport.Options; diff --git a/frontend/src/context/OrganizationContext/OrganizationContext.tsx b/frontend/src/context/OrganizationContext/OrganizationContext.tsx index b657a827d..07216b6d7 100644 --- a/frontend/src/context/OrganizationContext/OrganizationContext.tsx +++ b/frontend/src/context/OrganizationContext/OrganizationContext.tsx @@ -1,8 +1,8 @@ +import { useMemo } from "react"; import { useSuspenseQuery } from "@tanstack/react-query"; import { useRouteContext, useSearch } from "@tanstack/react-router"; import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries"; -import { useMemo } from "react"; export const useOrganization = () => { const organizationId = useRouteContext({ diff --git a/frontend/src/hooks/api/subOrganizations/index.tsx b/frontend/src/hooks/api/subOrganizations/index.tsx index 85095fcc6..480377464 100644 --- a/frontend/src/hooks/api/subOrganizations/index.tsx +++ b/frontend/src/hooks/api/subOrganizations/index.tsx @@ -1,7 +1,8 @@ -export { useCreateSubOrganization } from "./mutations"; +export { useCreateSubOrganization, useUpdateSubOrganization } from "./mutations"; export { subOrganizationsQuery } from "./queries"; export type { TCreateSubOrganizationDTO, TListSubOrganizationsDTO, - TSubOrganization + TSubOrganization, + TUpdateSubOrganizationDTO } from "./types"; diff --git a/frontend/src/hooks/api/subOrganizations/mutations.tsx b/frontend/src/hooks/api/subOrganizations/mutations.tsx index 828aea6f4..f2b9ac7a8 100644 --- a/frontend/src/hooks/api/subOrganizations/mutations.tsx +++ b/frontend/src/hooks/api/subOrganizations/mutations.tsx @@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { subOrganizationsQuery } from "./queries"; -import { TCreateSubOrganizationDTO, TSubOrganization } from "./types"; +import { TCreateSubOrganizationDTO, TSubOrganization, TUpdateSubOrganizationDTO } from "./types"; export const useCreateSubOrganization = () => { const queryClient = useQueryClient(); @@ -20,3 +20,19 @@ export const useCreateSubOrganization = () => { } }); }; + +export const useUpdateSubOrganization = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ subOrgId, name }: TUpdateSubOrganizationDTO) => { + const { data } = await apiRequest.patch<{ organization: TSubOrganization }>( + `/api/v1/sub-organizations/${subOrgId}`, + { name } + ); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: subOrganizationsQuery.allKey() }); + } + }); +}; diff --git a/frontend/src/hooks/api/subOrganizations/types.ts b/frontend/src/hooks/api/subOrganizations/types.ts index e6fa39e1e..6086830fb 100644 --- a/frontend/src/hooks/api/subOrganizations/types.ts +++ b/frontend/src/hooks/api/subOrganizations/types.ts @@ -15,3 +15,8 @@ export type TListSubOrganizationsDTO = { offset?: number; isAccessible?: boolean; }; + +export type TUpdateSubOrganizationDTO = { + subOrgId: string; + name: string; +}; diff --git a/frontend/src/pages/organization/ProjectsPage/components/MyProjectView.tsx b/frontend/src/pages/organization/ProjectsPage/components/MyProjectView.tsx index 020947f28..a86023b6c 100644 --- a/frontend/src/pages/organization/ProjectsPage/components/MyProjectView.tsx +++ b/frontend/src/pages/organization/ProjectsPage/components/MyProjectView.tsx @@ -378,7 +378,7 @@ export const MyProjectView = ({
{ const { hasOrgRole } = useOrgPermission(); + const { isSubOrganization } = useOrganization(); return (
- + {isSubOrganization ? : } {hasOrgRole(OrgMembershipRole.Admin) && }
diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/SubOrgNameChangeSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/SubOrgNameChangeSection.tsx new file mode 100644 index 000000000..38eb5f012 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/SubOrgNameChangeSection.tsx @@ -0,0 +1,101 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQueryClient } from "@tanstack/react-query"; +import { useNavigate, useRouter } from "@tanstack/react-router"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { Button, FormControl, Input } from "@app/components/v2"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + useOrganization, + useOrgPermission +} from "@app/context"; +import { useUpdateSubOrganization } from "@app/hooks/api"; + +const formSchema = z.object({ + name: z + .string() + .regex(/^[a-zA-Z0-9-]+$/, "Name must only contain alphanumeric characters or hyphens") +}); + +type FormData = z.infer; + +export const SubOrgNameChangeSection = (): JSX.Element => { + const { currentOrg } = useOrganization(); + const { permission } = useOrgPermission(); + const navigate = useNavigate(); + const router = useRouter(); + const queryClient = useQueryClient(); + + const { handleSubmit, control } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + name: currentOrg?.subOrganization?.name || "" + } + }); + const { mutateAsync, isPending } = useUpdateSubOrganization(); + + const onFormSubmit = async ({ name }: FormData) => { + try { + await mutateAsync({ + name, + subOrgId: currentOrg.id + }); + + navigate({ to: "/organization/settings", search: { subOrganization: name } }); + queryClient.clear(); + await router.invalidate({ sync: true }); + createNotification({ + text: "Successfully updated sub-organization details", + type: "success" + }); + } catch (error) { + console.error(error); + createNotification({ + text: "Failed to update sub-organization details", + type: "error" + }); + } + }; + + return ( + +
+

Organization Name

+ ( + + + + )} + control={control} + name="name" + /> +
+ + {(isAllowed) => ( + + )} + + + ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/index.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/index.tsx index 4d86fcddb..70fe29455 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/index.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/index.tsx @@ -1 +1,2 @@ export { OrgNameChangeSection } from "./OrgNameChangeSection"; +export { SubOrgNameChangeSection } from "./SubOrgNameChangeSection"; From 45a7b4925ba4efd4fe912b1d5c043e07c683c7ed Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 20 Oct 2025 11:53:20 -0700 Subject: [PATCH 17/44] additions: sub-org ui updates --- backend/src/ee/routes/v1/sub-org-router.ts | 3 +- .../components/v2/PageHeader/PageHeader.tsx | 2 +- frontend/src/config/request.ts | 13 +- .../hooks/api/subOrganizations/mutations.tsx | 5 +- .../src/hooks/api/subOrganizations/types.ts | 1 + .../components/NavBar/Navbar.tsx | 377 +++++++++++------- .../NavBar/NewSubOrganizationForm.tsx | 15 +- .../components/OrgNavBar/OrgNavBar.tsx | 18 +- .../AccessManagementPage.tsx | 10 +- .../OrgRoleTabSection/OrgRoleTable.tsx | 6 +- .../AppConnectionsPage/AppConnectionsPage.tsx | 5 +- .../AuditLogsPage/AuditLogsPage.tsx | 5 +- .../GroupDetailsByIDPage.tsx | 10 +- .../IdentityDetailsByIDPage.tsx | 8 +- .../NetworkingPage/NetworkingPage.tsx | 5 +- .../NetworkingTabGroup/NetworkingTabGroup.tsx | 5 +- .../ProjectsPage/ProjectsPage.tsx | 6 +- .../RoleByIDPage/RoleByIDPage.tsx | 4 +- .../SecretSharingPage/SecretSharingPage.tsx | 5 +- .../SecretSharingPage/ShareSecretSection.tsx | 8 +- .../SecretSharingSettingsPage.tsx | 7 +- .../SettingsPage/SettingsPage.tsx | 4 +- .../components/OrgTabGroup/OrgTabGroup.tsx | 2 +- .../UserDetailsByIDPage.tsx | 6 +- .../src/pages/public/ErrorPage/ErrorPage.tsx | 2 +- 25 files changed, 342 insertions(+), 190 deletions(-) diff --git a/backend/src/ee/routes/v1/sub-org-router.ts b/backend/src/ee/routes/v1/sub-org-router.ts index c89fa40a3..200130488 100644 --- a/backend/src/ee/routes/v1/sub-org-router.ts +++ b/backend/src/ee/routes/v1/sub-org-router.ts @@ -13,7 +13,8 @@ const sanitizedSubOrganizationSchema = OrganizationsSchema.pick({ name: true, slug: true, createdAt: true, - updatedAt: true + updatedAt: true, + parentOrgId: true }); export const registerSubOrgRouter = async (server: FastifyZodProvider) => { diff --git a/frontend/src/components/v2/PageHeader/PageHeader.tsx b/frontend/src/components/v2/PageHeader/PageHeader.tsx index 3c9e743ff..e3f72f61b 100644 --- a/frontend/src/components/v2/PageHeader/PageHeader.tsx +++ b/frontend/src/components/v2/PageHeader/PageHeader.tsx @@ -24,7 +24,7 @@ const SCOPE_NAME: Record, { label: string; icon: Ico [ProjectType.KMS]: { label: "Project", icon: faCube }, [ProjectType.PAM]: { label: "Project", icon: faCube }, [ProjectType.SecretScanning]: { label: "Project", icon: faCube }, - namespace: { label: "Namespace", icon: faCubes }, + namespace: { label: "Sub-Organization", icon: faCubes }, instance: { label: "Server", icon: faServer } }; diff --git a/frontend/src/config/request.ts b/frontend/src/config/request.ts index 3fc01ccac..16b8b4f45 100644 --- a/frontend/src/config/request.ts +++ b/frontend/src/config/request.ts @@ -40,9 +40,16 @@ apiRequest.interceptors.request.use((config) => { // eslint-disable-next-line no-param-reassign config.headers.Authorization = `Bearer ${providerAuthToken}`; } - const subOrganization = params.get("subOrganization"); - if (subOrganization) { - config.headers.set("x-infisical-org", subOrganization); + + const rootOrgHeader = config.headers.get("x-root-org"); + + if (rootOrgHeader) { + config.headers.delete("x-root-org"); + } else { + const subOrganization = params.get("subOrganization"); + if (subOrganization) { + config.headers.set("x-infisical-org", subOrganization); + } } } diff --git a/frontend/src/hooks/api/subOrganizations/mutations.tsx b/frontend/src/hooks/api/subOrganizations/mutations.tsx index f2b9ac7a8..81369a62e 100644 --- a/frontend/src/hooks/api/subOrganizations/mutations.tsx +++ b/frontend/src/hooks/api/subOrganizations/mutations.tsx @@ -11,7 +11,10 @@ export const useCreateSubOrganization = () => { mutationFn: async (dto: TCreateSubOrganizationDTO) => { const { data } = await apiRequest.post<{ organization: TSubOrganization }>( "/api/v1/sub-organizations", - dto + dto, + { + headers: { "x-root-org": "discard" } // akhi/scott: this just tells the request to use the root org ID header + } ); return data; }, diff --git a/frontend/src/hooks/api/subOrganizations/types.ts b/frontend/src/hooks/api/subOrganizations/types.ts index 6086830fb..e9b3f2f01 100644 --- a/frontend/src/hooks/api/subOrganizations/types.ts +++ b/frontend/src/hooks/api/subOrganizations/types.ts @@ -4,6 +4,7 @@ export type TSubOrganization = { slug: string; createdAt: string; updatedAt: string; + parentOrgId: string; }; export type TCreateSubOrganizationDTO = { diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx index 0bc4439ae..ec0ba3737 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx @@ -6,6 +6,7 @@ import { faBook, faCaretDown, faCheck, + faChevronRight, faCubes, faEnvelope, faExclamationTriangle, @@ -59,7 +60,7 @@ import { import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries"; import { MfaMethod } from "@app/hooks/api/auth/types"; import { getAuthToken } from "@app/hooks/api/reactQuery"; -import { SubscriptionPlan } from "@app/hooks/api/types"; +import { Organization, SubscriptionPlan } from "@app/hooks/api/types"; import { AuthMethod } from "@app/hooks/api/users/types"; import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils"; @@ -131,7 +132,9 @@ export const INFISICAL_SUPPORT_OPTIONS = [ export const Navbar = () => { const { user } = useUser(); const { subscription } = useSubscription(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); + + console.log("current", currentOrg, isSubOrganization); const [showAdminsModal, setShowAdminsModal] = useState(false); const [showSubOrgForm, setShowSubOrgForm] = useState(false); @@ -155,6 +158,7 @@ export const Navbar = () => { const [shouldShowMfa, toggleShowMfa] = useToggle(false); const router = useRouter(); const queryClient = useQueryClient(); + const [isOrgSelectOpen, setIsOrgSelectOpen] = useState(false); const location = useLocation(); const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context }); @@ -224,6 +228,33 @@ export const Navbar = () => { const isOrgScope = location.pathname.startsWith("/organization"); // TODO: scott/akhil is this adequate? + const handleOrgNav = async (org: Organization) => { + if (currentOrg?.id === org.id) return; + + if (org.authEnforced) { + // org has an org-level auth method enabled (e.g. SAML) + // -> logout + redirect to SAML SSO + + await logout.mutateAsync(); + if (org.orgAuthMethod === AuthMethod.OIDC) { + window.open(`/api/v1/sso/oidc/login?orgSlug=${org.slug}`); + } else { + window.open(`/api/v1/sso/redirect/saml2/organizations/${org.slug}`); + } + window.close(); + return; + } + + if (org.googleSsoAuthEnforced) { + await logout.mutateAsync(); + window.open(`/api/v1/sso/redirect/google?org_slug=${org.slug}`); + window.close(); + return; + } + + handleOrgChange(org?.id); + }; + return (
@@ -253,38 +284,46 @@ export const Navbar = () => { ) : ( <>
- - -
- - -

{currentOrg?.name}

-
-
- {getPlan(subscription)} -
- {subscription.cardDeclined && ( - -
- -
-
+ +
+ { + navigate({ + to: "/organization/projects", + search: (search) => ({ ...search, subOrganization: undefined }) + }); + if (isSubOrganization) { + queryClient.clear(); + await router.invalidate({ sync: true }).catch(() => null); + } + }} + variant="org" + className={twMerge( + "max-w-full min-w-0 cursor-pointer text-sm", + (!isOrgScope || isSubOrganization) && + "bg-transparent text-mineshaft-200 hover:bg-transparent hover:underline" )} + > + +

{currentOrg?.name}

+
+
+ {getPlan(subscription)}
- + {subscription.cardDeclined && ( + +
+ +
+
+ )} +
{
- {subscription?.subOrganization && ( - <> - - - - - - } - onClick={() => setShowSubOrgForm(true)} - > - New Sub Organization - - {Boolean(subOrganizations.length) && ( -
- )} - {subOrganizations?.map((org) => { - return ( - - - - ); - })} - - -
- - )}
- organizations + Organizations
{orgs?.map((org) => { + if ( + subscription.subOrganization && + (org.id === currentOrg?.id || org.id === currentOrg?.parentOrgId) + ) { + return ( + + { + setIsOrgSelectOpen(false); + handleOrgNav(org); + }} + className="cursor-pointer font-normal" + > +
+ {currentOrg?.id === org.id && ( + + )} +

{org.name}

+ +
+
+ +
+ Sub-Organizations +
+ {subOrganizations.map((subOrg) => ( + { + navigate({ + to: "/organization/projects", + search: (prev) => ({ ...prev, subOrganization: subOrg.name }) + }); + queryClient.clear(); + await router.invalidate({ sync: true }).catch(() => null); + }} + className="cursor-pointer font-normal" + key={subOrg.id} + > +
+ {currentOrg?.id === subOrg.id && ( + + )} +

{subOrg.name}

+
+
+ ))} + {Boolean(subOrganizations.length) && ( +
+ )} + } + onClick={() => setShowSubOrgForm(true)} + > + New Sub-Organization + + + + ); + } + return ( - - + handleOrgNav(org)} + className="cursor-pointer font-normal" + key={org.id} + > +
+ {currentOrg?.id === org.id && ( + + )} +

{org.name}

+
); })} @@ -432,6 +440,79 @@ export const Navbar = () => {
+ {currentOrg.subOrganization && ( + <> +

/

+ + + + +

{currentOrg.subOrganization.name}

+
+ + +
+ + + +
+
+ +
+ Sub-Organizations +
+ {subOrganizations.map((subOrg) => ( + { + navigate({ + to: "/organization/projects", + search: (prev) => ({ ...prev, subOrganization: subOrg.name }) + }); + queryClient.clear(); + await router.invalidate({ sync: true }).catch(() => null); + }} + className="cursor-pointer font-normal" + key={subOrg.id} + > +
+ {currentOrg?.id === subOrg.id && ( + + )} +

{subOrg.name}

+
+
+ ))} + {Boolean(subOrganizations.length) && ( +
+ )} + } + onClick={() => setShowSubOrgForm(true)} + > + New Sub-Organization + + + + + )} {!isOrgScope && ( <>

/

@@ -654,7 +735,11 @@ export const Navbar = () => { subTitle="Define a new sub-organization under your current organization." >
- setShowSubOrgForm(false)} /> + { + setShowSubOrgForm(false); + }} + />
diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx index 5a736491d..32c557735 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx @@ -1,5 +1,7 @@ import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useQueryClient } from "@tanstack/react-query"; +import { useNavigate, useRouter } from "@tanstack/react-router"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; @@ -31,9 +33,13 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => { resolver: zodResolver(AddOrgSchema) }); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + const router = useRouter(); + const onSubmit = async ({ name }: FormData) => { try { - await createSubOrg.mutateAsync({ + const { organization } = await createSubOrg.mutateAsync({ name }); @@ -42,6 +48,13 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => { text: "Successfully created sub organization" }); onClose(); + + navigate({ + to: "/organization/projects", + search: (prev) => ({ ...prev, subOrganization: organization.name }) + }); + queryClient.clear(); + await router.invalidate({ sync: true }).catch(() => null); } catch { createNotification({ text: "Failed to create sub organization", diff --git a/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx b/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx index 4e72f9b06..564970904 100644 --- a/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx @@ -16,6 +16,8 @@ export const OrgNavBar = ({ isHidden }: Props) => { const { pathname } = useLocation(); + const variant = isRootOrganization ? "org" : "namespace"; + return ( <> {!isHidden && ( @@ -32,28 +34,28 @@ export const OrgNavBar = ({ isHidden }: Props) => { {({ isActive }) => ( - + Overview )} {({ isActive }) => ( - + App Connections )} {({ isActive }) => ( - + Networking )} {({ isActive }) => ( - + Secret Sharing )} @@ -61,7 +63,7 @@ export const OrgNavBar = ({ isHidden }: Props) => { {({ isActive }) => ( { {({ isActive }) => ( - + Audit Logs )} @@ -85,7 +87,7 @@ export const OrgNavBar = ({ isHidden }: Props) => { {isRootOrganization && ( {({ isActive }) => ( - + Usage & Billing )} @@ -93,7 +95,7 @@ export const OrgNavBar = ({ isHidden }: Props) => { )} {({ isActive }) => ( - + Settings )} diff --git a/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx b/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx index 8059f71ce..d3e93bcea 100644 --- a/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx @@ -24,7 +24,7 @@ import { OrgGroupsTab, OrgIdentityTab, OrgMembersTab, OrgRoleTabSection } from " export const AccessManagementPage = () => { const { t } = useTranslation(); const { permission } = useOrgPermission(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const navigate = useNavigate({ from: ROUTE_PATHS.Organization.AccessControlPage.path @@ -82,7 +82,7 @@ export const AccessManagementPage = () => {
@@ -116,7 +116,11 @@ export const AccessManagementPage = () => { {tabSections .filter((el) => !el.isHidden) .map((el) => ( - + {el.label} ))} diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx index 03de66a38..6aaae7d57 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx @@ -68,7 +68,7 @@ enum RolesOrderBy { export const OrgRoleTable = () => { const navigate = useNavigate(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const orgId = currentOrg?.id || ""; const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ @@ -200,7 +200,9 @@ export const OrgRoleTable = () => { return (
-

Organization Roles

+

+ {isSubOrganization ? "Sub-" : ""}Organization Roles +

{(isAllowed) => ( - )} - + Add Role + )} -
-
- - - - - - - - - - {roles.length ? ( - roles.map((role) => { - return ( - { - if (evt.key === "Enter") { - handlePopUpOpen("editRole", role); - } - }} - onClick={() => handlePopUpOpen("editRole", role)} - > - - - + + + )} + +
NameSlug -
{role.name}{role.slug} - {isCustomProjectRole(role.slug) && ( -
- + )} +
+
+ + + + + + + + + + {roles.length ? ( + roles.map((role) => { + return ( + { + if (evt.key === "Enter") { + handlePopUpOpen("editRole", role); + } + }} + onClick={() => handlePopUpOpen("editRole", role)} + > + + + - - ); - }) - ) : ( - - - )} - -
NameSlug +
{role.name}{role.slug} + {isCustomProjectRole(role.slug) && ( +
+ + {(isAllowed) => ( + { + e.stopPropagation(); + e.preventDefault(); + handlePopUpOpen("removeRole", role); + }} > - {(isAllowed) => ( - { - e.stopPropagation(); - e.preventDefault(); - handlePopUpOpen("removeRole", role); - }} - > - - - )} - -
- )} -
- + + + )} + + + )}
-
-
- handlePopUpToggle("removeRole", isOpen)} - onDeleteApproved={() => handleRemoveRole(roleToDelete?.slug)} - /> - -
- - )} - + ); + }) + ) : ( +
+ +
+
+
+ handlePopUpToggle("removeRole", isOpen)} + onDeleteApproved={() => handleRemoveRole(roleToDelete?.slug)} + /> +
+ )}
); }; diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplatesSection.tsx b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplatesSection.tsx index c542410bd..8d524c41c 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplatesSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplatesSection.tsx @@ -1,7 +1,6 @@ import { useState } from "react"; import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { AnimatePresence, motion } from "framer-motion"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { OrgPermissionCan } from "@app/components/permissions"; @@ -25,93 +24,70 @@ export const ProjectTemplatesSection = () => { return (
- - {editTemplate ? ( - - setEditTemplate(null)} - /> - - ) : ( - -
-

- Create and configure templates with predefined roles and environments to streamline - project setup -

-
-
-

Project Templates

- -
- - Docs - -
-
- - {(isAllowed) => ( - - )} - + {editTemplate ? ( + setEditTemplate(null)} /> + ) : ( +
+

+ Create and configure templates with predefined roles and environments to streamline + project setup +

+
+ + + + {(isAllowed) => ( + + )} +
- - )} - + + setEditTemplate(template)} + isOpen={popUp.addTemplate.isOpen} + onOpenChange={(isOpen) => handlePopUpToggle("addTemplate", isOpen)} + /> + handlePopUpToggle("upgradePlan", isOpen)} + text="You can create project templates if you switch to Infisical's Enterprise plan." + /> +
+
+ )}
); }; From c12661ca9dd37e6c5116fcc4aa7678998bdcad10 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 20 Oct 2025 16:42:31 -0700 Subject: [PATCH 24/44] improvement: add helper text and slug validation to create sub org form --- .../components/NavBar/NewSubOrganizationForm.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx index 32c557735..6ffed5578 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx @@ -7,14 +7,14 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input } from "@app/components/v2"; import { useCreateSubOrganization } from "@app/hooks/api"; -import { GenericResourceNameSchema } from "@app/lib/schemas"; +import { GenericResourceNameSchema, slugSchema } from "@app/lib/schemas"; type ContentProps = { onClose: () => void; }; const AddOrgSchema = z.object({ - name: GenericResourceNameSchema.nonempty("Suborganization name required") + name: slugSchema() }); type FormData = z.infer; @@ -67,7 +67,12 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => {
( - + )} From d7bfa384995f5ee059bafef863dfd7ecdea4aa0f Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 20 Oct 2025 16:46:16 -0700 Subject: [PATCH 25/44] improvement: only show accessible sub-orgs --- .../src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx index ec0ba3737..31e2d1b4d 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx @@ -140,7 +140,7 @@ export const Navbar = () => { const [showSubOrgForm, setShowSubOrgForm] = useState(false); const [showCardDeclinedModal, setShowCardDeclinedModal] = useState(false); const { data: subOrganizations = [] } = useQuery({ - ...subOrganizationsQuery.list({ limit: 500 }), + ...subOrganizationsQuery.list({ limit: 500, isAccessible: true }), enabled: Boolean(subscription.subOrganization) }); From 9bb0e2c401cf879df619016505f3245da0397020 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 20 Oct 2025 18:18:21 -0700 Subject: [PATCH 26/44] fix: fix isAcessible query join/filter and clear sub org query cache when org changes --- backend/src/services/org/org-dal.ts | 18 ++++++++++-------- .../components/NavBar/Navbar.tsx | 7 ++++--- .../NavBar/NewSubOrganizationForm.tsx | 2 +- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index 114ff3790..c2565f36b 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -172,14 +172,16 @@ export const orgDALFactory = (db: TDbClient) => { .select(selectAllTableCols(TableName.Organization)); if (dto.isAccessible) { - void query.leftJoin(`${TableName.Membership}`, (qb) => { - void qb.on(`${TableName.Membership}.scope`, AccessScope.Organization); - if (dto.actorType === ActorType.IDENTITY) { - void qb.andOn(`${TableName.Membership}.actorIdentityId`, dto.actorId); - } else { - void qb.andOn(`${TableName.Membership}.actorUserId`, dto.actorId); - } - }); + void query + .leftJoin(`${TableName.Membership}`, `${TableName.Membership}.scopeOrgId`, `${TableName.Organization}.id`) + .where((qb) => { + void qb.where(`${TableName.Membership}.scope`, AccessScope.Organization); + if (dto.actorType === ActorType.IDENTITY) { + void qb.andWhere(`${TableName.Membership}.actorIdentityId`, dto.actorId); + } else { + void qb.andWhere(`${TableName.Membership}.actorUserId`, dto.actorId); + } + }); } if (dto.limit) void query.limit(dto.limit); if (dto.offset) void query.offset(dto.offset); diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx index 31e2d1b4d..7e926d690 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx @@ -134,13 +134,13 @@ export const Navbar = () => { const { subscription } = useSubscription(); const { currentOrg, isSubOrganization } = useOrganization(); - console.log("current", currentOrg, isSubOrganization); - const [showAdminsModal, setShowAdminsModal] = useState(false); const [showSubOrgForm, setShowSubOrgForm] = useState(false); const [showCardDeclinedModal, setShowCardDeclinedModal] = useState(false); + + const subOrgQuery = subOrganizationsQuery.list({ limit: 500, isAccessible: true }); const { data: subOrganizations = [] } = useQuery({ - ...subOrganizationsQuery.list({ limit: 500, isAccessible: true }), + ...subOrgQuery, enabled: Boolean(subscription.subOrganization) }); @@ -183,6 +183,7 @@ export const Navbar = () => { } await router.invalidate(); await navigateUserToOrg(navigate, orgId); + queryClient.removeQueries({ queryKey: subOrgQuery.queryKey }); }; const { mutateAsync } = useGetOrgTrialUrl(); diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx index 6ffed5578..368b8e64b 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx @@ -7,7 +7,7 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input } from "@app/components/v2"; import { useCreateSubOrganization } from "@app/hooks/api"; -import { GenericResourceNameSchema, slugSchema } from "@app/lib/schemas"; +import { slugSchema } from "@app/lib/schemas"; type ContentProps = { onClose: () => void; From 67c8a0e1688f99d40a824d945992e7f74bef15a7 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 20 Oct 2025 18:44:44 -0700 Subject: [PATCH 27/44] fix: add sub org permissions UI --- .../src/context/OrgPermissionContext/types.ts | 11 +- .../components/OrgRoleModifySection.utils.ts | 11 +- .../OrgPermissionSubOrgRow.tsx | 159 ++++++++++++++++++ .../RolePermissionRow.tsx | 1 + .../RolePermissionsSection.tsx | 6 + .../ProjectTemplateRolesSection.tsx | 1 - 6 files changed, 185 insertions(+), 4 deletions(-) create mode 100644 frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionSubOrgRow.tsx diff --git a/frontend/src/context/OrgPermissionContext/types.ts b/frontend/src/context/OrgPermissionContext/types.ts index bcab6169e..87dc40263 100644 --- a/frontend/src/context/OrgPermissionContext/types.ts +++ b/frontend/src/context/OrgPermissionContext/types.ts @@ -62,7 +62,8 @@ export enum OrgPermissionSubjects { SecretShare = "secret-share", GithubOrgSync = "github-org-sync", GithubOrgSyncManual = "github-org-sync-manual", - MachineIdentityAuthTemplate = "machine-identity-auth-template" + MachineIdentityAuthTemplate = "machine-identity-auth-template", + SubOrganization = "sub-organization" } export enum OrgPermissionAdminConsoleAction { @@ -112,6 +113,11 @@ export enum OrgPermissionGroupActions { RemoveMembers = "remove-members" } +export enum OrgPermissionSubOrgActions { + Create = "create", + DirectAccess = "direct-access" +} + export type AppConnectionSubjectFields = { connectionId: string; }; @@ -151,6 +157,7 @@ export type OrgPermissionSet = | OrgPermissionSubjects.AppConnections | (ForcedSubject & AppConnectionSubjectFields) ) - ]; + ] + | [OrgPermissionSubOrgActions, OrgPermissionSubjects.SubOrganization]; export type TOrgPermission = MongoAbility; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts b/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts index 68da9cad2..a355029ad 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts +++ b/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts @@ -12,6 +12,7 @@ import { OrgPermissionKmipActions, OrgPermissionMachineIdentityAuthTemplateActions, OrgPermissionSecretShareAction, + OrgPermissionSubOrgActions, OrgRelayPermissionActions } from "@app/context/OrgPermissionContext/types"; import { TPermission } from "@app/hooks/api/roles/types"; @@ -123,6 +124,13 @@ const secretSharingPermissionSchema = z }) .optional(); +const subOrganizationPermissionSchema = z + .object({ + [OrgPermissionSubOrgActions.Create]: z.boolean().optional(), + [OrgPermissionSubOrgActions.DirectAccess]: z.boolean().optional() + }) + .optional(); + export const formSchema = z.object({ name: z.string().trim(), description: z.string().trim().optional(), @@ -159,7 +167,8 @@ export const formSchema = z.object({ gateway: orgGatewayPermissionSchema, relay: orgRelayPermissionSchema, "machine-identity-auth-template": machineIdentityAuthTemplatePermissionSchema, - "secret-share": secretSharingPermissionSchema + "secret-share": secretSharingPermissionSchema, + "sub-organization": subOrganizationPermissionSchema }) .optional() }); diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionSubOrgRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionSubOrgRow.tsx new file mode 100644 index 000000000..aa51c6752 --- /dev/null +++ b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionSubOrgRow.tsx @@ -0,0 +1,159 @@ +import { useEffect, useMemo } from "react"; +import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form"; +import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2"; +import { OrgPermissionSubOrgActions } from "@app/context/OrgPermissionContext/types"; +import { useToggle } from "@app/hooks"; + +import { TFormSchema } from "../OrgRoleModifySection.utils"; + +const PERMISSION_ACTIONS = [ + { action: OrgPermissionSubOrgActions.Create, label: "Create" }, + { action: OrgPermissionSubOrgActions.DirectAccess, label: "Direct Access" } +] as const; + +type Props = { + isEditable: boolean; + setValue: UseFormSetValue; + control: Control; +}; + +enum Permission { + NoAccess = "no-access", + FullAccess = "full-access", + Custom = "custom" +} + +export const OrgPermissionSubOrgRow = ({ isEditable, control, setValue }: Props) => { + const [isRowExpanded, setIsRowExpanded] = useToggle(); + const [isCustom, setIsCustom] = useToggle(); + + const rule = useWatch({ + control, + name: "permissions.sub-organization" + }); + + const selectedPermissionCategory = useMemo(() => { + const actions = Object.keys(rule || {}) as Array; + const totalActions = PERMISSION_ACTIONS.length; + const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number); + + if (isCustom) return Permission.Custom; + if (score === 0) return Permission.NoAccess; + if (score === totalActions) return Permission.FullAccess; + return Permission.Custom; + }, [rule, isCustom]); + + useEffect(() => { + if (selectedPermissionCategory === Permission.Custom) setIsCustom.on(); + else setIsCustom.off(); + }, [selectedPermissionCategory]); + + const handlePermissionChange = (val: Permission) => { + if (val === Permission.Custom) { + setIsRowExpanded.on(); + setIsCustom.on(); + return; + } + setIsCustom.off(); + + switch (val) { + case Permission.NoAccess: + setValue( + "permissions.sub-organization", + { + [OrgPermissionSubOrgActions.Create]: false, + [OrgPermissionSubOrgActions.DirectAccess]: false + }, + { shouldDirty: true } + ); + break; + case Permission.FullAccess: + setValue( + "permissions.sub-organization", + { + [OrgPermissionSubOrgActions.Create]: true, + [OrgPermissionSubOrgActions.DirectAccess]: true + }, + { shouldDirty: true } + ); + break; + default: + setValue( + "permissions.sub-organization", + { + [OrgPermissionSubOrgActions.Create]: true, + [OrgPermissionSubOrgActions.DirectAccess]: true + }, + { shouldDirty: true } + ); + break; + } + }; + + return ( + <> + setIsRowExpanded.toggle()} + > + + + + Sub-Organizations + + + + + {isRowExpanded && ( + + +
+ {PERMISSION_ACTIONS.map(({ action, label }) => { + return ( + ( + { + if (!isEditable) { + createNotification({ + type: "error", + text: "Failed to update default role" + }); + return; + } + field.onChange(e); + }} + id={`permissions.sub-organization.${action}`} + > + {label} + + )} + /> + ); + })} +
+ + + )} + + ); +}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionRow.tsx index 0e3b48a4e..7fb1a3842 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionRow.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionRow.tsx @@ -74,6 +74,7 @@ type Props = { | "billing" | "audit-logs" | "machine-identity-auth-template" + | "sub-organization" >; setValue: UseFormSetValue; control: Control; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx index 1ee3b4d91..02aa0a08c 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx @@ -25,6 +25,7 @@ import { OrgPermissionKmipRow } from "./OrgPermissionKmipRow"; import { OrgPermissionMachineIdentityAuthTemplateRow } from "./OrgPermissionMachineIdentityAuthTemplateRow"; import { OrgRelayPermissionRow } from "./OrgPermissionRelayRow"; import { OrgPermissionSecretShareRow } from "./OrgPermissionSecretShareRow"; +import { OrgPermissionSubOrgRow } from "./OrgPermissionSubOrgRow"; import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow"; import { RolePermissionRow } from "./RolePermissionRow"; @@ -224,6 +225,11 @@ export const RolePermissionsSection = ({ roleId }: Props) => { setValue={setValue} isEditable={isCustomRole} /> + diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx index 1df8128f4..d65f7d785 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx @@ -1,6 +1,5 @@ import { faPlus, faTrash, faUnlock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { AnimatePresence, motion } from "framer-motion"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; From f2c6884499a937e6374b7101fa66a22304c69278 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 20 Oct 2025 19:38:27 -0700 Subject: [PATCH 28/44] fix: handle assign identity modal overflow --- .../components/IdentitySection/IdentitySection.tsx | 1 + 1 file changed, 1 insertion(+) 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 ceac67251..5c15a0cdd 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 @@ -244,6 +244,7 @@ export const IdentitySection = withPermission( handlePopUpClose("linkIdentity")} /> From a7d337f97ad6663ebd1ca1c807be15289926a819 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 21 Oct 2025 01:01:48 -0400 Subject: [PATCH 29/44] fix lint issue with permission --- frontend/src/hoc/withPermission/withPermission.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/hoc/withPermission/withPermission.tsx b/frontend/src/hoc/withPermission/withPermission.tsx index 529a74930..762b067c2 100644 --- a/frontend/src/hoc/withPermission/withPermission.tsx +++ b/frontend/src/hoc/withPermission/withPermission.tsx @@ -24,7 +24,7 @@ export const withPermission = ( // akhilmhdh: Set as any due to casl/react ts type bug // REASON: casl due to its type checking can't seem to union even if union intersection is applied - if (permission.cannot(action as any, subject)) { + if (permission.cannot(action as any, subject as any)) { return (
Date: Tue, 21 Oct 2025 14:49:30 +0530 Subject: [PATCH 30/44] feat: switched to new org invitation modal for sub organizations --- .../src/services/identity/identity-org-dal.ts | 2 +- .../membership-user/membership-user-dal.ts | 2 +- .../NavBar/NewSubOrganizationForm.tsx | 2 +- .../AddSubOrgMemberModal.tsx | 280 ++++++++++++++++++ .../OrgMembersSection/OrgMembersSection.tsx | 18 +- .../OrgMembersSection/OrgMembersTable.tsx | 3 +- 6 files changed, 301 insertions(+), 6 deletions(-) create mode 100644 frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddSubOrgMemberModal.tsx diff --git a/backend/src/services/identity/identity-org-dal.ts b/backend/src/services/identity/identity-org-dal.ts index 898040458..66556f5fa 100644 --- a/backend/src/services/identity/identity-org-dal.ts +++ b/backend/src/services/identity/identity-org-dal.ts @@ -657,7 +657,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { tx?: Knex ) => { try { - const query = (tx || db.replicaNode())(TableName.Identity) + const query = (tx || db.replicaNode())(TableName.Membership) .where(`${TableName.Membership}.scope`, AccessScope.Organization) .whereNotNull(`${TableName.Membership}.actorIdentityId`) .where(filter) diff --git a/backend/src/services/membership-user/membership-user-dal.ts b/backend/src/services/membership-user/membership-user-dal.ts index 221228465..17970f16f 100644 --- a/backend/src/services/membership-user/membership-user-dal.ts +++ b/backend/src/services/membership-user/membership-user-dal.ts @@ -308,7 +308,7 @@ export const membershipUserDALFactory = (db: TDbClient) => { .where(`${TableName.Users}.isGhost`, false) .whereNotNull(`${TableName.Membership}.actorUserId`) .where(`${TableName.Membership}.scopeOrgId`, rootOrgId) - .whereNot(`${TableName.Membership}.actorUserId`, usersConnectedToOrg) + .whereNotIn(`${TableName.Membership}.actorUserId`, usersConnectedToOrg) .select( db.ref("id").withSchema(TableName.Users), db.ref("email").withSchema(TableName.Users), diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx index 368b8e64b..869ad00b1 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx @@ -73,7 +73,7 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => { errorText={error?.message} label="Name" > - + )} control={control} diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddSubOrgMemberModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddSubOrgMemberModal.tsx new file mode 100644 index 000000000..290f8db4b --- /dev/null +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddSubOrgMemberModal.tsx @@ -0,0 +1,280 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { RoleOption } from "@app/components/roles"; +import { Button, FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { findOrgMembershipRole } from "@app/helpers/roles"; +import { + useAddUsersToOrg, + useAddUserToWsNonE2EE, + useGetOrgRoles, + useGetUserProjects +} from "@app/hooks/api"; +import { useGetAvailableOrgUsers } from "@app/hooks/api/organization/queries"; +import { ProjectType, ProjectVersion } from "@app/hooks/api/projects/types"; +import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; + +const DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG = "member"; + +const addMemberFormSchema = z.object({ + users: z + .array( + z.object({ + username: z.string().trim(), + email: z.string().trim() + }) + ) + .min(1), + projects: z + .array( + z.object({ + name: z.string(), + id: z.string(), + slug: z.string(), + version: z.nativeEnum(ProjectVersion) + }) + ) + .default([]), + projectRoleSlug: z.string().min(1).default(DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG), + organizationRole: z.object({ + name: z.string(), + slug: z.string(), + description: z.string().optional() + }) +}); + +type TAddMemberForm = z.infer; + +type Props = { + onClose: () => void; +}; + +export const AddSubOrgMemberModal = ({ onClose }: Props) => { + const { currentOrg } = useOrganization(); + + const { data: organizationRoles } = useGetOrgRoles(currentOrg?.id ?? ""); + const { data: members = [], isPending: isMembersPending } = useGetAvailableOrgUsers(); + + const { mutateAsync: addUsersMutateAsync } = useAddUsersToOrg(); + const { mutateAsync: addUserToProject } = useAddUserToWsNonE2EE(); + + const { data: projects, isPending: isProjectsLoading } = useGetUserProjects({ + includeRoles: true + }); + + const { + control, + handleSubmit, + watch, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(addMemberFormSchema) + }); + + // set initial form role based off org default role + useEffect(() => { + if (organizationRoles) { + reset({ + organizationRole: findOrgMembershipRole(organizationRoles, currentOrg.defaultMembershipRole) + }); + } + }, [organizationRoles]); + + const onAddMembers = async ({ + users, + organizationRole, + projects: selectedProjects, + projectRoleSlug + }: TAddMemberForm) => { + if (!currentOrg?.id) return; + + if (selectedProjects?.length) { + // eslint-disable-next-line no-restricted-syntax + for (const project of selectedProjects) { + if (project.version !== ProjectVersion.V3) { + createNotification({ + type: "error", + text: `Cannot add users to project "${project.name}" because it's incompatible. Please upgrade the project.` + }); + return; + } + } + } + + try { + const usernames = users.map((el) => el.username); + await addUsersMutateAsync({ + organizationId: currentOrg?.id, + inviteeEmails: usernames, + organizationRoleSlug: organizationRole.slug + }); + + await Promise.allSettled( + selectedProjects.map((el) => + addUserToProject({ + orgId: currentOrg.id, + projectId: el.id, + roleSlugs: [projectRoleSlug], + usernames + }) + ) + ); + onClose(); + } catch (error) { + console.error(error); + createNotification({ + text: "Failed to add user to suborganization", + type: "error" + }); + } + }; + + const getGroupHeaderLabel = (type: ProjectType) => { + switch (type) { + case ProjectType.SecretManager: + return "Secrets"; + case ProjectType.CertificateManager: + return "PKI"; + case ProjectType.KMS: + return "KMS"; + case ProjectType.SSH: + return "SSH"; + default: + return "Other"; + } + }; + + return ( + + ( + + option.username} + getOptionLabel={(option) => option.username} + /* eslint-disable-next-line react/no-unstable-nested-components */ + noOptionsMessage={() => ( +

All root organization users are already assigned to this project

+ )} + /> +
+ )} + /> + ( + + option.slug} + getOptionLabel={(option) => option.name} + value={value} + onChange={onChange} + components={{ Option: RoleOption }} + /> + + )} + /> + +
+
+ ( + + project.name} + getOptionValue={(project) => project.id} + options={projects} + groupBy="type" + getGroupHeaderLabel={getGroupHeaderLabel} + placeholder="Select projects..." + /> + + )} + /> +
+
+ ( + +
+ +
+
+ )} + /> +
+
+ +
+ + +
+ + ); +}; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx index 010a12b38..823d0e0ce 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx @@ -11,6 +11,8 @@ import { Button, DeleteActionModal, EmailServiceSetupModal, + Modal, + ModalContent, Tooltip } from "@app/components/v2"; import { @@ -27,10 +29,11 @@ import { usePopUp } from "@app/hooks/usePopUp"; import { AddOrgMemberModal } from "./AddOrgMemberModal"; import { OrgMembersTable } from "./OrgMembersTable"; +import { AddSubOrgMemberModal } from "./AddSubOrgMemberModal"; export const OrgMembersSection = () => { const { subscription } = useSubscription(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const orgId = currentOrg?.id ?? ""; const { user } = useUser(); const userId = user?.id || ""; @@ -41,6 +44,7 @@ export const OrgMembersSection = () => { const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "addMember", + "addMemberToSubOrg", "removeMember", "deactivateMember", "upgradePlan", @@ -210,7 +214,9 @@ export const OrgMembersSection = () => { colorSchema="secondary" type="submit" leftIcon={} - onClick={() => handleAddMemberModal()} + onClick={() => + isSubOrganization ? handlePopUpOpen("addMemberToSubOrg") : handleAddMemberModal() + } isDisabled={!isAllowed} > Add Member @@ -230,6 +236,14 @@ export const OrgMembersSection = () => { completeInviteLinks={completeInviteLinks} setCompleteInviteLinks={setCompleteInviteLinks} /> + handlePopUpToggle("addMemberToSubOrg", isOpen)} + > + + handlePopUpClose("addMemberToSubOrg")} /> + + { const navigate = useNavigate(); const { subscription } = useSubscription(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const { user } = useUser(); const userId = user?.id || ""; const orgId = currentOrg?.id || ""; @@ -586,6 +586,7 @@ export const OrgMembersTable = ({ {isActive && (status === "invited" || status === "verified") && email && + !isSubOrganization && serverDetails?.emailConfigured && ( Date: Tue, 21 Oct 2025 15:02:57 +0530 Subject: [PATCH 31/44] feat: resolved organization secret sharing failing in suborg --- .../secret-sharing/secret-sharing-service.ts | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts index 3f5df049c..87dd207f1 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -92,8 +92,10 @@ export const secretSharingServiceFactory = ({ if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" }); $validateSharedSecretExpiry(expiresAt); - const org = await orgDAL.findOrgById(orgId); - if (!org.allowSecretSharingOutsideOrganization && accessType === SecretSharingAccessType.Anyone) { + const rootOrg = await orgDAL.findRootOrgDetails(orgId); + if (!rootOrg) throw new BadRequestError({ message: `Organization with id ${orgId} not found` }); + + if (!rootOrg.allowSecretSharingOutsideOrganization && accessType === SecretSharingAccessType.Anyone) { throw new BadRequestError({ message: "Organization does not allow sharing secrets to members outside of this organization" }); @@ -107,13 +109,16 @@ export const secretSharingServiceFactory = ({ const expiresAtTimestamp = new Date(expiresAt).getTime(); const lifetime = expiresAtTimestamp - new Date().getTime(); - // org.maxSharedSecretLifetime is in seconds - if (org.maxSharedSecretLifetime && lifetime / 1000 > org.maxSharedSecretLifetime) { + // rootOrg.maxSharedSecretLifetime is in seconds + if (rootOrg.maxSharedSecretLifetime && lifetime / 1000 > rootOrg.maxSharedSecretLifetime) { throw new BadRequestError({ message: "Secret lifetime exceeds organization limit" }); } // Check max view count is within org allowance - if (org.maxSharedSecretViewLimit && (!expiresAfterViews || expiresAfterViews > org.maxSharedSecretViewLimit)) { + if ( + rootOrg.maxSharedSecretViewLimit && + (!expiresAfterViews || expiresAfterViews > rootOrg.maxSharedSecretViewLimit) + ) { throw new BadRequestError({ message: "Secret max views parameter exceeds organization limit" }); } @@ -129,7 +134,10 @@ export const secretSharingServiceFactory = ({ if (allOrgMembers.some((v) => v.user.email === email)) { orgEmails.push(email); // If the email is not part of the org, but access type / org settings require it - } else if (!org.allowSecretSharingOutsideOrganization || accessType === SecretSharingAccessType.Organization) { + } else if ( + !rootOrg.allowSecretSharingOutsideOrganization || + accessType === SecretSharingAccessType.Organization + ) { throw new BadRequestError({ message: "Organization does not allow sharing secrets to members outside of this organization" }); From 4247c09b66060ce68d13f0aaf565e196b72cfda2 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 21 Oct 2025 15:42:21 +0530 Subject: [PATCH 32/44] feat: resolved permission rendering for sub org --- backend/src/ee/routes/v1/org-role-router.ts | 35 +++++++++++++++- .../OrgMembersSection/OrgMembersSection.tsx | 2 +- .../RolePermissionsSection.tsx | 42 +++++++++++++------ 3 files changed, 65 insertions(+), 14 deletions(-) diff --git a/backend/src/ee/routes/v1/org-role-router.ts b/backend/src/ee/routes/v1/org-role-router.ts index 5a8f03038..591458bb4 100644 --- a/backend/src/ee/routes/v1/org-role-router.ts +++ b/backend/src/ee/routes/v1/org-role-router.ts @@ -3,12 +3,35 @@ import { z } from "zod"; import { AccessScope, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { OrgPermissionSchema } from "@app/ee/services/permission/org-permission"; +import { OrgPermissionSchema, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { BadRequestError } from "@app/lib/errors"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +const INVALID_SUBORG_PERMISSIONS = [ + OrgPermissionSubjects.Sso, + OrgPermissionSubjects.Ldap, + OrgPermissionSubjects.Scim, + OrgPermissionSubjects.GithubOrgSync, + OrgPermissionSubjects.GithubOrgSyncManual, + OrgPermissionSubjects.Billing, + OrgPermissionSubjects.SubOrganization +]; + +const validateSubOrganizationSubjects = (permissions: unknown) => { + const invalidPermissionSubjects = (permissions as { subject: OrgPermissionSubjects }[]) + .filter((el) => INVALID_SUBORG_PERMISSIONS.includes(el.subject)) + .map((el) => el.subject); + if (invalidPermissionSubjects.length) { + const deduplication = Array.from(new Set(invalidPermissionSubjects)); + throw new BadRequestError({ + message: `Suborganization contains invalid permission subjects: ${deduplication.join(",")}` + }); + } +}; + export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", @@ -37,6 +60,11 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { + const isSubOrganization = req.permission.rootOrgId !== req.permission.orgId; + if (isSubOrganization) { + validateSubOrganizationSubjects(req.body.permissions); + } + const stringifiedPermissions = JSON.stringify(packRules(req.body.permissions)); const role = await server.services.role.createRole({ permission: req.permission, @@ -133,6 +161,11 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { + const isSubOrganization = req.permission.rootOrgId !== req.permission.orgId; + if (isSubOrganization && req.body.permissions) { + validateSubOrganizationSubjects(req.body.permissions); + } + const stringifiedPermissions = req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined; const role = await server.services.role.updateRole({ permission: req.permission, diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx index 823d0e0ce..a8f5ea059 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx @@ -28,8 +28,8 @@ import { OrgUser } from "@app/hooks/api/users/types"; import { usePopUp } from "@app/hooks/usePopUp"; import { AddOrgMemberModal } from "./AddOrgMemberModal"; -import { OrgMembersTable } from "./OrgMembersTable"; import { AddSubOrgMemberModal } from "./AddSubOrgMemberModal"; +import { OrgMembersTable } from "./OrgMembersTable"; export const OrgMembersSection = () => { const { subscription } = useSubscription(); diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx index 02aa0a08c..0d6b269a8 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx @@ -77,8 +77,18 @@ type Props = { roleId: string; }; +const INVALID_SUBORG_PERMISSIONS = [ + OrgPermissionSubjects.Sso, + OrgPermissionSubjects.Ldap, + OrgPermissionSubjects.Scim, + OrgPermissionSubjects.GithubOrgSync, + OrgPermissionSubjects.GithubOrgSyncManual, + OrgPermissionSubjects.Billing, + OrgPermissionSubjects.SubOrganization +]; + export const RolePermissionsSection = ({ roleId }: Props) => { - const { currentOrg } = useOrganization(); + const { currentOrg, isRootOrganization } = useOrganization(); const orgId = currentOrg?.id || ""; const { data: role } = useGetOrgRole(orgId, roleId); @@ -153,7 +163,11 @@ export const RolePermissionsSection = ({ roleId }: Props) => { - {SIMPLE_PERMISSION_OPTIONS.map((permission) => { + {SIMPLE_PERMISSION_OPTIONS.filter((el) => + isRootOrganization + ? true + : !INVALID_SUBORG_PERMISSIONS.includes(el.formName as OrgPermissionSubjects) + ).map((permission) => { return ( { setValue={setValue} isEditable={isCustomRole} /> - + {isRootOrganization && ( + + )} { setValue={setValue} isEditable={isCustomRole} /> - + {isRootOrganization && ( + + )}
From b6694b0356cd82b332f0b3eda4dd9be47a1d7b38 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 21 Oct 2025 19:52:00 +0530 Subject: [PATCH 33/44] feat: patched project select --- .../components/ProjectSelect/ProjectSelect.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx b/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx index 4e26a7393..72430fec6 100644 --- a/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx +++ b/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx @@ -171,9 +171,21 @@ export const ProjectSelect = () => { to: getProjectHomePage(workspace.type, workspace.environments), params: { projectId: workspace.id + }, + search: { + subOrganization: currentOrg?.subOrganization?.name } }); - window.location.assign(url.to.replaceAll("$projectId", workspace.id)); + const urlInstance = new URL( + `${window.location.origin}/${url.to.replaceAll("$projectId", workspace.id)}` + ); + if (currentOrg?.subOrganization) { + urlInstance.searchParams.set( + "subOrganization", + currentOrg.subOrganization.name + ); + } + window.location.assign(urlInstance); }} icon={ currentWorkspace?.id === workspace.id && ( From bd5764031359082b93f7360c6b25869148c56137 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Tue, 21 Oct 2025 09:45:21 -0700 Subject: [PATCH 34/44] fix: add secret settings tab to secret sharing page --- frontend/src/const/routes.ts | 4 -- .../SecretSharingPage/ShareSecretSection.tsx | 12 +++++- .../OrgSecretShareLimitSection.tsx | 0 .../SecretSharingAllowShareToAnyone.tsx | 0 .../SecretSharingSettingsTab.tsx | 21 ++++++++++ .../SecretSharingSettingsPage.tsx | 40 ------------------- .../OrgSecretShareLimitSection/index.tsx | 1 - .../SecretSharingAllowShareToAnyone/index.tsx | 1 - .../SecretSharingSettingsGeneralTab.tsx | 11 ----- .../SecretSharingSettingsGeneralTab/index.tsx | 1 - .../SecretSharingSettingsTabGroup.tsx | 39 ------------------ .../SecretSharingSettingsTabGroup/index.tsx | 1 - .../components/index.tsx | 1 - .../SecretSharingSettingsPage/route.tsx | 30 -------------- frontend/src/routeTree.gen.ts | 32 +-------------- frontend/src/routes.ts | 5 +-- 16 files changed, 34 insertions(+), 165 deletions(-) rename frontend/src/pages/organization/{SecretSharingSettingsPage/components/OrgSecretShareLimitSection => SecretSharingPage/components/SecretSharingSettings}/OrgSecretShareLimitSection.tsx (100%) rename frontend/src/pages/organization/{SecretSharingSettingsPage/components/SecretSharingAllowShareToAnyone => SecretSharingPage/components/SecretSharingSettings}/SecretSharingAllowShareToAnyone.tsx (100%) create mode 100644 frontend/src/pages/organization/SecretSharingPage/components/SecretSharingSettings/SecretSharingSettingsTab.tsx delete mode 100644 frontend/src/pages/organization/SecretSharingSettingsPage/SecretSharingSettingsPage.tsx delete mode 100644 frontend/src/pages/organization/SecretSharingSettingsPage/components/OrgSecretShareLimitSection/index.tsx delete mode 100644 frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingAllowShareToAnyone/index.tsx delete mode 100644 frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsGeneralTab/SecretSharingSettingsGeneralTab.tsx delete mode 100644 frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsGeneralTab/index.tsx delete mode 100644 frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsTabGroup/SecretSharingSettingsTabGroup.tsx delete mode 100644 frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsTabGroup/index.tsx delete mode 100644 frontend/src/pages/organization/SecretSharingSettingsPage/components/index.tsx delete mode 100644 frontend/src/pages/organization/SecretSharingSettingsPage/route.tsx diff --git a/frontend/src/const/routes.ts b/frontend/src/const/routes.ts index 2e835a1db..410df7440 100644 --- a/frontend/src/const/routes.ts +++ b/frontend/src/const/routes.ts @@ -33,10 +33,6 @@ export const ROUTE_PATHS = Object.freeze({ "/organization/secret-sharing", "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/" ), - SecretSharingSettings: setRoute( - "/organization/secret-sharing/settings", - "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings" - ), SettingsPage: setRoute( "/organization/settings", "/_authenticate/_inject-org-details/_org-layout/organization/settings/" diff --git a/frontend/src/pages/organization/SecretSharingPage/ShareSecretSection.tsx b/frontend/src/pages/organization/SecretSharingPage/ShareSecretSection.tsx index c8bfa7dc9..134e341a6 100644 --- a/frontend/src/pages/organization/SecretSharingPage/ShareSecretSection.tsx +++ b/frontend/src/pages/organization/SecretSharingPage/ShareSecretSection.tsx @@ -6,11 +6,13 @@ import { ROUTE_PATHS } from "@app/const/routes"; import { useOrganization } from "@app/context"; import { RequestSecretTab } from "./components/RequestSecret/RequestSecretTab"; +import { SecretSharingSettingsTab } from "./components/SecretSharingSettings/SecretSharingSettingsTab"; import { ShareSecretTab } from "./components/ShareSecret/ShareSecretTab"; enum SecretSharingPageTabs { ShareSecret = "share-secret", - RequestSecret = "request-secret" + RequestSecret = "request-secret", + Settings = "settings" } export const ShareSecretSection = () => { @@ -46,6 +48,11 @@ export const ShareSecretSection = () => { Request Secrets + {!isSubOrganization && ( + + Settings + + )} @@ -53,6 +60,9 @@ export const ShareSecretSection = () => { + + +
); diff --git a/frontend/src/pages/organization/SecretSharingSettingsPage/components/OrgSecretShareLimitSection/OrgSecretShareLimitSection.tsx b/frontend/src/pages/organization/SecretSharingPage/components/SecretSharingSettings/OrgSecretShareLimitSection.tsx similarity index 100% rename from frontend/src/pages/organization/SecretSharingSettingsPage/components/OrgSecretShareLimitSection/OrgSecretShareLimitSection.tsx rename to frontend/src/pages/organization/SecretSharingPage/components/SecretSharingSettings/OrgSecretShareLimitSection.tsx diff --git a/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingAllowShareToAnyone/SecretSharingAllowShareToAnyone.tsx b/frontend/src/pages/organization/SecretSharingPage/components/SecretSharingSettings/SecretSharingAllowShareToAnyone.tsx similarity index 100% rename from frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingAllowShareToAnyone/SecretSharingAllowShareToAnyone.tsx rename to frontend/src/pages/organization/SecretSharingPage/components/SecretSharingSettings/SecretSharingAllowShareToAnyone.tsx diff --git a/frontend/src/pages/organization/SecretSharingPage/components/SecretSharingSettings/SecretSharingSettingsTab.tsx b/frontend/src/pages/organization/SecretSharingPage/components/SecretSharingSettings/SecretSharingSettingsTab.tsx new file mode 100644 index 000000000..96517eaa9 --- /dev/null +++ b/frontend/src/pages/organization/SecretSharingPage/components/SecretSharingSettings/SecretSharingSettingsTab.tsx @@ -0,0 +1,21 @@ +import { OrgPermissionSubjects } from "@app/context"; +import { OrgPermissionSecretShareAction } from "@app/context/OrgPermissionContext/types"; +import { withPermission } from "@app/hoc"; + +import { OrgSecretShareLimitSection } from "./OrgSecretShareLimitSection"; +import { SecretSharingAllowShareToAnyone } from "./SecretSharingAllowShareToAnyone"; + +export const SecretSharingSettingsTab = withPermission( + () => { + return ( +
+ + +
+ ); + }, + { + action: OrgPermissionSecretShareAction.ManageSettings, + subject: OrgPermissionSubjects.SecretShare + } +); diff --git a/frontend/src/pages/organization/SecretSharingSettingsPage/SecretSharingSettingsPage.tsx b/frontend/src/pages/organization/SecretSharingSettingsPage/SecretSharingSettingsPage.tsx deleted file mode 100644 index 5d6424681..000000000 --- a/frontend/src/pages/organization/SecretSharingSettingsPage/SecretSharingSettingsPage.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Helmet } from "react-helmet"; -import { useTranslation } from "react-i18next"; - -import { PageHeader } from "@app/components/v2"; -import { useOrganization } from "@app/context"; -import { - OrgPermissionSecretShareAction, - OrgPermissionSubjects -} from "@app/context/OrgPermissionContext/types"; -import { withPermission } from "@app/hoc"; - -import { SecretSharingSettingsTabGroup } from "./components"; - -export const SecretSharingSettingsPage = withPermission( - () => { - const { t } = useTranslation(); - const { isSubOrganization } = useOrganization(); - - return ( - <> - - {t("common.head-title", { title: "Secret Share Settings" })} - -
-
- - -
-
- - ); - }, - { - action: OrgPermissionSecretShareAction.ManageSettings, - subject: OrgPermissionSubjects.SecretShare - } -); diff --git a/frontend/src/pages/organization/SecretSharingSettingsPage/components/OrgSecretShareLimitSection/index.tsx b/frontend/src/pages/organization/SecretSharingSettingsPage/components/OrgSecretShareLimitSection/index.tsx deleted file mode 100644 index 1e83c4be8..000000000 --- a/frontend/src/pages/organization/SecretSharingSettingsPage/components/OrgSecretShareLimitSection/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { OrgSecretShareLimitSection } from "./OrgSecretShareLimitSection"; diff --git a/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingAllowShareToAnyone/index.tsx b/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingAllowShareToAnyone/index.tsx deleted file mode 100644 index d02460498..000000000 --- a/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingAllowShareToAnyone/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { SecretSharingAllowShareToAnyone } from "./SecretSharingAllowShareToAnyone"; diff --git a/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsGeneralTab/SecretSharingSettingsGeneralTab.tsx b/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsGeneralTab/SecretSharingSettingsGeneralTab.tsx deleted file mode 100644 index ba849507d..000000000 --- a/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsGeneralTab/SecretSharingSettingsGeneralTab.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { OrgSecretShareLimitSection } from "../OrgSecretShareLimitSection"; -import { SecretSharingAllowShareToAnyone } from "../SecretSharingAllowShareToAnyone"; - -export const SecretSharingSettingsGeneralTab = () => { - return ( -
- - -
- ); -}; diff --git a/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsGeneralTab/index.tsx b/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsGeneralTab/index.tsx deleted file mode 100644 index 306109025..000000000 --- a/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsGeneralTab/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { SecretSharingSettingsGeneralTab } from "./SecretSharingSettingsGeneralTab"; diff --git a/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsTabGroup/SecretSharingSettingsTabGroup.tsx b/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsTabGroup/SecretSharingSettingsTabGroup.tsx deleted file mode 100644 index 594df474f..000000000 --- a/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsTabGroup/SecretSharingSettingsTabGroup.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { useState } from "react"; -import { useSearch } from "@tanstack/react-router"; - -import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; -import { ROUTE_PATHS } from "@app/const/routes"; - -import { SecretSharingSettingsGeneralTab } from "../SecretSharingSettingsGeneralTab"; - -export const SecretSharingSettingsTabGroup = () => { - const search = useSearch({ - from: ROUTE_PATHS.Organization.SecretSharingSettings.id - }); - const tabs = [ - { - name: "General", - key: "tab-secret-sharing-general", - component: SecretSharingSettingsGeneralTab - } - ]; - - const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key); - - return ( - - - {tabs.map((tab) => ( - - {tab.name} - - ))} - - {tabs.map(({ key, component: Component }) => ( - - - - ))} - - ); -}; diff --git a/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsTabGroup/index.tsx b/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsTabGroup/index.tsx deleted file mode 100644 index 2c60c7e20..000000000 --- a/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsTabGroup/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { SecretSharingSettingsTabGroup } from "./SecretSharingSettingsTabGroup"; diff --git a/frontend/src/pages/organization/SecretSharingSettingsPage/components/index.tsx b/frontend/src/pages/organization/SecretSharingSettingsPage/components/index.tsx deleted file mode 100644 index 2c60c7e20..000000000 --- a/frontend/src/pages/organization/SecretSharingSettingsPage/components/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { SecretSharingSettingsTabGroup } from "./SecretSharingSettingsTabGroup"; diff --git a/frontend/src/pages/organization/SecretSharingSettingsPage/route.tsx b/frontend/src/pages/organization/SecretSharingSettingsPage/route.tsx deleted file mode 100644 index b938abd2e..000000000 --- a/frontend/src/pages/organization/SecretSharingSettingsPage/route.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router"; -import { zodValidator } from "@tanstack/zod-adapter"; -import { z } from "zod"; - -import { SecretSharingSettingsPage } from "./SecretSharingSettingsPage"; - -const SettingsPageQueryParams = z.object({ - selectedTab: z.string().catch("") -}); - -export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings" -)({ - component: SecretSharingSettingsPage, - validateSearch: zodValidator(SettingsPageQueryParams), - search: { - middlewares: [stripSearchParams({ selectedTab: "" })] - }, - context: () => ({ - breadcrumbs: [ - { - label: "Secret Sharing", - link: linkOptions({ to: "/organization/secret-sharing" }) - }, - { - label: "Settings" - } - ] - }) -}); diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 1562f302d..f95b37a5d 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -56,7 +56,6 @@ import { Route as organizationAccessManagementPageRouteImport } from './pages/or import { Route as adminGeneralPageRouteImport } from './pages/admin/GeneralPage/route' import { Route as secretManagerRedirectsRedirectApprovalPageImport } from './pages/secret-manager/redirects/redirect-approval-page' import { Route as adminResourceOverviewPageRouteImport } from './pages/admin/ResourceOverviewPage/route' -import { Route as organizationSecretSharingSettingsPageRouteImport } from './pages/organization/SecretSharingSettingsPage/route' import { Route as organizationRoleByIDPageRouteImport } from './pages/organization/RoleByIDPage/route' import { Route as organizationUserDetailsByIDPageRouteImport } from './pages/organization/UserDetailsByIDPage/route' import { Route as organizationIdentityDetailsByIDPageRouteImport } from './pages/organization/IdentityDetailsByIDPage/route' @@ -751,14 +750,6 @@ const adminResourceOverviewPageRouteRoute = getParentRoute: () => adminLayoutRoute, } as any) -const organizationSecretSharingSettingsPageRouteRoute = - organizationSecretSharingSettingsPageRouteImport.update({ - id: '/settings', - path: '/settings', - getParentRoute: () => - AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute, - } as any) - const organizationRoleByIDPageRouteRoute = organizationRoleByIDPageRouteImport.update({ id: '/roles/$roleId', @@ -2617,13 +2608,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof organizationRoleByIDPageRouteImport parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport } - '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings': { - id: '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings' - path: '/settings' - fullPath: '/organization/secret-sharing/settings' - preLoaderRoute: typeof organizationSecretSharingSettingsPageRouteImport - parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingImport - } '/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview': { id: '/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview' path: '/resources/overview' @@ -4018,15 +4002,12 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteWithChildr interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteChildren { organizationSecretSharingPageRouteRoute: typeof organizationSecretSharingPageRouteRoute - organizationSecretSharingSettingsPageRouteRoute: typeof organizationSecretSharingSettingsPageRouteRoute } const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteChildren = { organizationSecretSharingPageRouteRoute: organizationSecretSharingPageRouteRoute, - organizationSecretSharingSettingsPageRouteRoute: - organizationSecretSharingSettingsPageRouteRoute, } const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren = @@ -5077,7 +5058,6 @@ export interface FileRoutesByFullPath { '/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute '/organization/members/$membershipId': typeof organizationUserDetailsByIDPageRouteRoute '/organization/roles/$roleId': typeof organizationRoleByIDPageRouteRoute - '/organization/secret-sharing/settings': typeof organizationSecretSharingSettingsPageRouteRoute '/admin/resources/overview': typeof adminResourceOverviewPageRouteRoute '/projects/cert-management/$projectId': typeof certManagerLayoutRouteWithChildren '/projects/kms/$projectId': typeof kmsLayoutRouteWithChildren @@ -5310,7 +5290,6 @@ export interface FileRoutesByTo { '/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute '/organization/members/$membershipId': typeof organizationUserDetailsByIDPageRouteRoute '/organization/roles/$roleId': typeof organizationRoleByIDPageRouteRoute - '/organization/secret-sharing/settings': typeof organizationSecretSharingSettingsPageRouteRoute '/admin/resources/overview': typeof adminResourceOverviewPageRouteRoute '/projects/cert-management/$projectId': typeof certManagerLayoutRouteWithChildren '/projects/kms/$projectId': typeof kmsLayoutRouteWithChildren @@ -5549,7 +5528,6 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/members/$membershipId': typeof organizationUserDetailsByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId': typeof organizationRoleByIDPageRouteRoute - '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings': typeof organizationSecretSharingSettingsPageRouteRoute '/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview': typeof adminResourceOverviewPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdRouteWithChildren @@ -5798,7 +5776,6 @@ export interface FileRouteTypes { | '/organization/identities/$identityId' | '/organization/members/$membershipId' | '/organization/roles/$roleId' - | '/organization/secret-sharing/settings' | '/admin/resources/overview' | '/projects/cert-management/$projectId' | '/projects/kms/$projectId' @@ -6030,7 +6007,6 @@ export interface FileRouteTypes { | '/organization/identities/$identityId' | '/organization/members/$membershipId' | '/organization/roles/$roleId' - | '/organization/secret-sharing/settings' | '/admin/resources/overview' | '/projects/cert-management/$projectId' | '/projects/kms/$projectId' @@ -6267,7 +6243,6 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId' | '/_authenticate/_inject-org-details/_org-layout/organization/members/$membershipId' | '/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId' - | '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings' | '/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview' | '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId' | '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId' @@ -6791,8 +6766,7 @@ export const routeTree = rootRoute "filePath": "", "parent": "/_authenticate/_inject-org-details/_org-layout/organization", "children": [ - "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/", - "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings" + "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/" ] }, "/_authenticate/_inject-org-details/_org-layout/organization/settings": { @@ -6842,10 +6816,6 @@ export const routeTree = rootRoute "filePath": "organization/RoleByIDPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization" }, - "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings": { - "filePath": "organization/SecretSharingSettingsPage/route.tsx", - "parent": "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing" - }, "/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview": { "filePath": "admin/ResourceOverviewPage/route.tsx", "parent": "/_authenticate/_inject-org-details/admin/_admin-layout" diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index bebc50339..9fb30c1c5 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -21,10 +21,7 @@ const organizationRoutes = route("/organization", [ route("/access-management", "organization/AccessManagementPage/route.tsx"), route("/audit-logs", "organization/AuditLogsPage/route.tsx"), route("/billing", "organization/BillingPage/route.tsx"), - route("/secret-sharing", [ - index("organization/SecretSharingPage/route.tsx"), - route("/settings", "organization/SecretSharingSettingsPage/route.tsx") - ]), + route("/secret-sharing", [index("organization/SecretSharingPage/route.tsx")]), route("/settings", [ index("organization/SettingsPage/route.tsx"), route("/oauth/callback", "organization/SettingsPage/OauthCallbackPage/route.tsx") From 8a1b57281381766a324dd151dc1709e104e44108 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 21 Oct 2025 23:29:00 +0530 Subject: [PATCH 35/44] feat: fixed cleanup on leaving root and api issue in frontend --- backend/src/ee/services/scim/scim-service.ts | 1 + .../membership-user-service.ts | 4 ++-- backend/src/services/org/org-fns.ts | 19 ++++++------------- .../OrganizationContext.tsx | 4 ++-- frontend/src/pages/root.tsx | 12 +++++++++++- 5 files changed, 22 insertions(+), 18 deletions(-) diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts index 4c5ee04a3..8b9256023 100644 --- a/backend/src/ee/services/scim/scim-service.ts +++ b/backend/src/ee/services/scim/scim-service.ts @@ -57,6 +57,7 @@ type TScimServiceFactoryDep = { TOrgDALFactory, | "createMembership" | "findById" + | "find" | "findMembership" | "findMembershipWithScimFilter" | "deleteMembershipById" diff --git a/backend/src/services/membership-user/membership-user-service.ts b/backend/src/services/membership-user/membership-user-service.ts index f6265d2bb..4b14ee771 100644 --- a/backend/src/services/membership-user/membership-user-service.ts +++ b/backend/src/services/membership-user/membership-user-service.ts @@ -40,7 +40,7 @@ import { newProjectMembershipUserFactory } from "./project/project-membership-us type TMembershipUserServiceFactoryDep = { membershipUserDAL: TMembershipUserDALFactory; membershipRoleDAL: Pick; - orgDAL: Pick; + orgDAL: Pick; roleDAL: Pick; userDAL: TUserDALFactory; permissionService: Pick< @@ -405,7 +405,7 @@ export const membershipUserServiceFactory = ({ const membershipDoc = await membershipUserDAL.transaction(async (tx) => { if (dto.scopeData.scope === AccessScope.Organization) { const [doc] = await deleteOrgMembershipsFn({ - orgMembershipIds: [], + orgMembershipIds: [existingMembership.id], orgId: dto.permission.orgId, orgDAL, projectKeyDAL, diff --git a/backend/src/services/org/org-fns.ts b/backend/src/services/org/org-fns.ts index 78d52e816..b887eeea1 100644 --- a/backend/src/services/org/org-fns.ts +++ b/backend/src/services/org/org-fns.ts @@ -13,7 +13,7 @@ import { TMembershipUserDALFactory } from "../membership-user/membership-user-da type TDeleteOrgMemberships = { orgMembershipIds: string[]; orgId: string; - orgDAL: Pick; + orgDAL: Pick; userGroupMembershipDAL: Pick; membershipUserDAL: Pick; membershipRoleDAL: Pick; @@ -34,19 +34,9 @@ export const deleteOrgMembershipsFn = async ({ userId, membershipUserDAL, userGroupMembershipDAL, - membershipRoleDAL, additionalPrivilegeDAL }: TDeleteOrgMemberships) => { const deletedMemberships = await orgDAL.transaction(async (tx) => { - await membershipRoleDAL.delete( - { - $in: { - membershipId: orgMembershipIds - } - }, - tx - ); - const orgMemberships = await membershipUserDAL.delete( { scopeOrgId: orgId, @@ -83,12 +73,13 @@ export const deleteOrgMembershipsFn = async ({ ); // Get all the project memberships of the users in the organization + const childOrgs = await orgDAL.find({ rootOrgId: orgId }, { tx }); // Delete all the project memberships of the users in the organization const otherMemberships = await membershipUserDAL.delete( { - scopeOrgId: orgId, $in: { + scopeOrgId: [orgId].concat(childOrgs.map((el) => el.id)), actorUserId: membershipUserIds } }, @@ -96,7 +87,9 @@ export const deleteOrgMembershipsFn = async ({ ); const orgGroups = await membershipUserDAL.find({ - scopeOrgId: orgId, + $in: { + scopeOrgId: [orgId].concat(childOrgs.map((el) => el.id)) + }, $notNull: ["actorGroupId"] }); diff --git a/frontend/src/context/OrganizationContext/OrganizationContext.tsx b/frontend/src/context/OrganizationContext/OrganizationContext.tsx index 07216b6d7..bfc17d143 100644 --- a/frontend/src/context/OrganizationContext/OrganizationContext.tsx +++ b/frontend/src/context/OrganizationContext/OrganizationContext.tsx @@ -16,7 +16,7 @@ export const useOrganization = () => { }); const { data: currentOrg } = useSuspenseQuery({ - queryKey: organizationKeys.getOrgById(organizationId, subOrganization), + queryKey: organizationKeys.getOrgById(organizationId, subOrganization || "root"), queryFn: () => fetchOrganizationById(organizationId), staleTime: Infinity }); @@ -31,7 +31,7 @@ export const useOrganization = () => { isSubOrganization: Boolean(currentOrg.subOrganization), isRootOrganization: !currentOrg.subOrganization }), - [currentOrg] + [currentOrg, subOrganization] ); return org; diff --git a/frontend/src/pages/root.tsx b/frontend/src/pages/root.tsx index 42aea6d8f..c8062a5a3 100644 --- a/frontend/src/pages/root.tsx +++ b/frontend/src/pages/root.tsx @@ -1,11 +1,12 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { createRootRouteWithContext, Outlet } from "@tanstack/react-router"; +import { createRootRouteWithContext, Outlet, useSearch } from "@tanstack/react-router"; import { NotificationContainer } from "@app/components/notifications"; import { TooltipProvider } from "@app/components/v2"; import { adminQueryKeys, fetchServerConfig } from "@app/hooks/api/admin/queries"; import { TServerConfig } from "@app/hooks/api/admin/types"; import { queryClient } from "@app/hooks/api/reactQuery"; +import { useEffect } from "react"; type TRouterContext = { serverConfig: TServerConfig | null; @@ -13,6 +14,15 @@ type TRouterContext = { }; const RootPage = () => { + const subOrganization = useSearch({ + strict: false, + select: (el) => el?.subOrganization + }); + + useEffect(() => { + queryClient.clear(); + }, [subOrganization]); + return ( From 3b3203560cb9b85fa69ac1d5f89b85bb4ea9dda2 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Tue, 21 Oct 2025 10:23:11 -0700 Subject: [PATCH 36/44] fix: correct sub-org invite url query param and add hypen to sub org email template --- .../membership-user/org/org-membership-user-factory.ts | 2 +- .../smtp/emails/SubOrganizationInvitationTemplate.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/src/services/membership-user/org/org-membership-user-factory.ts b/backend/src/services/membership-user/org/org-membership-user-factory.ts index 2da263426..d21b27b69 100644 --- a/backend/src/services/membership-user/org/org-membership-user-factory.ts +++ b/backend/src/services/membership-user/org/org-membership-user-factory.ts @@ -129,7 +129,7 @@ export const newOrgMembershipUserFactory = ({ recipients: emails as string[], substitutions: { subOrganizationName: orgDetails.slug, - callback_url: `${appCfg.SITE_URL}/organization/projects?${orgDetails.slug}` + callback_url: `${appCfg.SITE_URL}/organization/projects?subOrganization=${orgDetails.slug}` } }); } else { diff --git a/backend/src/services/smtp/emails/SubOrganizationInvitationTemplate.tsx b/backend/src/services/smtp/emails/SubOrganizationInvitationTemplate.tsx index e0b347dae..da93fc045 100644 --- a/backend/src/services/smtp/emails/SubOrganizationInvitationTemplate.tsx +++ b/backend/src/services/smtp/emails/SubOrganizationInvitationTemplate.tsx @@ -17,19 +17,19 @@ export const SubOrganizationInvitationTemplate = ({ return ( - You've been invited to join a suborganization on Infisical + You've been invited to join a sub-organization on Infisical
- You've been invited to join the suborganization {subOrganizationName}. + You've been invited to join the sub-organization {subOrganizationName}.
- Join Suborganization + Join Sub-Organization
From b25f5010df0f6b72e2dc838f1b292e63cebd06a9 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Tue, 21 Oct 2025 10:33:42 -0700 Subject: [PATCH 37/44] fix: correct display for delete sub-org button --- .../components/OrgDeleteSection/OrgDeleteSection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx index e78ad61b9..4dfe89f71 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx @@ -55,7 +55,7 @@ export const OrgDeleteSection = () => { onClick={() => handlePopUpOpen("deleteOrg")} isDisabled={Boolean(!hasOrgRole(OrgMembershipRole.Admin))} > - {`Delete ${currentOrg?.name}`} + {`Delete ${currentOrg.subOrganization?.name ?? currentOrg?.name}`}
Date: Tue, 21 Oct 2025 11:29:58 -0700 Subject: [PATCH 38/44] fix: remove redundant query clients --- .../layouts/OrganizationLayout/components/NavBar/Navbar.tsx | 4 +--- .../components/NavBar/NewSubOrganizationForm.tsx | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx index 7e926d690..dab5c5d9f 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx @@ -294,7 +294,6 @@ export const Navbar = () => { search: (search) => ({ ...search, subOrganization: undefined }) }); if (isSubOrganization) { - queryClient.clear(); await router.invalidate({ sync: true }).catch(() => null); } }} @@ -384,7 +383,7 @@ export const Navbar = () => { to: "/organization/projects", search: (prev) => ({ ...prev, subOrganization: subOrg.name }) }); - queryClient.clear(); + await router.invalidate({ sync: true }).catch(() => null); }} className="cursor-pointer font-normal" @@ -486,7 +485,6 @@ export const Navbar = () => { to: "/organization/projects", search: (prev) => ({ ...prev, subOrganization: subOrg.name }) }); - queryClient.clear(); await router.invalidate({ sync: true }).catch(() => null); }} className="cursor-pointer font-normal" diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx index 869ad00b1..d74e1d5dd 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx @@ -34,7 +34,6 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => { }); const navigate = useNavigate(); - const queryClient = useQueryClient(); const router = useRouter(); const onSubmit = async ({ name }: FormData) => { @@ -53,7 +52,7 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => { to: "/organization/projects", search: (prev) => ({ ...prev, subOrganization: organization.name }) }); - queryClient.clear(); + await router.invalidate({ sync: true }).catch(() => null); } catch { createNotification({ From 894fa90b8334bb0ba855f4c56141fe8489e16b1d Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Tue, 21 Oct 2025 12:01:08 -0700 Subject: [PATCH 39/44] imrpovement: improve nav bar truncation for sub-orgs --- .../layouts/OrganizationLayout/components/NavBar/Navbar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx index dab5c5d9f..314c48df4 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx @@ -444,11 +444,11 @@ export const Navbar = () => { <>

/

- + Date: Tue, 21 Oct 2025 12:02:57 -0700 Subject: [PATCH 40/44] chore: remove unused dep --- .../components/NavBar/NewSubOrganizationForm.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx index d74e1d5dd..3f743663c 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx @@ -1,6 +1,5 @@ import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useQueryClient } from "@tanstack/react-query"; import { useNavigate, useRouter } from "@tanstack/react-router"; import { z } from "zod"; From cf40bbf3a7fc12a20007147ed69732797f71f30e Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Tue, 21 Oct 2025 12:15:09 -0700 Subject: [PATCH 41/44] fix: dont show resend invite on sub-org user details page --- .../UserDetailsByIDPage/components/UserDetailsSection.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx index 680795e2e..bef98e875 100644 --- a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx +++ b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx @@ -35,7 +35,7 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) => }); const { user } = useUser(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const userId = user?.id || ""; const orgId = currentOrg?.id || ""; @@ -214,7 +214,8 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>

-

)}
- {membership.isActive && + {!isSubOrganization && + membership.isActive && (membership.status === "invited" || membership.status === "verified") && membership.user.email && serverDetails?.emailConfigured && ( From 008b67738b42a8d644454a29ea1c0f2d9c150304 Mon Sep 17 00:00:00 2001 From: = Date: Wed, 22 Oct 2025 01:44:11 +0530 Subject: [PATCH 42/44] feat: patchy patchy for request secret --- backend/src/ee/services/license/license-fns.ts | 2 +- backend/src/ee/services/license/license-types.ts | 2 +- .../components/NavBar/Navbar.tsx | 1 - .../components/NavBar/NewSubOrganizationForm.tsx | 1 - .../components/RequestSecret/RequestSecretForm.tsx | 14 +++++++++++--- .../SubOrgNameChangeSection.tsx | 2 +- frontend/src/pages/root.tsx | 4 ++-- 7 files changed, 16 insertions(+), 10 deletions(-) diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index 8d0d9d74b..0981d93a8 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -28,7 +28,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ rbac: false, githubOrgSync: false, customRateLimits: false, - subOrganization: true, + subOrganization: false, customAlerts: false, secretAccessInsights: false, auditLogs: false, diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 88c1edb2e..e1fb5ab77 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -33,7 +33,7 @@ export type TFeatureSet = { membersUsed: number; identityLimit: null; identitiesUsed: number; - subOrganization: true; + subOrganization: false; environmentLimit: null; environmentsUsed: 0; secretVersioning: true; diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx index 314c48df4..f9ef51133 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx @@ -383,7 +383,6 @@ export const Navbar = () => { to: "/organization/projects", search: (prev) => ({ ...prev, subOrganization: subOrg.name }) }); - await router.invalidate({ sync: true }).catch(() => null); }} className="cursor-pointer font-normal" diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx index 3f743663c..75041a69e 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx @@ -51,7 +51,6 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => { to: "/organization/projects", search: (prev) => ({ ...prev, subOrganization: organization.name }) }); - await router.invalidate({ sync: true }).catch(() => null); } catch { createNotification({ diff --git a/frontend/src/pages/organization/SecretSharingPage/components/RequestSecret/RequestSecretForm.tsx b/frontend/src/pages/organization/SecretSharingPage/components/RequestSecret/RequestSecretForm.tsx index d2ada2b29..50a9e0f58 100644 --- a/frontend/src/pages/organization/SecretSharingPage/components/RequestSecret/RequestSecretForm.tsx +++ b/frontend/src/pages/organization/SecretSharingPage/components/RequestSecret/RequestSecretForm.tsx @@ -3,6 +3,7 @@ import { Controller, useForm } from "react-hook-form"; import { faCheck, faCopy, faRedo } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useSearch } from "@tanstack/react-router"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; @@ -36,6 +37,10 @@ export const RequestSecretForm = () => { const [, isCopyingSecret, setCopyTextSecret] = useTimedReset({ initialState: "Copy to clipboard" }); + const subOrganization = useSearch({ + strict: false, + select: (el) => el?.subOrganization + }); const { mutateAsync: createSecretRequest } = useCreateSecretRequest(); @@ -58,12 +63,15 @@ export const RequestSecretForm = () => { expiresAt }); - const link = `${window.location.origin}/secret-request/secret/${id}`; + const link = new URL(`${window.location.origin}/secret-request/secret/${id}`); + if (subOrganization) { + link.searchParams.set("subOrganization", subOrganization); + } - setSecretLink(link); + setSecretLink(link.toString()); reset(); - navigator.clipboard.writeText(link); + navigator.clipboard.writeText(link.toString()); setCopyTextSecret("secret"); createNotification({ diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/SubOrgNameChangeSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/SubOrgNameChangeSection.tsx index 38eb5f012..ca625be6c 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/SubOrgNameChangeSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/SubOrgNameChangeSection.tsx @@ -46,7 +46,7 @@ export const SubOrgNameChangeSection = (): JSX.Element => { }); navigate({ to: "/organization/settings", search: { subOrganization: name } }); - queryClient.clear(); + queryClient.invalidateQueries(); await router.invalidate({ sync: true }); createNotification({ text: "Successfully updated sub-organization details", diff --git a/frontend/src/pages/root.tsx b/frontend/src/pages/root.tsx index c8062a5a3..ee47a5472 100644 --- a/frontend/src/pages/root.tsx +++ b/frontend/src/pages/root.tsx @@ -1,3 +1,4 @@ +import { useEffect } from "react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { createRootRouteWithContext, Outlet, useSearch } from "@tanstack/react-router"; @@ -6,7 +7,6 @@ import { TooltipProvider } from "@app/components/v2"; import { adminQueryKeys, fetchServerConfig } from "@app/hooks/api/admin/queries"; import { TServerConfig } from "@app/hooks/api/admin/types"; import { queryClient } from "@app/hooks/api/reactQuery"; -import { useEffect } from "react"; type TRouterContext = { serverConfig: TServerConfig | null; @@ -20,7 +20,7 @@ const RootPage = () => { }); useEffect(() => { - queryClient.clear(); + queryClient.invalidateQueries(); }, [subOrganization]); return ( From 083e8e605f2d30f7a969493461a16caa6e0dfe2b Mon Sep 17 00:00:00 2001 From: = Date: Wed, 22 Oct 2025 02:04:55 +0530 Subject: [PATCH 43/44] feat: fixed count inconsistency --- backend/src/services/project/project-dal.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/src/services/project/project-dal.ts b/backend/src/services/project/project-dal.ts index 2abdebdbc..11d4239db 100644 --- a/backend/src/services/project/project-dal.ts +++ b/backend/src/services/project/project-dal.ts @@ -413,10 +413,12 @@ export const projectDALFactory = (db: TDbClient) => { const countOfOrgProjects = async (orgId: string | null, tx?: Knex) => { try { + const subOrgProjects = db.replicaNode()(TableName.Organization).where({ rootOrgId: orgId }).select("id"); + const doc = await (tx || db.replicaNode())(TableName.Project) .andWhere((bd) => { if (orgId) { - void bd.where({ orgId }); + void bd.where({ orgId }).orWhereIn("orgId", subOrgProjects); } }) .count(); From 2215362cc816204099908091e8431b60550d07c4 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Tue, 21 Oct 2025 14:05:01 -0700 Subject: [PATCH 44/44] improvement: add managed by badge to add identity to project select --- .../server/routes/v2/identity-org-router.ts | 2 +- .../IdentityTab/components/IdentityModal.tsx | 38 ++++++++++++++++++- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/backend/src/server/routes/v2/identity-org-router.ts b/backend/src/server/routes/v2/identity-org-router.ts index 8680a2dca..630e09dda 100644 --- a/backend/src/server/routes/v2/identity-org-router.ts +++ b/backend/src/server/routes/v2/identity-org-router.ts @@ -60,7 +60,7 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => { permissions: true, description: true }).optional(), - identity: IdentitiesSchema.pick({ name: true, id: true }).extend({ + identity: IdentitiesSchema.pick({ name: true, id: true, orgId: true }).extend({ authMethods: z.array(z.string()) }) }) diff --git a/frontend/src/pages/project/AccessControlPage/components/IdentityTab/components/IdentityModal.tsx b/frontend/src/pages/project/AccessControlPage/components/IdentityTab/components/IdentityModal.tsx index 7261ba839..ce8ca2fbd 100644 --- a/frontend/src/pages/project/AccessControlPage/components/IdentityTab/components/IdentityModal.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/IdentityTab/components/IdentityModal.tsx @@ -1,11 +1,16 @@ 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 { faGlobe } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { Link } from "@tanstack/react-router"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { + Badge, Button, FilterableSelect, FormControl, @@ -24,7 +29,7 @@ import { import { UsePopUpState } from "@app/hooks/usePopUp"; const schema = z.object({ - identity: z.object({ name: z.string(), id: z.string() }), + identity: z.object({ name: z.string(), id: z.string(), isManagedByRootOrg: z.boolean() }), role: z.object({ name: z.string(), slug: z.string() }) }); @@ -35,6 +40,29 @@ type Props = { 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 && ( + + + Managed by Root Org + + )} + {isSelected && ( + + )} +
+
+ ); +}; + const Content = ({ popUp, handlePopUpToggle }: Props) => { const { currentOrg } = useOrganization(); const { projectId } = useProject(); @@ -131,9 +159,15 @@ const Content = ({ popUp, handlePopUpToggle }: Props) => { value={value} onChange={onChange} placeholder="Select identity..." - options={filteredIdentityMembershipOrgs.map((membership) => membership.identity)} + options={filteredIdentityMembershipOrgs.map((membership) => ({ + ...membership.identity, + isManagedByRootOrg: membership.identity.orgId !== currentOrg.id + }))} getOptionValue={(option) => option.id} getOptionLabel={(option) => option.name} + components={{ + Option + }} /> )}