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 + + + ( + +