From f0edcd1f0f1d52f386df9929be95fe2657e885cd Mon Sep 17 00:00:00 2001 From: x032205 Date: Thu, 16 Oct 2025 18:12:03 -0400 Subject: [PATCH 001/100] feat(pam): account credential rotation --- .../20251015042917_pam-account-rotation.ts | 43 +++++++ backend/src/db/schemas/pam-accounts.ts | 5 +- backend/src/db/schemas/pam-resources.ts | 4 +- .../pam-account-endpoints.ts | 12 +- .../pam-resource-endpoints.ts | 2 + .../ee/services/audit-log/audit-log-types.ts | 16 +++ .../services/pam-account/pam-account-dal.ts | 16 ++- .../pam-account/pam-account-service.ts | 114 +++++++++++++++++- .../services/pam-account/pam-account-types.ts | 5 +- .../services/pam-resource/pam-resource-fns.ts | 10 +- .../pam-resource/pam-resource-schemas.ts | 9 +- .../pam-resource/pam-resource-service.ts | 60 ++++++++- .../pam-resource/pam-resource-types.ts | 7 +- .../postgres/postgres-resource-schemas.ts | 14 ++- .../shared/sql/sql-resource-factory.ts | 67 +++++++++- backend/src/queue/queue-service.ts | 10 +- backend/src/server/routes/index.ts | 10 +- .../pam-account-rotation-queue.ts | 61 ++++++++++ .../src/hooks/api/pam/types/base-account.ts | 3 + .../hooks/api/pam/types/postgres-resource.ts | 1 + .../PamAccountForm/PamAccountForm.tsx | 10 +- .../PamAccountForm/PostgresAccountForm.tsx | 4 +- .../PamAccountForm/RotateAccountFields.tsx | 71 +++++++++++ .../PamResourceForm/PostgresResourceForm.tsx | 6 +- .../shared/SqlRotateAccountFields.tsx | 64 ++++++++++ 25 files changed, 588 insertions(+), 36 deletions(-) create mode 100644 backend/src/db/migrations/20251015042917_pam-account-rotation.ts create mode 100644 backend/src/services/pam-account-rotation/pam-account-rotation-queue.ts create mode 100644 frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/RotateAccountFields.tsx create mode 100644 frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/shared/SqlRotateAccountFields.tsx diff --git a/backend/src/db/migrations/20251015042917_pam-account-rotation.ts b/backend/src/db/migrations/20251015042917_pam-account-rotation.ts new file mode 100644 index 000000000..cd9a7d19a --- /dev/null +++ b/backend/src/db/migrations/20251015042917_pam-account-rotation.ts @@ -0,0 +1,43 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if ( + !(await knex.schema.hasColumn(TableName.PamAccount, "rotationEnabled")) && + !(await knex.schema.hasColumn(TableName.PamAccount, "rotationIntervalSeconds")) && + !(await knex.schema.hasColumn(TableName.PamAccount, "lastRotatedAt")) + ) { + await knex.schema.alterTable(TableName.PamAccount, (t) => { + t.boolean("rotationEnabled").notNullable().defaultTo(false); + t.integer("rotationIntervalSeconds").notNullable().defaultTo(2592000); + t.timestamp("lastRotatedAt").nullable(); + }); + } + + if (!(await knex.schema.hasColumn(TableName.PamResource, "encryptedRotationAccountCredentials"))) { + await knex.schema.alterTable(TableName.PamResource, (t) => { + t.binary("encryptedRotationAccountCredentials").nullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.PamResource, "encryptedRotationAccountCredentials")) { + await knex.schema.alterTable(TableName.PamResource, (t) => { + t.dropColumn("encryptedRotationAccountCredentials"); + }); + } + + if ( + (await knex.schema.hasColumn(TableName.PamAccount, "rotationEnabled")) && + (await knex.schema.hasColumn(TableName.PamAccount, "rotationIntervalSeconds")) && + (await knex.schema.hasColumn(TableName.PamAccount, "lastRotatedAt")) + ) { + await knex.schema.alterTable(TableName.PamAccount, (t) => { + t.dropColumn("lastRotatedAt"); + t.dropColumn("rotationIntervalSeconds"); + t.dropColumn("rotationEnabled"); + }); + } +} diff --git a/backend/src/db/schemas/pam-accounts.ts b/backend/src/db/schemas/pam-accounts.ts index 5a9a45617..9bcfa5cec 100644 --- a/backend/src/db/schemas/pam-accounts.ts +++ b/backend/src/db/schemas/pam-accounts.ts @@ -18,7 +18,10 @@ export const PamAccountsSchema = z.object({ description: z.string().nullable().optional(), encryptedCredentials: zodBuffer, createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + rotationEnabled: z.boolean().default(false), + rotationIntervalSeconds: z.number().default(2592000), + lastRotatedAt: z.date().nullable().optional() }); export type TPamAccounts = z.infer; diff --git a/backend/src/db/schemas/pam-resources.ts b/backend/src/db/schemas/pam-resources.ts index d34017d0f..929a31c79 100644 --- a/backend/src/db/schemas/pam-resources.ts +++ b/backend/src/db/schemas/pam-resources.ts @@ -17,7 +17,9 @@ export const PamResourcesSchema = z.object({ resourceType: z.string(), encryptedConnectionDetails: zodBuffer, createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + encryptedRotationAccountDetails: zodBuffer.nullable().optional(), + encryptedRotationAccountCredentials: zodBuffer.nullable().optional() }); export type TPamResources = z.infer; diff --git a/backend/src/ee/routes/v1/pam-account-routers/pam-account-endpoints.ts b/backend/src/ee/routes/v1/pam-account-routers/pam-account-endpoints.ts index 0ed7e238a..dccaaa8ca 100644 --- a/backend/src/ee/routes/v1/pam-account-routers/pam-account-endpoints.ts +++ b/backend/src/ee/routes/v1/pam-account-routers/pam-account-endpoints.ts @@ -22,11 +22,15 @@ export const registerPamResourceEndpoints = ({ folderId?: C["folderId"]; name: C["name"]; description?: C["description"]; + rotationEnabled: C["rotationEnabled"]; + rotationIntervalSeconds: C["rotationIntervalSeconds"]; }>; updateAccountSchema: z.ZodType<{ credentials?: C["credentials"]; name?: C["name"]; description?: C["description"]; + rotationEnabled?: C["rotationEnabled"]; + rotationIntervalSeconds?: C["rotationIntervalSeconds"]; }>; accountResponseSchema: z.ZodTypeAny; }) => { @@ -60,7 +64,9 @@ export const registerPamResourceEndpoints = ({ resourceType, folderId: req.body.folderId, name: req.body.name, - description: req.body.description + description: req.body.description, + rotationEnabled: req.body.rotationEnabled, + rotationIntervalSeconds: req.body.rotationIntervalSeconds } } }); @@ -108,7 +114,9 @@ export const registerPamResourceEndpoints = ({ resourceId: account.resourceId, resourceType, name: req.body.name, - description: req.body.description + description: req.body.description, + rotationEnabled: req.body.rotationEnabled, + rotationIntervalSeconds: req.body.rotationIntervalSeconds } } }); diff --git a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-endpoints.ts b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-endpoints.ts index 776de8e48..ffbeae5c0 100644 --- a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-endpoints.ts +++ b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-endpoints.ts @@ -21,11 +21,13 @@ export const registerPamResourceEndpoints = ({ connectionDetails: T["connectionDetails"]; gatewayId: T["gatewayId"]; name: T["name"]; + rotationAccountCredentials?: T["rotationAccountCredentials"]; }>; updateResourceSchema: z.ZodType<{ connectionDetails?: T["connectionDetails"]; gatewayId?: T["gatewayId"]; name?: T["name"]; + rotationAccountCredentials?: T["rotationAccountCredentials"]; }>; resourceResponseSchema: z.ZodTypeAny; }) => { 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..ff2dafa9f 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -515,6 +515,7 @@ export enum EventType { PAM_ACCOUNT_CREATE = "pam-account-create", PAM_ACCOUNT_UPDATE = "pam-account-update", PAM_ACCOUNT_DELETE = "pam-account-delete", + PAM_ACCOUNT_CREDENTIAL_ROTATION = "pam-account-credential-rotation", PAM_RESOURCE_LIST = "pam-resource-list", PAM_RESOURCE_GET = "pam-resource-get", PAM_RESOURCE_CREATE = "pam-resource-create", @@ -3795,6 +3796,8 @@ interface PamAccountCreateEvent { folderId?: string | null; name: string; description?: string | null; + rotationEnabled: boolean; + rotationIntervalSeconds: number; }; } @@ -3806,6 +3809,8 @@ interface PamAccountUpdateEvent { resourceType: string; name?: string; description?: string | null; + rotationEnabled?: boolean; + rotationIntervalSeconds?: number; }; } @@ -3819,6 +3824,16 @@ interface PamAccountDeleteEvent { }; } +interface PamAccountCredentialRotationEvent { + type: EventType.PAM_ACCOUNT_CREDENTIAL_ROTATION; + metadata: { + accountName: string; + accountId: string; + resourceId: string; + resourceType: string; + }; +} + interface PamResourceListEvent { type: EventType.PAM_RESOURCE_LIST; metadata: { @@ -4209,6 +4224,7 @@ export type Event = | PamAccountCreateEvent | PamAccountUpdateEvent | PamAccountDeleteEvent + | PamAccountCredentialRotationEvent | PamResourceListEvent | PamResourceGetEvent | PamResourceCreateEvent diff --git a/backend/src/ee/services/pam-account/pam-account-dal.ts b/backend/src/ee/services/pam-account/pam-account-dal.ts index b62e940fe..c422c2b1d 100644 --- a/backend/src/ee/services/pam-account/pam-account-dal.ts +++ b/backend/src/ee/services/pam-account/pam-account-dal.ts @@ -39,5 +39,19 @@ export const pamAccountDALFactory = (db: TDbClient) => { })); }; - return { ...orm, findWithResourceDetails }; + const findAccountsDueForRotation = async (tx?: Knex) => { + const dbClient = tx || db.replicaNode(); + + const accounts = await dbClient(TableName.PamAccount) + .innerJoin(TableName.PamResource, `${TableName.PamAccount}.resourceId`, `${TableName.PamResource}.id`) + .whereNotNull(`${TableName.PamResource}.encryptedRotationAccountCredentials`) + .whereRaw( + `COALESCE("${TableName.PamAccount}"."lastRotatedAt", "${TableName.PamAccount}"."createdAt") + "${TableName.PamAccount}"."rotationIntervalSeconds" * interval '1 second' < NOW()` + ) + .select(selectAllTableCols(TableName.PamAccount)); + + return accounts; + }; + + return { ...orm, findWithResourceDetails, findAccountsDueForRotation }; }; 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..6146c1a13 100644 --- a/backend/src/ee/services/pam-account/pam-account-service.ts +++ b/backend/src/ee/services/pam-account/pam-account-service.ts @@ -11,12 +11,14 @@ import { } from "@app/ee/services/permission/project-permission"; import { DatabaseErrorCode } from "@app/lib/error-codes"; import { BadRequestError, DatabaseError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; import { OrgServiceActor } from "@app/lib/types"; import { ActorType } from "@app/services/auth/auth-type"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TUserDALFactory } from "@app/services/user/user-dal"; +import { EventType, TAuditLogServiceFactory } from "../audit-log/audit-log-types"; import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service"; import { TLicenseServiceFactory } from "../license/license-service"; import { TPamFolderDALFactory } from "../pam-folder/pam-folder-dal"; @@ -45,10 +47,12 @@ type TPamAccountServiceFactoryDep = { "getPAMConnectionDetails" | "getPlatformConnectionDetailsByGatewayId" >; userDAL: TUserDALFactory; + auditLogService: Pick; }; - export type TPamAccountServiceFactory = ReturnType; +const ROTATION_CONCURRENCY_LIMIT = 10; + export const pamAccountServiceFactory = ({ pamResourceDAL, pamSessionDAL, @@ -59,10 +63,19 @@ export const pamAccountServiceFactory = ({ permissionService, licenseService, kmsService, - gatewayV2Service + gatewayV2Service, + auditLogService }: TPamAccountServiceFactoryDep) => { const create = async ( - { credentials, resourceId, name, description, folderId }: TCreateAccountDTO, + { + credentials, + resourceId, + name, + description, + folderId, + rotationEnabled, + rotationIntervalSeconds + }: TCreateAccountDTO, actor: OrgServiceActor ) => { const orgLicensePlan = await licenseService.getPlan(actor.orgId); @@ -84,6 +97,10 @@ export const pamAccountServiceFactory = ({ actionProjectType: ActionProjectType.PAM }); + if (!resource.encryptedRotationAccountCredentials && rotationEnabled) { + throw new NotFoundError({ message: "Rotation credentials are not configured for this account's resource" }); + } + const accountPath = await getFullPamFolderPath({ pamFolderDAL, folderId, @@ -126,7 +143,9 @@ export const pamAccountServiceFactory = ({ encryptedCredentials, name, description, - folderId + folderId, + rotationEnabled, + rotationIntervalSeconds }); return { @@ -145,7 +164,7 @@ export const pamAccountServiceFactory = ({ }; const updateById = async ( - { accountId, credentials, description, name }: TUpdateAccountDTO, + { accountId, credentials, description, name, rotationEnabled, rotationIntervalSeconds }: TUpdateAccountDTO, actor: OrgServiceActor ) => { const orgLicensePlan = await licenseService.getPlan(actor.orgId); @@ -195,6 +214,17 @@ export const pamAccountServiceFactory = ({ updateDoc.description = description; } + if (rotationEnabled !== undefined) { + if (!resource.encryptedRotationAccountCredentials && rotationEnabled) { + throw new NotFoundError({ message: "Rotation credentials are not configured for this account's resource" }); + } + updateDoc.rotationEnabled = rotationEnabled; + } + + if (rotationIntervalSeconds !== undefined) { + updateDoc.rotationIntervalSeconds = rotationIntervalSeconds; + } + if (credentials !== undefined) { const connectionDetails = await decryptResourceConnectionDetails({ projectId: account.projectId, @@ -516,12 +546,84 @@ export const pamAccountServiceFactory = ({ }; }; + const rotateAllDueAccounts = async () => { + const accounts = await pamAccountDAL.findAccountsDueForRotation(); + + for (let i = 0; i < accounts.length; i += ROTATION_CONCURRENCY_LIMIT) { + const batch = accounts.slice(i, i + ROTATION_CONCURRENCY_LIMIT); + + const rotationPromises = batch.map(async (account) => { + try { + const resource = await pamResourceDAL.findById(account.resourceId); + if (!resource || !resource.encryptedRotationAccountCredentials) return; + + const { connectionDetails, rotationAccountCredentials, gatewayId, resourceType } = await decryptResource( + resource, + account.projectId, + kmsService + ); + + if (!rotationAccountCredentials) return; + + const accountCredentials = await decryptAccountCredentials({ + encryptedCredentials: account.encryptedCredentials, + projectId: account.projectId, + kmsService + }); + + const factory = PAM_RESOURCE_FACTORY_MAP[resourceType as PamResource]( + resourceType as PamResource, + connectionDetails, + gatewayId, + gatewayV2Service + ); + + const newCredentials = await factory.rotateAccountCredentials(rotationAccountCredentials, accountCredentials); + + const encryptedCredentials = await encryptAccountCredentials({ + credentials: newCredentials, + projectId: account.projectId, + kmsService + }); + + await pamAccountDAL.updateById(account.id, { + encryptedCredentials, + lastRotatedAt: new Date() + }); + + await auditLogService.createAuditLog({ + projectId: account.projectId, + actor: { + type: ActorType.PLATFORM, + metadata: {} + }, + event: { + type: EventType.PAM_ACCOUNT_CREDENTIAL_ROTATION, + metadata: { + accountId: account.id, + accountName: account.name, + resourceId: resource.id, + resourceType: resource.resourceType + } + } + }); + } catch (error) { + logger.error(error, `Failed to rotate credentials for account ${account.id}`); + } + }); + + // eslint-disable-next-line no-await-in-loop + await Promise.all(rotationPromises); + } + }; + return { create, updateById, deleteById, list, access, - getSessionCredentials + getSessionCredentials, + rotateAllDueAccounts }; }; diff --git a/backend/src/ee/services/pam-account/pam-account-types.ts b/backend/src/ee/services/pam-account/pam-account-types.ts index 514d7d780..4bbccc6fa 100644 --- a/backend/src/ee/services/pam-account/pam-account-types.ts +++ b/backend/src/ee/services/pam-account/pam-account-types.ts @@ -1,7 +1,10 @@ import { TPamAccount } from "../pam-resource/pam-resource-types"; // DTOs -export type TCreateAccountDTO = Pick; +export type TCreateAccountDTO = Pick< + TPamAccount, + "name" | "description" | "credentials" | "folderId" | "resourceId" | "rotationEnabled" | "rotationIntervalSeconds" +>; export type TUpdateAccountDTO = Partial> & { accountId: string; diff --git a/backend/src/ee/services/pam-resource/pam-resource-fns.ts b/backend/src/ee/services/pam-resource/pam-resource-fns.ts index 1d79e892e..9d7493e68 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-fns.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-fns.ts @@ -2,6 +2,7 @@ import { TPamResources } from "@app/db/schemas"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { decryptAccountCredentials } from "../pam-account/pam-account-fns"; import { TPamResource, TPamResourceConnectionDetails } from "./pam-resource-types"; import { getPostgresResourceListItem } from "./postgres/postgres-resource-fns"; @@ -63,6 +64,13 @@ export const decryptResource = async ( encryptedConnectionDetails: resource.encryptedConnectionDetails, projectId, kmsService - }) + }), + rotationAccountCredentials: resource.encryptedRotationAccountCredentials + ? await decryptAccountCredentials({ + encryptedCredentials: resource.encryptedRotationAccountCredentials, + projectId, + kmsService + }) + : null } as TPamResource; }; diff --git a/backend/src/ee/services/pam-resource/pam-resource-schemas.ts b/backend/src/ee/services/pam-resource/pam-resource-schemas.ts index 80a50a9a4..5b0199166 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-schemas.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-schemas.ts @@ -6,6 +6,7 @@ import { slugSchema } from "@app/server/lib/schemas"; // Resources export const BasePamResourceSchema = PamResourcesSchema.omit({ encryptedConnectionDetails: true, + encryptedRotationAccountCredentials: true, resourceType: true }); @@ -37,10 +38,14 @@ export const BaseCreatePamAccountSchema = z.object({ resourceId: z.string().uuid(), folderId: z.string().uuid().optional(), name: slugSchema({ field: "name" }), - description: z.string().max(512).nullable().optional() + description: z.string().max(512).nullable().optional(), + rotationEnabled: z.boolean(), + rotationIntervalSeconds: z.number().min(3600) }); export const BaseUpdatePamAccountSchema = z.object({ name: slugSchema({ field: "name" }).optional(), - description: z.string().max(512).nullable().optional() + description: z.string().max(512).nullable().optional(), + rotationEnabled: z.boolean().optional(), + rotationIntervalSeconds: z.number().min(3600).optional() }); diff --git a/backend/src/ee/services/pam-resource/pam-resource-service.ts b/backend/src/ee/services/pam-resource/pam-resource-service.ts index 312795a50..1cc3ca135 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-service.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-service.ts @@ -10,10 +10,16 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service"; import { TLicenseServiceFactory } from "../license/license-service"; +import { encryptAccountCredentials } from "../pam-account/pam-account-fns"; import { TPamResourceDALFactory } from "./pam-resource-dal"; import { PamResource } from "./pam-resource-enums"; import { PAM_RESOURCE_FACTORY_MAP } from "./pam-resource-factory"; -import { decryptResource, encryptResourceConnectionDetails, listResourceOptions } from "./pam-resource-fns"; +import { + decryptResource, + decryptResourceConnectionDetails, + encryptResourceConnectionDetails, + listResourceOptions +} from "./pam-resource-fns"; import { TCreateResourceDTO, TUpdateResourceDTO } from "./pam-resource-types"; type TPamResourceServiceFactoryDep = { @@ -61,7 +67,7 @@ export const pamResourceServiceFactory = ({ }; const create = async ( - { resourceType, connectionDetails, gatewayId, name, projectId }: TCreateResourceDTO, + { resourceType, connectionDetails, gatewayId, name, projectId, rotationAccountCredentials }: TCreateResourceDTO, actor: OrgServiceActor ) => { const orgLicensePlan = await licenseService.getPlan(actor.orgId); @@ -88,26 +94,42 @@ export const pamResourceServiceFactory = ({ gatewayId, gatewayV2Service ); - const validatedConnectionDetails = await factory.validateConnection(); + const validatedConnectionDetails = await factory.validateConnection(); const encryptedConnectionDetails = await encryptResourceConnectionDetails({ connectionDetails: validatedConnectionDetails, projectId, kmsService }); + let encryptedRotationAccountCredentials: Buffer | null = null; + + if (rotationAccountCredentials) { + const validatedRotationAccountCredentials = await factory.validateAccountCredentials(rotationAccountCredentials); + + encryptedRotationAccountCredentials = await encryptAccountCredentials({ + credentials: validatedRotationAccountCredentials, + projectId, + kmsService + }); + } + const resource = await pamResourceDAL.create({ resourceType, encryptedConnectionDetails, gatewayId, name, - projectId + projectId, + encryptedRotationAccountCredentials }); return decryptResource(resource, projectId, kmsService); }; - const updateById = async ({ connectionDetails, resourceId, name }: TUpdateResourceDTO, actor: OrgServiceActor) => { + const updateById = async ( + { connectionDetails, resourceId, name, rotationAccountCredentials }: TUpdateResourceDTO, + actor: OrgServiceActor + ) => { const orgLicensePlan = await licenseService.getPlan(actor.orgId); if (!orgLicensePlan.pam) { throw new BadRequestError({ @@ -151,6 +173,34 @@ export const pamResourceServiceFactory = ({ updateDoc.encryptedConnectionDetails = encryptedConnectionDetails; } + if (rotationAccountCredentials !== undefined) { + updateDoc.encryptedRotationAccountCredentials = null; + + if (rotationAccountCredentials) { + const decryptedConnectionDetails = await decryptResourceConnectionDetails({ + encryptedConnectionDetails: resource.encryptedConnectionDetails, + projectId: resource.projectId, + kmsService + }); + + const factory = PAM_RESOURCE_FACTORY_MAP[resource.resourceType as PamResource]( + resource.resourceType as PamResource, + decryptedConnectionDetails, + resource.gatewayId, + gatewayV2Service + ); + + const validatedRotationAccountCredentials = + await factory.validateAccountCredentials(rotationAccountCredentials); + + updateDoc.encryptedRotationAccountCredentials = await encryptAccountCredentials({ + credentials: validatedRotationAccountCredentials, + projectId: resource.projectId, + kmsService + }); + } + } + // If nothing was updated, return the fetched resource if (Object.keys(updateDoc).length === 0) { return decryptResource(resource, resource.projectId, kmsService); diff --git a/backend/src/ee/services/pam-resource/pam-resource-types.ts b/backend/src/ee/services/pam-resource/pam-resource-types.ts index fb1b669ed..f2016420a 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-types.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-types.ts @@ -18,7 +18,7 @@ export type TPamAccountCredentials = TPostgresAccountCredentials; // Resource DTOs export type TCreateResourceDTO = Pick< TPamResource, - "name" | "connectionDetails" | "resourceType" | "gatewayId" | "projectId" + "name" | "connectionDetails" | "resourceType" | "gatewayId" | "projectId" | "rotationAccountCredentials" >; export type TUpdateResourceDTO = Partial> & { @@ -30,6 +30,10 @@ export type TPamResourceFactoryValidateConnection = ( credentials: C ) => Promise; +export type TPamResourceFactoryRotateAccountCredentials = ( + rotationAccountCredentials: C, + currentCredentials: C +) => Promise; export type TPamResourceFactory = ( resourceType: PamResource, @@ -39,4 +43,5 @@ export type TPamResourceFactory { validateConnection: TPamResourceFactoryValidateConnection; validateAccountCredentials: TPamResourceFactoryValidateAccountCredentials; + rotateAccountCredentials: TPamResourceFactoryRotateAccountCredentials; }; diff --git a/backend/src/ee/services/pam-resource/postgres/postgres-resource-schemas.ts b/backend/src/ee/services/pam-resource/postgres/postgres-resource-schemas.ts index a97e3f2e7..f383f521a 100644 --- a/backend/src/ee/services/pam-resource/postgres/postgres-resource-schemas.ts +++ b/backend/src/ee/services/pam-resource/postgres/postgres-resource-schemas.ts @@ -15,13 +15,15 @@ import { BaseSqlResourceConnectionDetailsSchema } from "../shared/sql/sql-resource-schemas"; -// Resources export const PostgresResourceConnectionDetailsSchema = BaseSqlResourceConnectionDetailsSchema; +export const PostgresAccountCredentialsSchema = BaseSqlAccountCredentialsSchema; +// Resources const BasePostgresResourceSchema = BasePamResourceSchema.extend({ resourceType: z.literal(PamResource.Postgres) }); export const PostgresResourceSchema = BasePostgresResourceSchema.extend({ - connectionDetails: PostgresResourceConnectionDetailsSchema + connectionDetails: PostgresResourceConnectionDetailsSchema, + rotationAccountCredentials: PostgresAccountCredentialsSchema.nullable().optional() }); export const PostgresResourceListItemSchema = z.object({ @@ -30,16 +32,16 @@ export const PostgresResourceListItemSchema = z.object({ }); export const CreatePostgresResourceSchema = BaseCreatePamResourceSchema.extend({ - connectionDetails: PostgresResourceConnectionDetailsSchema + connectionDetails: PostgresResourceConnectionDetailsSchema, + rotationAccountCredentials: PostgresAccountCredentialsSchema.nullable().optional() }); export const UpdatePostgresResourceSchema = BaseUpdatePamResourceSchema.extend({ - connectionDetails: PostgresResourceConnectionDetailsSchema.optional() + connectionDetails: PostgresResourceConnectionDetailsSchema.optional(), + rotationAccountCredentials: PostgresAccountCredentialsSchema.nullable().optional() }); // Accounts -export const PostgresAccountCredentialsSchema = BaseSqlAccountCredentialsSchema; - export const PostgresAccountSchema = BasePamAccountSchema.extend({ credentials: PostgresAccountCredentialsSchema }); diff --git a/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts b/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts index 74a2c74ae..73defd6e6 100644 --- a/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts +++ b/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts @@ -6,9 +6,14 @@ import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2 import { BadRequestError } from "@app/lib/errors"; import { GatewayProxyProtocol } from "@app/lib/gateway"; import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; import { PamResource } from "../../pam-resource-enums"; -import { TPamResourceFactory, TPamResourceFactoryValidateAccountCredentials } from "../../pam-resource-types"; +import { + TPamResourceFactory, + TPamResourceFactoryRotateAccountCredentials, + TPamResourceFactoryValidateAccountCredentials +} from "../../pam-resource-types"; import { TSqlAccountCredentials, TSqlResourceConnectionDetails } from "./sql-resource-types"; const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000; @@ -176,8 +181,66 @@ export const sqlResourceFactory: TPamResourceFactory = async ( + rotationAccountCredentials, + currentCredentials + ) => { + try { + const newPassword = alphaNumericNanoId(32); + + await executeWithGateway( + { + connectionDetails, + gatewayId, + resourceType, + username: rotationAccountCredentials.username, + password: rotationAccountCredentials.password + }, + gatewayV2Service, + async (client) => { + switch (resourceType) { + case PamResource.Postgres: + await client.raw(`ALTER USER ?? WITH PASSWORD '${newPassword}'`, [currentCredentials.username]); + break; + default: + throw new BadRequestError({ + message: `Password rotation for ${resourceType as PamResource} is not supported.` + }); + } + } + ); + + return { username: currentCredentials.username, password: newPassword }; + } catch (error) { + if (error instanceof BadRequestError) { + if (error.message === `password authentication failed for user "${rotationAccountCredentials.username}"`) { + throw new BadRequestError({ + message: "Management credentials invalid: Username or password incorrect" + }); + } + + if (error.message.includes("permission denied")) { + throw new BadRequestError({ + message: `Management credentials lack permission to rotate password for user "${currentCredentials.username}"` + }); + } + + if (error.message === "Connection terminated unexpectedly") { + throw new BadRequestError({ + message: "Connection terminated unexpectedly. Verify that host and port are correct" + }); + } + } + + throw new BadRequestError({ + message: `Unable to rotate account credentials for ${resourceType}: ${(error as Error).message || String(error)}` + }); + } + }; + return { validateConnection, - validateAccountCredentials + validateAccountCredentials, + rotateAccountCredentials }; }; diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts index 7f45e3821..9d8c472f4 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -77,7 +77,8 @@ export enum QueueName { DailyReminders = "daily-reminders", SecretReminderMigration = "secret-reminder-migration", UserNotification = "user-notification", - HealthAlert = "health-alert" + HealthAlert = "health-alert", + PamAccountRotation = "pam-account-rotation" } export enum QueueJobs { @@ -126,7 +127,8 @@ export enum QueueJobs { DailyReminders = "daily-reminders", SecretReminderMigration = "secret-reminder-migration", UserNotification = "user-notification-job", - HealthAlert = "health-alert" + HealthAlert = "health-alert", + PamAccountRotation = "pam-account-rotation" } export type TQueueJobTypes = { @@ -357,6 +359,10 @@ export type TQueueJobTypes = { name: QueueJobs.HealthAlert; payload: undefined; }; + [QueueName.PamAccountRotation]: { + name: QueueJobs.PamAccountRotation; + payload: undefined; + }; }; const SECRET_SCANNING_JOBS = [ diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index e95dd501c..448564704 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -250,6 +250,7 @@ import { orgDALFactory } from "@app/services/org/org-dal"; import { orgServiceFactory } from "@app/services/org/org-service"; import { orgAdminServiceFactory } from "@app/services/org-admin/org-admin-service"; import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; +import { pamAccountRotationServiceFactory } from "@app/services/pam-account-rotation/pam-account-rotation-queue"; import { dailyExpiringPkiItemAlertQueueServiceFactory } from "@app/services/pki-alert/expiring-pki-item-alert-queue"; import { pkiAlertDALFactory } from "@app/services/pki-alert/pki-alert-dal"; import { pkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service"; @@ -2193,7 +2194,13 @@ export const registerRoutes = async ( pamSessionDAL, permissionService, projectDAL, - userDAL + userDAL, + auditLogService + }); + + const pamAccountRotation = pamAccountRotationServiceFactory({ + queueService, + pamAccountService }); const pamSessionService = pamSessionServiceFactory({ @@ -2220,6 +2227,7 @@ export const registerRoutes = async ( await dailyResourceCleanUp.init(); await healthAlert.init(); await pkiSyncCleanup.init(); + await pamAccountRotation.init(); await dailyReminderQueueService.startDailyRemindersJob(); await dailyReminderQueueService.startSecretReminderMigrationJob(); await dailyExpiringPkiItemAlert.startSendingAlerts(); diff --git a/backend/src/services/pam-account-rotation/pam-account-rotation-queue.ts b/backend/src/services/pam-account-rotation/pam-account-rotation-queue.ts new file mode 100644 index 000000000..fb265ac56 --- /dev/null +++ b/backend/src/services/pam-account-rotation/pam-account-rotation-queue.ts @@ -0,0 +1,61 @@ +import { TPamAccountServiceFactory } from "@app/ee/services/pam-account/pam-account-service"; +import { getConfig } from "@app/lib/config/env"; +import { logger } from "@app/lib/logger"; +import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; + +type TPamAccountRotationServiceFactoryDep = { + queueService: TQueueServiceFactory; + pamAccountService: Pick; +}; + +export type TPamAccountRotationServiceFactory = ReturnType; + +export const pamAccountRotationServiceFactory = ({ + queueService, + pamAccountService +}: TPamAccountRotationServiceFactoryDep) => { + const appCfg = getConfig(); + + const init = async () => { + if (appCfg.isSecondaryInstance) { + return; + } + + await queueService.stopRepeatableJob( + QueueName.PamAccountRotation, + QueueJobs.PamAccountRotation, + { pattern: "0 * * * *", utc: true }, + QueueName.PamAccountRotation // job id + ); + + await queueService.startPg( + QueueJobs.PamAccountRotation, + async () => { + try { + logger.info(`${QueueName.PamAccountRotation}: pam account rotation task started`); + await pamAccountService.rotateAllDueAccounts(); + logger.info(`${QueueName.PamAccountRotation}: pam account rotation task completed`); + } catch (error) { + logger.error(error, `${QueueName.PamAccountRotation}: pam account rotation failed`); + throw error; + } + }, + { + batchSize: 1, + workerCount: 1, + pollingIntervalSeconds: 1 // 5 * 60 + } + ); + + await queueService.schedulePg( + QueueJobs.PamAccountRotation, + "0 * * * *", // Schedule to run every hour + undefined, + { tz: "UTC" } + ); + }; + + return { + init + }; +}; diff --git a/frontend/src/hooks/api/pam/types/base-account.ts b/frontend/src/hooks/api/pam/types/base-account.ts index 9f45b1a4a..ec94d51b2 100644 --- a/frontend/src/hooks/api/pam/types/base-account.ts +++ b/frontend/src/hooks/api/pam/types/base-account.ts @@ -12,6 +12,9 @@ export interface TBasePamAccount { }; name: string; description?: string | null; + rotationEnabled: boolean; + rotationIntervalSeconds: number; + lastRotatedAt?: string | null; createdAt: string; updatedAt: string; } diff --git a/frontend/src/hooks/api/pam/types/postgres-resource.ts b/frontend/src/hooks/api/pam/types/postgres-resource.ts index 513610be1..b1b5b7487 100644 --- a/frontend/src/hooks/api/pam/types/postgres-resource.ts +++ b/frontend/src/hooks/api/pam/types/postgres-resource.ts @@ -6,6 +6,7 @@ import { TBasePamResource } from "./base-resource"; // Resources export type TPostgresResource = TBasePamResource & { resourceType: PamResourceType.Postgres } & { connectionDetails: TBaseSqlConnectionDetails; + rotationAccountCredentials?: TBaseSqlCredentials | null; }; // Accounts diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PamAccountForm.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PamAccountForm.tsx index ead2b06e1..bb64e33c0 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PamAccountForm.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PamAccountForm.tsx @@ -37,7 +37,10 @@ const CreateForm = ({ console.log({ folderId }); const onSubmit = async ( - formData: DiscriminativePick + formData: DiscriminativePick< + TPamAccount, + "name" | "description" | "credentials" | "rotationEnabled" | "rotationIntervalSeconds" + > ) => { try { const account = await createPamAccount.mutateAsync({ @@ -74,7 +77,10 @@ const UpdateForm = ({ account, onComplete }: UpdateFormProps) => { const updatePamAccount = useUpdatePamAccount(); const onSubmit = async ( - formData: DiscriminativePick + formData: DiscriminativePick< + TPamAccount, + "name" | "description" | "credentials" | "rotationEnabled" | "rotationIntervalSeconds" + > ) => { try { const updatedAccount = await updatePamAccount.mutateAsync({ diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx index 5bcd459aa..0033c48f3 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx @@ -8,13 +8,14 @@ import { TPostgresAccount } from "@app/hooks/api/pam"; import { BaseSqlAccountSchema } from "./shared/sql-account-schemas"; import { SqlAccountFields } from "./shared/SqlAccountFields"; import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields"; +import { RotateAccountFields, rotateAccountFieldsSchema } from "./RotateAccountFields"; type Props = { account?: TPostgresAccount; onSubmit: (formData: FormData) => Promise; }; -const formSchema = genericAccountFieldsSchema.extend({ +const formSchema = genericAccountFieldsSchema.extend(rotateAccountFieldsSchema.shape).extend({ credentials: BaseSqlAccountSchema }); @@ -50,6 +51,7 @@ export const PostgresAccountForm = ({ account, onSubmit }: Props) => { > +
From ebb28844eed1adc852f68b6da79d892195e8e87d Mon Sep 17 00:00:00 2001 From: x032205 Date: Fri, 17 Oct 2025 22:54:22 -0400 Subject: [PATCH 007/100] make resource rotation account credentials write-only --- .../routes/v1/pam-resource-routers/index.ts | 4 +-- .../pam-resource-router.ts | 6 ++-- .../pam-resource/pam-resource-service.ts | 17 ++++++++-- .../postgres/postgres-resource-schemas.ts | 9 ++++++ .../components/PamAccountRow.tsx | 2 +- .../PamResourceForm/PostgresResourceForm.tsx | 32 ++++++++++++------- .../shared/SqlRotateAccountFields.tsx | 21 +++++++++--- 7 files changed, 65 insertions(+), 26 deletions(-) diff --git a/backend/src/ee/routes/v1/pam-resource-routers/index.ts b/backend/src/ee/routes/v1/pam-resource-routers/index.ts index a63b67d94..6b53781ae 100644 --- a/backend/src/ee/routes/v1/pam-resource-routers/index.ts +++ b/backend/src/ee/routes/v1/pam-resource-routers/index.ts @@ -1,7 +1,7 @@ import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums"; import { CreatePostgresResourceSchema, - PostgresResourceSchema, + SanitizedPostgresResourceSchema, UpdatePostgresResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; @@ -12,7 +12,7 @@ export const PAM_RESOURCE_REGISTER_ROUTER_MAP: Record { }), response: { 200: z.object({ - resources: ResourceSchema.array() + resources: SanitizedResourceSchema.array() }) } }, diff --git a/backend/src/ee/services/pam-resource/pam-resource-service.ts b/backend/src/ee/services/pam-resource/pam-resource-service.ts index 1c5ead252..076ec6855 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-service.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-service.ts @@ -10,7 +10,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service"; import { TLicenseServiceFactory } from "../license/license-service"; -import { encryptAccountCredentials } from "../pam-account/pam-account-fns"; +import { decryptAccountCredentials, encryptAccountCredentials } from "../pam-account/pam-account-fns"; import { TPamResourceDALFactory } from "./pam-resource-dal"; import { PamResource } from "./pam-resource-enums"; import { PAM_RESOURCE_FACTORY_MAP } from "./pam-resource-factory"; @@ -192,8 +192,19 @@ export const pamResourceServiceFactory = ({ gatewayV2Service ); - const validatedRotationAccountCredentials = - await factory.validateAccountCredentials(rotationAccountCredentials); + // Logic to prevent overwriting unedited censored values + const finalCredentials = { ...rotationAccountCredentials }; + if (resource.encryptedRotationAccountCredentials && rotationAccountCredentials.password === "******") { + const decryptedCredentials = await decryptAccountCredentials({ + encryptedCredentials: resource.encryptedRotationAccountCredentials, + projectId: resource.projectId, + kmsService + }); + + finalCredentials.password = decryptedCredentials.password; + } + + const validatedRotationAccountCredentials = await factory.validateAccountCredentials(finalCredentials); updateDoc.encryptedRotationAccountCredentials = await encryptAccountCredentials({ credentials: validatedRotationAccountCredentials, diff --git a/backend/src/ee/services/pam-resource/postgres/postgres-resource-schemas.ts b/backend/src/ee/services/pam-resource/postgres/postgres-resource-schemas.ts index f383f521a..bbe83a3a4 100644 --- a/backend/src/ee/services/pam-resource/postgres/postgres-resource-schemas.ts +++ b/backend/src/ee/services/pam-resource/postgres/postgres-resource-schemas.ts @@ -26,6 +26,15 @@ export const PostgresResourceSchema = BasePostgresResourceSchema.extend({ rotationAccountCredentials: PostgresAccountCredentialsSchema.nullable().optional() }); +export const SanitizedPostgresResourceSchema = BasePostgresResourceSchema.extend({ + connectionDetails: PostgresResourceConnectionDetailsSchema, + rotationAccountCredentials: PostgresAccountCredentialsSchema.pick({ + username: true + }) + .nullable() + .optional() +}); + export const PostgresResourceListItemSchema = z.object({ name: z.literal("PostgreSQL"), resource: z.literal(PamResource.Postgres) diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountRow.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountRow.tsx index 758ef2c20..8e7507ac9 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountRow.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountRow.tsx @@ -11,8 +11,8 @@ import { faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { twMerge } from "tailwind-merge"; import { formatDistance } from "date-fns"; +import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; diff --git a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PostgresResourceForm.tsx b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PostgresResourceForm.tsx index c172bb561..427fff40b 100644 --- a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PostgresResourceForm.tsx +++ b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PostgresResourceForm.tsx @@ -31,17 +31,25 @@ export const PostgresResourceForm = ({ resource, onSubmit }: Props) => { const form = useForm({ resolver: zodResolver(formSchema), - defaultValues: resource ?? { - resourceType: PamResourceType.Postgres, - connectionDetails: { - host: "", - port: 5432, - database: "default", - sslEnabled: true, - sslRejectUnauthorized: true, - sslCertificate: undefined - } - } + defaultValues: resource + ? { + ...resource, + rotationAccountCredentials: { + ...resource.rotationAccountCredentials, + password: "******" + } + } + : { + resourceType: PamResourceType.Postgres, + connectionDetails: { + host: "", + port: 5432, + database: "default", + sslEnabled: true, + sslRejectUnauthorized: true, + sslCertificate: undefined + } + } }); const { @@ -62,7 +70,7 @@ export const PostgresResourceForm = ({ resource, onSubmit }: Props) => { selectedTabIndex={selectedTabIndex} setSelectedTabIndex={setSelectedTabIndex} /> - +
+ + +
+ 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 012/100] 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 013/100] 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 (
-
-
+
+ + {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 014/100] 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 015/100] 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 016/100] 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 017/100] 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 018/100] 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 019/100] 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 020/100] 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 021/100] 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 022/100] 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 023/100] 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 024/100] 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 025/100] 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 034/100] 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 035/100] 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 5c6b7ed95c875d2b25d0e5d11604b5b841ce1337 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Mon, 20 Oct 2025 21:28:05 -0300 Subject: [PATCH 036/100] Use commit batch logic on SecretDropzone --- .../SecretDashboardPage.tsx | 1 - .../SecretDropzone/SecretDropzone.tsx | 170 ++++++++++-------- 2 files changed, 96 insertions(+), 75 deletions(-) diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx index caef19326..f1a7ffba8 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -1109,7 +1109,6 @@ const Page = () => { secretPath={secretPath} isSmaller={isNotEmpty} environments={currentProject?.environments} - isProtectedBranch={isProtectedBranch} /> ; -type TSecOverwriteOpt = { update: TParsedEnv; create: TParsedEnv }; +type TSecOverwriteOpt = { + update: TParsedEnv; + create: TParsedEnv; + existingSecrets: SecretV3RawSanitized[]; +}; type Props = { isSmaller: boolean; @@ -56,7 +61,6 @@ type Props = { projectId: string; environment: string; secretPath: string; - isProtectedBranch?: boolean; }; type SecretMatrixMap = { @@ -142,8 +146,7 @@ export const SecretDropzone = ({ environments = [], projectId, environment, - secretPath, - isProtectedBranch = false + secretPath }: Props): JSX.Element => { const { t } = useTranslation(); const [isDragActive, setDragActive] = useToggle(); @@ -157,18 +160,12 @@ export const SecretDropzone = ({ }); const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp(popupKeys); - const queryClient = useQueryClient(); const { openPopUp } = usePopUpAction(); + const { addPendingChange } = useBatchModeActions(); - const { mutateAsync: updateSecretBatch, isPending: isUpdatingSecrets } = useUpdateSecretBatch({ - options: { onSuccess: undefined } - }); - const { mutateAsync: createSecretBatch, isPending: isCreatingSecrets } = useCreateSecretBatch({ - options: { onSuccess: undefined } - }); // hide copy secrets from board due to import folders feature const shouldRenderCopySecrets = false; - const isSubmitting = isCreatingSecrets || isUpdatingSecrets; + const [isSubmitting, setIsSubmitting] = useToggle(); const handleDrag = (e: DragEvent) => { e.preventDefault(); @@ -193,29 +190,38 @@ export const SecretDropzone = ({ try { setIsLoading.on(); - const { secrets: existingSecrets } = await fetchDashboardProjectSecretsByKeys({ - secretPath, - environment, + const { secrets: rawExistingSecrets } = await fetchProjectSecrets({ projectId, - keys: envSecretKeys + environment, + secretPath, + viewSecretValue: true }); - const secretsGroupedByKey = existingSecrets.reduce>( - (prev, curr) => ({ ...prev, [curr.secretKey]: true }), + const allExistingSecrets = mergePersonalSecrets(rawExistingSecrets); + + const existingSecretsMap = allExistingSecrets.reduce>( + (prev, curr) => ({ ...prev, [curr.key]: curr }), {} ); - const updateSecrets = Object.keys(env) - .filter((secKey) => secretsGroupedByKey[secKey]) - .reduce((prev, curr) => ({ ...prev, [curr]: env[curr] }), {}); + const updateSecrets: TParsedEnv = {}; + const createSecrets: TParsedEnv = {}; + const relevantExistingSecrets: SecretV3RawSanitized[] = []; - const createSecrets = Object.keys(env) - .filter((secKey) => !secretsGroupedByKey[secKey]) - .reduce((prev, curr) => ({ ...prev, [curr]: env[curr] }), {}); + Object.entries(env).forEach(([secretKey, secretData]) => { + const existingSecret = existingSecretsMap[secretKey]; + if (existingSecret) { + updateSecrets[secretKey] = secretData; + relevantExistingSecrets.push(existingSecret); + } else { + createSecrets[secretKey] = secretData; + } + }); handlePopUpOpen("confirmUpload", { update: updateSecrets, - create: createSecrets + create: createSecrets, + existingSecrets: relevantExistingSecrets }); } catch (e) { console.error(e); @@ -329,56 +335,72 @@ export const SecretDropzone = ({ }; const handleSaveSecrets = async () => { - const { update, create } = popUp?.confirmUpload?.data as TSecOverwriteOpt; + const { update, create, existingSecrets } = popUp?.confirmUpload?.data as TSecOverwriteOpt; + try { + setIsSubmitting.on(); + + const context: BatchContext = { + projectId, + environment, + secretPath + }; + + const existingSecretsMap = existingSecrets.reduce>( + (prev, curr) => ({ ...prev, [curr.key]: curr }), + {} + ); + if (Object.keys(create || {}).length) { - await createSecretBatch({ - secretPath, - projectId, - environment, - secrets: Object.entries(create).map(([secretKey, secData]) => ({ - type: SecretType.Shared, - secretComment: secData.comments.join("\n"), + Object.entries(create).forEach(([secretKey, secData]) => { + const createChange: PendingSecretCreate = { + id: secretKey, + timestamp: Date.now(), + resourceType: "secret", + type: PendingAction.Create, + secretKey, secretValue: secData.value, - secretKey - })) + secretComment: secData.comments.join("\n") || undefined, + tags: [], + secretMetadata: [] + }; + addPendingChange(createChange, context); }); } + if (Object.keys(update || {}).length) { - await updateSecretBatch({ - secretPath, - projectId, - environment, - secrets: Object.entries(update).map(([secretKey, secData]) => ({ - type: SecretType.Shared, - secretComment: secData.comments.join("\n"), + Object.entries(update).forEach(([secretKey, secData]) => { + const existingSecret = existingSecretsMap[secretKey]; + + if (!existingSecret) { + console.warn(`Existing secret not found for key: ${secretKey}`); + return; + } + + const updateChange: PendingSecretUpdate = { + id: existingSecret.id, + timestamp: Date.now(), + resourceType: "secret", + type: PendingAction.Update, + secretKey, secretValue: secData.value, - secretKey - })) + secretComment: secData.comments.join("\n") || undefined, + existingSecret, + originalValue: existingSecret.value || "", + originalComment: existingSecret.comment || "", + originalSkipMultilineEncoding: existingSecret.skipMultilineEncoding || false, + originalTags: existingSecret.tags || [], + originalSecretMetadata: existingSecret.secretMetadata || [] + }; + addPendingChange(updateChange, context); }); } - queryClient.invalidateQueries({ - queryKey: secretKeys.getProjectSecret({ projectId, environment, secretPath }) - }); - queryClient.invalidateQueries({ - queryKey: dashboardKeys.getDashboardSecrets({ projectId, secretPath }) - }); - queryClient.invalidateQueries({ - queryKey: secretApprovalRequestKeys.count({ projectId }) - }); + handlePopUpClose("confirmUpload"); - createNotification({ - type: "success", - text: isProtectedBranch - ? "Uploaded changes have been sent for review" - : "Successfully uploaded secrets" - }); } catch (err) { console.log(err); - createNotification({ - type: "error", - text: "Failed to upload secrets" - }); + } finally { + setIsSubmitting.off(); } }; @@ -541,7 +563,7 @@ export const SecretDropzone = ({ ? ` and import ${createSecretCount} new one${createSecretCount > 1 ? "s" : ""}` : ""} - ? + ? These will be applied when you commit your changes.
)} From 3a61ce07365fc6f5ec0dcda06c3ec6476c8b6dac Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Mon, 20 Oct 2025 21:44:39 -0300 Subject: [PATCH 037/100] Lint fix --- .../components/SecretDropzone/SecretDropzone.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/SecretDropzone.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/SecretDropzone.tsx index 5d8ea59c3..ded34f9d4 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/SecretDropzone.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/SecretDropzone.tsx @@ -38,12 +38,12 @@ import { fetchProjectSecrets, mergePersonalSecrets } from "@app/hooks/api/secret import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types"; import { - PopUpNames, - usePopUpAction, - useBatchModeActions, + BatchContext, PendingSecretCreate, PendingSecretUpdate, - BatchContext + PopUpNames, + useBatchModeActions, + usePopUpAction } from "../../SecretMainPage.store"; import { CopySecretsFromBoard } from "./CopySecretsFromBoard"; import { PasteSecretEnvModal } from "./PasteSecretEnvModal"; From 64b3d6132599bfa9a163f5f20f86a0cb42131b95 Mon Sep 17 00:00:00 2001 From: x032205 Date: Mon, 20 Oct 2025 20:45:54 -0400 Subject: [PATCH 038/100] tweak pam resource query --- frontend/src/hooks/api/pam/queries.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/hooks/api/pam/queries.tsx b/frontend/src/hooks/api/pam/queries.tsx index c6df40e37..60ead74ec 100644 --- a/frontend/src/hooks/api/pam/queries.tsx +++ b/frontend/src/hooks/api/pam/queries.tsx @@ -12,7 +12,7 @@ export const pamKeys = { session: () => [...pamKeys.all, "session"] as const, listResourceOptions: () => [...pamKeys.resource(), "options"] as const, listResources: (projectId: string) => [...pamKeys.resource(), "list", projectId], - getResource: (resourceId?: string) => [...pamKeys.resource(), "get", resourceId], + getResource: (resourceId: string) => [...pamKeys.resource(), "get", resourceId], listAccounts: (projectId: string) => [...pamKeys.account(), "list", projectId], getSession: (sessionId: string) => [...pamKeys.session(), "get", sessionId], listSessions: (projectId: string) => [...pamKeys.session(), "list", projectId] @@ -77,7 +77,7 @@ export const useGetPamResourceById = ( > ) => { return useQuery({ - queryKey: pamKeys.getResource(resourceId), + queryKey: pamKeys.getResource(resourceId || ""), queryFn: async () => { const { data } = await apiRequest.get<{ resource: TPamResource }>( `/api/v1/pam/resources/${resourceId}` From 2b6fcb2564d61a35fd76f7ce6d665f8f633942e1 Mon Sep 17 00:00:00 2001 From: x032205 Date: Mon, 20 Oct 2025 20:46:15 -0400 Subject: [PATCH 039/100] fix rotation account configured detection --- backend/src/ee/services/pam-resource/pam-resource-schemas.ts | 2 ++ .../components/PamAccountForm/PostgresAccountForm.tsx | 2 +- .../components/PamAccountForm/RotateAccountFields.tsx | 5 ++++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/services/pam-resource/pam-resource-schemas.ts b/backend/src/ee/services/pam-resource/pam-resource-schemas.ts index 5b0199166..8f4752f4e 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-schemas.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-schemas.ts @@ -31,6 +31,8 @@ export const BasePamAccountSchemaWithResource = BasePamAccountSchema.extend({ id: true, name: true, resourceType: true + }).extend({ + rotationCredentialsConfigured: z.boolean() }) }); diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx index bbf7982fb..5a2104275 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx @@ -54,7 +54,7 @@ export const PostgresAccountForm = ({ account, resourceId, onSubmit }: Props) => } else { setRotationCredentialsConfigured(!!resource?.rotationAccountCredentials); } - }, [account]); + }, [account, resource]); return ( diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/RotateAccountFields.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/RotateAccountFields.tsx index 32a7040ec..2739e1cd5 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/RotateAccountFields.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/RotateAccountFields.tsx @@ -22,7 +22,10 @@ export const RotateAccountFields = ({ const rotationEnabled = watch("rotationEnabled"); return ( - +
Date: Mon, 20 Oct 2025 21:03:35 -0400 Subject: [PATCH 040/100] fix endpoint and improve frontend performance --- frontend/src/hooks/api/pam/queries.tsx | 8 +++++--- .../components/PamAccountForm/PamAccountForm.tsx | 8 +++++++- .../components/PamAccountForm/PostgresAccountForm.tsx | 9 ++++++--- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/frontend/src/hooks/api/pam/queries.tsx b/frontend/src/hooks/api/pam/queries.tsx index 60ead74ec..22810c4e3 100644 --- a/frontend/src/hooks/api/pam/queries.tsx +++ b/frontend/src/hooks/api/pam/queries.tsx @@ -4,6 +4,7 @@ import { apiRequest } from "@app/config/request"; import { TPamResourceOption } from "./types/resource-options"; import { TPamAccount, TPamFolder, TPamResource, TPamSession } from "./types"; +import { PamResourceType } from "./enums"; export const pamKeys = { all: ["pam"] as const, @@ -70,22 +71,23 @@ export const useListPamResources = ( }; export const useGetPamResourceById = ( + resourceType?: PamResourceType, resourceId?: string, options?: Omit< UseQueryOptions>, - "queryKey" | "queryFn" | "enabled" + "queryKey" | "queryFn" > ) => { return useQuery({ queryKey: pamKeys.getResource(resourceId || ""), queryFn: async () => { const { data } = await apiRequest.get<{ resource: TPamResource }>( - `/api/v1/pam/resources/${resourceId}` + `/api/v1/pam/resources/${resourceType}/${resourceId}` ); return data.resource; }, - enabled: !!resourceId, + enabled: !!resourceId && !!resourceType && (options?.enabled ?? true), ...options }); }; diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PamAccountForm.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PamAccountForm.tsx index eee101a54..8b553e656 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PamAccountForm.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PamAccountForm.tsx @@ -65,7 +65,13 @@ const CreateForm = ({ switch (resourceType) { case PamResourceType.Postgres: - return ; + return ( + + ); default: throw new Error(`Unhandled resource: ${resourceType}`); } diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx index 5a2104275..c353a278b 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx @@ -4,7 +4,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Button, ModalClose } from "@app/components/v2"; -import { TPostgresAccount, useGetPamResourceById } from "@app/hooks/api/pam"; +import { PamResourceType, TPostgresAccount, useGetPamResourceById } from "@app/hooks/api/pam"; import { BaseSqlAccountSchema } from "./shared/sql-account-schemas"; import { SqlAccountFields } from "./shared/SqlAccountFields"; @@ -14,6 +14,7 @@ import { RotateAccountFields, rotateAccountFieldsSchema } from "./RotateAccountF type Props = { account?: TPostgresAccount; resourceId?: string; + resourceType?: PamResourceType; onSubmit: (formData: FormData) => Promise; }; @@ -23,7 +24,7 @@ const formSchema = genericAccountFieldsSchema.extend(rotateAccountFieldsSchema.s type FormData = z.infer; -export const PostgresAccountForm = ({ account, resourceId, onSubmit }: Props) => { +export const PostgresAccountForm = ({ account, resourceId, resourceType, onSubmit }: Props) => { const isUpdate = Boolean(account); const form = useForm({ @@ -46,7 +47,9 @@ export const PostgresAccountForm = ({ account, resourceId, onSubmit }: Props) => const [rotationCredentialsConfigured, setRotationCredentialsConfigured] = useState(false); - const { data: resource } = useGetPamResourceById(resourceId); + const { data: resource } = useGetPamResourceById(resourceType, resourceId, { + enabled: !account && !!resourceId && !!resourceType + }); useEffect(() => { if (account) { From bdbec5b1589320d705f5acf9ab7069b0daed0810 Mon Sep 17 00:00:00 2001 From: x032205 Date: Mon, 20 Oct 2025 21:09:01 -0400 Subject: [PATCH 041/100] add resource type to error log --- backend/src/ee/services/audit-log/audit-log-types.ts | 1 + backend/src/ee/services/pam-account/pam-account-service.ts | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) 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 be5906af5..94561e6d8 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -3841,6 +3841,7 @@ interface PamAccountCredentialRotationFailedEvent { accountName: string; accountId: string; resourceId: string; + resourceType: string; errorMessage: string; }; } 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 209eb25b9..4d0ccc6ab 100644 --- a/backend/src/ee/services/pam-account/pam-account-service.ts +++ b/backend/src/ee/services/pam-account/pam-account-service.ts @@ -554,9 +554,11 @@ export const pamAccountServiceFactory = ({ const batch = accounts.slice(i, i + ROTATION_CONCURRENCY_LIMIT); const rotationPromises = batch.map(async (account) => { + let logResourceType = "unknown"; try { const resource = await pamResourceDAL.findById(account.resourceId); if (!resource || !resource.encryptedRotationAccountCredentials) return; + logResourceType = resource.resourceType; const { connectionDetails, rotationAccountCredentials, gatewayId, resourceType } = await decryptResource( resource, @@ -604,7 +606,7 @@ export const pamAccountServiceFactory = ({ accountId: account.id, accountName: account.name, resourceId: resource.id, - resourceType: resource.resourceType + resourceType: logResourceType } } }); @@ -623,6 +625,7 @@ export const pamAccountServiceFactory = ({ accountId: account.id, accountName: account.name, resourceId: account.resourceId, + resourceType: logResourceType, errorMessage } } From efd63dcb35b85e7751dc9ba0daf09282d25729a1 Mon Sep 17 00:00:00 2001 From: x032205 Date: Mon, 20 Oct 2025 21:13:40 -0400 Subject: [PATCH 042/100] improve rotation account error response --- .../src/ee/services/pam-resource/pam-resource-service.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/backend/src/ee/services/pam-resource/pam-resource-service.ts b/backend/src/ee/services/pam-resource/pam-resource-service.ts index 683e0bca4..9ec702b24 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-service.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-service.ts @@ -213,12 +213,9 @@ export const pamResourceServiceFactory = ({ kmsService }); } catch (err) { - if ( - err instanceof BadRequestError && - err.message === "Account credentials invalid: Username or password incorrect" - ) { + if (err instanceof BadRequestError) { throw new BadRequestError({ - message: "Rotation Account credentials invalid: Username or password incorrect" + message: `Rotation Account Error: ${err.message}` }); } From 9bb0e2c401cf879df619016505f3245da0397020 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 20 Oct 2025 18:18:21 -0700 Subject: [PATCH 043/100] 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 35d4720d8631288211a9a1a55c93b17464abf046 Mon Sep 17 00:00:00 2001 From: x032205 Date: Mon, 20 Oct 2025 21:24:57 -0400 Subject: [PATCH 044/100] final small fixes --- .../services/pam-account/pam-account-service.ts | 4 ++-- .../CustomProviderAuditLogStreamForm.tsx | 15 +++++++++------ .../PamAccountForm/shared/SqlAccountFields.tsx | 12 +++++++----- .../shared/SqlRotateAccountFields.tsx | 12 +++++++----- 4 files changed, 25 insertions(+), 18 deletions(-) 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 4d0ccc6ab..83e488231 100644 --- a/backend/src/ee/services/pam-account/pam-account-service.ts +++ b/backend/src/ee/services/pam-account/pam-account-service.ts @@ -611,6 +611,8 @@ export const pamAccountServiceFactory = ({ } }); } catch (error) { + logger.error(error, `Failed to rotate credentials for account [accountId=${account.id}]`); + const errorMessage = error instanceof Error ? error.message : "An unknown error occurred"; await auditLogService.createAuditLog({ @@ -630,8 +632,6 @@ export const pamAccountServiceFactory = ({ } } }); - - logger.error(error, `Failed to rotate credentials for account ${account.id}`); } }); diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/CustomProviderAuditLogStreamForm.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/CustomProviderAuditLogStreamForm.tsx index 7d2868dcc..b9a125900 100644 --- a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/CustomProviderAuditLogStreamForm.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/CustomProviderAuditLogStreamForm.tsx @@ -7,6 +7,7 @@ import { z } from "zod"; import { Button, FormControl, FormLabel, IconButton, Input, ModalClose } from "@app/components/v2"; import { LogProvider } from "@app/hooks/api/auditLogStreams/enums"; import { TCustomProviderLogStream } from "@app/hooks/api/auditLogStreams/types/providers/custom-provider"; +import { useState } from "react"; type Props = { auditLogStream?: TCustomProviderLogStream; @@ -29,6 +30,8 @@ const formSchema = z.object({ type FormData = z.infer; export const CustomProviderAuditLogStreamForm = ({ auditLogStream, onSubmit }: Props) => { + const [showPassword, setShowPassword] = useState(false); + const isUpdate = Boolean(auditLogStream); const form = useForm({ @@ -96,10 +99,10 @@ export const CustomProviderAuditLogStreamForm = ({ auditLogStream, onSubmit }: P > { + placeholder="Bearer " + onFocus={() => { if ( auditLogStream && auditLogStream.credentials.headers[i] && @@ -108,9 +111,9 @@ export const CustomProviderAuditLogStreamForm = ({ auditLogStream, onSubmit }: P ) { field.onChange(""); } - e.target.type = "text"; + setShowPassword(true); }} - onBlur={(e) => { + onBlur={() => { if ( auditLogStream && auditLogStream.credentials.headers[i] && @@ -119,7 +122,7 @@ export const CustomProviderAuditLogStreamForm = ({ auditLogStream, onSubmit }: P ) { field.onChange("******"); } - e.target.type = "password"; + setShowPassword(false); }} /> diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/shared/SqlAccountFields.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/shared/SqlAccountFields.tsx index c32102231..3e9db178f 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/shared/SqlAccountFields.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/shared/SqlAccountFields.tsx @@ -1,9 +1,11 @@ import { Controller, useFormContext } from "react-hook-form"; import { FormControl, Input } from "@app/components/v2"; +import { useState } from "react"; export const SqlAccountFields = ({ isUpdate }: { isUpdate: boolean }) => { const { control } = useFormContext(); + const [showPassword, setShowPassword] = useState(false); return (
@@ -33,19 +35,19 @@ export const SqlAccountFields = ({ isUpdate }: { isUpdate: boolean }) => { > { + onFocus={() => { if (isUpdate && field.value === "******") { field.onChange(""); } - e.target.type = "text"; + setShowPassword(true); }} - onBlur={(e) => { + onBlur={() => { if (isUpdate && field.value === "") { field.onChange("******"); } - e.target.type = "password"; + setShowPassword(false); }} /> diff --git a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/shared/SqlRotateAccountFields.tsx b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/shared/SqlRotateAccountFields.tsx index 05b28aa94..869594140 100644 --- a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/shared/SqlRotateAccountFields.tsx +++ b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/shared/SqlRotateAccountFields.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import { Controller, useFormContext } from "react-hook-form"; import { @@ -11,6 +12,7 @@ import { export const SqlRotateAccountFields = ({ isUpdate }: { isUpdate: boolean }) => { const { control } = useFormContext(); + const [showPassword, setShowPassword] = useState(false); return ( @@ -50,19 +52,19 @@ export const SqlRotateAccountFields = ({ isUpdate }: { isUpdate: boolean }) => { > { + onFocus={() => { if (isUpdate && field.value === "******") { field.onChange(""); } - e.target.type = "text"; + setShowPassword(true); }} - onBlur={(e) => { + onBlur={() => { if (isUpdate && field.value === "") { field.onChange("******"); } - e.target.type = "password"; + setShowPassword(false); }} /> From 8bbddb09c0a1b47cde961325bd6fa8837cbd806e Mon Sep 17 00:00:00 2001 From: x032205 Date: Mon, 20 Oct 2025 21:28:27 -0400 Subject: [PATCH 045/100] lint --- frontend/src/hooks/api/pam/queries.tsx | 2 +- .../AuditLogStreamForm/CustomProviderAuditLogStreamForm.tsx | 2 +- .../components/PamAccountForm/shared/SqlAccountFields.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/hooks/api/pam/queries.tsx b/frontend/src/hooks/api/pam/queries.tsx index 22810c4e3..77afbe832 100644 --- a/frontend/src/hooks/api/pam/queries.tsx +++ b/frontend/src/hooks/api/pam/queries.tsx @@ -3,8 +3,8 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { TPamResourceOption } from "./types/resource-options"; -import { TPamAccount, TPamFolder, TPamResource, TPamSession } from "./types"; import { PamResourceType } from "./enums"; +import { TPamAccount, TPamFolder, TPamResource, TPamSession } from "./types"; export const pamKeys = { all: ["pam"] as const, diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/CustomProviderAuditLogStreamForm.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/CustomProviderAuditLogStreamForm.tsx index b9a125900..e60f09c4a 100644 --- a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/CustomProviderAuditLogStreamForm.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/CustomProviderAuditLogStreamForm.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import { Controller, FormProvider, useFieldArray, useForm } from "react-hook-form"; import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -7,7 +8,6 @@ import { z } from "zod"; import { Button, FormControl, FormLabel, IconButton, Input, ModalClose } from "@app/components/v2"; import { LogProvider } from "@app/hooks/api/auditLogStreams/enums"; import { TCustomProviderLogStream } from "@app/hooks/api/auditLogStreams/types/providers/custom-provider"; -import { useState } from "react"; type Props = { auditLogStream?: TCustomProviderLogStream; diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/shared/SqlAccountFields.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/shared/SqlAccountFields.tsx index 3e9db178f..7d9e85c44 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/shared/SqlAccountFields.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/shared/SqlAccountFields.tsx @@ -1,7 +1,7 @@ +import { useState } from "react"; import { Controller, useFormContext } from "react-hook-form"; import { FormControl, Input } from "@app/components/v2"; -import { useState } from "react"; export const SqlAccountFields = ({ isUpdate }: { isUpdate: boolean }) => { const { control } = useFormContext(); From df069cbd5b5037a4c4b0d01ed5c4dc612e816b57 Mon Sep 17 00:00:00 2001 From: x032205 Date: Mon, 20 Oct 2025 21:34:28 -0400 Subject: [PATCH 046/100] tiny greptile improvement --- frontend/src/hooks/api/pam/queries.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/src/hooks/api/pam/queries.tsx b/frontend/src/hooks/api/pam/queries.tsx index 77afbe832..6339b4761 100644 --- a/frontend/src/hooks/api/pam/queries.tsx +++ b/frontend/src/hooks/api/pam/queries.tsx @@ -13,7 +13,12 @@ export const pamKeys = { session: () => [...pamKeys.all, "session"] as const, listResourceOptions: () => [...pamKeys.resource(), "options"] as const, listResources: (projectId: string) => [...pamKeys.resource(), "list", projectId], - getResource: (resourceId: string) => [...pamKeys.resource(), "get", resourceId], + getResource: (resourceType: string, resourceId: string) => [ + ...pamKeys.resource(), + "get", + resourceType, + resourceId + ], listAccounts: (projectId: string) => [...pamKeys.account(), "list", projectId], getSession: (sessionId: string) => [...pamKeys.session(), "get", sessionId], listSessions: (projectId: string) => [...pamKeys.session(), "list", projectId] @@ -79,7 +84,7 @@ export const useGetPamResourceById = ( > ) => { return useQuery({ - queryKey: pamKeys.getResource(resourceId || ""), + queryKey: pamKeys.getResource(resourceType || "", resourceId || ""), queryFn: async () => { const { data } = await apiRequest.get<{ resource: TPamResource }>( `/api/v1/pam/resources/${resourceType}/${resourceId}` From 67c8a0e1688f99d40a824d945992e7f74bef15a7 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 20 Oct 2025 18:44:44 -0700 Subject: [PATCH 047/100] 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 048/100] 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 049/100] 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 050/100] 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 051/100] 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 052/100] 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 c8a00e7e3fd6b0df3ad6f44a05a095a71b514b2f Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 21 Oct 2025 16:46:22 +0400 Subject: [PATCH 053/100] checkpoint --- backend/src/db/migrations/utils/env-config.ts | 15 +++++--- backend/src/db/migrations/utils/services.ts | 3 +- .../ee/services/license/license-service.ts | 38 ++++++++++--------- backend/src/lib/config/env.ts | 5 --- backend/src/server/routes/index.ts | 3 +- 5 files changed, 35 insertions(+), 29 deletions(-) diff --git a/backend/src/db/migrations/utils/env-config.ts b/backend/src/db/migrations/utils/env-config.ts index de32f4db9..6da7044aa 100644 --- a/backend/src/db/migrations/utils/env-config.ts +++ b/backend/src/db/migrations/utils/env-config.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { crypto } from "@app/lib/crypto/cryptography"; +import { removeTrailingSlash } from "@app/lib/fn"; import { zpStr } from "@app/lib/zod"; import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; @@ -22,13 +23,17 @@ const envSchema = z HSM_LIB_PATH: zpStr(z.string().optional()), HSM_PIN: zpStr(z.string().optional()), HSM_KEY_LABEL: zpStr(z.string().optional()), - HSM_SLOT: z.coerce.number().optional().default(0) + HSM_SLOT: z.coerce.number().optional().default(0), + + LICENSE_SERVER_URL: zpStr(z.string().optional().default("https://portal.infisical.com")), + LICENSE_SERVER_KEY: zpStr(z.string().optional()), + LICENSE_KEY: zpStr(z.string().optional()), + LICENSE_KEY_OFFLINE: zpStr(z.string().optional()), + INTERNAL_REGION: zpStr(z.enum(["us", "eu"]).optional()), + + SITE_URL: zpStr(z.string().transform((val) => (val ? removeTrailingSlash(val) : val))).optional() }) // To ensure that basic encryption is always possible. - .refine( - (data) => Boolean(data.ENCRYPTION_KEY) || Boolean(data.ROOT_ENCRYPTION_KEY), - "Either ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY must be defined." - ) .transform((data) => ({ ...data, isHsmConfigured: diff --git a/backend/src/db/migrations/utils/services.ts b/backend/src/db/migrations/utils/services.ts index 1d61086fd..26640e38e 100644 --- a/backend/src/db/migrations/utils/services.ts +++ b/backend/src/db/migrations/utils/services.ts @@ -61,7 +61,8 @@ export const getMigrationEncryptionServices = async ({ envConfig, db, keyStore } licenseDAL, keyStore, identityOrgMembershipDAL, - projectDAL + projectDAL, + envConfig }); // ----- HSM startup ----- diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index c2e67908f..3a5928713 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -10,7 +10,7 @@ import { CronJob } from "cron"; import { Knex } from "knex"; import { TKeyStoreFactory } from "@app/keystore/keystore"; -import { getConfig } from "@app/lib/config/env"; +import { TEnvConfig } from "@app/lib/config/env"; import { verifyOfflineLicense } from "@app/lib/crypto"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; @@ -45,6 +45,10 @@ import { } from "./license-types"; type TLicenseServiceFactoryDep = { + envConfig: Pick< + TEnvConfig, + "LICENSE_SERVER_URL" | "LICENSE_SERVER_KEY" | "LICENSE_KEY" | "LICENSE_KEY_OFFLINE" | "INTERNAL_REGION" | "SITE_URL" + >; orgDAL: Pick; permissionService: Pick; licenseDAL: TLicenseDALFactory; @@ -67,26 +71,26 @@ export const licenseServiceFactory = ({ licenseDAL, keyStore, identityOrgMembershipDAL, - projectDAL + projectDAL, + envConfig }: TLicenseServiceFactoryDep) => { let isValidLicense = false; let instanceType = InstanceType.OnPrem; let onPremFeatures: TFeatureSet = getDefaultOnPremFeatures(); let selfHostedLicense: TOfflineLicense | null = null; - const appCfg = getConfig(); const licenseServerCloudApi = setupLicenseRequestWithStore( - appCfg.LICENSE_SERVER_URL || "", + envConfig.LICENSE_SERVER_URL || "", LICENSE_SERVER_CLOUD_LOGIN, - appCfg.LICENSE_SERVER_KEY || "", - appCfg.INTERNAL_REGION + envConfig.LICENSE_SERVER_KEY || "", + envConfig.INTERNAL_REGION ); const licenseServerOnPremApi = setupLicenseRequestWithStore( - appCfg.LICENSE_SERVER_URL || "", + envConfig.LICENSE_SERVER_URL || "", LICENSE_SERVER_ON_PREM_LOGIN, - appCfg.LICENSE_KEY || "", - appCfg.INTERNAL_REGION + envConfig.LICENSE_KEY || "", + envConfig.INTERNAL_REGION ); const syncLicenseKeyOnPremFeatures = async (shouldThrow: boolean = false) => { @@ -120,7 +124,7 @@ export const licenseServiceFactory = ({ const init = async () => { try { - if (appCfg.LICENSE_SERVER_KEY) { + if (envConfig.LICENSE_SERVER_KEY) { const token = await licenseServerCloudApi.refreshLicense(); if (token) instanceType = InstanceType.Cloud; logger.info(`Instance type: ${InstanceType.Cloud}`); @@ -128,7 +132,7 @@ export const licenseServiceFactory = ({ return; } - if (appCfg.LICENSE_KEY) { + if (envConfig.LICENSE_KEY) { const token = await licenseServerOnPremApi.refreshLicense(); if (token) { await syncLicenseKeyOnPremFeatures(true); @@ -139,10 +143,10 @@ export const licenseServiceFactory = ({ return; } - if (appCfg.LICENSE_KEY_OFFLINE) { + if (envConfig.LICENSE_KEY_OFFLINE) { let isValidOfflineLicense = true; const contents: TOfflineLicenseContents = JSON.parse( - Buffer.from(appCfg.LICENSE_KEY_OFFLINE, "base64").toString("utf8") + Buffer.from(envConfig.LICENSE_KEY_OFFLINE, "base64").toString("utf8") ); const isVerified = await verifyOfflineLicense(JSON.stringify(contents.license), contents.signature); @@ -181,7 +185,7 @@ export const licenseServiceFactory = ({ }; const initializeBackgroundSync = async () => { - if (appCfg.LICENSE_KEY) { + if (envConfig.LICENSE_KEY) { logger.info("Setting up background sync process for refresh onPremFeatures"); const job = new CronJob("*/10 * * * *", syncLicenseKeyOnPremFeatures); job.start(); @@ -397,8 +401,8 @@ export const licenseServiceFactory = ({ } = await licenseServerCloudApi.request.post( `/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods`, { - success_url: `${appCfg.SITE_URL}/organization/billing`, - cancel_url: `${appCfg.SITE_URL}/organization/billing` + success_url: `${envConfig.SITE_URL}/organization/billing`, + cancel_url: `${envConfig.SITE_URL}/organization/billing` } ); @@ -411,7 +415,7 @@ export const licenseServiceFactory = ({ } = await licenseServerCloudApi.request.post( `/api/license-server/v1/customers/${organization.customerId}/billing-details/billing-portal`, { - return_url: `${appCfg.SITE_URL}/organization/billing` + return_url: `${envConfig.SITE_URL}/organization/billing` } ); diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 15f878323..31e0eaeb4 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -363,11 +363,6 @@ const envSchema = z /* INTERNAL ----------------------------------------------------------------------------- */ INTERNAL_REGION: zpStr(z.enum(["us", "eu"]).optional()) }) - // To ensure that basic encryption is always possible. - .refine( - (data) => Boolean(data.ENCRYPTION_KEY) || Boolean(data.ROOT_ENCRYPTION_KEY), - "Either ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY must be defined." - ) .refine( (data) => Boolean(data.REDIS_URL) || Boolean(data.REDIS_SENTINEL_HOSTS) || Boolean(data.REDIS_CLUSTER_HOSTS), "Either REDIS_URL, REDIS_SENTINEL_HOSTS or REDIS_CLUSTER_HOSTS must be defined." diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index eb94afd4f..bee447c36 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -559,7 +559,8 @@ export const registerRoutes = async ( licenseDAL, keyStore, identityOrgMembershipDAL, - projectDAL + projectDAL, + envConfig }); const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, membershipUserDAL }); From b6694b0356cd82b332f0b3eda4dd9be47a1d7b38 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 21 Oct 2025 19:52:00 +0530 Subject: [PATCH 054/100] 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 055/100] 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 0f925cfaad3f835272ade12fe7fcd3e2a561e565 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 21 Oct 2025 20:48:47 +0400 Subject: [PATCH 056/100] smaller fixes --- backend/src/db/migrations/utils/services.ts | 2 +- backend/src/ee/services/hsm/hsm-fns.ts | 3 ++- backend/src/ee/services/hsm/hsm-service.ts | 15 ++++++++++++++- backend/src/services/kms/kms-service.ts | 4 ++-- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/backend/src/db/migrations/utils/services.ts b/backend/src/db/migrations/utils/services.ts index 26640e38e..e4b675f7e 100644 --- a/backend/src/db/migrations/utils/services.ts +++ b/backend/src/db/migrations/utils/services.ts @@ -68,13 +68,13 @@ export const getMigrationEncryptionServices = async ({ envConfig, db, keyStore } // ----- HSM startup ----- const hsmModule = initializeHsmModule(envConfig); + hsmModule.initialize(); const hsmService = hsmServiceFactory({ hsmModule: hsmModule.getModule(), envConfig }); - hsmModule.initialize(); await hsmService.startService(); const hsmStatus = await isHsmActiveAndEnabled({ diff --git a/backend/src/ee/services/hsm/hsm-fns.ts b/backend/src/ee/services/hsm/hsm-fns.ts index 352a36443..2603f80bd 100644 --- a/backend/src/ee/services/hsm/hsm-fns.ts +++ b/backend/src/ee/services/hsm/hsm-fns.ts @@ -35,6 +35,7 @@ export const initializeHsmModule = (envConfig: Pick null); - rootKmsConfigEncryptionStrategy = rootKmsConfig?.encryptionStrategy as RootKeyEncryptionStrategy | null; + rootKmsConfigEncryptionStrategy = (rootKmsConfig?.encryptionStrategy || null) as RootKeyEncryptionStrategy | null; if (rootKmsConfigEncryptionStrategy === RootKeyEncryptionStrategy.HSM && !licenseService.onPremFeatures.hsm) { throw new BadRequestError({ message: "Your license does not include HSM integration. Please upgrade to the Enterprise plan to use HSM." diff --git a/backend/src/ee/services/hsm/hsm-service.ts b/backend/src/ee/services/hsm/hsm-service.ts index 0ed4c5faf..3b332e446 100644 --- a/backend/src/ee/services/hsm/hsm-service.ts +++ b/backend/src/ee/services/hsm/hsm-service.ts @@ -460,10 +460,23 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon } }; + const randomBytes = async (length: number) => { + if (!pkcs11 || !isInitialized) { + throw new Error("PKCS#11 module is not initialized"); + } + + const randomData = await $withSession((sessionHandle) => + pkcs11.C_GenerateRandom(sessionHandle, Buffer.alloc(length)) + ); + + return randomData; + }; + return { encrypt, startService, isActive, - decrypt + decrypt, + randomBytes }; }; diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index 035b3db02..5b35f2f63 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -1083,8 +1083,8 @@ export const kmsServiceFactory = ({ const isHsmActive = hsmStatus.isHsmConfigured; - logger.info("KMS: Generating new ROOT Key"); - const newRootKey = crypto.randomBytes(32); + logger.info(`KMS: Generating new ROOT Key with ${isHsmActive ? "HSM" : "software"} encryption`); + const newRootKey = isHsmActive ? await hsmService.randomBytes(32) : crypto.randomBytes(32); const encryptionStrategy = isHsmActive ? RootKeyEncryptionStrategy.HSM : RootKeyEncryptionStrategy.Software; From 8a1b57281381766a324dd151dc1709e104e44108 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 21 Oct 2025 23:29:00 +0530 Subject: [PATCH 057/100] 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 aa8aff9d8b998da52206393a22c51da0837642e7 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 21 Oct 2025 22:01:43 +0400 Subject: [PATCH 058/100] fix: persist softhsm configuration --- backend/Dockerfile.dev | 7 ++++--- backend/Dockerfile.dev.fips | 7 ++++--- backend/dev-entrypoint.sh | 16 ++++++++++++++++ docker-compose.dev.yml | 3 +++ 4 files changed, 27 insertions(+), 6 deletions(-) create mode 100755 backend/dev-entrypoint.sh diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev index 5e17cf2bb..b5f4f7ac2 100644 --- a/backend/Dockerfile.dev +++ b/backend/Dockerfile.dev @@ -49,9 +49,6 @@ RUN rm -fr ${SOFTHSM2_SOURCES} # Install pkcs11-tool RUN apt-get install -y opensc -RUN mkdir -p /etc/softhsm2/tokens && \ - softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000 - # ? App setup # Install Infisical CLI @@ -64,10 +61,14 @@ WORKDIR /app COPY package.json package.json COPY package-lock.json package-lock.json +COPY dev-entrypoint.sh dev-entrypoint.sh +RUN chmod +x dev-entrypoint.sh + RUN npm install COPY . . ENV HOST=0.0.0.0 +ENTRYPOINT ["/app/dev-entrypoint.sh"] CMD ["npm", "run", "dev:docker"] diff --git a/backend/Dockerfile.dev.fips b/backend/Dockerfile.dev.fips index db5107985..4d5b84260 100644 --- a/backend/Dockerfile.dev.fips +++ b/backend/Dockerfile.dev.fips @@ -50,9 +50,6 @@ RUN rm -fr ${SOFTHSM2_SOURCES} # Install pkcs11-tool RUN apt-get install -y opensc -RUN mkdir -p /etc/softhsm2/tokens && \ - softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000 - WORKDIR /openssl-build RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \ && tar -xf openssl-3.1.2.tar.gz \ @@ -77,6 +74,9 @@ WORKDIR /app COPY package.json package.json COPY package-lock.json package-lock.json +COPY dev-entrypoint.sh dev-entrypoint.sh +RUN chmod +x dev-entrypoint.sh + RUN npm install COPY . . @@ -87,4 +87,5 @@ ENV OPENSSL_MODULES=/usr/local/lib/ossl-modules # ENV NODE_OPTIONS=--force-fips # Note(Daniel): We can't set this on the node options because it may break for existing folks using the infisical/infisical-fips image. Instead we call crypto.setFips(true) at runtime. ENV FIPS_ENABLED=true +ENTRYPOINT ["/app/dev-entrypoint.sh"] CMD ["npm", "run", "dev:docker"] diff --git a/backend/dev-entrypoint.sh b/backend/dev-entrypoint.sh new file mode 100755 index 000000000..9cb3c0a5e --- /dev/null +++ b/backend/dev-entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +update-ca-certificates + +# Initialize SoftHSM token if it doesn't exist +if [ ! -f /etc/softhsm2/tokens/auth-app.db ]; then + echo "Initializing SoftHSM token..." + mkdir -p /etc/softhsm2/tokens + softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000 + echo "SoftHSM token initialized" +else + echo "SoftHSM token already exists, skipping initialization" +fi + + +exec "$@" \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 00dc19a46..e60ef1ba5 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -77,6 +77,7 @@ services: - TELEMETRY_ENABLED=false volumes: - ./backend/src:/app/src + - softhsm_tokens:/etc/softhsm2/tokens # SoftHSM tokens are stored in a volume to persist across container restarts extra_hosts: - "host.docker.internal:host-gateway" @@ -198,3 +199,5 @@ volumes: ldap_data: ldap_config: grafana_storage: + softhsm_tokens: + driver: local \ No newline at end of file From 3b3203560cb9b85fa69ac1d5f89b85bb4ea9dda2 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Tue, 21 Oct 2025 10:23:11 -0700 Subject: [PATCH 059/100] 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 060/100] 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 061/100] 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 407a7e60758c6e1ac5f503e087e4a75264dfc917 Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 21 Oct 2025 14:54:05 -0400 Subject: [PATCH 062/100] remove default from rotationIntervalSeconds --- .../src/db/migrations/20251015042917_pam-account-rotation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/db/migrations/20251015042917_pam-account-rotation.ts b/backend/src/db/migrations/20251015042917_pam-account-rotation.ts index cd9a7d19a..eee9fd1fd 100644 --- a/backend/src/db/migrations/20251015042917_pam-account-rotation.ts +++ b/backend/src/db/migrations/20251015042917_pam-account-rotation.ts @@ -10,7 +10,7 @@ export async function up(knex: Knex): Promise { ) { await knex.schema.alterTable(TableName.PamAccount, (t) => { t.boolean("rotationEnabled").notNullable().defaultTo(false); - t.integer("rotationIntervalSeconds").notNullable().defaultTo(2592000); + t.integer("rotationIntervalSeconds").notNullable(); t.timestamp("lastRotatedAt").nullable(); }); } From c68da1201886a6936705c1942597d01e08577d54 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 21 Oct 2025 15:54:46 -0300 Subject: [PATCH 063/100] docs: enhance login documentation with detailed authentication methods and examples --- docs/cli/commands/login.mdx | 203 ++++++++++++++++++++++++++++++++++-- 1 file changed, 196 insertions(+), 7 deletions(-) diff --git a/docs/cli/commands/login.mdx b/docs/cli/commands/login.mdx index f93e3b4b2..50b96baa1 100644 --- a/docs/cli/commands/login.mdx +++ b/docs/cli/commands/login.mdx @@ -9,22 +9,92 @@ infisical login ### Description -The CLI uses authentication to verify your identity. When you enter the correct email and password for your account, a token is generated and saved in your system Keyring to allow you to make future interactions with the CLI. +The CLI uses authentication to verify your identity. You can authenticate using: +- **Browser Login** (default): Opens a browser for authentication +- **Direct Login**: Provide email and password via flags or environment variables for non-interactive workflows +- **Interactive CLI Login**: Use the `--interactive` flag to enter credentials via CLI prompts + +When authenticated, a token is generated and saved in your system Keyring to allow you to make future interactions with the CLI. To change where the login credentials are stored, visit the [vaults command](./vault). If you have added multiple users, you can switch between the users by using the [user command](./user). - When you authenticate with **any other method than `user`**, an access token will be printed to the console upon successful login. This token can be used to authenticate with the Infisical API and the CLI by passing it in the `--token` flag when applicable. - - Use flag `--plain` along with `--silent` to print only the token in plain text when using a machine identity auth method. - + **JWT Token Output:** + - For **user authentication** with the `--plain` flag: outputs only the JWT access token (useful for scripting) + - For **machine identity authentication**: an access token is always printed to the console + + Use the `--plain` flag to print only the token in plain text, which is ideal for capturing in environment variables or CI/CD pipelines. ### Authentication Methods -The Infisical CLI supports multiple authentication methods. Below are the available authentication methods, with their respective flags. +The Infisical CLI supports two main categories of authentication: User Authentication and Machine Identity Authentication. + +#### User Authentication + +User authentication is designed for individual developers and supports multiple login flows. + + + + The User authentication method allows you to log in with your email and password. This method supports three different login flows: + + - **Browser Login** (default): Opens a browser for authentication + - **Direct Login**: Provide credentials via flags or environment variables for CI/CD + - **Interactive CLI Login**: Enter credentials via CLI prompts using `--interactive` + + + + + Your email address. Required for direct login along with `--password`. + + + Your password. Required for direct login along with `--email`. + + + Force interactive CLI login instead of browser-based authentication. + + + Output only the JWT token (useful for scripting and CI/CD). + + + + + + + **Browser Login (Default)** + ```bash + infisical login + ``` + + **Direct Login (CI/CD)** + ```bash + infisical login --email=user@example.com --password=your-password + + # Or using environment variables + export INFISICAL_EMAIL="user@example.com" + export INFISICAL_PASSWORD="your-password" + infisical login + ``` + + **Interactive CLI Login** + ```bash + infisical login --interactive + ``` + + **Plain Token Output (for scripting)** + ```bash + export INFISICAL_TOKEN=$(infisical login --email=user@example.com --password=your-password --plain) + ``` + + + + + +#### Machine Identity Authentication + +Machine identity authentication methods are designed for automated systems, services, and CI/CD pipelines. @@ -330,6 +400,59 @@ The login command supports a number of flags that you can use for different auth + + ```bash + infisical login --email= --password= + ``` + + #### Description + Email address for direct user login. Must be used together with `--password` for non-interactive authentication. + + + The `email` flag can be substituted with the `INFISICAL_EMAIL` environment variable. + + + + + ```bash + infisical login --email= --password= + ``` + + #### Description + Password for direct user login. Must be used together with `--email` for non-interactive authentication. + + + For security in CI/CD environments, prefer using the `INFISICAL_PASSWORD` environment variable instead of passing the password as a command-line flag. + + + + The `password` flag can be substituted with the `INFISICAL_PASSWORD` environment variable. + + + + + ```bash + infisical login --interactive + ``` + + #### Description + Forces interactive CLI login where you'll be prompted to enter your email and password in the terminal, instead of opening a browser. + + + + ```bash + infisical login --email= --password= --plain + ``` + + #### Description + When used with direct user login or machine identity authentication, outputs only the JWT access token without any additional formatting. This is useful for scripting and CI/CD pipelines where you need to capture the token. + + ```bash + # Example: Capture token in a variable + export INFISICAL_TOKEN=$(infisical login --email= --password= --plain) + ``` + + @@ -346,6 +469,72 @@ The login command supports a number of flags that you can use for different auth +### User Authentication Examples + +The following examples demonstrate different ways to authenticate as a user with the Infisical CLI. + + + + Direct login is ideal for CI/CD pipelines and automation scripts where browser-based authentication is not possible. + + #### Using Command-Line Flags + + ```bash + # Basic direct login + infisical login --email user@example.com --password "your-password" + + # With custom domain (US Cloud) + infisical login --email user@example.com --password "your-password" --domain https://app.infisical.com + + # With custom domain (EU Cloud) + infisical login --email user@example.com --password "your-password" --domain https://eu.infisical.com + + # Output only JWT token for scripting + export INFISICAL_TOKEN=$(infisical login --email user@example.com --password "your-password" --plain) + ``` + + #### Using Environment Variables (Recommended for CI/CD) + + ```bash + # Set credentials as environment variables + export INFISICAL_EMAIL="user@example.com" + export INFISICAL_PASSWORD="your-password" + export INFISICAL_API_URL="https://app.infisical.com/api" + + # Login without additional flags + infisical login + + # Or with plain output for token capture + export INFISICAL_TOKEN=$(infisical login --plain) + ``` + + + + Interactive login prompts you to enter credentials in the terminal instead of opening a browser. + + ```bash + # Force interactive CLI login + infisical login --interactive + ``` + + You'll be prompted to enter: + - Email address + - Password + + + + + By default, running `infisical login` without any flags opens your browser for authentication. + + ```bash + # Opens browser for authentication + infisical login + ``` + + The browser will open to the Infisical login page, and upon successful authentication, the CLI will be automatically authenticated. + + + ### Machine Identity Authentication Quick Start @@ -367,7 +556,7 @@ In this example we'll be using the `universal-auth` method to login to obtain an ``` - + ```bash infisical secrets --projectId= Date: Tue, 21 Oct 2025 12:01:08 -0700 Subject: [PATCH 064/100] 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 065/100] 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 066/100] 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 ef22fb4366894b3432ef95cc3f909322501e02f6 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 21 Oct 2025 23:20:22 +0400 Subject: [PATCH 067/100] Update hsm-fns.ts --- backend/src/ee/services/hsm/hsm-fns.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/src/ee/services/hsm/hsm-fns.ts b/backend/src/ee/services/hsm/hsm-fns.ts index 2603f80bd..f4d0b8801 100644 --- a/backend/src/ee/services/hsm/hsm-fns.ts +++ b/backend/src/ee/services/hsm/hsm-fns.ts @@ -31,8 +31,6 @@ export const initializeHsmModule = (envConfig: Pick Date: Wed, 22 Oct 2025 03:28:54 +0800 Subject: [PATCH 068/100] misc: added handling for non-string vault values for migration --- .../hc-vault/hc-vault-connection-fns.ts | 21 +++++++++++++++++-- .../external-migration-service.ts | 3 ++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/backend/src/services/app-connection/hc-vault/hc-vault-connection-fns.ts b/backend/src/services/app-connection/hc-vault/hc-vault-connection-fns.ts index 38f97700c..425c9c4d2 100644 --- a/backend/src/services/app-connection/hc-vault/hc-vault-connection-fns.ts +++ b/backend/src/services/app-connection/hc-vault/hc-vault-connection-fns.ts @@ -25,6 +25,23 @@ import { THCVaultMountResponse } from "./hc-vault-connection-types"; +// HashiCorp Vault stores JSON data, so values can be any valid JSON type +type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }; + +export const convertVaultValueToString = (value: JsonValue): string => { + if (value === null) { + return ""; + } + if (typeof value === "string") { + return value; + } + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } + // For objects and arrays, serialize as JSON + return JSON.stringify(value); +}; + // Concurrency limit for HC Vault API requests to avoid rate limiting const HC_VAULT_CONCURRENCY_LIMIT = 20; @@ -598,7 +615,7 @@ export const getHCVaultSecretsForPath = async ( // For KV v2: /v1/{mount}/data/{path} const { data } = await requestWithHCVaultGateway<{ data: { - data: Record; // KV v2 has nested data structure + data: Record; // KV v2 has nested data structure, supports all JSON types metadata: { created_time: string; deletion_time: string; @@ -620,7 +637,7 @@ export const getHCVaultSecretsForPath = async ( // For KV v1: /v1/{mount}/{path} const { data } = await requestWithHCVaultGateway<{ - data: Record; // KV v1 has flat data structure + data: Record; // KV v1 has flat data structure, supports all JSON types lease_duration: number; lease_id: string; renewable: boolean; diff --git a/backend/src/services/external-migration/external-migration-service.ts b/backend/src/services/external-migration/external-migration-service.ts index 4192ffcd5..469b27bb3 100644 --- a/backend/src/services/external-migration/external-migration-service.ts +++ b/backend/src/services/external-migration/external-migration-service.ts @@ -16,6 +16,7 @@ import { AppConnection } from "../app-connection/app-connection-enums"; import { decryptAppConnectionCredentials } from "../app-connection/app-connection-fns"; import { TAppConnectionServiceFactory } from "../app-connection/app-connection-service"; import { + convertVaultValueToString, getHCVaultAuthMounts, getHCVaultKubernetesAuthRoles, getHCVaultSecretsForPath, @@ -581,7 +582,7 @@ export const externalMigrationServiceFactory = ({ projectId, secrets: Object.entries(vaultSecrets).map(([secretKey, secretValue]) => ({ secretKey, - secretValue + secretValue: convertVaultValueToString(secretValue) })) }); From 10cebfbbe29b824a6ef5b7def3e475e91195e19e Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Tue, 21 Oct 2025 16:37:08 -0300 Subject: [PATCH 069/100] Check identitiesUsed instead of identityLimit on getPlan --- backend/src/ee/services/license/license-service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index fba9d0cca..817f4609c 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -212,9 +212,8 @@ export const licenseServiceFactory = ({ const membersUsed = await licenseDAL.countOfOrgMembers(orgId); currentPlan.membersUsed = membersUsed; const identityUsed = await licenseDAL.countOrgUsersAndIdentities(orgId); - currentPlan.identitiesUsed = identityUsed; - if (currentPlan.identityLimit && currentPlan.identityLimit !== identityUsed) { + if (currentPlan?.identitiesUsed && currentPlan.identitiesUsed !== identityUsed) { try { await licenseServerCloudApi.request.patch(`/api/license-server/v1/customers/${org.customerId}/cloud-plan`, { quantity: membersUsed, @@ -227,6 +226,7 @@ export const licenseServiceFactory = ({ ); } } + currentPlan.identitiesUsed = identityUsed; await keyStore.setItemWithExpiry( FEATURE_CACHE_KEY(org.id), From 98b2c556b8dc66b786997ebc61c98214284490a8 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 21 Oct 2025 16:45:57 -0300 Subject: [PATCH 070/100] docs: fix syntax error in login command example for fetching secrets --- docs/cli/commands/login.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cli/commands/login.mdx b/docs/cli/commands/login.mdx index 50b96baa1..16fef7985 100644 --- a/docs/cli/commands/login.mdx +++ b/docs/cli/commands/login.mdx @@ -558,7 +558,7 @@ In this example we'll be using the `universal-auth` method to login to obtain an ```bash - infisical secrets --projectId= --env=dev --recursive ``` This command will fetch all secrets from the `dev` environment in your project, including all secrets in subfolders. From 65b36e6232cfbfb4e4a3c88752a317c000babba6 Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 21 Oct 2025 15:57:44 -0400 Subject: [PATCH 071/100] use __INFISICAL_UNCHANGED__ instead of empty string for unchanged placeholder --- .../pam-account/pam-account-service.ts | 23 +++++++++++++++---- .../pam-resource/pam-resource-service.ts | 5 +++- .../PamAccountForm/PostgresAccountForm.tsx | 2 +- .../shared/SqlAccountFields.tsx | 4 ++-- .../PamResourceForm/PostgresResourceForm.tsx | 2 +- .../shared/SqlRotateAccountFields.tsx | 4 ++-- 6 files changed, 29 insertions(+), 11 deletions(-) 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 83e488231..2dfdec196 100644 --- a/backend/src/ee/services/pam-account/pam-account-service.ts +++ b/backend/src/ee/services/pam-account/pam-account-service.ts @@ -150,7 +150,12 @@ export const pamAccountServiceFactory = ({ return { ...(await decryptAccount(account, resource.projectId, kmsService)), - resource: { id: resource.id, name: resource.name, resourceType: resource.resourceType } + resource: { + id: resource.id, + name: resource.name, + resourceType: resource.resourceType, + rotationCredentialsConfigured: !!resource.encryptedRotationAccountCredentials + } }; } catch (err) { if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) { @@ -241,7 +246,7 @@ export const pamAccountServiceFactory = ({ // Logic to prevent overwriting unedited censored values const finalCredentials = { ...credentials }; - if (credentials.password === "******") { + if (credentials.password === "__INFISICAL_UNCHANGED__") { const decryptedCredentials = await decryptAccountCredentials({ encryptedCredentials: account.encryptedCredentials, projectId: account.projectId, @@ -269,7 +274,12 @@ export const pamAccountServiceFactory = ({ return { ...(await decryptAccount(updatedAccount, account.projectId, kmsService)), - resource: { id: resource.id, name: resource.name, resourceType: resource.resourceType } + resource: { + id: resource.id, + name: resource.name, + resourceType: resource.resourceType, + rotationCredentialsConfigured: !!resource.encryptedRotationAccountCredentials + } }; }; @@ -308,7 +318,12 @@ export const pamAccountServiceFactory = ({ return { ...(await decryptAccount(deletedAccount, account.projectId, kmsService)), - resource: { id: resource.id, name: resource.name, resourceType: resource.resourceType } + resource: { + id: resource.id, + name: resource.name, + resourceType: resource.resourceType, + rotationCredentialsConfigured: !!resource.encryptedRotationAccountCredentials + } }; }; diff --git a/backend/src/ee/services/pam-resource/pam-resource-service.ts b/backend/src/ee/services/pam-resource/pam-resource-service.ts index 9ec702b24..d97905dbe 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-service.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-service.ts @@ -194,7 +194,10 @@ export const pamResourceServiceFactory = ({ // Logic to prevent overwriting unedited censored values const finalCredentials = { ...rotationAccountCredentials }; - if (resource.encryptedRotationAccountCredentials && rotationAccountCredentials.password === "******") { + if ( + resource.encryptedRotationAccountCredentials && + rotationAccountCredentials.password === "__INFISICAL_UNCHANGED__" + ) { const decryptedCredentials = await decryptAccountCredentials({ encryptedCredentials: resource.encryptedRotationAccountCredentials, projectId: resource.projectId, diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx index c353a278b..17d68bdf0 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx @@ -34,7 +34,7 @@ export const PostgresAccountForm = ({ account, resourceId, resourceType, onSubmi ...account, credentials: { ...account.credentials, - password: "******" + password: "__INFISICAL_UNCHANGED__" } } : undefined diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/shared/SqlAccountFields.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/shared/SqlAccountFields.tsx index 7d9e85c44..0fd5760f3 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/shared/SqlAccountFields.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/shared/SqlAccountFields.tsx @@ -38,14 +38,14 @@ export const SqlAccountFields = ({ isUpdate }: { isUpdate: boolean }) => { type={showPassword ? "text" : "password"} autoComplete="new-password" onFocus={() => { - if (isUpdate && field.value === "******") { + if (isUpdate && field.value === "__INFISICAL_UNCHANGED__") { field.onChange(""); } setShowPassword(true); }} onBlur={() => { if (isUpdate && field.value === "") { - field.onChange("******"); + field.onChange("__INFISICAL_UNCHANGED__"); } setShowPassword(false); }} diff --git a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PostgresResourceForm.tsx b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PostgresResourceForm.tsx index 20ac710c3..2c53c89df 100644 --- a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PostgresResourceForm.tsx +++ b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PostgresResourceForm.tsx @@ -37,7 +37,7 @@ export const PostgresResourceForm = ({ resource, onSubmit }: Props) => { rotationAccountCredentials: resource.rotationAccountCredentials ? { ...resource.rotationAccountCredentials, - password: "******" + password: "__INFISICAL_UNCHANGED__" } : resource.rotationAccountCredentials } diff --git a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/shared/SqlRotateAccountFields.tsx b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/shared/SqlRotateAccountFields.tsx index 869594140..6ce92b8e6 100644 --- a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/shared/SqlRotateAccountFields.tsx +++ b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/shared/SqlRotateAccountFields.tsx @@ -55,14 +55,14 @@ export const SqlRotateAccountFields = ({ isUpdate }: { isUpdate: boolean }) => { type={showPassword ? "text" : "password"} autoComplete="new-password" onFocus={() => { - if (isUpdate && field.value === "******") { + if (isUpdate && field.value === "__INFISICAL_UNCHANGED__") { field.onChange(""); } setShowPassword(true); }} onBlur={() => { if (isUpdate && field.value === "") { - field.onChange("******"); + field.onChange("__INFISICAL_UNCHANGED__"); } setShowPassword(false); }} From 008b67738b42a8d644454a29ea1c0f2d9c150304 Mon Sep 17 00:00:00 2001 From: = Date: Wed, 22 Oct 2025 01:44:11 +0530 Subject: [PATCH 072/100] 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 85daaf806e536f749140a94ffc7bdc0c4085f41f Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 21 Oct 2025 16:21:52 -0400 Subject: [PATCH 073/100] Make rotationIntervalSeconds nullable --- .../db/migrations/20251015042917_pam-account-rotation.ts | 2 +- backend/src/db/schemas/pam-accounts.ts | 2 +- .../routes/v1/pam-account-routers/pam-account-endpoints.ts | 2 +- backend/src/ee/services/audit-log/audit-log-types.ts | 4 ++-- backend/src/ee/services/pam-account/pam-account-dal.ts | 1 + .../src/ee/services/pam-resource/pam-resource-schemas.ts | 4 ++-- frontend/src/hooks/api/pam/types/base-account.ts | 2 +- .../components/PamAccountForm/RotateAccountFields.tsx | 6 +++--- 8 files changed, 12 insertions(+), 11 deletions(-) diff --git a/backend/src/db/migrations/20251015042917_pam-account-rotation.ts b/backend/src/db/migrations/20251015042917_pam-account-rotation.ts index eee9fd1fd..4e9fc90d4 100644 --- a/backend/src/db/migrations/20251015042917_pam-account-rotation.ts +++ b/backend/src/db/migrations/20251015042917_pam-account-rotation.ts @@ -10,7 +10,7 @@ export async function up(knex: Knex): Promise { ) { await knex.schema.alterTable(TableName.PamAccount, (t) => { t.boolean("rotationEnabled").notNullable().defaultTo(false); - t.integer("rotationIntervalSeconds").notNullable(); + t.integer("rotationIntervalSeconds").nullable(); t.timestamp("lastRotatedAt").nullable(); }); } diff --git a/backend/src/db/schemas/pam-accounts.ts b/backend/src/db/schemas/pam-accounts.ts index 9bcfa5cec..7e78e0874 100644 --- a/backend/src/db/schemas/pam-accounts.ts +++ b/backend/src/db/schemas/pam-accounts.ts @@ -20,7 +20,7 @@ export const PamAccountsSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), rotationEnabled: z.boolean().default(false), - rotationIntervalSeconds: z.number().default(2592000), + rotationIntervalSeconds: z.number().nullable().optional(), lastRotatedAt: z.date().nullable().optional() }); diff --git a/backend/src/ee/routes/v1/pam-account-routers/pam-account-endpoints.ts b/backend/src/ee/routes/v1/pam-account-routers/pam-account-endpoints.ts index dccaaa8ca..44e2a5ea1 100644 --- a/backend/src/ee/routes/v1/pam-account-routers/pam-account-endpoints.ts +++ b/backend/src/ee/routes/v1/pam-account-routers/pam-account-endpoints.ts @@ -23,7 +23,7 @@ export const registerPamResourceEndpoints = ({ name: C["name"]; description?: C["description"]; rotationEnabled: C["rotationEnabled"]; - rotationIntervalSeconds: C["rotationIntervalSeconds"]; + rotationIntervalSeconds?: C["rotationIntervalSeconds"]; }>; updateAccountSchema: z.ZodType<{ credentials?: C["credentials"]; 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 94561e6d8..8569d764b 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -3798,7 +3798,7 @@ interface PamAccountCreateEvent { name: string; description?: string | null; rotationEnabled: boolean; - rotationIntervalSeconds: number; + rotationIntervalSeconds?: number | null; }; } @@ -3811,7 +3811,7 @@ interface PamAccountUpdateEvent { name?: string; description?: string | null; rotationEnabled?: boolean; - rotationIntervalSeconds?: number; + rotationIntervalSeconds?: number | null; }; } diff --git a/backend/src/ee/services/pam-account/pam-account-dal.ts b/backend/src/ee/services/pam-account/pam-account-dal.ts index e0377871c..4c8f5136a 100644 --- a/backend/src/ee/services/pam-account/pam-account-dal.ts +++ b/backend/src/ee/services/pam-account/pam-account-dal.ts @@ -49,6 +49,7 @@ export const pamAccountDALFactory = (db: TDbClient) => { const accounts = await dbClient(TableName.PamAccount) .innerJoin(TableName.PamResource, `${TableName.PamAccount}.resourceId`, `${TableName.PamResource}.id`) .whereNotNull(`${TableName.PamResource}.encryptedRotationAccountCredentials`) + .whereNotNull(`${TableName.PamAccount}.rotationIntervalSeconds`) .whereRaw( `COALESCE("${TableName.PamAccount}"."lastRotatedAt", "${TableName.PamAccount}"."createdAt") + "${TableName.PamAccount}"."rotationIntervalSeconds" * interval '1 second' < NOW()` ) diff --git a/backend/src/ee/services/pam-resource/pam-resource-schemas.ts b/backend/src/ee/services/pam-resource/pam-resource-schemas.ts index 8f4752f4e..7f6165d88 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-schemas.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-schemas.ts @@ -42,12 +42,12 @@ export const BaseCreatePamAccountSchema = z.object({ name: slugSchema({ field: "name" }), description: z.string().max(512).nullable().optional(), rotationEnabled: z.boolean(), - rotationIntervalSeconds: z.number().min(3600) + rotationIntervalSeconds: z.number().min(3600).nullable().optional() }); export const BaseUpdatePamAccountSchema = z.object({ name: slugSchema({ field: "name" }).optional(), description: z.string().max(512).nullable().optional(), rotationEnabled: z.boolean().optional(), - rotationIntervalSeconds: z.number().min(3600).optional() + rotationIntervalSeconds: z.number().min(3600).nullable().optional() }); diff --git a/frontend/src/hooks/api/pam/types/base-account.ts b/frontend/src/hooks/api/pam/types/base-account.ts index 09ad6550d..20cb7aa60 100644 --- a/frontend/src/hooks/api/pam/types/base-account.ts +++ b/frontend/src/hooks/api/pam/types/base-account.ts @@ -14,7 +14,7 @@ export interface TBasePamAccount { name: string; description?: string | null; rotationEnabled: boolean; - rotationIntervalSeconds: number; + rotationIntervalSeconds?: number | null; lastRotatedAt?: string | null; createdAt: string; updatedAt: string; diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/RotateAccountFields.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/RotateAccountFields.tsx index 2739e1cd5..a73fad58b 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/RotateAccountFields.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/RotateAccountFields.tsx @@ -6,7 +6,7 @@ import { FormControl, Select, SelectItem, Switch, Tooltip } from "@app/component export const rotateAccountFieldsSchema = z.object({ rotationEnabled: z.boolean(), - rotationIntervalSeconds: z.number() + rotationIntervalSeconds: z.number().nullable().optional() }); export const RotateAccountFields = ({ @@ -16,7 +16,7 @@ export const RotateAccountFields = ({ }) => { const { control, watch } = useFormContext<{ rotationEnabled: boolean; - rotationIntervalSeconds: number; + rotationIntervalSeconds?: number | null; }>(); const rotationEnabled = watch("rotationEnabled"); @@ -62,7 +62,7 @@ export const RotateAccountFields = ({ className="mb-0" >