From b80b77ec367ba8585e1e8ca4a0243b13b3d1191e Mon Sep 17 00:00:00 2001 From: = Date: Tue, 24 Jun 2025 16:46:46 +0530 Subject: [PATCH] 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; +};