From b80b77ec367ba8585e1e8ca4a0243b13b3d1191e Mon Sep 17 00:00:00 2001 From: = Date: Tue, 24 Jun 2025 16:46:46 +0530 Subject: [PATCH 01/97] feat: completed backend changes for tls auth --- backend/src/@types/fastify.d.ts | 2 + backend/src/@types/knex.d.ts | 8 + .../20250624061429_identity-tls-auth.ts | 28 ++ .../src/db/schemas/identity-tls-cert-auths.ts | 27 ++ backend/src/db/schemas/index.ts | 1 + backend/src/db/schemas/models.ts | 2 + .../ee/services/audit-log/audit-log-types.ts | 58 +++ backend/src/lib/api-docs/constants.ts | 33 ++ backend/src/server/routes/index.ts | 13 + .../v1/identity-tls-cert-auth-router.ts | 345 +++++++++++++++ backend/src/server/routes/v1/index.ts | 2 + .../identity-access-token-dal.ts | 9 +- .../identity-access-token-service.ts | 3 +- .../identity-tls-cert-auth-dal.ts | 10 + .../identity-tls-cert-auth-service.ts | 417 ++++++++++++++++++ .../identity-tls-cert-auth-types.ts | 49 ++ 16 files changed, 1005 insertions(+), 2 deletions(-) create mode 100644 backend/src/db/migrations/20250624061429_identity-tls-auth.ts create mode 100644 backend/src/db/schemas/identity-tls-cert-auths.ts create mode 100644 backend/src/server/routes/v1/identity-tls-cert-auth-router.ts create mode 100644 backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-dal.ts create mode 100644 backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts create mode 100644 backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 4fe4e17cf..2d641b4a0 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -110,6 +110,7 @@ import { TUserServiceFactory } from "@app/services/user/user-service"; import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service"; import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service"; import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service"; +import { TIdentityTlsCertAuthServiceFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-types"; declare module "@fastify/request-context" { interface RequestContextData { @@ -218,6 +219,7 @@ declare module "fastify" { identityKubernetesAuth: TIdentityKubernetesAuthServiceFactory; identityGcpAuth: TIdentityGcpAuthServiceFactory; identityAliCloudAuth: TIdentityAliCloudAuthServiceFactory; + identityTlsCertAuth: TIdentityTlsCertAuthServiceFactory; identityAwsAuth: TIdentityAwsAuthServiceFactory; identityAzureAuth: TIdentityAzureAuthServiceFactory; identityOciAuth: TIdentityOciAuthServiceFactory; diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 1d4ad1797..7ead9f84b 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -164,6 +164,9 @@ import { TIdentityProjectMemberships, TIdentityProjectMembershipsInsert, TIdentityProjectMembershipsUpdate, + TIdentityTlsCertAuths, + TIdentityTlsCertAuthsInsert, + TIdentityTlsCertAuthsUpdate, TIdentityTokenAuths, TIdentityTokenAuthsInsert, TIdentityTokenAuthsUpdate, @@ -794,6 +797,11 @@ declare module "knex/types/tables" { TIdentityAlicloudAuthsInsert, TIdentityAlicloudAuthsUpdate >; + [TableName.IdentityTlsCertAuth]: KnexOriginal.CompositeTableType< + TIdentityTlsCertAuths, + TIdentityTlsCertAuthsInsert, + TIdentityTlsCertAuthsUpdate + >; [TableName.IdentityAwsAuth]: KnexOriginal.CompositeTableType< TIdentityAwsAuths, TIdentityAwsAuthsInsert, diff --git a/backend/src/db/migrations/20250624061429_identity-tls-auth.ts b/backend/src/db/migrations/20250624061429_identity-tls-auth.ts new file mode 100644 index 000000000..3e6dc1af8 --- /dev/null +++ b/backend/src/db/migrations/20250624061429_identity-tls-auth.ts @@ -0,0 +1,28 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.IdentityTlsCertAuth))) { + await knex.schema.createTable(TableName.IdentityTlsCertAuth, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.bigInteger("accessTokenTTL").defaultTo(7200).notNullable(); + t.bigInteger("accessTokenMaxTTL").defaultTo(7200).notNullable(); + t.bigInteger("accessTokenNumUsesLimit").defaultTo(0).notNullable(); + t.jsonb("accessTokenTrustedIps").notNullable(); + t.timestamps(true, true, true); + t.uuid("identityId").notNullable().unique(); + t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE"); + t.string("allowedCommonNames").nullable(); + t.binary("encryptedCaCertificate").notNullable(); + }); + } + + await createOnUpdateTrigger(knex, TableName.IdentityTlsCertAuth); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.IdentityTlsCertAuth); + await dropOnUpdateTrigger(knex, TableName.IdentityTlsCertAuth); +} diff --git a/backend/src/db/schemas/identity-tls-cert-auths.ts b/backend/src/db/schemas/identity-tls-cert-auths.ts new file mode 100644 index 000000000..c907ead73 --- /dev/null +++ b/backend/src/db/schemas/identity-tls-cert-auths.ts @@ -0,0 +1,27 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const IdentityTlsCertAuthsSchema = z.object({ + id: z.string().uuid(), + accessTokenTTL: z.coerce.number().default(7200), + accessTokenMaxTTL: z.coerce.number().default(7200), + accessTokenNumUsesLimit: z.coerce.number().default(0), + accessTokenTrustedIps: z.unknown(), + createdAt: z.date(), + updatedAt: z.date(), + identityId: z.string().uuid(), + allowedCommonNames: z.string().nullable().optional(), + encryptedCaCertificate: zodBuffer +}); + +export type TIdentityTlsCertAuths = z.infer; +export type TIdentityTlsCertAuthsInsert = Omit, TImmutableDBKeys>; +export type TIdentityTlsCertAuthsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 292551c80..1642c3555 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -52,6 +52,7 @@ export * from "./identity-org-memberships"; export * from "./identity-project-additional-privilege"; export * from "./identity-project-membership-role"; export * from "./identity-project-memberships"; +export * from "./identity-tls-cert-auths"; export * from "./identity-token-auths"; export * from "./identity-ua-client-secrets"; export * from "./identity-universal-auths"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index ceba6e370..d47962110 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -86,6 +86,7 @@ export enum TableName { IdentityOidcAuth = "identity_oidc_auths", IdentityJwtAuth = "identity_jwt_auths", IdentityLdapAuth = "identity_ldap_auths", + IdentityTlsCertAuth = "identity_tls_cert_auths", IdentityOrgMembership = "identity_org_memberships", IdentityProjectMembership = "identity_project_memberships", IdentityProjectMembershipRole = "identity_project_membership_role", @@ -251,6 +252,7 @@ export enum IdentityAuthMethod { ALICLOUD_AUTH = "alicloud-auth", AWS_AUTH = "aws-auth", AZURE_AUTH = "azure-auth", + TLS_CERT_AUTH = "tls-cert-auth", OCI_AUTH = "oci-auth", OIDC_AUTH = "oidc-auth", JWT_AUTH = "jwt-auth", 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 e72b9fa46..a2acb62c4 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -202,6 +202,12 @@ export enum EventType { REVOKE_IDENTITY_ALICLOUD_AUTH = "revoke-identity-alicloud-auth", GET_IDENTITY_ALICLOUD_AUTH = "get-identity-alicloud-auth", + LOGIN_IDENTITY_TLS_CERT_AUTH = "login-identity-tls-cert-auth", + ADD_IDENTITY_TLS_CERT_AUTH = "add-identity-tls-cert-auth", + UPDATE_IDENTITY_TLS_CERT_AUTH = "update-identity-tls-cert-auth", + REVOKE_IDENTITY_TLS_CERT_AUTH = "revoke-identity-tls-cert-auth", + GET_IDENTITY_TLS_CERT_AUTH = "get-identity-tls-cert-auth", + LOGIN_IDENTITY_AWS_AUTH = "login-identity-aws-auth", ADD_IDENTITY_AWS_AUTH = "add-identity-aws-auth", UPDATE_IDENTITY_AWS_AUTH = "update-identity-aws-auth", @@ -1141,6 +1147,53 @@ interface GetIdentityAliCloudAuthEvent { }; } +interface LoginIdentityTlsCertAuthEvent { + type: EventType.LOGIN_IDENTITY_TLS_CERT_AUTH; + metadata: { + identityId: string; + identityTlsCertAuthId: string; + identityAccessTokenId: string; + }; +} + +interface AddIdentityTlsCertAuthEvent { + type: EventType.ADD_IDENTITY_TLS_CERT_AUTH; + metadata: { + identityId: string; + allowedCommonNames: string | null | undefined; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: Array; + }; +} + +interface DeleteIdentityTlsCertAuthEvent { + type: EventType.REVOKE_IDENTITY_TLS_CERT_AUTH; + metadata: { + identityId: string; + }; +} + +interface UpdateIdentityTlsCertAuthEvent { + type: EventType.UPDATE_IDENTITY_TLS_CERT_AUTH; + metadata: { + identityId: string; + allowedCommonNames: string | null | undefined; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: Array; + }; +} + +interface GetIdentityTlsCertAuthEvent { + type: EventType.GET_IDENTITY_TLS_CERT_AUTH; + metadata: { + identityId: string; + }; +} + interface LoginIdentityOciAuthEvent { type: EventType.LOGIN_IDENTITY_OCI_AUTH; metadata: { @@ -3358,6 +3411,11 @@ export type Event = | UpdateIdentityAliCloudAuthEvent | GetIdentityAliCloudAuthEvent | DeleteIdentityAliCloudAuthEvent + | LoginIdentityTlsCertAuthEvent + | AddIdentityTlsCertAuthEvent + | UpdateIdentityTlsCertAuthEvent + | GetIdentityTlsCertAuthEvent + | DeleteIdentityTlsCertAuthEvent | LoginIdentityOciAuthEvent | AddIdentityOciAuthEvent | UpdateIdentityOciAuthEvent diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 67b70079b..18cb4889c 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -22,6 +22,7 @@ export enum ApiDocsTags { UniversalAuth = "Universal Auth", GcpAuth = "GCP Auth", AliCloudAuth = "Alibaba Cloud Auth", + TlsCertAuth = "TLS Certificate Auth", AwsAuth = "AWS Auth", OciAuth = "OCI Auth", AzureAuth = "Azure Auth", @@ -283,6 +284,38 @@ export const ALICLOUD_AUTH = { } } as const; +export const TLS_CERT_AUTH = { + LOGIN: { + identityId: "The ID of the identity to login." + }, + ATTACH: { + identityId: "The ID of the identity to attach the configuration onto.", + allowedCommonNames: + "The comma-separated list of trusted common names that are allowed to authenticate with Infisical.", + caCertificate: "The PEM-encoded CA certificate to validate client certificates.", + accessTokenTTL: "The lifetime for an access token in seconds.", + accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.", + accessTokenNumUsesLimit: "The maximum number of times that an access token can be used.", + accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from." + }, + UPDATE: { + identityId: "The ID of the identity to update the auth method for.", + allowedCommonNames: + "The comma-separated list of trusted common names that are allowed to authenticate with Infisical.", + caCertificate: "The PEM-encoded CA certificate to validate client certificates.", + accessTokenTTL: "The new lifetime for an access token in seconds.", + accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.", + accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.", + accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from." + }, + RETRIEVE: { + identityId: "The ID of the identity to retrieve the auth method for." + }, + REVOKE: { + identityId: "The ID of the identity to revoke the auth method for." + } +} as const; + export const AWS_AUTH = { LOGIN: { identityId: "The ID of the identity to login.", diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index bde1b805e..3e5735fb9 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -301,6 +301,8 @@ import { registerSecretScannerGhApp } from "../plugins/secret-scanner"; import { registerV1Routes } from "./v1"; import { registerV2Routes } from "./v2"; import { registerV3Routes } from "./v3"; +import { identityTlsCertAuthDALFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-dal"; +import { identityTlsCertAuthServiceFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-service"; const histogram = monitorEventLoopDelay({ resolution: 20 }); histogram.enable(); @@ -386,6 +388,7 @@ export const registerRoutes = async ( const identityKubernetesAuthDAL = identityKubernetesAuthDALFactory(db); const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db); const identityAliCloudAuthDAL = identityAliCloudAuthDALFactory(db); + const identityTlsCertAuthDAL = identityTlsCertAuthDALFactory(db); const identityAwsAuthDAL = identityAwsAuthDALFactory(db); const identityGcpAuthDAL = identityGcpAuthDALFactory(db); const identityOciAuthDAL = identityOciAuthDALFactory(db); @@ -1493,6 +1496,15 @@ export const registerRoutes = async ( permissionService }); + const identityTlsCertAuthService = identityTlsCertAuthServiceFactory({ + identityAccessTokenDAL, + identityTlsCertAuthDAL, + identityOrgMembershipDAL, + licenseService, + permissionService, + kmsService + }); + const identityAwsAuthService = identityAwsAuthServiceFactory({ identityAccessTokenDAL, identityAwsAuthDAL, @@ -1947,6 +1959,7 @@ export const registerRoutes = async ( identityAwsAuth: identityAwsAuthService, identityAzureAuth: identityAzureAuthService, identityOciAuth: identityOciAuthService, + identityTlsCertAuth: identityTlsCertAuthService, identityOidcAuth: identityOidcAuthService, identityJwtAuth: identityJwtAuthService, identityLdapAuth: identityLdapAuthService, 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 new file mode 100644 index 000000000..38190d7e7 --- /dev/null +++ b/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts @@ -0,0 +1,345 @@ +import { z } from "zod"; + +// import { TLSSocket } from "tls"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { ApiDocsTags, TLS_CERT_AUTH } from "@app/lib/api-docs"; +import { IdentityTlsCertAuthsSchema } from "@app/db/schemas"; +import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; + +const validateCommonNames = z + .string() + .min(1) + .trim() + .transform((el) => + el + .split(",") + .map((i) => i.trim()) + .join(",") + ); + +export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvider) => { + // server.route({ + // method: "GET", + // url: "/", + // config: { + // rateLimit: readLimit + // }, + // schema: { + // params: z.object({}), + // response: { + // 200: z.object({}) + // } + // }, + // onRequest: verifyAuth([AuthMode.JWT]), + // handler: async (req) => { + // const { socket } = req; + // if (socket instanceof TLSSocket && socket.encrypted) { + // // Inside this block, TypeScript now knows `socket` is a TlsSocket + // const certificate = socket.getPeerCertificate(); + // + // if (Object.keys(certificate).length === 0) { + // return reply.send({ message: "Client did not provide a certificate." }); + // } + // + // return reply.send({ + // message: "Certificate received!", + // subject: certificate.subject, + // issuer: certificate.issuer, + // fingerprint: certificate.fingerprint + // }); + // } else { + // // This will handle plain HTTP requests gracefully + // return reply + // .status(400) + // .send({ error: "This endpoint requires an HTTPS connection with a client certificate." }); + // } + // } + // }); + + server.route({ + method: "POST", + url: "/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.TlsCertAuth], + description: "Attach TLS Certificate Auth configuration onto identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().trim().describe(TLS_CERT_AUTH.ATTACH.identityId) + }), + body: z + .object({ + allowedCommonNames: validateCommonNames.describe(TLS_CERT_AUTH.ATTACH.allowedCommonNames), + caCertificate: z.string().min(1).describe(TLS_CERT_AUTH.ATTACH.caCertificate), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(TLS_CERT_AUTH.ATTACH.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(TLS_CERT_AUTH.ATTACH.accessTokenTTL), + accessTokenMaxTTL: z + .number() + .int() + .min(1) + .max(315360000) + .default(2592000) + .describe(TLS_CERT_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .default(0) + .describe(TLS_CERT_AUTH.ATTACH.accessTokenNumUsesLimit) + }) + .refine( + (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, + "Access Token TTL cannot be greater than Access Token Max TTL." + ), + response: { + 200: z.object({ + identityTlsCloudAuth: IdentityTlsCertAuthsSchema + }) + } + }, + handler: async (req) => { + const identityTlsCertAuth = await server.services.identityTlsCertAuth.attachTlsCertAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body, + identityId: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth) + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.ADD_IDENTITY_TLS_CERT_AUTH, + metadata: { + identityId: identityTlsCertAuth.identityId, + allowedCommonNames: identityTlsCertAuth.allowedCommonNames, + accessTokenTTL: identityTlsCertAuth.accessTokenTTL, + accessTokenMaxTTL: identityTlsCertAuth.accessTokenMaxTTL, + accessTokenTrustedIps: identityTlsCertAuth.accessTokenTrustedIps as TIdentityTrustedIp[], + accessTokenNumUsesLimit: identityTlsCertAuth.accessTokenNumUsesLimit + } + } + }); + + return { identityTlsCertAuth }; + } + }); + + server.route({ + method: "PATCH", + url: "/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.TlsCertAuth], + description: "Update Tls Certificate Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().describe(TLS_CERT_AUTH.UPDATE.identityId) + }), + body: z + .object({ + allowedCommonNames: validateCommonNames.describe(TLS_CERT_AUTH.UPDATE.allowedCommonNames), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .optional() + .describe(TLS_CERT_AUTH.UPDATE.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .optional() + .describe(TLS_CERT_AUTH.UPDATE.accessTokenTTL), + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .optional() + .describe(TLS_CERT_AUTH.UPDATE.accessTokenNumUsesLimit), + accessTokenMaxTTL: z + .number() + .int() + .max(315360000) + .min(0) + .optional() + .describe(TLS_CERT_AUTH.UPDATE.accessTokenMaxTTL) + }) + .refine( + (val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true), + "Access Token TTL cannot be greater than Access Token Max TTL." + ), + response: { + 200: z.object({ + identityTlsCloudAuth: IdentityTlsCertAuthsSchema + }) + } + }, + handler: async (req) => { + const identityTlsCertAuth = await server.services.identityTlsCertAuth.updateTlsCertAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body, + identityId: req.params.identityId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.UPDATE_IDENTITY_TLS_CERT_AUTH, + metadata: { + identityId: identityTlsCertAuth.identityId, + allowedCommonNames: identityTlsCertAuth.allowedCommonNames, + accessTokenTTL: identityTlsCertAuth.accessTokenTTL, + accessTokenMaxTTL: identityTlsCertAuth.accessTokenMaxTTL, + accessTokenTrustedIps: identityTlsCertAuth.accessTokenTrustedIps as TIdentityTrustedIp[], + accessTokenNumUsesLimit: identityTlsCertAuth.accessTokenNumUsesLimit + } + } + }); + + return { identityTlsCertAuth }; + } + }); + + server.route({ + method: "GET", + url: "/identities/:identityId", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.TlsCertAuth], + description: "Retrieve Tls Certificate Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().describe(TLS_CERT_AUTH.RETRIEVE.identityId) + }), + response: { + 200: z.object({ + identityTlsCloudAuth: IdentityTlsCertAuthsSchema.extend({ + caCertificate: z.string() + }) + }) + } + }, + handler: async (req) => { + const identityTlsCertAuth = await server.services.identityTlsCertAuth.getTlsCertAuth({ + identityId: req.params.identityId, + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.GET_IDENTITY_TLS_CERT_AUTH, + metadata: { + identityId: identityTlsCertAuth.identityId + } + } + }); + return { identityTlsCertAuth }; + } + }); + + server.route({ + method: "DELETE", + url: "/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.TlsCertAuth], + description: "Delete Tls Certificate Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().describe(TLS_CERT_AUTH.REVOKE.identityId) + }), + response: { + 200: z.object({ + identityTlsCloudAuth: IdentityTlsCertAuthsSchema + }) + } + }, + handler: async (req) => { + const identityTlsCertAuth = await server.services.identityTlsCertAuth.revokeTlsCertAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityId: req.params.identityId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.REVOKE_IDENTITY_TLS_CERT_AUTH, + metadata: { + identityId: identityTlsCertAuth.identityId + } + } + }); + + return { identityTlsCertAuth }; + } + }); +}; diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 2363147b6..bebdfbe65 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -53,6 +53,7 @@ import { registerUserEngagementRouter } from "./user-engagement-router"; import { registerUserRouter } from "./user-router"; import { registerWebhookRouter } from "./webhook-router"; import { registerWorkflowIntegrationRouter } from "./workflow-integration-router"; +import { registerIdentityTlsCertAuthRouter } from "./identity-tls-cert-auth-router"; export const registerV1Routes = async (server: FastifyZodProvider) => { await server.register(registerSsoRouter, { prefix: "/sso" }); @@ -66,6 +67,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await authRouter.register(registerIdentityAccessTokenRouter); await authRouter.register(registerIdentityAliCloudAuthRouter); await authRouter.register(registerIdentityAwsAuthRouter); + await authRouter.register(registerIdentityTlsCertAuthRouter, { prefix: "/tls-cert-auth" }); await authRouter.register(registerIdentityAzureAuthRouter); await authRouter.register(registerIdentityOciAuthRouter); await authRouter.register(registerIdentityOidcAuthRouter); diff --git a/backend/src/services/identity-access-token/identity-access-token-dal.ts b/backend/src/services/identity-access-token/identity-access-token-dal.ts index 879ca9fd3..a1b70b21c 100644 --- a/backend/src/services/identity-access-token/identity-access-token-dal.ts +++ b/backend/src/services/identity-access-token/identity-access-token-dal.ts @@ -45,6 +45,11 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { .leftJoin(TableName.IdentityOidcAuth, `${TableName.Identity}.id`, `${TableName.IdentityOidcAuth}.identityId`) .leftJoin(TableName.IdentityTokenAuth, `${TableName.Identity}.id`, `${TableName.IdentityTokenAuth}.identityId`) .leftJoin(TableName.IdentityJwtAuth, `${TableName.Identity}.id`, `${TableName.IdentityJwtAuth}.identityId`) + .leftJoin( + TableName.IdentityTlsCertAuth, + `${TableName.Identity}.id`, + `${TableName.IdentityTlsCertAuth}.identityId` + ) .select(selectAllTableCols(TableName.IdentityAccessToken)) .select( db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth).as("accessTokenTrustedIpsUa"), @@ -61,6 +66,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTokenAuth).as("accessTokenTrustedIpsToken"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityJwtAuth).as("accessTokenTrustedIpsJwt"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityLdapAuth).as("accessTokenTrustedIpsLdap"), + db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTlsCertAuth).as("accessTokenTrustedIpsTlsCert"), db.ref("name").withSchema(TableName.Identity) ) .first(); @@ -79,7 +85,8 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { trustedIpsOidcAuth: doc.accessTokenTrustedIpsOidc, trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken, trustedIpsAccessJwtAuth: doc.accessTokenTrustedIpsJwt, - trustedIpsAccessLdapAuth: doc.accessTokenTrustedIpsLdap + trustedIpsAccessLdapAuth: doc.accessTokenTrustedIpsLdap, + trustedIpsAccessTlsCertAuth: doc.accessTokenTrustedIpsTlsCert }; } catch (error) { throw new DatabaseError({ error, name: "IdAccessTokenFindOne" }); 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 7c8944f50..587b7c436 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 @@ -201,7 +201,8 @@ export const identityAccessTokenServiceFactory = ({ [IdentityAuthMethod.OIDC_AUTH]: identityAccessToken.trustedIpsOidcAuth, [IdentityAuthMethod.TOKEN_AUTH]: identityAccessToken.trustedIpsAccessTokenAuth, [IdentityAuthMethod.JWT_AUTH]: identityAccessToken.trustedIpsAccessJwtAuth, - [IdentityAuthMethod.LDAP_AUTH]: identityAccessToken.trustedIpsAccessLdapAuth + [IdentityAuthMethod.LDAP_AUTH]: identityAccessToken.trustedIpsAccessLdapAuth, + [IdentityAuthMethod.TLS_CERT_AUTH]: identityAccessToken.trustedIpsAccessTlsCertAuth }; const trustedIps = trustedIpsMap[identityAccessToken.authMethod as IdentityAuthMethod]; diff --git a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-dal.ts b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-dal.ts new file mode 100644 index 000000000..951077f33 --- /dev/null +++ b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify, TOrmify } from "@app/lib/knex"; + +export type TIdentityTlsCertAuthDALFactory = TOrmify; + +export const identityTlsCertAuthDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.IdentityTlsCertAuth); + return orm; +}; 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 new file mode 100644 index 000000000..8a416156d --- /dev/null +++ b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts @@ -0,0 +1,417 @@ +import crypto from "node:crypto"; + +import { ForbiddenError } from "@casl/ability"; +import jwt from "jsonwebtoken"; + +import { IdentityAuthMethod } from "@app/db/schemas"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { getConfig } from "@app/lib/config/env"; +import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; +import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; + +import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityOrgDALFactory } from "../identity/identity-org-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"; +import { KmsDataKey } from "../kms/kms-types"; +import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; +import { TIdentityTlsCertAuthDALFactory } from "./identity-tls-cert-auth-dal"; +import { TIdentityTlsCertAuthServiceFactory } from "./identity-tls-cert-auth-types"; + +type TIdentityTlsCertAuthServiceFactoryDep = { + identityAccessTokenDAL: Pick; + identityTlsCertAuthDAL: Pick< + TIdentityTlsCertAuthDALFactory, + "findOne" | "transaction" | "create" | "updateById" | "delete" + >; + identityOrgMembershipDAL: Pick; + licenseService: Pick; + permissionService: Pick; + kmsService: Pick; +}; + +const parseSubjectDetails = (data: string) => { + const values: Record = {}; + data.split("\n").forEach((el) => { + const [key, value] = el.split("="); + values[key.trim()] = value.trim(); + }); + return values; +}; + +export const identityTlsCertAuthServiceFactory = ({ + identityAccessTokenDAL, + identityTlsCertAuthDAL, + identityOrgMembershipDAL, + licenseService, + permissionService, + kmsService +}: TIdentityTlsCertAuthServiceFactoryDep): TIdentityTlsCertAuthServiceFactory => { + const login: TIdentityTlsCertAuthServiceFactory["login"] = async ({ identityId, clientCertificate }) => { + const identityTlsCertAuth = await identityTlsCertAuthDAL.findOne({ identityId }); + if (!identityTlsCertAuth) { + throw new NotFoundError({ + message: "TLS Certificate auth method not found for identity, did you configure TLS Certificate auth?" + }); + } + + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ + identityId: identityTlsCertAuth.identityId + }); + + if (!identityMembershipOrg) { + throw new NotFoundError({ + message: `Identity organization membership for identity with ID '${identityTlsCertAuth.identityId}' not found` + }); + } + + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId + }); + + const caCertificate = decryptor({ + cipherTextBlob: identityTlsCertAuth.encryptedCaCertificate + }).toString(); + + const clientCertificateX509 = new crypto.X509Certificate(Buffer.from(clientCertificate)); + const caCertificateX509 = new crypto.X509Certificate(caCertificate); + + const isValidCertificate = clientCertificateX509.verify(caCertificateX509.publicKey); + if (!isValidCertificate) + throw new UnauthorizedError({ + message: "Access denied: Certificate not issued by the provided CA." + }); + + if (new Date(clientCertificateX509.validTo) < new Date()) { + throw new UnauthorizedError({ + message: "Access denied: Certificate has expired." + }); + } + + if (new Date(clientCertificateX509.validFrom) > new Date()) { + throw new UnauthorizedError({ + message: "Access denied: Certificate not yet valid." + }); + } + + const subjectDetails = parseSubjectDetails(clientCertificateX509.subject); + if (identityTlsCertAuth.allowedCommonNames) { + const isValidCommonName = identityTlsCertAuth.allowedCommonNames.split(",").includes(subjectDetails.CN); + if (!isValidCommonName) { + throw new UnauthorizedError({ + message: "Access denied: TLS Certificate Auth common name not allowed." + }); + } + } + + // Generate the token + const identityAccessToken = await identityTlsCertAuthDAL.transaction(async (tx) => { + const newToken = await identityAccessTokenDAL.create( + { + identityId: identityTlsCertAuth.identityId, + isAccessTokenRevoked: false, + accessTokenTTL: identityTlsCertAuth.accessTokenTTL, + accessTokenMaxTTL: identityTlsCertAuth.accessTokenMaxTTL, + accessTokenNumUses: 0, + accessTokenNumUsesLimit: identityTlsCertAuth.accessTokenNumUsesLimit, + authMethod: IdentityAuthMethod.TLS_CERT_AUTH + }, + tx + ); + return newToken; + }); + + const appCfg = getConfig(); + const accessToken = jwt.sign( + { + identityId: identityTlsCertAuth.identityId, + identityAccessTokenId: identityAccessToken.id, + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN + } as TIdentityAccessTokenJwtPayload, + appCfg.AUTH_SECRET, + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } + ); + + return { + identityTlsCertAuth, + accessToken, + identityAccessToken, + identityMembershipOrg + }; + }; + + const attachTlsCertAuth: TIdentityTlsCertAuthServiceFactory["attachTlsCertAuth"] = async ({ + identityId, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + actorId, + actorAuthMethod, + actor, + actorOrgId, + isActorSuperAdmin, + caCertificate, + allowedCommonNames + }) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); + + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + + if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) { + throw new BadRequestError({ + message: "Failed to add TLS Certificate Auth to already configured identity" + }); + } + + if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { + throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + + const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { + if ( + !plan.ipAllowlisting && + accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" && + accessTokenTrustedIp.ipAddress !== "::/0" + ) + throw new BadRequestError({ + message: + "Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range." + }); + if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress)) + throw new BadRequestError({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + return extractIPDetails(accessTokenTrustedIp.ipAddress); + }); + + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId + }); + + const identityTlsCertAuth = await identityTlsCertAuthDAL.transaction(async (tx) => { + const doc = await identityTlsCertAuthDAL.create( + { + identityId: identityMembershipOrg.identityId, + accessTokenMaxTTL, + allowedCommonNames, + accessTokenTTL, + encryptedCaCertificate: encryptor({ plainText: Buffer.from(caCertificate) }).cipherTextBlob, + accessTokenNumUsesLimit, + accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps) + }, + tx + ); + return doc; + }); + return { ...identityTlsCertAuth, orgId: identityMembershipOrg.orgId }; + }; + + const updateTlsCertAuth: TIdentityTlsCertAuthServiceFactory["updateTlsCertAuth"] = async ({ + identityId, + caCertificate, + allowedCommonNames, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + actorId, + actorAuthMethod, + actor, + actorOrgId + }) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + + if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) { + throw new NotFoundError({ + message: "The identity does not have TLS Certificate Auth attached" + }); + } + + const identityTlsCertAuth = await identityTlsCertAuthDAL.findOne({ identityId }); + + if ( + (accessTokenMaxTTL || identityTlsCertAuth.accessTokenMaxTTL) > 0 && + (accessTokenTTL || identityTlsCertAuth.accessTokenTTL) > + (accessTokenMaxTTL || identityTlsCertAuth.accessTokenMaxTTL) + ) { + throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + + const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { + if ( + !plan.ipAllowlisting && + accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" && + accessTokenTrustedIp.ipAddress !== "::/0" + ) + throw new BadRequestError({ + message: + "Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range." + }); + if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress)) + throw new BadRequestError({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + return extractIPDetails(accessTokenTrustedIp.ipAddress); + }); + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId + }); + + const updatedTlsCertAuth = await identityTlsCertAuthDAL.updateById(identityTlsCertAuth.id, { + allowedCommonNames, + encryptedCaCertificate: caCertificate + ? encryptor({ plainText: Buffer.from(caCertificate) }).cipherTextBlob + : undefined, + accessTokenMaxTTL, + accessTokenTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: reformattedAccessTokenTrustedIps + ? JSON.stringify(reformattedAccessTokenTrustedIps) + : undefined + }); + + return { ...updatedTlsCertAuth, orgId: identityMembershipOrg.orgId }; + }; + + const getTlsCertAuth: TIdentityTlsCertAuthServiceFactory["getTlsCertAuth"] = async ({ + identityId, + actorId, + actor, + actorAuthMethod, + actorOrgId + }) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + + if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) { + throw new BadRequestError({ + message: "The identity does not have TLS Certificate Auth attached" + }); + } + + const identityAuth = await identityTlsCertAuthDAL.findOne({ identityId }); + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId + }); + let caCertificate = ""; + if (identityAuth.encryptedCaCertificate) { + caCertificate = decryptor({ cipherTextBlob: identityAuth.encryptedCaCertificate }).toString(); + } + + return { ...identityAuth, caCertificate, orgId: identityMembershipOrg.orgId }; + }; + + const revokeTlsCertAuth: TIdentityTlsCertAuthServiceFactory["revokeTlsCertAuth"] = async ({ + identityId, + actorId, + actor, + actorAuthMethod, + actorOrgId + }) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) { + throw new BadRequestError({ + message: "The identity does not have TLS Certificate auth" + }); + } + const { permission, membership } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission( + ActorType.IDENTITY, + identityMembershipOrg.identityId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to revoke TLS Certificate auth of identity with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + + const revokedIdentityTlsCertAuth = await identityTlsCertAuthDAL.transaction(async (tx) => { + const deletedTlsCertAuth = await identityTlsCertAuthDAL.delete({ identityId }, tx); + await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.TLS_CERT_AUTH }, tx); + + return { ...deletedTlsCertAuth?.[0], orgId: identityMembershipOrg.orgId }; + }); + return revokedIdentityTlsCertAuth; + }; + + return { + login, + attachTlsCertAuth, + updateTlsCertAuth, + getTlsCertAuth, + revokeTlsCertAuth + }; +}; 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 new file mode 100644 index 000000000..729f502a2 --- /dev/null +++ b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts @@ -0,0 +1,49 @@ +import { TIdentityAccessTokens, TIdentityOrgMemberships, TIdentityTlsCertAuths } from "@app/db/schemas"; +import { TProjectPermission } from "@app/lib/types"; + +export type TLoginTlsCertAuthDTO = { + identityId: string; + clientCertificate: string; +}; + +export type TAttachTlsCertAuthDTO = { + identityId: string; + caCertificate: string; + allowedCommonNames?: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: { ipAddress: string }[]; + isActorSuperAdmin?: boolean; +} & Omit; + +export type TUpdateTlsCertAuthDTO = { + identityId: string; + caCertificate?: string; + allowedCommonNames?: string; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: { ipAddress: string }[]; +} & Omit; + +export type TGetTlsCertAuthDTO = { + identityId: string; +} & Omit; + +export type TRevokeTlsCertAuthDTO = { + identityId: string; +} & Omit; + +export type TIdentityTlsCertAuthServiceFactory = { + login: (dto: TLoginTlsCertAuthDTO) => Promise<{ + identityTlsCertAuth: TIdentityTlsCertAuths; + accessToken: string; + identityAccessToken: TIdentityAccessTokens; + identityMembershipOrg: TIdentityOrgMemberships; + }>; + attachTlsCertAuth: (dto: TAttachTlsCertAuthDTO) => Promise; + updateTlsCertAuth: (dto: TUpdateTlsCertAuthDTO) => Promise; + revokeTlsCertAuth: (dto: TRevokeTlsCertAuthDTO) => Promise; + getTlsCertAuth: (dto: TGetTlsCertAuthDTO) => Promise; +}; From 4bd62aa46237d7c8db91b7f72f2a5d582671e1d9 Mon Sep 17 00:00:00 2001 From: = Date: Wed, 25 Jun 2025 14:26:55 +0530 Subject: [PATCH 02/97] feat: updated frontend to have the tls cert auth login --- backend/src/@types/fastify.d.ts | 2 +- backend/src/lib/config/env.ts | 3 + backend/src/server/routes/index.ts | 4 +- .../v1/identity-tls-cert-auth-router.ts | 151 +++++--- backend/src/server/routes/v1/index.ts | 2 +- .../identity-tls-cert-auth-service.ts | 8 +- .../identity-tls-cert-auth-types.ts | 4 +- backend/src/services/identity/identity-fns.ts | 7 +- .../src/services/identity/identity-org-dal.ts | 22 +- .../src/hooks/api/identities/constants.tsx | 3 +- frontend/src/hooks/api/identities/enums.tsx | 3 +- .../src/hooks/api/identities/mutations.tsx | 105 +++++ frontend/src/hooks/api/identities/queries.tsx | 24 ++ frontend/src/hooks/api/identities/types.ts | 41 ++ .../IdentityAuthMethodModalContent.tsx | 11 + .../IdentityTlsCertAuthForm.tsx | 358 ++++++++++++++++++ .../ViewIdentityAuthModal.tsx | 7 + .../ViewIdentityTlsCertAuthContent.tsx | 88 +++++ 18 files changed, 777 insertions(+), 66 deletions(-) create mode 100644 frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTlsCertAuthForm.tsx create mode 100644 frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityTlsCertAuthContent.tsx diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 2d641b4a0..2956be192 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -74,6 +74,7 @@ import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-a import { TIdentityOciAuthServiceFactory } from "@app/services/identity-oci-auth/identity-oci-auth-service"; import { TIdentityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service"; import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service"; +import { TIdentityTlsCertAuthServiceFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-types"; import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service"; import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service"; import { TIntegrationServiceFactory } from "@app/services/integration/integration-service"; @@ -110,7 +111,6 @@ import { TUserServiceFactory } from "@app/services/user/user-service"; import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service"; import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service"; import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service"; -import { TIdentityTlsCertAuthServiceFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-types"; declare module "@fastify/request-context" { interface RequestContextData { diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index b0b35cc2f..dfe9ac29d 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -193,6 +193,9 @@ const envSchema = z PYLON_API_KEY: zpStr(z.string().optional()), DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"), SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"), + IDENTITY_TLS_CERT_AUTH_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default( + "x-identity-tls-cert-auth-client-cert" + ), WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()), WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()), ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true"), diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 3e5735fb9..f687d358d 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -193,6 +193,8 @@ import { identityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { identityProjectMembershipRoleDALFactory } from "@app/services/identity-project/identity-project-membership-role-dal"; import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service"; +import { identityTlsCertAuthDALFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-dal"; +import { identityTlsCertAuthServiceFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-service"; import { identityTokenAuthDALFactory } from "@app/services/identity-token-auth/identity-token-auth-dal"; import { identityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service"; import { identityUaClientSecretDALFactory } from "@app/services/identity-ua/identity-ua-client-secret-dal"; @@ -301,8 +303,6 @@ import { registerSecretScannerGhApp } from "../plugins/secret-scanner"; import { registerV1Routes } from "./v1"; import { registerV2Routes } from "./v2"; import { registerV3Routes } from "./v3"; -import { identityTlsCertAuthDALFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-dal"; -import { identityTlsCertAuthServiceFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-service"; const histogram = monitorEventLoopDelay({ resolution: 20 }); histogram.enable(); 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 38190d7e7..2ae64cfe6 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 @@ -1,14 +1,17 @@ +import crypto from "node:crypto"; + import { z } from "zod"; -// import { TLSSocket } from "tls"; +import { IdentityTlsCertAuthsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags, TLS_CERT_AUTH } from "@app/lib/api-docs"; +import { getConfig } from "@app/lib/config/env"; +import { BadRequestError } from "@app/lib/errors"; +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 { readLimit, writeLimit } from "@app/server/config/rateLimiter"; -import { ApiDocsTags, TLS_CERT_AUTH } from "@app/lib/api-docs"; -import { IdentityTlsCertAuthsSchema } from "@app/db/schemas"; -import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; -import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; +import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; const validateCommonNames = z .string() @@ -21,44 +24,74 @@ const validateCommonNames = z .join(",") ); +const validateCaCertificate = (caCert: string) => { + if (!caCert) return true; + try { + // eslint-disable-next-line no-new + new crypto.X509Certificate(caCert); + return true; + } catch (err) { + return false; + } +}; + export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvider) => { - // server.route({ - // method: "GET", - // url: "/", - // config: { - // rateLimit: readLimit - // }, - // schema: { - // params: z.object({}), - // response: { - // 200: z.object({}) - // } - // }, - // onRequest: verifyAuth([AuthMode.JWT]), - // handler: async (req) => { - // const { socket } = req; - // if (socket instanceof TLSSocket && socket.encrypted) { - // // Inside this block, TypeScript now knows `socket` is a TlsSocket - // const certificate = socket.getPeerCertificate(); - // - // if (Object.keys(certificate).length === 0) { - // return reply.send({ message: "Client did not provide a certificate." }); - // } - // - // return reply.send({ - // message: "Certificate received!", - // subject: certificate.subject, - // issuer: certificate.issuer, - // fingerprint: certificate.fingerprint - // }); - // } else { - // // This will handle plain HTTP requests gracefully - // return reply - // .status(400) - // .send({ error: "This endpoint requires an HTTPS connection with a client certificate." }); - // } - // } - // }); + server.route({ + method: "POST", + url: "/login", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.TlsCertAuth], + description: "Login with TLS Certificate Auth", + body: z.object({ + identityId: z.string().trim().describe(TLS_CERT_AUTH.LOGIN.identityId) + }), + response: { + 200: z.object({ + accessToken: z.string(), + expiresIn: z.coerce.number(), + accessTokenMaxTTL: z.coerce.number(), + tokenType: z.literal("Bearer") + }) + } + }, + handler: async (req) => { + const appCfg = getConfig(); + const clientCertificate = req.headers[appCfg.IDENTITY_TLS_CERT_AUTH_CLIENT_CERTIFICATE_HEADER_KEY]; + if (!clientCertificate) { + throw new BadRequestError({ message: "Missing TLS certificate in header" }); + } + + const { identityTlsCertAuth, accessToken, identityAccessToken, identityMembershipOrg } = + await server.services.identityTlsCertAuth.login({ + identityId: req.body.identityId, + clientCertificate: clientCertificate as string + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityMembershipOrg?.orgId, + event: { + type: EventType.LOGIN_IDENTITY_TLS_CERT_AUTH, + metadata: { + identityId: identityTlsCertAuth.identityId, + identityAccessTokenId: identityAccessToken.id, + identityTlsCertAuthId: identityTlsCertAuth.id + } + } + }); + + return { + accessToken, + tokenType: "Bearer" as const, + expiresIn: identityTlsCertAuth.accessTokenTTL, + accessTokenMaxTTL: identityTlsCertAuth.accessTokenMaxTTL + }; + } + }); server.route({ method: "POST", @@ -81,8 +114,16 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid }), body: z .object({ - allowedCommonNames: validateCommonNames.describe(TLS_CERT_AUTH.ATTACH.allowedCommonNames), - caCertificate: z.string().min(1).describe(TLS_CERT_AUTH.ATTACH.caCertificate), + allowedCommonNames: validateCommonNames + .optional() + .nullable() + .describe(TLS_CERT_AUTH.ATTACH.allowedCommonNames), + caCertificate: z + .string() + .min(1) + .max(10240) + .refine(validateCaCertificate, "Invalid CA Certificate.") + .describe(TLS_CERT_AUTH.ATTACH.caCertificate), accessTokenTrustedIps: z .object({ ipAddress: z.string().trim() @@ -118,7 +159,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid ), response: { 200: z.object({ - identityTlsCloudAuth: IdentityTlsCertAuthsSchema + identityTlsCertAuth: IdentityTlsCertAuthsSchema }) } }, @@ -174,7 +215,17 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid }), body: z .object({ - allowedCommonNames: validateCommonNames.describe(TLS_CERT_AUTH.UPDATE.allowedCommonNames), + caCertificate: z + .string() + .min(1) + .max(10240) + .refine(validateCaCertificate, "Invalid CA Certificate.") + .optional() + .describe(TLS_CERT_AUTH.ATTACH.caCertificate), + allowedCommonNames: validateCommonNames + .optional() + .nullable() + .describe(TLS_CERT_AUTH.UPDATE.allowedCommonNames), accessTokenTrustedIps: z .object({ ipAddress: z.string().trim() @@ -210,7 +261,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid ), response: { 200: z.object({ - identityTlsCloudAuth: IdentityTlsCertAuthsSchema + identityTlsCertAuth: IdentityTlsCertAuthsSchema }) } }, @@ -265,7 +316,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid }), response: { 200: z.object({ - identityTlsCloudAuth: IdentityTlsCertAuthsSchema.extend({ + identityTlsCertAuth: IdentityTlsCertAuthsSchema.extend({ caCertificate: z.string() }) }) @@ -315,7 +366,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid }), response: { 200: z.object({ - identityTlsCloudAuth: IdentityTlsCertAuthsSchema + identityTlsCertAuth: IdentityTlsCertAuthsSchema }) } }, diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index bebdfbe65..e6aa2e83f 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -25,6 +25,7 @@ import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router"; import { registerIdentityOciAuthRouter } from "./identity-oci-auth-router"; import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router"; import { registerIdentityRouter } from "./identity-router"; +import { registerIdentityTlsCertAuthRouter } from "./identity-tls-cert-auth-router"; import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router"; import { registerIdentityUaRouter } from "./identity-universal-auth-router"; import { registerIntegrationAuthRouter } from "./integration-auth-router"; @@ -53,7 +54,6 @@ import { registerUserEngagementRouter } from "./user-engagement-router"; import { registerUserRouter } from "./user-router"; import { registerWebhookRouter } from "./webhook-router"; import { registerWorkflowIntegrationRouter } from "./workflow-integration-router"; -import { registerIdentityTlsCertAuthRouter } from "./identity-tls-cert-auth-router"; export const registerV1Routes = async (server: FastifyZodProvider) => { await server.register(registerSsoRouter, { prefix: "/sso" }); 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 8a416156d..11dd312ad 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,6 +11,7 @@ import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; @@ -81,7 +82,12 @@ export const identityTlsCertAuthServiceFactory = ({ cipherTextBlob: identityTlsCertAuth.encryptedCaCertificate }).toString(); - const clientCertificateX509 = new crypto.X509Certificate(Buffer.from(clientCertificate)); + const leafCertificate = extractX509CertFromChain(decodeURIComponent(clientCertificate))?.[0]; + if (!leafCertificate) { + throw new BadRequestError({ message: "Missing client certificate" }); + } + + const clientCertificateX509 = new crypto.X509Certificate(leafCertificate); const caCertificateX509 = new crypto.X509Certificate(caCertificate); const isValidCertificate = clientCertificateX509.verify(caCertificateX509.publicKey); 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 729f502a2..b7a08276b 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 @@ -9,7 +9,7 @@ export type TLoginTlsCertAuthDTO = { export type TAttachTlsCertAuthDTO = { identityId: string; caCertificate: string; - allowedCommonNames?: string; + allowedCommonNames?: string | null; accessTokenTTL: number; accessTokenMaxTTL: number; accessTokenNumUsesLimit: number; @@ -20,7 +20,7 @@ export type TAttachTlsCertAuthDTO = { export type TUpdateTlsCertAuthDTO = { identityId: string; caCertificate?: string; - allowedCommonNames?: string; + allowedCommonNames?: string | null; accessTokenTTL?: number; accessTokenMaxTTL?: number; accessTokenNumUsesLimit?: number; diff --git a/backend/src/services/identity/identity-fns.ts b/backend/src/services/identity/identity-fns.ts index 3020d9c47..dee87fc49 100644 --- a/backend/src/services/identity/identity-fns.ts +++ b/backend/src/services/identity/identity-fns.ts @@ -11,7 +11,8 @@ export const buildAuthMethods = ({ azureId, tokenId, jwtId, - ldapId + ldapId, + tlsCertId }: { uaId?: string; gcpId?: string; @@ -24,6 +25,7 @@ export const buildAuthMethods = ({ tokenId?: string; jwtId?: string; ldapId?: string; + tlsCertId?: string; }) => { return [ ...[uaId ? IdentityAuthMethod.UNIVERSAL_AUTH : null], @@ -36,6 +38,7 @@ export const buildAuthMethods = ({ ...[azureId ? IdentityAuthMethod.AZURE_AUTH : null], ...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null], ...[jwtId ? IdentityAuthMethod.JWT_AUTH : null], - ...[ldapId ? IdentityAuthMethod.LDAP_AUTH : null] + ...[ldapId ? IdentityAuthMethod.LDAP_AUTH : null], + ...[tlsCertId ? IdentityAuthMethod.TLS_CERT_AUTH : null] ].filter((authMethod) => authMethod) as IdentityAuthMethod[]; }; diff --git a/backend/src/services/identity/identity-org-dal.ts b/backend/src/services/identity/identity-org-dal.ts index 28064c9bb..5ca6cbbc1 100644 --- a/backend/src/services/identity/identity-org-dal.ts +++ b/backend/src/services/identity/identity-org-dal.ts @@ -12,6 +12,7 @@ import { TIdentityOciAuths, TIdentityOidcAuths, TIdentityOrgMemberships, + TIdentityTlsCertAuths, TIdentityTokenAuths, TIdentityUniversalAuths, TOrgRoles @@ -99,7 +100,11 @@ export const identityOrgDALFactory = (db: TDbClient) => { `${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityLdapAuth}.identityId` ) - + .leftJoin( + TableName.IdentityTlsCertAuth, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityTlsCertAuth}.identityId` + ) .select( selectAllTableCols(TableName.IdentityOrgMembership), @@ -114,6 +119,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth), db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth), db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth), + db.ref("id").as("tlsCertId").withSchema(TableName.IdentityTlsCertAuth), db.ref("name").withSchema(TableName.Identity), db.ref("hasDeleteProtection").withSchema(TableName.Identity) ); @@ -238,7 +244,11 @@ export const identityOrgDALFactory = (db: TDbClient) => { "paginatedIdentity.identityId", `${TableName.IdentityLdapAuth}.identityId` ) - + .leftJoin( + TableName.IdentityTlsCertAuth, + "paginatedIdentity.identityId", + `${TableName.IdentityTlsCertAuth}.identityId` + ) .select( db.ref("id").withSchema("paginatedIdentity"), db.ref("role").withSchema("paginatedIdentity"), @@ -260,7 +270,8 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth), db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth), db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth), - db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth) + db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth), + db.ref("id").as("tlsCertId").withSchema(TableName.IdentityTlsCertAuth) ) // cr stands for custom role .select(db.ref("id").as("crId").withSchema(TableName.OrgRoles)) @@ -306,6 +317,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { azureId, tokenId, ldapId, + tlsCertId, createdAt, updatedAt }) => ({ @@ -313,7 +325,6 @@ export const identityOrgDALFactory = (db: TDbClient) => { roleId, identityId, id, - orgId, createdAt, updatedAt, @@ -341,7 +352,8 @@ export const identityOrgDALFactory = (db: TDbClient) => { azureId, tokenId, jwtId, - ldapId + ldapId, + tlsCertId }) } }), diff --git a/frontend/src/hooks/api/identities/constants.tsx b/frontend/src/hooks/api/identities/constants.tsx index cc8d0cef6..b441f5015 100644 --- a/frontend/src/hooks/api/identities/constants.tsx +++ b/frontend/src/hooks/api/identities/constants.tsx @@ -11,5 +11,6 @@ export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = { [IdentityAuthMethod.OCI_AUTH]: "OCI Auth", [IdentityAuthMethod.OIDC_AUTH]: "OIDC Auth", [IdentityAuthMethod.LDAP_AUTH]: "LDAP Auth", - [IdentityAuthMethod.JWT_AUTH]: "JWT Auth" + [IdentityAuthMethod.JWT_AUTH]: "JWT Auth", + [IdentityAuthMethod.TLS_CERT_AUTH]: "TLS Certificate Auth" }; diff --git a/frontend/src/hooks/api/identities/enums.tsx b/frontend/src/hooks/api/identities/enums.tsx index de329d393..c850005cf 100644 --- a/frontend/src/hooks/api/identities/enums.tsx +++ b/frontend/src/hooks/api/identities/enums.tsx @@ -9,7 +9,8 @@ export enum IdentityAuthMethod { OCI_AUTH = "oci-auth", OIDC_AUTH = "oidc-auth", LDAP_AUTH = "ldap-auth", - JWT_AUTH = "jwt-auth" + JWT_AUTH = "jwt-auth", + TLS_CERT_AUTH = "tls-cert-auth" } export enum IdentityJwtConfigurationType { diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx index 7abf1a015..613ca85b8 100644 --- a/frontend/src/hooks/api/identities/mutations.tsx +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -14,6 +14,7 @@ import { AddIdentityLdapAuthDTO, AddIdentityOciAuthDTO, AddIdentityOidcAuthDTO, + AddIdentityTlsCertAuthDTO, AddIdentityTokenAuthDTO, AddIdentityUniversalAuthDTO, ClientSecretData, @@ -32,6 +33,7 @@ import { DeleteIdentityLdapAuthDTO, DeleteIdentityOciAuthDTO, DeleteIdentityOidcAuthDTO, + DeleteIdentityTlsCertAuthDTO, DeleteIdentityTokenAuthDTO, DeleteIdentityUniversalAuthClientSecretDTO, DeleteIdentityUniversalAuthDTO, @@ -46,6 +48,7 @@ import { IdentityLdapAuth, IdentityOciAuth, IdentityOidcAuth, + IdentityTlsCertAuth, IdentityTokenAuth, IdentityUniversalAuth, RevokeTokenDTO, @@ -60,6 +63,7 @@ import { UpdateIdentityLdapAuthDTO, UpdateIdentityOciAuthDTO, UpdateIdentityOidcAuthDTO, + UpdateIdentityTlsCertAuthDTO, UpdateIdentityTokenAuthDTO, UpdateIdentityUniversalAuthDTO, UpdateTokenIdentityTokenAuthDTO @@ -655,6 +659,107 @@ export const useDeleteIdentityAliCloudAuth = () => { }); }; +export const useAddIdentityTlsCertAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + allowedCommonNames, + caCertificate, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }) => { + const { + data: { identityTlsCertAuth } + } = await apiRequest.post<{ identityTlsCertAuth: IdentityTlsCertAuth }>( + `/api/v1/auth/tls-cert-auth/identities/${identityId}`, + { + allowedCommonNames, + caCertificate, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + } + ); + + return identityTlsCertAuth; + }, + onSuccess: (_, { identityId, organizationId }) => { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); + queryClient.invalidateQueries({ + queryKey: identitiesKeys.getIdentityAliCloudAuth(identityId) + }); + } + }); +}; + +export const useUpdateIdentityTlsCertAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + allowedCommonNames, + caCertificate, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }) => { + const { + data: { identityTlsCertAuth } + } = await apiRequest.patch<{ identityTlsCertAuth: IdentityTlsCertAuth }>( + `/api/v1/auth/tls-cert-auth/identities/${identityId}`, + { + caCertificate, + allowedCommonNames, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + } + ); + + return identityTlsCertAuth; + }, + onSuccess: (_, { identityId, organizationId }) => { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); + queryClient.invalidateQueries({ + queryKey: identitiesKeys.getIdentityAliCloudAuth(identityId) + }); + } + }); +}; + +export const useDeleteIdentityTlsCertAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ identityId }) => { + const { + data: { identityTlsCertAuth } + } = await apiRequest.delete(`/api/v1/auth/tls-cert-auth/identities/${identityId}`); + return identityTlsCertAuth; + }, + onSuccess: (_, { organizationId, identityId }) => { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); + queryClient.invalidateQueries({ + queryKey: identitiesKeys.getIdentityAliCloudAuth(identityId) + }); + } + }); +}; + export const useUpdateIdentityOidcAuth = () => { const queryClient = useQueryClient(); return useMutation({ diff --git a/frontend/src/hooks/api/identities/queries.tsx b/frontend/src/hooks/api/identities/queries.tsx index 806c1f0a4..b59526da1 100644 --- a/frontend/src/hooks/api/identities/queries.tsx +++ b/frontend/src/hooks/api/identities/queries.tsx @@ -17,6 +17,7 @@ import { IdentityMembershipOrg, IdentityOciAuth, IdentityOidcAuth, + IdentityTlsCertAuth, IdentityTokenAuth, IdentityUniversalAuth, TSearchIdentitiesDTO @@ -34,6 +35,8 @@ export const identitiesKeys = { getIdentityGcpAuth: (identityId: string) => [{ identityId }, "identity-gcp-auth"] as const, getIdentityOidcAuth: (identityId: string) => [{ identityId }, "identity-oidc-auth"] as const, getIdentityAwsAuth: (identityId: string) => [{ identityId }, "identity-aws-auth"] as const, + getIdentityTlsCertAuth: (identityId: string) => + [{ identityId }, "identity-tls-cert-auth"] as const, getIdentityAliCloudAuth: (identityId: string) => [{ identityId }, "identity-alicloud-auth"] as const, getIdentityOciAuth: (identityId: string) => [{ identityId }, "identity-oci-auth"] as const, @@ -175,6 +178,27 @@ export const useGetIdentityAwsAuth = ( }); }; +export const useGetIdentityTlsCertAuth = ( + identityId: string, + options?: TReactQueryOptions["options"] +) => { + return useQuery({ + queryKey: identitiesKeys.getIdentityTlsCertAuth(identityId), + queryFn: async () => { + const { + data: { identityTlsCertAuth } + } = await apiRequest.get<{ identityTlsCertAuth: IdentityTlsCertAuth }>( + `/api/v1/auth/tls-cert-auth/identities/${identityId}` + ); + return identityTlsCertAuth; + }, + staleTime: 0, + gcTime: 0, + ...options, + enabled: Boolean(identityId) && (options?.enabled ?? true) + }); +}; + export const useGetIdentityOciAuth = ( identityId: string, options?: TReactQueryOptions["options"] diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index 873338f09..1af1cc24c 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -485,6 +485,47 @@ export type DeleteIdentityKubernetesAuthDTO = { identityId: string; }; +export type IdentityTlsCertAuth = { + identityId: string; + caCertificate: string; + allowedCommonNames: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: IdentityTrustedIp[]; +}; + +export type AddIdentityTlsCertAuthDTO = { + organizationId: string; + identityId: string; + caCertificate: string; + allowedCommonNames?: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: { + ipAddress: string; + }[]; +}; + +export type UpdateIdentityTlsCertAuthDTO = { + organizationId: string; + identityId: string; + caCertificate: string; + allowedCommonNames?: string | null; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: { + ipAddress: string; + }[]; +}; + +export type DeleteIdentityTlsCertAuthDTO = { + organizationId: string; + identityId: string; +}; + export type CreateIdentityUniversalAuthClientSecretDTO = { identityId: string; description?: string; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx index 0d44bfe64..57663f441 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx @@ -17,6 +17,7 @@ import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm"; import { IdentityLdapAuthForm } from "./IdentityLdapAuthForm"; import { IdentityOciAuthForm } from "./IdentityOciAuthForm"; import { IdentityOidcAuthForm } from "./IdentityOidcAuthForm"; +import { IdentityTlsCertAuthForm } from "./IdentityTlsCertAuthForm"; import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm"; import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm"; @@ -52,6 +53,7 @@ const identityAuthMethods = [ { label: "OCI Auth", value: IdentityAuthMethod.OCI_AUTH }, { label: "OIDC Auth", value: IdentityAuthMethod.OIDC_AUTH }, { label: "LDAP Auth", value: IdentityAuthMethod.LDAP_AUTH }, + { label: "TLS Certificate Auth", value: IdentityAuthMethod.TLS_CERT_AUTH }, { label: "JWT Auth", value: IdentityAuthMethod.JWT_AUTH @@ -123,6 +125,15 @@ export const IdentityAuthMethodModalContent = ({ /> ) }, + [IdentityAuthMethod.TLS_CERT_AUTH]: { + render: () => ( + + ) + }, [IdentityAuthMethod.OIDC_AUTH]: { render: () => ( diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTlsCertAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTlsCertAuthForm.tsx new file mode 100644 index 000000000..e018bc770 --- /dev/null +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTlsCertAuthForm.tsx @@ -0,0 +1,358 @@ +import { useEffect, useState } from "react"; +import { Controller, useFieldArray, useForm } from "react-hook-form"; +import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + FormControl, + IconButton, + Input, + Tab, + TabList, + TabPanel, + Tabs, + TextArea +} from "@app/components/v2"; +import { useOrganization, useSubscription } from "@app/context"; +import { + useAddIdentityTlsCertAuth, + useGetIdentityTlsCertAuth, + useUpdateIdentityTlsCertAuth +} from "@app/hooks/api"; +import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +import { IdentityFormTab } from "./types"; + +const schema = z.object({ + allowedCommonNames: z.string().optional(), + caCertificate: z.string().min(1), + accessTokenTTL: z.string().refine((val) => Number(val) <= 315360000, { + message: "Access Token TTL cannot be greater than 315360000" + }), + accessTokenMaxTTL: z.string().refine((val) => Number(val) <= 315360000, { + message: "Access Token Max TTL cannot be greater than 315360000" + }), + accessTokenNumUsesLimit: z.string(), + accessTokenTrustedIps: z + .array( + z.object({ + ipAddress: z.string().max(50) + }) + ) + .min(1) +}); + +export type FormData = z.infer; + +type Props = { + handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["identityAuthMethod"]>, + state?: boolean + ) => void; + identityId?: string; + isUpdate?: boolean; +}; + +export const IdentityTlsCertAuthForm = ({ + handlePopUpOpen, + handlePopUpToggle, + identityId, + isUpdate +}: Props) => { + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + const { subscription } = useSubscription(); + + const { mutateAsync: addMutateAsync } = useAddIdentityTlsCertAuth(); + const { mutateAsync: updateMutateAsync } = useUpdateIdentityTlsCertAuth(); + const [tabValue, setTabValue] = useState(IdentityFormTab.Configuration); + + const { data } = useGetIdentityTlsCertAuth(identityId ?? "", { + enabled: isUpdate + }); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + caCertificate: "", + accessTokenTTL: "2592000", + accessTokenMaxTTL: "2592000", + accessTokenNumUsesLimit: "0", + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + } + }); + + const { + fields: accessTokenTrustedIpsFields, + append: appendAccessTokenTrustedIp, + remove: removeAccessTokenTrustedIp + } = useFieldArray({ control, name: "accessTokenTrustedIps" }); + + useEffect(() => { + if (data) { + reset({ + caCertificate: data.caCertificate, + allowedCommonNames: data.allowedCommonNames || undefined, + accessTokenTTL: String(data.accessTokenTTL), + accessTokenMaxTTL: String(data.accessTokenMaxTTL), + accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit), + accessTokenTrustedIps: data.accessTokenTrustedIps.map( + ({ ipAddress, prefix }: IdentityTrustedIp) => { + return { + ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}` + }; + } + ) + }); + } else { + reset({ + caCertificate: "", + accessTokenTTL: "2592000", + accessTokenMaxTTL: "2592000", + accessTokenNumUsesLimit: "0", + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + }); + } + }, [data]); + + const onFormSubmit = async ({ + caCertificate, + allowedCommonNames, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }: FormData) => { + try { + if (!identityId) return; + + if (data) { + await updateMutateAsync({ + organizationId: orgId, + caCertificate, + allowedCommonNames: allowedCommonNames || null, + identityId, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps + }); + } else { + await addMutateAsync({ + organizationId: orgId, + identityId, + caCertificate, + allowedCommonNames: allowedCommonNames || undefined, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps + }); + } + + handlePopUpToggle("identityAuthMethod", false); + + createNotification({ + text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`, + type: "success" + }); + + reset(); + } catch { + createNotification({ + text: `Failed to ${isUpdate ? "update" : "configure"} identity`, + type: "error" + }); + } + }; + + return ( +
+ setTabValue(value as IdentityFormTab)}> + + Configuration + Advanced + + + ( + + - + + {reviewer?.comment && reviewer.comment} + )} ); @@ -505,7 +516,7 @@ export const SecretApprovalRequestChanges = ({ /> -
+
Reviewers
{secretApprovalRequestDetails?.policy?.approvers From 447e28511c30b0406348efbe0b9a487211d7a8e4 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 30 Jun 2025 16:44:29 -0700 Subject: [PATCH 47/97] improvement: update stale/conflict text --- .../components/SecretApprovalRequestChangeItem.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx index 3701a4f86..da3b07909 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx @@ -8,7 +8,7 @@ import { faExclamationTriangle, faEye, faEyeSlash, - faInfo, + faInfoCircle, faKey } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -69,15 +69,15 @@ export const SecretApprovalRequestChangeItem = ({
{generateItemTitle(op)}
{!hasMerged && isStale && ( -
- - Secret has been changed(stale) +
+ + Secret has been changed (stale)
)} {hasMerged && hasConflict && ( -
+
- +
{generateConflictText(op)}
From 20366a8c077654cebc29ec70ee2051ff3c527c65 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 30 Jun 2025 18:09:50 -0700 Subject: [PATCH 48/97] improvement: address feedback --- .../SecretApprovalRequestChangeItem.tsx | 48 ++++++++++++++----- .../SecretApprovalRequestChanges.tsx | 12 ++--- 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx index da3b07909..430ec8492 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx @@ -96,7 +96,7 @@ export const SecretApprovalRequestChangeItem = ({
Key
-
{secretVersion?.secretKey}
+

{secretVersion?.secretKey}

Value
@@ -148,7 +148,7 @@ export const SecretApprovalRequestChangeItem = ({
Comment
-
+
{secretVersion?.secretComment || ( - )}{" "} @@ -187,15 +187,27 @@ export const SecretApprovalRequestChangeItem = ({ className="mr-0 flex items-center rounded-r-none border border-mineshaft-500" > -
{el.key}
+ +
+ {el.key} +
+
-
- {el.value} -
+ +
+ {el.value} +
+
))} @@ -222,7 +234,7 @@ export const SecretApprovalRequestChangeItem = ({
Key
-
{newVersion?.secretKey}
+
{newVersion?.secretKey}
Value
@@ -274,7 +286,7 @@ export const SecretApprovalRequestChangeItem = ({
Comment
-
+
{newVersion?.secretComment || ( - )}{" "} @@ -312,15 +324,27 @@ export const SecretApprovalRequestChangeItem = ({ className="mr-0 flex items-center rounded-r-none border border-mineshaft-500" > -
{el.key}
+ +
+ {el.key} +
+
-
- {el.value} -
+ +
+ {el.value} +
+
))} diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx index e1bcabe78..697ff05ea 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx @@ -225,7 +225,7 @@ export const SecretApprovalRequestChanges = ({ return (
-
+
@@ -493,7 +493,7 @@ export const SecretApprovalRequestChanges = ({ .
{reviewer?.comment && ( - + {reviewer?.comment && reviewer.comment} )} @@ -516,7 +516,7 @@ export const SecretApprovalRequestChanges = ({ />
-
+
Reviewers
{secretApprovalRequestDetails?.policy?.approvers @@ -537,17 +537,17 @@ export const SecretApprovalRequestChanges = ({ requiredApprover.lastName || "" }`} > - {requiredApprover?.email} + {requiredApprover?.email} *
{reviewer?.comment && ( - + )} From b5801af9a8a87c03636133f98c2f09f453eb3765 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 30 Jun 2025 18:32:36 -0700 Subject: [PATCH 49/97] improvements: address feedback --- .../components/SecretApprovalRequestAction.tsx | 4 ++-- .../components/SecretApprovalRequestChanges.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx index 4853f7cae..58b707b04 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx @@ -103,7 +103,7 @@ export const SecretApprovalRequestAction = ({ if (!hasMerged && status === "open") { return (
-
+
-
+
{canApprove || isSoftEnforcement ? (
+ + ); + })} +
+ } onClick={logOutUser}> + Log Out + + + +
+
+ + setOpenSupport(true)} + onMouseLeave={() => setOpenSupport(false)} + > +
+ +
+
+ setOpenSupport(true)} + onMouseLeave={() => setOpenSupport(false)} + > + {INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => { + if (url === "server-admins" && isInfisicalCloud()) { + return null; + } + return ( + + {url === "server-admins" ? ( + + ) : ( + +
+ {icon} +
{text}
+
+
+ )} +
+ ); + })} + {envConfig.PLATFORM_VERSION && ( +
+ + Version: {envConfig.PLATFORM_VERSION} +
+ )} +
+
+ + setOpenUser(true)} + onMouseLeave={() => setOpenUser(false)} + > +
+ +
+
+ setOpenUser(true)} + onMouseLeave={() => setOpenUser(false)} + > +
+
+
+ +
+
+
+ {user?.firstName} {user?.lastName} +
+
{user.email}
+
+
+
+ + Personal Settings + + + + Documentation + + + + + + Join Slack Community + + + +
+ }> + Log Out + + + +
+ ); +}; diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/index.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/index.tsx new file mode 100644 index 000000000..d97ce393e --- /dev/null +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/index.tsx @@ -0,0 +1 @@ +export { Navbar } from "./Navbar"; diff --git a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx b/frontend/src/layouts/OrganizationLayout/components/OrgSidebar/Copy.tsx similarity index 99% rename from frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx rename to frontend/src/layouts/OrganizationLayout/components/OrgSidebar/Copy.tsx index f4ce9bc40..4ddcce662 100644 --- a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/OrgSidebar/Copy.tsx @@ -93,7 +93,7 @@ export const INFISICAL_SUPPORT_OPTIONS = [ ] ]; -export const MinimizedOrgSidebar = () => { +export const OrgSidebar = () => { const [shouldShowMfa, toggleShowMfa] = useToggle(false); const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL); const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {}); diff --git a/frontend/src/layouts/OrganizationLayout/components/OrgSidebar/OrgSidebar.tsx b/frontend/src/layouts/OrganizationLayout/components/OrgSidebar/OrgSidebar.tsx new file mode 100644 index 000000000..413068061 --- /dev/null +++ b/frontend/src/layouts/OrganizationLayout/components/OrgSidebar/OrgSidebar.tsx @@ -0,0 +1,341 @@ +import { useState } from "react"; +import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons"; +import { + faArrowUpRightFromSquare, + faBook, + faCheck, + faCheckCircle, + faCog, + faDoorClosed, + faEnvelope, + faInfinity, + faInfo, + faInfoCircle, + faMoneyBill, + faPlug, + faSignOut, + faUser, + faUserCog, + faUsers +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useQueryClient } from "@tanstack/react-query"; +import { Link, linkOptions, useLocation, useNavigate, useRouter } from "@tanstack/react-router"; + +import { Mfa } from "@app/components/auth/Mfa"; +import { CreateOrgModal } from "@app/components/organization/CreateOrgModal"; +import SecurityClient from "@app/components/utilities/SecurityClient"; +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, + Menu, + MenuGroup, + MenuItem, + Modal, + ModalContent, + Tooltip +} from "@app/components/v2"; +import { envConfig } from "@app/config/env"; +import { useOrganization, useSubscription, useUser } from "@app/context"; +import { isInfisicalCloud } from "@app/helpers/platform"; +import { usePopUp, useToggle } from "@app/hooks"; +import { + useGetOrganizations, + useGetOrgTrialUrl, + useLogoutUser, + useSelectOrganization, + workspaceKeys +} from "@app/hooks/api"; +import { authKeys } from "@app/hooks/api/auth/queries"; +import { MfaMethod } from "@app/hooks/api/auth/types"; +import { SubscriptionPlan } from "@app/hooks/api/types"; +import { AuthMethod } from "@app/hooks/api/users/types"; +import { ProjectType } from "@app/hooks/api/workspace/types"; +import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils"; + +import { MenuIconButton } from "../MenuIconButton"; +import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel"; + +const getPlan = (subscription: SubscriptionPlan) => { + if (subscription.groups) return "Enterprise Plan"; + if (subscription.pitRecovery) return "Pro Plan"; + return "Free Plan"; +}; + +export const INFISICAL_SUPPORT_OPTIONS = [ + [ + , + "Support Forum", + "https://infisical.com/slack" + ], + [ + , + "Read Docs", + "https://infisical.com/docs/documentation/getting-started/introduction" + ], + [ + , + "GitHub Issues", + "https://github.com/Infisical/infisical/issues" + ], + [ + , + "Email Support", + "mailto:support@infisical.com" + ], + [ + , + "Instance Admins", + "server-admins" + ] +]; + +export const OrgSidebar = () => { + const [shouldShowMfa, toggleShowMfa] = useToggle(false); + const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL); + const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {}); + const { subscription } = useSubscription(); + const [open, setOpen] = useState(false); + const [openSupport, setOpenSupport] = useState(false); + const [openUser, setOpenUser] = useState(false); + const [openOrg, setOpenOrg] = useState(false); + const [showAdminsModal, setShowAdminsModal] = useState(false); + + const { user } = useUser(); + const { mutateAsync } = useGetOrgTrialUrl(); + + const { currentOrg } = useOrganization(); + const { data: orgs } = useGetOrganizations(); + + const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const); + const { mutateAsync: selectOrganization } = useSelectOrganization(); + const navigate = useNavigate(); + const router = useRouter(); + const location = useLocation(); + const queryClient = useQueryClient(); + + const isMoreSelected = ( + [ + linkOptions({ to: "/organization/access-management" }).to, + linkOptions({ to: "/organization/app-connections" }).to, + linkOptions({ to: "/organization/billing" }).to, + linkOptions({ to: "/organization/sso" }).to, + linkOptions({ to: "/organization/gateways" }).to, + linkOptions({ to: "/organization/settings" }).to, + linkOptions({ to: "/organization/audit-logs" }).to + ] as string[] + ).includes(location.pathname); + + const handleOrgChange = async (orgId: string) => { + queryClient.removeQueries({ queryKey: authKeys.getAuthToken }); + queryClient.removeQueries({ queryKey: workspaceKeys.getAllUserWorkspace() }); + + const { token, isMfaEnabled, mfaMethod } = await selectOrganization({ + organizationId: orgId + }); + + if (isMfaEnabled) { + SecurityClient.setMfaToken(token); + if (mfaMethod) { + setRequiredMfaMethod(mfaMethod); + } + toggleShowMfa.on(); + setMfaSuccessCallback(() => () => handleOrgChange(orgId)); + return; + } + await router.invalidate(); + await navigateUserToOrg(navigate, orgId); + }; + + const logout = useLogoutUser(); + const logOutUser = async () => { + try { + console.log("Logging out..."); + await logout.mutateAsync(); + navigate({ to: "/login" }); + } catch (error) { + console.error(error); + } + }; + + if (shouldShowMfa) { + return ( +
+ toggleShowMfa.off()} + /> +
+ ); + } + + return ( + <> + + + +
+ +
+
+
+ handlePopUpToggle("createOrg", false)} + /> + + ); +}; diff --git a/frontend/src/layouts/OrganizationLayout/components/OrgSidebar/index.tsx b/frontend/src/layouts/OrganizationLayout/components/OrgSidebar/index.tsx new file mode 100644 index 000000000..315d7ffab --- /dev/null +++ b/frontend/src/layouts/OrganizationLayout/components/OrgSidebar/index.tsx @@ -0,0 +1 @@ +export { OrgSidebar } from "./OrgSidebar"; From 0d97fc27c7fdbe865e06c7c50abf9c165bb761e2 Mon Sep 17 00:00:00 2001 From: = Date: Thu, 26 Jun 2025 12:50:13 +0530 Subject: [PATCH 57/97] feat: moved org breadcrumbs to top level --- .../components/v2/Breadcrumb/Breadcrumb.tsx | 22 +- frontend/src/components/v2/Menu/Menu.tsx | 2 +- frontend/src/layouts/AdminLayout/Sidebar.tsx | 2 +- .../OrganizationLayout/OrganizationLayout.tsx | 23 +- .../components/NavBar/Navbar.tsx | 34 +-- .../components/OrgSidebar/OrgSidebar.tsx | 209 +++--------------- .../AccessManagementPage/route.tsx | 9 +- .../pages/organization/AdminPage/route.tsx | 9 +- .../AppConnectionsPage/route.tsx | 9 +- .../organization/AuditLogsPage/route.tsx | 9 +- .../CertManagerOverviewPage/route.tsx | 6 - .../CertManagerSettingsPage/route.tsx | 6 - .../Gateways/GatewayListPage/route.tsx | 9 +- .../GroupDetailsByIDPage/route.tsx | 7 - .../IdentityDetailsByIDPage/route.tsx | 7 - .../organization/KmsOverviewPage/route.tsx | 6 - .../organization/KmsSettingsPage/route.tsx | 6 - .../pages/organization/RoleByIDPage/route.tsx | 7 - .../SecretManagerOverviewPage/route.tsx | 6 - .../SecretManagerSettingsPage/route.tsx | 6 - .../SecretScanningOverviewPage/route.tsx | 6 - .../SecretScanningSettingsPage/route.tsx | 6 - .../organization/SecretSharingPage/route.tsx | 9 +- .../SecretSharingSettingsPage/route.tsx | 7 - .../pages/organization/SettingsPage/route.tsx | 9 +- .../organization/SshOverviewPage/route.tsx | 6 - .../organization/SshSettingsPage/route.tsx | 6 - .../src/pages/organization/SsoPage/route.tsx | 9 +- .../UserDetailsByIDPage/route.tsx | 7 - .../project/GroupDetailsByIDPage/route.tsx | 7 - 30 files changed, 81 insertions(+), 385 deletions(-) diff --git a/frontend/src/components/v2/Breadcrumb/Breadcrumb.tsx b/frontend/src/components/v2/Breadcrumb/Breadcrumb.tsx index 0afd1bb1f..a956991fb 100644 --- a/frontend/src/components/v2/Breadcrumb/Breadcrumb.tsx +++ b/frontend/src/components/v2/Breadcrumb/Breadcrumb.tsx @@ -1,6 +1,11 @@ /* eslint-disable react/prop-types */ import React from "react"; -import { faCaretDown, faChevronRight, faEllipsis } from "@fortawesome/free-solid-svg-icons"; +import { + faCaretDown, + faChevronRight, + faEllipsis, + faSlash +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Link, ReactNode } from "@tanstack/react-router"; import { LinkComponentProps } from "node_modules/@tanstack/react-router/dist/esm/link"; @@ -78,13 +83,14 @@ const BreadcrumbPage = React.forwardRef) => ( - ); BreadcrumbSeparator.displayName = "BreadcrumbSeparator"; diff --git a/frontend/src/components/v2/Menu/Menu.tsx b/frontend/src/components/v2/Menu/Menu.tsx index 3a9c926a2..0a03beb42 100644 --- a/frontend/src/components/v2/Menu/Menu.tsx +++ b/frontend/src/components/v2/Menu/Menu.tsx @@ -86,7 +86,7 @@ export type MenuGroupProps = { export const MenuGroup = ({ children, title }: MenuGroupProps): JSX.Element => ( <> -
  • {title}
  • +
  • {title}
  • {children} ); diff --git a/frontend/src/layouts/AdminLayout/Sidebar.tsx b/frontend/src/layouts/AdminLayout/Sidebar.tsx index db6384e68..09303731b 100644 --- a/frontend/src/layouts/AdminLayout/Sidebar.tsx +++ b/frontend/src/layouts/AdminLayout/Sidebar.tsx @@ -14,7 +14,7 @@ import { import { envConfig } from "@app/config/env"; import { ProjectType } from "@app/hooks/api/workspace/types"; -import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar"; +import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/NavBar/Navbar"; const generalTabs = [ { diff --git a/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx b/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx index bd5a8f77b..cfeb6e637 100644 --- a/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx +++ b/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx @@ -1,25 +1,20 @@ import { useTranslation } from "react-i18next"; import { faMobile } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { linkOptions, Outlet, useLocation, useRouterState } from "@tanstack/react-router"; +import { Outlet } from "@tanstack/react-router"; import { twMerge } from "tailwind-merge"; import { CreateOrgModal } from "@app/components/organization/CreateOrgModal"; import { Banner } from "@app/components/page-frames/Banner"; -import { BreadcrumbContainer, TBreadcrumbFormat } from "@app/components/v2"; import { OrgPermissionSubjects, useOrgPermission, useServerConfig } from "@app/context"; import { OrgPermissionSecretShareAction } from "@app/context/OrgPermissionContext/types"; import { usePopUp } from "@app/hooks"; -import { ProjectType } from "@app/hooks/api/workspace/types"; import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner"; import { OrgSidebar } from "./components/OrgSidebar"; -import { DefaultSideBar, ProjectOverviewSideBar, SecretSharingSideBar } from "./ProductsSideBar"; import { Navbar } from "./components/NavBar"; export const OrganizationLayout = () => { - const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context }); - const location = useLocation(); const { config } = useServerConfig(); const { permission } = useOrgPermission(); @@ -28,12 +23,6 @@ export const OrganizationLayout = () => { OrgPermissionSubjects.SecretShare ); - const isOrganizationSpecificPage = location.pathname.startsWith("/organization"); - const breadcrumbs = - isOrganizationSpecificPage && matches && "breadcrumbs" in matches - ? matches.breadcrumbs - : undefined; - const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const); const { t } = useTranslation(); @@ -50,15 +39,7 @@ export const OrganizationLayout = () => { {!window.isSecureContext && }
    -
    - {breadcrumbs ? ( - - ) : null} +
    diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx index 68962dea0..ce57a440a 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx @@ -15,28 +15,25 @@ import { import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useQueryClient } from "@tanstack/react-query"; -import { Link, useNavigate, useRouter } from "@tanstack/react-router"; +import { Link, useNavigate, useRouter, useRouterState } from "@tanstack/react-router"; import { Mfa } from "@app/components/auth/Mfa"; import SecurityClient from "@app/components/utilities/SecurityClient"; import { + BreadcrumbContainer, Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, - Lottie + Lottie, + TBreadcrumbFormat } from "@app/components/v2"; 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 { - useGetOrganizations, - useGetOrgTrialUrl, - useLogoutUser, - workspaceKeys -} from "@app/hooks/api"; +import { useGetOrganizations, useLogoutUser, workspaceKeys } from "@app/hooks/api"; import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries"; import { MfaMethod } from "@app/hooks/api/auth/types"; import { SubscriptionPlan } from "@app/hooks/api/types"; @@ -83,8 +80,6 @@ export const Navbar = () => { const { currentOrg } = useOrganization(); const [openSupport, setOpenSupport] = useState(false); const [openUser, setOpenUser] = useState(false); - const [showAdminsModal, setShowAdminsModal] = useState(false); - const { mutateAsync } = useGetOrgTrialUrl(); const { data: orgs } = useGetOrganizations(); const navigate = useNavigate(); @@ -94,6 +89,9 @@ export const Navbar = () => { const router = useRouter(); const queryClient = useQueryClient(); + const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context }); + const breadcrumbs = matches && "breadcrumbs" in matches ? matches.breadcrumbs : undefined; + const handleOrgChange = async (orgId: string) => { queryClient.removeQueries({ queryKey: authKeys.getAuthToken }); queryClient.removeQueries({ queryKey: workspaceKeys.getAllUserWorkspace() }); @@ -140,7 +138,7 @@ export const Navbar = () => { } return ( -
    +
    infisical logo @@ -159,9 +157,7 @@ export const Navbar = () => {
    {getPlan(subscription)}
    -
    - -
    +
    { + +
    +
    + {breadcrumbs ? ( + + ) : null}
    diff --git a/frontend/src/layouts/OrganizationLayout/components/OrgSidebar/OrgSidebar.tsx b/frontend/src/layouts/OrganizationLayout/components/OrgSidebar/OrgSidebar.tsx index 413068061..b01f3c431 100644 --- a/frontend/src/layouts/OrganizationLayout/components/OrgSidebar/OrgSidebar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/OrgSidebar/OrgSidebar.tsx @@ -1,179 +1,36 @@ import { useState } from "react"; -import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons"; import { - faArrowUpRightFromSquare, faBook, - faCheck, faCheckCircle, faCog, faDoorClosed, - faEnvelope, faInfinity, - faInfo, - faInfoCircle, faMoneyBill, faPlug, - faSignOut, - faUser, faUserCog, faUsers } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useQueryClient } from "@tanstack/react-query"; -import { Link, linkOptions, useLocation, useNavigate, useRouter } from "@tanstack/react-router"; +import { Link } from "@tanstack/react-router"; -import { Mfa } from "@app/components/auth/Mfa"; import { CreateOrgModal } from "@app/components/organization/CreateOrgModal"; -import SecurityClient from "@app/components/utilities/SecurityClient"; -import { - Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuTrigger, - Menu, - MenuGroup, - MenuItem, - Modal, - ModalContent, - Tooltip -} from "@app/components/v2"; -import { envConfig } from "@app/config/env"; +import { Menu, MenuGroup, MenuItem, Modal, ModalContent, Tooltip } from "@app/components/v2"; import { useOrganization, useSubscription, useUser } from "@app/context"; -import { isInfisicalCloud } from "@app/helpers/platform"; -import { usePopUp, useToggle } from "@app/hooks"; -import { - useGetOrganizations, - useGetOrgTrialUrl, - useLogoutUser, - useSelectOrganization, - workspaceKeys -} from "@app/hooks/api"; -import { authKeys } from "@app/hooks/api/auth/queries"; -import { MfaMethod } from "@app/hooks/api/auth/types"; -import { SubscriptionPlan } from "@app/hooks/api/types"; -import { AuthMethod } from "@app/hooks/api/users/types"; -import { ProjectType } from "@app/hooks/api/workspace/types"; -import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils"; +import { usePopUp } from "@app/hooks"; +import { useGetOrgTrialUrl } from "@app/hooks/api"; -import { MenuIconButton } from "../MenuIconButton"; import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel"; -const getPlan = (subscription: SubscriptionPlan) => { - if (subscription.groups) return "Enterprise Plan"; - if (subscription.pitRecovery) return "Pro Plan"; - return "Free Plan"; -}; - -export const INFISICAL_SUPPORT_OPTIONS = [ - [ - , - "Support Forum", - "https://infisical.com/slack" - ], - [ - , - "Read Docs", - "https://infisical.com/docs/documentation/getting-started/introduction" - ], - [ - , - "GitHub Issues", - "https://github.com/Infisical/infisical/issues" - ], - [ - , - "Email Support", - "mailto:support@infisical.com" - ], - [ - , - "Instance Admins", - "server-admins" - ] -]; - export const OrgSidebar = () => { - const [shouldShowMfa, toggleShowMfa] = useToggle(false); - const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL); - const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {}); const { subscription } = useSubscription(); - const [open, setOpen] = useState(false); - const [openSupport, setOpenSupport] = useState(false); - const [openUser, setOpenUser] = useState(false); - const [openOrg, setOpenOrg] = useState(false); const [showAdminsModal, setShowAdminsModal] = useState(false); const { user } = useUser(); const { mutateAsync } = useGetOrgTrialUrl(); const { currentOrg } = useOrganization(); - const { data: orgs } = useGetOrganizations(); const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const); - const { mutateAsync: selectOrganization } = useSelectOrganization(); - const navigate = useNavigate(); - const router = useRouter(); - const location = useLocation(); - const queryClient = useQueryClient(); - - const isMoreSelected = ( - [ - linkOptions({ to: "/organization/access-management" }).to, - linkOptions({ to: "/organization/app-connections" }).to, - linkOptions({ to: "/organization/billing" }).to, - linkOptions({ to: "/organization/sso" }).to, - linkOptions({ to: "/organization/gateways" }).to, - linkOptions({ to: "/organization/settings" }).to, - linkOptions({ to: "/organization/audit-logs" }).to - ] as string[] - ).includes(location.pathname); - - const handleOrgChange = async (orgId: string) => { - queryClient.removeQueries({ queryKey: authKeys.getAuthToken }); - queryClient.removeQueries({ queryKey: workspaceKeys.getAllUserWorkspace() }); - - const { token, isMfaEnabled, mfaMethod } = await selectOrganization({ - organizationId: orgId - }); - - if (isMfaEnabled) { - SecurityClient.setMfaToken(token); - if (mfaMethod) { - setRequiredMfaMethod(mfaMethod); - } - toggleShowMfa.on(); - setMfaSuccessCallback(() => () => handleOrgChange(orgId)); - return; - } - await router.invalidate(); - await navigateUserToOrg(navigate, orgId); - }; - - const logout = useLogoutUser(); - const logOutUser = async () => { - try { - console.log("Logging out..."); - await logout.mutateAsync(); - navigate({ to: "/login" }); - } catch (error) { - console.error(error); - } - }; - - if (shouldShowMfa) { - return ( -
    - toggleShowMfa.off()} - /> -
    - ); - } return ( <> @@ -188,13 +45,6 @@ export const OrgSidebar = () => { )} - - {({ isActive }) => ( - - Share Secret - - )} - {({ isActive }) => ( @@ -205,26 +55,6 @@ export const OrgSidebar = () => { )} - - {({ isActive }) => ( - -
    - - App Connections -
    -
    - )} - - - {({ isActive }) => ( - -
    - - Gateways -
    -
    - )} - {({ isActive }) => ( @@ -266,6 +96,28 @@ export const OrgSidebar = () => { )} + + + {({ isActive }) => ( + +
    + + App Connections +
    +
    + )} + + + {({ isActive }) => ( + +
    + + Gateways +
    +
    + )} + +
    {user?.superAdmin && ( @@ -290,6 +142,15 @@ export const OrgSidebar = () => { )} + + + {({ isActive }) => ( + + Share Secret + + )} + +
    ({ breadcrumbs: [ - { - label: "Home", - icon: () => , - link: linkOptions({ to: "/" }) - }, { label: "Access Control" } diff --git a/frontend/src/pages/organization/AdminPage/route.tsx b/frontend/src/pages/organization/AdminPage/route.tsx index f0d6b4985..1ed4e370e 100644 --- a/frontend/src/pages/organization/AdminPage/route.tsx +++ b/frontend/src/pages/organization/AdminPage/route.tsx @@ -1,6 +1,4 @@ -import { faHome } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { createFileRoute } from "@tanstack/react-router"; import { AdminPage } from "./AdminPage"; @@ -10,11 +8,6 @@ export const Route = createFileRoute( component: AdminPage, context: () => ({ breadcrumbs: [ - { - label: "Home", - icon: () => , - link: linkOptions({ to: "/organization/secret-manager/overview" }) - }, { label: "Admin Console" } diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/route.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/route.tsx index 3b4205656..ea24aa679 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/route.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/route.tsx @@ -1,6 +1,4 @@ -import { faHome } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { createFileRoute } from "@tanstack/react-router"; import { AppConnectionsPage } from "./AppConnectionsPage"; @@ -10,11 +8,6 @@ export const Route = createFileRoute( component: AppConnectionsPage, context: () => ({ breadcrumbs: [ - { - label: "Home", - icon: () => , - link: linkOptions({ to: "/organization/secret-manager/overview" }) - }, { label: "App Connections" } diff --git a/frontend/src/pages/organization/AuditLogsPage/route.tsx b/frontend/src/pages/organization/AuditLogsPage/route.tsx index 11b2eea70..3646b4d16 100644 --- a/frontend/src/pages/organization/AuditLogsPage/route.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/route.tsx @@ -1,6 +1,4 @@ -import { faHome } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { createFileRoute } from "@tanstack/react-router"; import { AuditLogsPage } from "./AuditLogsPage"; @@ -10,11 +8,6 @@ export const Route = createFileRoute( component: AuditLogsPage, context: () => ({ breadcrumbs: [ - { - label: "Home", - icon: () => , - link: linkOptions({ to: "/organization/secret-manager/overview" }) - }, { label: "Audit Logs" } diff --git a/frontend/src/pages/organization/CertManagerOverviewPage/route.tsx b/frontend/src/pages/organization/CertManagerOverviewPage/route.tsx index 92fd906e8..1e3673189 100644 --- a/frontend/src/pages/organization/CertManagerOverviewPage/route.tsx +++ b/frontend/src/pages/organization/CertManagerOverviewPage/route.tsx @@ -1,5 +1,3 @@ -import { faHome } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { CertManagerOverviewPage } from "./CertManagerOverviewPage"; @@ -10,10 +8,6 @@ export const Route = createFileRoute( component: CertManagerOverviewPage, context: () => ({ breadcrumbs: [ - { - label: "Products", - icon: () => - }, { label: "Cert Management", link: linkOptions({ to: "/organization/cert-manager/overview" }) diff --git a/frontend/src/pages/organization/CertManagerSettingsPage/route.tsx b/frontend/src/pages/organization/CertManagerSettingsPage/route.tsx index aaccee4bf..f4e87892e 100644 --- a/frontend/src/pages/organization/CertManagerSettingsPage/route.tsx +++ b/frontend/src/pages/organization/CertManagerSettingsPage/route.tsx @@ -1,5 +1,3 @@ -import { faHome } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { CertManagerSettingsPage } from "./CertManagerSettingsPage"; @@ -10,10 +8,6 @@ export const Route = createFileRoute( component: CertManagerSettingsPage, context: () => ({ breadcrumbs: [ - { - label: "Products", - icon: () => - }, { label: "Cert Management", link: linkOptions({ to: "/organization/cert-manager/overview" }) diff --git a/frontend/src/pages/organization/Gateways/GatewayListPage/route.tsx b/frontend/src/pages/organization/Gateways/GatewayListPage/route.tsx index ab03869a3..88a263b4c 100644 --- a/frontend/src/pages/organization/Gateways/GatewayListPage/route.tsx +++ b/frontend/src/pages/organization/Gateways/GatewayListPage/route.tsx @@ -1,6 +1,4 @@ -import { faHome } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { createFileRoute } from "@tanstack/react-router"; import { GatewayListPage } from "./GatewayListPage"; @@ -10,11 +8,6 @@ export const Route = createFileRoute( component: GatewayListPage, context: () => ({ breadcrumbs: [ - { - label: "Home", - icon: () => , - link: linkOptions({ to: "/organization/secret-manager/overview" }) - }, { label: "Gateways" } diff --git a/frontend/src/pages/organization/GroupDetailsByIDPage/route.tsx b/frontend/src/pages/organization/GroupDetailsByIDPage/route.tsx index 83ead0221..3e6e53cb6 100644 --- a/frontend/src/pages/organization/GroupDetailsByIDPage/route.tsx +++ b/frontend/src/pages/organization/GroupDetailsByIDPage/route.tsx @@ -1,5 +1,3 @@ -import { faHome } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage"; @@ -10,11 +8,6 @@ export const Route = createFileRoute( component: GroupDetailsByIDPage, context: () => ({ breadcrumbs: [ - { - label: "Home", - icon: () => , - link: linkOptions({ to: "/organization/secret-manager/overview" }) - }, { label: "Access Control", link: linkOptions({ to: "/organization/access-management" }) diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/route.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/route.tsx index fd4a86260..137ef08a8 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/route.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/route.tsx @@ -1,5 +1,3 @@ -import { faHome } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage"; @@ -10,11 +8,6 @@ export const Route = createFileRoute( component: IdentityDetailsByIDPage, context: () => ({ breadcrumbs: [ - { - label: "Home", - icon: () => , - link: linkOptions({ to: "/organization/secret-manager/overview" }) - }, { label: "Access Control", link: linkOptions({ to: "/organization/access-management" }) diff --git a/frontend/src/pages/organization/KmsOverviewPage/route.tsx b/frontend/src/pages/organization/KmsOverviewPage/route.tsx index dcce98bd4..08f7ddf4a 100644 --- a/frontend/src/pages/organization/KmsOverviewPage/route.tsx +++ b/frontend/src/pages/organization/KmsOverviewPage/route.tsx @@ -1,5 +1,3 @@ -import { faHome } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { KmsOverviewPage } from "./KmsOverviewPage"; @@ -10,10 +8,6 @@ export const Route = createFileRoute( component: KmsOverviewPage, context: () => ({ breadcrumbs: [ - { - label: "Products", - icon: () => - }, { label: "KMS", link: linkOptions({ to: "/organization/kms/overview" }) diff --git a/frontend/src/pages/organization/KmsSettingsPage/route.tsx b/frontend/src/pages/organization/KmsSettingsPage/route.tsx index 17b0d6687..ebabb01f7 100644 --- a/frontend/src/pages/organization/KmsSettingsPage/route.tsx +++ b/frontend/src/pages/organization/KmsSettingsPage/route.tsx @@ -1,5 +1,3 @@ -import { faHome } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { KmsSettingsPage } from "./KmsSettingsPage"; @@ -10,10 +8,6 @@ export const Route = createFileRoute( component: KmsSettingsPage, context: () => ({ breadcrumbs: [ - { - label: "Products", - icon: () => - }, { label: "KMS", link: linkOptions({ to: "/organization/kms/overview" }) diff --git a/frontend/src/pages/organization/RoleByIDPage/route.tsx b/frontend/src/pages/organization/RoleByIDPage/route.tsx index bf6ba92d1..c9036c500 100644 --- a/frontend/src/pages/organization/RoleByIDPage/route.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/route.tsx @@ -1,5 +1,3 @@ -import { faHome } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { RoleByIDPage } from "./RoleByIDPage"; @@ -10,11 +8,6 @@ export const Route = createFileRoute( component: RoleByIDPage, context: () => ({ breadcrumbs: [ - { - label: "Home", - icon: () => , - link: linkOptions({ to: "/organization/secret-manager/overview" }) - }, { label: "Access Control", link: linkOptions({ to: "/organization/access-management" }) diff --git a/frontend/src/pages/organization/SecretManagerOverviewPage/route.tsx b/frontend/src/pages/organization/SecretManagerOverviewPage/route.tsx index 9dc554a65..daf048881 100644 --- a/frontend/src/pages/organization/SecretManagerOverviewPage/route.tsx +++ b/frontend/src/pages/organization/SecretManagerOverviewPage/route.tsx @@ -1,5 +1,3 @@ -import { faHome } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createFileRoute } from "@tanstack/react-router"; import { SecretManagerOverviewPage } from "./SecretManagerOverviewPage"; @@ -10,10 +8,6 @@ export const Route = createFileRoute( component: SecretManagerOverviewPage, context: () => ({ breadcrumbs: [ - { - label: "Products", - icon: () => - }, { label: "Secret Management" } diff --git a/frontend/src/pages/organization/SecretManagerSettingsPage/route.tsx b/frontend/src/pages/organization/SecretManagerSettingsPage/route.tsx index 8e4d6851a..83f6d0992 100644 --- a/frontend/src/pages/organization/SecretManagerSettingsPage/route.tsx +++ b/frontend/src/pages/organization/SecretManagerSettingsPage/route.tsx @@ -1,5 +1,3 @@ -import { faHome } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { SecretManagerSettingsPage } from "./SecretManagerSettingsPage"; @@ -10,10 +8,6 @@ export const Route = createFileRoute( component: SecretManagerSettingsPage, context: () => ({ breadcrumbs: [ - { - label: "Products", - icon: () => - }, { label: "Secret Management", link: linkOptions({ to: "/organization/secret-manager/overview" }) diff --git a/frontend/src/pages/organization/SecretScanningOverviewPage/route.tsx b/frontend/src/pages/organization/SecretScanningOverviewPage/route.tsx index 4ba20b622..a42b42b9b 100644 --- a/frontend/src/pages/organization/SecretScanningOverviewPage/route.tsx +++ b/frontend/src/pages/organization/SecretScanningOverviewPage/route.tsx @@ -1,5 +1,3 @@ -import { faHome } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { SecretScanningOverviewPage } from "./SecretScanningOverviewPage"; @@ -10,10 +8,6 @@ export const Route = createFileRoute( component: SecretScanningOverviewPage, context: () => ({ breadcrumbs: [ - { - label: "Products", - icon: () => - }, { label: "Secret Scanning", link: linkOptions({ to: "/organization/secret-scanning/overview" }) diff --git a/frontend/src/pages/organization/SecretScanningSettingsPage/route.tsx b/frontend/src/pages/organization/SecretScanningSettingsPage/route.tsx index 0a9fc4254..43d763c8a 100644 --- a/frontend/src/pages/organization/SecretScanningSettingsPage/route.tsx +++ b/frontend/src/pages/organization/SecretScanningSettingsPage/route.tsx @@ -1,5 +1,3 @@ -import { faHome } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { SecretScanningSettingsPage } from "./SecretScanningSettingsPage"; @@ -10,10 +8,6 @@ export const Route = createFileRoute( component: SecretScanningSettingsPage, context: () => ({ breadcrumbs: [ - { - label: "Products", - icon: () => - }, { label: "Secret Scanning", link: linkOptions({ to: "/organization/secret-scanning/overview" }) diff --git a/frontend/src/pages/organization/SecretSharingPage/route.tsx b/frontend/src/pages/organization/SecretSharingPage/route.tsx index 2040fd1bb..728fce0b6 100644 --- a/frontend/src/pages/organization/SecretSharingPage/route.tsx +++ b/frontend/src/pages/organization/SecretSharingPage/route.tsx @@ -1,6 +1,4 @@ -import { faHome } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router"; +import { createFileRoute, stripSearchParams } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; @@ -21,11 +19,6 @@ export const Route = createFileRoute( }, context: () => ({ breadcrumbs: [ - { - label: "Home", - icon: () => , - link: linkOptions({ to: "/" }) - }, { label: "Secret Sharing" } diff --git a/frontend/src/pages/organization/SecretSharingSettingsPage/route.tsx b/frontend/src/pages/organization/SecretSharingSettingsPage/route.tsx index b7a9e128d..b938abd2e 100644 --- a/frontend/src/pages/organization/SecretSharingSettingsPage/route.tsx +++ b/frontend/src/pages/organization/SecretSharingSettingsPage/route.tsx @@ -1,5 +1,3 @@ -import { faHome } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; @@ -20,11 +18,6 @@ export const Route = createFileRoute( }, context: () => ({ breadcrumbs: [ - { - label: "Home", - icon: () => , - link: linkOptions({ to: "/" }) - }, { label: "Secret Sharing", link: linkOptions({ to: "/organization/secret-sharing" }) diff --git a/frontend/src/pages/organization/SettingsPage/route.tsx b/frontend/src/pages/organization/SettingsPage/route.tsx index 3f7beb4e8..ca104cf4f 100644 --- a/frontend/src/pages/organization/SettingsPage/route.tsx +++ b/frontend/src/pages/organization/SettingsPage/route.tsx @@ -1,6 +1,4 @@ -import { faHome } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router"; +import { createFileRoute, stripSearchParams } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; @@ -20,11 +18,6 @@ export const Route = createFileRoute( }, context: () => ({ breadcrumbs: [ - { - label: "Home", - icon: () => , - link: linkOptions({ to: "/" }) - }, { label: "Settings" } diff --git a/frontend/src/pages/organization/SshOverviewPage/route.tsx b/frontend/src/pages/organization/SshOverviewPage/route.tsx index 9ae5ebe7e..287151325 100644 --- a/frontend/src/pages/organization/SshOverviewPage/route.tsx +++ b/frontend/src/pages/organization/SshOverviewPage/route.tsx @@ -1,5 +1,3 @@ -import { faHome } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { SshOverviewPage } from "./SshOverviewPage"; @@ -10,10 +8,6 @@ export const Route = createFileRoute( component: SshOverviewPage, context: () => ({ breadcrumbs: [ - { - label: "Products", - icon: () => - }, { label: "SSH", link: linkOptions({ to: "/organization/ssh/overview" }) diff --git a/frontend/src/pages/organization/SshSettingsPage/route.tsx b/frontend/src/pages/organization/SshSettingsPage/route.tsx index c6c65293d..fd55f84f5 100644 --- a/frontend/src/pages/organization/SshSettingsPage/route.tsx +++ b/frontend/src/pages/organization/SshSettingsPage/route.tsx @@ -1,5 +1,3 @@ -import { faHome } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { SshSettingsPage } from "./SshSettingsPage"; @@ -10,10 +8,6 @@ export const Route = createFileRoute( component: SshSettingsPage, context: () => ({ breadcrumbs: [ - { - label: "Products", - icon: () => - }, { label: "SSH", link: linkOptions({ to: "/organization/ssh/overview" }) diff --git a/frontend/src/pages/organization/SsoPage/route.tsx b/frontend/src/pages/organization/SsoPage/route.tsx index c3b144573..92d989569 100644 --- a/frontend/src/pages/organization/SsoPage/route.tsx +++ b/frontend/src/pages/organization/SsoPage/route.tsx @@ -1,6 +1,4 @@ -import { faHome } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router"; +import { createFileRoute, stripSearchParams } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; @@ -20,11 +18,6 @@ export const Route = createFileRoute( }, context: () => ({ breadcrumbs: [ - { - label: "Home", - icon: () => , - link: linkOptions({ to: "/" }) - }, { label: "Single Sign-On (SSO)" } diff --git a/frontend/src/pages/organization/UserDetailsByIDPage/route.tsx b/frontend/src/pages/organization/UserDetailsByIDPage/route.tsx index 2792bebfc..4b733029b 100644 --- a/frontend/src/pages/organization/UserDetailsByIDPage/route.tsx +++ b/frontend/src/pages/organization/UserDetailsByIDPage/route.tsx @@ -1,5 +1,3 @@ -import { faHome } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { UserDetailsByIDPage } from "./UserDetailsByIDPage"; @@ -10,11 +8,6 @@ export const Route = createFileRoute( component: UserDetailsByIDPage, context: () => ({ breadcrumbs: [ - { - label: "Home", - icon: () => , - link: linkOptions({ to: "/organization/secret-manager/overview" }) - }, { label: "Access Control", link: linkOptions({ to: "/organization/access-management" }) diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/route.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/route.tsx index 83ead0221..3e6e53cb6 100644 --- a/frontend/src/pages/project/GroupDetailsByIDPage/route.tsx +++ b/frontend/src/pages/project/GroupDetailsByIDPage/route.tsx @@ -1,5 +1,3 @@ -import { faHome } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage"; @@ -10,11 +8,6 @@ export const Route = createFileRoute( component: GroupDetailsByIDPage, context: () => ({ breadcrumbs: [ - { - label: "Home", - icon: () => , - link: linkOptions({ to: "/organization/secret-manager/overview" }) - }, { label: "Access Control", link: linkOptions({ to: "/organization/access-management" }) From b8f7ffbf533b49162582cd89965b8faa1dfc962d Mon Sep 17 00:00:00 2001 From: = Date: Thu, 26 Jun 2025 14:48:26 +0530 Subject: [PATCH 58/97] feat: re-arranged org project pages --- .../src/components/auth/TeamInviteStep.tsx | 3 +- .../src/components/navigation/NavHeader.tsx | 2 +- .../CreateOrgModal/CreateOrgModal.tsx | 5 +- .../components/projects/NewProjectModal.tsx | 19 +- .../components/ProjectTemplatesTable.tsx | 4 +- .../components/v2/Breadcrumb/Breadcrumb.tsx | 7 +- frontend/src/helpers/project.ts | 3 +- .../hooks/api/projectTemplates/queries.tsx | 11 +- frontend/src/hooks/api/workspace/queries.tsx | 27 +- .../src/hooks/api/workspace/query-keys.tsx | 3 +- frontend/src/hooks/api/workspace/types.ts | 2 - frontend/src/layouts/AdminLayout/Sidebar.tsx | 3 +- .../OrganizationLayout/OrganizationLayout.tsx | 1 - .../ProjectOverviewSideBar.tsx | 2 +- .../components/NavBar/Navbar.tsx | 2 +- .../components/OrgSidebar/OrgSidebar.tsx | 2 +- .../PersonalSettingsLayout.tsx | 5 +- .../ProjectSelect/ProjectSelect.tsx | 5 +- .../SidebarHeader/SidebarHeader.tsx | 5 +- frontend/src/pages/admin/layout.tsx | 2 +- .../src/pages/auth/LoginPage/Login.utils.tsx | 5 +- .../SignUpInvitePage/SignUpInvitePage.tsx | 3 +- .../UserInfoSSOStep/UserInfoSSOStep.tsx | 3 +- .../DeleteProjectSection.tsx | 2 +- .../middlewares/restrict-login-signup.tsx | 2 +- .../organization/BillingPage/BillingPage.tsx | 10 +- .../pages/organization/BillingPage/route.tsx | 11 +- .../CertManagerOverviewPage.tsx | 7 - .../CertManagerOverviewPage/route.tsx | 17 - .../IdentityAddToProjectModal.tsx | 2 +- .../KmsOverviewPage/KmsOverviewPage.tsx | 5 - .../organization/KmsOverviewPage/route.tsx | 17 - .../ProjectsPage.tsx} | 39 +-- .../components/AllProjectView.tsx | 5 +- .../components/MyProjectView.tsx | 18 +- .../components/ProjectListToggle.tsx | 0 .../pages/organization/ProjectsPage/route.tsx | 16 + .../SecretManagerOverviewPage/route.tsx | 16 - .../SecretScanningOverviewPage.tsx | 7 - .../SecretScanningOverviewPage/route.tsx | 17 - .../SshOverviewPage/SshOverviewPage.tsx | 5 - .../organization/SshOverviewPage/route.tsx | 17 - .../SecretV2MigrationSection.tsx | 4 +- frontend/src/routeTree.gen.ts | 331 ++---------------- frontend/src/routes.ts | 12 +- 45 files changed, 128 insertions(+), 556 deletions(-) delete mode 100644 frontend/src/pages/organization/CertManagerOverviewPage/CertManagerOverviewPage.tsx delete mode 100644 frontend/src/pages/organization/CertManagerOverviewPage/route.tsx delete mode 100644 frontend/src/pages/organization/KmsOverviewPage/KmsOverviewPage.tsx delete mode 100644 frontend/src/pages/organization/KmsOverviewPage/route.tsx rename frontend/src/pages/organization/{SecretManagerOverviewPage/SecretManagerOverviewPage.tsx => ProjectsPage/ProjectsPage.tsx} (76%) rename frontend/src/pages/organization/{SecretManagerOverviewPage => ProjectsPage}/components/AllProjectView.tsx (98%) rename frontend/src/pages/organization/{SecretManagerOverviewPage => ProjectsPage}/components/MyProjectView.tsx (96%) rename frontend/src/pages/organization/{SecretManagerOverviewPage => ProjectsPage}/components/ProjectListToggle.tsx (100%) create mode 100644 frontend/src/pages/organization/ProjectsPage/route.tsx delete mode 100644 frontend/src/pages/organization/SecretManagerOverviewPage/route.tsx delete mode 100644 frontend/src/pages/organization/SecretScanningOverviewPage/SecretScanningOverviewPage.tsx delete mode 100644 frontend/src/pages/organization/SecretScanningOverviewPage/route.tsx delete mode 100644 frontend/src/pages/organization/SshOverviewPage/SshOverviewPage.tsx delete mode 100644 frontend/src/pages/organization/SshOverviewPage/route.tsx diff --git a/frontend/src/components/auth/TeamInviteStep.tsx b/frontend/src/components/auth/TeamInviteStep.tsx index 5a9f11bde..b0a5618d9 100644 --- a/frontend/src/components/auth/TeamInviteStep.tsx +++ b/frontend/src/components/auth/TeamInviteStep.tsx @@ -4,7 +4,6 @@ import { useNavigate } from "@tanstack/react-router"; import { useAddUsersToOrg } from "@app/hooks/api"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; -import { ProjectType } from "@app/hooks/api/workspace/types"; import { usePopUp } from "@app/hooks/usePopUp"; import { Button, EmailServiceSetupModal } from "../v2"; @@ -23,7 +22,7 @@ export default function TeamInviteStep(): JSX.Element { // Redirect user to the getting started page const redirectToHome = async () => { - navigate({ to: `/organization/${ProjectType.SecretManager}/overview` as const }); + navigate({ to: "/organization/projects" as const }); }; const inviteUsers = async ({ emails: inviteEmails }: { emails: string }) => { diff --git a/frontend/src/components/navigation/NavHeader.tsx b/frontend/src/components/navigation/NavHeader.tsx index 4386818dd..99f5fbdd2 100644 --- a/frontend/src/components/navigation/NavHeader.tsx +++ b/frontend/src/components/navigation/NavHeader.tsx @@ -71,7 +71,7 @@ export default function NavHeader({ {currentOrg?.name?.charAt(0)}
    {currentOrg?.name} diff --git a/frontend/src/components/organization/CreateOrgModal/CreateOrgModal.tsx b/frontend/src/components/organization/CreateOrgModal/CreateOrgModal.tsx index bdf1c5c81..d4564ab6f 100644 --- a/frontend/src/components/organization/CreateOrgModal/CreateOrgModal.tsx +++ b/frontend/src/components/organization/CreateOrgModal/CreateOrgModal.tsx @@ -59,10 +59,7 @@ export const CreateOrgModal: FC = ({ isOpen, onClose }) => }); navigate({ - to: `/organization/${ProjectType.SecretManager}/overview` as const, - params: { - organizationId: organization.id - } + to: "/organization/projects" }); localStorage.setItem("orgData.id", organization.id); diff --git a/frontend/src/components/projects/NewProjectModal.tsx b/frontend/src/components/projects/NewProjectModal.tsx index c98118a96..b47a251c3 100644 --- a/frontend/src/components/projects/NewProjectModal.tsx +++ b/frontend/src/components/projects/NewProjectModal.tsx @@ -35,7 +35,6 @@ import { getProjectHomePage } from "@app/helpers/project"; import { useCreateWorkspace, useGetExternalKmsList, useGetUserWorkspaces } from "@app/hooks/api"; import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types"; import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates"; -import { ProjectType } from "@app/hooks/api/workspace/types"; const formSchema = z.object({ name: z.string().trim().min(1, "Required").max(64, "Too long, maximum length is 64 characters"), @@ -53,12 +52,11 @@ type TAddProjectFormData = z.infer; interface NewProjectModalProps { isOpen: boolean; onOpenChange: (isOpen: boolean) => void; - projectType: ProjectType; } -type NewProjectFormProps = Pick; +type NewProjectFormProps = Pick; -const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => { +const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => { const navigate = useNavigate(); const { currentOrg } = useOrganization(); const { permission } = useOrgPermission(); @@ -72,7 +70,7 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => { OrgPermissionSubjects.ProjectTemplates ); - const { data: projectTemplates = [] } = useListProjectTemplates(projectType, { + const { data: projectTemplates = [] } = useListProjectTemplates({ enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates) }); @@ -115,8 +113,7 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => { projectName: name, projectDescription: description, kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined, - template, - type: projectType + template }); await refetchWorkspaces(); @@ -270,18 +267,14 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => { ); }; -export const NewProjectModal: FC = ({ - isOpen, - onOpenChange, - projectType -}) => { +export const NewProjectModal: FC = ({ isOpen, onOpenChange }) => { return ( - + ); diff --git a/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/ProjectTemplatesTable.tsx b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/ProjectTemplatesTable.tsx index 0465973e2..70412df5a 100644 --- a/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/ProjectTemplatesTable.tsx +++ b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/ProjectTemplatesTable.tsx @@ -38,8 +38,8 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => { const projectType = useGetProjectTypeFromRoute(); - const { isPending, data: projectTemplates = [] } = useListProjectTemplates(projectType, { - enabled: subscription?.projectTemplates && Boolean(projectType) + const { isPending, data: projectTemplates = [] } = useListProjectTemplates({ + enabled: subscription?.projectTemplates }); const [search, setSearch] = useState(""); diff --git a/frontend/src/components/v2/Breadcrumb/Breadcrumb.tsx b/frontend/src/components/v2/Breadcrumb/Breadcrumb.tsx index a956991fb..20684ee61 100644 --- a/frontend/src/components/v2/Breadcrumb/Breadcrumb.tsx +++ b/frontend/src/components/v2/Breadcrumb/Breadcrumb.tsx @@ -1,11 +1,6 @@ /* eslint-disable react/prop-types */ import React from "react"; -import { - faCaretDown, - faChevronRight, - faEllipsis, - faSlash -} from "@fortawesome/free-solid-svg-icons"; +import { faCaretDown, faEllipsis, faSlash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Link, ReactNode } from "@tanstack/react-router"; import { LinkComponentProps } from "node_modules/@tanstack/react-router/dist/esm/link"; diff --git a/frontend/src/helpers/project.ts b/frontend/src/helpers/project.ts index 10085fd86..f60141bf9 100644 --- a/frontend/src/helpers/project.ts +++ b/frontend/src/helpers/project.ts @@ -42,8 +42,7 @@ export const initProjectHelper = async ({ projectName }: { projectName: string } const { data: { project } } = await createWorkspace({ - projectName, - type: ProjectType.SecretManager + projectName }); try { diff --git a/frontend/src/hooks/api/projectTemplates/queries.tsx b/frontend/src/hooks/api/projectTemplates/queries.tsx index 89bc96715..f5863915a 100644 --- a/frontend/src/hooks/api/projectTemplates/queries.tsx +++ b/frontend/src/hooks/api/projectTemplates/queries.tsx @@ -6,17 +6,14 @@ import { TProjectTemplate, TProjectTemplateResponse } from "@app/hooks/api/projectTemplates/types"; -import { ProjectType } from "@app/hooks/api/workspace/types"; export const projectTemplateKeys = { all: ["project-template"] as const, - list: (projectType?: ProjectType) => - [...projectTemplateKeys.all, "list", ...(projectType ? [projectType] : [])] as const, + list: () => [...projectTemplateKeys.all, "list"] as const, byId: (templateId: string) => [...projectTemplateKeys.all, templateId] as const }; export const useListProjectTemplates = ( - type?: ProjectType, options?: Omit< UseQueryOptions< TProjectTemplate[], @@ -28,11 +25,9 @@ export const useListProjectTemplates = ( > ) => { return useQuery({ - queryKey: projectTemplateKeys.list(type), + queryKey: projectTemplateKeys.list(), queryFn: async () => { - const { data } = await apiRequest.get("/api/v1/project-templates", { - params: { type } - }); + const { data } = await apiRequest.get("/api/v1/project-templates"); return data.projectTemplates; }, diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index fa9240101..ff2a170af 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -154,16 +154,14 @@ export const useGetWorkspaceById = ( export const useGetUserWorkspaces = ({ includeRoles, - type = "all", options = {} }: { includeRoles?: boolean; - type?: ProjectType | "all"; options?: { enabled?: boolean }; } = {}) => useQuery({ - queryKey: workspaceKeys.getAllUserWorkspace(type), - queryFn: () => fetchUserWorkspaces(includeRoles, type), + queryKey: workspaceKeys.getAllUserWorkspace(), + queryFn: () => fetchUserWorkspaces(includeRoles), ...options }); @@ -257,17 +255,16 @@ export const useCreateWorkspace = () => { const queryClient = useQueryClient(); return useMutation<{ data: { project: Workspace } }, object, CreateWorkspaceDTO>({ - mutationFn: async ({ projectName, projectDescription, kmsKeyId, template, type }) => + mutationFn: async ({ projectName, projectDescription, kmsKeyId, template }) => createWorkspace({ projectName, projectDescription, kmsKeyId, - template, - type + template }), - onSuccess: (dto) => { + onSuccess: () => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getAllUserWorkspace(dto.data.project.type) + queryKey: workspaceKeys.getAllUserWorkspace() }); } }); @@ -351,8 +348,8 @@ export const useUpdateWorkspaceVersionLimit = () => { }); return data.workspace; }, - onSuccess: (dto) => { - queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace(dto.type) }); + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace() }); } }); }; @@ -370,8 +367,8 @@ export const useUpdateWorkspaceAuditLogsRetention = () => { ); return data.workspace; }, - onSuccess: (dto) => { - queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace(dto.type) }); + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace() }); } }); }; @@ -384,8 +381,8 @@ export const useDeleteWorkspace = () => { const { data } = await apiRequest.delete(`/api/v1/workspace/${workspaceID}`); return data.workspace; }, - onSuccess: (dto) => { - queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace(dto.type) }); + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace() }); queryClient.invalidateQueries({ queryKey: ["org-admin-projects"] }); diff --git a/frontend/src/hooks/api/workspace/query-keys.tsx b/frontend/src/hooks/api/workspace/query-keys.tsx index 15317d2fd..aca6f48b7 100644 --- a/frontend/src/hooks/api/workspace/query-keys.tsx +++ b/frontend/src/hooks/api/workspace/query-keys.tsx @@ -12,8 +12,7 @@ export const workspaceKeys = { getWorkspaceMemberships: (orgId: string) => [{ orgId }, "workspace-memberships"], getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"], getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"], - getAllUserWorkspace: (type?: string) => - type ? ["workspaces", { type }] : (["workspaces"] as const), + getAllUserWorkspace: () => ["workspaces"] as const, getWorkspaceAuditLogs: (workspaceId: string) => [{ workspaceId }, "workspace-audit-logs"] as const, getWorkspaceUsers: ( diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts index fbaea7742..79110cc44 100644 --- a/frontend/src/hooks/api/workspace/types.ts +++ b/frontend/src/hooks/api/workspace/types.ts @@ -71,7 +71,6 @@ export type CreateWorkspaceDTO = { projectDescription?: string; kmsKeyId?: string; template?: string; - type: ProjectType; }; export type UpdateProjectDTO = { @@ -181,7 +180,6 @@ export enum ProjectIdentityOrderBy { Name = "name" } export type TSearchProjectsDTO = { - type?: ProjectType; name?: string; limit?: number; offset?: number; diff --git a/frontend/src/layouts/AdminLayout/Sidebar.tsx b/frontend/src/layouts/AdminLayout/Sidebar.tsx index 09303731b..60240f72f 100644 --- a/frontend/src/layouts/AdminLayout/Sidebar.tsx +++ b/frontend/src/layouts/AdminLayout/Sidebar.tsx @@ -12,7 +12,6 @@ import { MenuItem } from "@app/components/v2"; import { envConfig } from "@app/config/env"; -import { ProjectType } from "@app/hooks/api/workspace/types"; import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/NavBar/Navbar"; @@ -74,7 +73,7 @@ export const AdminSidebar = () => {