From f1072ee67ec41818dd8d8d1344e8386b184f66fb Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 20 Oct 2025 13:41:17 +0400 Subject: [PATCH 001/294] Update kubernetes-injector.mdx --- .../platforms/kubernetes-injector.mdx | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/integrations/platforms/kubernetes-injector.mdx b/docs/integrations/platforms/kubernetes-injector.mdx index d5d48598b..40fea2635 100644 --- a/docs/integrations/platforms/kubernetes-injector.mdx +++ b/docs/integrations/platforms/kubernetes-injector.mdx @@ -51,6 +51,27 @@ $ kubectl logs deployment/infisical-agent-injector 2025/05/19 14:20:06 Successfully updated webhook configuration with CA bundle ``` +## Windows support + +The Infisical Agent Injector supports both running on Windows-based pods, and injecting the agent into Windows-based pods. + +To run the agent injector on a Windows pod, it's important that you add the `nodeSelector.kubernetes.io/os` label to the pod's deployment with the value `windows`. +This can be done by changing the helm values.yaml by adding the following: + +```yaml values.yaml +nodeSelector: + kubernetes.io/os: windows +``` + +By default the agent injector will run on Linux-based pods, unless you specify otherwise like in the example above. +No extra configuration is needed to inject into Windows-based pods, as the agent injector will detect and handle the injection automatically. + +The Agent Injector will only run and inject into Windows-based pods that are running on the supported Windows versions: +- **Windows Server 2022** + +We're looking to add support for other Windows versions in the future. If you're using a different Windows version, please let us know by opening [an issue](https://github.com/Infisical/infisical-agent-injector/issues/new), and we'll look into adding support for your desired version as soon as possible. + + ## Supported annotations The Infisical Agent Injector supports the following annotations: From ecca51b1d60f6294985218b30920fd086009582c Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Fri, 24 Oct 2025 18:04:51 -0300 Subject: [PATCH 002/294] feat(slack-integration): add support for secret sync error notifications and enhance notification handling --- ...re-slack-secret-sync-error-notification.ts | 29 +++++ .../src/db/schemas/project-slack-configs.ts | 4 +- .../notification-handlers/microsoft-teams.ts | 92 ++++++++++++++++ .../notification-handlers/slack.ts | 80 ++++++++++++++ .../trigger-notification.ts | 104 ++++-------------- .../src/lib/workflow-integrations/types.ts | 13 ++- .../project-microsoft-teams-config-dal.ts | 8 +- .../services/secret-sync/secret-sync-queue.ts | 88 ++++++++++----- .../slack/project-slack-config-dal.ts | 5 +- backend/src/services/slack/slack-fns.ts | 65 ++++++++++- 10 files changed, 367 insertions(+), 121 deletions(-) create mode 100644 backend/src/db/migrations/20251024184713_feature-slack-secret-sync-error-notification.ts create mode 100644 backend/src/lib/workflow-integrations/notification-handlers/microsoft-teams.ts create mode 100644 backend/src/lib/workflow-integrations/notification-handlers/slack.ts diff --git a/backend/src/db/migrations/20251024184713_feature-slack-secret-sync-error-notification.ts b/backend/src/db/migrations/20251024184713_feature-slack-secret-sync-error-notification.ts new file mode 100644 index 000000000..4b5bf54d3 --- /dev/null +++ b/backend/src/db/migrations/20251024184713_feature-slack-secret-sync-error-notification.ts @@ -0,0 +1,29 @@ +import { Knex } from "knex"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn("project_slack_configs", "isSecretSyncErrorNotificationEnabled"))) { + await knex.schema.alterTable("project_slack_configs", (table) => { + table.boolean("isSecretSyncErrorNotificationEnabled").notNullable().defaultTo(false); + }); + } + + if (!(await knex.schema.hasColumn("project_slack_configs", "secretSyncErrorChannels"))) { + await knex.schema.alterTable("project_slack_configs", (table) => { + table.text("secretSyncErrorChannels").notNullable().defaultTo(""); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn("project_slack_configs", "isSecretSyncErrorNotificationEnabled")) { + await knex.schema.alterTable("project_slack_configs", (table) => { + table.dropColumn("isSecretSyncErrorNotificationEnabled"); + }); + } + + if (await knex.schema.hasColumn("project_slack_configs", "secretSyncErrorChannels")) { + await knex.schema.alterTable("project_slack_configs", (table) => { + table.dropColumn("secretSyncErrorChannels"); + }); + } +} diff --git a/backend/src/db/schemas/project-slack-configs.ts b/backend/src/db/schemas/project-slack-configs.ts index 0a46e5aae..48674ec8f 100644 --- a/backend/src/db/schemas/project-slack-configs.ts +++ b/backend/src/db/schemas/project-slack-configs.ts @@ -16,7 +16,9 @@ export const ProjectSlackConfigsSchema = z.object({ isSecretRequestNotificationEnabled: z.boolean().default(false), secretRequestChannels: z.string().default(""), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + isSecretSyncErrorNotificationEnabled: z.boolean().default(false), + secretSyncErrorChannels: z.string().default("") }); export type TProjectSlackConfigs = z.infer; diff --git a/backend/src/lib/workflow-integrations/notification-handlers/microsoft-teams.ts b/backend/src/lib/workflow-integrations/notification-handlers/microsoft-teams.ts new file mode 100644 index 000000000..0cfe28491 --- /dev/null +++ b/backend/src/lib/workflow-integrations/notification-handlers/microsoft-teams.ts @@ -0,0 +1,92 @@ +import { validateMicrosoftTeamsChannelsSchema } from "@app/services/microsoft-teams/microsoft-teams-fns"; +import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service"; +import { + TProjectMicrosoftTeamsConfigDALFactory, + TProjectMicrosoftTeamsConfigWithIntegrations +} from "@app/services/microsoft-teams/project-microsoft-teams-config-dal"; + +import { logger } from "../../logger"; +import { TNotification, TriggerFeature } from "../types"; + +const handleMicrosoftTeamsNotification = async ({ + microsoftTeamsConfig, + notification, + orgId, + microsoftTeamsService +}: { + microsoftTeamsConfig: TProjectMicrosoftTeamsConfigWithIntegrations; + notification: TNotification; + orgId: string; + microsoftTeamsService: Pick; +}): Promise => { + let targetChannels: unknown; + let isEnabled = false; + + switch (notification.type) { + case TriggerFeature.ACCESS_REQUEST: + case TriggerFeature.ACCESS_REQUEST_UPDATED: + targetChannels = microsoftTeamsConfig.accessRequestChannels; + isEnabled = microsoftTeamsConfig.isAccessRequestNotificationEnabled; + break; + case TriggerFeature.SECRET_APPROVAL: + targetChannels = microsoftTeamsConfig.secretRequestChannels; + isEnabled = microsoftTeamsConfig.isSecretRequestNotificationEnabled; + break; + default: + return; + } + + if (isEnabled && targetChannels) { + const { success, data, error: validationError } = validateMicrosoftTeamsChannelsSchema.safeParse(targetChannels); + + if (!success) { + logger.error(validationError, "Invalid Microsoft Teams channel configuration"); + return; + } + + if (data) { + await microsoftTeamsService + .sendNotification({ + notification, + target: data, + tenantId: microsoftTeamsConfig.tenantId, + microsoftTeamsIntegrationId: microsoftTeamsConfig.id, + orgId + }) + .catch((error) => { + logger.error( + error, + `Error sending Microsoft Teams notification. Notification type: ${notification.type}, Tenant ID: ${microsoftTeamsConfig.tenantId}, Project ID: ${microsoftTeamsConfig.projectId}` + ); + }); + } + } +}; + +export const triggerMicrosoftTeamsNotification = async ({ + projectId, + notification, + orgId, + projectMicrosoftTeamsConfigDAL, + microsoftTeamsService +}: { + projectId: string; + notification: TNotification; + orgId: string; + projectMicrosoftTeamsConfigDAL: Pick; + microsoftTeamsService: Pick; +}): Promise => { + try { + const config = await projectMicrosoftTeamsConfigDAL.getIntegrationDetailsByProject(projectId); + if (config) { + await handleMicrosoftTeamsNotification({ + microsoftTeamsConfig: config, + notification, + orgId, + microsoftTeamsService + }); + } + } catch (error) { + logger.error(error, `Error handling Microsoft Teams notification. Project ID: ${projectId}`); + } +}; diff --git a/backend/src/lib/workflow-integrations/notification-handlers/slack.ts b/backend/src/lib/workflow-integrations/notification-handlers/slack.ts new file mode 100644 index 000000000..66fd58b3a --- /dev/null +++ b/backend/src/lib/workflow-integrations/notification-handlers/slack.ts @@ -0,0 +1,80 @@ +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { + TProjectSlackConfigDALFactory, + TProjectSlackConfigWithIntegrations +} from "@app/services/slack/project-slack-config-dal"; +import { sendSlackNotification } from "@app/services/slack/slack-fns"; + +import { logger } from "../../logger"; +import { TNotification, TriggerFeature } from "../types"; + +const handleSlackNotification = async ({ + slackConfig, + notification, + orgId, + kmsService +}: { + slackConfig: TProjectSlackConfigWithIntegrations; + notification: TNotification; + orgId: string; + kmsService: Pick; +}): Promise => { + let targetChannelIds: string[] = []; + let isEnabled = false; + + switch (notification.type) { + case TriggerFeature.ACCESS_REQUEST: + case TriggerFeature.ACCESS_REQUEST_UPDATED: + targetChannelIds = slackConfig.accessRequestChannels?.split(", ") || []; + isEnabled = slackConfig.isAccessRequestNotificationEnabled; + break; + case TriggerFeature.SECRET_APPROVAL: + targetChannelIds = slackConfig.secretRequestChannels?.split(", ") || []; + isEnabled = slackConfig.isSecretRequestNotificationEnabled; + break; + case TriggerFeature.SECRET_SYNC_ERROR: + targetChannelIds = slackConfig.secretSyncErrorChannels?.split(", ") || []; + isEnabled = slackConfig.isSecretSyncErrorNotificationEnabled; + break; + default: + return; + } + + if (targetChannelIds.length && isEnabled) { + await sendSlackNotification({ + orgId, + notification, + kmsService, + targetChannelIds, + slackIntegration: slackConfig + }).catch((error) => { + logger.error( + error, + `Error sending Slack notification. Notification type: ${notification.type}, Target channel IDs: ${targetChannelIds.join(", ")}, Project ID: ${slackConfig.projectId}` + ); + }); + } +}; + +export const triggerSlackNotification = async ({ + projectId, + notification, + orgId, + projectSlackConfigDAL, + kmsService +}: { + projectId: string; + notification: TNotification; + orgId: string; + projectSlackConfigDAL: Pick; + kmsService: Pick; +}): Promise => { + try { + const config = await projectSlackConfigDAL.getIntegrationDetailsByProject(projectId); + if (config) { + await handleSlackNotification({ slackConfig: config, notification, orgId, kmsService }); + } + } catch (error) { + logger.error(error, `Error handling Slack notification. Project ID: ${projectId}`); + } +}; diff --git a/backend/src/lib/workflow-integrations/trigger-notification.ts b/backend/src/lib/workflow-integrations/trigger-notification.ts index 355761f73..2dc6f61fe 100644 --- a/backend/src/lib/workflow-integrations/trigger-notification.ts +++ b/backend/src/lib/workflow-integrations/trigger-notification.ts @@ -1,8 +1,7 @@ -import { validateMicrosoftTeamsChannelsSchema } from "@app/services/microsoft-teams/microsoft-teams-fns"; -import { sendSlackNotification } from "@app/services/slack/slack-fns"; - import { logger } from "../logger"; -import { TriggerFeature, TTriggerWorkflowNotificationDTO } from "./types"; +import { triggerMicrosoftTeamsNotification } from "./notification-handlers/microsoft-teams"; +import { triggerSlackNotification } from "./notification-handlers/slack"; +import { TTriggerWorkflowNotificationDTO } from "./types"; export const triggerWorkflowIntegrationNotification = async (dto: TTriggerWorkflowNotificationDTO) => { try { @@ -16,88 +15,25 @@ export const triggerWorkflowIntegrationNotification = async (dto: TTriggerWorkfl return; } - const microsoftTeamsConfig = await projectMicrosoftTeamsConfigDAL.getIntegrationDetailsByProject(projectId); - const slackConfig = await projectSlackConfigDAL.getIntegrationDetailsByProject(projectId); + const handlerPromises = [ + triggerSlackNotification({ + projectId, + notification, + orgId: project.orgId, + projectSlackConfigDAL, + kmsService + }), - if (slackConfig) { - if ( - notification.type === TriggerFeature.ACCESS_REQUEST || - notification.type === TriggerFeature.ACCESS_REQUEST_UPDATED - ) { - const targetChannelIds = slackConfig.accessRequestChannels?.split(", ") || []; - if (targetChannelIds.length && slackConfig.isAccessRequestNotificationEnabled) { - await sendSlackNotification({ - orgId: project.orgId, - notification, - kmsService, - targetChannelIds, - slackIntegration: slackConfig - }).catch((error) => { - logger.error(error, "Error sending Slack notification"); - }); - } - } else if (notification.type === TriggerFeature.SECRET_APPROVAL) { - const targetChannelIds = slackConfig.secretRequestChannels?.split(", ") || []; - if (targetChannelIds.length && slackConfig.isSecretRequestNotificationEnabled) { - await sendSlackNotification({ - orgId: project.orgId, - notification, - kmsService, - targetChannelIds, - slackIntegration: slackConfig - }).catch((error) => { - logger.error(error, "Error sending Slack notification"); - }); - } - } - } + triggerMicrosoftTeamsNotification({ + projectId, + notification, + orgId: project.orgId, + projectMicrosoftTeamsConfigDAL, + microsoftTeamsService + }) + ]; - if (microsoftTeamsConfig) { - if ( - notification.type === TriggerFeature.ACCESS_REQUEST || - notification.type === TriggerFeature.ACCESS_REQUEST_UPDATED - ) { - if (microsoftTeamsConfig.isAccessRequestNotificationEnabled && microsoftTeamsConfig.accessRequestChannels) { - const { success, data } = validateMicrosoftTeamsChannelsSchema.safeParse( - microsoftTeamsConfig.accessRequestChannels - ); - - if (success && data) { - await microsoftTeamsService - .sendNotification({ - notification, - target: data, - tenantId: microsoftTeamsConfig.tenantId, - microsoftTeamsIntegrationId: microsoftTeamsConfig.id, - orgId: project.orgId - }) - .catch((error) => { - logger.error(error, "Error sending Microsoft Teams notification"); - }); - } - } - } else if (notification.type === TriggerFeature.SECRET_APPROVAL) { - if (microsoftTeamsConfig.isSecretRequestNotificationEnabled && microsoftTeamsConfig.secretRequestChannels) { - const { success, data } = validateMicrosoftTeamsChannelsSchema.safeParse( - microsoftTeamsConfig.secretRequestChannels - ); - - if (success && data) { - await microsoftTeamsService - .sendNotification({ - notification, - target: data, - tenantId: microsoftTeamsConfig.tenantId, - microsoftTeamsIntegrationId: microsoftTeamsConfig.id, - orgId: project.orgId - }) - .catch((error) => { - logger.error(error, "Error sending Microsoft Teams notification"); - }); - } - } - } - } + await Promise.allSettled(handlerPromises); } catch (error) { logger.error(error, "Error triggering workflow integration notification"); } diff --git a/backend/src/lib/workflow-integrations/types.ts b/backend/src/lib/workflow-integrations/types.ts index f8f55eadd..08f75d89c 100644 --- a/backend/src/lib/workflow-integrations/types.ts +++ b/backend/src/lib/workflow-integrations/types.ts @@ -7,7 +7,8 @@ import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack export enum TriggerFeature { SECRET_APPROVAL = "secret-approval", ACCESS_REQUEST = "access-request", - ACCESS_REQUEST_UPDATED = "access-request-updated" + ACCESS_REQUEST_UPDATED = "access-request-updated", + SECRET_SYNC_ERROR = "secret-sync-error" } export type TNotification = @@ -51,6 +52,16 @@ export type TNotification = editorFullName?: string; editorEmail?: string; }; + } + | { + type: TriggerFeature.SECRET_SYNC_ERROR; + payload: { + syncName: string; + syncActionLabel: string; + syncDestination: string; + failureMessage: string; + syncUrl: string; + }; }; export type TTriggerWorkflowNotificationDTO = { diff --git a/backend/src/services/microsoft-teams/project-microsoft-teams-config-dal.ts b/backend/src/services/microsoft-teams/project-microsoft-teams-config-dal.ts index 918b96e89..b1f9fec02 100644 --- a/backend/src/services/microsoft-teams/project-microsoft-teams-config-dal.ts +++ b/backend/src/services/microsoft-teams/project-microsoft-teams-config-dal.ts @@ -1,16 +1,20 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; +import { TableName, TMicrosoftTeamsIntegrations } from "@app/db/schemas"; +import { TProjectMicrosoftTeamsConfigs } from "@app/db/schemas/project-microsoft-teams-configs"; import { ormify, selectAllTableCols } from "@app/lib/knex"; export type TProjectMicrosoftTeamsConfigDALFactory = ReturnType; +export type TProjectMicrosoftTeamsConfigWithIntegrations = TProjectMicrosoftTeamsConfigs & TMicrosoftTeamsIntegrations; export const projectMicrosoftTeamsConfigDALFactory = (db: TDbClient) => { const projectMicrosoftTeamsConfigOrm = ormify(db, TableName.ProjectMicrosoftTeamsConfigs); const getIntegrationDetailsByProject = (projectId: string, tx?: Knex) => { - return (tx || db.replicaNode())(TableName.ProjectMicrosoftTeamsConfigs) + return (tx || db.replicaNode())( + TableName.ProjectMicrosoftTeamsConfigs + ) .join( TableName.MicrosoftTeamsIntegrations, `${TableName.ProjectMicrosoftTeamsConfigs}.microsoftTeamsIntegrationId`, diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts index e83a0033f..acb2ea4d1 100644 --- a/backend/src/services/secret-sync/secret-sync-queue.ts +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -10,6 +10,8 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; +import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification"; +import { TriggerFeature } from "@app/lib/workflow-integrations/types"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { SecretNameSchema } from "@app/server/lib/schemas"; import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns"; @@ -62,8 +64,11 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal"; import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service"; +import { TMicrosoftTeamsServiceFactory } from "../microsoft-teams/microsoft-teams-service"; +import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal"; import { TNotificationServiceFactory } from "../notification/notification-service"; import { NotificationType } from "../notification/notification-types"; +import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal"; export type TSecretSyncQueueFactory = ReturnType; @@ -104,6 +109,9 @@ type TSecretSyncQueueFactoryDep = { gatewayService: Pick; gatewayV2Service: Pick; notificationService: Pick; + projectSlackConfigDAL: Pick; + projectMicrosoftTeamsConfigDAL: Pick; + microsoftTeamsService: Pick; }; type SecretSyncActionJob = Job< @@ -147,7 +155,10 @@ export const secretSyncQueueFactory = ({ licenseService, gatewayService, gatewayV2Service, - notificationService + notificationService, + projectSlackConfigDAL, + projectMicrosoftTeamsConfigDAL, + microsoftTeamsService }: TSecretSyncQueueFactoryDep) => { const appCfg = getConfig(); @@ -923,32 +934,57 @@ export const secretSyncQueueFactory = ({ const syncPath = `/projects/secret-management/${projectId}/integrations/secret-syncs/${destination}/${secretSync.id}`; - await notificationService.createUserNotifications( - projectAdmins.map((admin) => ({ - userId: admin.userId, - orgId: project.orgId, - type: NotificationType.SECRET_SYNC_FAILED, - title: `Secret Sync Failed to ${actionLabel} Secrets`, - body: `Your **${syncDestination}** sync **${name}** failed to complete${failureMessage ? `: \`${failureMessage}\`` : ""}`, - link: syncPath - })) - ); + const notifications = [ + triggerWorkflowIntegrationNotification({ + input: { + notification: { + type: TriggerFeature.SECRET_SYNC_ERROR, + payload: { + syncName: name, + syncDestination, + failureMessage: failureMessage || "An unknown error occurred", + syncUrl: `${appCfg.SITE_URL}${syncPath}`, + syncActionLabel: actionLabel + } + }, + projectId: project.id + }, + dependencies: { + projectDAL, + projectSlackConfigDAL, + kmsService, + microsoftTeamsService, + projectMicrosoftTeamsConfigDAL + } + }), + notificationService.createUserNotifications( + projectAdmins.map((admin) => ({ + userId: admin.userId, + orgId: project.orgId, + type: NotificationType.SECRET_SYNC_FAILED, + title: `Secret Sync Failed to ${actionLabel} Secrets`, + body: `Your **${syncDestination}** sync **${name}** failed to complete${failureMessage ? `: \`${failureMessage}\`` : ""}`, + link: syncPath + })) + ), + smtpService.sendMail({ + recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean), + template: SmtpTemplates.SecretSyncFailed, + subjectLine: `Secret Sync Failed to ${actionLabel} Secrets`, + substitutions: { + syncName: name, + syncDestination, + content: `Your ${syncDestination} Sync named "${name}" failed while attempting to ${action.toLowerCase()} secrets.`, + failureMessage, + secretPath: folder?.path, + environment: environment?.name, + projectName: project.name, + syncUrl: `${appCfg.SITE_URL}${syncPath}` + } + }) + ]; - await smtpService.sendMail({ - recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean), - template: SmtpTemplates.SecretSyncFailed, - subjectLine: `Secret Sync Failed to ${actionLabel} Secrets`, - substitutions: { - syncName: name, - syncDestination, - content: `Your ${syncDestination} Sync named "${name}" failed while attempting to ${action.toLowerCase()} secrets.`, - failureMessage, - secretPath: folder?.path, - environment: environment?.name, - projectName: project.name, - syncUrl: `${appCfg.SITE_URL}${syncPath}` - } - }); + await Promise.allSettled(notifications); }; const queueSecretSyncsSyncSecretsByPath = async ({ diff --git a/backend/src/services/slack/project-slack-config-dal.ts b/backend/src/services/slack/project-slack-config-dal.ts index 276442b1b..4b5b2146e 100644 --- a/backend/src/services/slack/project-slack-config-dal.ts +++ b/backend/src/services/slack/project-slack-config-dal.ts @@ -1,16 +1,17 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; +import { TableName, TProjectSlackConfigs, TSlackIntegrations } from "@app/db/schemas"; import { ormify, selectAllTableCols } from "@app/lib/knex"; export type TProjectSlackConfigDALFactory = ReturnType; +export type TProjectSlackConfigWithIntegrations = TProjectSlackConfigs & TSlackIntegrations; export const projectSlackConfigDALFactory = (db: TDbClient) => { const projectSlackConfigOrm = ormify(db, TableName.ProjectSlackConfigs); const getIntegrationDetailsByProject = (projectId: string, tx?: Knex) => { - return (tx || db.replicaNode())(TableName.ProjectSlackConfigs) + return (tx || db.replicaNode())(TableName.ProjectSlackConfigs) .join( TableName.SlackIntegrations, `${TableName.ProjectSlackConfigs}.slackIntegrationId`, diff --git a/backend/src/services/slack/slack-fns.ts b/backend/src/services/slack/slack-fns.ts index 88db1a84d..92684a707 100644 --- a/backend/src/services/slack/slack-fns.ts +++ b/backend/src/services/slack/slack-fns.ts @@ -8,6 +8,9 @@ import { TNotification, TriggerFeature } from "@app/lib/workflow-integrations/ty import { KmsDataKey } from "../kms/kms-types"; import { TSendSlackNotificationDTO } from "./slack-types"; +const COMPANY_BRAND_COLOR = "#e0ed34"; +const ERROR_COLOR = "#e74c3c"; + export const fetchSlackChannels = async (botKey: string) => { const slackChannels: { name: string; @@ -74,7 +77,8 @@ View the complete details <${appCfg.SITE_URL}/projects/secret-management/${paylo return { payloadMessage: messageBody, - payloadBlocks + payloadBlocks, + color: COMPANY_BRAND_COLOR }; } case TriggerFeature.ACCESS_REQUEST: { @@ -112,7 +116,8 @@ User Note: ${payload.note}` return { payloadMessage: messageBody, - payloadBlocks + payloadBlocks, + color: COMPANY_BRAND_COLOR }; } case TriggerFeature.ACCESS_REQUEST_UPDATED: { @@ -150,7 +155,52 @@ Editor Note: ${payload.editNote}` return { payloadMessage: messageBody, - payloadBlocks + payloadBlocks, + color: COMPANY_BRAND_COLOR + }; + } + case TriggerFeature.SECRET_SYNC_ERROR: { + const { payload } = notification; + const messageBody = `${payload.syncName} for ${payload.syncDestination} failed on ${payload.syncActionLabel} + +Sync Error: ${payload.failureMessage}`; + + const payloadBlocks = [ + { + type: "header", + text: { + type: "plain_text", + text: `${payload.syncName} for ${payload.syncDestination} failed on ${payload.syncActionLabel}`, + emoji: true + } + }, + { + type: "section", + text: { + type: "mrkdwn", + text: `*Sync Error:* ${payload.failureMessage}` + } + }, + { + type: "actions", + elements: [ + { + type: "button", + text: { + type: "plain_text", + text: `Open ${payload.syncName}`, + emoji: true + }, + url: payload.syncUrl + } + ] + } + ]; + + return { + payloadMessage: messageBody, + payloadBlocks, + color: ERROR_COLOR }; } default: { @@ -177,7 +227,7 @@ export const sendSlackNotification = async ({ }).toString("utf8"); const slackWebClient = new WebClient(botKey); - const { payloadMessage, payloadBlocks } = buildSlackPayload(notification); + const { payloadMessage, payloadBlocks, color } = buildSlackPayload(notification); for await (const conversationId of targetChannelIds) { // we send both text and blocks for compatibility with barebone clients @@ -185,7 +235,12 @@ export const sendSlackNotification = async ({ .postMessage({ channel: conversationId, text: payloadMessage, - blocks: payloadBlocks + attachments: [ + { + color, + blocks: payloadBlocks + } + ] }) .catch((err) => logger.error(err)); } From df3986098f56da25a26944c05bf1adb4e541652e Mon Sep 17 00:00:00 2001 From: x032205 Date: Sat, 25 Oct 2025 04:48:29 -0400 Subject: [PATCH 003/294] feat: add Terraform EC2 to interactive setup --- .../RelayTab/components/RelayDeployModal.tsx | 8 +- .../RelayTerraformDeploymentMethod.tsx | 520 ++++++++++++++++++ 2 files changed, 527 insertions(+), 1 deletion(-) create mode 100644 frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayTerraformDeploymentMethod.tsx diff --git a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeployModal.tsx b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeployModal.tsx index 496ecb762..aab599925 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeployModal.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeployModal.tsx @@ -4,6 +4,7 @@ import { Modal, ModalContent } from "@app/components/v2"; import { RelayDeploymentMethodSelect } from "@app/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeploymentMethodSelect"; import { RelayCliDeploymentMethod } from "./RelayCliDeploymentMethod"; +import { RelayTerraformDeploymentMethod } from "./RelayTerraformDeploymentMethod"; type Props = { isOpen: boolean; @@ -11,7 +12,12 @@ type Props = { }; export const RelayDeploymentInfoMap = { - cli: { name: "CLI", image: "SSH.png", component: RelayCliDeploymentMethod } + cli: { name: "CLI", image: "SSH.png", component: RelayCliDeploymentMethod }, + terraform: { + name: "Terraform", + image: "Terraform.png", + component: RelayTerraformDeploymentMethod + } } as const; export type RelayDeploymentMethod = keyof typeof RelayDeploymentInfoMap; diff --git a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayTerraformDeploymentMethod.tsx b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayTerraformDeploymentMethod.tsx new file mode 100644 index 000000000..6e3cfa9e2 --- /dev/null +++ b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayTerraformDeploymentMethod.tsx @@ -0,0 +1,520 @@ +import { useMemo, useState } from "react"; +import { SingleValue } from "react-select"; +import { faCopy, faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + Checkbox, + FilterableSelect, + FormLabel, + IconButton, + Input, + ModalClose, + Tooltip +} from "@app/components/v2"; +import { Tab } from "@headlessui/react"; +import { + OrgPermissionIdentityActions, + OrgPermissionSubjects, + useOrganization, + useOrgPermission +} from "@app/context"; +import { + useAddIdentityTokenAuth, + useCreateTokenIdentityTokenAuth, + useGetIdentityMembershipOrgs, + useGetIdentityTokenAuth +} from "@app/hooks/api"; +import { slugSchema } from "@app/lib/schemas"; + +const baseFormSchema = z.object({ + name: slugSchema({ field: "name" }), + instanceDomain: z.string().url("Must be a valid URL").or(z.literal("")) +}); + +const formSchemaWithIdentity = baseFormSchema.extend({ + identity: z + .object( + { + id: z.string(), + name: z.string() + }, + { required_error: "Identity is required" } + ) + .nullable() + .refine((val) => val !== null, { message: "Identity is required" }) +}); + +const formSchemaWithToken = baseFormSchema.extend({ + identityToken: z.string().min(1, "Token is required") +}); + +const ec2FormSchema = z.object({ + awsRegion: z.string().min(1, "AWS Region is required"), + vpcId: z.string().min(1, "VPC ID is required"), + ami: z.string().min(1, "AMI ID is required"), + subnetId: z.string().min(1, "Subnet ID is required") +}); + +export const RelayTerraformDeploymentMethod = () => { + const { protocol, hostname, port } = window.location; + const portSuffix = port && port !== "80" ? `:${port}` : ""; + const siteURL = `${protocol}//${hostname}${portSuffix}`; + + const [selectedTabIndex, setSelectedTabIndex] = useState(0); + + const [autogenerateToken, setAutogenerateToken] = useState(true); + const [step, setStep] = useState<"form" | "command">("form"); + const [name, setName] = useState(""); + + const [instanceDomain, setInstanceDomain] = useState(siteURL); + const [identity, setIdentity] = useState(null); + const [identityToken, setIdentityToken] = useState(""); + const [formErrors, setFormErrors] = useState([]); + + const [awsRegion, setAwsRegion] = useState("us-east-1"); + const [vpcId, setVpcId] = useState(""); + const [ami, setAmi] = useState("ami-01b2110eef525172b"); + const [subnetId, setSubnetId] = useState(""); + + const errors = useMemo(() => { + const errorMap: Record = {}; + formErrors.forEach((issue) => { + if (issue.path.length > 0) { + errorMap[String(issue.path[0])] = issue.message; + } + }); + return errorMap; + }, [formErrors]); + + const { currentOrg } = useOrganization(); + const organizationId = currentOrg?.id || ""; + + const { permission } = useOrgPermission(); + const canCreateToken = permission.can( + OrgPermissionIdentityActions.CreateToken, + OrgPermissionSubjects.Identity + ); + + const { data: identityMembershipOrgsData, isPending: isIdentitiesLoading } = + useGetIdentityMembershipOrgs({ + organizationId, + limit: 20000 + }); + const identityMembershipOrgs = identityMembershipOrgsData?.identityMemberships || []; + + const { mutateAsync: createToken, isPending: isCreatingToken } = + useCreateTokenIdentityTokenAuth(); + const { mutateAsync: addIdentityTokenAuth, isPending: isAddingTokenAuth } = + useAddIdentityTokenAuth(); + const { refetch } = useGetIdentityTokenAuth(identity?.id ?? ""); + + const handleGenerateCommand = async () => { + setFormErrors([]); + + if (canCreateToken && autogenerateToken) { + const validation = formSchemaWithIdentity.safeParse({ name, instanceDomain, identity }); + if (!validation.success) { + setFormErrors(validation.error.issues); + return; + } + + if (selectedTabIndex === 0) { + const ec2Validation = ec2FormSchema.safeParse({ awsRegion, vpcId, ami, subnetId }); + if (!ec2Validation.success) { + setFormErrors(ec2Validation.error.issues); + return; + } + } + + const validatedIdentity = validation.data.identity; + + try { + const { data: identityTokenAuth } = await refetch(); + if (!identityTokenAuth) { + await addIdentityTokenAuth({ + identityId: validatedIdentity.id, + organizationId, + accessTokenTTL: 2592000, + accessTokenMaxTTL: 2592000, + accessTokenNumUsesLimit: 0, + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + }); + createNotification({ + text: "Token authentication has been automatically enabled for the selected identity. By default, it is configured to allow all IP addresses with a default token TTL of 30 days. You can manage these settings in Access Control.", + type: "warning" + }); + } + + const token = await createToken({ + identityId: validatedIdentity.id, + name: `relay token for ${name} (autogenerated)` + }); + setIdentityToken(token.accessToken); + createNotification({ + text: "Automatically generated a token for the selected identity.", + type: "info" + }); + setStep("command"); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to generate token for the selected identity", + type: "error" + }); + setIdentityToken(""); + } + } else { + const validation = formSchemaWithToken.safeParse({ + name, + instanceDomain, + identityToken + }); + if (!validation.success) { + setFormErrors(validation.error.issues); + return; + } + + if (selectedTabIndex === 0) { + const ec2Validation = ec2FormSchema.safeParse({ awsRegion, vpcId, ami, subnetId }); + if (!ec2Validation.success) { + setFormErrors(ec2Validation.error.issues); + return; + } + } + setStep("command"); + } + }; + + const handleIdentityChange = ( + selectedIdentity: SingleValue<{ + id: string; + name: string; + }> + ) => { + setIdentity(selectedIdentity); + }; + + const terraformCommand = useMemo(() => { + const domain = instanceDomain || "https://app.infisical.com"; + return `terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} + +provider "aws" { + region = "${awsRegion}" +} + +# Security Group for the Infisical Relay instance +resource "aws_security_group" "infisical_relay_sg" { + name = "${name}-relay-sg" + description = "Allows inbound traffic for Infisical Relay and SSH" + vpc_id = "${vpcId}" + + # Inbound: Allows the Infisical platform to securely communicate with the Relay server. + ingress { + from_port = 8443 + to_port = 8443 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + # Inbound: Allows Infisical Gateway to securely communicate via the Relay. + ingress { + from_port = 2222 + to_port = 2222 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + # Inbound: Allows secure shell (SSH) access for administration. + ingress { + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] # Restrict this to your IP in production + } + + # Outbound: Allows the Relay server to make necessary outbound connections to the Infisical platform. + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "${name}-relay-sg" + } +} + +# Elastic IP for a static public IP address +resource "aws_eip" "infisical_relay_eip" { + tags = { + Name = "${name}-relay-eip" + } +} + +# EC2 instance to run Infisical Relay +module "infisical_relay_instance" { + source = "terraform-aws-modules/ec2-instance/aws" + version = "~> 5.6" + + name = "${name}-relay-instance" + ami = "${ami}" + instance_type = "t3.micro" + subnet_id = "${subnetId}" + + vpc_security_group_ids = [aws_security_group.infisical_relay_sg.id] + associate_public_ip_address = false # We are using an Elastic IP instead + + user_data = <<-EOT + #!/bin/bash + set -e + # Install Infisical CLI + curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash + apt-get update && apt-get install -y infisical + + # Install the relay as a systemd service. + # This example uses a Machine Identity token for authentication via the INFISICAL_TOKEN environment variable. + # + # Note: For production environments, you might consider fetching the token from AWS Parameter Store or AWS Secrets Manager. + export INFISICAL_TOKEN="${identityToken}" + sudo -E infisical relay systemd install \\ + --name "${name}" \\ + --domain "${domain}" \\ + --host "\${aws_eip.infisical_relay_eip.public_ip}" + + # Start and enable the service to run on boot + sudo systemctl start infisical-relay + sudo systemctl enable infisical-relay + EOT +} + +# Associate the Elastic IP with the EC2 instance +resource "aws_eip_association" "eip_assoc" { + instance_id = module.infisical_relay_instance.id + allocation_id = aws_eip.infisical_relay_eip.id +} +`; + }, [name, instanceDomain, identityToken, awsRegion, vpcId, ami, subnetId]); + + if (step === "command") { + return ( + <> +
+ Terraform Configuration + { + navigator.clipboard.writeText(terraformCommand); + createNotification({ + text: "Terraform configuration copied to clipboard", + type: "info" + }); + }} + className="w-10" + > + + +
+
+
+            {terraformCommand}
+          
+
+
+ + + +
+ + ); + } + + return ( + <> + + setName(e.target.value)} + placeholder="Enter relay name..." + isError={Boolean(errors.name)} + /> + {errors.name &&

{errors.name}

} + + + setInstanceDomain(e.target.value)} + placeholder="https://app.infisical.com" + isError={Boolean(errors.instanceDomain)} + /> + {errors.instanceDomain &&

{errors.instanceDomain}

} + + {canCreateToken && autogenerateToken ? ( + <> + + + handleIdentityChange( + e as SingleValue<{ + id: string; + name: string; + }> + ) + } + isLoading={isIdentitiesLoading} + placeholder="Select identity..." + options={identityMembershipOrgs.map((membership) => membership.identity)} + getOptionValue={(option) => option.id} + getOptionLabel={(option) => option.name} + /> + {errors.identity &&

{errors.identity}

} + + ) : ( + <> + + setIdentityToken(e.target.value)} + placeholder="Enter identity token..." + isError={Boolean(errors.identityToken)} + /> + {errors.identityToken &&

{errors.identityToken}

} + + )} + + {canCreateToken && ( +
+ { + setAutogenerateToken(Boolean(e)); + }} + id="autogenerate-token" + className="mr-2" + > +
+ Automatically enable token auth and generate a token for identity + + Token authentication will be automatically enabled for the selected identity if + it isn't already configured. By default, it will be configured to allow all + IP addresses with a token TTL of 30 days. You can manage these settings in + Access Control. +
+
A token will automatically be generated to be used with the CLI command. + + } + > + +
+
+
+
+ )} + + + + + `-mb-[0.14rem] px-4 py-2 text-sm font-medium whitespace-nowrap outline-hidden disabled:opacity-60 ${ + selected ? "border-b-2 border-mineshaft-300 text-mineshaft-200" : "text-bunker-300" + }` + } + > + EC2 + + + + + + setAwsRegion(e.target.value)} + placeholder="us-east-1" + isError={Boolean(errors.awsRegion)} + /> + {errors.awsRegion &&

{errors.awsRegion}

} + + setVpcId(e.target.value)} + placeholder="vpc-..." + isError={Boolean(errors.vpcId)} + /> + {errors.vpcId &&

{errors.vpcId}

} + + setAmi(e.target.value)} + placeholder="ami-..." + isError={Boolean(errors.ami)} + /> + {errors.ami &&

{errors.ami}

} + + setSubnetId(e.target.value)} + placeholder="subnet-..." + isError={Boolean(errors.subnetId)} + /> + {errors.subnetId &&

{errors.subnetId}

} +
+
+
+ +
+ + + + +
+ + ); +}; From c1dc3c2b7689a2e54174ded0a0263ede6aae2030 Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Mon, 27 Oct 2025 09:29:16 -0300 Subject: [PATCH 004/294] feat(workflow-integration): enhance project workflow integration with secret sync error notifications and improve Slack notification formatting --- .../access-approval-request-service.ts | 4 +- .../src/lib/workflow-integrations/types.ts | 5 + backend/src/server/routes/index.ts | 5 +- .../src/server/routes/v1/project-router.ts | 12 +- .../src/services/project/project-service.ts | 20 +- backend/src/services/project/project-types.ts | 2 + .../services/secret-sync/secret-sync-queue.ts | 12 +- backend/src/services/slack/slack-fns.ts | 70 +++-- .../hooks/api/workflowIntegrations/types.ts | 4 + .../WorkflowIntegrationTab.tsx | 2 +- .../components/MicrosoftTeamsConfigRow.tsx | 4 +- .../components/SlackConfigRow.tsx | 17 +- .../components/SlackIntegrationForm.tsx | 255 ++++++++++-------- 13 files changed, 260 insertions(+), 152 deletions(-) diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index 1027995a7..83d3a23a7 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -243,7 +243,8 @@ export const accessApprovalRequestServiceFactory = ({ ); const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`; - const approvalPath = `/projects/secret-management/${project.id}/approval`; + const projectPath = `/projects/secret-management/${project.id}`; + const approvalPath = `${projectPath}/approval`; const approvalUrl = `${cfg.SITE_URL}${approvalPath}`; await triggerWorkflowIntegrationNotification({ @@ -252,6 +253,7 @@ export const accessApprovalRequestServiceFactory = ({ type: TriggerFeature.ACCESS_REQUEST, payload: { projectName: project.name, + projectPath, requesterFullName, isTemporary, requesterEmail: requestedByUser.email as string, diff --git a/backend/src/lib/workflow-integrations/types.ts b/backend/src/lib/workflow-integrations/types.ts index 08f75d89c..db0725475 100644 --- a/backend/src/lib/workflow-integrations/types.ts +++ b/backend/src/lib/workflow-integrations/types.ts @@ -32,6 +32,7 @@ export type TNotification = secretPath: string; environment: string; projectName: string; + projectPath: string; permissions: string[]; approvalUrl: string; note?: string; @@ -61,6 +62,10 @@ export type TNotification = syncDestination: string; failureMessage: string; syncUrl: string; + environment: string; + secretPath: string; + projectName: string; + projectPath: string; }; }; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index b42d01850..0125df798 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1244,7 +1244,10 @@ export const registerRoutes = async ( licenseService, gatewayService, gatewayV2Service, - notificationService + notificationService, + projectSlackConfigDAL, + projectMicrosoftTeamsConfigDAL, + microsoftTeamsService }); const secretQueueService = secretQueueFactory({ diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index c1f4140e5..3bd050ac8 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -769,7 +769,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { isAccessRequestNotificationEnabled: true, accessRequestChannels: true, isSecretRequestNotificationEnabled: true, - secretRequestChannels: true + secretRequestChannels: true, + isSecretSyncErrorNotificationEnabled: true, + secretSyncErrorChannels: true }).merge( z.object({ integration: z.literal(WorkflowIntegration.SLACK), @@ -873,7 +875,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { accessRequestChannels: validateSlackChannelsField, secretRequestChannels: validateSlackChannelsField, isAccessRequestNotificationEnabled: z.boolean(), - isSecretRequestNotificationEnabled: z.boolean() + isSecretRequestNotificationEnabled: z.boolean(), + secretSyncErrorChannels: validateSlackChannelsField, + isSecretSyncErrorNotificationEnabled: z.boolean() }), z.object({ integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS), @@ -891,7 +895,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { isAccessRequestNotificationEnabled: true, accessRequestChannels: true, isSecretRequestNotificationEnabled: true, - secretRequestChannels: true + secretRequestChannels: true, + isSecretSyncErrorNotificationEnabled: true, + secretSyncErrorChannels: true }).merge( z.object({ integration: z.literal(WorkflowIntegration.SLACK), diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index e29f18404..3c6d541ce 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -1568,8 +1568,14 @@ export const projectServiceFactory = ({ isAccessRequestNotificationEnabled, accessRequestChannels, isSecretRequestNotificationEnabled, - secretRequestChannels - }: TUpdateProjectWorkflowIntegration) => { + secretRequestChannels, + secretSyncErrorChannels, + isSecretSyncErrorNotificationEnabled + }: TUpdateProjectWorkflowIntegration & { + // workaround intersection type while we don't have the microsoft teams integration for failed secret syncs + isSecretSyncErrorNotificationEnabled: boolean; + secretSyncErrorChannels: string; + }) => { const project = await projectDAL.findById(projectId); if (!project) { throw new NotFoundError({ @@ -1591,6 +1597,7 @@ export const projectServiceFactory = ({ const sanitizedAccessRequestChannels = validateSlackChannelsField.parse(accessRequestChannels); const sanitizedSecretRequestChannels = validateSlackChannelsField.parse(secretRequestChannels); + const sanitizedSecretSyncErrorChannels = validateSlackChannelsField.parse(secretSyncErrorChannels); const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(integrationId); @@ -1628,7 +1635,9 @@ export const projectServiceFactory = ({ isAccessRequestNotificationEnabled, accessRequestChannels: sanitizedAccessRequestChannels, isSecretRequestNotificationEnabled, - secretRequestChannels: sanitizedSecretRequestChannels + secretRequestChannels: sanitizedSecretRequestChannels, + isSecretSyncErrorNotificationEnabled, + secretSyncErrorChannels: sanitizedSecretSyncErrorChannels }, tx ); @@ -1641,7 +1650,9 @@ export const projectServiceFactory = ({ isAccessRequestNotificationEnabled, accessRequestChannels: sanitizedAccessRequestChannels, isSecretRequestNotificationEnabled, - secretRequestChannels: sanitizedSecretRequestChannels + secretRequestChannels: sanitizedSecretRequestChannels, + isSecretSyncErrorNotificationEnabled, + secretSyncErrorChannels: sanitizedSecretSyncErrorChannels }, tx ); @@ -1651,6 +1662,7 @@ export const projectServiceFactory = ({ ...updatedWorkflowIntegration, accessRequestChannels: sanitizedAccessRequestChannels, secretRequestChannels: sanitizedSecretRequestChannels, + secretSyncErrorChannels: sanitizedSecretSyncErrorChannels, integrationId: slackIntegration.id, integration: WorkflowIntegration.SLACK } as const; diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index 18ae74350..318b08a8d 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -184,8 +184,10 @@ export type TUpdateProjectWorkflowIntegration = ( integration: WorkflowIntegration.SLACK; isAccessRequestNotificationEnabled: boolean; isSecretRequestNotificationEnabled: boolean; + isSecretSyncErrorNotificationEnabled: boolean; accessRequestChannels?: string; secretRequestChannels?: string; + secretSyncErrorChannels?: string; } | { integrationId: string; diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts index acb2ea4d1..f6e23dded 100644 --- a/backend/src/services/secret-sync/secret-sync-queue.ts +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -932,7 +932,9 @@ export const secretSyncQueueFactory = ({ break; } - const syncPath = `/projects/secret-management/${projectId}/integrations/secret-syncs/${destination}/${secretSync.id}`; + const baseProjectPath = `/projects/secret-management/${projectId}`; + const overviewPath = `${baseProjectPath}/overview`; + const syncPath = `${baseProjectPath}/integrations/secret-syncs/${destination}/${secretSync.id}`; const notifications = [ triggerWorkflowIntegrationNotification({ @@ -944,10 +946,14 @@ export const secretSyncQueueFactory = ({ syncDestination, failureMessage: failureMessage || "An unknown error occurred", syncUrl: `${appCfg.SITE_URL}${syncPath}`, - syncActionLabel: actionLabel + syncActionLabel: actionLabel, + environment: environment?.name || "-", + secretPath: folder?.path || "-", + projectName: project.name, + projectPath: overviewPath } }, - projectId: project.id + projectId }, dependencies: { projectDAL, diff --git a/backend/src/services/slack/slack-fns.ts b/backend/src/services/slack/slack-fns.ts index 92684a707..d813cb9ae 100644 --- a/backend/src/services/slack/slack-fns.ts +++ b/backend/src/services/slack/slack-fns.ts @@ -76,6 +76,7 @@ View the complete details <${appCfg.SITE_URL}/projects/secret-management/${paylo ]; return { + headerBlocks: [], payloadMessage: messageBody, payloadBlocks, color: COMPANY_BRAND_COLOR @@ -83,20 +84,15 @@ View the complete details <${appCfg.SITE_URL}/projects/secret-management/${paylo } case TriggerFeature.ACCESS_REQUEST: { const { payload } = notification; - const messageBody = `${payload.requesterFullName} (${payload.requesterEmail}) has requested ${ - payload.isTemporary ? "temporary" : "permanent" - } access to ${payload.secretPath} in the ${payload.environment} environment of ${payload.projectName}. - -The following permissions are requested: ${payload.permissions.join(", ")} + const projectUrl = `${appCfg.SITE_URL}${payload.projectPath}`; + const accessType = payload.isTemporary ? "temporary" : "permanent"; + const permissionsFormatted = payload.permissions.map((p) => `*${p}*`).join(", "); -View the request and approve or deny it <${payload.approvalUrl}|here>.${ - payload.note - ? ` -User Note: ${payload.note}` - : "" + const messageBody = `${payload.requesterFullName} (${payload.requesterEmail}) has requested ${accessType} access to ${payload.secretPath} in the ${payload.environment} environment of ${payload.projectName}.\n\nThe following permissions are requested: ${payload.permissions.join(", ")}${ + payload.note ? `\n\nUser note\n${payload.note}` : "" }`; - const payloadBlocks = [ + const headerBlocks = [ { type: "header", text: { @@ -104,17 +100,38 @@ User Note: ${payload.note}` text: "New access approval request pending for review", emoji: true } - }, + } + ]; + + const payloadBlocks = [ { type: "section", text: { type: "mrkdwn", - text: messageBody + text: `*${payload.requesterFullName}* (${payload.requesterEmail}) has requested *${accessType}* access to *${payload.secretPath}* in the *${payload.environment}* environment of *<${projectUrl}|${payload.projectName}>*.\n\nThe following permissions are requested: ${permissionsFormatted}${ + payload.note ? `\n\n*User note*\n${payload.note}` : "" + }` } + }, + { + type: "actions", + elements: [ + { + type: "button", + text: { + type: "plain_text", + text: "View request", + emoji: true + }, + style: "primary", + url: payload.approvalUrl + } + ] } ]; return { + headerBlocks, payloadMessage: messageBody, payloadBlocks, color: COMPANY_BRAND_COLOR @@ -125,7 +142,7 @@ User Note: ${payload.note}` const messageBody = `${payload.editorFullName} (${payload.editorEmail}) has updated the ${ payload.isTemporary ? "temporary" : "permanent" } access request from ${payload.requesterFullName} (${payload.requesterEmail}) to ${payload.secretPath} in the ${payload.environment} environment of ${payload.projectName}. - + The following permissions are requested: ${payload.permissions.join(", ")} View the request and approve or deny it <${payload.approvalUrl}|here>.${ @@ -154,6 +171,7 @@ Editor Note: ${payload.editNote}` ]; return { + headerBlocks: [], payloadMessage: messageBody, payloadBlocks, color: COMPANY_BRAND_COLOR @@ -161,24 +179,26 @@ Editor Note: ${payload.editNote}` } case TriggerFeature.SECRET_SYNC_ERROR: { const { payload } = notification; - const messageBody = `${payload.syncName} for ${payload.syncDestination} failed on ${payload.syncActionLabel} + const projectUrl = `${appCfg.SITE_URL}${payload.projectPath}`; + const messageBody = `Secret sync ${payload.syncName} for ${payload.syncDestination} failed on ${payload.syncActionLabel}\n\n\nEnvironment: ${payload.environment}\n\n\nSecret Path: ${payload.secretPath}\n\n\nProject: ${payload.projectName} (${projectUrl})\n\n\nReason:\n${payload.failureMessage}`; -Sync Error: ${payload.failureMessage}`; - - const payloadBlocks = [ + const headerBlocks = [ { type: "header", text: { type: "plain_text", - text: `${payload.syncName} for ${payload.syncDestination} failed on ${payload.syncActionLabel}`, + text: `Secret sync ${payload.syncName} for ${payload.syncDestination} failed on ${payload.syncActionLabel}`, emoji: true } - }, + } + ]; + + const payloadBlocks = [ { type: "section", text: { type: "mrkdwn", - text: `*Sync Error:* ${payload.failureMessage}` + text: `*Environment*\n${payload.environment}\n\n\n*Secret Path*\n${payload.secretPath}\n\n\n*Project*\n<${projectUrl}|${payload.projectName}>\n\n\n*Reason*\n${payload.failureMessage}` } }, { @@ -188,9 +208,10 @@ Sync Error: ${payload.failureMessage}`; type: "button", text: { type: "plain_text", - text: `Open ${payload.syncName}`, + text: "Open secret sync", emoji: true }, + style: "primary", url: payload.syncUrl } ] @@ -199,6 +220,7 @@ Sync Error: ${payload.failureMessage}`; return { payloadMessage: messageBody, + headerBlocks, payloadBlocks, color: ERROR_COLOR }; @@ -227,14 +249,16 @@ export const sendSlackNotification = async ({ }).toString("utf8"); const slackWebClient = new WebClient(botKey); - const { payloadMessage, payloadBlocks, color } = buildSlackPayload(notification); + const { payloadMessage, payloadBlocks, color, headerBlocks } = buildSlackPayload(notification); for await (const conversationId of targetChannelIds) { // we send both text and blocks for compatibility with barebone clients + await slackWebClient.chat .postMessage({ channel: conversationId, text: payloadMessage, + blocks: headerBlocks, attachments: [ { color, diff --git a/frontend/src/hooks/api/workflowIntegrations/types.ts b/frontend/src/hooks/api/workflowIntegrations/types.ts index 668850124..7c51ae259 100644 --- a/frontend/src/hooks/api/workflowIntegrations/types.ts +++ b/frontend/src/hooks/api/workflowIntegrations/types.ts @@ -86,6 +86,8 @@ export type ProjectWorkflowIntegrationConfig = accessRequestChannels: string; isSecretRequestNotificationEnabled: boolean; secretRequestChannels: string; + isSecretSyncErrorNotificationEnabled: boolean; + secretSyncErrorChannels: string; } | { id: string; @@ -112,6 +114,8 @@ export type TUpdateProjectWorkflowIntegrationConfigDTO = accessRequestChannels: string; isSecretRequestNotificationEnabled: boolean; secretRequestChannels: string; + isSecretSyncErrorNotificationEnabled: boolean; + secretSyncErrorChannels: string; } | { integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS; diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/WorkflowIntegrationTab.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/WorkflowIntegrationTab.tsx index fb48717cc..2dbb43fe6 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/WorkflowIntegrationTab.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/WorkflowIntegrationTab.tsx @@ -111,7 +111,7 @@ export const WorkflowIntegrationTab = () => { Provider Access Request Notifications Destination Secret Request Notifications Destination - + Secret Sync Error Notifications Destination diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsConfigRow.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsConfigRow.tsx index ef7617d52..ff1f2622b 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsConfigRow.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsConfigRow.tsx @@ -92,7 +92,9 @@ export const MicrosoftTeamsConfigRow = ({ Disabled )} - + + Coming Soon + diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackConfigRow.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackConfigRow.tsx index df7211f99..418d7982d 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackConfigRow.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackConfigRow.tsx @@ -89,7 +89,22 @@ export const SlackConfigRow = ({ handlePopUpOpen, isSlackConfigLoading, slackCon Disabled )} - + + {slackConfig.isSecretSyncErrorNotificationEnabled && + !isLoadingConfig && + slackConfig.secretSyncErrorChannels.length > 0 ? ( + + {slackConfig.secretSyncErrorChannels + .split(", ") + .map((channel) => slackChannelIdToName[channel]) + .join(", ")} + + ) : isLoadingConfig ? ( + + ) : ( + Disabled + )} + diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx index bcc2d81b5..d73206969 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx @@ -37,7 +37,9 @@ const formSchema = z.object({ isSecretRequestNotificationEnabled: z.boolean(), secretRequestChannels: z.string().array(), isAccessRequestNotificationEnabled: z.boolean(), - accessRequestChannels: z.string().array() + accessRequestChannels: z.string().array(), + isSecretSyncErrorNotificationEnabled: z.boolean(), + secretSyncErrorChannels: z.string().array() }); type TSlackConfigForm = z.infer; @@ -71,7 +73,9 @@ export const SlackIntegrationForm = ({ onClose }: Props) => { isAccessRequestNotificationEnabled: false, accessRequestChannels: [], isSecretRequestNotificationEnabled: false, - secretRequestChannels: [] + secretRequestChannels: [], + isSecretSyncErrorNotificationEnabled: false, + secretSyncErrorChannels: [] } }); @@ -87,7 +91,8 @@ export const SlackIntegrationForm = ({ onClose }: Props) => { integration: WorkflowIntegrationPlatform.SLACK, integrationId: data.slackIntegrationId, accessRequestChannels: data.accessRequestChannels.filter(Boolean).join(", "), - secretRequestChannels: data.secretRequestChannels.filter(Boolean).join(", ") + secretRequestChannels: data.secretRequestChannels.filter(Boolean).join(", "), + secretSyncErrorChannels: data.secretSyncErrorChannels.filter(Boolean).join(", ") }); createNotification({ @@ -107,6 +112,7 @@ export const SlackIntegrationForm = ({ onClose }: Props) => { const secretRequestNotifState = watch("isSecretRequestNotificationEnabled"); const selectedSlackIntegrationId = watch("slackIntegrationId"); const accessRequestNotifState = watch("isAccessRequestNotificationEnabled"); + const secretSyncErrorNotifState = watch("isSecretSyncErrorNotificationEnabled"); const { data: slackChannels } = useGetSlackIntegrationChannels(selectedSlackIntegrationId); const slackChannelIdToName = Object.fromEntries( @@ -117,7 +123,7 @@ export const SlackIntegrationForm = ({ onClose }: Props) => { ); useEffect(() => { - if (slackConfig) { + if (slackConfig && slackConfig.integration === WorkflowIntegrationPlatform.SLACK) { setValue("slackIntegrationId", slackConfig.integrationId); setValue( "isSecretRequestNotificationEnabled", @@ -127,22 +133,30 @@ export const SlackIntegrationForm = ({ onClose }: Props) => { "isAccessRequestNotificationEnabled", slackConfig.isAccessRequestNotificationEnabled ); + setValue( + "isSecretSyncErrorNotificationEnabled", + slackConfig.isSecretSyncErrorNotificationEnabled + ); - if (slackConfig.integration === WorkflowIntegrationPlatform.SLACK) { - if (slackChannels) { - setValue( - "secretRequestChannels", - slackConfig.secretRequestChannels - .split(", ") - .filter((channel) => channel in slackChannelIdToName) - ); - setValue( - "accessRequestChannels", - slackConfig.accessRequestChannels - .split(", ") - .filter((channel) => channel in slackChannelIdToName) - ); - } + if (slackChannels) { + setValue( + "secretRequestChannels", + slackConfig.secretRequestChannels + .split(", ") + .filter((channel) => channel in slackChannelIdToName) + ); + setValue( + "accessRequestChannels", + slackConfig.accessRequestChannels + .split(", ") + .filter((channel) => channel in slackChannelIdToName) + ); + setValue( + "secretSyncErrorChannels", + slackConfig.secretSyncErrorChannels + .split(", ") + .filter((channel) => channel in slackChannelIdToName) + ); } } }, [slackConfig, slackChannels]); @@ -215,54 +229,14 @@ export const SlackIntegrationForm = ({ onClose }: Props) => { control={control} name="secretRequestChannels" render={({ field: { value, onChange }, fieldState: { error } }) => ( - - - - slackChannelIdToName[entry]) - .join(", ")} - className="text-left" - /> - - - {sortedSlackChannels?.map((slackChannel) => { - const isChecked = value?.includes(slackChannel.id); - return ( - { - evt.preventDefault(); - onChange( - isChecked - ? value?.filter((el: string) => el !== slackChannel.id) - : [...(value || []), slackChannel.id] - ); - }} - key={`secret-requests-slack-channel-${slackChannel.id}`} - iconPos="right" - icon={isChecked && } - > - {slackChannel.name} - - ); - })} - - - + )} /> )} @@ -288,54 +262,47 @@ export const SlackIntegrationForm = ({ onClose }: Props) => { control={control} name="accessRequestChannels" render={({ field: { value, onChange }, fieldState: { error } }) => ( - - - - slackChannelIdToName[entry]) - .join(", ")} - className="text-left" - /> - - - {sortedSlackChannels?.map((slackChannel) => { - const isChecked = value?.includes(slackChannel.id); - return ( - { - evt.preventDefault(); - onChange( - isChecked - ? value?.filter((el: string) => el !== slackChannel.id) - : [...(value || []), slackChannel.id] - ); - }} - key={`access-requests-slack-channel-${slackChannel.id}`} - iconPos="right" - icon={isChecked && } - > - {slackChannel.name} - - ); - })} - - + + )} + /> + )} + { + return ( + + field.onChange(value)} + isChecked={field.value} + > +

Secret Sync Errors

+
+ ); + }} + /> + {secretSyncErrorNotifState && ( + ( + )} /> )} @@ -353,3 +320,63 @@ export const SlackIntegrationForm = ({ onClose }: Props) => { ); }; + +type TChannelSelectorProps = { + value: string[]; + onChange: (value: string[]) => void; + error?: { message?: string }; + slackChannelIdToName: Record; + sortedSlackChannels?: { id: string; name: string }[]; + keyPrefix: string; +}; + +const ChannelSelector = ({ + value, + onChange, + error, + slackChannelIdToName, + sortedSlackChannels, + keyPrefix +}: TChannelSelectorProps) => ( + + + + slackChannelIdToName[entry]).join(", ")} + className="text-left" + /> + + + {sortedSlackChannels?.map((slackChannel) => { + const isChecked = value?.includes(slackChannel.id); + return ( + { + evt.preventDefault(); + onChange( + isChecked + ? value?.filter((el: string) => el !== slackChannel.id) + : [...(value || []), slackChannel.id] + ); + }} + key={`${keyPrefix}-slack-channel-${slackChannel.id}`} + iconPos="right" + icon={isChecked && } + > + {slackChannel.name} + + ); + })} + + + +); \ No newline at end of file From 16cf4a91c00df3fbbf8212b9940bdece752648b2 Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Mon, 27 Oct 2025 09:44:09 -0300 Subject: [PATCH 005/294] fix(slack-integration): improve channel handling by filtering empty values and update Microsoft Teams badge status --- .../workflow-integrations/notification-handlers/slack.ts | 6 +++--- .../components/MicrosoftTeamsConfigRow.tsx | 2 +- .../components/SlackIntegrationForm.tsx | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/src/lib/workflow-integrations/notification-handlers/slack.ts b/backend/src/lib/workflow-integrations/notification-handlers/slack.ts index 66fd58b3a..b55f172cb 100644 --- a/backend/src/lib/workflow-integrations/notification-handlers/slack.ts +++ b/backend/src/lib/workflow-integrations/notification-handlers/slack.ts @@ -25,15 +25,15 @@ const handleSlackNotification = async ({ switch (notification.type) { case TriggerFeature.ACCESS_REQUEST: case TriggerFeature.ACCESS_REQUEST_UPDATED: - targetChannelIds = slackConfig.accessRequestChannels?.split(", ") || []; + targetChannelIds = slackConfig.accessRequestChannels?.split(", ").filter(Boolean) || []; isEnabled = slackConfig.isAccessRequestNotificationEnabled; break; case TriggerFeature.SECRET_APPROVAL: - targetChannelIds = slackConfig.secretRequestChannels?.split(", ") || []; + targetChannelIds = slackConfig.secretRequestChannels?.split(", ").filter(Boolean) || []; isEnabled = slackConfig.isSecretRequestNotificationEnabled; break; case TriggerFeature.SECRET_SYNC_ERROR: - targetChannelIds = slackConfig.secretSyncErrorChannels?.split(", ") || []; + targetChannelIds = slackConfig.secretSyncErrorChannels?.split(", ").filter(Boolean) || []; isEnabled = slackConfig.isSecretSyncErrorNotificationEnabled; break; default: diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsConfigRow.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsConfigRow.tsx index ff1f2622b..66a228ebe 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsConfigRow.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsConfigRow.tsx @@ -93,7 +93,7 @@ export const MicrosoftTeamsConfigRow = ({ )} - Coming Soon + Disabled diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx index d73206969..6092a465e 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx @@ -141,19 +141,19 @@ export const SlackIntegrationForm = ({ onClose }: Props) => { if (slackChannels) { setValue( "secretRequestChannels", - slackConfig.secretRequestChannels + (slackConfig.secretRequestChannels || "") .split(", ") .filter((channel) => channel in slackChannelIdToName) ); setValue( "accessRequestChannels", - slackConfig.accessRequestChannels + (slackConfig.accessRequestChannels || "") .split(", ") .filter((channel) => channel in slackChannelIdToName) ); setValue( "secretSyncErrorChannels", - slackConfig.secretSyncErrorChannels + (slackConfig.secretSyncErrorChannels || "") .split(", ") .filter((channel) => channel in slackChannelIdToName) ); From 66e7b44fef0eef0baf16580809a171d2b472d70c Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Mon, 27 Oct 2025 10:20:07 -0300 Subject: [PATCH 006/294] feat(secret-sync): add support for secret sync error notifications in project router and Slack integration, including channel selection enhancements --- .../routes/v1/deprecated-project-router.ts | 8 +- .../src/services/project/project-service.ts | 4 +- .../components/SlackIntegrationForm.tsx | 123 +++++++++--------- 3 files changed, 71 insertions(+), 64 deletions(-) diff --git a/backend/src/server/routes/v1/deprecated-project-router.ts b/backend/src/server/routes/v1/deprecated-project-router.ts index 687d95b9b..f6efdb78a 100644 --- a/backend/src/server/routes/v1/deprecated-project-router.ts +++ b/backend/src/server/routes/v1/deprecated-project-router.ts @@ -614,8 +614,10 @@ export const registerDeprecatedProjectRouter = async (server: FastifyZodProvider integrationId: z.string(), accessRequestChannels: validateSlackChannelsField, secretRequestChannels: validateSlackChannelsField, + secretSyncErrorChannels: validateSlackChannelsField, isAccessRequestNotificationEnabled: z.boolean(), - isSecretRequestNotificationEnabled: z.boolean() + isSecretRequestNotificationEnabled: z.boolean(), + isSecretSyncErrorNotificationEnabled: z.boolean() }), z.object({ integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS), @@ -633,7 +635,9 @@ export const registerDeprecatedProjectRouter = async (server: FastifyZodProvider isAccessRequestNotificationEnabled: true, accessRequestChannels: true, isSecretRequestNotificationEnabled: true, - secretRequestChannels: true + secretRequestChannels: true, + isSecretSyncErrorNotificationEnabled: true, + secretSyncErrorChannels: true }).merge( z.object({ integration: z.literal(WorkflowIntegration.SLACK), diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 3c6d541ce..19a13f45b 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -1573,8 +1573,8 @@ export const projectServiceFactory = ({ isSecretSyncErrorNotificationEnabled }: TUpdateProjectWorkflowIntegration & { // workaround intersection type while we don't have the microsoft teams integration for failed secret syncs - isSecretSyncErrorNotificationEnabled: boolean; - secretSyncErrorChannels: string; + isSecretSyncErrorNotificationEnabled?: boolean; + secretSyncErrorChannels?: string; }) => { const project = await projectDAL.findById(projectId); if (!project) { diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx index 6092a465e..cf5113fad 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx @@ -44,6 +44,69 @@ const formSchema = z.object({ type TSlackConfigForm = z.infer; +type TChannelSelectorProps = { + value: string[]; + onChange: (value: string[]) => void; + error?: { message?: string }; + slackChannelIdToName: Record; + sortedSlackChannels?: { id: string; name: string }[]; + keyPrefix: string; +}; + +const ChannelSelector = ({ + value, + onChange, + error, + slackChannelIdToName, + sortedSlackChannels, + keyPrefix +}: TChannelSelectorProps) => ( + + + + slackChannelIdToName[entry]) + .join(", ")} + className="text-left" + /> + + + {sortedSlackChannels?.map((slackChannel) => { + const isChecked = value?.includes(slackChannel.id); + return ( + { + evt.preventDefault(); + onChange( + isChecked + ? value?.filter((el: string) => el !== slackChannel.id) + : [...(value || []), slackChannel.id] + ); + }} + key={`${keyPrefix}-slack-channel-${slackChannel.id}`} + iconPos="right" + icon={isChecked && } + > + {slackChannel.name} + + ); + })} + + + +); + type Props = { onClose: () => void; }; @@ -320,63 +383,3 @@ export const SlackIntegrationForm = ({ onClose }: Props) => { ); }; - -type TChannelSelectorProps = { - value: string[]; - onChange: (value: string[]) => void; - error?: { message?: string }; - slackChannelIdToName: Record; - sortedSlackChannels?: { id: string; name: string }[]; - keyPrefix: string; -}; - -const ChannelSelector = ({ - value, - onChange, - error, - slackChannelIdToName, - sortedSlackChannels, - keyPrefix -}: TChannelSelectorProps) => ( - - - - slackChannelIdToName[entry]).join(", ")} - className="text-left" - /> - - - {sortedSlackChannels?.map((slackChannel) => { - const isChecked = value?.includes(slackChannel.id); - return ( - { - evt.preventDefault(); - onChange( - isChecked - ? value?.filter((el: string) => el !== slackChannel.id) - : [...(value || []), slackChannel.id] - ); - }} - key={`${keyPrefix}-slack-channel-${slackChannel.id}`} - iconPos="right" - icon={isChecked && } - > - {slackChannel.name} - - ); - })} - - - -); \ No newline at end of file From 83da0dd3d98c67197084153e49090e09007b2771 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Sat, 1 Nov 2025 00:46:27 +0800 Subject: [PATCH 007/294] misc: improved monitoring and telemetry docs --- .../guides/monitoring-telemetry.mdx | 763 ++++++++++-------- 1 file changed, 408 insertions(+), 355 deletions(-) diff --git a/docs/self-hosting/guides/monitoring-telemetry.mdx b/docs/self-hosting/guides/monitoring-telemetry.mdx index b23c51b27..441a78ac0 100644 --- a/docs/self-hosting/guides/monitoring-telemetry.mdx +++ b/docs/self-hosting/guides/monitoring-telemetry.mdx @@ -27,7 +27,9 @@ Both approaches provide the same metrics data in OTEL format, so you can choose - Access to deploy monitoring services (Prometheus, Grafana, etc.) - Basic understanding of Prometheus and Grafana -## Environment Variables +## Setup + +### Environment Variables Configure the following environment variables in your Infisical backend: @@ -37,287 +39,282 @@ OTEL_TELEMETRY_COLLECTION_ENABLED=true # Choose export type: "prometheus" or "otlp" OTEL_EXPORT_TYPE=prometheus - -# For OTLP push mode, also configure: -# OTEL_EXPORT_OTLP_ENDPOINT=http://otel-collector:4318/v1/metrics -# OTEL_COLLECTOR_BASIC_AUTH_USERNAME=your_collector_username -# OTEL_COLLECTOR_BASIC_AUTH_PASSWORD=your_collector_password -# OTEL_OTLP_PUSH_INTERVAL=30000 ``` -**Note**: The `OTEL_COLLECTOR_BASIC_AUTH_USERNAME` and `OTEL_COLLECTOR_BASIC_AUTH_PASSWORD` values must match the credentials configured in your OpenTelemetry Collector's `basicauth/server` extension. These are not hardcoded values - you configure them in your collector configuration file. + + + This approach exposes metrics on port 9464 at the `/metrics` endpoint, allowing Prometheus to scrape the data. The metrics are exposed in Prometheus format but originate from OpenTelemetry instrumentation. -## Option 1: Pull-based Monitoring (Prometheus) + ### Configuration -This approach exposes metrics on port 9464 at the `/metrics` endpoint, allowing Prometheus to scrape the data. The metrics are exposed in Prometheus format but originate from OpenTelemetry instrumentation. + 1. **Enable Prometheus export in Infisical**: -### Configuration + ```bash + OTEL_TELEMETRY_COLLECTION_ENABLED=true + OTEL_EXPORT_TYPE=prometheus + ``` -1. **Enable Prometheus export in Infisical**: + 2. **Expose the metrics port** in your Infisical backend: - ```bash - OTEL_TELEMETRY_COLLECTION_ENABLED=true - OTEL_EXPORT_TYPE=prometheus - ``` + - **Docker**: Expose port 9464 + - **Kubernetes**: Create a service exposing port 9464 + - **Other**: Ensure port 9464 is accessible to your monitoring stack -2. **Expose the metrics port** in your Infisical backend: + 3. **Create Prometheus configuration** (`prometheus.yml`): - - **Docker**: Expose port 9464 - - **Kubernetes**: Create a service exposing port 9464 - - **Other**: Ensure port 9464 is accessible to your monitoring stack + ```yaml + global: + scrape_interval: 30s + evaluation_interval: 30s -3. **Create Prometheus configuration** (`prometheus.yml`): + scrape_configs: + - job_name: "infisical" + scrape_interval: 30s + static_configs: + - targets: ["infisical-backend:9464"] # Adjust hostname/port based on your deployment + metrics_path: "/metrics" + ``` - ```yaml - global: - scrape_interval: 30s - evaluation_interval: 30s + **Note**: Replace `infisical-backend:9464` with the actual hostname and port where your Infisical backend is running. This could be: - scrape_configs: - - job_name: "infisical" - scrape_interval: 30s - static_configs: - - targets: ["infisical-backend:9464"] # Adjust hostname/port based on your deployment - metrics_path: "/metrics" - ``` + - **Docker Compose**: `infisical-backend:9464` (service name) + - **Kubernetes**: `infisical-backend.default.svc.cluster.local:9464` (service name) + - **Bare Metal**: `192.168.1.100:9464` (actual IP address) + - **Cloud**: `your-infisical.example.com:9464` (domain name) - **Note**: Replace `infisical-backend:9464` with the actual hostname and port where your Infisical backend is running. This could be: + ### Deployment Options - - **Docker Compose**: `infisical-backend:9464` (service name) - - **Kubernetes**: `infisical-backend.default.svc.cluster.local:9464` (service name) - - **Bare Metal**: `192.168.1.100:9464` (actual IP address) - - **Cloud**: `your-infisical.example.com:9464` (domain name) + + + ```yaml + services: + prometheus: + image: prom/prometheus:latest + ports: + - "9090:9090" + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro + command: + - "--config.file=/etc/prometheus/prometheus.yml" -### Deployment Options + grafana: + image: grafana/grafana:latest + ports: + - "3000:3000" + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=admin + ``` + + + ```yaml + # prometheus-deployment.yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: prometheus + spec: + replicas: 1 + selector: + matchLabels: + app: prometheus + template: + metadata: + labels: + app: prometheus + spec: + containers: + - name: prometheus + image: prom/prometheus:latest + ports: + - containerPort: 9090 + volumeMounts: + - name: config + mountPath: /etc/prometheus + volumes: + - name: config + configMap: + name: prometheus-config -#### Docker Compose - -```yaml -services: - prometheus: - image: prom/prometheus:latest - ports: - - "9090:9090" - volumes: - - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro - command: - - "--config.file=/etc/prometheus/prometheus.yml" - - grafana: - image: grafana/grafana:latest - ports: - - "3000:3000" - environment: - - GF_SECURITY_ADMIN_USER=admin - - GF_SECURITY_ADMIN_PASSWORD=admin -``` - -#### Kubernetes - -```yaml -# prometheus-deployment.yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: prometheus -spec: - replicas: 1 - selector: - matchLabels: - app: prometheus - template: - metadata: - labels: - app: prometheus - spec: - containers: - - name: prometheus - image: prom/prometheus:latest + --- + # prometheus-service.yaml + apiVersion: v1 + kind: Service + metadata: + name: prometheus + spec: + selector: + app: prometheus ports: - - containerPort: 9090 - volumeMounts: - - name: config - mountPath: /etc/prometheus - volumes: - - name: config - configMap: - name: prometheus-config + - port: 9090 + targetPort: 9090 + type: ClusterIP + ``` + + + ```bash + helm repo add prometheus-community https://prometheus-community.github.io/helm-charts + helm install prometheus prometheus-community/prometheus \ + --set server.config.global.scrape_interval=30s \ + --set server.config.scrape_configs[0].job_name=infisical \ + --set server.config.scrape_configs[0].static_configs[0].targets[0]=infisical-backend:9464 + ``` + + ---- -# prometheus-service.yaml -apiVersion: v1 -kind: Service -metadata: - name: prometheus -spec: - selector: - app: prometheus - ports: - - port: 9090 - targetPort: 9090 - type: ClusterIP -``` + + + This approach sends metrics directly to an OpenTelemetry Collector via the OTLP protocol. This gives you the most flexibility as you can configure the collector to export to multiple backends simultaneously. -#### Helm + ### Configuration -```bash -helm repo add prometheus-community https://prometheus-community.github.io/helm-charts -helm install prometheus prometheus-community/prometheus \ - --set server.config.global.scrape_interval=30s \ - --set server.config.scrape_configs[0].job_name=infisical \ - --set server.config.scrape_configs[0].static_configs[0].targets[0]=infisical-backend:9464 -``` + 1. **Enable OTLP export in Infisical**: -## Option 2: Push-based Monitoring (OTLP) + ```bash + OTEL_TELEMETRY_COLLECTION_ENABLED=true + OTEL_EXPORT_TYPE=otlp + OTEL_EXPORT_OTLP_ENDPOINT=http://otel-collector:4318/v1/metrics + OTEL_COLLECTOR_BASIC_AUTH_USERNAME=infisical + OTEL_COLLECTOR_BASIC_AUTH_PASSWORD=infisical + OTEL_OTLP_PUSH_INTERVAL=30000 + ``` -This approach sends metrics directly to an OpenTelemetry Collector via the OTLP protocol. This gives you the most flexibility as you can configure the collector to export to multiple backends simultaneously. + 2. **Create OpenTelemetry Collector configuration** (`otel-collector-config.yaml`): -### Configuration + ```yaml + extensions: + health_check: + pprof: + zpages: + basicauth/server: + htpasswd: + inline: | + your_username:your_password -1. **Enable OTLP export in Infisical**: + receivers: + otlp: + protocols: + http: + endpoint: 0.0.0.0:4318 + auth: + authenticator: basicauth/server - ```bash - OTEL_TELEMETRY_COLLECTION_ENABLED=true - OTEL_EXPORT_TYPE=otlp - OTEL_EXPORT_OTLP_ENDPOINT=http://otel-collector:4318/v1/metrics - OTEL_COLLECTOR_BASIC_AUTH_USERNAME=infisical - OTEL_COLLECTOR_BASIC_AUTH_PASSWORD=infisical - OTEL_OTLP_PUSH_INTERVAL=30000 - ``` + prometheus: + config: + scrape_configs: + - job_name: otel-collector + scrape_interval: 30s + static_configs: + - targets: [infisical-backend:9464] + metric_relabel_configs: + - action: labeldrop + regex: "service_instance_id|service_name" -2. **Create OpenTelemetry Collector configuration** (`otel-collector-config.yaml`): + processors: + batch: - ```yaml - extensions: - health_check: - pprof: - zpages: - basicauth/server: - htpasswd: - inline: | - your_username:your_password - - receivers: - otlp: - protocols: - http: - endpoint: 0.0.0.0:4318 + exporters: + prometheus: + endpoint: "0.0.0.0:8889" auth: authenticator: basicauth/server + resource_to_telemetry_conversion: + enabled: true - prometheus: - config: - scrape_configs: - - job_name: otel-collector - scrape_interval: 30s - static_configs: - - targets: [infisical-backend:9464] - metric_relabel_configs: - - action: labeldrop - regex: "service_instance_id|service_name" + service: + extensions: [basicauth/server, health_check, pprof, zpages] + pipelines: + metrics: + receivers: [otlp] + processors: [batch] + exporters: [prometheus] + ``` - processors: - batch: + **Important**: Replace `your_username:your_password` with your chosen credentials. These must match the values you set in Infisical's `OTEL_COLLECTOR_BASIC_AUTH_USERNAME` and `OTEL_COLLECTOR_BASIC_AUTH_PASSWORD` environment variables. - exporters: - prometheus: - endpoint: "0.0.0.0:8889" - auth: - authenticator: basicauth/server - resource_to_telemetry_conversion: - enabled: true + 3. **Create Prometheus configuration** for the collector: - service: - extensions: [basicauth/server, health_check, pprof, zpages] - pipelines: - metrics: - receivers: [otlp] - processors: [batch] - exporters: [prometheus] - ``` + ```yaml + global: + scrape_interval: 30s + evaluation_interval: 30s - **Important**: Replace `your_username:your_password` with your chosen credentials. These must match the values you set in Infisical's `OTEL_COLLECTOR_BASIC_AUTH_USERNAME` and `OTEL_COLLECTOR_BASIC_AUTH_PASSWORD` environment variables. + scrape_configs: + - job_name: "otel-collector" + scrape_interval: 30s + static_configs: + - targets: ["otel-collector:8889"] # Adjust hostname/port based on your deployment + metrics_path: "/metrics" + ``` -3. **Create Prometheus configuration** for the collector: + **Note**: Replace `otel-collector:8889` with the actual hostname and port where your OpenTelemetry Collector is running. This could be: - ```yaml - global: - scrape_interval: 30s - evaluation_interval: 30s + - **Docker Compose**: `otel-collector:8889` (service name) + - **Kubernetes**: `otel-collector.default.svc.cluster.local:8889` (service name) + - **Bare Metal**: `192.168.1.100:8889` (actual IP address) + - **Cloud**: `your-collector.example.com:8889` (domain name) - scrape_configs: - - job_name: "otel-collector" - scrape_interval: 30s - static_configs: - - targets: ["otel-collector:8889"] # Adjust hostname/port based on your deployment - metrics_path: "/metrics" - ``` + ### Deployment Options - **Note**: Replace `otel-collector:8889` with the actual hostname and port where your OpenTelemetry Collector is running. This could be: + + + ```yaml + services: + otel-collector: + image: otel/opentelemetry-collector-contrib:latest + ports: + - 4318:4318 # OTLP http receiver + - 8889:8889 # Prometheus exporter metrics + volumes: + - ./otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml:ro + command: + - "--config=/etc/otelcol-contrib/config.yaml" + ``` + + + ```yaml + # otel-collector-deployment.yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: otel-collector + spec: + replicas: 1 + selector: + matchLabels: + app: otel-collector + template: + metadata: + labels: + app: otel-collector + spec: + containers: + - name: otel-collector + image: otel/opentelemetry-collector-contrib:latest + ports: + - containerPort: 4318 + - containerPort: 8889 + volumeMounts: + - name: config + mountPath: /etc/otelcol-contrib + volumes: + - name: config + configMap: + name: otel-collector-config + ``` + + + ```bash + helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts + helm install otel-collector open-telemetry/opentelemetry-collector \ + --set config.receivers.otlp.protocols.http.endpoint=0.0.0.0:4318 \ + --set config.exporters.prometheus.endpoint=0.0.0.0:8889 + ``` + + - - **Docker Compose**: `otel-collector:8889` (service name) - - **Kubernetes**: `otel-collector.default.svc.cluster.local:8889` (service name) - - **Bare Metal**: `192.168.1.100:8889` (actual IP address) - - **Cloud**: `your-collector.example.com:8889` (domain name) - -### Deployment Options - -#### Docker Compose - -```yaml -services: - otel-collector: - image: otel/opentelemetry-collector-contrib:latest - ports: - - 4318:4318 # OTLP http receiver - - 8889:8889 # Prometheus exporter metrics - volumes: - - ./otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml:ro - command: - - "--config=/etc/otelcol-contrib/config.yaml" -``` - -#### Kubernetes - -```yaml -# otel-collector-deployment.yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: otel-collector -spec: - replicas: 1 - selector: - matchLabels: - app: otel-collector - template: - metadata: - labels: - app: otel-collector - spec: - containers: - - name: otel-collector - image: otel/opentelemetry-collector-contrib:latest - ports: - - containerPort: 4318 - - containerPort: 8889 - volumeMounts: - - name: config - mountPath: /etc/otelcol-contrib - volumes: - - name: config - configMap: - name: otel-collector-config -``` - -#### Helm - -```bash -helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts -helm install otel-collector open-telemetry/opentelemetry-collector \ - --set config.receivers.otlp.protocols.http.endpoint=0.0.0.0:4318 \ - --set config.exporters.prometheus.endpoint=0.0.0.0:8889 -``` + + ## Available Metrics @@ -327,154 +324,210 @@ Infisical exposes the following key metrics in OpenTelemetry format: These metrics track all HTTP API requests to Infisical, including request counts, latency, and errors. Use these to monitor overall API health, identify performance bottlenecks, and track usage patterns across users and machine identities. -#### Total API Requests + + + **Metric Name**: `infisical.http.server.request.count` -- **Metric Name**: `infisical.http.server.request.count` -- **Type**: Counter -- **Unit**: `{request}` -- **Description**: Total number of API requests to Infisical (covers both human users and machine identities) -- **Attributes**: - - `infisical.organization.id` (string): Organization ID - - `infisical.organization.name` (string): Organization name (e.g., "Platform Engineering Team") - - `infisical.user.id` (string, optional): User ID if human user - - `infisical.user.email` (string, optional): User email (e.g., "jane.doe@cisco.com") - - `infisical.identity.id` (string, optional): Machine identity ID - - `infisical.identity.name` (string, optional): Machine identity name (e.g., "prod-k8s-operator") - - `infisical.auth.method` (string, optional): Auth method used - - `http.request.method` (string): HTTP method (GET, POST, PUT, DELETE) - - `http.route` (string): API endpoint route pattern - - `http.response.status_code` (int): HTTP status code - - `infisical.project.id` (string, optional): Project ID - - `infisical.project.name` (string, optional): Project name - - `user_agent.original` (string, optional): User agent string - - `client.address` (string, optional): IP address + **Type**: Counter -#### Request Duration + **Unit**: `{request}` -- **Metric Name**: `infisical.http.server.request.duration` -- **Type**: Histogram -- **Unit**: `s` (seconds) -- **Description**: API request latency -- **Buckets**: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10] -- **Attributes**: - - `infisical.organization.id` (string): Organization ID - - `infisical.organization.name` (string): Organization name - - `infisical.user.id` (string, optional): User ID if human user - - `infisical.user.email` (string, optional): User email - - `infisical.identity.id` (string, optional): Machine identity ID - - `infisical.identity.name` (string, optional): Machine identity name - - `http.request.method` (string): HTTP method - - `http.route` (string): API endpoint route pattern - - `http.response.status_code` (int): HTTP status code - - `infisical.project.id` (string, optional): Project ID - - `infisical.project.name` (string, optional): Project name + **Description**: Total number of API requests to Infisical (covers both human users and machine identities) -#### API Errors by Actor + **Attributes**: + - `infisical.organization.id` (string): Organization ID + - `infisical.organization.name` (string): Organization name (e.g., "Platform Engineering Team") + - `infisical.user.id` (string, optional): User ID if human user + - `infisical.user.email` (string, optional): User email (e.g., "jane.doe@cisco.com") + - `infisical.identity.id` (string, optional): Machine identity ID + - `infisical.identity.name` (string, optional): Machine identity name (e.g., "prod-k8s-operator") + - `infisical.auth.method` (string, optional): Auth method used + - `http.request.method` (string): HTTP method (GET, POST, PUT, DELETE) + - `http.route` (string): API endpoint route pattern + - `http.response.status_code` (int): HTTP status code + - `infisical.project.id` (string, optional): Project ID + - `infisical.project.name` (string, optional): Project name + - `user_agent.original` (string, optional): User agent string + - `client.address` (string, optional): IP address + -- **Metric Name**: `infisical.http.server.error.count` -- **Type**: Counter -- **Unit**: `{error}` -- **Description**: API errors grouped by actor (for identifying misconfigured services) -- **Attributes**: - - `infisical.organization.id` (string): Organization ID - - `infisical.organization.name` (string): Organization name - - `infisical.user.id` (string, optional): User ID if human - - `infisical.user.email` (string, optional): User email - - `infisical.identity.id` (string, optional): Identity ID if machine - - `infisical.identity.name` (string, optional): Identity name - - `http.route` (string): API endpoint where error occurred - - `http.request.method` (string): HTTP method - - `error.type` (string): Error category/type (client_error, server_error, auth_error, rate_limit_error, etc.) - - `infisical.project.id` (string, optional): Project ID - - `infisical.project.name` (string, optional): Project name - - `client.address` (string, optional): IP address - - `user_agent.original` (string, optional): User agent information + + **Metric Name**: `infisical.http.server.request.duration` + + **Type**: Histogram + + **Unit**: `s` (seconds) + + **Description**: API request latency + + **Buckets**: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10] + + **Attributes**: + - `infisical.organization.id` (string): Organization ID + - `infisical.organization.name` (string): Organization name + - `infisical.user.id` (string, optional): User ID if human user + - `infisical.user.email` (string, optional): User email + - `infisical.identity.id` (string, optional): Machine identity ID + - `infisical.identity.name` (string, optional): Machine identity name + - `http.request.method` (string): HTTP method + - `http.route` (string): API endpoint route pattern + - `http.response.status_code` (int): HTTP status code + - `infisical.project.id` (string, optional): Project ID + - `infisical.project.name` (string, optional): Project name + + + + **Metric Name**: `infisical.http.server.error.count` + + **Type**: Counter + + **Unit**: `{error}` + + **Description**: API errors grouped by actor (for identifying misconfigured services) + + **Attributes**: + - `infisical.organization.id` (string): Organization ID + - `infisical.organization.name` (string): Organization name + - `infisical.user.id` (string, optional): User ID if human + - `infisical.user.email` (string, optional): User email + - `infisical.identity.id` (string, optional): Identity ID if machine + - `infisical.identity.name` (string, optional): Identity name + - `http.route` (string): API endpoint where error occurred + - `http.request.method` (string): HTTP method + - `error.type` (string): Error category/type (client_error, server_error, auth_error, rate_limit_error, etc.) + - `infisical.project.id` (string, optional): Project ID + - `infisical.project.name` (string, optional): Project name + - `client.address` (string, optional): IP address + - `user_agent.original` (string, optional): User agent information + + ### Secret Operations Metrics These metrics provide visibility into secret access patterns, helping you understand which secrets are being accessed, by whom, and from where. Essential for security auditing and access pattern analysis. -#### Secret Read Operations - -- **Metric Name**: `infisical.secret.read.count` -- **Type**: Counter -- **Unit**: `{operation}` -- **Description**: Number of secret read operations -- **Attributes**: - - `infisical.organization.id` (string): Organization ID - - `infisical.organization.name` (string): Organization name - - `infisical.project.id` (string): Project ID - - `infisical.project.name` (string): Project name (e.g., "payment-service-secrets") - - `infisical.environment` (string): Environment (dev, staging, prod) - - `infisical.secret.path` (string): Path to secrets (e.g., "/microservice-a/database") - - `infisical.secret.name` (string, optional): Name of secret - - `infisical.user.id` (string, optional): User ID if human - - `infisical.user.email` (string, optional): User email - - `infisical.identity.id` (string, optional): Machine identity ID - - `infisical.identity.name` (string, optional): Machine identity name - - `user_agent.original` (string, optional): User agent/SDK information - - `client.address` (string, optional): IP address + + + **Metric Name**: `infisical.secret.read.count` + + **Type**: Counter + + **Unit**: `{operation}` + + **Description**: Number of secret read operations + + **Attributes**: + - `infisical.organization.id` (string): Organization ID + - `infisical.organization.name` (string): Organization name + - `infisical.project.id` (string): Project ID + - `infisical.project.name` (string): Project name (e.g., "payment-service-secrets") + - `infisical.environment` (string): Environment (dev, staging, prod) + - `infisical.secret.path` (string): Path to secrets (e.g., "/microservice-a/database") + - `infisical.secret.name` (string, optional): Name of secret + - `infisical.user.id` (string, optional): User ID if human + - `infisical.user.email` (string, optional): User email + - `infisical.identity.id` (string, optional): Machine identity ID + - `infisical.identity.name` (string, optional): Machine identity name + - `user_agent.original` (string, optional): User agent/SDK information + - `client.address` (string, optional): IP address + + ### Authentication Metrics These metrics track authentication attempts and outcomes, enabling you to monitor login success rates, detect potential security threats, and identify authentication issues. -#### Login Attempts - -- **Metric Name**: `infisical.auth.attempt.count` -- **Type**: Counter -- **Unit**: `{attempt}` -- **Description**: Authentication attempts (both successful and failed) -- **Attributes**: - - `infisical.organization.id` (string): Organization ID - - `infisical.organization.name` (string): Organization name - - `infisical.user.id` (string, optional): User ID if human (if identifiable) - - `infisical.user.email` (string, optional): User email (if identifiable) - - `infisical.identity.id` (string, optional): Identity ID if machine (if identifiable) - - `infisical.identity.name` (string, optional): Identity name (if identifiable) - - `infisical.auth.method` (string): Authentication method attempted - - `infisical.auth.result` (string): success or failure - - `error.type` (string, optional): Reason for failure if failed (invalid_credentials, expired_token, invalid_token, etc.) - - `client.address` (string): IP address - - `user_agent.original` (string, optional): User agent/client information - - `infisical.auth.attempt.username` (string, optional): Attempted username/email (if available) + + + **Metric Name**: `infisical.auth.attempt.count` + + **Type**: Counter + + **Unit**: `{attempt}` + + **Description**: Authentication attempts (both successful and failed) + + **Attributes**: + - `infisical.organization.id` (string): Organization ID + - `infisical.organization.name` (string): Organization name + - `infisical.user.id` (string, optional): User ID if human (if identifiable) + - `infisical.user.email` (string, optional): User email (if identifiable) + - `infisical.identity.id` (string, optional): Identity ID if machine (if identifiable) + - `infisical.identity.name` (string, optional): Identity name (if identifiable) + - `infisical.auth.method` (string): Authentication method attempted + - `infisical.auth.result` (string): success or failure + - `error.type` (string, optional): Reason for failure if failed (invalid_credentials, expired_token, invalid_token, etc.) + - `client.address` (string): IP address + - `user_agent.original` (string, optional): User agent/client information + - `infisical.auth.attempt.username` (string, optional): Attempted username/email (if available) + + ### Legacy Metrics These metrics are from the previous instrumentation and may be deprecated in future versions. Consider migrating to the new Core API Metrics for more comprehensive observability. -- `API_latency` - API request latency histogram in milliseconds (Labels: `route`, `method`, `statusCode`) -- `API_errors` - API error count histogram (Labels: `route`, `method`, `type`, `name`) + + + API request latency histogram in milliseconds + + - **Labels**: `route`, `method`, `statusCode` + + + + API error count histogram + + - **Labels**: `route`, `method`, `type`, `name` + + ### Integration & Secret Sync Metrics These metrics monitor secret synchronization operations between Infisical and external systems, helping you track sync health, identify integration failures, and troubleshoot connectivity issues. -- `integration_secret_sync_errors` - Integration secret sync error count + + + Integration secret sync error count - - **Labels**: `version`, `integration`, `integrationId`, `type`, `status`, `name`, `projectId` - - **Example**: Monitor integration sync failures across different services + - **Labels**: `version`, `integration`, `integrationId`, `type`, `status`, `name`, `projectId` + - **Example**: Monitor integration sync failures across different services + -- `secret_sync_sync_secrets_errors` - Secret sync operation error count + + Secret sync operation error count - - **Labels**: `version`, `destination`, `syncId`, `projectId`, `type`, `status`, `name` - - **Example**: Track secret sync failures to external systems + - **Labels**: `version`, `destination`, `syncId`, `projectId`, `type`, `status`, `name` + - **Example**: Track secret sync failures to external systems + -- `secret_sync_import_secrets_errors` - Secret import operation error count + + Secret import operation error count - - **Labels**: `version`, `destination`, `syncId`, `projectId`, `type`, `status`, `name` - - **Example**: Monitor secret import failures + - **Labels**: `version`, `destination`, `syncId`, `projectId`, `type`, `status`, `name` + - **Example**: Monitor secret import failures + -- `secret_sync_remove_secrets_errors` - Secret removal operation error count - - **Labels**: `version`, `destination`, `syncId`, `projectId`, `type`, `status`, `name` - - **Example**: Track secret removal operation failures + + Secret removal operation error count + + - **Labels**: `version`, `destination`, `syncId`, `projectId`, `type`, `status`, `name` + - **Example**: Track secret removal operation failures + + ### System Metrics These low-level HTTP metrics are automatically collected by OpenTelemetry's instrumentation layer, providing baseline performance data for all HTTP traffic. -- `http_server_duration` - HTTP server request duration metrics (histogram buckets, count, sum) -- `http_client_duration` - HTTP client request duration metrics (histogram buckets, count, sum) + + + HTTP server request duration metrics (histogram buckets, count, sum) + + + + HTTP client request duration metrics (histogram buckets, count, sum) + + ## Troubleshooting From 78b8ff17da216cee27e853520e493ca7675bcab4 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Sat, 1 Nov 2025 03:55:31 +0800 Subject: [PATCH 008/294] misc: addressed comments --- .../guides/monitoring-telemetry.mdx | 263 +++++++++--------- 1 file changed, 137 insertions(+), 126 deletions(-) diff --git a/docs/self-hosting/guides/monitoring-telemetry.mdx b/docs/self-hosting/guides/monitoring-telemetry.mdx index 441a78ac0..763826d4f 100644 --- a/docs/self-hosting/guides/monitoring-telemetry.mdx +++ b/docs/self-hosting/guides/monitoring-telemetry.mdx @@ -47,43 +47,53 @@ OTEL_EXPORT_TYPE=prometheus ### Configuration - 1. **Enable Prometheus export in Infisical**: + + +```bash +OTEL_TELEMETRY_COLLECTION_ENABLED=true +OTEL_EXPORT_TYPE=prometheus +``` + - ```bash - OTEL_TELEMETRY_COLLECTION_ENABLED=true - OTEL_EXPORT_TYPE=prometheus - ``` + + Expose the metrics port in your Infisical backend: - 2. **Expose the metrics port** in your Infisical backend: + - **Docker**: Expose port 9464 + - **Kubernetes**: Create a service exposing port 9464 + - **Other**: Ensure port 9464 is accessible to your monitoring stack + - - **Docker**: Expose port 9464 - - **Kubernetes**: Create a service exposing port 9464 - - **Other**: Ensure port 9464 is accessible to your monitoring stack + +Create `prometheus.yml`: - 3. **Create Prometheus configuration** (`prometheus.yml`): +```yaml +global: + scrape_interval: 30s + evaluation_interval: 30s - ```yaml - global: - scrape_interval: 30s - evaluation_interval: 30s +scrape_configs: + - job_name: "infisical" + scrape_interval: 30s + static_configs: + - targets: ["infisical-backend:9464"] # Adjust hostname/port based on your deployment + metrics_path: "/metrics" +``` - scrape_configs: - - job_name: "infisical" - scrape_interval: 30s - static_configs: - - targets: ["infisical-backend:9464"] # Adjust hostname/port based on your deployment - metrics_path: "/metrics" - ``` + +Replace `infisical-backend:9464` with the actual hostname and port where your Infisical backend is running. This could be: - **Note**: Replace `infisical-backend:9464` with the actual hostname and port where your Infisical backend is running. This could be: - - - **Docker Compose**: `infisical-backend:9464` (service name) - - **Kubernetes**: `infisical-backend.default.svc.cluster.local:9464` (service name) - - **Bare Metal**: `192.168.1.100:9464` (actual IP address) - - **Cloud**: `your-infisical.example.com:9464` (domain name) +- **Docker Compose**: `infisical-backend:9464` (service name) +- **Kubernetes**: `infisical-backend.default.svc.cluster.local:9464` (service name) +- **Bare Metal**: `192.168.1.100:9464` (actual IP address) +- **Cloud**: `your-infisical.example.com:9464` (domain name) + + + ### Deployment Options + Once you've configured Infisical to expose metrics, you'll need to deploy Prometheus to scrape and store them. Below are examples for different deployment environments. Choose the option that matches your infrastructure. + ```yaml @@ -168,94 +178,106 @@ OTEL_EXPORT_TYPE=prometheus ### Configuration - 1. **Enable OTLP export in Infisical**: + + +```bash +OTEL_TELEMETRY_COLLECTION_ENABLED=true +OTEL_EXPORT_TYPE=otlp +OTEL_EXPORT_OTLP_ENDPOINT=http://otel-collector:4318/v1/metrics +OTEL_COLLECTOR_BASIC_AUTH_USERNAME=infisical +OTEL_COLLECTOR_BASIC_AUTH_PASSWORD=infisical +OTEL_OTLP_PUSH_INTERVAL=30000 +``` + - ```bash - OTEL_TELEMETRY_COLLECTION_ENABLED=true - OTEL_EXPORT_TYPE=otlp - OTEL_EXPORT_OTLP_ENDPOINT=http://otel-collector:4318/v1/metrics - OTEL_COLLECTOR_BASIC_AUTH_USERNAME=infisical - OTEL_COLLECTOR_BASIC_AUTH_PASSWORD=infisical - OTEL_OTLP_PUSH_INTERVAL=30000 - ``` + +Create `otel-collector-config.yaml`: - 2. **Create OpenTelemetry Collector configuration** (`otel-collector-config.yaml`): +```yaml +extensions: + health_check: + pprof: + zpages: + basicauth/server: + htpasswd: + inline: | + your_username:your_password - ```yaml - extensions: - health_check: - pprof: - zpages: - basicauth/server: - htpasswd: - inline: | - your_username:your_password +receivers: + otlp: + protocols: + http: + endpoint: 0.0.0.0:4318 + auth: + authenticator: basicauth/server - receivers: - otlp: - protocols: - http: - endpoint: 0.0.0.0:4318 - auth: - authenticator: basicauth/server + prometheus: + config: + scrape_configs: + - job_name: otel-collector + scrape_interval: 30s + static_configs: + - targets: [infisical-backend:9464] + metric_relabel_configs: + - action: labeldrop + regex: "service_instance_id|service_name" - prometheus: - config: - scrape_configs: - - job_name: otel-collector - scrape_interval: 30s - static_configs: - - targets: [infisical-backend:9464] - metric_relabel_configs: - - action: labeldrop - regex: "service_instance_id|service_name" +processors: + batch: - processors: - batch: +exporters: + prometheus: + endpoint: "0.0.0.0:8889" + auth: + authenticator: basicauth/server + resource_to_telemetry_conversion: + enabled: true - exporters: - prometheus: - endpoint: "0.0.0.0:8889" - auth: - authenticator: basicauth/server - resource_to_telemetry_conversion: - enabled: true +service: + extensions: [basicauth/server, health_check, pprof, zpages] + pipelines: + metrics: + receivers: [otlp] + processors: [batch] + exporters: [prometheus] +``` - service: - extensions: [basicauth/server, health_check, pprof, zpages] - pipelines: - metrics: - receivers: [otlp] - processors: [batch] - exporters: [prometheus] - ``` + +Replace `your_username:your_password` with your chosen credentials. These must match the values you set in Infisical's `OTEL_COLLECTOR_BASIC_AUTH_USERNAME` and `OTEL_COLLECTOR_BASIC_AUTH_PASSWORD` environment variables. + + - **Important**: Replace `your_username:your_password` with your chosen credentials. These must match the values you set in Infisical's `OTEL_COLLECTOR_BASIC_AUTH_USERNAME` and `OTEL_COLLECTOR_BASIC_AUTH_PASSWORD` environment variables. + +Create Prometheus configuration for the collector: - 3. **Create Prometheus configuration** for the collector: +```yaml +global: + scrape_interval: 30s + evaluation_interval: 30s - ```yaml - global: - scrape_interval: 30s - evaluation_interval: 30s +scrape_configs: + - job_name: "otel-collector" + scrape_interval: 30s + static_configs: + - targets: ["otel-collector:8889"] # Adjust hostname/port based on your deployment + metrics_path: "/metrics" +``` - scrape_configs: - - job_name: "otel-collector" - scrape_interval: 30s - static_configs: - - targets: ["otel-collector:8889"] # Adjust hostname/port based on your deployment - metrics_path: "/metrics" - ``` + +Replace `otel-collector:8889` with the actual hostname and port where your OpenTelemetry Collector is running. This could be: - **Note**: Replace `otel-collector:8889` with the actual hostname and port where your OpenTelemetry Collector is running. This could be: - - - **Docker Compose**: `otel-collector:8889` (service name) - - **Kubernetes**: `otel-collector.default.svc.cluster.local:8889` (service name) - - **Bare Metal**: `192.168.1.100:8889` (actual IP address) - - **Cloud**: `your-collector.example.com:8889` (domain name) +- **Docker Compose**: `otel-collector:8889` (service name) +- **Kubernetes**: `otel-collector.default.svc.cluster.local:8889` (service name) +- **Bare Metal**: `192.168.1.100:8889` (actual IP address) +- **Cloud**: `your-collector.example.com:8889` (domain name) + + + ### Deployment Options + After configuring Infisical and the OpenTelemetry Collector, you'll need to deploy the collector to receive metrics from Infisical. Below are examples for different deployment environments. Choose the option that matches your infrastructure. + ```yaml @@ -463,24 +485,6 @@ These metrics track authentication attempts and outcomes, enabling you to monito -### Legacy Metrics - -These metrics are from the previous instrumentation and may be deprecated in future versions. Consider migrating to the new Core API Metrics for more comprehensive observability. - - - - API request latency histogram in milliseconds - - - **Labels**: `route`, `method`, `statusCode` - - - - API error count histogram - - - **Labels**: `route`, `method`, `type`, `name` - - - ### Integration & Secret Sync Metrics These metrics monitor secret synchronization operations between Infisical and external systems, helping you track sync health, identify integration failures, and troubleshoot connectivity issues. @@ -531,15 +535,22 @@ These low-level HTTP metrics are automatically collected by OpenTelemetry's inst ## Troubleshooting -### Common Issues + + If your metrics are not showing up in Prometheus or your monitoring system, check the following: -1. **Metrics not appearing**: + - Verify `OTEL_TELEMETRY_COLLECTION_ENABLED=true` is set in your Infisical environment variables + - Ensure the correct `OTEL_EXPORT_TYPE` is set (`prometheus` or `otlp`) + - Check network connectivity between Infisical and your monitoring services (Prometheus or OTLP collector) + - For pull-based monitoring: Verify port 9464 is exposed and accessible + - For push-based monitoring: Verify the OTLP endpoint URL is correct and reachable + - Check Infisical backend logs for any errors related to metrics export + - - Check if `OTEL_TELEMETRY_COLLECTION_ENABLED=true` - - Verify the correct `OTEL_EXPORT_TYPE` is set - - Check network connectivity between services + + If you're experiencing authentication errors with the OpenTelemetry Collector: -2. **Authentication errors**: - - - Verify basic auth credentials in OTLP configuration - - Check if credentials match between Infisical and collector + - Verify basic auth credentials in your OTLP configuration match between Infisical and the collector + - Check that `OTEL_COLLECTOR_BASIC_AUTH_USERNAME` and `OTEL_COLLECTOR_BASIC_AUTH_PASSWORD` match the credentials in your `otel-collector-config.yaml` + - Ensure the htpasswd format in the collector configuration is correct + - Test the collector endpoint manually using curl with the same credentials to verify they work + From b26fb73053b9fd0d926fd0f8fe940c82959e22c9 Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Fri, 31 Oct 2025 18:43:34 -0300 Subject: [PATCH 009/294] refactor: streamline modal close handlers and completion callbacks across various components --- frontend/src/components/features/WishForm.tsx | 23 +- .../CreateOrgModal/CreateOrgModal.tsx | 40 ++-- .../pki-syncs/DeletePkiSyncModal.tsx | 31 +-- .../PkiSyncImportCertificatesModal.tsx | 29 +-- .../PkiSyncRemoveCertificatesModal.tsx | 29 +-- .../pki-syncs/forms/CreatePkiSyncForm.tsx | 8 +- .../pki-syncs/forms/EditPkiSyncForm.tsx | 33 ++- .../project/ProjectOverviewChangeSection.tsx | 36 ++-- .../components/projects/NewProjectModal.tsx | 39 ++-- .../DeleteSecretRotationV2Modal.tsx | 35 ++- .../RotateSecretRotationV2Modal.tsx | 31 +-- .../forms/SecretRotationV2Form.tsx | 20 +- .../DeleteSecretScanningDataSourceModal.tsx | 29 +-- .../forms/SecretScanningDataSourceForm.tsx | 20 +- .../secret-syncs/DeleteSecretSyncModal.tsx | 33 ++- .../SecretSyncImportSecretsModal.tsx | 31 +-- .../SecretSyncRemoveSecretsModal.tsx | 29 +-- .../forms/CreateSecretSyncForm.tsx | 8 +- .../secret-syncs/forms/EditSecretSyncForm.tsx | 35 ++- .../tags/CreateTagModal/CreateTagModal.tsx | 32 ++- .../NavBar/NewSubOrganizationForm.tsx | 33 ++- .../ProjectSelect/ProjectSelect.tsx | 31 +-- .../components/AddServerAdminModal.tsx | 19 +- .../components/ServerAdminsTable.tsx | 55 ++--- .../components/AuthenticationPageForm.tsx | 76 +++---- .../components/CachingPageForm.tsx | 13 +- .../components/EncryptionPageForm.tsx | 17 +- .../components/EnvironmentPageForm.tsx | 34 +-- .../components/GeneralPageForm.tsx | 52 ++--- .../components/UsageReportSection.tsx | 22 +- .../components/AddOrganizationModal.tsx | 29 +-- .../components/MachineIdentitiesTable.tsx | 17 +- .../components/OrganizationsTable.tsx | 35 +-- .../components/UserIdentitiesTable.tsx | 72 ++----- .../src/pages/admin/SignUpPage/SignUpPage.tsx | 35 ++- .../PasswordSetupPage/PasswordSetupPage.tsx | 6 +- .../EmailConfirmationStep.tsx | 82 +++---- .../AlertingPage/components/PkiAlertModal.tsx | 72 +++---- .../components/PkiAlertsSection.tsx | 28 +-- .../components/PkiCollectionModal.tsx | 70 +++--- .../components/PkiCollectionSection.tsx | 28 +-- .../CertAuthDetailsByIDPage.tsx | 41 ++-- .../components/CaRenewalModal.tsx | 30 ++- .../ExternalCaInstallForm.tsx | 33 ++- .../InternalCaInstallForm.tsx | 47 ++-- .../components/CaModal.tsx | 68 +++--- .../components/CaSection.tsx | 43 ++-- .../components/ExternalCaModal.tsx | 94 ++++---- .../components/ExternalCaSection.tsx | 43 ++-- .../components/CertificateImportModal.tsx | 46 ++-- .../components/CertificateIssuanceModal.tsx | 136 ++++++------ .../CertificateManageRenewalModal.tsx | 46 ++-- .../components/CertificateModal.tsx | 62 +++--- .../CertificateRenewalConfigModal.tsx | 40 ++-- .../CertificateRenewalDisableModal.tsx | 40 ++-- .../components/CertificateRenewalModal.tsx | 22 +- .../components/CertificateRevocationModal.tsx | 34 ++- .../components/CertificateTemplateModal.tsx | 94 ++++---- .../CertificateTemplatesSection.tsx | 26 +-- .../components/CertificatesSection.tsx | 22 +- .../components/CertificatesTable.tsx | 36 ++-- .../PkiSyncTable/PkiSyncsTable.tsx | 52 ++--- .../PkiCollectionDetailsByIDPage.tsx | 39 ++-- .../PkiSubscriberDetailsByIDPage.tsx | 34 ++- .../PkiSubscriberDetailsSection.tsx | 44 ++-- .../components/PkiSubscriberModal.tsx | 204 +++++++++--------- .../components/PkiSubscriberSection.tsx | 42 ++-- .../components/PkiSyncActionTriggers.tsx | 54 ++--- .../PkiTemplateListPage.tsx | 26 +-- .../components/PkiTemplateForm.tsx | 90 ++++---- .../CertificateProfilesTab.tsx | 25 +-- .../CreateProfileModal.tsx | 96 ++++----- .../CertificateTemplatesV2Tab.tsx | 26 +-- .../CreateTemplateModal.tsx | 84 ++++---- .../components/DeleteKmipClientModal.tsx | 29 +-- .../KmipPage/components/KmipClientModal.tsx | 20 +- .../components/CmekDecryptModal.tsx | 24 +-- .../components/CmekEncryptModal.tsx | 18 +- .../kms/OverviewPage/components/CmekModal.tsx | 20 +- .../OverviewPage/components/CmekSignModal.tsx | 18 +- .../kms/OverviewPage/components/CmekTable.tsx | 30 +-- .../components/CmekVerifyModal.tsx | 26 +-- .../components/DeleteCmekModal.tsx | 29 +-- .../OrgGroupsSection/OrgGroupModal.tsx | 57 +++-- .../OrgGroupsSection/OrgGroupsSection.tsx | 22 +- .../OrgGroupsSection/OrgGroupsTable.tsx | 24 +-- .../IdentitySection/IdentitySection.tsx | 58 ++--- .../OrgMembersSection/OrgMembersSection.tsx | 81 +++---- .../OrgMembersSection/OrgMembersTable.tsx | 49 ++--- .../OrgRoleTabSection/OrgRoleTable.tsx | 34 ++- .../AppConnectionForm/AppConnectionForm.tsx | 54 ++--- .../components/GroupCreateUpdateModal.tsx | 57 +++-- .../IdentityDetailsByIDPage.tsx | 41 ++-- .../RolePermissionsSection.tsx | 19 +- .../UserDetailsByIDPage.tsx | 64 ++---- .../components/UserDetailsSection.tsx | 28 +-- .../PamAccountForm/PamAccountForm.tsx | 62 ++---- .../components/PamAddFolderModal.tsx | 29 +-- .../components/PamDeleteAccountModal.tsx | 26 +-- .../components/PamDeleteFolderModal.tsx | 24 +-- .../components/PamUpdateFolderModal.tsx | 27 +-- .../components/PamDeleteResourceModal.tsx | 26 +-- .../PamResourceForm/PamResourceForm.tsx | 54 ++--- .../MemberRoleForm/MemberRbacSection.tsx | 18 +- .../IdentityRoleModify.tsx | 16 +- .../MemberRoleDetailsSection.tsx | 67 +++--- .../MemberRoleModify.tsx | 16 +- .../SecretRotationPage/SecretRotationPage.tsx | 50 ++--- .../components/SecretSyncActionTriggers.tsx | 52 ++--- .../components/SlackIntegrationForm.tsx | 43 ++-- .../components/SecretScanningResourceRow.tsx | 27 +-- .../SecretScanningUpdateFindingModal.tsx | 57 +++-- .../SecretScanningConfigForm.tsx | 23 +- .../components/SshHostGroupHostsSection.tsx | 26 +-- .../components/SshHostGroupsSection.tsx | 20 +- .../SshHostsPage/components/SshHostModal.tsx | 133 ++++++------ 116 files changed, 1865 insertions(+), 2949 deletions(-) diff --git a/frontend/src/components/features/WishForm.tsx b/frontend/src/components/features/WishForm.tsx index 118900bc9..28107809a 100644 --- a/frontend/src/components/features/WishForm.tsx +++ b/frontend/src/components/features/WishForm.tsx @@ -35,23 +35,16 @@ export const WishForm = () => { const [isOpen, setIsOpen] = useToggle(false); const createWish = async (data: TFormData) => { - try { - await mutateAsync({ - text: data.text - }); + await mutateAsync({ + text: data.text + }); - createNotification({ - text: "Your wish has been sent to the Infisical team!", - type: "success" - }); + createNotification({ + text: "Your wish has been sent to the Infisical team!", + type: "success" + }); - setIsOpen.off(); - } catch { - createNotification({ - text: "An error occured while sending your wish to the Infisical team.", - type: "error" - }); - } + setIsOpen.off(); }; return ( diff --git a/frontend/src/components/organization/CreateOrgModal/CreateOrgModal.tsx b/frontend/src/components/organization/CreateOrgModal/CreateOrgModal.tsx index 23c3fe6a5..84931f84a 100644 --- a/frontend/src/components/organization/CreateOrgModal/CreateOrgModal.tsx +++ b/frontend/src/components/organization/CreateOrgModal/CreateOrgModal.tsx @@ -43,35 +43,27 @@ export const CreateOrgModal: FC = ({ isOpen, onClose }) => const { mutateAsync: selectOrg } = useSelectOrganization(); const onFormSubmit = async ({ name }: FormData) => { - try { - const organization = await createOrg({ - name - }); + const organization = await createOrg({ + name + }); - await selectOrg({ - organizationId: organization.id - }); + await selectOrg({ + organizationId: organization.id + }); - createNotification({ - text: "Successfully created organization", - type: "success" - }); + createNotification({ + text: "Successfully created organization", + type: "success" + }); - navigate({ - to: "/organization/projects" - }); + navigate({ + to: "/organization/projects" + }); - localStorage.setItem("orgData.id", organization.id); + localStorage.setItem("orgData.id", organization.id); - reset(); - onClose(); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to created organization", - type: "error" - }); - } + reset(); + onClose(); }; return ( diff --git a/frontend/src/components/pki-syncs/DeletePkiSyncModal.tsx b/frontend/src/components/pki-syncs/DeletePkiSyncModal.tsx index e465d3f34..c01efcbf2 100644 --- a/frontend/src/components/pki-syncs/DeletePkiSyncModal.tsx +++ b/frontend/src/components/pki-syncs/DeletePkiSyncModal.tsx @@ -20,28 +20,19 @@ export const DeletePkiSyncModal = ({ isOpen, onOpenChange, pkiSync, onComplete } const handleDeletePkiSync = async () => { const destinationName = PKI_SYNC_MAP[destination].name; - try { - await deleteSync.mutateAsync({ - syncId, - projectId, - destination - }); + await deleteSync.mutateAsync({ + syncId, + projectId, + destination + }); - createNotification({ - text: `Successfully deleted ${destinationName} PKI Sync`, - type: "success" - }); + createNotification({ + text: `Successfully deleted ${destinationName} PKI Sync`, + type: "success" + }); - if (onComplete) onComplete(); - onOpenChange(false); - } catch (err) { - console.error(err); - - createNotification({ - text: `Failed to delete ${destinationName} PKI Sync`, - type: "error" - }); - } + if (onComplete) onComplete(); + onOpenChange(false); }; return ( diff --git a/frontend/src/components/pki-syncs/PkiSyncImportCertificatesModal.tsx b/frontend/src/components/pki-syncs/PkiSyncImportCertificatesModal.tsx index 33b06dbe4..43192c5e3 100644 --- a/frontend/src/components/pki-syncs/PkiSyncImportCertificatesModal.tsx +++ b/frontend/src/components/pki-syncs/PkiSyncImportCertificatesModal.tsx @@ -21,27 +21,18 @@ const Content = ({ pkiSync, onComplete }: ContentProps) => { const triggerImportCertificates = useTriggerPkiSyncImportCertificates(); const handleTriggerImportCertificates = async () => { - try { - await triggerImportCertificates.mutateAsync({ - syncId, - destination, - projectId - }); + await triggerImportCertificates.mutateAsync({ + syncId, + destination, + projectId + }); - createNotification({ - text: `Successfully triggered certificate import for ${destinationName} Sync`, - type: "success" - }); + createNotification({ + text: `Successfully triggered certificate import for ${destinationName} Sync`, + type: "success" + }); - onComplete(); - } catch (err) { - console.error(err); - - createNotification({ - text: `Failed to trigger certificate import for ${destinationName} Sync`, - type: "error" - }); - } + onComplete(); }; return ( diff --git a/frontend/src/components/pki-syncs/PkiSyncRemoveCertificatesModal.tsx b/frontend/src/components/pki-syncs/PkiSyncRemoveCertificatesModal.tsx index f17855289..cee845381 100644 --- a/frontend/src/components/pki-syncs/PkiSyncRemoveCertificatesModal.tsx +++ b/frontend/src/components/pki-syncs/PkiSyncRemoveCertificatesModal.tsx @@ -21,27 +21,18 @@ const Content = ({ pkiSync, onComplete }: ContentProps) => { const triggerRemoveCertificates = useTriggerPkiSyncRemoveCertificates(); const handleTriggerRemoveCertificates = async () => { - try { - await triggerRemoveCertificates.mutateAsync({ - syncId, - destination, - projectId - }); + await triggerRemoveCertificates.mutateAsync({ + syncId, + destination, + projectId + }); - createNotification({ - text: `Successfully triggered certificate removal for ${destinationName} Sync`, - type: "success" - }); + createNotification({ + text: `Successfully triggered certificate removal for ${destinationName} Sync`, + type: "success" + }); - onComplete(); - } catch (err) { - console.error(err); - - createNotification({ - text: `Failed to trigger certificate removal for ${destinationName} Sync`, - type: "error" - }); - } + onComplete(); }; return ( diff --git a/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx b/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx index 582518bd7..085e57882 100644 --- a/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx +++ b/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx @@ -72,14 +72,8 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel }: Props) type: "success" }); onComplete(pkiSync); - } catch (err: Error | unknown) { - console.error(err); + } catch { setShowConfirmation(false); - createNotification({ - title: `Failed to add ${destinationName} Certificate Sync`, - text: err instanceof Error ? err.message : "An unknown error occurred", - type: "error" - }); } }; diff --git a/frontend/src/components/pki-syncs/forms/EditPkiSyncForm.tsx b/frontend/src/components/pki-syncs/forms/EditPkiSyncForm.tsx index 73f5bc2bc..7f8ee207f 100644 --- a/frontend/src/components/pki-syncs/forms/EditPkiSyncForm.tsx +++ b/frontend/src/components/pki-syncs/forms/EditPkiSyncForm.tsx @@ -38,28 +38,19 @@ export const EditPkiSyncForm = ({ pkiSync, fields, onComplete }: Props) => { }); const onSubmit = async ({ connection, ...formData }: TUpdatePkiSyncForm) => { - try { - const updatedPkiSync = await updatePkiSync.mutateAsync({ - syncId: pkiSync.id, - ...formData, - connectionId: connection.id, - projectId: pkiSync.projectId, - destination: pkiSync.destination - }); + const updatedPkiSync = await updatePkiSync.mutateAsync({ + syncId: pkiSync.id, + ...formData, + connectionId: connection.id, + projectId: pkiSync.projectId, + destination: pkiSync.destination + }); - createNotification({ - text: `Successfully updated ${destinationName} PKI Sync`, - type: "success" - }); - onComplete(updatedPkiSync); - } catch (err: any) { - console.error(err); - createNotification({ - title: `Failed to update ${destinationName} PKI Sync`, - text: err.message, - type: "error" - }); - } + createNotification({ + text: `Successfully updated ${destinationName} PKI Sync`, + type: "success" + }); + onComplete(updatedPkiSync); }; let Component: ReactNode; diff --git a/frontend/src/components/project/ProjectOverviewChangeSection.tsx b/frontend/src/components/project/ProjectOverviewChangeSection.tsx index 548fbd1e3..455b0806a 100644 --- a/frontend/src/components/project/ProjectOverviewChangeSection.tsx +++ b/frontend/src/components/project/ProjectOverviewChangeSection.tsx @@ -56,30 +56,22 @@ export const ProjectOverviewChangeSection = ({ showSlugField = false }: Props) = }, [currentProject, showSlugField]); const onFormSubmit = async (data: BaseFormData | FormDataWithSlug) => { - try { - if (!currentProject?.id) return; + if (!currentProject?.id) return; - await mutateAsync({ - projectId: currentProject.id, - newProjectName: data.name, - newProjectDescription: data.description, - ...(showSlugField && - "slug" in data && { - newSlug: data.slug !== currentProject.slug ? data.slug : undefined - }) - }); + await mutateAsync({ + projectId: currentProject.id, + newProjectName: data.name, + newProjectDescription: data.description, + ...(showSlugField && + "slug" in data && { + newSlug: data.slug !== currentProject.slug ? data.slug : undefined + }) + }); - createNotification({ - text: "Successfully updated project overview", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to update project overview", - type: "error" - }); - } + createNotification({ + text: "Successfully updated project overview", + type: "success" + }); }; return ( diff --git a/frontend/src/components/projects/NewProjectModal.tsx b/frontend/src/components/projects/NewProjectModal.tsx index d71e7b957..663d6a0b7 100644 --- a/frontend/src/components/projects/NewProjectModal.tsx +++ b/frontend/src/components/projects/NewProjectModal.tsx @@ -141,29 +141,24 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => { // type check if (!currentOrg) return; if (!user) return; - try { - const { - data: { project } - } = await createWs.mutateAsync({ - projectName: name, - projectDescription: description, - kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined, - template, - type - }); - await refetchWorkspaces(); + const { + data: { project } + } = await createWs.mutateAsync({ + projectName: name, + projectDescription: description, + kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined, + template, + type + }); + await refetchWorkspaces(); - createNotification({ text: "Project created", type: "success" }); - reset(); - onOpenChange(false); - navigate({ - to: getProjectHomePage(project.type, project.environments), - params: { projectId: project.id } - }); - } catch (err) { - console.error(err); - createNotification({ text: "Failed to create project", type: "error" }); - } + createNotification({ text: "Project created", type: "success" }); + reset(); + onOpenChange(false); + navigate({ + to: getProjectHomePage(project.type, project.environments), + params: { projectId: project.id } + }); }; const onSubmit = handleSubmit((data) => { return onCreateProject(data); diff --git a/frontend/src/components/secret-rotations-v2/DeleteSecretRotationV2Modal.tsx b/frontend/src/components/secret-rotations-v2/DeleteSecretRotationV2Modal.tsx index 20b08eb49..524c66933 100644 --- a/frontend/src/components/secret-rotations-v2/DeleteSecretRotationV2Modal.tsx +++ b/frontend/src/components/secret-rotations-v2/DeleteSecretRotationV2Modal.tsx @@ -37,29 +37,22 @@ export const DeleteSecretRotationV2Modal = ({ const handleDeleteSecretRotation = async () => { const rotationType = SECRET_ROTATION_MAP[type].name; - try { - await deleteSecretRotation.mutateAsync({ - rotationId, - type, - revokeGeneratedCredentials, - deleteSecrets, - projectId, - secretPath: folder.path - }); + await deleteSecretRotation.mutateAsync({ + rotationId, + type, + revokeGeneratedCredentials, + deleteSecrets, + projectId, + secretPath: folder.path + }); - createNotification({ - text: `Successfully deleted ${rotationType} Rotation`, - type: "success" - }); + createNotification({ + text: `Successfully deleted ${rotationType} Rotation`, + type: "success" + }); - if (onComplete) onComplete(); - onOpenChange(false); - } catch { - createNotification({ - text: `Failed to delete ${rotationType} Rotation`, - type: "error" - }); - } + if (onComplete) onComplete(); + onOpenChange(false); }; return ( diff --git a/frontend/src/components/secret-rotations-v2/RotateSecretRotationV2Modal.tsx b/frontend/src/components/secret-rotations-v2/RotateSecretRotationV2Modal.tsx index e2c931d49..7ad9611be 100644 --- a/frontend/src/components/secret-rotations-v2/RotateSecretRotationV2Modal.tsx +++ b/frontend/src/components/secret-rotations-v2/RotateSecretRotationV2Modal.tsx @@ -22,28 +22,19 @@ const Content = ({ secretRotation, onComplete }: ContentProps) => { const rotationType = SECRET_ROTATION_MAP[type].name; const handleRotateSecrets = async () => { - try { - await rotateSecrets.mutateAsync({ - rotationId, - type, - projectId, - secretPath: folder.path - }); + await rotateSecrets.mutateAsync({ + rotationId, + type, + projectId, + secretPath: folder.path + }); - createNotification({ - text: `Successfully rotated ${rotationType} secrets`, - type: "success" - }); + createNotification({ + text: `Successfully rotated ${rotationType} secrets`, + type: "success" + }); - onComplete(); - } catch (err) { - console.error(err); - - createNotification({ - text: `Failed to rotate ${rotationType} secrets`, - type: "error" - }); - } + onComplete(); }; return ( diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2Form.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2Form.tsx index 9887da026..320793ed1 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2Form.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2Form.tsx @@ -120,21 +120,13 @@ export const SecretRotationV2Form = ({ environment: environment.slug, projectId: currentProject.id }); - try { - const rotation = await mutation; + const rotation = await mutation; - createNotification({ - text: `Successfully ${secretRotation ? "updated" : "created"} ${rotationType} Rotation`, - type: "success" - }); - onComplete(rotation); - } catch (err: any) { - createNotification({ - title: `Failed to ${secretRotation ? "update" : "create"} ${rotationType} Rotation`, - text: err.message, - type: "error" - }); - } + createNotification({ + text: `Successfully ${secretRotation ? "updated" : "created"} ${rotationType} Rotation`, + type: "success" + }); + onComplete(rotation); }; const handlePrev = () => { diff --git a/frontend/src/components/secret-scanning/DeleteSecretScanningDataSourceModal.tsx b/frontend/src/components/secret-scanning/DeleteSecretScanningDataSourceModal.tsx index 9cf918dfb..091de4cc0 100644 --- a/frontend/src/components/secret-scanning/DeleteSecretScanningDataSourceModal.tsx +++ b/frontend/src/components/secret-scanning/DeleteSecretScanningDataSourceModal.tsx @@ -28,26 +28,19 @@ export const DeleteSecretScanningDataSourceModal = ({ const handleDeleteDataSource = async () => { const dataSourceType = SECRET_SCANNING_DATA_SOURCE_MAP[type].name; - try { - await deleteDataSource.mutateAsync({ - dataSourceId, - type, - projectId - }); + await deleteDataSource.mutateAsync({ + dataSourceId, + type, + projectId + }); - createNotification({ - text: `Successfully deleted ${dataSourceType} Data Source`, - type: "success" - }); + createNotification({ + text: `Successfully deleted ${dataSourceType} Data Source`, + type: "success" + }); - if (onComplete) onComplete(); - onOpenChange(false); - } catch { - createNotification({ - text: `Failed to delete ${dataSourceType} Data Source`, - type: "error" - }); - } + if (onComplete) onComplete(); + onOpenChange(false); }; return ( diff --git a/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceForm.tsx b/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceForm.tsx index 4d5ccdc79..563f82d46 100644 --- a/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceForm.tsx +++ b/frontend/src/components/secret-scanning/forms/SecretScanningDataSourceForm.tsx @@ -73,21 +73,13 @@ export const SecretScanningDataSourceForm = ({ connectionId: connection?.id, projectId: currentProject.id }); - try { - const source = await mutation; + const source = await mutation; - createNotification({ - text: `Successfully ${source ? "updated" : "created"} ${sourceType} Data Source`, - type: "success" - }); - onComplete(source); - } catch (err: any) { - createNotification({ - title: `Failed to ${dataSource ? "update" : "create"} ${sourceType} Data Source`, - text: err.message, - type: "error" - }); - } + createNotification({ + text: `Successfully ${source ? "updated" : "created"} ${sourceType} Data Source`, + type: "success" + }); + onComplete(source); }; const handlePrev = () => { diff --git a/frontend/src/components/secret-syncs/DeleteSecretSyncModal.tsx b/frontend/src/components/secret-syncs/DeleteSecretSyncModal.tsx index 2b3903a9b..de8b2e79d 100644 --- a/frontend/src/components/secret-syncs/DeleteSecretSyncModal.tsx +++ b/frontend/src/components/secret-syncs/DeleteSecretSyncModal.tsx @@ -23,29 +23,20 @@ export const DeleteSecretSyncModal = ({ isOpen, onOpenChange, secretSync, onComp const handleDeleteSecretSync = async () => { const destinationName = SECRET_SYNC_MAP[destination].name; - try { - await deleteSync.mutateAsync({ - syncId, - destination, - removeSecrets, - projectId - }); + await deleteSync.mutateAsync({ + syncId, + destination, + removeSecrets, + projectId + }); - createNotification({ - text: `Successfully removed ${destinationName} Sync`, - type: "success" - }); + createNotification({ + text: `Successfully removed ${destinationName} Sync`, + type: "success" + }); - if (onComplete) onComplete(); - onOpenChange(false); - } catch (err) { - console.error(err); - - createNotification({ - text: `Failed to remove ${destinationName} Sync`, - type: "error" - }); - } + if (onComplete) onComplete(); + onOpenChange(false); }; return ( diff --git a/frontend/src/components/secret-syncs/SecretSyncImportSecretsModal.tsx b/frontend/src/components/secret-syncs/SecretSyncImportSecretsModal.tsx index c1b21a771..656faffeb 100644 --- a/frontend/src/components/secret-syncs/SecretSyncImportSecretsModal.tsx +++ b/frontend/src/components/secret-syncs/SecretSyncImportSecretsModal.tsx @@ -51,28 +51,19 @@ const Content = ({ secretSync, onComplete }: ContentProps) => { const triggerImportSecrets = useTriggerSecretSyncImportSecrets(); const handleTriggerImportSecrets = async ({ importBehavior }: TFormData) => { - try { - await triggerImportSecrets.mutateAsync({ - syncId, - destination, - importBehavior, - projectId - }); + await triggerImportSecrets.mutateAsync({ + syncId, + destination, + importBehavior, + projectId + }); - createNotification({ - text: `Successfully triggered secret import for ${destinationName} Sync`, - type: "success" - }); + createNotification({ + text: `Successfully triggered secret import for ${destinationName} Sync`, + type: "success" + }); - onComplete(); - } catch (err) { - console.error(err); - - createNotification({ - text: `Failed to trigger secret import for ${destinationName} Sync`, - type: "error" - }); - } + onComplete(); }; return ( diff --git a/frontend/src/components/secret-syncs/SecretSyncRemoveSecretsModal.tsx b/frontend/src/components/secret-syncs/SecretSyncRemoveSecretsModal.tsx index 718392b92..9c9c0e659 100644 --- a/frontend/src/components/secret-syncs/SecretSyncRemoveSecretsModal.tsx +++ b/frontend/src/components/secret-syncs/SecretSyncRemoveSecretsModal.tsx @@ -21,27 +21,18 @@ const Content = ({ secretSync, onComplete }: ContentProps) => { const triggerSyncImport = useTriggerSecretSyncRemoveSecrets(); const handleTriggerRemoveSecrets = async () => { - try { - await triggerSyncImport.mutateAsync({ - syncId, - destination, - projectId - }); + await triggerSyncImport.mutateAsync({ + syncId, + destination, + projectId + }); - createNotification({ - text: `Successfully triggered secret removal for ${destinationName} Sync`, - type: "success" - }); + createNotification({ + text: `Successfully triggered secret removal for ${destinationName} Sync`, + type: "success" + }); - onComplete(); - } catch (err) { - console.error(err); - - createNotification({ - text: `Failed to trigger secret removal for ${destinationName} Sync`, - type: "error" - }); - } + onComplete(); }; return ( diff --git a/frontend/src/components/secret-syncs/forms/CreateSecretSyncForm.tsx b/frontend/src/components/secret-syncs/forms/CreateSecretSyncForm.tsx index ebad86cbd..21a69b163 100644 --- a/frontend/src/components/secret-syncs/forms/CreateSecretSyncForm.tsx +++ b/frontend/src/components/secret-syncs/forms/CreateSecretSyncForm.tsx @@ -88,14 +88,8 @@ export const CreateSecretSyncForm = ({ type: "success" }); onComplete(secretSync); - } catch (err: any) { - console.error(err); + } catch { setShowConfirmation(false); - createNotification({ - title: `Failed to add ${destinationName} Sync`, - text: err.message, - type: "error" - }); } }; diff --git a/frontend/src/components/secret-syncs/forms/EditSecretSyncForm.tsx b/frontend/src/components/secret-syncs/forms/EditSecretSyncForm.tsx index 2085afe9c..207b72cd8 100644 --- a/frontend/src/components/secret-syncs/forms/EditSecretSyncForm.tsx +++ b/frontend/src/components/secret-syncs/forms/EditSecretSyncForm.tsx @@ -58,29 +58,20 @@ export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) => const performUpdate = useCallback( async (formData: TSecretSyncForm) => { - try { - const { environment, connection, ...updateData } = formData; - const updatedSecretSync = await updateSecretSync.mutateAsync({ - syncId: secretSync.id, - ...updateData, - environment: environment?.slug, - connectionId: connection.id, - projectId: secretSync.projectId - }); + const { environment, connection, ...updateData } = formData; + const updatedSecretSync = await updateSecretSync.mutateAsync({ + syncId: secretSync.id, + ...updateData, + environment: environment?.slug, + connectionId: connection.id, + projectId: secretSync.projectId + }); - createNotification({ - text: `Successfully updated ${destinationName} Sync`, - type: "success" - }); - onComplete(updatedSecretSync); - } catch (err: any) { - console.error(err); - createNotification({ - title: `Failed to update ${destinationName} Sync`, - text: err.message, - type: "error" - }); - } + createNotification({ + text: `Successfully updated ${destinationName} Sync`, + type: "success" + }); + onComplete(updatedSecretSync); }, [updateSecretSync, secretSync.id, secretSync.projectId, destinationName, onComplete] ); diff --git a/frontend/src/components/tags/CreateTagModal/CreateTagModal.tsx b/frontend/src/components/tags/CreateTagModal/CreateTagModal.tsx index 3c38926af..97e74cae4 100644 --- a/frontend/src/components/tags/CreateTagModal/CreateTagModal.tsx +++ b/frontend/src/components/tags/CreateTagModal/CreateTagModal.tsx @@ -130,26 +130,18 @@ export const CreateTagModal = ({ isOpen, onToggle, append, currentSecret }: Prop }, [isOpen]); const onFormSubmit = async ({ slug, color }: FormData) => { - try { - const data = await createWsTag({ - projectId, - tagColor: color, - tagSlug: slug - }); - append(data); - onToggle(false); - reset(); - createNotification({ - text: "Successfully created a tag", - type: "success" - }); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to create a tag", - type: "error" - }); - } + const data = await createWsTag({ + projectId, + tagColor: color, + tagSlug: slug + }); + append(data); + onToggle(false); + reset(); + createNotification({ + text: "Successfully created a tag", + type: "success" + }); }; return ( diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx index 75041a69e..bfea2e788 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx @@ -36,28 +36,21 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => { const router = useRouter(); const onSubmit = async ({ name }: FormData) => { - try { - const { organization } = await createSubOrg.mutateAsync({ - name - }); + const { organization } = await createSubOrg.mutateAsync({ + name + }); - createNotification({ - type: "success", - text: "Successfully created sub organization" - }); - onClose(); + createNotification({ + type: "success", + text: "Successfully created sub organization" + }); + onClose(); - navigate({ - to: "/organization/projects", - search: (prev) => ({ ...prev, subOrganization: organization.name }) - }); - await router.invalidate({ sync: true }).catch(() => null); - } catch { - createNotification({ - text: "Failed to create sub organization", - type: "error" - }); - } + navigate({ + to: "/organization/projects", + search: (prev) => ({ ...prev, subOrganization: organization.name }) + }); + await router.invalidate({ sync: true }).catch(() => null); }; return ( diff --git a/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx b/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx index c26333332..8e985fe58 100644 --- a/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx +++ b/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx @@ -11,7 +11,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Link, linkOptions } from "@tanstack/react-router"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; -import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; import { NewProjectModal } from "@app/components/projects"; import { @@ -59,31 +58,17 @@ export const ProjectSelect = () => { const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites(); const addProjectToFavorites = async (projectId: string) => { - try { - await updateUserProjectFavorites({ - orgId: currentOrg!.id, - projectFavorites: [...(projectFavorites || []), projectId] - }); - } catch { - createNotification({ - text: "Failed to add project to favorites.", - type: "error" - }); - } + await updateUserProjectFavorites({ + orgId: currentOrg!.id, + projectFavorites: [...(projectFavorites || []), projectId] + }); }; const removeProjectFromFavorites = async (projectId: string) => { - try { - await updateUserProjectFavorites({ - orgId: currentOrg!.id, - projectFavorites: [...(projectFavorites || []).filter((entry) => entry !== projectId)] - }); - } catch { - createNotification({ - text: "Failed to remove project from favorites.", - type: "error" - }); - } + await updateUserProjectFavorites({ + orgId: currentOrg!.id, + projectFavorites: [...(projectFavorites || []).filter((entry) => entry !== projectId)] + }); }; const isAddingProjectsAllowed = subscription?.workspaceLimit diff --git a/frontend/src/pages/admin/AccessManagementPage/components/AddServerAdminModal.tsx b/frontend/src/pages/admin/AccessManagementPage/components/AddServerAdminModal.tsx index 3e7638627..63ec2fa44 100644 --- a/frontend/src/pages/admin/AccessManagementPage/components/AddServerAdminModal.tsx +++ b/frontend/src/pages/admin/AccessManagementPage/components/AddServerAdminModal.tsx @@ -65,20 +65,13 @@ const Content = ({ onClose }: ContentProps) => { const users = usersData.filter((user) => !user.superAdmin); const onSubmit = async ({ user }: FormData) => { - try { - await grantAdmin.mutateAsync(user.id); + await grantAdmin.mutateAsync(user.id); - createNotification({ - type: "success", - text: "Successfully granted server admin status" - }); - onClose(); - } catch { - createNotification({ - text: "Failed to grant server admin status", - type: "error" - }); - } + createNotification({ + type: "success", + text: "Successfully granted server admin status" + }); + onClose(); }; return ( diff --git a/frontend/src/pages/admin/AccessManagementPage/components/ServerAdminsTable.tsx b/frontend/src/pages/admin/AccessManagementPage/components/ServerAdminsTable.tsx index 7e207fbc5..2e7016ee7 100644 --- a/frontend/src/pages/admin/AccessManagementPage/components/ServerAdminsTable.tsx +++ b/frontend/src/pages/admin/AccessManagementPage/components/ServerAdminsTable.tsx @@ -303,18 +303,11 @@ export const ServerAdminsTable = () => { const handleRemoveUser = async () => { const { id } = popUp?.removeUser?.data as { id: string; username: string }; - try { - await deleteUser(id); - createNotification({ - type: "success", - text: "Successfully deleted user" - }); - } catch { - createNotification({ - type: "error", - text: "Error deleting user" - }); - } + await deleteUser(id); + createNotification({ + type: "success", + text: "Successfully deleted user" + }); handlePopUpClose("removeUser"); }; @@ -322,39 +315,25 @@ export const ServerAdminsTable = () => { const handleRemoveServerAdminAccess = async () => { const { id } = popUp?.removeServerAdmin?.data as { id: string; username: string }; - try { - await removeAdminAccess(id); - createNotification({ - type: "success", - text: "Successfully removed server admin access from user" - }); - } catch { - createNotification({ - type: "error", - text: "Error removing server admin access from user" - }); - } + await removeAdminAccess(id); + createNotification({ + type: "success", + text: "Successfully removed server admin access from user" + }); handlePopUpClose("removeServerAdmin"); }; const handleRemoveUsers = async () => { - try { - await deleteUsers(selectedUsers.map((user) => user.id)); + await deleteUsers(selectedUsers.map((user) => user.id)); - createNotification({ - text: "Successfully removed users", - type: "success" - }); + createNotification({ + text: "Successfully removed users", + type: "success" + }); - setSelectedUsers([]); - handlePopUpClose("removeUsers"); - } catch { - createNotification({ - text: "Failed to remove users", - type: "error" - }); - } + setSelectedUsers([]); + handlePopUpClose("removeUsers"); }; return ( diff --git a/frontend/src/pages/admin/AuthenticationPage/components/AuthenticationPageForm.tsx b/frontend/src/pages/admin/AuthenticationPage/components/AuthenticationPageForm.tsx index 2abb96e60..f8ca30c52 100644 --- a/frontend/src/pages/admin/AuthenticationPage/components/AuthenticationPageForm.tsx +++ b/frontend/src/pages/admin/AuthenticationPage/components/AuthenticationPageForm.tsx @@ -54,59 +54,51 @@ export const AuthenticationPageForm = () => { }); const onAuthFormSubmit = async (formData: TAuthForm) => { - try { - const enabledMethods: LoginMethod[] = []; - if (formData.isEmailEnabled) { - enabledMethods.push(LoginMethod.EMAIL); - } + const enabledMethods: LoginMethod[] = []; + if (formData.isEmailEnabled) { + enabledMethods.push(LoginMethod.EMAIL); + } - if (formData.isGoogleEnabled) { - enabledMethods.push(LoginMethod.GOOGLE); - } + if (formData.isGoogleEnabled) { + enabledMethods.push(LoginMethod.GOOGLE); + } - if (formData.isGithubEnabled) { - enabledMethods.push(LoginMethod.GITHUB); - } + if (formData.isGithubEnabled) { + enabledMethods.push(LoginMethod.GITHUB); + } - if (formData.isGitlabEnabled) { - enabledMethods.push(LoginMethod.GITLAB); - } + if (formData.isGitlabEnabled) { + enabledMethods.push(LoginMethod.GITLAB); + } - if (formData.isSamlEnabled) { - enabledMethods.push(LoginMethod.SAML); - } + if (formData.isSamlEnabled) { + enabledMethods.push(LoginMethod.SAML); + } - if (formData.isLdapEnabled) { - enabledMethods.push(LoginMethod.LDAP); - } + if (formData.isLdapEnabled) { + enabledMethods.push(LoginMethod.LDAP); + } - if (formData.isOidcEnabled) { - enabledMethods.push(LoginMethod.OIDC); - } + if (formData.isOidcEnabled) { + enabledMethods.push(LoginMethod.OIDC); + } - if (!enabledMethods.length) { - createNotification({ - type: "error", - text: "At least one login method should be enabled." - }); - return; - } - - await updateServerConfig({ - enabledLoginMethods: enabledMethods - }); - - createNotification({ - text: "Login methods have been successfully updated.", - type: "success" - }); - } catch (e) { - console.error(e); + if (!enabledMethods.length) { createNotification({ type: "error", - text: "Failed to update login methods." + text: "At least one login method should be enabled." }); + return; } + + await updateServerConfig({ + enabledLoginMethods: enabledMethods + }); + + createNotification({ + text: "Login methods have been successfully updated.", + type: "success" + }); }; return ( diff --git a/frontend/src/pages/admin/CachingPage/components/CachingPageForm.tsx b/frontend/src/pages/admin/CachingPage/components/CachingPageForm.tsx index fbf956b7b..e9bdc3d1e 100644 --- a/frontend/src/pages/admin/CachingPage/components/CachingPageForm.tsx +++ b/frontend/src/pages/admin/CachingPage/components/CachingPageForm.tsx @@ -31,15 +31,10 @@ export const CachingPageForm = () => { const handleInvalidateCacheSubmit = async () => { if (!type || isInvalidating) return; - try { - await invalidateCache({ type }); - createNotification({ text: `Began invalidating ${type} cache`, type: "success" }); - setShouldPoll(true); - handlePopUpClose("invalidateCache"); - } catch (err) { - console.error(err); - createNotification({ text: `Failed to invalidate ${type} cache`, type: "error" }); - } + await invalidateCache({ type }); + createNotification({ text: `Began invalidating ${type} cache`, type: "success" }); + setShouldPoll(true); + handlePopUpClose("invalidateCache"); }; useEffect(() => { diff --git a/frontend/src/pages/admin/EncryptionPage/components/EncryptionPageForm.tsx b/frontend/src/pages/admin/EncryptionPage/components/EncryptionPageForm.tsx index 8372dd557..3dafdabd3 100644 --- a/frontend/src/pages/admin/EncryptionPage/components/EncryptionPageForm.tsx +++ b/frontend/src/pages/admin/EncryptionPage/components/EncryptionPageForm.tsx @@ -60,19 +60,12 @@ export const EncryptionPageForm = () => { return; } - try { - await updateEncryptionStrategy(formData.encryptionStrategy); + await updateEncryptionStrategy(formData.encryptionStrategy); - createNotification({ - type: "success", - text: "Encryption strategy updated successfully" - }); - } catch { - createNotification({ - type: "error", - text: "Failed to update encryption strategy" - }); - } + createNotification({ + type: "success", + text: "Encryption strategy updated successfully" + }); }, []); return ( diff --git a/frontend/src/pages/admin/EnvironmentPage/components/EnvironmentPageForm.tsx b/frontend/src/pages/admin/EnvironmentPage/components/EnvironmentPageForm.tsx index 5f74593a3..8361aa944 100644 --- a/frontend/src/pages/admin/EnvironmentPage/components/EnvironmentPageForm.tsx +++ b/frontend/src/pages/admin/EnvironmentPage/components/EnvironmentPageForm.tsx @@ -176,31 +176,19 @@ export const EnvironmentPageForm = () => { const onSubmit = useCallback( async (formData: TForm) => { - try { - const filteredFormData = Object.fromEntries( - Object.entries(formData).filter(([, value]) => value !== "") - ); - await updateServerConfig({ - envOverrides: filteredFormData - }); + const filteredFormData = Object.fromEntries( + Object.entries(formData).filter(([, value]) => value !== "") + ); + await updateServerConfig({ + envOverrides: filteredFormData + }); - createNotification({ - type: "success", - text: "Environment overrides updated successfully. It can take up to 5 minutes to take effect." - }); + createNotification({ + type: "success", + text: "Environment overrides updated successfully. It can take up to 5 minutes to take effect." + }); - reset(formData); - } catch (error) { - const errorMessage = - (error as any)?.response?.data?.message || - (error as any)?.message || - "An unknown error occurred"; - createNotification({ - type: "error", - title: "Failed to update environment overrides", - text: errorMessage - }); - } + reset(formData); }, [reset, updateServerConfig] ); diff --git a/frontend/src/pages/admin/GeneralPage/components/GeneralPageForm.tsx b/frontend/src/pages/admin/GeneralPage/components/GeneralPageForm.tsx index be92dd679..eff3e3cf8 100644 --- a/frontend/src/pages/admin/GeneralPage/components/GeneralPageForm.tsx +++ b/frontend/src/pages/admin/GeneralPage/components/GeneralPageForm.tsx @@ -68,37 +68,29 @@ export const GeneralPageForm = () => { const organizations = useGetOrganizations(); const onFormSubmit = async (formData: TDashboardForm) => { - try { - const { - allowedSignUpDomain, - trustSamlEmails, - trustLdapEmails, - trustOidcEmails, - authConsentContent, - pageFrameContent - } = formData; + const { + allowedSignUpDomain, + trustSamlEmails, + trustLdapEmails, + trustOidcEmails, + authConsentContent, + pageFrameContent + } = formData; - await updateServerConfig({ - defaultAuthOrgId: defaultAuthOrgId || null, - allowSignUp: signUpMode !== SignUpModes.Disabled, - allowedSignUpDomain: signUpMode === SignUpModes.Anyone ? allowedSignUpDomain : null, - trustSamlEmails, - trustLdapEmails, - trustOidcEmails, - authConsentContent, - pageFrameContent - }); - createNotification({ - text: "Successfully changed sign up setting.", - type: "success" - }); - } catch (e) { - console.error(e); - createNotification({ - type: "error", - text: "Failed to update sign up setting." - }); - } + await updateServerConfig({ + defaultAuthOrgId: defaultAuthOrgId || null, + allowSignUp: signUpMode !== SignUpModes.Disabled, + allowedSignUpDomain: signUpMode === SignUpModes.Anyone ? allowedSignUpDomain : null, + trustSamlEmails, + trustLdapEmails, + trustOidcEmails, + authConsentContent, + pageFrameContent + }); + createNotification({ + text: "Successfully changed sign up setting.", + type: "success" + }); }; return ( diff --git a/frontend/src/pages/admin/GeneralPage/components/UsageReportSection.tsx b/frontend/src/pages/admin/GeneralPage/components/UsageReportSection.tsx index d05800996..663aa18b7 100644 --- a/frontend/src/pages/admin/GeneralPage/components/UsageReportSection.tsx +++ b/frontend/src/pages/admin/GeneralPage/components/UsageReportSection.tsx @@ -10,23 +10,15 @@ export const UsageReportSection = () => { const generateUsageReport = useGenerateUsageReport(); const handleGenerateReport = async () => { - try { - const response = await generateUsageReport.mutateAsync(); - const { csvContent, filename } = response; + const response = await generateUsageReport.mutateAsync(); + const { csvContent, filename } = response; - downloadFile(csvContent, filename, "text/csv"); + downloadFile(csvContent, filename, "text/csv"); - createNotification({ - text: `Usage report downloaded: "${filename}"`, - type: "success" - }); - } catch (error) { - console.error("Failed to generate usage report:", error); - createNotification({ - text: "Failed to generate usage report. Please try again.", - type: "error" - }); - } + createNotification({ + text: `Usage report downloaded: "${filename}"`, + type: "success" + }); }; return ( diff --git a/frontend/src/pages/admin/ResourceOverviewPage/components/AddOrganizationModal.tsx b/frontend/src/pages/admin/ResourceOverviewPage/components/AddOrganizationModal.tsx index d577998b5..48983496f 100644 --- a/frontend/src/pages/admin/ResourceOverviewPage/components/AddOrganizationModal.tsx +++ b/frontend/src/pages/admin/ResourceOverviewPage/components/AddOrganizationModal.tsx @@ -81,25 +81,18 @@ const Content = ({ onClose }: ContentProps) => { const { users = [] } = data ?? {}; const onSubmit = async ({ name, invitees }: FormData) => { - try { - await createOrg.mutateAsync({ - name, - inviteAdminEmails: invitees - .filter((user) => Boolean(user.email)) - .map((user) => user.email) as string[] - }); + await createOrg.mutateAsync({ + name, + inviteAdminEmails: invitees + .filter((user) => Boolean(user.email)) + .map((user) => user.email) as string[] + }); - createNotification({ - type: "success", - text: "Successfully created organization" - }); - onClose(); - } catch { - createNotification({ - text: "Failed to create organization", - type: "error" - }); - } + createNotification({ + type: "success", + text: "Successfully created organization" + }); + onClose(); }; const { append } = useFieldArray({ control, name: "invitees" }); diff --git a/frontend/src/pages/admin/ResourceOverviewPage/components/MachineIdentitiesTable.tsx b/frontend/src/pages/admin/ResourceOverviewPage/components/MachineIdentitiesTable.tsx index 9929d7500..d17786712 100644 --- a/frontend/src/pages/admin/ResourceOverviewPage/components/MachineIdentitiesTable.tsx +++ b/frontend/src/pages/admin/ResourceOverviewPage/components/MachineIdentitiesTable.tsx @@ -185,18 +185,11 @@ export const MachineIdentitiesTable = () => { const handleRemoveServerAdmin = async () => { const { id } = popUp?.removeServerAdmin?.data as { id: string; name: string }; - try { - await deleteIdentitySuperAdminAccess(id); - createNotification({ - type: "success", - text: "Successfully removed server admin permissions" - }); - } catch { - createNotification({ - type: "error", - text: "Error removing server admin permissions" - }); - } + await deleteIdentitySuperAdminAccess(id); + createNotification({ + type: "success", + text: "Successfully removed server admin permissions" + }); handlePopUpClose("removeServerAdmin"); }; diff --git a/frontend/src/pages/admin/ResourceOverviewPage/components/OrganizationsTable.tsx b/frontend/src/pages/admin/ResourceOverviewPage/components/OrganizationsTable.tsx index fbb9462d0..93d0253ff 100644 --- a/frontend/src/pages/admin/ResourceOverviewPage/components/OrganizationsTable.tsx +++ b/frontend/src/pages/admin/ResourceOverviewPage/components/OrganizationsTable.tsx @@ -179,12 +179,6 @@ const ViewMembersModalContent = ({ text: "Successfully resent org invitation", type: "success" }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to resend org invitation", - type: "error" - }); } finally { setResendInviteId(null); } @@ -479,26 +473,19 @@ const OrganizationsPanelTable = ({ const { mutateAsync: accessOrganization } = useServerAdminAccessOrg(); const handleAccessOrg = async (orgId: string) => { - try { - await accessOrganization(orgId); + await accessOrganization(orgId); - navigate({ - to: "/login/select-organization", - search: { - org_id: orgId - } - }); + navigate({ + to: "/login/select-organization", + search: { + org_id: orgId + } + }); - createNotification({ - text: "Successfully joined organization", - type: "success" - }); - } catch { - createNotification({ - text: "Failed to join organization", - type: "error" - }); - } + createNotification({ + text: "Successfully joined organization", + type: "success" + }); }; return ( diff --git a/frontend/src/pages/admin/ResourceOverviewPage/components/UserIdentitiesTable.tsx b/frontend/src/pages/admin/ResourceOverviewPage/components/UserIdentitiesTable.tsx index 05d07d426..4f44a32aa 100644 --- a/frontend/src/pages/admin/ResourceOverviewPage/components/UserIdentitiesTable.tsx +++ b/frontend/src/pages/admin/ResourceOverviewPage/components/UserIdentitiesTable.tsx @@ -367,18 +367,11 @@ export const UserIdentitiesTable = () => { const handleRemoveUser = async () => { const { id } = popUp?.removeUser?.data as { id: string; username: string }; - try { - await deleteUser(id); - createNotification({ - type: "success", - text: "Successfully deleted user" - }); - } catch { - createNotification({ - type: "error", - text: "Error deleting user" - }); - } + await deleteUser(id); + createNotification({ + type: "success", + text: "Successfully deleted user" + }); handlePopUpClose("removeUser"); }; @@ -386,18 +379,11 @@ export const UserIdentitiesTable = () => { const handleGrantServerAdminAccess = async () => { const { id } = popUp?.upgradeToServerAdmin?.data as { id: string; username: string }; - try { - await grantAdminAccess(id); - createNotification({ - type: "success", - text: "Successfully granted server admin access to user" - }); - } catch { - createNotification({ - type: "error", - text: "Error granting server admin access to user" - }); - } + await grantAdminAccess(id); + createNotification({ + type: "success", + text: "Successfully granted server admin access to user" + }); handlePopUpClose("upgradeToServerAdmin"); }; @@ -405,39 +391,25 @@ export const UserIdentitiesTable = () => { const handleRemoveServerAdminAccess = async () => { const { id } = popUp?.removeServerAdmin?.data as { id: string; username: string }; - try { - await removeAdminAccess(id); - createNotification({ - type: "success", - text: "Successfully removed server admin access from user" - }); - } catch { - createNotification({ - type: "error", - text: "Error removing server admin access from user" - }); - } + await removeAdminAccess(id); + createNotification({ + type: "success", + text: "Successfully removed server admin access from user" + }); handlePopUpClose("removeServerAdmin"); }; const handleRemoveUsers = async () => { - try { - await deleteUsers(selectedUsers.map((user) => user.id)); + await deleteUsers(selectedUsers.map((user) => user.id)); - createNotification({ - text: "Successfully removed users", - type: "success" - }); + createNotification({ + text: "Successfully removed users", + type: "success" + }); - setSelectedUsers([]); - handlePopUpClose("removeUsers"); - } catch { - createNotification({ - text: "Failed to remove users", - type: "error" - }); - } + setSelectedUsers([]); + handlePopUpClose("removeUsers"); }; return ( diff --git a/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx b/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx index 04bd2f55c..81959a647 100644 --- a/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx +++ b/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx @@ -6,7 +6,6 @@ import { useNavigate } from "@tanstack/react-router"; import { AnimatePresence, motion } from "framer-motion"; import { z } from "zod"; -import { createNotification } from "@app/components/notifications"; // TODO(akhilmhdh): rewrite this into module functions in lib import SecurityClient from "@app/components/utilities/SecurityClient"; import { Button, ContentLoader, FormControl, Input } from "@app/components/v2"; @@ -46,29 +45,21 @@ export const SignUpPage = () => { const handleFormSubmit = async ({ email, password, firstName, lastName }: TFormSchema) => { // avoid multi submission if (isSubmitting) return; - try { - const res = await createAdminUser({ - email, - password, - firstName, - lastName - }); + const res = await createAdminUser({ + email, + password, + firstName, + lastName + }); - SecurityClient.setToken(res.token); - await selectOrganization({ organizationId: res.organization.id }); + SecurityClient.setToken(res.token); + await selectOrganization({ organizationId: res.organization.id }); - // TODO(akhilmhdh): This is such a confusing pattern and too unreliable - // Will be refactored in next iteration to make it url based rather than local storage ones - // Part of migration to nextjs 14 - localStorage.setItem("orgData.id", res.organization.id); - navigate({ to: "/admin" }); - } catch (err) { - console.log(err); - createNotification({ - type: "error", - text: "Failed to create admin" - }); - } + // TODO(akhilmhdh): This is such a confusing pattern and too unreliable + // Will be refactored in next iteration to make it url based rather than local storage ones + // Part of migration to nextjs 14 + localStorage.setItem("orgData.id", res.organization.id); + navigate({ to: "/admin" }); }; if (config?.initialized) return ; diff --git a/frontend/src/pages/auth/PasswordSetupPage/PasswordSetupPage.tsx b/frontend/src/pages/auth/PasswordSetupPage/PasswordSetupPage.tsx index a1dad3cc5..9626f4cc5 100644 --- a/frontend/src/pages/auth/PasswordSetupPage/PasswordSetupPage.tsx +++ b/frontend/src/pages/auth/PasswordSetupPage/PasswordSetupPage.tsx @@ -75,11 +75,7 @@ export const PasswordSetupPage = () => { setTimeout(() => { window.location.href = "/login"; }, 3000); - } catch (error) { - createNotification({ - type: "error", - text: (error as Error).message ?? "Error setting password" - }); + } catch { navigate({ to: "/personal-settings" }); } } diff --git a/frontend/src/pages/auth/SignUpSsoPage/components/EmailConfirmationStep/EmailConfirmationStep.tsx b/frontend/src/pages/auth/SignUpSsoPage/components/EmailConfirmationStep/EmailConfirmationStep.tsx index efa498749..2240fe263 100644 --- a/frontend/src/pages/auth/SignUpSsoPage/components/EmailConfirmationStep/EmailConfirmationStep.tsx +++ b/frontend/src/pages/auth/SignUpSsoPage/components/EmailConfirmationStep/EmailConfirmationStep.tsx @@ -73,67 +73,53 @@ export const EmailConfirmationStep = ({ const { mutateAsync: verifyEmailVerificationCode } = useVerifyEmailVerificationCode(); const checkCode = async () => { - try { - await verifyEmailVerificationCode({ username, code }); - setCodeError(false); + await verifyEmailVerificationCode({ username, code }); + setCodeError(false); - createNotification({ - text: "Successfully verified code", - type: "success" - }); + createNotification({ + text: "Successfully verified code", + type: "success" + }); - switch (authType) { - case UserAliasType.SAML: { - window.open(`/api/v1/sso/redirect/saml2/organizations/${organizationSlug}`); - window.close(); - break; - } - case UserAliasType.LDAP: { - navigate({ to: "/login/ldap", search: { organizationSlug } }); - break; - } - case UserAliasType.OIDC: { - window.open(`/api/v1/sso/oidc/login?orgSlug=${organizationSlug}`); - window.close(); - break; - } - default: { - setStep(1); - break; - } + switch (authType) { + case UserAliasType.SAML: { + window.open(`/api/v1/sso/redirect/saml2/organizations/${organizationSlug}`); + window.close(); + break; + } + case UserAliasType.LDAP: { + navigate({ to: "/login/ldap", search: { organizationSlug } }); + break; + } + case UserAliasType.OIDC: { + window.open(`/api/v1/sso/oidc/login?orgSlug=${organizationSlug}`); + window.close(); + break; + } + default: { + setStep(1); + break; } - } catch { - createNotification({ - text: "Failed to verify code", - type: "error" - }); } setCode(""); }; const resendCode = async () => { - try { - const queryParams = new URLSearchParams(window.location.search); - const token = queryParams.get("token"); - if (!token) { - createNotification({ - text: "Failed to resend code, no token found", - type: "error" - }); - return; - } - await sendEmailVerificationCode(token); + const queryParams = new URLSearchParams(window.location.search); + const token = queryParams.get("token"); + if (!token) { createNotification({ - text: "Successfully resent code", - type: "success" - }); - } catch { - createNotification({ - text: "Failed to resend code", + text: "Failed to resend code, no token found", type: "error" }); + return; } + await sendEmailVerificationCode(token); + createNotification({ + text: "Successfully resent code", + type: "success" + }); }; return ( diff --git a/frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertModal.tsx b/frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertModal.tsx index 5601e48db..af5bcb7ae 100644 --- a/frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertModal.tsx +++ b/frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertModal.tsx @@ -113,52 +113,44 @@ export const PkiAlertModal = ({ popUp, handlePopUpToggle }: Props) => { alertUnit, emails }: FormData) => { - try { - if (!projectId) return; + if (!projectId) return; - const emailArray = emails - .split(",") - .map((email) => email.trim()) - .filter((email) => email.length > 0); + const emailArray = emails + .split(",") + .map((email) => email.trim()) + .filter((email) => email.length > 0); - const alertBeforeDays = convertToDays(alertUnit, Number(alertBefore)); + const alertBeforeDays = convertToDays(alertUnit, Number(alertBefore)); - if (alert) { - // update - await updatePkiAlert({ - alertId: alert.id, - pkiCollectionId, - name, - projectId, - alertBeforeDays, - emails: emailArray - }); - } else { - // create - await createPkiAlert({ - name, - projectId, - pkiCollectionId, - alertBeforeDays, - emails: emailArray - }); - } - - handlePopUpToggle("pkiAlert", false); - - reset(); - - createNotification({ - text: `Successfully ${alert ? "updated" : "created"} alert`, - type: "success" + if (alert) { + // update + await updatePkiAlert({ + alertId: alert.id, + pkiCollectionId, + name, + projectId, + alertBeforeDays, + emails: emailArray }); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to ${alert ? "updated" : "created"} alert`, - type: "error" + } else { + // create + await createPkiAlert({ + name, + projectId, + pkiCollectionId, + alertBeforeDays, + emails: emailArray }); } + + handlePopUpToggle("pkiAlert", false); + + reset(); + + createNotification({ + text: `Successfully ${alert ? "updated" : "created"} alert`, + type: "success" + }); }; return ( diff --git a/frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertsSection.tsx b/frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertsSection.tsx index 329974f65..1529dfc19 100644 --- a/frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertsSection.tsx +++ b/frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertsSection.tsx @@ -22,27 +22,19 @@ export const PkiAlertsSection = () => { ] as const); const onRemoveAlertSubmit = async (alertId: string) => { - try { - if (!projectId) return; + if (!projectId) return; - await deletePkiAlert({ - alertId, - projectId - }); + await deletePkiAlert({ + alertId, + projectId + }); - createNotification({ - text: "Successfully deleted alert", - type: "success" - }); + createNotification({ + text: "Successfully deleted alert", + type: "success" + }); - handlePopUpClose("deletePkiAlert"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete alert", - type: "error" - }); - } + handlePopUpClose("deletePkiAlert"); }; return ( diff --git a/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionModal.tsx b/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionModal.tsx index 5900a1ba9..a29e9028b 100644 --- a/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionModal.tsx +++ b/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionModal.tsx @@ -62,49 +62,41 @@ export const PkiCollectionModal = ({ popUp, handlePopUpToggle }: Props) => { }, [pkiCollection]); const onFormSubmit = async ({ name, description }: FormData) => { - try { - if (!projectId) return; + if (!projectId) return; - if (pkiCollection) { - // update - await updatePkiCollection({ - collectionId: pkiCollection.id, - name, - description, - projectId - }); - } else { - // create - const { id: collectionId } = await createPkiCollection({ - name, - description, - projectId - }); - - navigate({ - to: "/projects/cert-management/$projectId/pki-collections/$collectionId", - params: { - projectId, - collectionId - } - }); - } - - handlePopUpToggle("pkiCollection", false); - - reset(); - - createNotification({ - text: `Successfully ${pkiCollection ? "updated" : "created"} PKI collection`, - type: "success" + if (pkiCollection) { + // update + await updatePkiCollection({ + collectionId: pkiCollection.id, + name, + description, + projectId }); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to ${pkiCollection ? "updated" : "created"} PKI collection`, - type: "error" + } else { + // create + const { id: collectionId } = await createPkiCollection({ + name, + description, + projectId + }); + + navigate({ + to: "/projects/cert-management/$projectId/pki-collections/$collectionId", + params: { + projectId, + collectionId + } }); } + + handlePopUpToggle("pkiCollection", false); + + reset(); + + createNotification({ + text: `Successfully ${pkiCollection ? "updated" : "created"} PKI collection`, + type: "success" + }); }; return ( diff --git a/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionSection.tsx b/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionSection.tsx index eecceaa7d..fa9cf7bea 100644 --- a/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionSection.tsx +++ b/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionSection.tsx @@ -22,27 +22,19 @@ export const PkiCollectionSection = () => { ] as const); const onRemovePkiCollectionSubmit = async (collectionId: string) => { - try { - if (!projectId) return; + if (!projectId) return; - await deletePkiCollection({ - collectionId, - projectId - }); + await deletePkiCollection({ + collectionId, + projectId + }); - createNotification({ - text: "Successfully deleted PKI collection", - type: "success" - }); + createNotification({ + text: "Successfully deleted PKI collection", + type: "success" + }); - handlePopUpClose("deletePkiCollection"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete PKI collection", - type: "error" - }); - } + handlePopUpClose("deletePkiCollection"); }; return ( diff --git a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx index ca73abd50..b464a2230 100644 --- a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx +++ b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx @@ -57,33 +57,26 @@ const Page = () => { ] as const); const onRemoveCaSubmit = async () => { - try { - if (!currentProject?.slug) return; + if (!currentProject?.slug) return; - await deleteCa({ - caName, - projectId: currentProject.id, - type: CaType.INTERNAL - }); + await deleteCa({ + caName, + projectId: currentProject.id, + type: CaType.INTERNAL + }); - createNotification({ - text: "Successfully deleted CA", - type: "success" - }); + createNotification({ + text: "Successfully deleted CA", + type: "success" + }); - handlePopUpClose("deleteCa"); - navigate({ - to: "/projects/cert-management/$projectId/certificate-authorities", - params: { - projectId - } - }); - } catch { - createNotification({ - text: "Failed to delete CA", - type: "error" - }); - } + handlePopUpClose("deleteCa"); + navigate({ + to: "/projects/cert-management/$projectId/certificate-authorities", + params: { + projectId + } + }); }; return ( diff --git a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaRenewalModal.tsx b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaRenewalModal.tsx index 857586a58..c6283da37 100644 --- a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaRenewalModal.tsx +++ b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaRenewalModal.tsx @@ -84,27 +84,23 @@ export const CaRenewalModal = ({ popUp, handlePopUpToggle }: Props) => { // }, [ca, parentCa]); const onFormSubmit = async ({ type, notAfter }: FormData) => { - try { - if (!projectSlug || !popUpData.caId) return; + if (!projectSlug || !popUpData.caId) return; - await renewCa({ - projectSlug, - caId: popUpData.caId, - notAfter, - type - }); + await renewCa({ + projectSlug, + caId: popUpData.caId, + notAfter, + type + }); - handlePopUpToggle("renewCa", false); + handlePopUpToggle("renewCa", false); - createNotification({ - text: "Successfully renewed CA", - type: "success" - }); + createNotification({ + text: "Successfully renewed CA", + type: "success" + }); - reset(); - } catch (err) { - console.error(err); - } + reset(); }; return ( diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/ExternalCaInstallForm.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/ExternalCaInstallForm.tsx index f730322c9..8a7e9690e 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/ExternalCaInstallForm.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/ExternalCaInstallForm.tsx @@ -48,29 +48,22 @@ export const ExternalCaInstallForm = ({ caId, handlePopUpToggle }: Props) => { }, []); const onFormSubmit = async ({ certificate, certificateChain }: FormData) => { - try { - if (!csr || !caId || !currentProject?.slug) return; + if (!csr || !caId || !currentProject?.slug) return; - await importCaCertificate({ - caId, - projectSlug: currentProject?.slug, - certificate, - certificateChain - }); + await importCaCertificate({ + caId, + projectSlug: currentProject?.slug, + certificate, + certificateChain + }); - reset(); + reset(); - createNotification({ - text: "Successfully installed certificate for CA", - type: "success" - }); - handlePopUpToggle("installCaCert", false); - } catch { - createNotification({ - text: "Failed to install certificate for CA", - type: "error" - }); - } + createNotification({ + text: "Successfully installed certificate for CA", + type: "success" + }); + handlePopUpToggle("installCaCert", false); }; const downloadTxtFile = (filename: string, content: string) => { diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/InternalCaInstallForm.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/InternalCaInstallForm.tsx index 5979d9711..f80e21212 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/InternalCaInstallForm.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/InternalCaInstallForm.tsx @@ -101,37 +101,30 @@ export const InternalCaInstallForm = ({ caId, handlePopUpToggle }: Props) => { }, [parentCa]); const onFormSubmit = async ({ notAfter, maxPathLength }: FormData) => { - try { - if (!csr || !caId || !currentProject?.slug) return; + if (!csr || !caId || !currentProject?.slug) return; - const { certificate, certificateChain } = await signIntermediate({ - caId: parentCaId, - csr, - maxPathLength: Number(maxPathLength), - notAfter, - notBefore: new Date().toISOString() - }); + const { certificate, certificateChain } = await signIntermediate({ + caId: parentCaId, + csr, + maxPathLength: Number(maxPathLength), + notAfter, + notBefore: new Date().toISOString() + }); - await importCaCertificate({ - caId, - projectSlug: currentProject?.slug, - certificate, - certificateChain - }); + await importCaCertificate({ + caId, + projectSlug: currentProject?.slug, + certificate, + certificateChain + }); - reset(); + reset(); - createNotification({ - text: "Successfully installed certificate for CA", - type: "success" - }); - handlePopUpToggle("installCaCert", false); - } catch { - createNotification({ - text: "Failed to install certificate for CA", - type: "error" - }); - } + createNotification({ + text: "Successfully installed certificate for CA", + type: "success" + }); + handlePopUpToggle("installCaCert", false); }; function generatePathLengthOpts(parentCaMaxPathLength: number): number[] { diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaModal.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaModal.tsx index a19a3a9c7..53434b79d 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaModal.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaModal.tsx @@ -175,48 +175,40 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { status, configuration }: FormData) => { - try { - if (!currentProject?.slug) return; + if (!currentProject?.slug) return; - if (ca) { - // update - await updateMutateAsync({ - caName: ca.name, - projectId: currentProject.id, - name, - type: CaType.INTERNAL, - status, - enableDirectIssuance - }); - } else { - // create - await createMutateAsync({ - projectId: currentProject.id, - name, - type, - status, - enableDirectIssuance, - configuration: { - ...configuration, - maxPathLength: Number(configuration.maxPathLength) - } - }); - } - - reset(); - handlePopUpToggle("ca", false); - - createNotification({ - text: `Successfully ${ca ? "updated" : "created"} CA`, - type: "success" + if (ca) { + // update + await updateMutateAsync({ + caName: ca.name, + projectId: currentProject.id, + name, + type: CaType.INTERNAL, + status, + enableDirectIssuance }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to create CA", - type: "error" + } else { + // create + await createMutateAsync({ + projectId: currentProject.id, + name, + type, + status, + enableDirectIssuance, + configuration: { + ...configuration, + maxPathLength: Number(configuration.maxPathLength) + } }); } + + reset(); + handlePopUpToggle("ca", false); + + createNotification({ + text: `Successfully ${ca ? "updated" : "created"} CA`, + type: "success" + }); }; return ( diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaSection.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaSection.tsx index e853dc801..a817042a7 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaSection.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaSection.tsx @@ -29,44 +29,29 @@ export const CaSection = () => { ] as const); const onRemoveCaSubmit = async (caName: string) => { - try { - if (!currentProject?.slug) return; + if (!currentProject?.slug) return; - await deleteCa({ caName, projectId: currentProject.id, type: CaType.INTERNAL }); + await deleteCa({ caName, projectId: currentProject.id, type: CaType.INTERNAL }); - createNotification({ - text: "Successfully deleted CA", - type: "success" - }); + createNotification({ + text: "Successfully deleted CA", + type: "success" + }); - handlePopUpClose("deleteCa"); - } catch { - createNotification({ - text: "Failed to delete CA", - type: "error" - }); - } + handlePopUpClose("deleteCa"); }; const onUpdateCaStatus = async ({ caName, status }: { caName: string; status: CaStatus }) => { - try { - if (!currentProject?.slug) return; + if (!currentProject?.slug) return; - await updateCa({ caName, projectId: currentProject.id, type: CaType.INTERNAL, status }); + await updateCa({ caName, projectId: currentProject.id, type: CaType.INTERNAL, status }); - createNotification({ - text: `Successfully ${status === CaStatus.ACTIVE ? "enabled" : "disabled"} CA`, - type: "success" - }); + createNotification({ + text: `Successfully ${status === CaStatus.ACTIVE ? "enabled" : "disabled"} CA`, + type: "success" + }); - handlePopUpClose("caStatus"); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to ${status === CaStatus.ACTIVE ? "enabled" : "disabled"} CA`, - type: "error" - }); - } + handlePopUpClose("caStatus"); }; return ( diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx index 121c32e5f..825368d1c 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx @@ -297,63 +297,55 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { status, configuration: formConfiguration }: FormData) => { - try { - if (!currentProject?.slug) return; + if (!currentProject?.slug) return; - let configPayload: any; + let configPayload: any; - if (type === CaType.ACME && "dnsAppConnection" in formConfiguration) { - configPayload = { - dnsProviderConfig: formConfiguration.dnsProviderConfig, - directoryUrl: formConfiguration.directoryUrl, - accountEmail: formConfiguration.accountEmail, - dnsAppConnectionId: formConfiguration.dnsAppConnection.id, - eabKid: formConfiguration.eabKid, - eabHmacKey: formConfiguration.eabHmacKey - }; - } else if (type === CaType.AZURE_AD_CS && "azureAdcsConnection" in formConfiguration) { - configPayload = { - azureAdcsConnectionId: formConfiguration.azureAdcsConnection.id - }; - } else { - throw new Error("Invalid certificate authority configuration"); - } + if (type === CaType.ACME && "dnsAppConnection" in formConfiguration) { + configPayload = { + dnsProviderConfig: formConfiguration.dnsProviderConfig, + directoryUrl: formConfiguration.directoryUrl, + accountEmail: formConfiguration.accountEmail, + dnsAppConnectionId: formConfiguration.dnsAppConnection.id, + eabKid: formConfiguration.eabKid, + eabHmacKey: formConfiguration.eabHmacKey + }; + } else if (type === CaType.AZURE_AD_CS && "azureAdcsConnection" in formConfiguration) { + configPayload = { + azureAdcsConnectionId: formConfiguration.azureAdcsConnection.id + }; + } else { + throw new Error("Invalid certificate authority configuration"); + } - if (ca) { - await updateMutateAsync({ - caName: ca.name, - projectId: currentProject.id, - name, - type, - status, - enableDirectIssuance: type === CaType.AZURE_AD_CS ? false : enableDirectIssuance, - configuration: configPayload - }); - } else { - await createMutateAsync({ - projectId: currentProject.id, - name, - type, - status, - enableDirectIssuance: type === CaType.AZURE_AD_CS ? false : enableDirectIssuance, - configuration: configPayload - }); - } - - reset(); - handlePopUpToggle("ca", false); - - createNotification({ - text: `Successfully ${ca ? "updated" : "created"} CA`, - type: "success" + if (ca) { + await updateMutateAsync({ + caName: ca.name, + projectId: currentProject.id, + name, + type, + status, + enableDirectIssuance: type === CaType.AZURE_AD_CS ? false : enableDirectIssuance, + configuration: configPayload }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to create CA", - type: "error" + } else { + await createMutateAsync({ + projectId: currentProject.id, + name, + type, + status, + enableDirectIssuance: type === CaType.AZURE_AD_CS ? false : enableDirectIssuance, + configuration: configPayload }); } + + reset(); + handlePopUpToggle("ca", false); + + createNotification({ + text: `Successfully ${ca ? "updated" : "created"} CA`, + type: "success" + }); }; return ( diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaSection.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaSection.tsx index 14ce122ec..b40dec6f5 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaSection.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaSection.tsx @@ -25,23 +25,16 @@ export const ExternalCaSection = () => { ] as const); const onRemoveCaSubmit = async (caName: string, type: CaType) => { - try { - if (!currentProject?.id) return; + if (!currentProject?.id) return; - await deleteCa({ caName, type, projectId: currentProject.id }); + await deleteCa({ caName, type, projectId: currentProject.id }); - createNotification({ - text: "Successfully deleted CA", - type: "success" - }); + createNotification({ + text: "Successfully deleted CA", + type: "success" + }); - handlePopUpClose("deleteCa"); - } catch { - createNotification({ - text: "Failed to delete CA", - type: "error" - }); - } + handlePopUpClose("deleteCa"); }; const onUpdateCaStatus = async ({ @@ -53,24 +46,16 @@ export const ExternalCaSection = () => { type: CaType; status: CaStatus; }) => { - try { - if (!currentProject?.slug) return; + if (!currentProject?.slug) return; - await updateCa({ caName: name, type, status, projectId: currentProject.id }); + await updateCa({ caName: name, type, status, projectId: currentProject.id }); - createNotification({ - text: `Successfully ${status === CaStatus.ACTIVE ? "enabled" : "disabled"} CA`, - type: "success" - }); + createNotification({ + text: `Successfully ${status === CaStatus.ACTIVE ? "enabled" : "disabled"} CA`, + type: "success" + }); - handlePopUpClose("caStatus"); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to ${status === CaStatus.ACTIVE ? "enable" : "disable"} CA`, - type: "error" - }); - } + handlePopUpClose("caStatus"); }; return ( diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateImportModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateImportModal.tsx index bb0bbda0b..d0434b79a 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateImportModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateImportModal.tsx @@ -71,38 +71,30 @@ export const CertificateImportModal = ({ popUp, handlePopUpToggle }: Props) => { chainPem, collectionId }: FormData) => { - try { - if (!currentProject?.slug) return; + if (!currentProject?.slug) return; - const { serialNumber, certificate, certificateChain, privateKey } = await importCertificate({ - projectSlug: currentProject.slug, + const { serialNumber, certificate, certificateChain, privateKey } = await importCertificate({ + projectSlug: currentProject.slug, - certificatePem, - privateKeyPem, - chainPem, - pkiCollectionId: collectionId - }); + certificatePem, + privateKeyPem, + chainPem, + pkiCollectionId: collectionId + }); - reset(); + reset(); - setCertificateDetails({ - serialNumber, - certificate, - certificateChain, - privateKey - }); + setCertificateDetails({ + serialNumber, + certificate, + certificateChain, + privateKey + }); - createNotification({ - text: "Successfully imported certificate", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to import certificate", - type: "error" - }); - } + createNotification({ + text: "Successfully imported certificate", + type: "success" + }); }; return ( diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal.tsx index dc0dffb0d..537229714 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal.tsx @@ -242,84 +242,72 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId } keyUsages, extendedKeyUsages }: FormData) => { - try { - if (!currentProject?.slug) { - createNotification({ - text: "Project not found. Please refresh and try again.", - type: "error" - }); - return; - } - - if (!formProfileId) { - createNotification({ - text: "Please select a certificate profile.", - type: "error" - }); - return; - } - - let commonName = ""; - if ( - constraints.shouldShowSubjectSection && - subjectAttributes && - subjectAttributes.length > 0 - ) { - commonName = getAttributeValue(subjectAttributes, "common_name"); - if (!commonName.trim()) { - createNotification({ - text: "Common name is required.", - type: "error" - }); - return; - } - } - - const certificateRequest: any = { - profileId: formProfileId, - projectSlug: currentProject.slug, - ttl, - signatureAlgorithm, - keyAlgorithm, - keyUsages: filterUsages(keyUsages) as CertKeyUsage[], - extendedKeyUsages: filterUsages(extendedKeyUsages) as CertExtendedKeyUsage[] - }; - - if (constraints.shouldShowSubjectSection && commonName) { - certificateRequest.commonName = commonName; - } - if (constraints.shouldShowSanSection && subjectAltNames && subjectAltNames.length > 0) { - const formattedSans = formatSubjectAltNames(subjectAltNames); - if (formattedSans && formattedSans.length > 0) { - certificateRequest.altNames = formattedSans; - } - } - - const { serialNumber, certificate, certificateChain, privateKey } = - await createCertificate(certificateRequest); - - setCertificateDetails({ - serialNumber, - certificate, - certificateChain, - privateKey - }); - + if (!currentProject?.slug) { createNotification({ - text: "Successfully created certificate", - type: "success" - }); - } catch (err) { - console.error("Certificate creation failed:", err); - const errorMessage = - err instanceof Error - ? err.message - : "An unexpected error occurred while creating the certificate"; - createNotification({ - text: `Failed to create certificate: ${errorMessage}`, + text: "Project not found. Please refresh and try again.", type: "error" }); + return; } + + if (!formProfileId) { + createNotification({ + text: "Please select a certificate profile.", + type: "error" + }); + return; + } + + let commonName = ""; + if ( + constraints.shouldShowSubjectSection && + subjectAttributes && + subjectAttributes.length > 0 + ) { + commonName = getAttributeValue(subjectAttributes, "common_name"); + if (!commonName.trim()) { + createNotification({ + text: "Common name is required.", + type: "error" + }); + return; + } + } + + const certificateRequest: any = { + profileId: formProfileId, + projectSlug: currentProject.slug, + ttl, + signatureAlgorithm, + keyAlgorithm, + keyUsages: filterUsages(keyUsages) as CertKeyUsage[], + extendedKeyUsages: filterUsages(extendedKeyUsages) as CertExtendedKeyUsage[] + }; + + if (constraints.shouldShowSubjectSection && commonName) { + certificateRequest.commonName = commonName; + } + if (constraints.shouldShowSanSection && subjectAltNames && subjectAltNames.length > 0) { + const formattedSans = formatSubjectAltNames(subjectAltNames); + if (formattedSans && formattedSans.length > 0) { + certificateRequest.altNames = formattedSans; + } + } + + const { serialNumber, certificate, certificateChain, privateKey } = + await createCertificate(certificateRequest); + + setCertificateDetails({ + serialNumber, + certificate, + certificateChain, + privateKey + }); + + createNotification({ + text: "Successfully created certificate", + type: "success" + }); }, [ currentProject?.slug, diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateManageRenewalModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateManageRenewalModal.tsx index d6199678a..96eb2654a 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateManageRenewalModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateManageRenewalModal.tsx @@ -163,38 +163,28 @@ export const CertificateManageRenewalModal = ({ popUp, handlePopUpToggle }: Prop }, [popUp.manageRenewal.isOpen, defaultRenewalDays, reset]); const onUpdateRenewal = async (data: FormData) => { - try { - if (!currentProject?.slug) { - createNotification({ - text: "Unable to update auto-renewal: Project not found. Please refresh the page and try again.", - type: "error" - }); - return; - } - - await updateRenewalConfig({ - certificateId: certificateData.certificateId, - renewBeforeDays: data.renewBeforeDays, - projectSlug: currentProject.slug - }); - + if (!currentProject?.slug) { createNotification({ - text: isAutoRenewalEnabled - ? "Auto-renewal configuration updated successfully" - : "Auto-renewal enabled successfully", - type: "success" - }); - - handlePopUpToggle("manageRenewal", false); - } catch (err) { - console.error(err); - createNotification({ - text: isAutoRenewalEnabled - ? "Failed to update auto-renewal configuration. Please check your inputs and try again." - : "Failed to enable auto-renewal. Please check your inputs and try again.", + text: "Unable to update auto-renewal: Project not found. Please refresh the page and try again.", type: "error" }); + return; } + + await updateRenewalConfig({ + certificateId: certificateData.certificateId, + renewBeforeDays: data.renewBeforeDays, + projectSlug: currentProject.slug + }); + + createNotification({ + text: isAutoRenewalEnabled + ? "Auto-renewal configuration updated successfully" + : "Auto-renewal enabled successfully", + type: "success" + }); + + handlePopUpToggle("manageRenewal", false); }; const getModalTitle = () => { diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateModal.tsx index 22718222a..f56f0fde8 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateModal.tsx @@ -186,45 +186,37 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { keyUsages, extendedKeyUsages }: FormData) => { - try { - if (!currentProject?.slug) return; + if (!currentProject?.slug) return; - const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate({ - caId: !selectedCertTemplate ? caId : undefined, - certificateTemplateId: selectedCertTemplate ? selectedCertTemplateId : undefined, - projectSlug: currentProject.slug, - pkiCollectionId: collectionId, - commonName, - subjectAltNames, - ttl, - keyUsages: Object.entries(keyUsages) - .filter(([, value]) => value) - .map(([key]) => key as CertKeyUsage), - extendedKeyUsages: Object.entries(extendedKeyUsages) - .filter(([, value]) => value) - .map(([key]) => key as CertExtendedKeyUsage) - }); + const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate({ + caId: !selectedCertTemplate ? caId : undefined, + certificateTemplateId: selectedCertTemplate ? selectedCertTemplateId : undefined, + projectSlug: currentProject.slug, + pkiCollectionId: collectionId, + commonName, + subjectAltNames, + ttl, + keyUsages: Object.entries(keyUsages) + .filter(([, value]) => value) + .map(([key]) => key as CertKeyUsage), + extendedKeyUsages: Object.entries(extendedKeyUsages) + .filter(([, value]) => value) + .map(([key]) => key as CertExtendedKeyUsage) + }); - reset(); + reset(); - setCertificateDetails({ - serialNumber, - certificate, - certificateChain, - privateKey - }); + setCertificateDetails({ + serialNumber, + certificate, + certificateChain, + privateKey + }); - createNotification({ - text: "Successfully created certificate", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to create certificate", - type: "error" - }); - } + createNotification({ + text: "Successfully created certificate", + type: "success" + }); }; useEffect(() => { diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateRenewalConfigModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateRenewalConfigModal.tsx index c952f1e54..039151866 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateRenewalConfigModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateRenewalConfigModal.tsx @@ -61,34 +61,26 @@ export const CertificateRenewalConfigModal = ({ popUp, handlePopUpToggle }: Prop const renewBeforeDays = watch("renewBeforeDays"); const onSubmit = async (data: FormData) => { - try { - if (!currentProject?.slug) { - createNotification({ - text: "Project not found", - type: "error" - }); - return; - } - - await updateRenewalConfig({ - certificateId: certificateData.certificateId, - renewBeforeDays: data.renewBeforeDays, - projectSlug: currentProject.slug - }); - + if (!currentProject?.slug) { createNotification({ - text: "Successfully updated auto-renewal configuration", - type: "success" - }); - - handlePopUpToggle("configureRenewal", false); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to update auto-renewal configuration", + text: "Project not found", type: "error" }); + return; } + + await updateRenewalConfig({ + certificateId: certificateData.certificateId, + renewBeforeDays: data.renewBeforeDays, + projectSlug: currentProject.slug + }); + + createNotification({ + text: "Successfully updated auto-renewal configuration", + type: "success" + }); + + handlePopUpToggle("configureRenewal", false); }; return ( diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateRenewalDisableModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateRenewalDisableModal.tsx index 613080cd7..e44d7e774 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateRenewalDisableModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateRenewalDisableModal.tsx @@ -19,34 +19,26 @@ export const CertificateRenewalDisableModal = ({ popUp, handlePopUpToggle }: Pro }; const onDisableConfirm = async () => { - try { - if (!currentProject?.slug) { - createNotification({ - text: "Project not found", - type: "error" - }); - return; - } - - await updateRenewalConfig({ - certificateId: certificateData.certificateId, - projectSlug: currentProject.slug, - enableAutoRenewal: false - }); - + if (!currentProject?.slug) { createNotification({ - text: "Successfully disabled auto-renewal", - type: "success" - }); - - handlePopUpToggle("disableRenewal", false); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to disable auto-renewal", + text: "Project not found", type: "error" }); + return; } + + await updateRenewalConfig({ + certificateId: certificateData.certificateId, + projectSlug: currentProject.slug, + enableAutoRenewal: false + }); + + createNotification({ + text: "Successfully disabled auto-renewal", + type: "success" + }); + + handlePopUpToggle("disableRenewal", false); }; return ( diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateRenewalModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateRenewalModal.tsx index 0e2b1c17d..87906e5a5 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateRenewalModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateRenewalModal.tsx @@ -18,22 +18,18 @@ export const CertificateRenewalModal = ({ popUp, handlePopUpToggle }: Props) => const { mutateAsync: renewCertificate, isPending: isRenewing } = useRenewCertificate(); const onRenewConfirm = async () => { - try { - const { certificateId } = popUp.renewCertificate.data as { certificateId: string }; + const { certificateId } = popUp.renewCertificate.data as { certificateId: string }; - await renewCertificate({ - certificateId - }); + await renewCertificate({ + certificateId + }); - createNotification({ - text: "Certificate renewed successfully", - type: "success" - }); + createNotification({ + text: "Certificate renewed successfully", + type: "success" + }); - handlePopUpToggle("renewCertificate", false); - } catch (err) { - console.error(err); - } + handlePopUpToggle("renewCertificate", false); }; const certificateData = popUp.renewCertificate.data as { diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateRevocationModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateRevocationModal.tsx index 1d1539296..d9a564fb1 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateRevocationModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateRevocationModal.tsx @@ -48,31 +48,23 @@ export const CertificateRevocationModal = ({ popUp, handlePopUpToggle }: Props) }); const onFormSubmit = async ({ revocationReason }: FormData) => { - try { - if (!currentProject?.slug) return; + if (!currentProject?.slug) return; - const { serialNumber } = popUp.revokeCertificate.data as { serialNumber: string }; + const { serialNumber } = popUp.revokeCertificate.data as { serialNumber: string }; - await revokeCertificate({ - projectSlug: currentProject.slug, - serialNumber, - revocationReason - }); + await revokeCertificate({ + projectSlug: currentProject.slug, + serialNumber, + revocationReason + }); - reset(); - handlePopUpToggle("revokeCertificate", false); + reset(); + handlePopUpToggle("revokeCertificate", false); - createNotification({ - text: "Successfully revoked certificate", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to revoke certificate", - type: "error" - }); - } + createNotification({ + text: "Successfully revoked certificate", + type: "success" + }); }; return ( diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplateModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplateModal.tsx index 9d5d9355f..66e9ec57a 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplateModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplateModal.tsx @@ -159,61 +159,53 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro return; } - try { - if (certTemplate) { - await updateCertTemplate({ - id: certTemplate.id, - projectId: currentProject.id, - pkiCollectionId: collectionId, - caId, - name, - commonName, - subjectAlternativeName, - ttl, - keyUsages: Object.entries(keyUsages) - .filter(([, value]) => value) - .map(([key]) => key as CertKeyUsage), - extendedKeyUsages: Object.entries(extendedKeyUsages) - .filter(([, value]) => value) - .map(([key]) => key as CertExtendedKeyUsage) - }); + if (certTemplate) { + await updateCertTemplate({ + id: certTemplate.id, + projectId: currentProject.id, + pkiCollectionId: collectionId, + caId, + name, + commonName, + subjectAlternativeName, + ttl, + keyUsages: Object.entries(keyUsages) + .filter(([, value]) => value) + .map(([key]) => key as CertKeyUsage), + extendedKeyUsages: Object.entries(extendedKeyUsages) + .filter(([, value]) => value) + .map(([key]) => key as CertExtendedKeyUsage) + }); - createNotification({ - text: "Successfully updated certificate template", - type: "success" - }); - } else { - await createCertTemplate({ - projectId: currentProject.id, - pkiCollectionId: collectionId, - caId, - name, - commonName, - subjectAlternativeName, - ttl, - keyUsages: Object.entries(keyUsages) - .filter(([, value]) => value) - .map(([key]) => key as CertKeyUsage), - extendedKeyUsages: Object.entries(extendedKeyUsages) - .filter(([, value]) => value) - .map(([key]) => key as CertExtendedKeyUsage) - }); - - createNotification({ - text: "Successfully created certificate template", - type: "success" - }); - } - - reset(); - handlePopUpToggle("certificateTemplate", false); - } catch (err) { - console.error(err); createNotification({ - text: "Failed to save changes", - type: "error" + text: "Successfully updated certificate template", + type: "success" + }); + } else { + await createCertTemplate({ + projectId: currentProject.id, + pkiCollectionId: collectionId, + caId, + name, + commonName, + subjectAlternativeName, + ttl, + keyUsages: Object.entries(keyUsages) + .filter(([, value]) => value) + .map(([key]) => key as CertKeyUsage), + extendedKeyUsages: Object.entries(extendedKeyUsages) + .filter(([, value]) => value) + .map(([key]) => key as CertExtendedKeyUsage) + }); + + createNotification({ + text: "Successfully created certificate template", + type: "success" }); } + + reset(); + handlePopUpToggle("certificateTemplate", false); }; return ( diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplatesSection.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplatesSection.tsx index 629553200..ce55c7e80 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplatesSection.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplatesSection.tsx @@ -41,25 +41,17 @@ export const CertificateTemplatesSection = ({ caId }: Props) => { return; } - try { - await deleteCertTemplate({ - id, - projectId: currentProject.id - }); + await deleteCertTemplate({ + id, + projectId: currentProject.id + }); - createNotification({ - text: "Successfully deleted certificate template", - type: "success" - }); + createNotification({ + text: "Successfully deleted certificate template", + type: "success" + }); - handlePopUpClose("deleteCertificateTemplate"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete certificate template", - type: "error" - }); - } + handlePopUpClose("deleteCertificateTemplate"); }; return ( diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx index 4102d8ea5..0252b1238 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx @@ -41,24 +41,16 @@ export const CertificatesSection = () => { ] as const); const onRemoveCertificateSubmit = async (serialNumber: string) => { - try { - if (!currentProject?.slug) return; + if (!currentProject?.slug) return; - await deleteCert({ serialNumber, projectSlug: currentProject.slug }); + await deleteCert({ serialNumber, projectSlug: currentProject.slug }); - createNotification({ - text: "Successfully deleted certificate", - type: "success" - }); + createNotification({ + text: "Successfully deleted certificate", + type: "success" + }); - handlePopUpClose("deleteCertificate"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete certificate", - type: "error" - }); - } + handlePopUpClose("deleteCertificate"); }; return ( diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx index 500286d67..77f287ac5 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx @@ -200,32 +200,24 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { }, [caData]); const handleDisableAutoRenewal = async (certificateId: string, commonName: string) => { - try { - if (!currentProject?.slug) { - createNotification({ - text: "Unable to disable auto-renewal: Project not found. Please refresh the page and try again.", - type: "error" - }); - return; - } - - await updateRenewalConfig({ - certificateId, - projectSlug: currentProject.slug, - enableAutoRenewal: false - }); - + if (!currentProject?.slug) { createNotification({ - text: `Auto-renewal disabled for ${commonName}`, - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to disable auto-renewal. Please try again or contact support if the issue persists.", + text: "Unable to disable auto-renewal: Project not found. Please refresh the page and try again.", type: "error" }); + return; } + + await updateRenewalConfig({ + certificateId, + projectSlug: currentProject.slug, + enableAutoRenewal: false + }); + + createNotification({ + text: `Auto-renewal disabled for ${commonName}`, + type: "success" + }); }; return ( diff --git a/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncsTable.tsx b/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncsTable.tsx index ca4aa7473..f997c350d 100644 --- a/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncsTable.tsx +++ b/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncsTable.tsx @@ -226,46 +226,32 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => { const isAutoSyncEnabled = !pkiSync.isAutoSyncEnabled; - try { - await updateSync.mutateAsync({ - syncId: pkiSync.id, - projectId: pkiSync.projectId, - destination: pkiSync.destination, - isAutoSyncEnabled - }); + await updateSync.mutateAsync({ + syncId: pkiSync.id, + projectId: pkiSync.projectId, + destination: pkiSync.destination, + isAutoSyncEnabled + }); - createNotification({ - text: `Successfully ${isAutoSyncEnabled ? "enabled" : "disabled"} auto-sync for ${destinationName} Sync`, - type: "success" - }); - } catch { - createNotification({ - text: `Failed to ${isAutoSyncEnabled ? "enable" : "disable"} auto-sync for ${destinationName} Sync`, - type: "error" - }); - } + createNotification({ + text: `Successfully ${isAutoSyncEnabled ? "enabled" : "disabled"} auto-sync for ${destinationName} Sync`, + type: "success" + }); }; const handleTriggerSync = async (pkiSync: TPkiSync) => { const destinationName = PKI_SYNC_MAP[pkiSync.destination].name; - try { - await triggerSync.mutateAsync({ - syncId: pkiSync.id, - destination: pkiSync.destination, - projectId: pkiSync.projectId - }); + await triggerSync.mutateAsync({ + syncId: pkiSync.id, + destination: pkiSync.destination, + projectId: pkiSync.projectId + }); - createNotification({ - text: `Successfully triggered ${destinationName} Sync`, - type: "success" - }); - } catch { - createNotification({ - text: `Failed to trigger ${destinationName} Sync`, - type: "error" - }); - } + createNotification({ + text: `Successfully triggered ${destinationName} Sync`, + type: "success" + }); }; return ( diff --git a/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx b/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx index b8235f488..064f8f7a2 100644 --- a/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx +++ b/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx @@ -45,31 +45,24 @@ export const PkiCollectionPage = () => { ] as const); const onDeletePkiCollectionSubmit = async (collectionIdToDelete: string) => { - try { - if (!projectId) return; + if (!projectId) return; - await deletePkiCollection({ - projectId, - collectionId: collectionIdToDelete - }); + await deletePkiCollection({ + projectId, + collectionId: collectionIdToDelete + }); - createNotification({ - text: "Successfully deleted PKI collection", - type: "success" - }); - handlePopUpClose("deletePkiCollection"); - navigate({ - to: "/projects/cert-management/$projectId/certificates", - params: { - projectId - } - }); - } catch { - createNotification({ - text: "Failed to delete PKI collection", - type: "error" - }); - } + createNotification({ + text: "Successfully deleted PKI collection", + type: "success" + }); + handlePopUpClose("deletePkiCollection"); + navigate({ + to: "/projects/cert-management/$projectId/certificates", + params: { + projectId + } + }); }; return ( diff --git a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/PkiSubscriberDetailsByIDPage.tsx b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/PkiSubscriberDetailsByIDPage.tsx index 1f723b395..572ee998e 100644 --- a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/PkiSubscriberDetailsByIDPage.tsx +++ b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/PkiSubscriberDetailsByIDPage.tsx @@ -51,30 +51,22 @@ const Page = () => { ] as const); const onRemoveSubscriberSubmit = async (subscriberNameToDelete: string) => { - try { - if (!projectId) return; + if (!projectId) return; - await deletePkiSubscriber({ subscriberName: subscriberNameToDelete, projectId }); + await deletePkiSubscriber({ subscriberName: subscriberNameToDelete, projectId }); - createNotification({ - text: "Successfully deleted subscriber", - type: "success" - }); + createNotification({ + text: "Successfully deleted subscriber", + type: "success" + }); - handlePopUpClose("deletePkiSubscriber"); - navigate({ - to: "/projects/cert-management/$projectId/subscribers", - params: { - projectId - } - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete subscriber", - type: "error" - }); - } + handlePopUpClose("deletePkiSubscriber"); + navigate({ + to: "/projects/cert-management/$projectId/subscribers", + params: { + projectId + } + }); }; return ( diff --git a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberDetailsSection.tsx b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberDetailsSection.tsx index f9c94740a..5da5be55a 100644 --- a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberDetailsSection.tsx +++ b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberDetailsSection.tsx @@ -70,36 +70,28 @@ export const PkiSubscriberDetailsSection = ({ subscriberName, handlePopUpOpen }: useOrderPkiSubscriberCert(); const onIssuePkiSubscriberCert = async () => { - try { - if (pkiSubscriber?.supportsImmediateCertIssuance) { - const response = await issuePkiSubscriberCert({ subscriberName, projectId }); + if (pkiSubscriber?.supportsImmediateCertIssuance) { + const response = await issuePkiSubscriberCert({ subscriberName, projectId }); - setCertificateDetails({ - serialNumber: response.serialNumber, - certificate: response.certificate, - certificateChain: response.certificateChain, - privateKey: response.privateKey - }); + setCertificateDetails({ + serialNumber: response.serialNumber, + certificate: response.certificate, + certificateChain: response.certificateChain, + privateKey: response.privateKey + }); - setIsModalOpen(true); + setIsModalOpen(true); - createNotification({ - text: "Successfully issued certificate", - type: "success" - }); - } else { - await orderPkiSubscriberCert({ subscriberName, projectId }); - - createNotification({ - text: "Successfully ordered certificate. It will be issued after CA processing which could take a few minutes.", - type: "info" - }); - } - } catch (err) { - console.error(err); createNotification({ - text: "Failed to issue certificate", - type: "error" + text: "Successfully issued certificate", + type: "success" + }); + } else { + await orderPkiSubscriberCert({ subscriberName, projectId }); + + createNotification({ + text: "Successfully ordered certificate. It will be issued after CA processing which could take a few minutes.", + type: "info" }); } }; diff --git a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberModal.tsx b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberModal.tsx index 4bc427e48..15c016587 100644 --- a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberModal.tsx +++ b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberModal.tsx @@ -276,117 +276,109 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => { locality, emailAddress }: FormData) => { - try { - if (!projectId) return; - - if (!caId) { - createNotification({ - text: "Please select an Issuing CA", - type: "error" - }); - return; - } - - // Check if there is already a different subscriber with the same name - const existingNames = - subscribers?.filter((s) => s.id !== pkiSubscriber?.id).map((s) => s.name) || []; - - if (existingNames.includes(name.trim())) { - createNotification({ - text: "A subscriber with this name already exists.", - type: "error" - }); - return; - } - - // Validate Azure template for Azure ADCS CA - if (selectedCa?.type === CaType.AZURE_AD_CS && !azureTemplateType) { - createNotification({ - text: "Please select an Azure certificate template", - type: "error" - }); - return; - } - - const keyUsagesList = - selectedCa?.type === CaType.AZURE_AD_CS - ? [] - : Object.entries(keyUsages) - .filter(([, value]) => value) - .map(([key]) => key as CertKeyUsage); - - const extendedKeyUsagesList = - selectedCa?.type === CaType.AZURE_AD_CS - ? [] - : Object.entries(extendedKeyUsages) - .filter(([, value]) => value) - .map(([key]) => key as CertExtendedKeyUsage); - - const subjectAlternativeNamesList = subjectAlternativeNames - .split(",") - .map((san) => san.trim()) - .filter(Boolean); - - const autoRenewalPeriodInDays = enableAutoRenewal - ? convertTimeUnitValueToDays(renewalUnit, renewalBefore) - : undefined; - - // Build properties object - const properties = { - ...(selectedCa?.type === CaType.AZURE_AD_CS && azureTemplateType && { azureTemplateType }), - ...(organization && { organization }), - ...(organizationalUnit && { organizationalUnit }), - ...(country && { country }), - ...(state && { state }), - ...(locality && { locality }), - ...(emailAddress && { emailAddress }) - }; - - if (pkiSubscriber) { - await updateMutateAsync({ - subscriberName: pkiSubscriber.name, - projectId, - name, - caId, - commonName, - subjectAlternativeNames: subjectAlternativeNamesList, - ttl, - keyUsages: keyUsagesList, - extendedKeyUsages: extendedKeyUsagesList, - enableAutoRenewal, - autoRenewalPeriodInDays, - properties: Object.keys(properties).length > 0 ? properties : undefined - }); - } else { - await createMutateAsync({ - projectId, - name, - caId, - commonName, - subjectAlternativeNames: subjectAlternativeNamesList, - ttl, - keyUsages: keyUsagesList, - extendedKeyUsages: extendedKeyUsagesList, - enableAutoRenewal, - autoRenewalPeriodInDays, - properties: Object.keys(properties).length > 0 ? properties : undefined - }); - } - - reset(); - handlePopUpToggle("pkiSubscriber", false); + if (!projectId) return; + if (!caId) { createNotification({ - text: `Successfully ${pkiSubscriber ? "updated" : "added"} PKI subscriber`, - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to ${pkiSubscriber ? "update" : "add"} PKI subscriber`, + text: "Please select an Issuing CA", type: "error" }); + return; } + + // Check if there is already a different subscriber with the same name + const existingNames = + subscribers?.filter((s) => s.id !== pkiSubscriber?.id).map((s) => s.name) || []; + + if (existingNames.includes(name.trim())) { + createNotification({ + text: "A subscriber with this name already exists.", + type: "error" + }); + return; + } + + // Validate Azure template for Azure ADCS CA + if (selectedCa?.type === CaType.AZURE_AD_CS && !azureTemplateType) { + createNotification({ + text: "Please select an Azure certificate template", + type: "error" + }); + return; + } + + const keyUsagesList = + selectedCa?.type === CaType.AZURE_AD_CS + ? [] + : Object.entries(keyUsages) + .filter(([, value]) => value) + .map(([key]) => key as CertKeyUsage); + + const extendedKeyUsagesList = + selectedCa?.type === CaType.AZURE_AD_CS + ? [] + : Object.entries(extendedKeyUsages) + .filter(([, value]) => value) + .map(([key]) => key as CertExtendedKeyUsage); + + const subjectAlternativeNamesList = subjectAlternativeNames + .split(",") + .map((san) => san.trim()) + .filter(Boolean); + + const autoRenewalPeriodInDays = enableAutoRenewal + ? convertTimeUnitValueToDays(renewalUnit, renewalBefore) + : undefined; + + // Build properties object + const properties = { + ...(selectedCa?.type === CaType.AZURE_AD_CS && azureTemplateType && { azureTemplateType }), + ...(organization && { organization }), + ...(organizationalUnit && { organizationalUnit }), + ...(country && { country }), + ...(state && { state }), + ...(locality && { locality }), + ...(emailAddress && { emailAddress }) + }; + + if (pkiSubscriber) { + await updateMutateAsync({ + subscriberName: pkiSubscriber.name, + projectId, + name, + caId, + commonName, + subjectAlternativeNames: subjectAlternativeNamesList, + ttl, + keyUsages: keyUsagesList, + extendedKeyUsages: extendedKeyUsagesList, + enableAutoRenewal, + autoRenewalPeriodInDays, + properties: Object.keys(properties).length > 0 ? properties : undefined + }); + } else { + await createMutateAsync({ + projectId, + name, + caId, + commonName, + subjectAlternativeNames: subjectAlternativeNamesList, + ttl, + keyUsages: keyUsagesList, + extendedKeyUsages: extendedKeyUsagesList, + enableAutoRenewal, + autoRenewalPeriodInDays, + properties: Object.keys(properties).length > 0 ? properties : undefined + }); + } + + reset(); + handlePopUpToggle("pkiSubscriber", false); + + createNotification({ + text: `Successfully ${pkiSubscriber ? "updated" : "added"} PKI subscriber`, + type: "success" + }); }; return ( diff --git a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberSection.tsx b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberSection.tsx index cb4b9ef39..e902711ce 100644 --- a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberSection.tsx +++ b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberSection.tsx @@ -33,22 +33,14 @@ export const PkiSubscriberSection = () => { ] as const); const onRemovePkiSubscriberSubmit = async (subscriberName: string) => { - try { - const subscriber = await deletePkiSubscriber({ subscriberName, projectId }); + const subscriber = await deletePkiSubscriber({ subscriberName, projectId }); - createNotification({ - text: `Successfully deleted PKI subscriber: ${subscriber.name}`, - type: "success" - }); + createNotification({ + text: `Successfully deleted PKI subscriber: ${subscriber.name}`, + type: "success" + }); - handlePopUpClose("deletePkiSubscriber"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete PKI subscriber", - type: "error" - }); - } + handlePopUpClose("deletePkiSubscriber"); }; const onUpdatePkiSubscriberStatus = async ({ @@ -58,24 +50,16 @@ export const PkiSubscriberSection = () => { subscriberName: string; status: PkiSubscriberStatus; }) => { - try { - if (!currentProject?.slug) return; + if (!currentProject?.slug) return; - await updatePkiSubscriber({ subscriberName, projectId, status }); + await updatePkiSubscriber({ subscriberName, projectId, status }); - createNotification({ - text: `Successfully ${status === PkiSubscriberStatus.ACTIVE ? "enabled" : "disabled"} subscriber`, - type: "success" - }); + createNotification({ + text: `Successfully ${status === PkiSubscriberStatus.ACTIVE ? "enabled" : "disabled"} subscriber`, + type: "success" + }); - handlePopUpClose("pkiSubscriberStatus"); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to ${status === PkiSubscriberStatus.ACTIVE ? "enable" : "disable"} subscriber`, - type: "error" - }); - } + handlePopUpClose("pkiSubscriberStatus"); }; const subscriberStatusData = popUp?.pkiSubscriberStatus?.data as { diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncActionTriggers.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncActionTriggers.tsx index bb8e9d02a..5171b4d95 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncActionTriggers.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncActionTriggers.tsx @@ -86,44 +86,28 @@ export const PkiSyncActionTriggers = ({ pkiSync }: Props) => { }, [pkiSync.id, setIsIdCopied]); const handleTriggerSync = useCallback(async () => { - try { - await triggerSyncMutation.mutateAsync({ - syncId: id, - destination, - projectId - }); - createNotification({ - text: "PKI sync job queued successfully", - type: "success" - }); - } catch (error) { - console.error("Failed to trigger sync:", error); - createNotification({ - text: "Failed to trigger PKI sync", - type: "error" - }); - } + await triggerSyncMutation.mutateAsync({ + syncId: id, + destination, + projectId + }); + createNotification({ + text: "PKI sync job queued successfully", + type: "success" + }); }, [triggerSyncMutation, id, destination, projectId]); const handleToggleAutoSync = useCallback(async () => { - try { - await updatePkiSyncMutation.mutateAsync({ - syncId: id, - projectId, - destination, - isAutoSyncEnabled: !pkiSync.isAutoSyncEnabled - }); - createNotification({ - text: `Auto-sync ${pkiSync.isAutoSyncEnabled ? "disabled" : "enabled"} successfully`, - type: "success" - }); - } catch (error) { - console.error("Failed to toggle auto-sync:", error); - createNotification({ - text: "Failed to toggle auto-sync", - type: "error" - }); - } + await updatePkiSyncMutation.mutateAsync({ + syncId: id, + projectId, + destination, + isAutoSyncEnabled: !pkiSync.isAutoSyncEnabled + }); + createNotification({ + text: `Auto-sync ${pkiSync.isAutoSyncEnabled ? "disabled" : "enabled"} successfully`, + type: "success" + }); }, [updatePkiSyncMutation, id, projectId, pkiSync.isAutoSyncEnabled]); const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { diff --git a/frontend/src/pages/cert-manager/PkiTemplateListPage/PkiTemplateListPage.tsx b/frontend/src/pages/cert-manager/PkiTemplateListPage/PkiTemplateListPage.tsx index b395d0ec2..9ad290062 100644 --- a/frontend/src/pages/cert-manager/PkiTemplateListPage/PkiTemplateListPage.tsx +++ b/frontend/src/pages/cert-manager/PkiTemplateListPage/PkiTemplateListPage.tsx @@ -78,25 +78,17 @@ export const PkiTemplateListPage = () => { const deleteCertTemplate = useDeleteCertTemplateV2(); const onRemovePkiSubscriberSubmit = async () => { - try { - const pkiTemplate = await deleteCertTemplate.mutateAsync({ - projectId: currentProject.id, - templateName: popUp?.deleteTemplate?.data?.name - }); + const pkiTemplate = await deleteCertTemplate.mutateAsync({ + projectId: currentProject.id, + templateName: popUp?.deleteTemplate?.data?.name + }); - createNotification({ - text: `Successfully deleted PKI template: ${pkiTemplate.name}`, - type: "success" - }); + createNotification({ + text: `Successfully deleted PKI template: ${pkiTemplate.name}`, + type: "success" + }); - handlePopUpClose("deleteTemplate"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete PKI subscriber", - type: "error" - }); - } + handlePopUpClose("deleteTemplate"); }; return ( diff --git a/frontend/src/pages/cert-manager/PkiTemplateListPage/components/PkiTemplateForm.tsx b/frontend/src/pages/cert-manager/PkiTemplateListPage/components/PkiTemplateForm.tsx index 093a2fcc1..00aa192a6 100644 --- a/frontend/src/pages/cert-manager/PkiTemplateListPage/components/PkiTemplateForm.tsx +++ b/frontend/src/pages/cert-manager/PkiTemplateListPage/components/PkiTemplateForm.tsx @@ -128,59 +128,51 @@ export const PkiTemplateForm = ({ certTemplate, handlePopUpToggle }: Props) => { return; } - try { - if (certTemplate) { - await updateCertTemplate({ - templateName: certTemplate.name, - projectId: currentProject.id, - caName: ca.name, - name, - commonName, - subjectAlternativeName, - ttl, - keyUsages: Object.entries(keyUsages) - .filter(([, value]) => value) - .map(([key]) => key as CertKeyUsage), - extendedKeyUsages: Object.entries(extendedKeyUsages) - .filter(([, value]) => value) - .map(([key]) => key as CertExtendedKeyUsage) - }); + if (certTemplate) { + await updateCertTemplate({ + templateName: certTemplate.name, + projectId: currentProject.id, + caName: ca.name, + name, + commonName, + subjectAlternativeName, + ttl, + keyUsages: Object.entries(keyUsages) + .filter(([, value]) => value) + .map(([key]) => key as CertKeyUsage), + extendedKeyUsages: Object.entries(extendedKeyUsages) + .filter(([, value]) => value) + .map(([key]) => key as CertExtendedKeyUsage) + }); - createNotification({ - text: "Successfully updated certificate template", - type: "success" - }); - } else { - await createCertTemplate({ - projectId: currentProject.id, - caName: ca.name, - name, - commonName, - subjectAlternativeName, - ttl, - keyUsages: Object.entries(keyUsages) - .filter(([, value]) => value) - .map(([key]) => key as CertKeyUsage), - extendedKeyUsages: Object.entries(extendedKeyUsages) - .filter(([, value]) => value) - .map(([key]) => key as CertExtendedKeyUsage) - }); - - createNotification({ - text: "Successfully created certificate template", - type: "success" - }); - } - - reset(); - handlePopUpToggle(false); - } catch (err) { - console.error(err); createNotification({ - text: "Failed to save changes", - type: "error" + text: "Successfully updated certificate template", + type: "success" + }); + } else { + await createCertTemplate({ + projectId: currentProject.id, + caName: ca.name, + name, + commonName, + subjectAlternativeName, + ttl, + keyUsages: Object.entries(keyUsages) + .filter(([, value]) => value) + .map(([key]) => key as CertKeyUsage), + extendedKeyUsages: Object.entries(extendedKeyUsages) + .filter(([, value]) => value) + .map(([key]) => key as CertExtendedKeyUsage) + }); + + createNotification({ + text: "Successfully created certificate template", + type: "success" }); } + + reset(); + handlePopUpToggle(false); }; return ( diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx index d034aeda1..7938d04f5 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx @@ -51,22 +51,15 @@ export const CertificateProfilesTab = () => { const handleDeleteConfirm = async () => { if (!selectedProfile) return; - try { - await deleteProfile.mutateAsync({ - profileId: selectedProfile.id - }); - setIsDeleteModalOpen(false); - setSelectedProfile(null); - createNotification({ - text: `Certificate profile "${selectedProfile.slug}" deleted successfully`, - type: "success" - }); - } catch (error) { - console.error( - `Failed to delete profile "${selectedProfile.slug}" (ID: ${selectedProfile.id}):`, - error - ); - } + await deleteProfile.mutateAsync({ + profileId: selectedProfile.id + }); + setIsDeleteModalOpen(false); + setSelectedProfile(null); + createNotification({ + text: `Certificate profile "${selectedProfile.slug}" deleted successfully`, + type: "success" + }); }; return ( diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx index ea82fd843..56e72ca2b 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx @@ -236,64 +236,56 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" } }, [isEdit, profile, reset]); const onFormSubmit = async (data: FormData) => { - try { - if (!currentProject?.id && !isEdit) return; + if (!currentProject?.id && !isEdit) return; - if (isEdit) { - const updateData: TUpdateCertificateProfileDTO = { - profileId: profile.id, - slug: data.slug, - description: data.description - }; + if (isEdit) { + const updateData: TUpdateCertificateProfileDTO = { + profileId: profile.id, + slug: data.slug, + description: data.description + }; - if (data.enrollmentType === "est" && data.estConfig) { - updateData.estConfig = data.estConfig; - } else if (data.enrollmentType === "api" && data.apiConfig) { - updateData.apiConfig = data.apiConfig; - } - - await updateProfile.mutateAsync(updateData); - } else { - if (!currentProject?.id) { - throw new Error("Project ID is required for creating a profile"); - } - - const createData: TCreateCertificateProfileDTO = { - projectId: currentProject.id, - slug: data.slug, - description: data.description, - enrollmentType: data.enrollmentType, - caId: data.certificateAuthorityId, - certificateTemplateId: data.certificateTemplateId - }; - - if (data.enrollmentType === "est" && data.estConfig) { - createData.estConfig = { - passphrase: data.estConfig.passphrase, - caChain: data.estConfig.caChain || undefined, - disableBootstrapCaValidation: data.estConfig.disableBootstrapCaValidation - }; - } else if (data.enrollmentType === "api" && data.apiConfig) { - createData.apiConfig = data.apiConfig; - } - - await createProfile.mutateAsync(createData); + if (data.enrollmentType === "est" && data.estConfig) { + updateData.estConfig = data.estConfig; + } else if (data.enrollmentType === "api" && data.apiConfig) { + updateData.apiConfig = data.apiConfig; } - createNotification({ - text: `Certificate profile ${isEdit ? "updated" : "created"} successfully`, - type: "success" - }); + await updateProfile.mutateAsync(updateData); + } else { + if (!currentProject?.id) { + throw new Error("Project ID is required for creating a profile"); + } - reset(); - onClose(); - } catch (error) { - console.error(`Error ${isEdit ? "updating" : "creating"} profile:`, error); - createNotification({ - text: `Failed to ${isEdit ? "update" : "create"} certificate profile`, - type: "error" - }); + const createData: TCreateCertificateProfileDTO = { + projectId: currentProject.id, + slug: data.slug, + description: data.description, + enrollmentType: data.enrollmentType, + caId: data.certificateAuthorityId, + certificateTemplateId: data.certificateTemplateId + }; + + if (data.enrollmentType === "est" && data.estConfig) { + createData.estConfig = { + passphrase: data.estConfig.passphrase, + caChain: data.estConfig.caChain || undefined, + disableBootstrapCaValidation: data.estConfig.disableBootstrapCaValidation + }; + } else if (data.enrollmentType === "api" && data.apiConfig) { + createData.apiConfig = data.apiConfig; + } + + await createProfile.mutateAsync(createData); } + + createNotification({ + text: `Certificate profile ${isEdit ? "updated" : "created"} successfully`, + type: "success" + }); + + reset(); + onClose(); }; return ( diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CertificateTemplatesV2Tab.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CertificateTemplatesV2Tab.tsx index ae14f2dea..ec660b339 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CertificateTemplatesV2Tab.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CertificateTemplatesV2Tab.tsx @@ -48,23 +48,15 @@ export const CertificateTemplatesV2Tab = () => { const handleDeleteConfirm = async () => { if (!selectedTemplate) return; - try { - await deleteTemplateV2.mutateAsync({ - templateId: selectedTemplate.id - }); - setIsDeleteModalOpen(false); - setSelectedTemplate(null); - createNotification({ - text: `Certificate template "${selectedTemplate.name}" deleted successfully`, - type: "success" - }); - } catch (error) { - console.error("Failed to delete template:", error); - createNotification({ - text: "Failed to delete certificate template", - type: "error" - }); - } + await deleteTemplateV2.mutateAsync({ + templateId: selectedTemplate.id + }); + setIsDeleteModalOpen(false); + setSelectedTemplate(null); + createNotification({ + text: `Certificate template "${selectedTemplate.name}" deleted successfully`, + type: "success" + }); }; return ( diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CreateTemplateModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CreateTemplateModal.tsx index a96beccad..1387171fb 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CreateTemplateModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CreateTemplateModal.tsx @@ -407,59 +407,51 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" }; const onFormSubmit = async (data: FormData) => { - try { - if (!currentProject?.id && !isEdit) return; + if (!currentProject?.id && !isEdit) return; - const hasEmptyAttributeValues = data.attributes?.some( - (attr) => !attr.value || attr.value.length === 0 || attr.value.some((v) => !v.trim()) - ); + const hasEmptyAttributeValues = data.attributes?.some( + (attr) => !attr.value || attr.value.length === 0 || attr.value.some((v) => !v.trim()) + ); - const hasEmptySanValues = data.subjectAlternativeNames?.some( - (san) => !san.value || san.value.length === 0 || san.value.some((v) => !v.trim()) - ); - - if (hasEmptyAttributeValues || hasEmptySanValues) { - createNotification({ - text: "All values must be non-empty. Use wildcards (*) if needed.", - type: "error" - }); - return; - } - - const transformedData = transformToApiFormat(data); - - if (isEdit) { - const updateData = { - templateId: template.id, - ...transformedData - }; - await updateTemplate.mutateAsync(updateData); - } else { - if (!currentProject?.id) { - throw new Error("Project ID is required for creating a template"); - } - - const createData = { - projectId: currentProject.id, - ...transformedData - }; - await createTemplate.mutateAsync(createData); - } + const hasEmptySanValues = data.subjectAlternativeNames?.some( + (san) => !san.value || san.value.length === 0 || san.value.some((v) => !v.trim()) + ); + if (hasEmptyAttributeValues || hasEmptySanValues) { createNotification({ - text: `Certificate template ${isEdit ? "updated" : "created"} successfully`, - type: "success" - }); - - reset(); - onClose(); - } catch (error) { - console.error(`Error ${isEdit ? "updating" : "creating"} template:`, error); - createNotification({ - text: `Failed to ${isEdit ? "update" : "create"} certificate template`, + text: "All values must be non-empty. Use wildcards (*) if needed.", type: "error" }); + return; } + + const transformedData = transformToApiFormat(data); + + if (isEdit) { + const updateData = { + templateId: template.id, + ...transformedData + }; + await updateTemplate.mutateAsync(updateData); + } else { + if (!currentProject?.id) { + throw new Error("Project ID is required for creating a template"); + } + + const createData = { + projectId: currentProject.id, + ...transformedData + }; + await createTemplate.mutateAsync(createData); + } + + createNotification({ + text: `Certificate template ${isEdit ? "updated" : "created"} successfully`, + type: "success" + }); + + reset(); + onClose(); }; const addAttribute = () => { diff --git a/frontend/src/pages/kms/KmipPage/components/DeleteKmipClientModal.tsx b/frontend/src/pages/kms/KmipPage/components/DeleteKmipClientModal.tsx index fe9c956ab..2d6accf76 100644 --- a/frontend/src/pages/kms/KmipPage/components/DeleteKmipClientModal.tsx +++ b/frontend/src/pages/kms/KmipPage/components/DeleteKmipClientModal.tsx @@ -17,28 +17,17 @@ export const DeleteKmipClientModal = ({ isOpen, onOpenChange, kmipClient }: Prop const { id, projectId, name } = kmipClient; const handleDeleteKmipClient = async () => { - try { - await deleteKmipClients.mutateAsync({ - id, - projectId - }); + await deleteKmipClients.mutateAsync({ + id, + projectId + }); - createNotification({ - text: "KMIP client successfully deleted", - type: "success" - }); + createNotification({ + text: "KMIP client successfully deleted", + type: "success" + }); - onOpenChange(false); - } catch (err) { - console.error(err); - const error = err as any; - const text = error?.response?.data?.message ?? "Failed to delete KMIP client"; - - createNotification({ - text, - type: "error" - }); - } + onOpenChange(false); }; return ( diff --git a/frontend/src/pages/kms/KmipPage/components/KmipClientModal.tsx b/frontend/src/pages/kms/KmipPage/components/KmipClientModal.tsx index b3f3fb380..f10d077a4 100644 --- a/frontend/src/pages/kms/KmipPage/components/KmipClientModal.tsx +++ b/frontend/src/pages/kms/KmipPage/components/KmipClientModal.tsx @@ -98,20 +98,12 @@ const KmipClientForm = ({ onComplete, kmipClient }: FormProps) => { .map(([key]) => key as KmipPermission) }); - try { - await mutation; - createNotification({ - text: `Successfully ${isUpdate ? "updated" : "added"} KMIP client`, - type: "success" - }); - onComplete(); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to ${isUpdate ? "update" : "add"} KMIP client`, - type: "error" - }); - } + await mutation; + createNotification({ + text: `Successfully ${isUpdate ? "updated" : "added"} KMIP client`, + type: "success" + }); + onComplete(); }; return ( diff --git a/frontend/src/pages/kms/OverviewPage/components/CmekDecryptModal.tsx b/frontend/src/pages/kms/OverviewPage/components/CmekDecryptModal.tsx index 4c9cf685b..4fd6b3419 100644 --- a/frontend/src/pages/kms/OverviewPage/components/CmekDecryptModal.tsx +++ b/frontend/src/pages/kms/OverviewPage/components/CmekDecryptModal.tsx @@ -52,23 +52,15 @@ const DecryptForm = ({ cmek }: FormProps) => { }); const handleDecryptData = async (formData: FormData) => { - try { - const data = await cmekDecrypt.mutateAsync({ ...formData, keyId: cmek.id }); - createNotification({ - text: "Successfully decrypted data", - type: "success" - }); + const data = await cmekDecrypt.mutateAsync({ ...formData, keyId: cmek.id }); + createNotification({ + text: "Successfully decrypted data", + type: "success" + }); - setPlaintext( - shouldDecode ? Buffer.from(decodeBase64(data.plaintext)).toString("utf8") : data.plaintext - ); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to decrypt data", - type: "error" - }); - } + setPlaintext( + shouldDecode ? Buffer.from(decodeBase64(data.plaintext)).toString("utf8") : data.plaintext + ); }; useEffect(() => { diff --git a/frontend/src/pages/kms/OverviewPage/components/CmekEncryptModal.tsx b/frontend/src/pages/kms/OverviewPage/components/CmekEncryptModal.tsx index 4fb09cf4e..a5bf01d5a 100644 --- a/frontend/src/pages/kms/OverviewPage/components/CmekEncryptModal.tsx +++ b/frontend/src/pages/kms/OverviewPage/components/CmekEncryptModal.tsx @@ -53,19 +53,11 @@ const EncryptForm = ({ cmek }: FormProps) => { }); const handleEncryptData = async (formData: FormData) => { - try { - await cmekEncrypt.mutateAsync({ ...formData, keyId: cmek.id }); - createNotification({ - text: "Successfully encrypted data", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to encrypt data", - type: "error" - }); - } + await cmekEncrypt.mutateAsync({ ...formData, keyId: cmek.id }); + createNotification({ + text: "Successfully encrypted data", + type: "success" + }); }; const ciphertext = cmekEncrypt.data?.ciphertext; diff --git a/frontend/src/pages/kms/OverviewPage/components/CmekModal.tsx b/frontend/src/pages/kms/OverviewPage/components/CmekModal.tsx index c3cccd678..e79d91e15 100644 --- a/frontend/src/pages/kms/OverviewPage/components/CmekModal.tsx +++ b/frontend/src/pages/kms/OverviewPage/components/CmekModal.tsx @@ -86,20 +86,12 @@ const CmekForm = ({ onComplete, cmek }: FormProps) => { encryptionAlgorithm: encryptionAlgorithm as AsymmetricKeyAlgorithm | SymmetricKeyAlgorithm }); - try { - await mutation; - createNotification({ - text: `Successfully ${isUpdate ? "updated" : "added"} key`, - type: "success" - }); - onComplete(); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to ${isUpdate ? "update" : "add"} key`, - type: "error" - }); - } + await mutation; + createNotification({ + text: `Successfully ${isUpdate ? "updated" : "added"} key`, + type: "success" + }); + onComplete(); }; const selectedKeyUsage = watch("keyUsage"); diff --git a/frontend/src/pages/kms/OverviewPage/components/CmekSignModal.tsx b/frontend/src/pages/kms/OverviewPage/components/CmekSignModal.tsx index fec66a31f..e52c03dbe 100644 --- a/frontend/src/pages/kms/OverviewPage/components/CmekSignModal.tsx +++ b/frontend/src/pages/kms/OverviewPage/components/CmekSignModal.tsx @@ -59,19 +59,11 @@ const SignForm = ({ cmek }: FormProps) => { }); const handleSignData = async (formData: FormData) => { - try { - await cmekSign.mutateAsync({ ...formData, keyId: cmek.id }); - createNotification({ - text: "Successfully signed data", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to sign data", - type: "error" - }); - } + await cmekSign.mutateAsync({ ...formData, keyId: cmek.id }); + createNotification({ + text: "Successfully signed data", + type: "success" + }); }; const signature = cmekSign.data?.signature; diff --git a/frontend/src/pages/kms/OverviewPage/components/CmekTable.tsx b/frontend/src/pages/kms/OverviewPage/components/CmekTable.tsx index a6675ea88..c9a0054c5 100644 --- a/frontend/src/pages/kms/OverviewPage/components/CmekTable.tsx +++ b/frontend/src/pages/kms/OverviewPage/components/CmekTable.tsx @@ -152,28 +152,16 @@ export const CmekTable = () => { const updateCmek = useUpdateCmek(); const handleDisableCmek = async ({ id: keyId, isDisabled }: TCmek) => { - try { - await updateCmek.mutateAsync({ - keyId, - projectId, - isDisabled: !isDisabled - }); + await updateCmek.mutateAsync({ + keyId, + projectId, + isDisabled: !isDisabled + }); - createNotification({ - text: `Key successfully ${isDisabled ? "enabled" : "disabled"}`, - type: "success" - }); - } catch (err) { - console.error(err); - const error = err as any; - const text = - error?.response?.data?.message ?? `Failed to ${isDisabled ? "enable" : "disable"} key`; - - createNotification({ - text, - type: "error" - }); - } + createNotification({ + text: `Key successfully ${isDisabled ? "enabled" : "disabled"}`, + type: "success" + }); }; const cannotEditKey = permission.cannot( diff --git a/frontend/src/pages/kms/OverviewPage/components/CmekVerifyModal.tsx b/frontend/src/pages/kms/OverviewPage/components/CmekVerifyModal.tsx index 5c9eac072..d8203e373 100644 --- a/frontend/src/pages/kms/OverviewPage/components/CmekVerifyModal.tsx +++ b/frontend/src/pages/kms/OverviewPage/components/CmekVerifyModal.tsx @@ -69,25 +69,17 @@ const VerifyForm = ({ cmek }: FormProps) => { }); const handleVerifyData = async (formData: FormData) => { - try { - const result = await cmekVerify.mutateAsync({ ...formData, keyId: cmek.id }); + const result = await cmekVerify.mutateAsync({ ...formData, keyId: cmek.id }); - if (result.signatureValid) { - createNotification({ - text: "Successfully verified signature", - type: "success" - }); - } else { - createNotification({ - title: "Signature Verification Failed", - text: "The signature is invalid. The signature was not created using the same signing algorithm and key as the one used to sign the data. The data and signature may have been tampered with.", - type: "error" - }); - } - } catch (err) { - console.error(err); + if (result.signatureValid) { createNotification({ - text: "Failed to sign data", + text: "Successfully verified signature", + type: "success" + }); + } else { + createNotification({ + title: "Signature Verification Failed", + text: "The signature is invalid. The signature was not created using the same signing algorithm and key as the one used to sign the data. The data and signature may have been tampered with.", type: "error" }); } diff --git a/frontend/src/pages/kms/OverviewPage/components/DeleteCmekModal.tsx b/frontend/src/pages/kms/OverviewPage/components/DeleteCmekModal.tsx index 4c5528c39..62389d1ba 100644 --- a/frontend/src/pages/kms/OverviewPage/components/DeleteCmekModal.tsx +++ b/frontend/src/pages/kms/OverviewPage/components/DeleteCmekModal.tsx @@ -16,28 +16,17 @@ export const DeleteCmekModal = ({ isOpen, onOpenChange, cmek }: Props) => { const { id: keyId, projectId, name } = cmek; const handleDeleteCmek = async () => { - try { - await deleteCmek.mutateAsync({ - keyId, - projectId - }); + await deleteCmek.mutateAsync({ + keyId, + projectId + }); - createNotification({ - text: "Key successfully deleted", - type: "success" - }); + createNotification({ + text: "Key successfully deleted", + type: "success" + }); - onOpenChange(false); - } catch (err) { - console.error(err); - const error = err as any; - const text = error?.response?.data?.message ?? "Failed to delete key"; - - createNotification({ - text, - type: "error" - }); - } + onOpenChange(false); }; return ( diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupModal.tsx index 908be4935..4b6ea0858 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupModal.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupModal.tsx @@ -74,43 +74,36 @@ export const OrgGroupModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Pr }, [popUp?.group?.data, roles]); const onGroupModalSubmit = async ({ name, slug, role }: TGroupFormData) => { - try { - if (!currentOrg?.id) return; + if (!currentOrg?.id) return; - const group = popUp?.group?.data as { - groupId: string; - name: string; - slug: string; - }; + const group = popUp?.group?.data as { + groupId: string; + name: string; + slug: string; + }; - if (group) { - await updateMutateAsync({ - id: group.groupId, - name, - slug, - role: role.slug || undefined - }); - } else { - await createMutateAsync({ - name, - slug, - organizationId: currentOrg.id, - role: role.slug || undefined - }); - } - handlePopUpToggle("group", false); - reset(); - - createNotification({ - text: `Successfully ${popUp?.group?.data ? "updated" : "created"} group`, - type: "success" + if (group) { + await updateMutateAsync({ + id: group.groupId, + name, + slug, + role: role.slug || undefined }); - } catch { - createNotification({ - text: `Failed to ${popUp?.group?.data ? "updated" : "created"} group`, - type: "error" + } else { + await createMutateAsync({ + name, + slug, + organizationId: currentOrg.id, + role: role.slug || undefined }); } + handlePopUpToggle("group", false); + reset(); + + createNotification({ + text: `Successfully ${popUp?.group?.data ? "updated" : "created"} group`, + type: "success" + }); }; return ( diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx index 88cbc17f0..1cf5407d1 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx @@ -37,21 +37,13 @@ export const OrgGroupsSection = () => { }; const onDeleteGroupSubmit = async ({ name, groupId }: { name: string; groupId: string }) => { - try { - await deleteMutateAsync({ - id: groupId - }); - createNotification({ - text: `Successfully deleted the group named ${name}`, - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to delete the group named ${name}`, - type: "error" - }); - } + await deleteMutateAsync({ + id: groupId + }); + createNotification({ + text: `Successfully deleted the group named ${name}`, + type: "success" + }); handlePopUpClose("deleteGroup"); }; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx index 61119fce1..cc6d7c7aa 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx @@ -79,23 +79,15 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { const { data: roles } = useGetOrgRoles(orgId); const handleChangeRole = async ({ id, role }: { id: string; role: string }) => { - try { - await updateMutateAsync({ - id, - role - }); + await updateMutateAsync({ + id, + role + }); - createNotification({ - text: "Successfully updated group role", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to update group role", - type: "error" - }); - } + createNotification({ + text: "Successfully updated group role", + type: "success" + }); }; const { diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx index 355dec803..b7ebfcdc4 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx @@ -56,53 +56,31 @@ export const IdentitySection = withPermission( const isEnterprise = subscription?.slug === "enterprise"; const onDeleteIdentitySubmit = async (identityId: string) => { - try { - await deleteMutateAsync({ - identityId, - organizationId: orgId - }); + await deleteMutateAsync({ + identityId, + organizationId: orgId + }); - createNotification({ - text: "Successfully deleted identity", - type: "success" - }); + createNotification({ + text: "Successfully deleted identity", + type: "success" + }); - handlePopUpClose("deleteIdentity"); - } catch (err) { - console.error(err); - const error = err as any; - const text = error?.response?.data?.message ?? "Failed to delete identity"; - - createNotification({ - text, - type: "error" - }); - } + handlePopUpClose("deleteIdentity"); }; const onDeleteTemplateSubmit = async (templateId: string) => { - try { - await deleteTemplateMutateAsync({ - templateId, - organizationId: orgId - }); + await deleteTemplateMutateAsync({ + templateId, + organizationId: orgId + }); - createNotification({ - text: "Successfully deleted template", - type: "success" - }); + createNotification({ + text: "Successfully deleted template", + type: "success" + }); - handlePopUpClose("deleteTemplate"); - } catch (err) { - console.error(err); - const error = err as any; - const text = error?.response?.data?.message ?? "Failed to delete template"; - - createNotification({ - text, - type: "error" - }); - } + handlePopUpClose("deleteTemplate"); }; return ( diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx index adb729d75..dc033f3fa 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx @@ -85,46 +85,30 @@ export const OrgMembersSection = () => { }; const onDeactivateMemberSubmit = async (orgMembershipId: string) => { - try { - await updateOrgMembership({ - organizationId: orgId, - membershipId: orgMembershipId, - isActive: false - }); + await updateOrgMembership({ + organizationId: orgId, + membershipId: orgMembershipId, + isActive: false + }); - createNotification({ - text: "Successfully deactivated user in organization", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to deactivate user in organization", - type: "error" - }); - } + createNotification({ + text: "Successfully deactivated user in organization", + type: "success" + }); handlePopUpClose("deactivateMember"); }; const onRemoveMemberSubmit = async (orgMembershipId: string) => { - try { - await deleteMutateAsync({ - orgId, - membershipId: orgMembershipId - }); + await deleteMutateAsync({ + orgId, + membershipId: orgMembershipId + }); - createNotification({ - text: "Successfully removed user from org", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to remove user from the organization", - type: "error" - }); - } + createNotification({ + text: "Successfully removed user from org", + type: "success" + }); handlePopUpClose("removeMember"); }; @@ -132,27 +116,20 @@ export const OrgMembersSection = () => { const { data: members = [] } = useGetOrgUsers(orgId); const handleRemoveMembers = async (selectedMembers: OrgUser[]) => { - try { - await deleteBatchMutateAsync({ - orgId, - membershipIds: selectedMembers - .filter((member) => member.user.id !== userId) - .map((member) => member.id) - }); + await deleteBatchMutateAsync({ + orgId, + membershipIds: selectedMembers + .filter((member) => member.user.id !== userId) + .map((member) => member.id) + }); - createNotification({ - text: "Successfully removed users from organization", - type: "success" - }); + createNotification({ + text: "Successfully removed users from organization", + type: "success" + }); - setSelectedMemberIds([]); - handlePopUpClose("removeMembers"); - } catch { - createNotification({ - text: "Failed to remove users from the organization", - type: "error" - }); - } + setSelectedMemberIds([]); + handlePopUpClose("removeMembers"); }; return ( diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx index 68a4e795a..fe71c207d 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx @@ -127,35 +127,26 @@ export const OrgMembersTable = ({ const onRoleChange = async (membershipId: string, role: string) => { if (!currentOrg?.id) return; - try { - // TODO: replace hardcoding default role - const isCustomRole = !["admin", "member", "no-access"].includes(role); + // TODO: replace hardcoding default role + const isCustomRole = !["admin", "member", "no-access"].includes(role); - if (isCustomRole && subscription && !subscription?.rbac) { - handlePopUpOpen("upgradePlan", { - description: - "You can assign custom roles to members if you switch to Infisical's Pro plan." - }); - return; - } - - await updateOrgMembership({ - organizationId: currentOrg?.id, - membershipId, - role - }); - - createNotification({ - text: "Successfully updated user role", - type: "success" - }); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to update user role", - type: "error" + if (isCustomRole && subscription && !subscription?.rbac) { + handlePopUpOpen("upgradePlan", { + description: "You can assign custom roles to members if you switch to Infisical's Pro plan." }); + return; } + + await updateOrgMembership({ + organizationId: currentOrg?.id, + membershipId, + role + }); + + createNotification({ + text: "Successfully updated user role", + type: "success" + }); }; const onResendInvite = async (membershipId: string) => { @@ -174,12 +165,6 @@ export const OrgMembersTable = ({ text: "Successfully resent org invitation", type: "success" }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to resend org invitation", - type: "error" - }); } finally { setResendInviteId(null); } diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx index d6dae6ae2..36249967a 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx @@ -86,17 +86,12 @@ export const OrgRoleTable = () => { const handleRoleDelete = async () => { const { id } = popUp?.deleteRole?.data as TOrgRole; - try { - await deleteRole({ - orgId, - id - }); - createNotification({ type: "success", text: "Successfully removed the role" }); - handlePopUpClose("deleteRole"); - } catch (err) { - console.log(err); - createNotification({ type: "error", text: "Failed to delete role" }); - } + await deleteRole({ + orgId, + id + }); + createNotification({ type: "success", text: "Successfully removed the role" }); + handlePopUpClose("deleteRole"); }; const handleSetRoleAsDefault = async (defaultMembershipRoleSlug: string) => { @@ -110,17 +105,12 @@ export const OrgRoleTable = () => { return; } - try { - await updateOrg({ - orgId, - defaultMembershipRoleSlug - }); - createNotification({ type: "success", text: "Successfully updated default membership role" }); - handlePopUpClose("deleteRole"); - } catch (err) { - console.log(err); - createNotification({ type: "error", text: "Failed to update default membership role" }); - } + await updateOrg({ + orgId, + defaultMembershipRoleSlug + }); + createNotification({ type: "success", text: "Successfully updated default membership role" }); + handlePopUpClose("deleteRole"); }; const { diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index c09a58960..9d7e3ab48 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -73,24 +73,15 @@ const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => { "method" | "name" | "app" | "credentials" | "isPlatformManagedCredentials" > ) => { - try { - const connection = await createAppConnection.mutateAsync({ - ...formData, - projectId - }); - createNotification({ - text: `Successfully added ${appName} Connection`, - type: "success" - }); - onComplete(connection); - } catch (err: any) { - console.error(err); - createNotification({ - title: `Failed to add ${appName} Connection`, - text: err.message, - type: "error" - }); - } + const connection = await createAppConnection.mutateAsync({ + ...formData, + projectId + }); + createNotification({ + text: `Successfully added ${appName} Connection`, + type: "success" + }); + onComplete(connection); }; switch (app) { @@ -191,24 +182,15 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { "method" | "name" | "app" | "credentials" | "isPlatformManagedCredentials" > ) => { - try { - const connection = await updateAppConnection.mutateAsync({ - connectionId: appConnection.id, - ...formData - }); - createNotification({ - text: `Successfully updated ${appName} Connection`, - type: "success" - }); - onComplete(connection); - } catch (err: any) { - console.error(err); - createNotification({ - title: `Failed to update ${appName} Connection`, - text: err.message, - type: "error" - }); - } + const connection = await updateAppConnection.mutateAsync({ + connectionId: appConnection.id, + ...formData + }); + createNotification({ + text: `Successfully updated ${appName} Connection`, + type: "success" + }); + onComplete(connection); }; switch (appConnection.app) { diff --git a/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupCreateUpdateModal.tsx b/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupCreateUpdateModal.tsx index e4364e874..bc6e3fab5 100644 --- a/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupCreateUpdateModal.tsx +++ b/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupCreateUpdateModal.tsx @@ -77,43 +77,36 @@ export const GroupCreateUpdateModal = ({ popUp, handlePopUpClose, handlePopUpTog }, [popUp?.groupCreateUpdate?.data, roles]); const onGroupModalSubmit = async ({ name, slug, role }: TGroupFormData) => { - try { - if (!currentOrg?.id) return; + if (!currentOrg?.id) return; - const group = popUp?.groupCreateUpdate?.data as { - groupId: string; - name: string; - slug: string; - }; + const group = popUp?.groupCreateUpdate?.data as { + groupId: string; + name: string; + slug: string; + }; - if (group) { - await updateMutateAsync({ - id: group.groupId, - name, - slug, - role: role.slug || undefined - }); - } else { - await createMutateAsync({ - name, - slug, - organizationId: currentOrg.id, - role: role.slug || undefined - }); - } - handlePopUpToggle("groupCreateUpdate", false); - reset(); - - createNotification({ - text: `Successfully ${popUp?.groupCreateUpdate?.data ? "updated" : "created"} group`, - type: "success" + if (group) { + await updateMutateAsync({ + id: group.groupId, + name, + slug, + role: role.slug || undefined }); - } catch { - createNotification({ - text: `Failed to ${popUp?.groupCreateUpdate?.data ? "updated" : "created"} group`, - type: "error" + } else { + await createMutateAsync({ + name, + slug, + organizationId: currentOrg.id, + role: role.slug || undefined }); } + handlePopUpToggle("groupCreateUpdate", false); + reset(); + + createNotification({ + text: `Successfully ${popUp?.groupCreateUpdate?.data ? "updated" : "created"} group`, + type: "success" + }); }; return ( diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx index f3bd05eeb..c2e7bbe57 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx @@ -44,34 +44,23 @@ const Page = () => { ] as const); const onDeleteIdentitySubmit = async (id: string) => { - try { - await deleteIdentity({ - identityId: id, - organizationId: orgId - }); + await deleteIdentity({ + identityId: id, + organizationId: orgId + }); - createNotification({ - text: "Successfully deleted identity", - type: "success" - }); + createNotification({ + text: "Successfully deleted identity", + type: "success" + }); - handlePopUpClose("deleteIdentity"); - navigate({ - to: "/organization/access-management", - search: { - selectedTab: OrgAccessControlTabSections.Identities - } - }); - } catch (err) { - console.error(err); - const error = err as any; - const text = error?.response?.data?.message ?? "Failed to delete identity"; - - createNotification({ - text, - type: "error" - }); - } + handlePopUpClose("deleteIdentity"); + navigate({ + to: "/organization/access-management", + search: { + selectedTab: OrgAccessControlTabSections.Identities + } + }); }; return ( diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx index 0d6b269a8..c25d11cb7 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx @@ -107,18 +107,13 @@ export const RolePermissionsSection = ({ roleId }: Props) => { const { mutateAsync: updateRole } = useUpdateOrgRole(); const onSubmit = async (el: TFormSchema) => { - try { - await updateRole({ - orgId, - id: roleId, - ...el, - permissions: formRolePermission2API(el.permissions) - }); - createNotification({ type: "success", text: "Successfully updated role" }); - } catch (err) { - console.log(err); - createNotification({ type: "error", text: "Failed to update role" }); - } + await updateRole({ + orgId, + id: roleId, + ...el, + permissions: formRolePermission2API(el.permissions) + }); + createNotification({ type: "success", text: "Successfully updated role" }); }; const isCustomRole = !["admin", "member", "no-access"].includes(role?.slug ?? ""); diff --git a/frontend/src/pages/organization/UserDetailsByIDPage/UserDetailsByIDPage.tsx b/frontend/src/pages/organization/UserDetailsByIDPage/UserDetailsByIDPage.tsx index 2d433bba8..8ddc27763 100644 --- a/frontend/src/pages/organization/UserDetailsByIDPage/UserDetailsByIDPage.tsx +++ b/frontend/src/pages/organization/UserDetailsByIDPage/UserDetailsByIDPage.tsx @@ -64,56 +64,38 @@ const Page = withPermission( ] as const); const onDeactivateMemberSubmit = async (orgMembershipId: string) => { - try { - await updateOrgMembership({ - organizationId: orgId, - membershipId: orgMembershipId, - isActive: false - }); + await updateOrgMembership({ + organizationId: orgId, + membershipId: orgMembershipId, + isActive: false + }); - createNotification({ - text: "Successfully deactivated user in organization", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to deactivate user in organization", - type: "error" - }); - } + createNotification({ + text: "Successfully deactivated user in organization", + type: "success" + }); handlePopUpClose("deactivateMember"); }; const onRemoveMemberSubmit = async (orgMembershipId: string) => { - try { - await deleteOrgMembership({ - orgId, - membershipId: orgMembershipId - }); + await deleteOrgMembership({ + orgId, + membershipId: orgMembershipId + }); - createNotification({ - text: "Successfully removed user from org", - type: "success" - }); - - handlePopUpClose("removeMember"); - navigate({ - to: "/organization/access-management" as const, - search: { - selectedTab: OrgAccessControlTabSections.Member - } - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to remove user from the organization", - type: "error" - }); - } + createNotification({ + text: "Successfully removed user from org", + type: "success" + }); handlePopUpClose("removeMember"); + navigate({ + to: "/organization/access-management" as const, + search: { + selectedTab: OrgAccessControlTabSections.Member + } + }); }; return ( diff --git a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx index bef98e875..d26163de0 100644 --- a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx +++ b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx @@ -46,26 +46,18 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) => const { mutateAsync: resendOrgMemberInvitation, isPending } = useResendOrgMemberInvitation(); const onResendInvite = async () => { - try { - const signupToken = await resendOrgMemberInvitation({ - membershipId - }); + const signupToken = await resendOrgMemberInvitation({ + membershipId + }); - if (signupToken) { - return; - } - - createNotification({ - text: "Successfully resent org invitation", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to resend org invitation", - type: "error" - }); + if (signupToken) { + return; } + + createNotification({ + text: "Successfully resent org invitation", + type: "success" + }); }; const getStatus = (m: OrgUser) => { diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PamAccountForm.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PamAccountForm.tsx index b9599be2d..346195e11 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PamAccountForm.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PamAccountForm.tsx @@ -38,27 +38,18 @@ const CreateForm = ({ const onSubmit = async ( formData: DiscriminativePick ) => { - try { - const account = await createPamAccount.mutateAsync({ - ...formData, - folderId, - resourceId, - resourceType, - projectId - }); - createNotification({ - text: "Successfully created account", - type: "success" - }); - onComplete(account); - } catch (err: any) { - console.error(err); - createNotification({ - title: "Failed to create account", - text: err.message, - type: "error" - }); - } + const account = await createPamAccount.mutateAsync({ + ...formData, + folderId, + resourceId, + resourceType, + projectId + }); + createNotification({ + text: "Successfully created account", + type: "success" + }); + onComplete(account); }; switch (resourceType) { @@ -85,25 +76,16 @@ const UpdateForm = ({ account, onComplete }: UpdateFormProps) => { const onSubmit = async ( formData: DiscriminativePick ) => { - try { - const updatedAccount = await updatePamAccount.mutateAsync({ - accountId: account.id, - resourceType: account.resource.resourceType, - ...formData - }); - createNotification({ - text: "Successfully updated account", - type: "success" - }); - onComplete(updatedAccount); - } catch (err: any) { - console.error(err); - createNotification({ - title: "Failed to update account", - text: err.message, - type: "error" - }); - } + const updatedAccount = await updatePamAccount.mutateAsync({ + accountId: account.id, + resourceType: account.resource.resourceType, + ...formData + }); + createNotification({ + text: "Successfully updated account", + type: "success" + }); + onComplete(updatedAccount); }; switch (account.resource.resourceType) { diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAddFolderModal.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAddFolderModal.tsx index 7b057a648..266a724cd 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAddFolderModal.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAddFolderModal.tsx @@ -17,25 +17,16 @@ export const PamAddFolderModal = ({ isOpen, onOpenChange, projectId, currentFold console.log({ currentFolderId }); const onSubmit = async (formData: Pick) => { - try { - await createPamFolder.mutateAsync({ - ...formData, - parentId: currentFolderId, - projectId - }); - createNotification({ - text: "Successfully created folder", - type: "success" - }); - onOpenChange(false); - } catch (err: any) { - console.error(err); - createNotification({ - title: "Failed to create folder", - text: err.message, - type: "error" - }); - } + await createPamFolder.mutateAsync({ + ...formData, + parentId: currentFolderId, + projectId + }); + createNotification({ + text: "Successfully created folder", + type: "success" + }); + onOpenChange(false); }; return ( diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamDeleteAccountModal.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamDeleteAccountModal.tsx index 45ac03082..c094d3c53 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamDeleteAccountModal.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamDeleteAccountModal.tsx @@ -20,25 +20,17 @@ export const PamDeleteAccountModal = ({ isOpen, onOpenChange, account }: Props) } = account; const handleDelete = async () => { - try { - await deletePamAccount.mutateAsync({ - accountId, - resourceType - }); + await deletePamAccount.mutateAsync({ + accountId, + resourceType + }); - createNotification({ - text: "Successfully deleted account", - type: "success" - }); + createNotification({ + text: "Successfully deleted account", + type: "success" + }); - onOpenChange(false); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete account", - type: "error" - }); - } + onOpenChange(false); }; return ( diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamDeleteFolderModal.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamDeleteFolderModal.tsx index f0e2476c2..d901d0ba0 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamDeleteFolderModal.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamDeleteFolderModal.tsx @@ -16,24 +16,16 @@ export const PamDeleteFolderModal = ({ isOpen, onOpenChange, folder }: Props) => const { id: folderId, name } = folder; const handleDelete = async () => { - try { - await deletePamFolder.mutateAsync({ - folderId - }); + await deletePamFolder.mutateAsync({ + folderId + }); - createNotification({ - text: "Successfully deleted folder", - type: "success" - }); + createNotification({ + text: "Successfully deleted folder", + type: "success" + }); - onOpenChange(false); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete folder", - type: "error" - }); - } + onOpenChange(false); }; return ( diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamUpdateFolderModal.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamUpdateFolderModal.tsx index b4bcc46a4..4162ce25f 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamUpdateFolderModal.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamUpdateFolderModal.tsx @@ -16,24 +16,15 @@ export const PamUpdateFolderModal = ({ isOpen, onOpenChange, folder }: Props) => if (!folder) return null; const onSubmit = async (formData: Pick) => { - try { - await updatePamFolder.mutateAsync({ - ...formData, - folderId: folder.id - }); - createNotification({ - text: "Successfully updated folder", - type: "success" - }); - onOpenChange(false); - } catch (err: any) { - console.error(err); - createNotification({ - title: "Failed to updated folder", - text: err.message, - type: "error" - }); - } + await updatePamFolder.mutateAsync({ + ...formData, + folderId: folder.id + }); + createNotification({ + text: "Successfully updated folder", + type: "success" + }); + onOpenChange(false); }; return ( diff --git a/frontend/src/pages/pam/PamResourcesPage/components/PamDeleteResourceModal.tsx b/frontend/src/pages/pam/PamResourcesPage/components/PamDeleteResourceModal.tsx index efda53466..7f0d2bd19 100644 --- a/frontend/src/pages/pam/PamResourcesPage/components/PamDeleteResourceModal.tsx +++ b/frontend/src/pages/pam/PamResourcesPage/components/PamDeleteResourceModal.tsx @@ -16,25 +16,17 @@ export const PamDeleteResourceModal = ({ isOpen, onOpenChange, resource }: Props const { id: resourceId, name, resourceType } = resource; const handleDelete = async () => { - try { - await deletePamResource.mutateAsync({ - resourceId, - resourceType - }); + await deletePamResource.mutateAsync({ + resourceId, + resourceType + }); - createNotification({ - text: `Successfully removed ${PAM_RESOURCE_TYPE_MAP[resourceType].name} resource`, - type: "success" - }); + createNotification({ + text: `Successfully removed ${PAM_RESOURCE_TYPE_MAP[resourceType].name} resource`, + type: "success" + }); - onOpenChange(false); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to remove ${PAM_RESOURCE_TYPE_MAP[resourceType].name} resource`, - type: "error" - }); - } + onOpenChange(false); }; return ( diff --git a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PamResourceForm.tsx b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PamResourceForm.tsx index 2bc54e7cd..3dd3aeec8 100644 --- a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PamResourceForm.tsx +++ b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PamResourceForm.tsx @@ -35,24 +35,15 @@ const CreateForm = ({ resourceType, onComplete, projectId }: CreateFormProps) => "name" | "resourceType" | "connectionDetails" | "gatewayId" > ) => { - try { - const resource = await createPamResource.mutateAsync({ - ...formData, - projectId - }); - createNotification({ - text: `Successfully created ${resourceName} resource`, - type: "success" - }); - onComplete(resource); - } catch (err: any) { - console.error(err); - createNotification({ - title: `Failed to create ${resourceName} resource`, - text: err.message, - type: "error" - }); - } + const resource = await createPamResource.mutateAsync({ + ...formData, + projectId + }); + createNotification({ + text: `Successfully created ${resourceName} resource`, + type: "success" + }); + onComplete(resource); }; switch (resourceType) { @@ -72,24 +63,15 @@ const UpdateForm = ({ resource, onComplete }: UpdateFormProps) => { const onSubmit = async ( formData: DiscriminativePick ) => { - try { - const updatedResource = await updatePamResource.mutateAsync({ - resourceId: resource.id, - ...formData - }); - createNotification({ - text: `Successfully updated ${resourceName} resource`, - type: "success" - }); - onComplete(updatedResource); - } catch (err: any) { - console.error(err); - createNotification({ - title: `Failed to update ${resourceName} resource`, - text: err.message, - type: "error" - }); - } + const updatedResource = await updatePamResource.mutateAsync({ + resourceId: resource.id, + ...formData + }); + createNotification({ + text: `Successfully updated ${resourceName} resource`, + type: "success" + }); + onComplete(updatedResource); }; switch (resource.resourceType) { diff --git a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MemberRoleForm/MemberRbacSection.tsx b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MemberRoleForm/MemberRbacSection.tsx index 291a5b58b..5be239042 100644 --- a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MemberRoleForm/MemberRbacSection.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MemberRoleForm/MemberRbacSection.tsx @@ -127,17 +127,13 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props) return; } - try { - await updateMembershipRole.mutateAsync({ - projectId, - membershipId: projectMember.id, - roles: sanitizedRoles - }); - createNotification({ text: "Successfully updated roles", type: "success" }); - roleForm.reset(undefined, { keepValues: true }); - } catch { - createNotification({ text: "Failed to update role", type: "error" }); - } + await updateMembershipRole.mutateAsync({ + projectId, + membershipId: projectMember.id, + roles: sanitizedRoles + }); + createNotification({ text: "Successfully updated roles", type: "success" }); + roleForm.reset(undefined, { keepValues: true }); }; if (isRolesLoading) diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleModify.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleModify.tsx index a74428e98..8bc451151 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleModify.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleModify.tsx @@ -114,16 +114,12 @@ export const IdentityRoleModify = ({ identityProjectMembership }: Props) => { }; }); - try { - await updateIdentityWorkspaceRole.mutateAsync({ - projectId, - identityId: identityProjectMembership.identity.id, - roles: sanitizedRoles - }); - createNotification({ text: "Successfully updated roles", type: "success" }); - } catch { - createNotification({ text: "Failed to update roles", type: "error" }); - } + await updateIdentityWorkspaceRole.mutateAsync({ + projectId, + identityId: identityProjectMembership.identity.id, + roles: sanitizedRoles + }); + createNotification({ text: "Successfully updated roles", type: "success" }); }; if (isRolesLoading) diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberRoleDetailsSection/MemberRoleDetailsSection.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberRoleDetailsSection/MemberRoleDetailsSection.tsx index 703511c84..2654e237e 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberRoleDetailsSection/MemberRoleDetailsSection.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberRoleDetailsSection/MemberRoleDetailsSection.tsx @@ -55,42 +55,37 @@ export const MemberRoleDetailsSection = ({ const handleRoleDelete = async () => { const { id } = popUp?.deleteRole?.data as TProjectRole; - try { - const updatedRoles = membershipDetails?.roles?.filter((el) => el.id !== id); - await updateUserWorkspaceRole({ - projectId, - roles: updatedRoles.map( - ({ - role, - customRoleSlug, - isTemporary, - temporaryMode, - temporaryRange, - temporaryAccessStartTime, - temporaryAccessEndTime - }) => ({ - role: role === "custom" ? customRoleSlug : role, - ...(isTemporary - ? { - isTemporary, - temporaryMode, - temporaryRange, - temporaryAccessStartTime, - temporaryAccessEndTime - } - : { - isTemporary - }) - }) - ), - membershipId: membershipDetails.id - }); - createNotification({ type: "success", text: "Successfully removed role" }); - handlePopUpClose("deleteRole"); - } catch (err) { - console.log(err); - createNotification({ type: "error", text: "Failed to delete role" }); - } + const updatedRoles = membershipDetails?.roles?.filter((el) => el.id !== id); + await updateUserWorkspaceRole({ + projectId, + roles: updatedRoles.map( + ({ + role, + customRoleSlug, + isTemporary, + temporaryMode, + temporaryRange, + temporaryAccessStartTime, + temporaryAccessEndTime + }) => ({ + role: role === "custom" ? customRoleSlug : role, + ...(isTemporary + ? { + isTemporary, + temporaryMode, + temporaryRange, + temporaryAccessStartTime, + temporaryAccessEndTime + } + : { + isTemporary + }) + }) + ), + membershipId: membershipDetails.id + }); + createNotification({ type: "success", text: "Successfully removed role" }); + handlePopUpClose("deleteRole"); }; return ( diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberRoleDetailsSection/MemberRoleModify.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberRoleDetailsSection/MemberRoleModify.tsx index 287ea3ad7..947424191 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberRoleDetailsSection/MemberRoleModify.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberRoleDetailsSection/MemberRoleModify.tsx @@ -126,16 +126,12 @@ export const MemberRoleModify = ({ projectMember, onOpenUpgradeModal }: Props) = return; } - try { - await updateMembershipRole.mutateAsync({ - projectId, - membershipId: projectMember.id, - roles: sanitizedRoles - }); - createNotification({ text: "Successfully updated roles", type: "success" }); - } catch { - createNotification({ text: "Failed to update roles", type: "error" }); - } + await updateMembershipRole.mutateAsync({ + projectId, + membershipId: projectMember.id, + roles: sanitizedRoles + }); + createNotification({ text: "Successfully updated roles", type: "success" }); }; if (isRolesLoading) diff --git a/frontend/src/pages/secret-manager/SecretRotationPage/SecretRotationPage.tsx b/frontend/src/pages/secret-manager/SecretRotationPage/SecretRotationPage.tsx index 8efa28a63..b69d8fadb 100644 --- a/frontend/src/pages/secret-manager/SecretRotationPage/SecretRotationPage.tsx +++ b/frontend/src/pages/secret-manager/SecretRotationPage/SecretRotationPage.tsx @@ -95,42 +95,26 @@ const Page = () => { const handleDeleteRotation = async () => { const { id } = popUp.deleteRotation.data as { id: string }; - try { - await deleteSecretRotation({ - id, - workspaceId - }); - handlePopUpClose("deleteRotation"); - createNotification({ - type: "success", - text: "Successfully removed rotation" - }); - } catch (error) { - console.log(error); - createNotification({ - type: "error", - text: "Failed to remove rotation" - }); - } + await deleteSecretRotation({ + id, + workspaceId + }); + handlePopUpClose("deleteRotation"); + createNotification({ + type: "success", + text: "Successfully removed rotation" + }); }; const handleRestartRotation = async (id: string) => { - try { - await restartSecretRotation({ - id, - workspaceId - }); - createNotification({ - type: "success", - text: "Secret rotation initiated" - }); - } catch (error) { - console.log(error); - createNotification({ - type: "error", - text: "Failed to restart rotation" - }); - } + await restartSecretRotation({ + id, + workspaceId + }); + createNotification({ + type: "success", + text: "Secret rotation initiated" + }); }; const handleCreateRotation = (provider: TSecretRotationProviderTemplate) => { diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncActionTriggers.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncActionTriggers.tsx index 78cdf899e..ad82460eb 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncActionTriggers.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncActionTriggers.tsx @@ -89,44 +89,30 @@ export const SecretSyncActionTriggers = ({ secretSync }: Props) => { const handleToggleEnableSync = async () => { const isAutoSyncEnabled = !secretSync.isAutoSyncEnabled; - try { - await updateSync.mutateAsync({ - syncId: secretSync.id, - destination: secretSync.destination, - isAutoSyncEnabled, - projectId: secretSync.projectId - }); + await updateSync.mutateAsync({ + syncId: secretSync.id, + destination: secretSync.destination, + isAutoSyncEnabled, + projectId: secretSync.projectId + }); - createNotification({ - text: `Successfully ${isAutoSyncEnabled ? "enabled" : "disabled"} auto-sync for ${destinationName} Sync`, - type: "success" - }); - } catch { - createNotification({ - text: `Failed to ${isAutoSyncEnabled ? "enable" : "disable"} auto-sync for ${destinationName} Sync`, - type: "error" - }); - } + createNotification({ + text: `Successfully ${isAutoSyncEnabled ? "enabled" : "disabled"} auto-sync for ${destinationName} Sync`, + type: "success" + }); }; const handleTriggerSync = async () => { - try { - await triggerSyncSecrets.mutateAsync({ - syncId: secretSync.id, - destination: secretSync.destination, - projectId: secretSync.projectId - }); + await triggerSyncSecrets.mutateAsync({ + syncId: secretSync.id, + destination: secretSync.destination, + projectId: secretSync.projectId + }); - createNotification({ - text: `Successfully triggered ${destinationName} Sync`, - type: "success" - }); - } catch { - createNotification({ - text: `Failed to trigger ${destinationName} Sync`, - type: "error" - }); - } + createNotification({ + text: `Successfully triggered ${destinationName} Sync`, + type: "success" + }); }; const permissionSubject = diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx index bcc2d81b5..4af602e12 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx @@ -76,32 +76,25 @@ export const SlackIntegrationForm = ({ onClose }: Props) => { }); const handleIntegrationSave = async (data: TSlackConfigForm) => { - try { - if (!currentProject) { - return; - } - - await updateProjectSlackConfig({ - ...data, - projectId: currentProject.id, - integration: WorkflowIntegrationPlatform.SLACK, - integrationId: data.slackIntegrationId, - accessRequestChannels: data.accessRequestChannels.filter(Boolean).join(", "), - secretRequestChannels: data.secretRequestChannels.filter(Boolean).join(", ") - }); - - createNotification({ - type: "success", - text: "Successfully created slack integration" - }); - - onClose(); - } catch { - createNotification({ - type: "error", - text: "Failed to create slack integration" - }); + if (!currentProject) { + return; } + + await updateProjectSlackConfig({ + ...data, + projectId: currentProject.id, + integration: WorkflowIntegrationPlatform.SLACK, + integrationId: data.slackIntegrationId, + accessRequestChannels: data.accessRequestChannels.filter(Boolean).join(", "), + secretRequestChannels: data.secretRequestChannels.filter(Boolean).join(", ") + }); + + createNotification({ + type: "success", + text: "Successfully created slack integration" + }); + + onClose(); }; const secretRequestNotifState = watch("isSecretRequestNotificationEnabled"); diff --git a/frontend/src/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/SecretScanningResourceRow.tsx b/frontend/src/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/SecretScanningResourceRow.tsx index 740cf6d71..b2021bf5c 100644 --- a/frontend/src/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/SecretScanningResourceRow.tsx +++ b/frontend/src/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/SecretScanningResourceRow.tsx @@ -81,24 +81,17 @@ export const SecretScanningResourceRow = ({ resource, dataSource }: Props) => { const navigate = useNavigate(); const handleTriggerScan = async () => { - try { - await triggerDataSourceScan.mutateAsync({ - dataSourceId: dataSource.id, - type: dataSource.type, - projectId: dataSource.projectId, - resourceId: id - }); + await triggerDataSourceScan.mutateAsync({ + dataSourceId: dataSource.id, + type: dataSource.type, + projectId: dataSource.projectId, + resourceId: id + }); - createNotification({ - text: `Successfully triggered scan for ${name}`, - type: "success" - }); - } catch { - createNotification({ - text: `Failed to trigger scan for ${name}`, - type: "error" - }); - } + createNotification({ + text: `Successfully triggered scan for ${name}`, + type: "success" + }); }; const [isIdCopied, setIsIdCopied] = useToggle(false); diff --git a/frontend/src/pages/secret-scanning/SecretScanningFindingsPage/components/SecretScanningUpdateFindingModal.tsx b/frontend/src/pages/secret-scanning/SecretScanningFindingsPage/components/SecretScanningUpdateFindingModal.tsx index 484cbcf36..c4838673b 100644 --- a/frontend/src/pages/secret-scanning/SecretScanningFindingsPage/components/SecretScanningUpdateFindingModal.tsx +++ b/frontend/src/pages/secret-scanning/SecretScanningFindingsPage/components/SecretScanningUpdateFindingModal.tsx @@ -55,39 +55,32 @@ const Content = ({ findings, onComplete }: ContentProps) => { const onSubmit = async (data: FormType) => { if (!data.status) return; - try { - if (findings.length > 1) { - await updateMultipleFindings.mutateAsync( - findings.map((f) => ({ - ...data, - status: data.status!, - findingId: f.id, - projectId: f.projectId - })) - ); - } else { - await updateMultipleFindings.mutateAsync([ - { - ...data, - status: data.status, - findingId: findings[0].id, - projectId: findings[0].projectId - } - ]); - } - - createNotification({ - type: "success", - text: `Finding status${single ? "" : "es"} successfully updated` - }); - - onComplete(); - } catch { - createNotification({ - type: "error", - text: `Failed to update finding status${single ? "" : "es"}` - }); + if (findings.length > 1) { + await updateMultipleFindings.mutateAsync( + findings.map((f) => ({ + ...data, + status: data.status!, + findingId: f.id, + projectId: f.projectId + })) + ); + } else { + await updateMultipleFindings.mutateAsync([ + { + ...data, + status: data.status, + findingId: findings[0].id, + projectId: findings[0].projectId + } + ]); } + + createNotification({ + type: "success", + text: `Finding status${single ? "" : "es"} successfully updated` + }); + + onComplete(); }; return ( diff --git a/frontend/src/pages/secret-scanning/SettingsPage/components/ProjectScanningConfigTab/SecretScanningConfigForm.tsx b/frontend/src/pages/secret-scanning/SettingsPage/components/ProjectScanningConfigTab/SecretScanningConfigForm.tsx index c9df333c4..7ff36964d 100644 --- a/frontend/src/pages/secret-scanning/SettingsPage/components/ProjectScanningConfigTab/SecretScanningConfigForm.tsx +++ b/frontend/src/pages/secret-scanning/SettingsPage/components/ProjectScanningConfigTab/SecretScanningConfigForm.tsx @@ -38,22 +38,15 @@ export const SecretScanningConfigForm = ({ config }: Props) => { }); const onSubmit = async ({ content }: FormType) => { - try { - await updateConfig.mutateAsync({ - projectId: config.projectId, - content: content || null - }); + await updateConfig.mutateAsync({ + projectId: config.projectId, + content: content || null + }); - createNotification({ - type: "success", - text: "Configuration successfully updated" - }); - } catch { - createNotification({ - type: "error", - text: "Failed to update Configuration" - }); - } + createNotification({ + type: "success", + text: "Configuration successfully updated" + }); }; return ( diff --git a/frontend/src/pages/ssh/SshHostGroupDetailsByIDPage/components/SshHostGroupHostsSection.tsx b/frontend/src/pages/ssh/SshHostGroupDetailsByIDPage/components/SshHostGroupHostsSection.tsx index df6f2b723..9b482538c 100644 --- a/frontend/src/pages/ssh/SshHostGroupDetailsByIDPage/components/SshHostGroupHostsSection.tsx +++ b/frontend/src/pages/ssh/SshHostGroupDetailsByIDPage/components/SshHostGroupHostsSection.tsx @@ -41,25 +41,17 @@ export const SshHostGroupHostsSection = ({ sshHostGroupId }: Props) => { }; const onRemoveSshHostSubmit = async (sshHostId: string) => { - try { - await removeHostFromGroup({ - sshHostId, - sshHostGroupId - }); + await removeHostFromGroup({ + sshHostId, + sshHostGroupId + }); - await createNotification({ - text: "Successfully removed host from SSH group", - type: "success" - }); + createNotification({ + text: "Successfully removed host from SSH group", + type: "success" + }); - handlePopUpClose("removeHostFromSshHostGroup"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to remove host from SSH group", - type: "error" - }); - } + handlePopUpClose("removeHostFromSshHostGroup"); }; return ( diff --git a/frontend/src/pages/ssh/SshHostsPage/components/SshHostGroupsSection.tsx b/frontend/src/pages/ssh/SshHostsPage/components/SshHostGroupsSection.tsx index 189a49686..e690480e1 100644 --- a/frontend/src/pages/ssh/SshHostsPage/components/SshHostGroupsSection.tsx +++ b/frontend/src/pages/ssh/SshHostsPage/components/SshHostGroupsSection.tsx @@ -35,22 +35,14 @@ export const SshHostGroupsSection = () => { }; const onRemoveSshHostGroupSubmit = async (sshHostGroupId: string) => { - try { - const hostGroup = await deleteSshHostGroup({ sshHostGroupId }); + const hostGroup = await deleteSshHostGroup({ sshHostGroupId }); - createNotification({ - text: `Successfully deleted SSH host group: ${hostGroup.name}`, - type: "success" - }); + createNotification({ + text: `Successfully deleted SSH host group: ${hostGroup.name}`, + type: "success" + }); - handlePopUpClose("deleteSshHostGroup"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete SSH host group", - type: "error" - }); - } + handlePopUpClose("deleteSshHostGroup"); }; return ( diff --git a/frontend/src/pages/ssh/SshHostsPage/components/SshHostModal.tsx b/frontend/src/pages/ssh/SshHostsPage/components/SshHostModal.tsx index e9b0d870a..afcde908f 100644 --- a/frontend/src/pages/ssh/SshHostsPage/components/SshHostModal.tsx +++ b/frontend/src/pages/ssh/SshHostsPage/components/SshHostModal.tsx @@ -140,93 +140,84 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => { }, [sshHost]); const onFormSubmit = async ({ hostname, alias, userCertTtl, loginMappings }: FormData) => { - try { - if (!projectId) return; + if (!projectId) return; - // Filter out login mappings that are from host groups - const hostLoginMappings = loginMappings.filter( - (mapping) => mapping.source === LoginMappingSource.HOST - ); + // Filter out login mappings that are from host groups + const hostLoginMappings = loginMappings.filter( + (mapping) => mapping.source === LoginMappingSource.HOST + ); - // check if there is already a different host with the same hostname - const existingHostnames = - sshHosts?.filter((h) => h.id !== sshHost?.id).map((h) => h.hostname) || []; + // check if there is already a different host with the same hostname + const existingHostnames = + sshHosts?.filter((h) => h.id !== sshHost?.id).map((h) => h.hostname) || []; - if (existingHostnames.includes(hostname.trim())) { + if (existingHostnames.includes(hostname.trim())) { + createNotification({ + text: "A host with this hostname already exists.", + type: "error" + }); + return; + } + + const trimmedAlias = alias.trim(); + + // check if there is already a different host with the same non-null alias + if (trimmedAlias) { + const existingAliases = + sshHosts?.filter((h) => h.id !== sshHost?.id && h.alias !== null).map((h) => h.alias) || []; + + if (existingAliases.includes(trimmedAlias)) { createNotification({ - text: "A host with this hostname already exists.", + text: "A host with this alias already exists.", type: "error" }); return; } + } - const trimmedAlias = alias.trim(); + const transformedLoginMappings = hostLoginMappings.map(({ loginUser, allowedPrincipals }) => { + const usernames = allowedPrincipals + .filter((p) => p.type === "user" && p.value) + .map((p) => p.value); - // check if there is already a different host with the same non-null alias - if (trimmedAlias) { - const existingAliases = - sshHosts?.filter((h) => h.id !== sshHost?.id && h.alias !== null).map((h) => h.alias) || - []; + const groupNames = allowedPrincipals + .filter((p) => p.type === "group" && p.value) + .map((p) => p.value); - if (existingAliases.includes(trimmedAlias)) { - createNotification({ - text: "A host with this alias already exists.", - type: "error" - }); - return; + return { + loginUser, + allowedPrincipals: { + usernames, + groups: groupNames } - } + }; + }); - const transformedLoginMappings = hostLoginMappings.map(({ loginUser, allowedPrincipals }) => { - const usernames = allowedPrincipals - .filter((p) => p.type === "user" && p.value) - .map((p) => p.value); - - const groupNames = allowedPrincipals - .filter((p) => p.type === "group" && p.value) - .map((p) => p.value); - - return { - loginUser, - allowedPrincipals: { - usernames, - groups: groupNames - } - }; + if (sshHost) { + await updateMutateAsync({ + sshHostId: sshHost.id, + hostname, + alias: trimmedAlias, + userCertTtl, + loginMappings: transformedLoginMappings }); - - if (sshHost) { - await updateMutateAsync({ - sshHostId: sshHost.id, - hostname, - alias: trimmedAlias, - userCertTtl, - loginMappings: transformedLoginMappings - }); - } else { - await createMutateAsync({ - projectId, - hostname, - alias: trimmedAlias, - userCertTtl, - loginMappings: transformedLoginMappings - }); - } - - reset(); - handlePopUpToggle("sshHost", false); - - createNotification({ - text: `Successfully ${sshHost ? "updated" : "added"} SSH host`, - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to ${sshHost ? "update" : "add"} SSH host`, - type: "error" + } else { + await createMutateAsync({ + projectId, + hostname, + alias: trimmedAlias, + userCertTtl, + loginMappings: transformedLoginMappings }); } + + reset(); + handlePopUpToggle("sshHost", false); + + createNotification({ + text: `Successfully ${sshHost ? "updated" : "added"} SSH host`, + type: "success" + }); }; const toggleMapping = (index: number) => { From eba04e4278ba71814daee4b5c7631adebfbcc7b4 Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Fri, 31 Oct 2025 19:23:32 -0300 Subject: [PATCH 010/294] refactor: simplify authentication form submission logic across multiple identity sections --- .../IdentityAliCloudAuthForm.tsx | 63 ++++---- .../IdentityAuthTemplateModal.tsx | 82 +++++------ .../IdentitySection/IdentityAwsAuthForm.tsx | 71 +++++---- .../IdentitySection/IdentityAzureAuthForm.tsx | 71 +++++---- .../IdentitySection/IdentityGcpAuthForm.tsx | 75 +++++----- .../IdentitySection/IdentityJwtAuthForm.tsx | 95 ++++++------ .../IdentityKubernetesAuthForm.tsx | 115 +++++++-------- .../IdentitySection/IdentityLdapAuthForm.tsx | 135 +++++++++--------- .../IdentitySection/IdentityLinkForm.tsx | 39 ++--- .../IdentitySection/IdentityModal.tsx | 123 +++++++--------- .../IdentitySection/IdentityOciAuthForm.tsx | 67 ++++----- .../IdentitySection/IdentityOidcAuthForm.tsx | 99 ++++++------- 12 files changed, 468 insertions(+), 567 deletions(-) diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAliCloudAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAliCloudAuthForm.tsx index 4ddcf964e..7dc5d7f70 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAliCloudAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAliCloudAuthForm.tsx @@ -137,45 +137,38 @@ export const IdentityAliCloudAuthForm = ({ accessTokenNumUsesLimit, accessTokenTrustedIps }: FormData) => { - try { - if (!identityId) return; + if (!identityId) return; - if (data) { - await updateMutateAsync({ - organizationId: orgId, - allowedArns, - identityId, - accessTokenTTL: Number(accessTokenTTL), - accessTokenMaxTTL: Number(accessTokenMaxTTL), - accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), - accessTokenTrustedIps - }); - } else { - await addMutateAsync({ - organizationId: orgId, - identityId, - allowedArns, - accessTokenTTL: Number(accessTokenTTL), - accessTokenMaxTTL: Number(accessTokenMaxTTL), - accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), - accessTokenTrustedIps - }); - } - - handlePopUpToggle("identityAuthMethod", false); - - createNotification({ - text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`, - type: "success" + if (data) { + await updateMutateAsync({ + organizationId: orgId, + allowedArns, + identityId, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps }); - - reset(); - } catch { - createNotification({ - text: `Failed to ${isUpdate ? "update" : "configure"} identity`, - type: "error" + } else { + await addMutateAsync({ + organizationId: orgId, + identityId, + allowedArns, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps }); } + + handlePopUpToggle("identityAuthMethod", false); + + createNotification({ + text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`, + type: "success" + }); + + reset(); }; return ( diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthTemplateModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthTemplateModal.tsx index 3bd423982..6db81f19e 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthTemplateModal.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthTemplateModal.tsx @@ -103,56 +103,44 @@ export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) = const selectedMethod = watch("method"); const onFormSubmit = async (data: FormData) => { - try { - if (isEdit && template) { - await updateTemplate({ - templateId: template.id, - organizationId: orgId, - name: data.name, - templateFields: { - url: data.url, - bindDN: data.bindDN, - bindPass: data.bindPass, - searchBase: data.searchBase, - ldapCaCertificate: data.ldapCaCertificate - } - }); - createNotification({ - text: "Successfully updated auth template", - type: "success" - }); - } else { - await createTemplate({ - organizationId: orgId, - name: data.name, - authMethod: data.method, - templateFields: { - url: data.url, - bindDN: data.bindDN, - bindPass: data.bindPass, - searchBase: data.searchBase, - ldapCaCertificate: data.ldapCaCertificate - } - }); - createNotification({ - text: "Successfully created auth template", - type: "success" - }); - } - - handlePopUpToggle(isEdit ? "editTemplate" : "createTemplate", false); - reset(); - } catch (err) { - console.error(err); - const error = err as any; - const text = - error?.response?.data?.message ?? `Failed to ${isEdit ? "update" : "create"} auth template`; - + if (isEdit && template) { + await updateTemplate({ + templateId: template.id, + organizationId: orgId, + name: data.name, + templateFields: { + url: data.url, + bindDN: data.bindDN, + bindPass: data.bindPass, + searchBase: data.searchBase, + ldapCaCertificate: data.ldapCaCertificate + } + }); createNotification({ - text, - type: "error" + text: "Successfully updated auth template", + type: "success" + }); + } else { + await createTemplate({ + organizationId: orgId, + name: data.name, + authMethod: data.method, + templateFields: { + url: data.url, + bindDN: data.bindDN, + bindPass: data.bindPass, + searchBase: data.searchBase, + ldapCaCertificate: data.ldapCaCertificate + } + }); + createNotification({ + text: "Successfully created auth template", + type: "success" }); } + + handlePopUpToggle(isEdit ? "editTemplate" : "createTemplate", false); + reset(); }; const handleClose = () => { diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsAuthForm.tsx index 7e3292c18..db1066731 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsAuthForm.tsx @@ -147,49 +147,42 @@ export const IdentityAwsAuthForm = ({ accessTokenNumUsesLimit, accessTokenTrustedIps }: FormData) => { - try { - if (!identityId) return; + if (!identityId) return; - if (data) { - await updateMutateAsync({ - organizationId: orgId, - stsEndpoint, - allowedPrincipalArns, - allowedAccountIds, - identityId, - accessTokenTTL: Number(accessTokenTTL), - accessTokenMaxTTL: Number(accessTokenMaxTTL), - accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), - accessTokenTrustedIps - }); - } else { - await addMutateAsync({ - organizationId: orgId, - identityId, - stsEndpoint: stsEndpoint || "", - allowedPrincipalArns: allowedPrincipalArns || "", - allowedAccountIds: allowedAccountIds || "", - accessTokenTTL: Number(accessTokenTTL), - accessTokenMaxTTL: Number(accessTokenMaxTTL), - accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), - accessTokenTrustedIps - }); - } - - handlePopUpToggle("identityAuthMethod", false); - - createNotification({ - text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`, - type: "success" + if (data) { + await updateMutateAsync({ + organizationId: orgId, + stsEndpoint, + allowedPrincipalArns, + allowedAccountIds, + identityId, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps }); - - reset(); - } catch { - createNotification({ - text: `Failed to ${isUpdate ? "update" : "configure"} identity`, - type: "error" + } else { + await addMutateAsync({ + organizationId: orgId, + identityId, + stsEndpoint: stsEndpoint || "", + allowedPrincipalArns: allowedPrincipalArns || "", + allowedAccountIds: allowedAccountIds || "", + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps }); } + + handlePopUpToggle("identityAuthMethod", false); + + createNotification({ + text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`, + type: "success" + }); + + reset(); }; return ( diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm.tsx index 518d97e7b..86f22c27e 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm.tsx @@ -143,49 +143,42 @@ export const IdentityAzureAuthForm = ({ accessTokenNumUsesLimit, accessTokenTrustedIps }: FormData) => { - try { - if (!identityId) return; + if (!identityId) return; - if (data) { - await updateMutateAsync({ - organizationId: orgId, - identityId, - tenantId, - resource, - allowedServicePrincipalIds, - accessTokenTTL: Number(accessTokenTTL), - accessTokenMaxTTL: Number(accessTokenMaxTTL), - accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), - accessTokenTrustedIps - }); - } else { - await addMutateAsync({ - organizationId: orgId, - identityId, - tenantId: tenantId || "", - resource: resource || "", - allowedServicePrincipalIds: allowedServicePrincipalIds || "", - accessTokenTTL: Number(accessTokenTTL), - accessTokenMaxTTL: Number(accessTokenMaxTTL), - accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), - accessTokenTrustedIps - }); - } - - handlePopUpToggle("identityAuthMethod", false); - - createNotification({ - text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`, - type: "success" + if (data) { + await updateMutateAsync({ + organizationId: orgId, + identityId, + tenantId, + resource, + allowedServicePrincipalIds, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps }); - - reset(); - } catch { - createNotification({ - text: `Failed to ${isUpdate ? "update" : "configure"} identity`, - type: "error" + } else { + await addMutateAsync({ + organizationId: orgId, + identityId, + tenantId: tenantId || "", + resource: resource || "", + allowedServicePrincipalIds: allowedServicePrincipalIds || "", + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps }); } + + handlePopUpToggle("identityAuthMethod", false); + + createNotification({ + text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`, + type: "success" + }); + + reset(); }; return ( diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm.tsx index 7175e8b62..4d6ea63b2 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm.tsx @@ -152,51 +152,44 @@ export const IdentityGcpAuthForm = ({ accessTokenNumUsesLimit, accessTokenTrustedIps }: FormData) => { - try { - if (!identityId) return; + if (!identityId) return; - if (data) { - await updateMutateAsync({ - identityId, - organizationId: orgId, - type, - allowedServiceAccounts, - allowedProjects, - allowedZones, - accessTokenTTL: Number(accessTokenTTL), - accessTokenMaxTTL: Number(accessTokenMaxTTL), - accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), - accessTokenTrustedIps - }); - } else { - await addMutateAsync({ - identityId, - organizationId: orgId, - type, - allowedServiceAccounts: allowedServiceAccounts || "", - allowedProjects: allowedProjects || "", - allowedZones: allowedZones || "", - accessTokenTTL: Number(accessTokenTTL), - accessTokenMaxTTL: Number(accessTokenMaxTTL), - accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), - accessTokenTrustedIps - }); - } - - handlePopUpToggle("identityAuthMethod", false); - - createNotification({ - text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`, - type: "success" + if (data) { + await updateMutateAsync({ + identityId, + organizationId: orgId, + type, + allowedServiceAccounts, + allowedProjects, + allowedZones, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps }); - - reset(); - } catch { - createNotification({ - text: `Failed to ${isUpdate ? "update" : "configure"} identity`, - type: "error" + } else { + await addMutateAsync({ + identityId, + organizationId: orgId, + type, + allowedServiceAccounts: allowedServiceAccounts || "", + allowedProjects: allowedProjects || "", + allowedZones: allowedZones || "", + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps }); } + + handlePopUpToggle("identityAuthMethod", false); + + createNotification({ + text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`, + type: "success" + }); + + reset(); }; return ( diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm.tsx index f62045810..dfc079790 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm.tsx @@ -217,61 +217,54 @@ export const IdentityJwtAuthForm = ({ boundClaims, boundSubject }: FormData) => { - try { - if (!identityId) { - return; - } + if (!identityId) { + return; + } - if (data) { - await updateMutateAsync({ - identityId, - organizationId: orgId, - configurationType, - jwksUrl, - jwksCaCert, - publicKeys: publicKeys?.map((field) => field.value).filter(Boolean), - boundIssuer, - boundAudiences, - boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])), - boundSubject, - accessTokenTTL: Number(accessTokenTTL), - accessTokenMaxTTL: Number(accessTokenMaxTTL), - accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), - accessTokenTrustedIps - }); - } else { - await addMutateAsync({ - identityId, - configurationType, - jwksUrl, - jwksCaCert, - publicKeys: publicKeys?.map((field) => field.value).filter(Boolean), - boundIssuer, - boundAudiences, - boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])), - boundSubject, - organizationId: orgId, - accessTokenTTL: Number(accessTokenTTL), - accessTokenMaxTTL: Number(accessTokenMaxTTL), - accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), - accessTokenTrustedIps - }); - } - - handlePopUpToggle("identityAuthMethod", false); - - createNotification({ - text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`, - type: "success" + if (data) { + await updateMutateAsync({ + identityId, + organizationId: orgId, + configurationType, + jwksUrl, + jwksCaCert, + publicKeys: publicKeys?.map((field) => field.value).filter(Boolean), + boundIssuer, + boundAudiences, + boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])), + boundSubject, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps }); - - reset(); - } catch { - createNotification({ - text: `Failed to ${isUpdate ? "update" : "configure"} identity`, - type: "error" + } else { + await addMutateAsync({ + identityId, + configurationType, + jwksUrl, + jwksCaCert, + publicKeys: publicKeys?.map((field) => field.value).filter(Boolean), + boundIssuer, + boundAudiences, + boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])), + boundSubject, + organizationId: orgId, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps }); } + + handlePopUpToggle("identityAuthMethod", false); + + createNotification({ + text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`, + type: "success" + }); + + reset(); }; return ( diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx index 9b8624990..7fedf4a99 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx @@ -311,71 +311,64 @@ export const IdentityKubernetesAuthForm = ({ tokenReviewMode, accessTokenTrustedIps }: FormData) => { - try { - if (!identityId) return; + if (!identityId) return; - if (data) { - await updateMutateAsync({ - organizationId: orgId, - ...(tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Api - ? { - kubernetesHost: kubernetesHost || "" - } - : { - kubernetesHost: null - }), - tokenReviewerJwt: tokenReviewerJwt || null, - allowedNames, - allowedNamespaces, - allowedAudience, - caCert, - identityId, - gatewayId: gatewayId || null, - tokenReviewMode, - accessTokenTTL: Number(accessTokenTTL), - accessTokenMaxTTL: Number(accessTokenMaxTTL), - accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), - accessTokenTrustedIps - }); - } else { - await addMutateAsync({ - organizationId: orgId, - identityId, - ...(tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Api - ? { - kubernetesHost: kubernetesHost || "" - } - : { - kubernetesHost: null - }), - tokenReviewerJwt: tokenReviewerJwt || undefined, - allowedNames: allowedNames || "", - allowedNamespaces: allowedNamespaces || "", - allowedAudience: allowedAudience || "", - gatewayId: gatewayId || null, - caCert: caCert || "", - tokenReviewMode, - accessTokenTTL: Number(accessTokenTTL), - accessTokenMaxTTL: Number(accessTokenMaxTTL), - accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), - accessTokenTrustedIps - }); - } - - handlePopUpToggle("identityAuthMethod", false); - - createNotification({ - text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`, - type: "success" + if (data) { + await updateMutateAsync({ + organizationId: orgId, + ...(tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Api + ? { + kubernetesHost: kubernetesHost || "" + } + : { + kubernetesHost: null + }), + tokenReviewerJwt: tokenReviewerJwt || null, + allowedNames, + allowedNamespaces, + allowedAudience, + caCert, + identityId, + gatewayId: gatewayId || null, + tokenReviewMode, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps }); - - reset(); - } catch { - createNotification({ - text: `Failed to ${isUpdate ? "update" : "configure"} identity`, - type: "error" + } else { + await addMutateAsync({ + organizationId: orgId, + identityId, + ...(tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Api + ? { + kubernetesHost: kubernetesHost || "" + } + : { + kubernetesHost: null + }), + tokenReviewerJwt: tokenReviewerJwt || undefined, + allowedNames: allowedNames || "", + allowedNamespaces: allowedNamespaces || "", + allowedAudience: allowedAudience || "", + gatewayId: gatewayId || null, + caCert: caCert || "", + tokenReviewMode, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps }); } + + handlePopUpToggle("identityAuthMethod", false); + + createNotification({ + text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`, + type: "success" + }); + + reset(); }; const tokenReviewMode = watch("tokenReviewMode"); diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm.tsx index b2096fa06..44b3a32de 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm.tsx @@ -316,83 +316,76 @@ export const IdentityLdapAuthForm = ({ }, [subscription, handlePopUpOpen, handlePopUpToggle]); const onFormSubmit = async (formData: FormData) => { - try { - if (!identityId) return; + if (!identityId) return; - const { - scope: submissionScope, - templateId: submissionTemplateId, - url: submissionUrl, - bindDN: submissionBindDN, - bindPass: submissionBindPass, - searchBase: submissionSearchBase, - searchFilter, - ldapCaCertificate, - allowedFields, - accessTokenTTL, - accessTokenMaxTTL, - accessTokenNumUsesLimit, - accessTokenTrustedIps, - lockoutEnabled, - lockoutThreshold, - lockoutDurationValue, - lockoutDurationUnit, - lockoutCounterResetValue, - lockoutCounterResetUnit - } = formData; + const { + scope: submissionScope, + templateId: submissionTemplateId, + url: submissionUrl, + bindDN: submissionBindDN, + bindPass: submissionBindPass, + searchBase: submissionSearchBase, + searchFilter, + ldapCaCertificate, + allowedFields, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + lockoutEnabled, + lockoutThreshold, + lockoutDurationValue, + lockoutDurationUnit, + lockoutCounterResetValue, + lockoutCounterResetUnit + } = formData; - const lockoutDurationSeconds = ms(`${lockoutDurationValue}${lockoutDurationUnit}`) / 1000; - const lockoutCounterResetSeconds = - ms(`${lockoutCounterResetValue}${lockoutCounterResetUnit}`) / 1000; + const lockoutDurationSeconds = ms(`${lockoutDurationValue}${lockoutDurationUnit}`) / 1000; + const lockoutCounterResetSeconds = + ms(`${lockoutCounterResetValue}${lockoutCounterResetUnit}`) / 1000; - const basePayload = { - organizationId: orgId, - identityId, - searchFilter, - ldapCaCertificate, - allowedFields, - accessTokenTTL: Number(accessTokenTTL), - accessTokenMaxTTL: Number(accessTokenMaxTTL), - accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), - accessTokenTrustedIps, - lockoutEnabled, - lockoutThreshold: Number(lockoutThreshold), - lockoutDurationSeconds, - lockoutCounterResetSeconds - }; + const basePayload = { + organizationId: orgId, + identityId, + searchFilter, + ldapCaCertificate, + allowedFields, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps, + lockoutEnabled, + lockoutThreshold: Number(lockoutThreshold), + lockoutDurationSeconds, + lockoutCounterResetSeconds + }; - // Add scope-specific fields - const payload = - submissionScope === "template" - ? { ...basePayload, templateId: submissionTemplateId } - : { - ...basePayload, - url: submissionUrl, - bindDN: submissionBindDN, - bindPass: submissionBindPass, - searchBase: submissionSearchBase - }; + // Add scope-specific fields + const payload = + submissionScope === "template" + ? { ...basePayload, templateId: submissionTemplateId } + : { + ...basePayload, + url: submissionUrl, + bindDN: submissionBindDN, + bindPass: submissionBindPass, + searchBase: submissionSearchBase + }; - if (data) { - await updateMutateAsync(payload); - } else { - await addMutateAsync(payload); - } - - 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" - }); + if (data) { + await updateMutateAsync(payload); + } else { + await addMutateAsync(payload); } + + handlePopUpToggle("identityAuthMethod", false); + + createNotification({ + text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`, + type: "success" + }); + + reset(); }; return ( diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLinkForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLinkForm.tsx index b0977437b..545aea5aa 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLinkForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLinkForm.tsx @@ -42,31 +42,20 @@ export const IdentityLinkForm = ({ onClose }: Props) => { }); const onFormSubmit = async ({ identity, role }: FormData) => { - try { - await createMutateAsync({ - identityId: identity.id, - roles: [{ role: role.slug, isTemporary: false }] - }); - createNotification({ - text: "Successfully linked identity", - type: "success" - }); - navigate({ - to: "/organization/identities/$identityId", - params: { - identityId: identity.id - } - }); - } catch (err) { - console.error(err); - const error = err as any; - const text = error?.response?.data?.message ?? "Failed to link identity"; - - createNotification({ - text, - type: "error" - }); - } + await createMutateAsync({ + identityId: identity.id, + roles: [{ role: role.slug, isTemporary: false }] + }); + createNotification({ + text: "Successfully linked identity", + type: "success" + }); + navigate({ + to: "/organization/identities/$identityId", + params: { + identityId: identity.id + } + }); }; return ( diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx index dacbba428..09e779fa9 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx @@ -108,81 +108,68 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { }, [popUp?.identity?.data, roles]); const onFormSubmit = async ({ name, role, metadata, hasDeleteProtection }: FormData) => { - try { - const identity = popUp?.identity?.data as { - identityId: string; - name: string; - role: string; - hasDeleteProtection: boolean; - orgId: string; - }; + const identity = popUp?.identity?.data as { + identityId: string; + name: string; + role: string; + hasDeleteProtection: boolean; + orgId: string; + }; - if (identity) { - // update + if (identity) { + // update - await updateMutateAsync({ - identityId: identity.identityId, - name, - role: role.slug || undefined, - hasDeleteProtection, - organizationId: orgId, - metadata - }); - - handlePopUpToggle("identity", false); - } else { - // create - - const { id: createdId } = await createMutateAsync({ - name, - role: role.slug || undefined, - hasDeleteProtection, - organizationId: orgId, - metadata - }); - - await addMutateAsync({ - organizationId: orgId, - identityId: createdId, - clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }], - accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }], - accessTokenTTL: 2592000, - accessTokenMaxTTL: 2592000, - accessTokenNumUsesLimit: 0, - accessTokenPeriod: 0, - lockoutEnabled: true, - lockoutThreshold: 3, - lockoutDurationSeconds: 300, - lockoutCounterResetSeconds: 30 - }); - - handlePopUpToggle("identity", false); - navigate({ - to: "/organization/identities/$identityId", - params: { - identityId: createdId - } - }); - } - - createNotification({ - text: `Successfully ${popUp?.identity?.data ? "updated" : "created"} identity`, - type: "success" + await updateMutateAsync({ + identityId: identity.identityId, + name, + role: role.slug || undefined, + hasDeleteProtection, + organizationId: orgId, + metadata }); - reset(); - } catch (err) { - console.error(err); - const error = err as any; - const text = - error?.response?.data?.message ?? - `Failed to ${popUp?.identity?.data ? "update" : "create"} identity`; + handlePopUpToggle("identity", false); + } else { + // create - createNotification({ - text, - type: "error" + const { id: createdId } = await createMutateAsync({ + name, + role: role.slug || undefined, + hasDeleteProtection, + organizationId: orgId, + metadata + }); + + await addMutateAsync({ + organizationId: orgId, + identityId: createdId, + clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }], + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }], + accessTokenTTL: 2592000, + accessTokenMaxTTL: 2592000, + accessTokenNumUsesLimit: 0, + accessTokenPeriod: 0, + lockoutEnabled: true, + lockoutThreshold: 3, + lockoutDurationSeconds: 300, + lockoutCounterResetSeconds: 30 + }); + + handlePopUpToggle("identity", false); + navigate({ + to: "/organization/identities/$identityId", + params: { + identityId: createdId + } }); } + + createNotification({ + text: `Successfully ${popUp?.identity?.data ? "updated" : "created"} identity`, + type: "success" + }); + + reset(); }; return ( diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOciAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOciAuthForm.tsx index 5e33c71bb..ffbde6f7a 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOciAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOciAuthForm.tsx @@ -149,47 +149,40 @@ export const IdentityOciAuthForm = ({ accessTokenNumUsesLimit, accessTokenTrustedIps }: FormData) => { - try { - if (!identityId) return; + if (!identityId) return; - if (data) { - await updateMutateAsync({ - organizationId: orgId, - tenancyOcid, - allowedUsernames, - identityId, - accessTokenTTL: Number(accessTokenTTL), - accessTokenMaxTTL: Number(accessTokenMaxTTL), - accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), - accessTokenTrustedIps - }); - } else { - await addMutateAsync({ - organizationId: orgId, - identityId, - tenancyOcid, - allowedUsernames: allowedUsernames || undefined, - accessTokenTTL: Number(accessTokenTTL), - accessTokenMaxTTL: Number(accessTokenMaxTTL), - accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), - accessTokenTrustedIps - }); - } - - handlePopUpToggle("identityAuthMethod", false); - - createNotification({ - text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`, - type: "success" + if (data) { + await updateMutateAsync({ + organizationId: orgId, + tenancyOcid, + allowedUsernames, + identityId, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps }); - - reset(); - } catch { - createNotification({ - text: `Failed to ${isUpdate ? "update" : "configure"} identity`, - type: "error" + } else { + await addMutateAsync({ + organizationId: orgId, + identityId, + tenancyOcid, + allowedUsernames: allowedUsernames || 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(); }; return ( diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx index 15fbc4b72..0ada2e663 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx @@ -201,63 +201,56 @@ export const IdentityOidcAuthForm = ({ claimMetadataMapping, boundSubject }: FormData) => { - try { - if (!identityId) { - return; - } + if (!identityId) { + return; + } - if (data) { - await updateMutateAsync({ - identityId, - organizationId: orgId, - oidcDiscoveryUrl, - caCert, - boundIssuer, - boundAudiences, - boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])), - claimMetadataMapping: claimMetadataMapping - ? Object.fromEntries(claimMetadataMapping.map((entry) => [entry.key, entry.value])) - : undefined, - boundSubject, - accessTokenTTL: Number(accessTokenTTL), - accessTokenMaxTTL: Number(accessTokenMaxTTL), - accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), - accessTokenTrustedIps - }); - } else { - await addMutateAsync({ - identityId, - oidcDiscoveryUrl, - caCert, - boundIssuer, - boundAudiences, - boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])), - claimMetadataMapping: claimMetadataMapping - ? Object.fromEntries(claimMetadataMapping.map((entry) => [entry.key, entry.value])) - : undefined, - boundSubject, - organizationId: orgId, - accessTokenTTL: Number(accessTokenTTL), - accessTokenMaxTTL: Number(accessTokenMaxTTL), - accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), - accessTokenTrustedIps - }); - } - - handlePopUpToggle("identityAuthMethod", false); - - createNotification({ - text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`, - type: "success" + if (data) { + await updateMutateAsync({ + identityId, + organizationId: orgId, + oidcDiscoveryUrl, + caCert, + boundIssuer, + boundAudiences, + boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])), + claimMetadataMapping: claimMetadataMapping + ? Object.fromEntries(claimMetadataMapping.map((entry) => [entry.key, entry.value])) + : undefined, + boundSubject, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps }); - - reset(); - } catch { - createNotification({ - text: `Failed to ${isUpdate ? "update" : "configure"} identity`, - type: "error" + } else { + await addMutateAsync({ + identityId, + oidcDiscoveryUrl, + caCert, + boundIssuer, + boundAudiences, + boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])), + claimMetadataMapping: claimMetadataMapping + ? Object.fromEntries(claimMetadataMapping.map((entry) => [entry.key, entry.value])) + : undefined, + boundSubject, + organizationId: orgId, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps }); } + + handlePopUpToggle("identityAuthMethod", false); + + createNotification({ + text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`, + type: "success" + }); + + reset(); }; return ( From 19ba7a28922431dd76e166933e46f011594fe662 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Fri, 31 Oct 2025 16:17:46 -0700 Subject: [PATCH 011/294] fix: update audit log clear filters to apply to subsequent filter settings --- .../AuditLogsPage/components/LogsFilter.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx index 72561b90d..8e0124fb5 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx @@ -165,7 +165,7 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => { { - resetField("eventType"); + setValue("eventType", [], { shouldDirty: true }); }} > { { - resetField("userAgentType"); + setValue("userAgentType", undefined, { shouldDirty: true }); }} > { { - resetField("project"); - resetField("environment"); - setValue("secretPath", ""); - setValue("secretKey", ""); + setValue("project", null, { shouldDirty: true }); + setValue("environment", undefined, { shouldDirty: true }); + setValue("secretPath", "", { shouldDirty: true }); + setValue("secretKey", "", { shouldDirty: true }); }} > { } className={twMerge(!selectedProject && "opacity-50")} onClear={() => { - resetField("environment"); + setValue("environment", undefined, { shouldDirty: true }); }} > { } className={twMerge(!selectedProject && "opacity-50")} onClear={() => { - setValue("secretPath", ""); + setValue("secretPath", "", { shouldDirty: true }); }} > { className={twMerge(!selectedProject && "opacity-50")} label="Secret Key" onClear={() => { - setValue("secretKey", ""); + setValue("secretKey", "", { shouldDirty: true }); }} > Date: Mon, 3 Nov 2025 18:16:01 +0530 Subject: [PATCH 012/294] chore: moves chef integration to ee --- .../chef-connection-router.ts | 11 +++++------ .../v1/secret-sync-routers/chef-sync-router.ts | 5 ++--- .../app-connections}/chef/chef-connection-enums.ts | 0 .../app-connections}/chef/chef-connection-fns.ts | 2 +- .../chef/chef-connection-schemas.ts | 0 .../chef/chef-connection-service.ts | 2 +- .../app-connections}/chef/chef-connection-types.ts | 4 ++-- .../services/app-connections}/chef/index.ts | 0 .../secret-sync/chef/chef-sync-constants.ts | 3 ++- .../services/secret-sync/chef/chef-sync-fns.ts | 2 +- .../services/secret-sync/chef/chef-sync-schemas.ts | 3 ++- .../services/secret-sync/chef/chef-sync-types.ts | 2 +- .../{ => ee}/services/secret-sync/chef/index.ts | 0 .../app-connection-router.ts | 2 +- .../routes/v1/app-connection-routers/index.ts | 2 +- .../server/routes/v1/secret-sync-routers/index.ts | 2 +- .../v1/secret-sync-routers/secret-sync-router.ts | 2 +- .../services/app-connection/app-connection-fns.ts | 8 ++++++-- .../services/app-connection/app-connection-maps.ts | 2 +- .../app-connection/app-connection-service.ts | 4 ++-- .../app-connection/app-connection-types.ts | 12 ++++++------ .../src/services/secret-sync/secret-sync-fns.ts | 14 ++++---------- .../src/services/secret-sync/secret-sync-maps.ts | 2 +- .../src/services/secret-sync/secret-sync-types.ts | 7 ++++++- frontend/src/helpers/appConnections.ts | 2 +- 25 files changed, 48 insertions(+), 45 deletions(-) rename backend/src/{server => ee}/routes/v1/app-connection-routers/chef-connection-router.ts (92%) rename backend/src/{server => ee}/routes/v1/secret-sync-routers/chef-sync-router.ts (72%) rename backend/src/{services/app-connection => ee/services/app-connections}/chef/chef-connection-enums.ts (100%) rename backend/src/{services/app-connection => ee/services/app-connections}/chef/chef-connection-fns.ts (98%) rename backend/src/{services/app-connection => ee/services/app-connections}/chef/chef-connection-schemas.ts (100%) rename backend/src/{services/app-connection => ee/services/app-connections}/chef/chef-connection-service.ts (93%) rename backend/src/{services/app-connection => ee/services/app-connections}/chef/chef-connection-types.ts (87%) rename backend/src/{services/app-connection => ee/services/app-connections}/chef/index.ts (100%) rename backend/src/{ => ee}/services/secret-sync/chef/chef-sync-constants.ts (89%) rename backend/src/{ => ee}/services/secret-sync/chef/chef-sync-fns.ts (99%) rename backend/src/{ => ee}/services/secret-sync/chef/chef-sync-schemas.ts (96%) rename backend/src/{ => ee}/services/secret-sync/chef/chef-sync-types.ts (92%) rename backend/src/{ => ee}/services/secret-sync/chef/index.ts (100%) diff --git a/backend/src/server/routes/v1/app-connection-routers/chef-connection-router.ts b/backend/src/ee/routes/v1/app-connection-routers/chef-connection-router.ts similarity index 92% rename from backend/src/server/routes/v1/app-connection-routers/chef-connection-router.ts rename to backend/src/ee/routes/v1/app-connection-routers/chef-connection-router.ts index 6bfd83391..2855d8b37 100644 --- a/backend/src/server/routes/v1/app-connection-routers/chef-connection-router.ts +++ b/backend/src/ee/routes/v1/app-connection-routers/chef-connection-router.ts @@ -1,17 +1,16 @@ import z from "zod"; -import { readLimit } from "@app/server/config/rateLimiter"; -import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { CreateChefConnectionSchema, SanitizedChefConnectionSchema, UpdateChefConnectionSchema -} from "@app/services/app-connection/chef"; +} from "@app/ee/services/app-connections/chef"; +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { registerAppConnectionEndpoints } from "@app/server/routes/v1/app-connection-routers/app-connection-endpoints"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AuthMode } from "@app/services/auth/auth-type"; -import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; - export const registerChefConnectionRouter = async (server: FastifyZodProvider) => { registerAppConnectionEndpoints({ app: AppConnection.Chef, diff --git a/backend/src/server/routes/v1/secret-sync-routers/chef-sync-router.ts b/backend/src/ee/routes/v1/secret-sync-routers/chef-sync-router.ts similarity index 72% rename from backend/src/server/routes/v1/secret-sync-routers/chef-sync-router.ts rename to backend/src/ee/routes/v1/secret-sync-routers/chef-sync-router.ts index 6972a9b70..3bdd5bce6 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/chef-sync-router.ts +++ b/backend/src/ee/routes/v1/secret-sync-routers/chef-sync-router.ts @@ -1,8 +1,7 @@ -import { ChefSyncSchema, CreateChefSyncSchema, UpdateChefSyncSchema } from "@app/services/secret-sync/chef"; +import { ChefSyncSchema, CreateChefSyncSchema, UpdateChefSyncSchema } from "@app/ee/services/secret-sync/chef"; +import { registerSyncSecretsEndpoints } from "@app/server/routes/v1/secret-sync-routers/secret-sync-endpoints"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; -import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; - export const registerChefSyncRouter = async (server: FastifyZodProvider) => registerSyncSecretsEndpoints({ destination: SecretSync.Chef, diff --git a/backend/src/services/app-connection/chef/chef-connection-enums.ts b/backend/src/ee/services/app-connections/chef/chef-connection-enums.ts similarity index 100% rename from backend/src/services/app-connection/chef/chef-connection-enums.ts rename to backend/src/ee/services/app-connections/chef/chef-connection-enums.ts diff --git a/backend/src/services/app-connection/chef/chef-connection-fns.ts b/backend/src/ee/services/app-connections/chef/chef-connection-fns.ts similarity index 98% rename from backend/src/services/app-connection/chef/chef-connection-fns.ts rename to backend/src/ee/services/app-connections/chef/chef-connection-fns.ts index 1b35628bc..31f5d0ee0 100644 --- a/backend/src/services/app-connection/chef/chef-connection-fns.ts +++ b/backend/src/ee/services/app-connections/chef/chef-connection-fns.ts @@ -8,7 +8,7 @@ import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; import { TChefDataBagItemContent } from "../../secret-sync/chef/chef-sync-types"; -import { AppConnection } from "../app-connection-enums"; +import { AppConnection } from "../../../../services/app-connection/app-connection-enums"; import { ChefConnectionMethod } from "./chef-connection-enums"; import { TChefConnection, diff --git a/backend/src/services/app-connection/chef/chef-connection-schemas.ts b/backend/src/ee/services/app-connections/chef/chef-connection-schemas.ts similarity index 100% rename from backend/src/services/app-connection/chef/chef-connection-schemas.ts rename to backend/src/ee/services/app-connections/chef/chef-connection-schemas.ts diff --git a/backend/src/services/app-connection/chef/chef-connection-service.ts b/backend/src/ee/services/app-connections/chef/chef-connection-service.ts similarity index 93% rename from backend/src/services/app-connection/chef/chef-connection-service.ts rename to backend/src/ee/services/app-connections/chef/chef-connection-service.ts index c989e7eaf..05969bf5d 100644 --- a/backend/src/services/app-connection/chef/chef-connection-service.ts +++ b/backend/src/ee/services/app-connections/chef/chef-connection-service.ts @@ -1,7 +1,7 @@ import { ForbiddenRequestError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; -import { AppConnection } from "../app-connection-enums"; +import { AppConnection } from "../../../../services/app-connection/app-connection-enums"; import { listChefDataBagItems, listChefDataBags } from "./chef-connection-fns"; import { TChefConnection } from "./chef-connection-types"; diff --git a/backend/src/services/app-connection/chef/chef-connection-types.ts b/backend/src/ee/services/app-connections/chef/chef-connection-types.ts similarity index 87% rename from backend/src/services/app-connection/chef/chef-connection-types.ts rename to backend/src/ee/services/app-connections/chef/chef-connection-types.ts index a2da80d3d..096218951 100644 --- a/backend/src/services/app-connection/chef/chef-connection-types.ts +++ b/backend/src/ee/services/app-connections/chef/chef-connection-types.ts @@ -1,9 +1,9 @@ import z from "zod"; import { DiscriminativePick } from "@app/lib/types"; -import { TChefDataBagItemContent } from "@app/services/secret-sync/chef"; +import { TChefDataBagItemContent } from "@app/ee/services/secret-sync/chef"; -import { AppConnection } from "../app-connection-enums"; +import { AppConnection } from "../../../../services/app-connection/app-connection-enums"; import { ChefConnectionSchema, CreateChefConnectionSchema, diff --git a/backend/src/services/app-connection/chef/index.ts b/backend/src/ee/services/app-connections/chef/index.ts similarity index 100% rename from backend/src/services/app-connection/chef/index.ts rename to backend/src/ee/services/app-connections/chef/index.ts diff --git a/backend/src/services/secret-sync/chef/chef-sync-constants.ts b/backend/src/ee/services/secret-sync/chef/chef-sync-constants.ts similarity index 89% rename from backend/src/services/secret-sync/chef/chef-sync-constants.ts rename to backend/src/ee/services/secret-sync/chef/chef-sync-constants.ts index 567b25bbe..8bbf0e12a 100644 --- a/backend/src/services/secret-sync/chef/chef-sync-constants.ts +++ b/backend/src/ee/services/secret-sync/chef/chef-sync-constants.ts @@ -6,5 +6,6 @@ export const CHEF_SYNC_LIST_OPTION: TSecretSyncListItem = { name: "Chef", destination: SecretSync.Chef, connection: AppConnection.Chef, - canImportSecrets: true + canImportSecrets: true, + enterprise: true }; diff --git a/backend/src/services/secret-sync/chef/chef-sync-fns.ts b/backend/src/ee/services/secret-sync/chef/chef-sync-fns.ts similarity index 99% rename from backend/src/services/secret-sync/chef/chef-sync-fns.ts rename to backend/src/ee/services/secret-sync/chef/chef-sync-fns.ts index 7f32b398a..65a4fca9b 100644 --- a/backend/src/services/secret-sync/chef/chef-sync-fns.ts +++ b/backend/src/ee/services/secret-sync/chef/chef-sync-fns.ts @@ -1,4 +1,4 @@ -import { getChefDataBagItem, updateChefDataBagItem } from "@app/services/app-connection/chef"; +import { getChefDataBagItem, updateChefDataBagItem } from "@app/ee/services/app-connections/chef"; import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; diff --git a/backend/src/services/secret-sync/chef/chef-sync-schemas.ts b/backend/src/ee/services/secret-sync/chef/chef-sync-schemas.ts similarity index 96% rename from backend/src/services/secret-sync/chef/chef-sync-schemas.ts rename to backend/src/ee/services/secret-sync/chef/chef-sync-schemas.ts index c2e09011e..9702f97d3 100644 --- a/backend/src/services/secret-sync/chef/chef-sync-schemas.ts +++ b/backend/src/ee/services/secret-sync/chef/chef-sync-schemas.ts @@ -42,5 +42,6 @@ export const ChefSyncListItemSchema = z.object({ name: z.literal("Chef"), connection: z.literal(AppConnection.Chef), destination: z.literal(SecretSync.Chef), - canImportSecrets: z.literal(true) + canImportSecrets: z.literal(true), + enterprise: z.boolean() }); diff --git a/backend/src/services/secret-sync/chef/chef-sync-types.ts b/backend/src/ee/services/secret-sync/chef/chef-sync-types.ts similarity index 92% rename from backend/src/services/secret-sync/chef/chef-sync-types.ts rename to backend/src/ee/services/secret-sync/chef/chef-sync-types.ts index 464e0372d..0f70e7e1a 100644 --- a/backend/src/services/secret-sync/chef/chef-sync-types.ts +++ b/backend/src/ee/services/secret-sync/chef/chef-sync-types.ts @@ -1,6 +1,6 @@ import z from "zod"; -import { TChefConnection } from "@app/services/app-connection/chef"; +import { TChefConnection } from "@app/ee/services/app-connections/chef"; import { ChefSyncListItemSchema, ChefSyncSchema, CreateChefSyncSchema } from "./chef-sync-schemas"; diff --git a/backend/src/services/secret-sync/chef/index.ts b/backend/src/ee/services/secret-sync/chef/index.ts similarity index 100% rename from backend/src/services/secret-sync/chef/index.ts rename to backend/src/ee/services/secret-sync/chef/index.ts diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index 9a4d7f38b..5a3496750 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { ProjectType } from "@app/db/schemas"; +import { ChefConnectionListItemSchema, SanitizedChefConnectionSchema } from "@app/ee/services/app-connections/chef"; import { OCIConnectionListItemSchema, SanitizedOCIConnectionSchema } from "@app/ee/services/app-connections/oci"; import { OracleDBConnectionListItemSchema, @@ -48,7 +49,6 @@ import { ChecklyConnectionListItemSchema, SanitizedChecklyConnectionSchema } from "@app/services/app-connection/checkly"; -import { ChefConnectionListItemSchema, SanitizedChefConnectionSchema } from "@app/services/app-connection/chef"; import { CloudflareConnectionListItemSchema, SanitizedCloudflareConnectionSchema diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index 32e9efe4c..aa1d671b6 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -1,3 +1,4 @@ +import { registerChefConnectionRouter } from "@app/ee/routes/v1/app-connection-routers/chef-connection-router"; import { registerOCIConnectionRouter } from "@app/ee/routes/v1/app-connection-routers/oci-connection-router"; import { registerOracleDBConnectionRouter } from "@app/ee/routes/v1/app-connection-routers/oracledb-connection-router"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; @@ -13,7 +14,6 @@ import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connect import { registerBitbucketConnectionRouter } from "./bitbucket-connection-router"; import { registerCamundaConnectionRouter } from "./camunda-connection-router"; import { registerChecklyConnectionRouter } from "./checkly-connection-router"; -import { registerChefConnectionRouter } from "./chef-connection-router"; import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router"; import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; import { registerDigitalOceanConnectionRouter } from "./digital-ocean-connection-router"; diff --git a/backend/src/server/routes/v1/secret-sync-routers/index.ts b/backend/src/server/routes/v1/secret-sync-routers/index.ts index d305dc342..810e5b7fa 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -1,3 +1,4 @@ +import { registerChefSyncRouter } from "@app/ee/routes/v1/secret-sync-routers/chef-sync-router"; import { registerOCIVaultSyncRouter } from "@app/ee/routes/v1/secret-sync-routers/oci-vault-sync-router"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; @@ -10,7 +11,6 @@ import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router"; import { registerBitbucketSyncRouter } from "./bitbucket-sync-router"; import { registerCamundaSyncRouter } from "./camunda-sync-router"; import { registerChecklySyncRouter } from "./checkly-sync-router"; -import { registerChefSyncRouter } from "./chef-sync-router"; import { registerCloudflarePagesSyncRouter } from "./cloudflare-pages-sync-router"; import { registerCloudflareWorkersSyncRouter } from "./cloudflare-workers-sync-router"; import { registerDatabricksSyncRouter } from "./databricks-sync-router"; diff --git a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts index 85adf12f1..0f394f8b2 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts @@ -24,7 +24,7 @@ import { AzureKeyVaultSyncListItemSchema, AzureKeyVaultSyncSchema } from "@app/s import { BitbucketSyncListItemSchema, BitbucketSyncSchema } from "@app/services/secret-sync/bitbucket"; import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda"; import { ChecklySyncListItemSchema, ChecklySyncSchema } from "@app/services/secret-sync/checkly/checkly-sync-schemas"; -import { ChefSyncListItemSchema, ChefSyncSchema } from "@app/services/secret-sync/chef"; +import { ChefSyncListItemSchema, ChefSyncSchema } from "@app/ee/services/secret-sync/chef"; import { CloudflarePagesSyncListItemSchema, CloudflarePagesSyncSchema diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 3c511fd4d..87f22b031 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -1,5 +1,10 @@ import { ProjectType } from "@app/db/schemas"; import { TAppConnections } from "@app/db/schemas/app-connections"; +import { + ChefConnectionMethod, + getChefConnectionListItem, + validateChefConnectionCredentials +} from "@app/ee/services/app-connections/chef"; import { getOCIConnectionListItem, OCIConnectionMethod, @@ -68,7 +73,6 @@ import { } from "./bitbucket"; import { CamundaConnectionMethod, getCamundaConnectionListItem, validateCamundaConnectionCredentials } from "./camunda"; import { ChecklyConnectionMethod, getChecklyConnectionListItem, validateChecklyConnectionCredentials } from "./checkly"; -import { ChefConnectionMethod, getChefConnectionListItem, validateChefConnectionCredentials } from "./chef"; import { CloudflareConnectionMethod } from "./cloudflare/cloudflare-connection-enum"; import { getCloudflareConnectionListItem, @@ -292,7 +296,7 @@ export const decryptAppConnectionCredentials = async ({ cipherTextBlob: encryptedCredentials }); - return JSON.parse(decryptedPlainTextBlob.toString()) as TAppConnection["credentials"]; + return JSON.parse(decryptedPlainTextBlob.toString()); }; export const validateAppConnectionCredentials = async ( diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 08ca0c681..5b8cc3fc1 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -86,6 +86,6 @@ export const APP_CONNECTION_PLAN_MAP: Record = { [SecretSync.Northflank]: SecretSyncPlanType.Regular, [SecretSync.Bitbucket]: SecretSyncPlanType.Regular, [SecretSync.LaravelForge]: SecretSyncPlanType.Regular, - [SecretSync.Chef]: SecretSyncPlanType.Regular + [SecretSync.Chef]: SecretSyncPlanType.Enterprise }; export const SECRET_SYNC_SKIP_FIELDS_MAP: Record = { diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index d6ef0b3fc..f1a87a9d2 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -1,6 +1,12 @@ import { Job } from "bullmq"; import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types"; +import { + TChefSync, + TChefSyncInput, + TChefSyncListItem, + TChefSyncWithCredentials +} from "@app/ee/services/secret-sync/chef"; import { TOCIVaultSync, TOCIVaultSyncInput, @@ -21,7 +27,6 @@ import { TCamundaSyncListItem, TCamundaSyncWithCredentials } from "@app/services/secret-sync/camunda"; -import { TChefSync, TChefSyncInput, TChefSyncListItem, TChefSyncWithCredentials } from "@app/services/secret-sync/chef"; import { TDatabricksSync, TDatabricksSyncInput, diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 1d0d7bec3..b27da2441 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -131,7 +131,7 @@ export const APP_CONNECTION_MAP: Record< image: "Laravel Forge.png", size: 65 }, - [AppConnection.Chef]: { name: "Chef", image: "Chef.png" } + [AppConnection.Chef]: { name: "Chef", image: "Chef.png", enterprise: true } }; export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => { From ebab43716ec2321fca9c76f90e773e35069aa392 Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Mon, 3 Nov 2025 19:27:56 +0530 Subject: [PATCH 013/294] feat: Implement enterprise plan check for Chef connections and update documentation --- .../chef/chef-connection-fns.ts | 2 +- .../chef/chef-connection-service.ts | 22 +++++++++++++++++-- .../chef/chef-connection-types.ts | 2 +- .../secret-sync-routers/secret-sync-router.ts | 2 +- .../app-connection/app-connection-service.ts | 2 +- .../services/secret-sync/secret-sync-fns.ts | 12 +++++++--- docs/integrations/app-connections/chef.mdx | 8 +++++++ docs/integrations/secret-syncs/chef.mdx | 8 +++++++ 8 files changed, 49 insertions(+), 9 deletions(-) diff --git a/backend/src/ee/services/app-connections/chef/chef-connection-fns.ts b/backend/src/ee/services/app-connections/chef/chef-connection-fns.ts index 31f5d0ee0..6cef8373f 100644 --- a/backend/src/ee/services/app-connections/chef/chef-connection-fns.ts +++ b/backend/src/ee/services/app-connections/chef/chef-connection-fns.ts @@ -5,10 +5,10 @@ import { request } from "@app/lib/config/request"; import { BadRequestError } from "@app/lib/errors"; import { removeTrailingSlash } from "@app/lib/fn"; import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; import { TChefDataBagItemContent } from "../../secret-sync/chef/chef-sync-types"; -import { AppConnection } from "../../../../services/app-connection/app-connection-enums"; import { ChefConnectionMethod } from "./chef-connection-enums"; import { TChefConnection, diff --git a/backend/src/ee/services/app-connections/chef/chef-connection-service.ts b/backend/src/ee/services/app-connections/chef/chef-connection-service.ts index 05969bf5d..242b1fbf9 100644 --- a/backend/src/ee/services/app-connections/chef/chef-connection-service.ts +++ b/backend/src/ee/services/app-connections/chef/chef-connection-service.ts @@ -1,7 +1,8 @@ -import { ForbiddenRequestError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; import { AppConnection } from "../../../../services/app-connection/app-connection-enums"; +import { TLicenseServiceFactory } from "../../license/license-service"; import { listChefDataBagItems, listChefDataBags } from "./chef-connection-fns"; import { TChefConnection } from "./chef-connection-types"; @@ -11,8 +12,23 @@ type TGetAppConnectionFunc = ( actor: OrgServiceActor ) => Promise; -export const chefConnectionService = (getAppConnection: TGetAppConnectionFunc) => { +// Enterprise check +export const checkPlan = async (licenseService: Pick, orgId: string) => { + const plan = await licenseService.getPlan(orgId); + if (!plan.enterpriseAppConnections) + throw new BadRequestError({ + message: + "Failed to use app connection due to plan restriction. Upgrade plan to access enterprise app connections." + }); +}; + +export const chefConnectionService = ( + getAppConnection: TGetAppConnectionFunc, + licenseService: Pick +) => { const listDataBags = async (appConnectionId: string, actor: OrgServiceActor) => { + await checkPlan(licenseService, actor.orgId); + const appConnection = await getAppConnection(AppConnection.Chef, appConnectionId, actor); if (!appConnection) { @@ -23,6 +39,8 @@ export const chefConnectionService = (getAppConnection: TGetAppConnectionFunc) = }; const listDataBagItems = async (appConnectionId: string, dataBagName: string, actor: OrgServiceActor) => { + await checkPlan(licenseService, actor.orgId); + const appConnection = await getAppConnection(AppConnection.Chef, appConnectionId, actor); if (!appConnection) { diff --git a/backend/src/ee/services/app-connections/chef/chef-connection-types.ts b/backend/src/ee/services/app-connections/chef/chef-connection-types.ts index 096218951..5673614e9 100644 --- a/backend/src/ee/services/app-connections/chef/chef-connection-types.ts +++ b/backend/src/ee/services/app-connections/chef/chef-connection-types.ts @@ -1,7 +1,7 @@ import z from "zod"; -import { DiscriminativePick } from "@app/lib/types"; import { TChefDataBagItemContent } from "@app/ee/services/secret-sync/chef"; +import { DiscriminativePick } from "@app/lib/types"; import { AppConnection } from "../../../../services/app-connection/app-connection-enums"; import { diff --git a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts index 0f394f8b2..61e4ab79d 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ChefSyncListItemSchema, ChefSyncSchema } from "@app/ee/services/secret-sync/chef"; import { OCIVaultSyncListItemSchema, OCIVaultSyncSchema } from "@app/ee/services/secret-sync/oci-vault"; import { ApiDocsTags, SecretSyncs } from "@app/lib/api-docs"; import { readLimit } from "@app/server/config/rateLimiter"; @@ -24,7 +25,6 @@ import { AzureKeyVaultSyncListItemSchema, AzureKeyVaultSyncSchema } from "@app/s import { BitbucketSyncListItemSchema, BitbucketSyncSchema } from "@app/services/secret-sync/bitbucket"; import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda"; import { ChecklySyncListItemSchema, ChecklySyncSchema } from "@app/services/secret-sync/checkly/checkly-sync-schemas"; -import { ChefSyncListItemSchema, ChefSyncSchema } from "@app/ee/services/secret-sync/chef"; import { CloudflarePagesSyncListItemSchema, CloudflarePagesSyncSchema diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index 6d23d3c0a..99ff253d2 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -885,6 +885,6 @@ export const appConnectionServiceFactory = ({ northflank: northflankConnectionService(connectAppConnectionById), okta: oktaConnectionService(connectAppConnectionById), laravelForge: laravelForgeConnectionService(connectAppConnectionById), - chef: chefConnectionService(connectAppConnectionById) + chef: chefConnectionService(connectAppConnectionById, licenseService) }; }; diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index ca779bf6a..6ee9b91d3 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -290,7 +290,9 @@ export const SecretSyncFns = { case SecretSync.Chef: return ChefSyncFns.syncSecrets(secretSync, schemaSecretMap); default: - throw new Error(`Unhandled sync destination for sync secrets fns: ${secretSync.destination}`); + throw new Error( + `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` + ); } }, getSecrets: async ( @@ -413,7 +415,9 @@ export const SecretSyncFns = { secretMap = await ChefSyncFns.getSecrets(secretSync); break; default: - throw new Error(`Unhandled sync destination for get secrets fns: ${secretSync.destination}`); + throw new Error( + `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` + ); } const filtered = filterForSchema(secretMap, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema); @@ -510,7 +514,9 @@ export const SecretSyncFns = { case SecretSync.Chef: return ChefSyncFns.removeSecrets(secretSync, schemaSecretMap); default: - throw new Error(`Unhandled sync destination for remove secrets fns: ${secretSync.destination}`); + throw new Error( + `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` + ); } } }; diff --git a/docs/integrations/app-connections/chef.mdx b/docs/integrations/app-connections/chef.mdx index 60c49fb1f..fdb866557 100644 --- a/docs/integrations/app-connections/chef.mdx +++ b/docs/integrations/app-connections/chef.mdx @@ -3,6 +3,14 @@ title: "Chef Connection" description: "Learn how to configure a Chef Connection for Infisical." --- + + Chef App Connection is a paid feature. + + If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical, + then you should contact team@infisical.com to purchase an enterprise license to use it. + + + Infisical supports the use of User Private Key to connect with Chef Server. Please access your **starter kit** to get all the required information to create a Chef Connection. diff --git a/docs/integrations/secret-syncs/chef.mdx b/docs/integrations/secret-syncs/chef.mdx index 13f3b3474..423d42b4b 100644 --- a/docs/integrations/secret-syncs/chef.mdx +++ b/docs/integrations/secret-syncs/chef.mdx @@ -3,6 +3,14 @@ title: "Chef Sync" description: "Learn how to configure a Chef Sync for Infisical." --- + + Chef Sync is a paid feature. + + If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical, + then you should contact team@infisical.com to purchase an enterprise license to use it. + + + **Prerequisites:** - Create a [Chef Connection](/integrations/app-connections/chef) From 14e0a0ff6d27f198a72525f10d2acc3e32c8be6a Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Mon, 3 Nov 2025 19:41:20 +0530 Subject: [PATCH 014/294] fix: lint --- backend/src/services/app-connection/app-connection-fns.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 87f22b031..863fa75f9 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -296,7 +296,7 @@ export const decryptAppConnectionCredentials = async ({ cipherTextBlob: encryptedCredentials }); - return JSON.parse(decryptedPlainTextBlob.toString()); + return JSON.parse(decryptedPlainTextBlob.toString()) as TAppConnection["credentials"]; }; export const validateAppConnectionCredentials = async ( From 1ed59a7304af103b1004f28040dafd5abde94d21 Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Mon, 3 Nov 2025 11:12:38 -0300 Subject: [PATCH 015/294] refactor: streamline error handling and notification logic across various components --- .../src/components/mfa/TotpRegistration.tsx | 31 ++--- .../IdentitySection/IdentityTable.tsx | 29 ++-- .../IdentityTlsCertAuthForm.tsx | 67 ++++----- .../IdentitySection/IdentityTokenAuthForm.tsx | 59 ++++---- .../IdentityUniversalAuthForm.tsx | 95 ++++++------- .../OrgMembersSection/AddOrgMemberModal.tsx | 89 ++++++------ .../AddSubOrgMemberModal.tsx | 42 +++--- .../UpgradePrivilegeSystemModal.tsx | 19 +-- .../components/DeleteAppConnectionModal.tsx | 27 ++-- .../EditAppConnectionDetailsModal.tsx | 27 ++-- .../OauthCallbackPage/OauthCallbackPage.tsx | 56 ++------ .../BillingDetailsTab/CompanyNameSection.tsx | 28 ++-- .../BillingDetailsTab/InvoiceEmailSection.tsx | 28 ++-- .../BillingDetailsTab/PmtMethodsTable.tsx | 25 ++-- .../BillingDetailsTab/TaxIDModal.tsx | 30 ++-- .../GroupDetailsByIDPage.tsx | 34 ++--- .../components/AddGroupMemberModal.tsx | 35 ++--- .../GroupMembersSection.tsx | 27 ++-- .../IdentityAddToProjectModal.tsx | 33 ++--- .../IdentityProjectsSection.tsx | 29 ++-- .../components/IdentityTokenModal.tsx | 55 +++----- .../IdentityAuthLockoutFields.tsx | 26 ++-- .../IdentityTokenAuthTokensTable.tsx | 29 ++-- ...dentityUniversalAuthClientSecretsTable.tsx | 26 ++-- .../ViewIdentityAuthModal.tsx | 28 ++-- .../components/GatewayCliDeploymentMethod.tsx | 7 +- .../components/RelayCliDeploymentMethod.tsx | 7 +- .../components/AllProjectView.tsx | 24 ++-- .../ProjectsPage/components/MyProjectView.tsx | 31 ++--- .../RoleByIDPage/RoleByIDPage.tsx | 43 +++--- .../RoleByIDPage/components/RoleModal.tsx | 75 +++++----- .../RequestSecret/RequestSecretForm.tsx | 46 +++---- .../RequestSecret/RequestSecretTab.tsx | 24 ++-- .../OrgSecretShareLimitSection.tsx | 31 ++--- .../SecretSharingAllowShareToAnyone.tsx | 26 ++-- .../components/ShareSecret/ShareSecretTab.tsx | 24 ++-- .../AuditLogStreamForm/AuditLogStreamForm.tsx | 48 ++----- .../components/DeleteAuditLogStreamModal.tsx | 27 ++-- .../components/VaultConnectionSection.tsx | 22 +-- .../components/VaultNamespaceConfigModal.tsx | 46 +++---- .../OrgDeleteSection/OrgDeleteSection.tsx | 28 ++-- .../OrgEncryptionTab/OrgEncryptionTab.tsx | 16 +-- .../AddOrgIncidentContactModal.tsx | 34 ++--- .../OrgIncidentContactsTable.tsx | 30 ++-- .../OrgNameChangeSection.tsx | 30 ++-- .../SubOrgNameChangeSection.tsx | 30 ++-- .../OrgProductSelectSection.tsx | 20 +-- .../OrgProductSettingsTab.tsx | 6 - .../ExternalGroupOrgRoleMappings.tsx | 18 +-- .../components/OrgSsoTab/LDAPModal.tsx | 78 +++++------ .../components/OrgSsoTab/OIDCModal.tsx | 129 ++++++++---------- .../components/OrgSsoTab/SSOModal.tsx | 85 +++++------- .../components/GroupsSection/GroupRoles.tsx | 20 ++- .../ProjectRoleList/ProjectRoleList.tsx | 17 +-- ...rojectAdditionalPrivilegeModifySection.tsx | 47 +++---- ...ntityProjectAdditionalPrivilegeSection.tsx | 19 +-- .../IdentityRoleDetailsSection.tsx | 67 +++++---- ...emberProjectAdditionalPrivilegeSection.tsx | 17 +-- ...rojectAdditionalPrivilegeModifySection.tsx | 43 +++--- .../components/RolePermissionsSection.tsx | 21 ++- .../BackfillSecretReferenceSection.tsx | 8 +- 61 files changed, 843 insertions(+), 1375 deletions(-) diff --git a/frontend/src/components/mfa/TotpRegistration.tsx b/frontend/src/components/mfa/TotpRegistration.tsx index 0b79bfd98..bcb0d3691 100644 --- a/frontend/src/components/mfa/TotpRegistration.tsx +++ b/frontend/src/components/mfa/TotpRegistration.tsx @@ -25,27 +25,20 @@ const TotpRegistration = ({ onComplete, shouldCenterQr }: Props) => { const handleTotpVerify = async (event: React.FormEvent) => { event.preventDefault(); - try { - const result = await verifyUserTotp({ - totp - }); + const result = await verifyUserTotp({ + totp + }); - createNotification({ - text: "Successfully configured mobile authenticator", - type: "success" - }); + createNotification({ + text: "Successfully configured mobile authenticator", + type: "success" + }); - if (result.recoveryCodes && result.recoveryCodes.length > 0) { - setRecoveryCodes(result.recoveryCodes); - setShowRecoveryModal(true); - } else if (onComplete) { - onComplete(); - } - } catch { - createNotification({ - text: "Failed to verify TOTP code", - type: "error" - }); + if (result.recoveryCodes && result.recoveryCodes.length > 0) { + setRecoveryCodes(result.recoveryCodes); + setShowRecoveryModal(true); + } else if (onComplete) { + onComplete(); } }; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx index c4c1561e7..d202b909a 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx @@ -145,27 +145,16 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { }; const handleChangeRole = async ({ identityId, role }: { identityId: string; role: string }) => { - try { - await updateMutateAsync({ - identityId, - role, - organizationId - }); + await updateMutateAsync({ + identityId, + role, + organizationId + }); - createNotification({ - text: "Successfully updated identity role", - type: "success" - }); - } catch (err) { - console.error(err); - const error = err as any; - const text = error?.response?.data?.message ?? "Failed to update identity role"; - - createNotification({ - text, - type: "error" - }); - } + createNotification({ + text: "Successfully updated identity role", + type: "success" + }); }; const handleRoleToggle = useCallback( 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 index 19a0499fc..2a2ca45c3 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTlsCertAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTlsCertAuthForm.tsx @@ -134,47 +134,40 @@ export const IdentityTlsCertAuthForm = ({ accessTokenNumUsesLimit, accessTokenTrustedIps }: FormData) => { - try { - if (!identityId) return; + 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" + if (data) { + await updateMutateAsync({ + organizationId: orgId, + caCertificate, + allowedCommonNames: allowedCommonNames || null, + identityId, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps }); - - reset(); - } catch { - createNotification({ - text: `Failed to ${isUpdate ? "update" : "configure"} identity`, - type: "error" + } 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(); }; return ( diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthForm.tsx index b819202fc..8da4bad17 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthForm.tsx @@ -127,43 +127,36 @@ export const IdentityTokenAuthForm = ({ accessTokenNumUsesLimit, accessTokenTrustedIps }: FormData) => { - try { - if (!identityId) return; + if (!identityId) return; - if (data) { - await updateMutateAsync({ - organizationId: orgId, - identityId, - accessTokenTTL: Number(accessTokenTTL), - accessTokenMaxTTL: Number(accessTokenMaxTTL), - accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), - accessTokenTrustedIps - }); - } else { - await addMutateAsync({ - organizationId: orgId, - identityId, - accessTokenTTL: Number(accessTokenTTL), - accessTokenMaxTTL: Number(accessTokenMaxTTL), - accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), - accessTokenTrustedIps - }); - } - - handlePopUpToggle("identityAuthMethod", false); - - createNotification({ - text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`, - type: "success" + if (data) { + await updateMutateAsync({ + organizationId: orgId, + identityId, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps }); - - reset(); - } catch { - createNotification({ - text: `Failed to ${isUpdate ? "update" : "configure"} identity`, - type: "error" + } else { + await addMutateAsync({ + organizationId: orgId, + identityId, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps }); } + + handlePopUpToggle("identityAuthMethod", false); + + createNotification({ + text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`, + type: "success" + }); + + reset(); }; return ( diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx index d43d84c86..dd85f5342 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx @@ -220,64 +220,55 @@ export const IdentityUniversalAuthForm = ({ lockoutCounterResetValue, lockoutCounterResetUnit }: FormData) => { - try { - if (!identityId) return; + if (!identityId) return; - const lockoutDurationSeconds = ms(`${lockoutDurationValue}${lockoutDurationUnit}`) / 1000; - const lockoutCounterResetSeconds = - ms(`${lockoutCounterResetValue}${lockoutCounterResetUnit}`) / 1000; + const lockoutDurationSeconds = ms(`${lockoutDurationValue}${lockoutDurationUnit}`) / 1000; + const lockoutCounterResetSeconds = + ms(`${lockoutCounterResetValue}${lockoutCounterResetUnit}`) / 1000; - if (data) { - // update universal auth configuration - await updateMutateAsync({ - organizationId: orgId, - identityId, - clientSecretTrustedIps, - accessTokenTTL: Number(accessTokenTTL), - accessTokenMaxTTL: Number(accessTokenMaxTTL), - accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), - accessTokenTrustedIps, - accessTokenPeriod: Number(accessTokenPeriod), - lockoutEnabled, - lockoutThreshold: Number(lockoutThreshold), - lockoutDurationSeconds, - lockoutCounterResetSeconds - }); - } else { - // create new universal auth configuration - - await addMutateAsync({ - organizationId: orgId, - identityId, - clientSecretTrustedIps, - accessTokenTTL: Number(accessTokenTTL), - accessTokenMaxTTL: Number(accessTokenMaxTTL), - accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), - accessTokenTrustedIps, - accessTokenPeriod: Number(accessTokenPeriod), - lockoutEnabled, - lockoutThreshold: Number(lockoutThreshold), - lockoutDurationSeconds: Number(lockoutDurationSeconds), - lockoutCounterResetSeconds: Number(lockoutCounterResetSeconds) - }); - } - - handlePopUpToggle("identityAuthMethod", false); - - createNotification({ - text: `Successfully ${isUpdate ? "updated" : "created"} auth method`, - type: "success" + if (data) { + // update universal auth configuration + await updateMutateAsync({ + organizationId: orgId, + identityId, + clientSecretTrustedIps, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps, + accessTokenPeriod: Number(accessTokenPeriod), + lockoutEnabled, + lockoutThreshold: Number(lockoutThreshold), + lockoutDurationSeconds, + lockoutCounterResetSeconds }); + } else { + // create new universal auth configuration - reset(); - } catch { - const text = `Failed to ${isUpdate ? "update" : "configure"} identity`; - - createNotification({ - text, - type: "error" + await addMutateAsync({ + organizationId: orgId, + identityId, + clientSecretTrustedIps, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps, + accessTokenPeriod: Number(accessTokenPeriod), + lockoutEnabled, + lockoutThreshold: Number(lockoutThreshold), + lockoutDurationSeconds: Number(lockoutDurationSeconds), + lockoutCounterResetSeconds: Number(lockoutCounterResetSeconds) }); } + + handlePopUpToggle("identityAuthMethod", false); + + createNotification({ + text: `Successfully ${isUpdate ? "updated" : "created"} auth method`, + type: "success" + }); + + reset(); }; return ( diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx index e49beadae..da88603fc 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx @@ -122,64 +122,55 @@ export const AddOrgMemberModal = ({ } } - try { - const parsedEmails = emails - .replace(/\s/g, "") - .split(",") - .map((email) => { - if (EmailSchema.safeParse(email).success) { - return email.trim(); - } + const parsedEmails = emails + .replace(/\s/g, "") + .split(",") + .map((email) => { + if (EmailSchema.safeParse(email).success) { + return email.trim(); + } - return null; - }); - - if (parsedEmails.includes(null)) { - createNotification({ - text: "Invalid email addresses provided.", - type: "error" - }); - return; - } - - const usernames = emails.split(",").map((email) => email.trim()); - const { data } = await addUsersMutateAsync({ - organizationId: currentOrg?.id, - inviteeEmails: usernames, - organizationRoleSlug: organizationRole.slug + return null; }); - await Promise.allSettled( - selectedProjects.map((el) => - addUserToProject({ - orgId: currentOrg.id, - projectId: el.id, - roleSlugs: [projectRoleSlug], - usernames - }) - ) - ); - - setCompleteInviteLinks(data?.completeInviteLinks ?? null); - - // only show this notification when email is configured. - // A [completeInviteLink] will not be sent if smtp is configured - - if (!data.completeInviteLinks) { - createNotification({ - text: "Successfully invited user to the organization.", - type: "success" - }); - } - } catch (error) { - console.error(error); + if (parsedEmails.includes(null)) { createNotification({ - text: "Failed to invite user to org", + text: "Invalid email addresses provided.", type: "error" }); return; } + const usernames = emails.split(",").map((email) => email.trim()); + const { data } = await addUsersMutateAsync({ + organizationId: currentOrg?.id, + inviteeEmails: usernames, + organizationRoleSlug: organizationRole.slug + }); + + await Promise.allSettled( + selectedProjects.map((el) => + addUserToProject({ + orgId: currentOrg.id, + projectId: el.id, + roleSlugs: [projectRoleSlug], + usernames + }) + ) + ); + + setCompleteInviteLinks(data?.completeInviteLinks ?? null); + + // only show this notification when email is configured. + // A [completeInviteLink] will not be sent if smtp is configured + + if (!data.completeInviteLinks) { + createNotification({ + text: "Successfully invited user to the organization.", + type: "success" + }); + } + if (serverDetails?.emailConfigured) { handlePopUpToggle("addMember", false); } diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddSubOrgMemberModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddSubOrgMemberModal.tsx index 290f8db4b..5edba7cde 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddSubOrgMemberModal.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddSubOrgMemberModal.tsx @@ -106,32 +106,24 @@ export const AddSubOrgMemberModal = ({ onClose }: Props) => { } } - try { - const usernames = users.map((el) => el.username); - await addUsersMutateAsync({ - organizationId: currentOrg?.id, - inviteeEmails: usernames, - organizationRoleSlug: organizationRole.slug - }); + const usernames = users.map((el) => el.username); + await addUsersMutateAsync({ + organizationId: currentOrg?.id, + inviteeEmails: usernames, + organizationRoleSlug: organizationRole.slug + }); - await Promise.allSettled( - selectedProjects.map((el) => - addUserToProject({ - orgId: currentOrg.id, - projectId: el.id, - roleSlugs: [projectRoleSlug], - usernames - }) - ) - ); - onClose(); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to add user to suborganization", - type: "error" - }); - } + await Promise.allSettled( + selectedProjects.map((el) => + addUserToProject({ + orgId: currentOrg.id, + projectId: el.id, + roleSlugs: [projectRoleSlug], + usernames + }) + ) + ); + onClose(); }; const getGroupHeaderLabel = (type: ProjectType) => { diff --git a/frontend/src/pages/organization/AccessManagementPage/components/UpgradePrivilegeSystemModal/UpgradePrivilegeSystemModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/UpgradePrivilegeSystemModal/UpgradePrivilegeSystemModal.tsx index 97aaff73d..c2a200805 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/UpgradePrivilegeSystemModal/UpgradePrivilegeSystemModal.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/UpgradePrivilegeSystemModal/UpgradePrivilegeSystemModal.tsx @@ -44,21 +44,14 @@ export const UpgradePrivilegeSystemModal = ({ isOpen, onOpenChange }: Props) => acknowledgesPermanentChange; const handlePrivilegeSystemUpgrade = async () => { - try { - await upgradePrivilegeSystem(); + await upgradePrivilegeSystem(); - createNotification({ - text: "Privilege system upgrade completed", - type: "success" - }); + createNotification({ + text: "Privilege system upgrade completed", + type: "success" + }); - onOpenChange(false); - } catch { - createNotification({ - text: "Failed to upgrade privilege system", - type: "error" - }); - } + onOpenChange(false); }; const handleClose = () => { diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/DeleteAppConnectionModal.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/DeleteAppConnectionModal.tsx index e5f6b3684..f5782adbb 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/DeleteAppConnectionModal.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/DeleteAppConnectionModal.tsx @@ -18,26 +18,17 @@ export const DeleteAppConnectionModal = ({ isOpen, onOpenChange, appConnection } const { id: connectionId, name, app } = appConnection; const handleDeleteAppConnection = async () => { - try { - await deleteAppConnection.mutateAsync({ - connectionId, - app - }); + await deleteAppConnection.mutateAsync({ + connectionId, + app + }); - createNotification({ - text: `Successfully removed ${APP_CONNECTION_MAP[app].name} connection`, - type: "success" - }); + createNotification({ + text: `Successfully removed ${APP_CONNECTION_MAP[app].name} connection`, + type: "success" + }); - onOpenChange(false); - } catch (err) { - console.error(err); - - createNotification({ - text: `Failed to remove ${APP_CONNECTION_MAP[app].name} connection`, - type: "error" - }); - } + onOpenChange(false); }; return ( diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/EditAppConnectionDetailsModal.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/EditAppConnectionDetailsModal.tsx index f46a3b3cf..7d1a542c0 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/EditAppConnectionDetailsModal.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/EditAppConnectionDetailsModal.tsx @@ -43,24 +43,15 @@ const Content = ({ appConnection, onComplete }: ContentProps) => { } = form; const onSubmit = async (formData: FormData) => { - try { - await updateAppConnection.mutateAsync({ - connectionId: appConnection.id, - ...formData - }); - createNotification({ - text: `Successfully updated ${appName} Connection`, - type: "success" - }); - onComplete(); - } catch (err: any) { - console.error(err); - createNotification({ - title: `Failed to update ${appName} Connection`, - text: err.message, - type: "error" - }); - } + await updateAppConnection.mutateAsync({ + connectionId: appConnection.id, + ...formData + }); + createNotification({ + text: `Successfully updated ${appName} Connection`, + type: "success" + }); + onComplete(); }; return ( diff --git a/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx b/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx index df3d1327a..96a474714 100644 --- a/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx +++ b/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx @@ -127,12 +127,7 @@ export const OAuthCallbackPage = () => { projectId, connection }; - } catch (err: any) { - createNotification({ - title: `Failed to ${connectionId ? "update" : "add"} GitLab Connection`, - text: err?.message, - type: "error" - }); + } catch { navigate({ to: returnUrl, params: { @@ -176,12 +171,7 @@ export const OAuthCallbackPage = () => { } }); } - } catch (err: any) { - createNotification({ - title: `Failed to ${connectionId ? "update" : "add"} Azure Key Vault Connection`, - text: err?.message, - type: "error" - }); + } catch { navigate({ to: returnUrl, params: { @@ -233,12 +223,7 @@ export const OAuthCallbackPage = () => { } }); } - } catch (err: any) { - createNotification({ - title: `Failed to ${connectionId ? "update" : "add"} Azure App Configuration Connection`, - text: err?.message, - type: "error" - }); + } catch { navigate({ to: returnUrl, params: { @@ -290,12 +275,7 @@ export const OAuthCallbackPage = () => { } }); } - } catch (err: any) { - createNotification({ - title: `Failed to ${connectionId ? "update" : "add"} Azure Client Secrets Connection`, - text: err?.message, - type: "error" - }); + } catch { navigate({ to: returnUrl, params: { @@ -353,12 +333,7 @@ export const OAuthCallbackPage = () => { } }); } - } catch (err: any) { - createNotification({ - title: `Failed to ${connectionId ? "update" : "add"} Azure DevOps Connection`, - text: err?.message, - type: "error" - }); + } catch { navigate({ to: returnUrl, params: { @@ -441,12 +416,7 @@ export const OAuthCallbackPage = () => { }) }); } - } catch (e: any) { - createNotification({ - title: `Failed to ${connectionId ? "update" : "add"} GitHub Connection`, - text: e.message, - type: "error" - }); + } catch { navigate({ to: returnUrl, params: { @@ -498,12 +468,7 @@ export const OAuthCallbackPage = () => { } }); } - } catch (e: any) { - createNotification({ - title: `Failed to ${connectionId ? "update" : "add"} GitHub Radar Connection`, - text: e.message, - type: "error" - }); + } catch { navigate({ to: returnUrl, params: { @@ -553,12 +518,7 @@ export const OAuthCallbackPage = () => { } }); } - } catch (e: any) { - createNotification({ - title: `Failed to ${connectionId ? "update" : "add"} Heroku Connection`, - text: e.message, - type: "error" - }); + } catch { navigate({ to: returnUrl, params: { diff --git a/frontend/src/pages/organization/BillingPage/components/BillingDetailsTab/CompanyNameSection.tsx b/frontend/src/pages/organization/BillingPage/components/BillingDetailsTab/CompanyNameSection.tsx index fce3978c7..f63b5d6d1 100644 --- a/frontend/src/pages/organization/BillingPage/components/BillingDetailsTab/CompanyNameSection.tsx +++ b/frontend/src/pages/organization/BillingPage/components/BillingDetailsTab/CompanyNameSection.tsx @@ -35,25 +35,17 @@ export const CompanyNameSection = () => { }, [data]); const onFormSubmit = async ({ name }: { name: string }) => { - try { - if (!currentOrg?.id) return; - if (name === "") return; - await mutateAsync({ - name, - organizationId: currentOrg.id - }); + if (!currentOrg?.id) return; + if (name === "") return; + await mutateAsync({ + name, + organizationId: currentOrg.id + }); - createNotification({ - text: "Successfully updated business name", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to update business name", - type: "error" - }); - } + createNotification({ + text: "Successfully updated business name", + type: "success" + }); }; return ( diff --git a/frontend/src/pages/organization/BillingPage/components/BillingDetailsTab/InvoiceEmailSection.tsx b/frontend/src/pages/organization/BillingPage/components/BillingDetailsTab/InvoiceEmailSection.tsx index 8b471346f..dc05a7208 100644 --- a/frontend/src/pages/organization/BillingPage/components/BillingDetailsTab/InvoiceEmailSection.tsx +++ b/frontend/src/pages/organization/BillingPage/components/BillingDetailsTab/InvoiceEmailSection.tsx @@ -35,26 +35,18 @@ export const InvoiceEmailSection = () => { }, [data]); const onFormSubmit = async ({ email }: { email: string }) => { - try { - if (!currentOrg?.id) return; - if (email === "") return; + if (!currentOrg?.id) return; + if (email === "") return; - await mutateAsync({ - email, - organizationId: currentOrg.id - }); + await mutateAsync({ + email, + organizationId: currentOrg.id + }); - createNotification({ - text: "Successfully updated invoice email recipient", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to update invoice email recipient", - type: "error" - }); - } + createNotification({ + text: "Successfully updated invoice email recipient", + type: "success" + }); }; return ( diff --git a/frontend/src/pages/organization/BillingPage/components/BillingDetailsTab/PmtMethodsTable.tsx b/frontend/src/pages/organization/BillingPage/components/BillingDetailsTab/PmtMethodsTable.tsx index bf0851e11..15a6bc8e9 100644 --- a/frontend/src/pages/organization/BillingPage/components/BillingDetailsTab/PmtMethodsTable.tsx +++ b/frontend/src/pages/organization/BillingPage/components/BillingDetailsTab/PmtMethodsTable.tsx @@ -39,22 +39,15 @@ export const PmtMethodsTable = () => { }); return; } - try { - await deleteOrgPmtMethod.mutateAsync({ - organizationId: currentOrg.id, - pmtMethodId: pmtMethodToRemove.id - }); - createNotification({ - type: "success", - text: "Successfully removed payment method" - }); - handlePopUpClose("removeCard"); - } catch (error: any) { - createNotification({ - type: "error", - text: error.message ?? "Error removing payment method" - }); - } + await deleteOrgPmtMethod.mutateAsync({ + organizationId: currentOrg.id, + pmtMethodId: pmtMethodToRemove.id + }); + createNotification({ + type: "success", + text: "Successfully removed payment method" + }); + handlePopUpClose("removeCard"); }; return ( diff --git a/frontend/src/pages/organization/BillingPage/components/BillingDetailsTab/TaxIDModal.tsx b/frontend/src/pages/organization/BillingPage/components/BillingDetailsTab/TaxIDModal.tsx index 01b11e81a..23542ac8d 100644 --- a/frontend/src/pages/organization/BillingPage/components/BillingDetailsTab/TaxIDModal.tsx +++ b/frontend/src/pages/organization/BillingPage/components/BillingDetailsTab/TaxIDModal.tsx @@ -98,26 +98,18 @@ export const TaxIDModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props }); const onTaxIDModalSubmit = async ({ type, value }: AddTaxIDFormData) => { - try { - if (!currentOrg?.id) return; - await addOrgTaxId.mutateAsync({ - organizationId: currentOrg.id, - type, - value - }); + if (!currentOrg?.id) return; + await addOrgTaxId.mutateAsync({ + organizationId: currentOrg.id, + type, + value + }); - createNotification({ - text: "Successfully added Tax ID", - type: "success" - }); - handlePopUpClose("addTaxID"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to add Tax ID", - type: "error" - }); - } + createNotification({ + text: "Successfully added Tax ID", + type: "success" + }); + handlePopUpClose("addTaxID"); }; return ( diff --git a/frontend/src/pages/organization/GroupDetailsByIDPage/GroupDetailsByIDPage.tsx b/frontend/src/pages/organization/GroupDetailsByIDPage/GroupDetailsByIDPage.tsx index b33ce0ef2..57c7b5a7d 100644 --- a/frontend/src/pages/organization/GroupDetailsByIDPage/GroupDetailsByIDPage.tsx +++ b/frontend/src/pages/organization/GroupDetailsByIDPage/GroupDetailsByIDPage.tsx @@ -56,27 +56,19 @@ const Page = () => { ] as const); const onDeleteGroupSubmit = async ({ name, id }: { name: string; id: string }) => { - try { - await deleteMutateAsync({ - id - }); - createNotification({ - text: `Successfully deleted the ${name} group`, - type: "success" - }); - navigate({ - to: "/organization/access-management" as const, - search: { - selectedTab: TabSections.Groups - } - }); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to delete the ${name} group`, - type: "error" - }); - } + await deleteMutateAsync({ + id + }); + createNotification({ + text: `Successfully deleted the ${name} group`, + type: "success" + }); + navigate({ + to: "/organization/access-management" as const, + search: { + selectedTab: TabSections.Groups + } + }); handlePopUpClose("deleteGroup"); }; diff --git a/frontend/src/pages/organization/GroupDetailsByIDPage/components/AddGroupMemberModal.tsx b/frontend/src/pages/organization/GroupDetailsByIDPage/components/AddGroupMemberModal.tsx index 0995830e5..b9d1b184c 100644 --- a/frontend/src/pages/organization/GroupDetailsByIDPage/components/AddGroupMemberModal.tsx +++ b/frontend/src/pages/organization/GroupDetailsByIDPage/components/AddGroupMemberModal.tsx @@ -63,31 +63,24 @@ export const AddGroupMembersModal = ({ popUp, handlePopUpToggle }: Props) => { const { mutateAsync: addUserToGroupMutateAsync } = useAddUserToGroup(); const handleAddMember = async (username: string) => { - try { - if (!popUpData?.slug) { - createNotification({ - text: "Some data is missing, please refresh the page and try again", - type: "error" - }); - return; - } - - await addUserToGroupMutateAsync({ - groupId: popUpData.groupId, - username, - slug: popUpData.slug - }); - + if (!popUpData?.slug) { createNotification({ - text: "Successfully assigned user to the group", - type: "success" - }); - } catch { - createNotification({ - text: "Failed to assign user to the group", + text: "Some data is missing, please refresh the page and try again", type: "error" }); + return; } + + await addUserToGroupMutateAsync({ + groupId: popUpData.groupId, + username, + slug: popUpData.slug + }); + + createNotification({ + text: "Successfully assigned user to the group", + type: "success" + }); }; return ( diff --git a/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersSection.tsx b/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersSection.tsx index 2ca6859b0..c78b5404e 100644 --- a/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersSection.tsx +++ b/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersSection.tsx @@ -29,25 +29,18 @@ export const GroupMembersSection = ({ groupId, groupSlug }: Props) => { const { mutateAsync: removeUserFromGroupMutateAsync } = useRemoveUserFromGroup(); const handleRemoveUserFromGroup = async (username: string) => { - try { - await removeUserFromGroupMutateAsync({ - groupId, - username, - slug: groupSlug - }); + await removeUserFromGroupMutateAsync({ + groupId, + username, + slug: groupSlug + }); - createNotification({ - text: `Successfully removed user ${username} from the group`, - type: "success" - }); + createNotification({ + text: `Successfully removed user ${username} from the group`, + type: "success" + }); - handlePopUpToggle("removeMemberFromGroup", false); - } catch { - createNotification({ - text: `Failed to remove user ${username} from the group`, - type: "error" - }); - } + handlePopUpToggle("removeMemberFromGroup", false); }; return ( diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityAddToProjectModal.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityAddToProjectModal.tsx index 2e5c7101c..2f407142a 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityAddToProjectModal.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityAddToProjectModal.tsx @@ -75,30 +75,19 @@ const Content = ({ identityId, handlePopUpToggle }: Omit) => { }, [workspaces, projectMemberships]); const onFormSubmit = async ({ project: selectedProject, role }: FormData) => { - try { - await addIdentityToWorkspace({ - projectId: selectedProject.id, - identityId, - role: role.slug || undefined - }); + await addIdentityToWorkspace({ + projectId: selectedProject.id, + identityId, + role: role.slug || undefined + }); - createNotification({ - text: "Successfully added identity to project", - type: "success" - }); + createNotification({ + text: "Successfully added identity to project", + type: "success" + }); - reset(); - handlePopUpToggle("addIdentityToProject", false); - } catch (err) { - console.error(err); - const error = err as any; - const text = error?.response?.data?.message ?? "Failed to add identity to project"; - - createNotification({ - text, - type: "error" - }); - } + reset(); + handlePopUpToggle("addIdentityToProject", false); }; const isProjectSelected = Boolean(projectId); diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectsSection.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectsSection.tsx index 6530b9ae5..8d67ef6a4 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectsSection.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectsSection.tsx @@ -22,28 +22,17 @@ export const IdentityProjectsSection = ({ identityId }: Props) => { ] as const); const onRemoveIdentitySubmit = async (id: string, projectId: string) => { - try { - await deleteMutateAsync({ - identityId: id, - projectId - }); + await deleteMutateAsync({ + identityId: id, + projectId + }); - createNotification({ - text: "Successfully removed identity from project", - type: "success" - }); + createNotification({ + text: "Successfully removed identity from project", + type: "success" + }); - handlePopUpClose("removeIdentityFromProject"); - } catch (err) { - console.error(err); - const error = err as any; - const text = error?.response?.data?.message ?? "Failed to remove identity from project"; - - createNotification({ - text, - type: "error" - }); - } + handlePopUpClose("removeIdentityFromProject"); }; return ( diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityTokenModal.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityTokenModal.tsx index d2b865a5a..d5c1aa2ac 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityTokenModal.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityTokenModal.tsx @@ -72,46 +72,33 @@ export const IdentityTokenModal = ({ popUp, handlePopUpToggle }: Props) => { }, [popUp?.token?.data]); const onFormSubmit = async ({ name }: FormData) => { - try { - if (tokenData?.tokenId) { - // update + if (tokenData?.tokenId) { + // update - await updateToken({ - identityId: tokenData.identityId, - tokenId: tokenData.tokenId, - name - }); - - handlePopUpToggle("token", false); - } else { - // create - - const newTokenData = await createToken({ - identityId: tokenData.identityId, - name - }); - - setToken(newTokenData.accessToken); - } - - createNotification({ - text: `Successfully ${popUp?.token?.data ? "updated" : "created"} token`, - type: "success" + await updateToken({ + identityId: tokenData.identityId, + tokenId: tokenData.tokenId, + name }); - reset(); - } catch (err) { - console.error(err); - const error = err as any; - const text = - error?.response?.data?.message ?? - `Failed to ${popUp?.token?.data ? "update" : "create"} token`; + handlePopUpToggle("token", false); + } else { + // create - createNotification({ - text, - type: "error" + const newTokenData = await createToken({ + identityId: tokenData.identityId, + name }); + + setToken(newTokenData.accessToken); } + + createNotification({ + text: `Successfully ${popUp?.token?.data ? "updated" : "created"} token`, + type: "success" + }); + + reset(); }; return ( diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityAuthLockoutFields.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityAuthLockoutFields.tsx index b5cb2d901..4ec5871a2 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityAuthLockoutFields.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityAuthLockoutFields.tsx @@ -31,23 +31,15 @@ export const LockoutFields = ({ const [lockedOutState, setLockedOutState] = useState(lockedOut); - async function clearLockouts() { - try { - const deleted = await mutateAsync({ identityId }); - createNotification({ - text: `Successfully cleared ${deleted} lockout${deleted === 1 ? "" : "s"}`, - type: "success" - }); - setLockedOutState(false); - onResetAllLockouts(); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to clear lockouts. Please try again.", - type: "error" - }); - } - } + const clearLockouts = async () => { + const deleted = await mutateAsync({ identityId }); + createNotification({ + text: `Successfully cleared ${deleted} lockout${deleted === 1 ? "" : "s"}`, + type: "success" + }); + setLockedOutState(false); + onResetAllLockouts(); + }; return ( <> diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityTokenAuthTokensTable.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityTokenAuthTokensTable.tsx index be9c166b3..cc11d9d10 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityTokenAuthTokensTable.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityTokenAuthTokensTable.tsx @@ -51,28 +51,17 @@ export const IdentityTokenAuthTokensTable = ({ tokens, identityId }: Props) => { tokenId: string; name: string; }) => { - try { - await revokeToken({ - identityId: parentIdentityId, - tokenId - }); + await revokeToken({ + identityId: parentIdentityId, + tokenId + }); - handlePopUpClose("revokeToken"); + handlePopUpClose("revokeToken"); - createNotification({ - text: `Successfully revoked token ${name ?? ""}`, - type: "success" - }); - } catch (err) { - console.error(err); - const error = err as any; - const text = error?.response?.data?.message ?? "Failed to revoke token"; - - createNotification({ - text, - type: "error" - }); - } + createNotification({ + text: `Successfully revoked token ${name ?? ""}`, + type: "success" + }); }; return ( diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityUniversalAuthClientSecretsTable.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityUniversalAuthClientSecretsTable.tsx index b034b5367..017f30719 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityUniversalAuthClientSecretsTable.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityUniversalAuthClientSecretsTable.tsx @@ -43,25 +43,17 @@ export const IdentityUniversalAuthClientSecretsTable = ({ clientSecrets, identit const { mutateAsync: revokeClientSecret } = useRevokeIdentityUniversalAuthClientSecret(); const onDeleteClientSecretSubmit = async (clientSecretId: string) => { - try { - await revokeClientSecret({ - identityId, - clientSecretId - }); + await revokeClientSecret({ + identityId, + clientSecretId + }); - handlePopUpToggle("revokeClientSecret", false); + handlePopUpToggle("revokeClientSecret", false); - createNotification({ - text: "Successfully deleted client secret", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete client secret", - type: "error" - }); - } + createNotification({ + text: "Successfully deleted client secret", + type: "success" + }); }; return ( diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx index f95a20789..09fc9f67e 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx @@ -140,25 +140,17 @@ export const Content = ({ } const handleDeleteAuthMethod = async () => { - try { - await revokeMethod({ - identityId, - organizationId: orgId - }); + await revokeMethod({ + identityId, + organizationId: orgId + }); - createNotification({ - text: "Successfully removed auth method", - type: "success" - }); - - handlePopUpToggle("revokeAuthMethod", false); - onDeleteAuthMethod(); - } catch { - createNotification({ - text: "Failed to remove auth method", - type: "error" - }); - } + createNotification({ + text: "Successfully removed auth method", + type: "success" + }); + handlePopUpToggle("revokeAuthMethod", false); + onDeleteAuthMethod(); }; return ( diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliDeploymentMethod.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliDeploymentMethod.tsx index 7537d95a3..8e69e7c0a 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliDeploymentMethod.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliDeploymentMethod.tsx @@ -168,12 +168,7 @@ export const GatewayCliDeploymentMethod = () => { type: "info" }); setStep("command"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to generate token for the selected identity", - type: "error" - }); + } catch { setIdentityToken(""); } } else { diff --git a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayCliDeploymentMethod.tsx b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayCliDeploymentMethod.tsx index 1d89e9877..f41f8204b 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayCliDeploymentMethod.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayCliDeploymentMethod.tsx @@ -141,12 +141,7 @@ export const RelayCliDeploymentMethod = () => { type: "info" }); setStep("command"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to generate token for the selected identity", - type: "error" - }); + } catch { setIdentityToken(""); } } else { diff --git a/frontend/src/pages/organization/ProjectsPage/components/AllProjectView.tsx b/frontend/src/pages/organization/ProjectsPage/components/AllProjectView.tsx index cf60ffa59..a1bd8c9e4 100644 --- a/frontend/src/pages/organization/ProjectsPage/components/AllProjectView.tsx +++ b/frontend/src/pages/organization/ProjectsPage/components/AllProjectView.tsx @@ -13,7 +13,6 @@ import { useNavigate } from "@tanstack/react-router"; import { CheckIcon } from "lucide-react"; import { twMerge } from "tailwind-merge"; -import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; import { RequestProjectAccessModal } from "@app/components/projects/RequestProjectAccessModal"; import { @@ -103,22 +102,15 @@ export const AllProjectView = ({ projectId: string, environments: ProjectEnv[] ) => { - try { - await orgAdminAccessProject.mutateAsync({ + await orgAdminAccessProject.mutateAsync({ + projectId + }); + await navigate({ + to: getProjectHomePage(type, environments), + params: { projectId - }); - await navigate({ - to: getProjectHomePage(type, environments), - params: { - projectId - } - }); - } catch { - createNotification({ - text: "Failed to access project", - type: "error" - }); - } + } + }); }; useResetPageHelper({ diff --git a/frontend/src/pages/organization/ProjectsPage/components/MyProjectView.tsx b/frontend/src/pages/organization/ProjectsPage/components/MyProjectView.tsx index a86023b6c..32301923b 100644 --- a/frontend/src/pages/organization/ProjectsPage/components/MyProjectView.tsx +++ b/frontend/src/pages/organization/ProjectsPage/components/MyProjectView.tsx @@ -15,7 +15,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNavigate } from "@tanstack/react-router"; import { twMerge } from "tailwind-merge"; -import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; import { Button, @@ -158,32 +157,18 @@ export const MyProjectView = ({ }; const addProjectToFavorites = async (projectId: string) => { - try { - if (currentOrg?.id) { - await updateUserProjectFavorites({ - orgId: currentOrg?.id, - projectFavorites: [...(projectFavorites || []), projectId] - }); - } - } catch { - createNotification({ - text: "Failed to add project to favorites.", - type: "error" + if (currentOrg?.id) { + await updateUserProjectFavorites({ + orgId: currentOrg?.id, + projectFavorites: [...(projectFavorites || []), projectId] }); } }; const removeProjectFromFavorites = async (projectId: string) => { - try { - if (currentOrg?.id) { - await updateUserProjectFavorites({ - orgId: currentOrg?.id, - projectFavorites: [...(projectFavorites || []).filter((entry) => entry !== projectId)] - }); - } - } catch { - createNotification({ - text: "Failed to remove project from favorites.", - type: "error" + if (currentOrg?.id) { + await updateUserProjectFavorites({ + orgId: currentOrg?.id, + projectFavorites: [...(projectFavorites || []).filter((entry) => entry !== projectId)] }); } }; diff --git a/frontend/src/pages/organization/RoleByIDPage/RoleByIDPage.tsx b/frontend/src/pages/organization/RoleByIDPage/RoleByIDPage.tsx index 0d30dd2f8..78c5393a7 100644 --- a/frontend/src/pages/organization/RoleByIDPage/RoleByIDPage.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/RoleByIDPage.tsx @@ -42,36 +42,25 @@ export const Page = () => { ] as const); const onDeleteOrgRoleSubmit = async () => { - try { - if (!orgId || !roleId) return; + if (!orgId || !roleId) return; - await deleteOrgRole({ - orgId, - id: roleId - }); + await deleteOrgRole({ + orgId, + id: roleId + }); - createNotification({ - text: "Successfully deleted organization role", - type: "success" - }); + createNotification({ + text: "Successfully deleted organization role", + type: "success" + }); - handlePopUpClose("deleteOrgRole"); - navigate({ - to: "/organization/access-management" as const, - search: { - selectedTab: OrgAccessControlTabSections.Roles - } - }); - } catch (err) { - console.error(err); - const error = err as any; - const text = error?.response?.data?.message ?? "Failed to delete organization role"; - - createNotification({ - text, - type: "error" - }); - } + handlePopUpClose("deleteOrgRole"); + navigate({ + to: "/organization/access-management" as const, + search: { + selectedTab: OrgAccessControlTabSections.Roles + } + }); }; const isCustomRole = !["admin", "member", "no-access"].includes(data?.slug ?? ""); diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RoleModal.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RoleModal.tsx index 88f568c4a..73da4f161 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/RoleModal.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/components/RoleModal.tsx @@ -70,55 +70,46 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => { }, [role]); const onFormSubmit = async ({ name, description, slug }: FormData) => { - try { - if (!orgId) return; + if (!orgId) return; - if (role) { - // update + if (role) { + // update - await updateOrgRole({ - orgId, - id: role.id, - name, - description, - slug - }); - - handlePopUpToggle("role", false); - } else { - // create - - const newRole = await createOrgRole({ - orgId, - name, - description, - slug, - permissions: [] - }); - - handlePopUpToggle("role", false); - navigate({ - to: "/organization/roles/$roleId", - params: { - roleId: newRole.id - } - }); - } - - createNotification({ - text: `Successfully ${popUp?.role?.data ? "updated" : "created"} role`, - type: "success" + await updateOrgRole({ + orgId, + id: role.id, + name, + description, + slug }); - reset(); - } catch { - const text = `Failed to ${popUp?.role?.data ? "update" : "create"} role`; + handlePopUpToggle("role", false); + } else { + // create - createNotification({ - text, - type: "error" + const newRole = await createOrgRole({ + orgId, + name, + description, + slug, + permissions: [] + }); + + handlePopUpToggle("role", false); + navigate({ + to: "/organization/roles/$roleId", + params: { + roleId: newRole.id + } }); } + + createNotification({ + text: `Successfully ${popUp?.role?.data ? "updated" : "created"} role`, + type: "success" + }); + + reset(); }; return ( diff --git a/frontend/src/pages/organization/SecretSharingPage/components/RequestSecret/RequestSecretForm.tsx b/frontend/src/pages/organization/SecretSharingPage/components/RequestSecret/RequestSecretForm.tsx index 50a9e0f58..d50873339 100644 --- a/frontend/src/pages/organization/SecretSharingPage/components/RequestSecret/RequestSecretForm.tsx +++ b/frontend/src/pages/organization/SecretSharingPage/components/RequestSecret/RequestSecretForm.tsx @@ -56,35 +56,27 @@ export const RequestSecretForm = () => { const onFormSubmit = async ({ name, accessType, expiresIn }: FormData) => { const expiresAt = new Date(new Date().getTime() + Number(expiresIn)); - try { - const { id } = await createSecretRequest({ - name, - accessType, - expiresAt - }); + const { id } = await createSecretRequest({ + name, + accessType, + expiresAt + }); - const link = new URL(`${window.location.origin}/secret-request/secret/${id}`); - if (subOrganization) { - link.searchParams.set("subOrganization", subOrganization); - } - - setSecretLink(link.toString()); - reset(); - - navigator.clipboard.writeText(link.toString()); - setCopyTextSecret("secret"); - - createNotification({ - text: "Shared secret link copied to clipboard.", - type: "success" - }); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to create a shared secret.", - type: "error" - }); + const link = new URL(`${window.location.origin}/secret-request/secret/${id}`); + if (subOrganization) { + link.searchParams.set("subOrganization", subOrganization); } + + setSecretLink(link.toString()); + reset(); + + navigator.clipboard.writeText(link.toString()); + setCopyTextSecret("secret"); + + createNotification({ + text: "Shared secret link copied to clipboard.", + type: "success" + }); }; const hasSecretLink = Boolean(secretLink); diff --git a/frontend/src/pages/organization/SecretSharingPage/components/RequestSecret/RequestSecretTab.tsx b/frontend/src/pages/organization/SecretSharingPage/components/RequestSecret/RequestSecretTab.tsx index fd3b1d5b5..1253cdbc0 100644 --- a/frontend/src/pages/organization/SecretSharingPage/components/RequestSecret/RequestSecretTab.tsx +++ b/frontend/src/pages/organization/SecretSharingPage/components/RequestSecret/RequestSecretTab.tsx @@ -22,23 +22,15 @@ export const RequestSecretTab = () => { const { mutateAsync: deleteSecretRequest } = useDeleteSecretRequest(); const onDeleteApproved = async () => { - try { - await deleteSecretRequest({ - secretRequestId: popUp.deleteSecretRequestConfirmation.data?.id - }); - createNotification({ - text: "Successfully deleted secret request", - type: "success" - }); + await deleteSecretRequest({ + secretRequestId: popUp.deleteSecretRequestConfirmation.data?.id + }); + createNotification({ + text: "Successfully deleted secret request", + type: "success" + }); - handlePopUpClose("deleteSecretRequestConfirmation"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete shared secret", - type: "error" - }); - } + handlePopUpClose("deleteSecretRequestConfirmation"); }; return ( diff --git a/frontend/src/pages/organization/SecretSharingPage/components/SecretSharingSettings/OrgSecretShareLimitSection.tsx b/frontend/src/pages/organization/SecretSharingPage/components/SecretSharingSettings/OrgSecretShareLimitSection.tsx index 034c7d8fe..d7c337332 100644 --- a/frontend/src/pages/organization/SecretSharingPage/components/SecretSharingSettings/OrgSecretShareLimitSection.tsx +++ b/frontend/src/pages/organization/SecretSharingPage/components/SecretSharingSettings/OrgSecretShareLimitSection.tsx @@ -90,28 +90,21 @@ export const OrgSecretShareLimitSection = () => { }, [currentOrg, reset]); const handleFormSubmit = async (formData: TForm) => { - try { - const maxSharedSecretLifetimeSeconds = - ms(`${formData.maxLifetimeValue}${formData.maxLifetimeUnit}`) / 1000; + const maxSharedSecretLifetimeSeconds = + ms(`${formData.maxLifetimeValue}${formData.maxLifetimeUnit}`) / 1000; - await mutateAsync({ - orgId: currentOrg.id, - maxSharedSecretViewLimit: formData.shouldLimitView ? Number(formData.maxViewLimit) : null, - maxSharedSecretLifetime: maxSharedSecretLifetimeSeconds - }); + await mutateAsync({ + orgId: currentOrg.id, + maxSharedSecretViewLimit: formData.shouldLimitView ? Number(formData.maxViewLimit) : null, + maxSharedSecretLifetime: maxSharedSecretLifetimeSeconds + }); - createNotification({ - text: "Successfully updated secret share limits", - type: "success" - }); + createNotification({ + text: "Successfully updated secret share limits", + type: "success" + }); - reset(formData); - } catch { - createNotification({ - text: "Failed to update secret share limits", - type: "error" - }); - } + reset(formData); }; // Units for the dropdown with readable labels diff --git a/frontend/src/pages/organization/SecretSharingPage/components/SecretSharingSettings/SecretSharingAllowShareToAnyone.tsx b/frontend/src/pages/organization/SecretSharingPage/components/SecretSharingSettings/SecretSharingAllowShareToAnyone.tsx index 1ea62c187..31b2221ce 100644 --- a/frontend/src/pages/organization/SecretSharingPage/components/SecretSharingSettings/SecretSharingAllowShareToAnyone.tsx +++ b/frontend/src/pages/organization/SecretSharingPage/components/SecretSharingSettings/SecretSharingAllowShareToAnyone.tsx @@ -9,25 +9,17 @@ export const SecretSharingAllowShareToAnyone = () => { const { mutateAsync } = useUpdateOrg(); const handleSecretSharingToggle = async (value: boolean) => { - try { - if (!currentOrg?.id) return; + if (!currentOrg?.id) return; - await mutateAsync({ - orgId: currentOrg.id, - allowSecretSharingOutsideOrganization: value - }); + await mutateAsync({ + orgId: currentOrg.id, + allowSecretSharingOutsideOrganization: value + }); - createNotification({ - text: `Successfully ${value ? "enabled" : "disabled"} secret sharing to members outside of this organization`, - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: (err as { response: { data: { message: string } } }).response.data.message, - type: "error" - }); - } + createNotification({ + text: `Successfully ${value ? "enabled" : "disabled"} secret sharing to members outside of this organization`, + type: "success" + }); }; return ( diff --git a/frontend/src/pages/organization/SecretSharingPage/components/ShareSecret/ShareSecretTab.tsx b/frontend/src/pages/organization/SecretSharingPage/components/ShareSecret/ShareSecretTab.tsx index d4f2f0a1f..2ea61ca98 100644 --- a/frontend/src/pages/organization/SecretSharingPage/components/ShareSecret/ShareSecretTab.tsx +++ b/frontend/src/pages/organization/SecretSharingPage/components/ShareSecret/ShareSecretTab.tsx @@ -20,23 +20,15 @@ export const ShareSecretTab = () => { const deleteSecretShare = useDeleteSharedSecret(); const onDeleteApproved = async () => { - try { - deleteSecretShare.mutateAsync({ - sharedSecretId: (popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.id - }); - createNotification({ - text: "Successfully deleted shared secret", - type: "success" - }); + deleteSecretShare.mutateAsync({ + sharedSecretId: (popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.id + }); + createNotification({ + text: "Successfully deleted shared secret", + type: "success" + }); - handlePopUpClose("deleteSharedSecretConfirmation"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete shared secret", - type: "error" - }); - } + handlePopUpClose("deleteSharedSecretConfirmation"); }; return ( diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/AuditLogStreamForm.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/AuditLogStreamForm.tsx index f59ae23aa..5ea16050a 100644 --- a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/AuditLogStreamForm.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/AuditLogStreamForm.tsx @@ -28,21 +28,12 @@ const CreateForm = ({ provider, onComplete }: CreateFormProps) => { const onSubmit = async ( formData: DiscriminativePick ) => { - try { - const logStream = await createAuditLogStream.mutateAsync(formData); - createNotification({ - text: `Successfully created ${providerName} Log Stream`, - type: "success" - }); - onComplete(logStream); - } catch (err: any) { - console.error(err); - createNotification({ - title: `Failed to create ${providerName} Log Stream`, - text: err.message, - type: "error" - }); - } + const logStream = await createAuditLogStream.mutateAsync(formData); + createNotification({ + text: `Successfully created ${providerName} Log Stream`, + type: "success" + }); + onComplete(logStream); }; switch (provider) { @@ -68,24 +59,15 @@ const UpdateForm = ({ auditLogStream, onComplete }: UpdateFormProps) => { const onSubmit = async ( formData: DiscriminativePick ) => { - try { - const connection = await updateAuditLogStream.mutateAsync({ - auditLogStreamId: auditLogStream.id, - ...formData - }); - createNotification({ - text: `Successfully updated ${providerName} Log Stream`, - type: "success" - }); - onComplete(connection); - } catch (err: any) { - console.error(err); - createNotification({ - title: `Failed to update ${providerName} Log Stream`, - text: err.message, - type: "error" - }); - } + const connection = await updateAuditLogStream.mutateAsync({ + auditLogStreamId: auditLogStream.id, + ...formData + }); + createNotification({ + text: `Successfully updated ${providerName} Log Stream`, + type: "success" + }); + onComplete(connection); }; switch (auditLogStream.provider) { diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/DeleteAuditLogStreamModal.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/DeleteAuditLogStreamModal.tsx index 1a65e6c67..74eb19af3 100644 --- a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/DeleteAuditLogStreamModal.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/DeleteAuditLogStreamModal.tsx @@ -20,26 +20,17 @@ export const DeleteAuditLogStreamModal = ({ isOpen, onOpenChange, auditLogStream const providerDetails = AUDIT_LOG_STREAM_PROVIDER_MAP[provider]; const handleDelete = async () => { - try { - await deleteAuditLogStream.mutateAsync({ - auditLogStreamId, - provider - }); + await deleteAuditLogStream.mutateAsync({ + auditLogStreamId, + provider + }); - createNotification({ - text: `Successfully deleted ${providerDetails.name} stream`, - type: "success" - }); + createNotification({ + text: `Successfully deleted ${providerDetails.name} stream`, + type: "success" + }); - onOpenChange(false); - } catch (err) { - console.error(err); - - createNotification({ - text: `Failed to delete ${providerDetails.name} stream`, - type: "error" - }); - } + onOpenChange(false); }; return ( diff --git a/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultConnectionSection.tsx b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultConnectionSection.tsx index c91e9c24c..e802e250b 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultConnectionSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultConnectionSection.tsx @@ -54,21 +54,13 @@ export const VaultConnectionSection = () => { const handleDeleteConfirm = async () => { if (!configToDelete) return; - try { - await deleteConfig({ id: configToDelete.id }); - createNotification({ - type: "success", - text: "Namespace configuration deleted successfully" - }); - setIsDeleteModalOpen(false); - setConfigToDelete(null); - } catch (error) { - console.error("Failed to delete namespace config:", error); - createNotification({ - type: "error", - text: "Failed to delete namespace configuration" - }); - } + await deleteConfig({ id: configToDelete.id }); + createNotification({ + type: "success", + text: "Namespace configuration deleted successfully" + }); + setIsDeleteModalOpen(false); + setConfigToDelete(null); }; const getConnectionName = (connectionId: string | null) => { diff --git a/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultNamespaceConfigModal.tsx b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultNamespaceConfigModal.tsx index 62fb78b48..f08aae204 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultNamespaceConfigModal.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultNamespaceConfigModal.tsx @@ -74,36 +74,28 @@ export const VaultNamespaceConfigModal = ({ isOpen, onOpenChange, editConfig }: }, [isOpen, editConfig, reset]); const onFormSubmit = async (data: FormData) => { - try { - if (isEdit && editConfig) { - await updateConfig({ - id: editConfig.id, - namespace: data.namespace, - connectionId: data.connectionId - }); - createNotification({ - type: "success", - text: "Namespace configuration updated successfully" - }); - } else { - await createConfig({ - namespace: data.namespace, - connectionId: data.connectionId - }); - createNotification({ - type: "success", - text: "Namespace configuration created successfully" - }); - } - reset(); - onOpenChange(false); - } catch (error) { - console.error("Failed to save namespace config:", error); + if (isEdit && editConfig) { + await updateConfig({ + id: editConfig.id, + namespace: data.namespace, + connectionId: data.connectionId + }); createNotification({ - type: "error", - text: `Failed to ${isEdit ? "update" : "create"} namespace configuration` + type: "success", + text: "Namespace configuration updated successfully" + }); + } else { + await createConfig({ + namespace: data.namespace, + connectionId: data.connectionId + }); + createNotification({ + type: "success", + text: "Namespace configuration created successfully" }); } + reset(); + onOpenChange(false); }; const handleClose = () => { diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx index 4dfe89f71..6e5c0e1cb 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx @@ -19,27 +19,19 @@ export const OrgDeleteSection = () => { const { mutateAsync, isPending } = useDeleteOrgById(); const handleDeleteOrgSubmit = async () => { - try { - if (!currentOrg?.id) return; + if (!currentOrg?.id) return; - await mutateAsync({ - organizationId: currentOrg?.id - }); + await mutateAsync({ + organizationId: currentOrg?.id + }); - createNotification({ - text: "Successfully deleted organization", - type: "success" - }); + createNotification({ + text: "Successfully deleted organization", + type: "success" + }); - clearSession(); - navigate({ to: "/login" }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete organization", - type: "error" - }); - } + clearSession(); + navigate({ to: "/login" }); }; return ( diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgEncryptionTab/OrgEncryptionTab.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgEncryptionTab/OrgEncryptionTab.tsx index a0b09b573..3fcf71e62 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgEncryptionTab/OrgEncryptionTab.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgEncryptionTab/OrgEncryptionTab.tsx @@ -51,18 +51,14 @@ export const OrgEncryptionTab = withPermission( kmsId: string; }; - try { - await removeExternalKms(kmsId); + await removeExternalKms(kmsId); - createNotification({ - text: "Successfully deleted external KMS", - type: "success" - }); + createNotification({ + text: "Successfully deleted external KMS", + type: "success" + }); - handlePopUpToggle("removeExternalKms", false); - } catch (err) { - console.error(err); - } + handlePopUpToggle("removeExternalKms", false); }; return ( diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgIncidentContactsSection/AddOrgIncidentContactModal.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgIncidentContactsSection/AddOrgIncidentContactModal.tsx index 84bb083ec..125a3f310 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgIncidentContactsSection/AddOrgIncidentContactModal.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgIncidentContactsSection/AddOrgIncidentContactModal.tsx @@ -35,31 +35,23 @@ export const AddOrgIncidentContactModal = ({ const { mutateAsync, isPending } = useAddIncidentContact(); const onFormSubmit = async ({ email }: TAddContactForm) => { - try { - if (!currentOrg?.id) return; + if (!currentOrg?.id) return; - await mutateAsync({ - orgId: currentOrg.id, - email - }); + await mutateAsync({ + orgId: currentOrg.id, + email + }); - createNotification({ - text: "Successfully added incident contact", - type: "success" - }); + createNotification({ + text: "Successfully added incident contact", + type: "success" + }); - if (serverDetails?.emailConfigured) { - handlePopUpClose("addContact"); - } - - reset(); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to add incident contact", - type: "error" - }); + if (serverDetails?.emailConfigured) { + handlePopUpClose("addContact"); } + + reset(); }; return ( diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgIncidentContactsSection/OrgIncidentContactsTable.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgIncidentContactsSection/OrgIncidentContactsTable.tsx index d0723fdaf..c16431b9c 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgIncidentContactsSection/OrgIncidentContactsTable.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgIncidentContactsSection/OrgIncidentContactsTable.tsx @@ -33,28 +33,20 @@ export const OrgIncidentContactsTable = () => { const { mutateAsync } = useDeleteIncidentContact(); const onRemoveIncidentContact = async () => { - try { - const incidentContactId = (popUp?.removeContact?.data as { id: string })?.id; + const incidentContactId = (popUp?.removeContact?.data as { id: string })?.id; - if (!currentOrg?.id) return; - await mutateAsync({ - orgId: currentOrg.id, - incidentContactId - }); + if (!currentOrg?.id) return; + await mutateAsync({ + orgId: currentOrg.id, + incidentContactId + }); - createNotification({ - text: "Successfully removed incident contact", - type: "success" - }); + createNotification({ + text: "Successfully removed incident contact", + type: "success" + }); - handlePopUpClose("removeContact"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to remove incident contact", - type: "error" - }); - } + handlePopUpClose("removeContact"); }; const filteredContacts = contacts diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx index 766a22158..03fb14dc7 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx @@ -56,27 +56,19 @@ export const OrgNameChangeSection = (): JSX.Element => { }, [roles]); const onFormSubmit = async ({ name, slug, defaultMembershipRole }: FormData) => { - try { - if (!currentOrg?.id || !roles?.length) return; + if (!currentOrg?.id || !roles?.length) return; - await mutateAsync({ - orgId: currentOrg?.id, - name, - slug, - defaultMembershipRoleSlug: defaultMembershipRole - }); + await mutateAsync({ + orgId: currentOrg?.id, + name, + slug, + defaultMembershipRoleSlug: defaultMembershipRole + }); - createNotification({ - text: "Successfully updated organization details", - type: "success" - }); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to update organization details", - type: "error" - }); - } + createNotification({ + text: "Successfully updated organization details", + type: "success" + }); }; if (!isFormInitialized) { diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/SubOrgNameChangeSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/SubOrgNameChangeSection.tsx index ca625be6c..e08d4d1b5 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/SubOrgNameChangeSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/SubOrgNameChangeSection.tsx @@ -39,26 +39,18 @@ export const SubOrgNameChangeSection = (): JSX.Element => { const { mutateAsync, isPending } = useUpdateSubOrganization(); const onFormSubmit = async ({ name }: FormData) => { - try { - await mutateAsync({ - name, - subOrgId: currentOrg.id - }); + await mutateAsync({ + name, + subOrgId: currentOrg.id + }); - navigate({ to: "/organization/settings", search: { subOrganization: name } }); - queryClient.invalidateQueries(); - await router.invalidate({ sync: true }); - createNotification({ - text: "Successfully updated sub-organization details", - type: "success" - }); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to update sub-organization details", - type: "error" - }); - } + navigate({ to: "/organization/settings", search: { subOrganization: name } }); + queryClient.invalidateQueries(); + await router.invalidate({ sync: true }); + createNotification({ + text: "Successfully updated sub-organization details", + type: "success" + }); }; return ( diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx index 5be15edad..e1fc2712d 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx @@ -1,7 +1,5 @@ import { useEffect, useState } from "react"; -import axios from "axios"; -import { createNotification } from "@app/components/notifications"; import { Switch } from "@app/components/v2"; import { useOrganization } from "@app/context"; import { useUpdateOrg } from "@app/hooks/api"; @@ -60,20 +58,10 @@ export const OrgProductSelectSection = () => { [key]: { ...products[key], enabled: value } })); - try { - await mutateAsync({ - orgId: currentOrg.id, - [key]: value - }); - } catch (e) { - if (axios.isAxiosError(e)) { - const { message = "Something went wrong" } = e.response?.data as { message: string }; - createNotification({ - type: "error", - text: message - }); - } - } + await mutateAsync({ + orgId: currentOrg.id, + [key]: value + }); setIsLoading(false); }; diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgProductSettingsTab/OrgProductSettingsTab.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgProductSettingsTab/OrgProductSettingsTab.tsx index e4d935996..95cc7b870 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgProductSettingsTab/OrgProductSettingsTab.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgProductSettingsTab/OrgProductSettingsTab.tsx @@ -30,12 +30,6 @@ export const OrgProductSettingsTab = () => { text: `Successfully ${state ? "enabled" : "disabled"} blocking duplicate secret sync destinations for this organization`, type: "success" }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to update blocking duplicate secret sync destinations setting for this organization", - type: "error" - }); } finally { setIsLoading(false); } diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgProvisioningTab/ExternalGroupOrgRoleMappings.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgProvisioningTab/ExternalGroupOrgRoleMappings.tsx index 8e181e8c4..bc9a02921 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgProvisioningTab/ExternalGroupOrgRoleMappings.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgProvisioningTab/ExternalGroupOrgRoleMappings.tsx @@ -75,19 +75,11 @@ export const ExternalGroupOrgRoleMappings = () => { const mappingField = useFieldArray({ control, name: "mappings" }); const handleUpdateMappings = async (form: TForm) => { - try { - await updateMappings.mutateAsync(form); - createNotification({ - text: "Group organization role mappings updated.", - type: "success" - }); - } catch (e) { - console.error(e); - createNotification({ - text: "Failed to update group organization role mappings.", - type: "error" - }); - } + await updateMappings.mutateAsync(form); + createNotification({ + text: "Group organization role mappings updated.", + type: "success" + }); }; const disableScimEdit = permission.cannot(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim); diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/LDAPModal.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/LDAPModal.tsx index 580ecdeb1..b9ba7a2b6 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/LDAPModal.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/LDAPModal.tsx @@ -122,53 +122,45 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele caCert, shouldCloseModal = true }: TLDAPFormData & { shouldCloseModal?: boolean }) => { - try { - if (!currentOrg) return; + if (!currentOrg) return; - if (!data) { - await createMutateAsync({ - organizationId: currentOrg.id, - isActive: false, - url, - bindDN, - bindPass, - searchBase, - searchFilter, - uniqueUserAttribute, - groupSearchBase, - groupSearchFilter, - caCert - }); - } else { - await updateMutateAsync({ - organizationId: currentOrg.id, - url, - bindDN, - bindPass, - searchBase, - searchFilter, - uniqueUserAttribute, - groupSearchBase, - groupSearchFilter, - caCert - }); - } - - if (shouldCloseModal) { - handlePopUpClose("addLDAP"); - } - - createNotification({ - text: `Successfully ${!data ? "added" : "updated"} LDAP configuration`, - type: "success" + if (!data) { + await createMutateAsync({ + organizationId: currentOrg.id, + isActive: false, + url, + bindDN, + bindPass, + searchBase, + searchFilter, + uniqueUserAttribute, + groupSearchBase, + groupSearchFilter, + caCert }); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to ${!data ? "add" : "update"} LDAP configuration`, - type: "error" + } else { + await updateMutateAsync({ + organizationId: currentOrg.id, + url, + bindDN, + bindPass, + searchBase, + searchFilter, + uniqueUserAttribute, + groupSearchBase, + groupSearchFilter, + caCert }); } + + if (shouldCloseModal) { + handlePopUpClose("addLDAP"); + } + + createNotification({ + text: `Successfully ${!data ? "added" : "updated"} LDAP configuration`, + type: "success" + }); }; const handleTestLDAPConnection = async () => { diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OIDCModal.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OIDCModal.tsx index 03f946ace..f980b2e4f 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OIDCModal.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OIDCModal.tsx @@ -122,31 +122,24 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele if (!currentOrg) { return; } - try { - await updateMutateAsync({ - issuer: "", - discoveryURL: "", - authorizationEndpoint: "", - allowedEmailDomains: "", - jwksUri: "", - tokenEndpoint: "", - userinfoEndpoint: "", - clientId: "", - clientSecret: "", - isActive: false, - organizationId: currentOrg.id - }); + await updateMutateAsync({ + issuer: "", + discoveryURL: "", + authorizationEndpoint: "", + allowedEmailDomains: "", + jwksUri: "", + tokenEndpoint: "", + userinfoEndpoint: "", + clientId: "", + clientSecret: "", + isActive: false, + organizationId: currentOrg.id + }); - createNotification({ - text: "Successfully deleted OIDC configuration.", - type: "success" - }); - } catch { - createNotification({ - text: "Failed deleting OIDC configuration.", - type: "error" - }); - } + createNotification({ + text: "Successfully deleted OIDC configuration.", + type: "success" + }); }; useEffect(() => { @@ -178,58 +171,50 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele clientSecret, jwtSignatureAlgorithm }: OIDCFormData) => { - try { - if (!currentOrg) { - return; - } + if (!currentOrg) { + return; + } - if (!data) { - await createMutateAsync({ - issuer, - configurationType, - discoveryURL, - authorizationEndpoint, - allowedEmailDomains, - jwksUri, - tokenEndpoint, - userinfoEndpoint, - clientId, - clientSecret, - isActive: true, - organizationId: currentOrg.id, - jwtSignatureAlgorithm - }); - } else { - await updateMutateAsync({ - issuer, - configurationType, - discoveryURL, - authorizationEndpoint, - allowedEmailDomains, - jwksUri, - tokenEndpoint, - userinfoEndpoint, - clientId, - clientSecret, - isActive: true, - organizationId: currentOrg.id, - jwtSignatureAlgorithm - }); - } - - handlePopUpClose("addOIDC"); - - createNotification({ - text: `Successfully ${!data ? "added" : "updated"} OIDC SSO configuration`, - type: "success" + if (!data) { + await createMutateAsync({ + issuer, + configurationType, + discoveryURL, + authorizationEndpoint, + allowedEmailDomains, + jwksUri, + tokenEndpoint, + userinfoEndpoint, + clientId, + clientSecret, + isActive: true, + organizationId: currentOrg.id, + jwtSignatureAlgorithm }); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to ${!data ? "add" : "update"} OIDC SSO configuration`, - type: "error" + } else { + await updateMutateAsync({ + issuer, + configurationType, + discoveryURL, + authorizationEndpoint, + allowedEmailDomains, + jwksUri, + tokenEndpoint, + userinfoEndpoint, + clientId, + clientSecret, + isActive: true, + organizationId: currentOrg.id, + jwtSignatureAlgorithm }); } + + handlePopUpClose("addOIDC"); + + createNotification({ + text: `Successfully ${!data ? "added" : "updated"} OIDC SSO configuration`, + type: "success" + }); }; return ( diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/SSOModal.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/SSOModal.tsx index df5fca59f..558f37dcb 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/SSOModal.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/SSOModal.tsx @@ -108,64 +108,49 @@ export const SSOModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDelet if (!currentOrg) { return; } - try { - await updateMutateAsync({ - organizationId: currentOrg.id, - isActive: false, - entryPoint: "", - issuer: "", - cert: "" - }); + await updateMutateAsync({ + organizationId: currentOrg.id, + isActive: false, + entryPoint: "", + issuer: "", + cert: "" + }); - createNotification({ - text: "Successfully deleted SAML SSO configuration.", - type: "success" - }); - } catch { - createNotification({ - text: "Failed deleting SAML SSO configuration.", - type: "error" - }); - } + createNotification({ + text: "Successfully deleted SAML SSO configuration.", + type: "success" + }); }; const onSSOModalSubmit = async ({ authProvider, entryPoint, issuer, cert }: AddSSOFormData) => { - try { - if (!currentOrg) return; + if (!currentOrg) return; - if (!data) { - await createMutateAsync({ - organizationId: currentOrg.id, - authProvider, - isActive: false, - entryPoint, - issuer, - cert - }); - } else { - await updateMutateAsync({ - organizationId: currentOrg.id, - authProvider, - isActive: false, - entryPoint, - issuer, - cert - }); - } - - handlePopUpClose("addSSO"); - - createNotification({ - text: `Successfully ${!data ? "added" : "updated"} SAML SSO configuration`, - type: "success" + if (!data) { + await createMutateAsync({ + organizationId: currentOrg.id, + authProvider, + isActive: false, + entryPoint, + issuer, + cert }); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to ${!data ? "add" : "update"} SAML SSO configuration`, - type: "error" + } else { + await updateMutateAsync({ + organizationId: currentOrg.id, + authProvider, + isActive: false, + entryPoint, + issuer, + cert }); } + + handlePopUpClose("addSSO"); + + createNotification({ + text: `Successfully ${!data ? "added" : "updated"} SAML SSO configuration`, + type: "success" + }); }; const renderLabels = (authProvider: string) => { diff --git a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx index 188c2ba7d..67e962cfc 100644 --- a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx @@ -253,18 +253,14 @@ const GroupRolesForm = ({ projectRoles, roles, groupId, onClose }: FormProps) => }; }); - try { - await updateGroupWorkspaceRole.mutateAsync({ - projectId: currentProject?.id || "", - groupId, - roles: selectedRoles - }); - createNotification({ text: "Successfully updated group role", type: "success" }); - onClose(); - setSearchRoles(""); - } catch { - createNotification({ text: "Failed to update group role", type: "error" }); - } + await updateGroupWorkspaceRole.mutateAsync({ + projectId: currentProject?.id || "", + groupId, + roles: selectedRoles + }); + createNotification({ text: "Successfully updated group role", type: "success" }); + onClose(); + setSearchRoles(""); }; return ( diff --git a/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx b/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx index fca53fa06..eeb29725f 100644 --- a/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx @@ -77,17 +77,12 @@ export const ProjectRoleList = () => { const handleRoleDelete = async () => { const { id } = popUp?.deleteRole?.data as TProjectRole; - try { - await deleteRole({ - projectId, - id - }); - createNotification({ type: "success", text: "Successfully removed the role" }); - handlePopUpClose("deleteRole"); - } catch (err) { - console.log(err); - createNotification({ type: "error", text: "Failed to delete role" }); - } + await deleteRole({ + projectId, + id + }); + createNotification({ type: "success", text: "Successfully removed the role" }); + handlePopUpClose("deleteRole"); }; const { diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx index a5d74271c..956c01d69 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx @@ -131,33 +131,28 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({ temporaryAccessStartTime: el.temporaryAccess.temporaryAccessStartTime }; - try { - if (isCreate) { - await createIdentityProjectAdditionalPrivilege({ - permissions: formRolePermission2API(el.permissions), - identityId, - projectId, - slug: el.slug || undefined, - type: accessType - }); - createNotification({ type: "success", text: "Successfully created privilege" }); - } else { - if (!projectId || !privilegeDetails?.id) return; - await updateIdentityProjectAdditionalPrivilege({ - privilegeId: privilegeDetails.id, - permissions: formRolePermission2API(el.permissions), - projectId, - identityId, - slug: el.slug || undefined, - type: accessType - }); - createNotification({ type: "success", text: "Successfully updated privilege" }); - } - onGoBack(); - } catch (err) { - console.log(err); - createNotification({ type: "error", text: "Failed to update privilege" }); + if (isCreate) { + await createIdentityProjectAdditionalPrivilege({ + permissions: formRolePermission2API(el.permissions), + identityId, + projectId, + slug: el.slug || undefined, + type: accessType + }); + createNotification({ type: "success", text: "Successfully created privilege" }); + } else { + if (!projectId || !privilegeDetails?.id) return; + await updateIdentityProjectAdditionalPrivilege({ + privilegeId: privilegeDetails.id, + permissions: formRolePermission2API(el.permissions), + projectId, + identityId, + slug: el.slug || undefined, + type: accessType + }); + createNotification({ type: "success", text: "Successfully updated privilege" }); } + onGoBack(); }; const privilegeTemporaryAccess = form.watch("temporaryAccess"); diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeSection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeSection.tsx index f98f6ebf8..431875b3c 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeSection.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeSection.tsx @@ -57,18 +57,13 @@ export const IdentityProjectAdditionalPrivilegeSection = ({ identityMembershipDe const handlePrivilegeDelete = async () => { const { id } = popUp?.deletePrivilege?.data as { id: string }; - try { - await deletePrivilege({ - privilegeId: id, - projectId, - identityId - }); - createNotification({ type: "success", text: "Successfully removed the privilege" }); - handlePopUpClose("deletePrivilege"); - } catch (err) { - console.log(err); - createNotification({ type: "error", text: "Failed to delete privilege" }); - } + await deletePrivilege({ + privilegeId: id, + projectId, + identityId + }); + createNotification({ type: "success", text: "Successfully removed the privilege" }); + handlePopUpClose("deletePrivilege"); }; return ( diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx index 34b5229d6..dce632121 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx @@ -50,42 +50,37 @@ export const IdentityRoleDetailsSection = ({ const handleRoleDelete = async () => { const { id } = popUp?.deleteRole?.data as TProjectRole; - try { - const updatedRoles = identityMembershipDetails?.roles?.filter((el) => el.id !== id); - await updateIdentityWorkspaceRole({ - projectId: currentProject?.id || "", - identityId: identityMembershipDetails.identity.id, - roles: updatedRoles.map( - ({ - role, - customRoleSlug, - isTemporary, - temporaryMode, - temporaryRange, - temporaryAccessStartTime, - temporaryAccessEndTime - }) => ({ - role: role === "custom" ? customRoleSlug : role, - ...(isTemporary - ? { - isTemporary, - temporaryMode, - temporaryRange, - temporaryAccessStartTime, - temporaryAccessEndTime - } - : { - isTemporary - }) - }) - ) - }); - createNotification({ type: "success", text: "Successfully removed role" }); - handlePopUpClose("deleteRole"); - } catch (err) { - console.log(err); - createNotification({ type: "error", text: "Failed to delete role" }); - } + const updatedRoles = identityMembershipDetails?.roles?.filter((el) => el.id !== id); + await updateIdentityWorkspaceRole({ + projectId: currentProject?.id || "", + identityId: identityMembershipDetails.identity.id, + roles: updatedRoles.map( + ({ + role, + customRoleSlug, + isTemporary, + temporaryMode, + temporaryRange, + temporaryAccessStartTime, + temporaryAccessEndTime + }) => ({ + role: role === "custom" ? customRoleSlug : role, + ...(isTemporary + ? { + isTemporary, + temporaryMode, + temporaryRange, + temporaryAccessStartTime, + temporaryAccessEndTime + } + : { + isTemporary + }) + }) + ) + }); + createNotification({ type: "success", text: "Successfully removed role" }); + handlePopUpClose("deleteRole"); }; return ( diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MemberProjectAdditionalPrivilegeSection.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MemberProjectAdditionalPrivilegeSection.tsx index 7094c9ca8..28c3c9b5f 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MemberProjectAdditionalPrivilegeSection.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MemberProjectAdditionalPrivilegeSection.tsx @@ -60,17 +60,12 @@ export const MemberProjectAdditionalPrivilegeSection = ({ membershipDetails }: P const handlePrivilegeDelete = async () => { const { id } = popUp?.deletePrivilege?.data as { id: string }; - try { - await deletePrivilege({ - privilegeId: id, - projectMembershipId: membershipDetails.id - }); - createNotification({ type: "success", text: "Successfully removed the privilege" }); - handlePopUpClose("deletePrivilege"); - } catch (err) { - console.log(err); - createNotification({ type: "error", text: "Failed to delete privilege" }); - } + await deletePrivilege({ + privilegeId: id, + projectMembershipId: membershipDetails.id + }); + createNotification({ type: "success", text: "Successfully removed the privilege" }); + handlePopUpClose("deletePrivilege"); }; return ( diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx index 1a217a509..bc5bd3a6f 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx @@ -129,31 +129,26 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({ temporaryAccessStartTime: el.temporaryAccess.temporaryAccessStartTime }; - try { - if (isCreate) { - await createUserProjectAdditionalPrivilege({ - permissions: formRolePermission2API(el.permissions), - projectMembershipId, - slug: el.slug || undefined, - type: accessType - }); - createNotification({ type: "success", text: "Successfully created privilege" }); - } else { - if (!projectId || !privilegeDetails?.id) return; - await updateUserProjectAdditionalPrivilege({ - privilegeId: privilegeDetails.id, - permissions: formRolePermission2API(el.permissions), - projectMembershipId, - slug: el.slug || undefined, - type: accessType - }); - createNotification({ type: "success", text: "Successfully updated privilege" }); - } - onGoBack(); - } catch (err) { - console.log(err); - createNotification({ type: "error", text: "Failed to update privilege" }); + if (isCreate) { + await createUserProjectAdditionalPrivilege({ + permissions: formRolePermission2API(el.permissions), + projectMembershipId, + slug: el.slug || undefined, + type: accessType + }); + createNotification({ type: "success", text: "Successfully created privilege" }); + } else { + if (!projectId || !privilegeDetails?.id) return; + await updateUserProjectAdditionalPrivilege({ + privilegeId: privilegeDetails.id, + permissions: formRolePermission2API(el.permissions), + projectMembershipId, + slug: el.slug || undefined, + type: accessType + }); + createNotification({ type: "success", text: "Successfully updated privilege" }); } + onGoBack(); }; const privilegeTemporaryAccess = form.watch("temporaryAccess"); diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx index fd95d2659..c93cea534 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx @@ -122,19 +122,14 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { const { mutateAsync: updateRole } = useUpdateProjectRole(); const onSubmit = async (el: TFormSchema) => { - try { - if (!projectId || !role?.id) return; - await updateRole({ - id: role?.id as string, - projectId, - ...el, - permissions: formRolePermission2API(el.permissions) - }); - createNotification({ type: "success", text: "Successfully updated role" }); - } catch (err) { - console.log(err); - createNotification({ type: "error", text: "Failed to update role" }); - } + if (!projectId || !role?.id) return; + await updateRole({ + id: role?.id as string, + projectId, + ...el, + permissions: formRolePermission2API(el.permissions) + }); + createNotification({ type: "success", text: "Successfully updated role" }); }; const isCustomRole = !Object.values(ProjectMembershipRole).includes( diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/BackfillSecretReferenceSection/BackfillSecretReferenceSection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/BackfillSecretReferenceSection/BackfillSecretReferenceSection.tsx index 26f5c7fc9..9350698dc 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/BackfillSecretReferenceSection/BackfillSecretReferenceSection.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/BackfillSecretReferenceSection/BackfillSecretReferenceSection.tsx @@ -13,12 +13,8 @@ export const BackfillSecretReferenceSecretion = () => { const handleBackfill = async () => { if (backfillSecretReferences.isPending) return; - try { - await backfillSecretReferences.mutateAsync({ projectId: currentProject.id || "" }); - createNotification({ text: "Successfully re-indexed secret references", type: "success" }); - } catch { - createNotification({ text: "Failed to re-index secret references", type: "error" }); - } + await backfillSecretReferences.mutateAsync({ projectId: currentProject.id || "" }); + createNotification({ text: "Successfully re-indexed secret references", type: "success" }); }; const isAdmin = hasProjectRole(ProjectMembershipRole.Admin); From 3aeee8d552fed4065029f8adee547accc339e499 Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Mon, 3 Nov 2025 11:58:44 -0300 Subject: [PATCH 016/294] refactor: enhance notification handling and streamline async logic across various components --- .../OauthCallbackPage/OauthCallbackPage.tsx | 18 +--- .../GithubOrgSyncConfigModal.tsx | 67 +++++------- .../OrgGithubSyncSection.tsx | 83 ++++++-------- .../OrgProvisioningTab/OrgSCIMSection.tsx | 37 +++---- .../OrgProvisioningTab/ScimTokenModal.tsx | 58 ++++------ .../OrgSecurityTab/OrgGenericAuthSection.tsx | 72 +++++-------- .../OrgUserAccessTokenLimitSection.tsx | 25 ++--- .../OrgSsoTab/LDAPGroupMapModal.tsx | 56 ++++------ .../components/OrgSsoTab/LDAPModal.tsx | 77 +++++-------- .../OrgSsoTab/OrgGeneralAuthSection.tsx | 100 ++++++++--------- .../components/OrgSsoTab/OrgLDAPSection.tsx | 38 +++---- .../components/OrgSsoTab/OrgOIDCSection.tsx | 64 +++++------ .../components/OrgSsoTab/OrgSSOSection.tsx | 79 ++++++-------- .../components/DeleteProjectTemplateModal.tsx | 25 ++--- .../components/EditProjectTemplate.tsx | 24 ++--- .../ProjectTemplateEditRoleForm.tsx | 42 +++----- .../ProjectTemplateEnvironmentsForm.tsx | 32 +++--- .../ProjectTemplateRolesSection.tsx | 30 ++---- .../ProjectTemplateDetailsModal.tsx | 26 ++--- .../components/UserOrgMembershipModal.tsx | 37 +++---- .../UserAddToProjectModal.tsx | 33 ++---- .../UserProjectsSection/UserGroupsSection.tsx | 27 ++--- .../components/GroupsSection/GroupModal.tsx | 29 ++--- .../GroupsSection/GroupsSection.tsx | 29 ++--- .../components/IdentityTab/IdentityTab.tsx | 29 ++--- .../IdentityTab/components/IdentityModal.tsx | 51 ++++----- .../MembersTab/components/AddMemberModal.tsx | 101 ++++++++---------- .../SpecificPrivilegeSection.tsx | 23 ++-- .../MembersTab/components/MembersSection.tsx | 26 ++--- .../AddServiceTokenModal.tsx | 57 ++++------ .../ServiceTokenSection.tsx | 24 ++--- .../components/GroupDetailsSection.tsx | 49 ++++----- .../IdentityDetailsByIDPage.tsx | 45 +++----- .../MemberDetailsByIDPage.tsx | 38 +++---- .../RoleDetailsBySlugPage.tsx | 49 ++++----- .../components/RoleModal.tsx | 79 ++++++-------- .../DeleteProjectProtection.tsx | 26 ++--- .../DeleteProjectSection.tsx | 12 --- 38 files changed, 653 insertions(+), 1064 deletions(-) diff --git a/frontend/src/pages/organization/SettingsPage/OauthCallbackPage/OauthCallbackPage.tsx b/frontend/src/pages/organization/SettingsPage/OauthCallbackPage/OauthCallbackPage.tsx index df5bf4f60..5efec93d9 100644 --- a/frontend/src/pages/organization/SettingsPage/OauthCallbackPage/OauthCallbackPage.tsx +++ b/frontend/src/pages/organization/SettingsPage/OauthCallbackPage/OauthCallbackPage.tsx @@ -72,20 +72,12 @@ export const OAuthCallbackPage = () => { if (!isReady) return; (async () => { - try { - await handleMicrosoftTeams(); + await handleMicrosoftTeams(); - createNotification({ - text: "Successfully created Microsoft Teams workflow integration", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to create Microsoft Teams workflow integration", - type: "error" - }); - } + createNotification({ + text: "Successfully created Microsoft Teams workflow integration", + type: "success" + }); })(); }, [isReady]); diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgProvisioningTab/GithubOrgSyncConfigModal.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgProvisioningTab/GithubOrgSyncConfigModal.tsx index 180b63278..868dd17f7 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgProvisioningTab/GithubOrgSyncConfigModal.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgProvisioningTab/GithubOrgSyncConfigModal.tsx @@ -55,55 +55,40 @@ export const GithubOrgSyncConfigModal = ({ }); const onFormSubmit = async ({ githubOrgName, githubOrgAccessToken }: FormData) => { - try { - if (isUpdate) { - await updateGithubSyncOrgConfig({ - githubOrgName, - githubOrgAccessToken - }); + if (isUpdate) { + await updateGithubSyncOrgConfig({ + githubOrgName, + githubOrgAccessToken + }); - createNotification({ - text: "Successfully updated GitHub Organization Sync", - type: "success" - }); - } else { - await createGithubSyncOrgConfig({ - githubOrgName, - githubOrgAccessToken, - isActive: false - }); - - createNotification({ - text: "Successfully created GitHub Organization Sync", - type: "success" - }); - } - handlePopUpToggle("githubOrgSyncConfig"); - } catch { createNotification({ - text: "Failed to setup GitHub Organization Sync", - type: "error" + text: "Successfully updated GitHub Organization Sync", + type: "success" + }); + } else { + await createGithubSyncOrgConfig({ + githubOrgName, + githubOrgAccessToken, + isActive: false + }); + + createNotification({ + text: "Successfully created GitHub Organization Sync", + type: "success" }); } + handlePopUpToggle("githubOrgSyncConfig"); }; const onDelete = async () => { - try { - await deleteGithubSyncOrgConfig(); + await deleteGithubSyncOrgConfig(); - handlePopUpToggle("deleteGithubOrgSyncConfig", false); - handlePopUpToggle("githubOrgSyncConfig", false); - createNotification({ - text: "Successfully deleted GitHub Organization Sync", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete GitHub Organization Sync", - type: "error" - }); - } + handlePopUpToggle("deleteGithubOrgSyncConfig", false); + handlePopUpToggle("githubOrgSyncConfig", false); + createNotification({ + text: "Successfully deleted GitHub Organization Sync", + type: "success" + }); }; return ( diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgProvisioningTab/OrgGithubSyncSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgProvisioningTab/OrgGithubSyncSection.tsx index 188a83354..610cb61c1 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgProvisioningTab/OrgGithubSyncSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgProvisioningTab/OrgGithubSyncSection.tsx @@ -35,64 +35,41 @@ export const OrgGithubSyncSection = () => { const data = !isPending && !githubOrgSyncConfig?.isError ? githubOrgSyncConfig?.data : undefined; const handleBulkSync = async () => { - try { - const result = await syncAllTeamsMutation.mutateAsync(); - let message = "Successfully synced teams"; + const result = await syncAllTeamsMutation.mutateAsync(); + let message = "Successfully synced teams"; - const details = []; - if (result.createdTeams.length > 0) { - details.push( - `${result.createdTeams.length} new team${result.createdTeams.length === 1 ? "" : "s"} created` - ); - } - if (result.updatedTeams.length > 0) { - details.push( - `${result.updatedTeams.length} team${result.updatedTeams.length === 1 ? "" : "s"} updated` - ); - } - if (result.removedMemberships > 0) { - details.push( - `${result.removedMemberships} membership${result.removedMemberships === 1 ? "" : "s"} removed` - ); - } + const details = []; + if (result.createdTeams.length > 0) { + details.push( + `${result.createdTeams.length} new team${result.createdTeams.length === 1 ? "" : "s"} created` + ); + } + if (result.updatedTeams.length > 0) { + details.push( + `${result.updatedTeams.length} team${result.updatedTeams.length === 1 ? "" : "s"} updated` + ); + } + if (result.removedMemberships > 0) { + details.push( + `${result.removedMemberships} membership${result.removedMemberships === 1 ? "" : "s"} removed` + ); + } - if (details.length > 0) { - message += `. ${details.join(", ")}`; - } + if (details.length > 0) { + message += `. ${details.join(", ")}`; + } + createNotification({ + text: message, + type: "success" + }); + + if (result.errors && result.errors.length > 0) { createNotification({ - text: message, - type: "success" + text: `Sync completed with ${result.errors.length} warnings. Check the console for details.`, + type: "warning" }); - - if (result.errors && result.errors.length > 0) { - createNotification({ - text: `Sync completed with ${result.errors.length} warnings. Check the console for details.`, - type: "warning" - }); - console.warn("Sync errors:", result.errors); - } - } catch (error) { - const errorMessage = - (error as any)?.response?.data?.message || (error as Error)?.message || "Unknown error"; - - if ( - errorMessage.includes("token") && - (errorMessage.includes("required") || - errorMessage.includes("invalid") || - errorMessage.includes("expired") || - errorMessage.includes("set a token first")) - ) { - createNotification({ - text: "Please set a GitHub access token in the configuration modal to continue with the sync", - type: "error" - }); - } else { - createNotification({ - text: `Failed to sync GitHub teams: ${errorMessage}`, - type: "error" - }); - } + console.warn("Sync errors:", result.errors); } }; diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgProvisioningTab/OrgSCIMSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgProvisioningTab/OrgSCIMSection.tsx index 3936a20ee..3d079ccef 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgProvisioningTab/OrgSCIMSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgProvisioningTab/OrgSCIMSection.tsx @@ -36,30 +36,23 @@ export const OrgScimSection = () => { }; const handleEnableSCIMToggle = async (value: boolean) => { - try { - if (!currentOrg?.id) return; - if (!subscription?.scim) { - handlePopUpOpen("upgradePlan", { - isEnterpriseFeature: true - }); - return; - } - - await mutateAsync({ - orgId: currentOrg?.id, - scimEnabled: value - }); - - createNotification({ - text: `Successfully ${value ? "enabled" : "disabled"} SCIM provisioning`, - type: "success" - }); - } catch (err) { - createNotification({ - text: (err as { response: { data: { message: string } } }).response.data.message, - type: "error" + if (!currentOrg?.id) return; + if (!subscription?.scim) { + handlePopUpOpen("upgradePlan", { + isEnterpriseFeature: true }); + return; } + + await mutateAsync({ + orgId: currentOrg?.id, + scimEnabled: value + }); + + createNotification({ + text: `Successfully ${value ? "enabled" : "disabled"} SCIM provisioning`, + type: "success" + }); }; return ( diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgProvisioningTab/ScimTokenModal.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgProvisioningTab/ScimTokenModal.tsx index c542a88bb..5d6007ac0 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgProvisioningTab/ScimTokenModal.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgProvisioningTab/ScimTokenModal.tsx @@ -92,52 +92,36 @@ export const ScimTokenModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Pr }, [isScimTokenCopied, isScimUrlCopied]); const onFormSubmit = async ({ description, ttlDays }: FormData) => { - try { - if (!currentOrg?.id) return; + if (!currentOrg?.id) return; - const { scimToken } = await createScimTokenMutateAsync({ - organizationId: currentOrg.id, - description, - ttlDays: Number(ttlDays) - }); + const { scimToken } = await createScimTokenMutateAsync({ + organizationId: currentOrg.id, + description, + ttlDays: Number(ttlDays) + }); - setToken(scimToken); + setToken(scimToken); - createNotification({ - text: "Successfully created SCIM token", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to create SCIM token", - type: "error" - }); - } + createNotification({ + text: "Successfully created SCIM token", + type: "success" + }); }; const onDeleteScimTokenSubmit = async (scimTokenId: string) => { - try { - if (!currentOrg?.id) return; + if (!currentOrg?.id) return; - await deleteScimTokenMutateAsync({ - organizationId: currentOrg.id, - scimTokenId - }); + await deleteScimTokenMutateAsync({ + organizationId: currentOrg.id, + scimTokenId + }); - handlePopUpToggle("deleteScimToken", false); + handlePopUpToggle("deleteScimToken", false); - createNotification({ - text: "Successfully deleted SCIM token", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete SCIM token", - type: "error" - }); - } + createNotification({ + text: "Successfully deleted SCIM token", + type: "success" + }); }; const hasToken = Boolean(token); diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgGenericAuthSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgGenericAuthSection.tsx index 7e2bd522b..7fe5fc219 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgGenericAuthSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgGenericAuthSection.tsx @@ -20,55 +20,39 @@ export const OrgGenericAuthSection = () => { const { mutateAsync } = useUpdateOrg(); const handleEnforceMfaToggle = async (value: boolean) => { - try { - if (!currentOrg?.id) return; - if (!subscription?.enforceMfa) { - handlePopUpOpen("upgradePlan"); - return; - } - - await mutateAsync({ - orgId: currentOrg?.id, - enforceMfa: value - }); - - createNotification({ - text: `Successfully ${value ? "enforced" : "un-enforced"} MFA`, - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: (err as { response: { data: { message: string } } }).response.data.message, - type: "error" - }); + if (!currentOrg?.id) return; + if (!subscription?.enforceMfa) { + handlePopUpOpen("upgradePlan"); + return; } + + await mutateAsync({ + orgId: currentOrg?.id, + enforceMfa: value + }); + + createNotification({ + text: `Successfully ${value ? "enforced" : "un-enforced"} MFA`, + type: "success" + }); }; const handleUpdateSelectedMfa = async (selectedMfaMethod: MfaMethod) => { - try { - if (!currentOrg?.id) return; - if (!subscription?.enforceMfa) { - handlePopUpOpen("upgradePlan"); - return; - } - - await mutateAsync({ - orgId: currentOrg?.id, - selectedMfaMethod - }); - - createNotification({ - text: "Successfully updated selected MFA method", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: (err as { response: { data: { message: string } } }).response.data.message, - type: "error" - }); + if (!currentOrg?.id) return; + if (!subscription?.enforceMfa) { + handlePopUpOpen("upgradePlan"); + return; } + + await mutateAsync({ + orgId: currentOrg?.id, + selectedMfaMethod + }); + + createNotification({ + text: "Successfully updated selected MFA method", + type: "success" + }); }; return ( diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgUserAccessTokenLimitSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgUserAccessTokenLimitSection.tsx index 6e9865532..c78b5475e 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgUserAccessTokenLimitSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgUserAccessTokenLimitSection.tsx @@ -57,24 +57,17 @@ export const OrgUserAccessTokenLimitSection = () => { if (!currentOrg) return null; const handleUserTokenExpirationSubmit = async (formData: TForm) => { - try { - const userTokenExpiration = formatDuration(formData.expirationValue, formData.expirationUnit); + const userTokenExpiration = formatDuration(formData.expirationValue, formData.expirationUnit); - await updateUserTokenExpiration({ - userTokenExpiration, - orgId: currentOrg.id - }); + await updateUserTokenExpiration({ + userTokenExpiration, + orgId: currentOrg.id + }); - createNotification({ - text: "Successfully updated user token expiration", - type: "success" - }); - } catch { - createNotification({ - text: "Failed updating user token expiration", - type: "error" - }); - } + createNotification({ + text: "Successfully updated user token expiration", + type: "success" + }); }; // Units for the dropdown with readable labels diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/LDAPGroupMapModal.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/LDAPGroupMapModal.tsx index aee845187..31e96a1cb 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/LDAPGroupMapModal.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/LDAPGroupMapModal.tsx @@ -79,28 +79,20 @@ export const LDAPGroupMapModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: }); const onFormSubmit = async ({ groupSlug, ldapGroupCN }: TFormData) => { - try { - if (!ldapConfig) return; + if (!ldapConfig) return; - await createLDAPGroupMapping({ - ldapConfigId: ldapConfig.id, - groupSlug, - ldapGroupCN - }); + await createLDAPGroupMapping({ + ldapConfigId: ldapConfig.id, + groupSlug, + ldapGroupCN + }); - reset(); + reset(); - createNotification({ - text: `Successfully added LDAP group mapping for ${ldapGroupCN}`, - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to add LDAP group mapping for ${ldapGroupCN}`, - type: "error" - }); - } + createNotification({ + text: `Successfully added LDAP group mapping for ${ldapGroupCN}`, + type: "success" + }); }; const onDeleteGroupMapSubmit = async ({ @@ -112,25 +104,17 @@ export const LDAPGroupMapModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: ldapGroupMapId: string; ldapGroupCN: string; }) => { - try { - await deleteLDAPGroupMapping({ - ldapConfigId, - ldapGroupMapId - }); + await deleteLDAPGroupMapping({ + ldapConfigId, + ldapGroupMapId + }); - handlePopUpToggle("deleteLdapGroupMap", false); + handlePopUpToggle("deleteLdapGroupMap", false); - createNotification({ - text: `Successfully deleted LDAP group mapping ${ldapGroupCN}`, - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to delete LDAP group mapping ${ldapGroupCN}`, - type: "error" - }); - } + createNotification({ + text: `Successfully deleted LDAP group mapping ${ldapGroupCN}`, + type: "success" + }); }; useEffect(() => { diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/LDAPModal.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/LDAPModal.tsx index b9ba7a2b6..01429f512 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/LDAPModal.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/LDAPModal.tsx @@ -62,31 +62,24 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele if (!currentOrg) { return; } - try { - await updateMutateAsync({ - organizationId: currentOrg.id, - isActive: false, - url: "", - bindDN: "", - bindPass: "", - searchBase: "", - searchFilter: "", - uniqueUserAttribute: "", - groupSearchBase: "", - groupSearchFilter: "", - caCert: "" - }); + await updateMutateAsync({ + organizationId: currentOrg.id, + isActive: false, + url: "", + bindDN: "", + bindPass: "", + searchBase: "", + searchFilter: "", + uniqueUserAttribute: "", + groupSearchBase: "", + groupSearchFilter: "", + caCert: "" + }); - createNotification({ - text: "Successfully deleted OIDC configuration.", - type: "success" - }); - } catch { - createNotification({ - text: "Failed deleting OIDC configuration.", - type: "error" - }); - } + createNotification({ + text: "Successfully deleted OIDC configuration.", + type: "success" + }); }; const watchUrl = watch("url"); @@ -164,33 +157,17 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele }; const handleTestLDAPConnection = async () => { - try { - const result = await testLDAPConnection({ - url: watchUrl, - bindDN: watchBindDN, - bindPass: watchBindPass, - caCert: watchCaCert ?? "" - }); + await testLDAPConnection({ + url: watchUrl, + bindDN: watchBindDN, + bindPass: watchBindPass, + caCert: watchCaCert ?? "" + }); - if (!result) { - createNotification({ - text: "Failed to test the LDAP connection: Bind operation was unsuccessful", - type: "error" - }); - return; - } - - createNotification({ - text: "Successfully tested the LDAP connection: Bind operation was successful", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to test the LDAP connection", - type: "error" - }); - } + createNotification({ + text: "Successfully tested the LDAP connection: Bind operation was successful", + type: "success" + }); }; return ( diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx index 527239898..f8d5b3200 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx @@ -45,69 +45,61 @@ export const OrgGeneralAuthSection = ({ const logout = useLogoutUser(); const handleEnforceOrgAuthToggle = async (value: boolean, type: EnforceAuthType) => { - try { - if (!currentOrg?.id) return; + if (!currentOrg?.id) return; - if (type === EnforceAuthType.SAML) { - if (!subscription?.samlSSO) { - handlePopUpOpen("upgradePlan"); - return; - } - - await mutateAsync({ - orgId: currentOrg?.id, - authEnforced: value - }); - } else if (type === EnforceAuthType.GOOGLE) { - if (!subscription?.enforceGoogleSSO) { - handlePopUpOpen("upgradePlan"); - return; - } - - await mutateAsync({ - orgId: currentOrg?.id, - googleSsoAuthEnforced: value - }); - } else if (type === EnforceAuthType.OIDC) { - if (!subscription?.oidcSSO) { - handlePopUpOpen("upgradePlan"); - return; - } - - await mutateAsync({ - orgId: currentOrg?.id, - authEnforced: value - }); - } else { - createNotification({ - text: `Invalid auth enforcement type ${type}`, - type: "error" - }); + if (type === EnforceAuthType.SAML) { + if (!subscription?.samlSSO) { + handlePopUpOpen("upgradePlan"); + return; } - createNotification({ - text: `Successfully ${value ? "enabled" : "disabled"} org-level auth`, - type: "success" + await mutateAsync({ + orgId: currentOrg?.id, + authEnforced: value }); - - if (value) { - await logout.mutateAsync(); - - if (type === EnforceAuthType.SAML) { - window.open(`/api/v1/sso/redirect/saml2/organizations/${currentOrg.slug}`); - } else if (type === EnforceAuthType.GOOGLE) { - window.open(`/api/v1/sso/redirect/google?org_slug=${currentOrg.slug}`); - } - - window.close(); + } else if (type === EnforceAuthType.GOOGLE) { + if (!subscription?.enforceGoogleSSO) { + handlePopUpOpen("upgradePlan"); + return; } - } catch (err) { - console.error(err); + + await mutateAsync({ + orgId: currentOrg?.id, + googleSsoAuthEnforced: value + }); + } else if (type === EnforceAuthType.OIDC) { + if (!subscription?.oidcSSO) { + handlePopUpOpen("upgradePlan"); + return; + } + + await mutateAsync({ + orgId: currentOrg?.id, + authEnforced: value + }); + } else { createNotification({ - text: (err as { response: { data: { message: string } } }).response.data.message, + text: `Invalid auth enforcement type ${type}`, type: "error" }); } + + createNotification({ + text: `Successfully ${value ? "enabled" : "disabled"} org-level auth`, + type: "success" + }); + + if (value) { + await logout.mutateAsync(); + + if (type === EnforceAuthType.SAML) { + window.open(`/api/v1/sso/redirect/saml2/organizations/${currentOrg.slug}`); + } else if (type === EnforceAuthType.GOOGLE) { + window.open(`/api/v1/sso/redirect/google?org_slug=${currentOrg.slug}`); + } + + window.close(); + } }; const handleEnableBypassOrgAuthToggle = async (value: boolean) => { diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgLDAPSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgLDAPSection.tsx index 9d94d595c..e4c4fec56 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgLDAPSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgLDAPSection.tsx @@ -31,31 +31,23 @@ export const OrgLDAPSection = (): JSX.Element => { const { mutateAsync: createMutateAsync } = useCreateLDAPConfig(); const handleLDAPToggle = async (value: boolean) => { - try { - if (!currentOrg?.id) return; - if (!subscription?.ldap) { - handlePopUpOpen("upgradePlan", { - isEnterpriseFeature: true - }); - return; - } - - await mutateAsync({ - organizationId: currentOrg?.id, - isActive: value - }); - - createNotification({ - text: `Successfully ${value ? "enabled" : "disabled"} LDAP`, - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to ${value ? "enable" : "disable"} LDAP`, - type: "error" + if (!currentOrg?.id) return; + if (!subscription?.ldap) { + handlePopUpOpen("upgradePlan", { + isEnterpriseFeature: true }); + return; } + + await mutateAsync({ + organizationId: currentOrg?.id, + isActive: value + }); + + createNotification({ + text: `Successfully ${value ? "enabled" : "disabled"} LDAP`, + type: "success" + }); }; const addLDAPBtnClick = async () => { diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgOIDCSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgOIDCSection.tsx index 7e4b2b3ef..35962349b 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgOIDCSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgOIDCSection.tsx @@ -30,49 +30,41 @@ export const OrgOIDCSection = (): JSX.Element => { ] as const); const handleOIDCToggle = async (value: boolean) => { - try { - if (!currentOrg?.id) return; + if (!currentOrg?.id) return; - if (!subscription?.oidcSSO) { - handlePopUpOpen("upgradePlan"); - return; - } - - await mutateAsync({ - organizationId: currentOrg?.id, - isActive: value - }); - - createNotification({ - text: `Successfully ${value ? "enabled" : "disabled"} OIDC SSO`, - type: "success" - }); - } catch (err) { - console.error(err); + if (!subscription?.oidcSSO) { + handlePopUpOpen("upgradePlan"); + return; } + + await mutateAsync({ + organizationId: currentOrg?.id, + isActive: value + }); + + createNotification({ + text: `Successfully ${value ? "enabled" : "disabled"} OIDC SSO`, + type: "success" + }); }; const handleOIDCGroupManagement = async (value: boolean) => { - try { - if (!currentOrg?.id) return; + if (!currentOrg?.id) return; - if (!subscription?.oidcSSO) { - handlePopUpOpen("upgradePlan"); - return; - } - - await mutateAsync({ - organizationId: currentOrg?.id, - manageGroupMemberships: value - }); - - createNotification({ - text: `Successfully ${value ? "enabled" : "disabled"} OIDC group membership mapping`, - type: "success" - }); - } catch (err) { - console.error(err); + if (!subscription?.oidcSSO) { + handlePopUpOpen("upgradePlan"); + return; } + + await mutateAsync({ + organizationId: currentOrg?.id, + manageGroupMemberships: value + }); + + createNotification({ + text: `Successfully ${value ? "enabled" : "disabled"} OIDC group membership mapping`, + type: "success" + }); }; const addOidcButtonClick = async () => { diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSSOSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSSOSection.tsx index 5e9d4c211..80ed79551 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSSOSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSSOSection.tsx @@ -34,63 +34,46 @@ export const OrgSSOSection = (): JSX.Element => { const { mutateAsync: createMutateAsync } = useCreateSSOConfig(); const handleSamlSSOToggle = async (value: boolean) => { - try { - if (!currentOrg?.id) return; + if (!currentOrg?.id) return; - if (!subscription?.samlSSO) { - handlePopUpOpen("upgradePlan", { - description: "You can use SAML SSO if you switch to Infisical's Pro plan." - }); - return; - } - - await mutateAsync({ - organizationId: currentOrg?.id, - isActive: value - }); - - createNotification({ - text: `Successfully ${value ? "enabled" : "disabled"} SAML SSO`, - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to ${value ? "enable" : "disable"} SAML SSO`, - type: "error" + if (!subscription?.samlSSO) { + handlePopUpOpen("upgradePlan", { + description: "You can use SAML SSO if you switch to Infisical's Pro plan." }); + return; } + + await mutateAsync({ + organizationId: currentOrg?.id, + isActive: value + }); + + createNotification({ + text: `Successfully ${value ? "enabled" : "disabled"} SAML SSO`, + type: "success" + }); }; const handleSamlGroupManagement = async (value: boolean) => { - try { - if (!currentOrg?.id) return; + if (!currentOrg?.id) return; - if (!subscription?.samlSSO || !subscription?.groups) { - handlePopUpOpen("upgradePlan", { - isEnterpriseFeature: true, - description: - "You can use SAML group mapping if you switch to Infisical's Enterprise plan." - }); - return; - } - - await mutateAsync({ - organizationId: currentOrg?.id, - enableGroupSync: value - }); - - createNotification({ - text: `Successfully ${value ? "enabled" : "disabled"} SAML group membership mapping`, - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to ${value ? "enable" : "disable"} SAML group membership mapping`, - type: "error" + if (!subscription?.samlSSO || !subscription?.groups) { + handlePopUpOpen("upgradePlan", { + isEnterpriseFeature: true, + description: "You can use SAML group mapping if you switch to Infisical's Enterprise plan." }); + return; } + + await mutateAsync({ + organizationId: currentOrg?.id, + enableGroupSync: value + }); + + createNotification({ + text: `Successfully ${value ? "enabled" : "disabled"} SAML group membership mapping`, + type: "success" + }); }; const addSSOBtnClick = async () => { diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/DeleteProjectTemplateModal.tsx b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/DeleteProjectTemplateModal.tsx index 8ccdb0191..33b54afca 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/DeleteProjectTemplateModal.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/DeleteProjectTemplateModal.tsx @@ -16,25 +16,16 @@ export const DeleteProjectTemplateModal = ({ isOpen, onOpenChange, template }: P const { id: templateId, name } = template; const handleDeleteProjectTemplate = async () => { - try { - await deleteTemplate.mutateAsync({ - templateId - }); + await deleteTemplate.mutateAsync({ + templateId + }); - createNotification({ - text: "Successfully removed project template", - type: "success" - }); + createNotification({ + text: "Successfully removed project template", + type: "success" + }); - onOpenChange(false); - } catch (err) { - console.error(err); - - createNotification({ - text: "Failed remove project template", - type: "error" - }); - } + onOpenChange(false); }; return ( diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/EditProjectTemplate.tsx b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/EditProjectTemplate.tsx index 7a99af1d6..afa8261b3 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/EditProjectTemplate.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/EditProjectTemplate.tsx @@ -31,22 +31,14 @@ export const EditProjectTemplate = ({ isInfisicalTemplate, projectTemplate, onBa const deleteProjectTemplate = useDeleteProjectTemplate(); const handleRemoveTemplate = async () => { - try { - await deleteProjectTemplate.mutateAsync({ - templateId - }); - createNotification({ - text: "Successfully removed project template", - type: "success" - }); - onBack(); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to remove project template", - type: "error" - }); - } + await deleteProjectTemplate.mutateAsync({ + templateId + }); + createNotification({ + text: "Successfully removed project template", + type: "success" + }); + onBack(); handlePopUpClose("removeTemplate"); }; diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEditRoleForm.tsx b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEditRoleForm.tsx index 09a2a69af..e54eaf19d 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEditRoleForm.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEditRoleForm.tsx @@ -57,31 +57,23 @@ export const ProjectTemplateEditRoleForm = ({ const updateProjectTemplate = useUpdateProjectTemplate(); const onSubmit = async (form: TFormSchema) => { - try { - await updateProjectTemplate.mutateAsync({ - templateId: projectTemplate.id, - roles: [ - ...projectTemplate.roles.filter( - (r) => r.slug !== role?.slug && isCustomProjectRole(r.slug) // filter out default roles as well - ), - { - ...form, - permissions: formRolePermission2API(form.permissions) - } - ] - }); - onGoBack(); - createNotification({ - text: "Template roles successfully updated", - type: "success" - }); - } catch (e: any) { - console.error(e); - createNotification({ - text: "Failed to update template roles", - type: "error" - }); - } + await updateProjectTemplate.mutateAsync({ + templateId: projectTemplate.id, + roles: [ + ...projectTemplate.roles.filter( + (r) => r.slug !== role?.slug && isCustomProjectRole(r.slug) // filter out default roles as well + ), + { + ...form, + permissions: formRolePermission2API(form.permissions) + } + ] + }); + onGoBack(); + createNotification({ + text: "Template roles successfully updated", + type: "success" + }); }; return ( diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEnvironmentsForm.tsx b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEnvironmentsForm.tsx index ec1899369..f11e9ecc8 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEnvironmentsForm.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEnvironmentsForm.tsx @@ -68,28 +68,20 @@ export const ProjectTemplateEnvironmentsForm = ({ const updateProjectTemplate = useUpdateProjectTemplate(); const onFormSubmit = async (form: TFormSchema) => { - try { - const { environments: updatedEnvs } = await updateProjectTemplate.mutateAsync({ - environments: form.environments?.map((env, index) => ({ - ...env, - position: index + 1 - })), - templateId: projectTemplate.id - }); + const { environments: updatedEnvs } = await updateProjectTemplate.mutateAsync({ + environments: form.environments?.map((env, index) => ({ + ...env, + position: index + 1 + })), + templateId: projectTemplate.id + }); - reset({ environments: updatedEnvs }); + reset({ environments: updatedEnvs }); - createNotification({ - text: "Project template updated successfully", - type: "success" - }); - } catch (e: any) { - console.error(e); - createNotification({ - text: e.message ?? "Failed to update project template", - type: "error" - }); - } + createNotification({ + text: "Project template updated successfully", + type: "success" + }); }; const isEnvironmentLimitExceeded = diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx index d65f7d785..cd5e41d2a 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx @@ -42,26 +42,18 @@ export const ProjectTemplateRolesSection = ({ projectTemplate, isInfisicalTempla const updateProjectTemplate = useUpdateProjectTemplate(); const handleRemoveRole = async (slug: string) => { - try { - await updateProjectTemplate.mutateAsync({ - templateId: projectTemplate.id, - roles: projectTemplate.roles.filter( - (role) => role.slug !== slug && isCustomProjectRole(role.slug) // filter out default roles as well - ) - }); + await updateProjectTemplate.mutateAsync({ + templateId: projectTemplate.id, + roles: projectTemplate.roles.filter( + (role) => role.slug !== slug && isCustomProjectRole(role.slug) // filter out default roles as well + ) + }); - createNotification({ - text: "Successfully removed role from template", - type: "success" - }); - handlePopUpClose("removeRole"); - } catch (e) { - console.error(e); - createNotification({ - text: "Error removing role from template", - type: "error" - }); - } + createNotification({ + text: "Successfully removed role from template", + type: "success" + }); + handlePopUpClose("removeRole"); }; const editRole = popUp?.editRole?.data as TProjectRole; diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplateDetailsModal.tsx b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplateDetailsModal.tsx index 6010d61f1..17224536d 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplateDetailsModal.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplateDetailsModal.tsx @@ -93,25 +93,15 @@ const ProjectTemplateForm = ({ onComplete, projectTemplate }: FormProps) => { ? updateProjectTemplate.mutateAsync({ templateId: projectTemplate.id, ...data }) : createProjectTemplate.mutateAsync({ ...data }); - try { - const template = await mutation; - createNotification({ - text: `Successfully ${ - projectTemplate ? "updated template details" : "created project template" - }`, - type: "success" - }); + const template = await mutation; + createNotification({ + text: `Successfully ${ + projectTemplate ? "updated template details" : "created project template" + }`, + type: "success" + }); - onComplete(template); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to ${ - projectTemplate ? "update template details" : "create project template" - }`, - type: "error" - }); - } + onComplete(template); }; return ( diff --git a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserOrgMembershipModal.tsx b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserOrgMembershipModal.tsx index 59d83749c..c7ebe8efe 100644 --- a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserOrgMembershipModal.tsx +++ b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserOrgMembershipModal.tsx @@ -87,34 +87,23 @@ export const UserOrgMembershipModal = ({ popUp, handlePopUpOpen, handlePopUpTogg }, [popUp?.orgMembership?.data, roles]); const onFormSubmit = async ({ role, metadata }: FormData) => { - try { - if (!orgId) return; + if (!orgId) return; - await updateOrgMembership({ - organizationId: orgId, - membershipId: popUpData.membershipId, - role: role.slug, - metadata - }); + await updateOrgMembership({ + organizationId: orgId, + membershipId: popUpData.membershipId, + role: role.slug, + metadata + }); - handlePopUpToggle("orgMembership", false); + handlePopUpToggle("orgMembership", false); - createNotification({ - text: "Successfully updated user organization role", - type: "success" - }); + createNotification({ + text: "Successfully updated user organization role", + type: "success" + }); - reset(); - } catch (err) { - console.error(err); - const error = err as any; - const text = error?.response?.data?.message ?? "Failed to update user organization role"; - - createNotification({ - text, - type: "error" - }); - } + reset(); }; return ( diff --git a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserProjectsSection/UserAddToProjectModal.tsx b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserProjectsSection/UserAddToProjectModal.tsx index 572be1d81..013358c5f 100644 --- a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserProjectsSection/UserAddToProjectModal.tsx +++ b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserProjectsSection/UserAddToProjectModal.tsx @@ -67,30 +67,19 @@ const UserAddToProjectModalChild = ({ membershipId, popUp, handlePopUpToggle }: }, [workspaces, projectMemberships]); const onFormSubmit = async ({ projectId }: FormData) => { - try { - await addUserToWorkspaceNonE2EE({ - projectId, - usernames: [popupData.username], - orgId - }); + await addUserToWorkspaceNonE2EE({ + projectId, + usernames: [popupData.username], + orgId + }); - createNotification({ - text: "Successfully added user to project", - type: "success" - }); + createNotification({ + text: "Successfully added user to project", + type: "success" + }); - reset(); - handlePopUpToggle("addUserToProject", false); - } catch (err) { - console.error(err); - const error = err as any; - const text = error?.response?.data?.message ?? "Failed to add identity to project"; - - createNotification({ - text, - type: "error" - }); - } + reset(); + handlePopUpToggle("addUserToProject", false); }; return ( diff --git a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserProjectsSection/UserGroupsSection.tsx b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserProjectsSection/UserGroupsSection.tsx index 8e7cf3ce8..fafa7aff3 100644 --- a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserProjectsSection/UserGroupsSection.tsx +++ b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserProjectsSection/UserGroupsSection.tsx @@ -20,25 +20,18 @@ export const UserGroupsSection = ({ orgMembership }: Props) => { const { mutateAsync: removeUserFromGroup } = useRemoveUserFromGroup(); const handleRemoveUserFromGroup = useCallback(async (groupId: string, groupSlug: string) => { - try { - await removeUserFromGroup({ - groupId, - slug: groupSlug, - username: orgMembership.user.username - }); + await removeUserFromGroup({ + groupId, + slug: groupSlug, + username: orgMembership.user.username + }); - createNotification({ - type: "success", - text: "User removed from group successfully" - }); + createNotification({ + type: "success", + text: "User removed from group successfully" + }); - handlePopUpClose("removeUserFromGroup"); - } catch { - createNotification({ - type: "error", - text: "Failed to remove user from group" - }); - } + handlePopUpClose("removeUserFromGroup"); }, []); return ( diff --git a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupModal.tsx b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupModal.tsx index a279bd1b0..f8788762c 100644 --- a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupModal.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupModal.tsx @@ -62,26 +62,19 @@ const Content = ({ popUp, handlePopUpToggle }: Props) => { }); const onFormSubmit = async ({ group, role }: FormData) => { - try { - await addGroupToWorkspaceMutateAsync({ - projectId: currentProject?.id || "", - groupId: group.id, - role: role.slug || undefined - }); + await addGroupToWorkspaceMutateAsync({ + projectId: currentProject?.id || "", + groupId: group.id, + role: role.slug || undefined + }); - reset(); - handlePopUpToggle("group", false); + reset(); + handlePopUpToggle("group", false); - createNotification({ - text: "Successfully added group to project", - type: "success" - }); - } catch { - createNotification({ - text: "Failed to add group to project", - type: "error" - }); - } + createNotification({ + text: "Successfully added group to project", + type: "success" + }); }; return filteredGroupMembershipOrgs.length ? ( diff --git a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsSection.tsx b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsSection.tsx index 115be60fe..bfac55bc2 100644 --- a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsSection.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsSection.tsx @@ -43,28 +43,17 @@ export const GroupsSection = () => { }; const onRemoveGroupSubmit = async (groupId: string) => { - try { - await deleteMutateAsync({ - groupId, - projectId: currentProject?.id || "" - }); + await deleteMutateAsync({ + groupId, + projectId: currentProject?.id || "" + }); - createNotification({ - text: "Successfully removed identity from project", - type: "success" - }); + createNotification({ + text: "Successfully removed identity from project", + type: "success" + }); - handlePopUpClose("deleteGroup"); - } catch (err) { - console.error(err); - const error = err as any; - const text = error?.response?.data?.message ?? "Failed to remove group from project"; - - createNotification({ - text, - type: "error" - }); - } + handlePopUpClose("deleteGroup"); }; return ( diff --git a/frontend/src/pages/project/AccessControlPage/components/IdentityTab/IdentityTab.tsx b/frontend/src/pages/project/AccessControlPage/components/IdentityTab/IdentityTab.tsx index 17dbdca27..48e01e2aa 100644 --- a/frontend/src/pages/project/AccessControlPage/components/IdentityTab/IdentityTab.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/IdentityTab/IdentityTab.tsx @@ -119,28 +119,17 @@ export const IdentityTab = withProjectPermission( ] as const); const onRemoveIdentitySubmit = async (identityId: string) => { - try { - await deleteMutateAsync({ - identityId, - projectId - }); + await deleteMutateAsync({ + identityId, + projectId + }); - createNotification({ - text: "Successfully removed identity from project", - type: "success" - }); + createNotification({ + text: "Successfully removed identity from project", + type: "success" + }); - handlePopUpClose("deleteIdentity"); - } catch (err) { - console.error(err); - const error = err as any; - const text = error?.response?.data?.message ?? "Failed to remove identity from project"; - - createNotification({ - text, - type: "error" - }); - } + handlePopUpClose("deleteIdentity"); }; const handleSort = (column: ProjectIdentityOrderBy) => { diff --git a/frontend/src/pages/project/AccessControlPage/components/IdentityTab/components/IdentityModal.tsx b/frontend/src/pages/project/AccessControlPage/components/IdentityTab/components/IdentityModal.tsx index 55f22c5ae..3274185b1 100644 --- a/frontend/src/pages/project/AccessControlPage/components/IdentityTab/components/IdentityModal.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/IdentityTab/components/IdentityModal.tsx @@ -104,40 +104,29 @@ const Content = ({ popUp, handlePopUpToggle }: Props) => { }); const onFormSubmit = async ({ identity, role }: FormData) => { - try { - await addIdentityToWorkspaceMutateAsync({ - projectId, - identityId: identity.id, - role: role.slug || undefined - }); + await addIdentityToWorkspaceMutateAsync({ + projectId, + identityId: identity.id, + role: role.slug || undefined + }); - createNotification({ - text: "Successfully added identity to project", - type: "success" - }); + createNotification({ + text: "Successfully added identity to project", + type: "success" + }); - const nextAvailableMembership = filteredIdentityMembershipOrgs.filter( - (membership) => membership.identity.id !== identity.id - )[0]; + const nextAvailableMembership = filteredIdentityMembershipOrgs.filter( + (membership) => membership.identity.id !== identity.id + )[0]; - // prevents combobox from displaying previously added identity - reset({ - identity: { - name: nextAvailableMembership?.identity.name, - id: nextAvailableMembership?.identity.id - } - }); - handlePopUpToggle("identity", false); - } catch (err) { - console.error(err); - const error = err as any; - const text = error?.response?.data?.message ?? "Failed to add identity to project"; - - createNotification({ - text, - type: "error" - }); - } + // prevents combobox from displaying previously added identity + reset({ + identity: { + name: nextAvailableMembership?.identity.name, + id: nextAvailableMembership?.identity.id + } + }); + handlePopUpToggle("identity", false); }; if (isMembershipsLoading || isRolesLoading) diff --git a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/AddMemberModal.tsx b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/AddMemberModal.tsx index 3477440fe..cb6934240 100644 --- a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/AddMemberModal.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/AddMemberModal.tsx @@ -110,65 +110,56 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => { if (!selectedMembers) return; - try { - if (currentProject.version === ProjectVersion.V1) { + if (currentProject.version === ProjectVersion.V1) { + createNotification({ + type: "error", + text: "Please upgrade your project to invite new members to the project." + }); + } else { + const inviteeEmails = selectedMembers + .map((member) => { + if (!member) return null; + + if (member.user.username) { + return member.user.username; + } + + if (member.user.email) { + return member.user.email; + } + + return null; + }) + .filter(Boolean) as string[]; + + if (inviteeEmails.length !== selectedMembers.length) { createNotification({ - type: "error", - text: "Please upgrade your project to invite new members to the project." + text: "Failed to add users to project. One or more users were invalid.", + type: "error" + }); + return; + } + + if (newInvitees.length) { + await addMemberToOrg({ + inviteeEmails: newInvitees, + organizationId: orgId, + organizationRoleSlug: ProjectMembershipRole.Member // only applies to new invites + }); + } + if (newInvitees.length || inviteeEmails.length) { + await addUserToProject({ + usernames: [...inviteeEmails, ...newInvitees], + orgId, + projectId: currentProject.id, + roleSlugs: projectRoleSlugs.map((role) => role.slug) }); - } else { - const inviteeEmails = selectedMembers - .map((member) => { - if (!member) return null; - - if (member.user.username) { - return member.user.username; - } - - if (member.user.email) { - return member.user.email; - } - - return null; - }) - .filter(Boolean) as string[]; - - if (inviteeEmails.length !== selectedMembers.length) { - createNotification({ - text: "Failed to add users to project. One or more users were invalid.", - type: "error" - }); - return; - } - - if (newInvitees.length) { - await addMemberToOrg({ - inviteeEmails: newInvitees, - organizationId: orgId, - organizationRoleSlug: ProjectMembershipRole.Member // only applies to new invites - }); - } - if (newInvitees.length || inviteeEmails.length) { - await addUserToProject({ - usernames: [...inviteeEmails, ...newInvitees], - orgId, - projectId: currentProject.id, - roleSlugs: projectRoleSlugs.map((role) => role.slug) - }); - } } - createNotification({ - text: "Successfully added user to the project", - type: "success" - }); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to add user to project", - type: "error" - }); - return; } + createNotification({ + text: "Successfully added user to the project", + type: "success" + }); handlePopUpToggle("addMember", false); reset(); }; diff --git a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection.tsx b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection.tsx index 33c0d1077..c3eb0c9aa 100644 --- a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -182,21 +182,14 @@ export const SpecificPrivilegeSecretForm = ({ } if (deleteUserPrivilege.isPending) return; - try { - await deleteUserPrivilege.mutateAsync({ - privilegeId: privilege.id, - projectMembershipId: privilege.projectMembershipId - }); - createNotification({ - type: "success", - text: "Successfully deleted privilege" - }); - } catch { - createNotification({ - type: "error", - text: "Failed to delete privilege" - }); - } + await deleteUserPrivilege.mutateAsync({ + privilegeId: privilege.id, + projectMembershipId: privilege.projectMembershipId + }); + createNotification({ + type: "success", + text: "Successfully deleted privilege" + }); }; // This is used for requesting access additional privileges, not directly creating a privilege! diff --git a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MembersSection.tsx b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MembersSection.tsx index 5904f12fb..7ec7bd4bc 100644 --- a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MembersSection.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MembersSection.tsx @@ -35,23 +35,15 @@ export const MembersSection = () => { if (!currentOrg?.id) return; if (!currentProject?.id) return; - try { - await removeUserFromWorkspace({ - projectId: currentProject.id, - usernames: [username], - orgId: currentOrg.id - }); - createNotification({ - text: "Successfully removed user from project", - type: "success" - }); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to remove user from the project", - type: "error" - }); - } + await removeUserFromWorkspace({ + projectId: currentProject.id, + usernames: [username], + orgId: currentOrg.id + }); + createNotification({ + text: "Successfully removed user from project", + type: "success" + }); handlePopUpClose("removeMember"); }; diff --git a/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx index 873422f3c..25d943f4a 100644 --- a/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx @@ -6,7 +6,6 @@ import { useTranslation } from "react-i18next"; import { faCheck, faCopy, faPlus, faTrashCan } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; -import { AxiosError } from "axios"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; @@ -110,45 +109,29 @@ const ServiceTokenForm = () => { }; const onFormSubmit = async ({ name, scopes, expiresIn, permissions }: FormData) => { - try { - if (!currentProject?.id) return; + if (!currentProject?.id) return; - const randomBytes = crypto.randomBytes(16).toString("hex"); + const randomBytes = crypto.randomBytes(16).toString("hex"); - const { serviceToken } = await createServiceToken.mutateAsync({ - encryptedKey: "", - iv: "", - tag: "", - scopes, - expiresIn: Number(expiresIn), - name, - workspaceId: currentProject.id, - randomBytes, - permissions: Object.entries(permissions) - .filter(([, permissionsValue]) => permissionsValue) - .map(([permissionsKey]) => permissionsKey) - }); + const { serviceToken } = await createServiceToken.mutateAsync({ + encryptedKey: "", + iv: "", + tag: "", + scopes, + expiresIn: Number(expiresIn), + name, + workspaceId: currentProject.id, + randomBytes, + permissions: Object.entries(permissions) + .filter(([, permissionsValue]) => permissionsValue) + .map(([permissionsKey]) => permissionsKey) + }); - setToken(serviceToken); - createNotification({ - text: "Successfully created a service token", - type: "success" - }); - } catch (err) { - console.error(err); - const axiosError = err as AxiosError; - if (axiosError?.response?.status === 401) { - createNotification({ - text: "You do not have access to the selected environment/path", - type: "error" - }); - } else { - createNotification({ - text: "Failed to create a service token", - type: "error" - }); - } - } + setToken(serviceToken); + createNotification({ + text: "Successfully created a service token", + type: "success" + }); }; return !hasServiceToken ? ( diff --git a/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/ServiceTokenSection.tsx b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/ServiceTokenSection.tsx index 64b042c35..6f2e82e02 100644 --- a/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/ServiceTokenSection.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/ServiceTokenSection.tsx @@ -28,23 +28,15 @@ export const ServiceTokenSection = withProjectPermission( ] as const); const onDeleteApproved = async () => { - try { - deleteServiceToken.mutateAsync( - (popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.id - ); - createNotification({ - text: "Successfully deleted service token", - type: "success" - }); + await deleteServiceToken.mutateAsync( + (popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.id + ); + createNotification({ + text: "Successfully deleted service token", + type: "success" + }); - handlePopUpClose("deleteAPITokenConfirmation"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete service token", - type: "error" - }); - } + handlePopUpClose("deleteAPITokenConfirmation"); }; return ( diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupDetailsSection.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupDetailsSection.tsx index cca6cabfd..368a740c2 100644 --- a/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupDetailsSection.tsx +++ b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupDetailsSection.tsx @@ -35,38 +35,27 @@ export const GroupDetailsSection = ({ groupMembership }: Props) => { const navigate = useNavigate(); const onRemoveGroupSubmit = async () => { - try { - await deleteMutateAsync({ - groupId: groupMembership.group.id, + await deleteMutateAsync({ + groupId: groupMembership.group.id, + projectId: currentProject.id + }); + + createNotification({ + text: "Successfully removed group from project", + type: "success" + }); + + navigate({ + to: `${getProjectBaseURL(currentProject.type)}/access-management`, + params: { projectId: currentProject.id - }); + }, + search: { + selectedTab: "groups" + } + }); - createNotification({ - text: "Successfully removed group from project", - type: "success" - }); - - navigate({ - to: `${getProjectBaseURL(currentProject.type)}/access-management`, - params: { - projectId: currentProject.id - }, - search: { - selectedTab: "groups" - } - }); - - handlePopUpClose("deleteGroup"); - } catch (err) { - console.error(err); - const error = err as any; - const text = error?.response?.data?.message ?? "Failed to remove group from project"; - - createNotification({ - text, - type: "error" - }); - } + handlePopUpClose("deleteGroup"); }; return ( diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx index e49e05022..65a2a5710 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx @@ -76,35 +76,24 @@ const Page = () => { }; const onRemoveIdentitySubmit = async () => { - try { - await deleteMutateAsync({ - identityId, + await deleteMutateAsync({ + identityId, + projectId + }); + createNotification({ + text: "Successfully removed identity from project", + type: "success" + }); + handlePopUpClose("deleteIdentity"); + navigate({ + to: `${getProjectBaseURL(currentProject.type)}/access-management` as const, + params: { projectId - }); - createNotification({ - text: "Successfully removed identity from project", - type: "success" - }); - handlePopUpClose("deleteIdentity"); - navigate({ - to: `${getProjectBaseURL(currentProject.type)}/access-management` as const, - params: { - projectId - }, - search: { - selectedTab: "identities" - } - }); - } catch (err) { - console.error(err); - const error = err as any; - const text = error?.response?.data?.message ?? "Failed to remove identity from project"; - - createNotification({ - text, - type: "error" - }); - } + }, + search: { + selectedTab: "identities" + } + }); }; if (isMembershipDetailsLoading) { diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx index 2b85291d0..b4b83bfb4 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx @@ -83,29 +83,21 @@ export const Page = () => { const handleRemoveUser = async () => { if (!currentOrg?.id || !currentProject?.id || !membershipDetails?.user?.username) return; - try { - await removeUserFromWorkspace({ - projectId, - usernames: [membershipDetails?.user?.username], - orgId: currentOrg.id - }); - createNotification({ - text: "Successfully removed user from project", - type: "success" - }); - navigate({ - to: `${getProjectBaseURL(currentProject.type)}/access-management` as const, - params: { - projectId: currentProject.id - } - }); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to remove user from the project", - type: "error" - }); - } + await removeUserFromWorkspace({ + projectId, + usernames: [membershipDetails?.user?.username], + orgId: currentOrg.id + }); + createNotification({ + text: "Successfully removed user from project", + type: "success" + }); + navigate({ + to: `${getProjectBaseURL(currentProject.type)}/access-management` as const, + params: { + projectId: currentProject.id + } + }); handlePopUpClose("removeMember"); }; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx index 9650b3fb8..9a87266c4 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx @@ -53,38 +53,27 @@ const Page = () => { ] as const); const onDeleteRoleSubmit = async () => { - try { - if (!currentProject?.slug || !data?.id) return; + if (!currentProject?.slug || !data?.id) return; - await deleteProjectRole({ - projectId, - id: data.id - }); + await deleteProjectRole({ + projectId, + id: data.id + }); - createNotification({ - text: "Successfully deleted project role", - type: "success" - }); - handlePopUpClose("deleteRole"); - navigate({ - to: `${getProjectBaseURL(currentProject.type)}/access-management` as const, - params: { - projectId - }, - search: { - selectedTab: ProjectAccessControlTabs.Roles - } - }); - } catch (err) { - console.error(err); - const error = err as any; - const text = error?.response?.data?.message ?? "Failed to delete project role"; - - createNotification({ - text, - type: "error" - }); - } + createNotification({ + text: "Successfully deleted project role", + type: "success" + }); + handlePopUpClose("deleteRole"); + navigate({ + to: `${getProjectBaseURL(currentProject.type)}/access-management` as const, + params: { + projectId + }, + search: { + selectedTab: ProjectAccessControlTabs.Roles + } + }); }; const isCustomRole = !Object.values(ProjectMembershipRole).includes( diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx index 43467876b..03525eac5 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx @@ -76,63 +76,54 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => { }, [role]); const onFormSubmit = async ({ name, description, slug }: FormData) => { - try { - if (!projectId) return; + if (!projectId) return; - if (role) { - // update - await updateProjectRole({ - id: role.id, - projectId, - name, - description, - slug - }); - - handlePopUpToggle("role", false); - if (slug) { - navigate({ - to: `${getProjectBaseURL(currentProject.type)}/roles/$roleSlug` as const, - params: { - roleSlug: slug, - projectId - } - }); - } - } else { - // create - const newRole = await createProjectRole({ - projectId, - name, - description, - slug, - permissions: [] - }); + if (role) { + // update + await updateProjectRole({ + id: role.id, + projectId, + name, + description, + slug + }); + handlePopUpToggle("role", false); + if (slug) { navigate({ to: `${getProjectBaseURL(currentProject.type)}/roles/$roleSlug` as const, params: { - roleSlug: newRole.slug, + roleSlug: slug, projectId } }); - handlePopUpToggle("role", false); } - - createNotification({ - text: `Successfully ${popUp?.role?.data ? "updated" : "created"} role`, - type: "success" + } else { + // create + const newRole = await createProjectRole({ + projectId, + name, + description, + slug, + permissions: [] }); - reset(); - } catch { - const text = `Failed to ${popUp?.role?.data ? "update" : "create"} role`; - - createNotification({ - text, - type: "error" + navigate({ + to: `${getProjectBaseURL(currentProject.type)}/roles/$roleSlug` as const, + params: { + roleSlug: newRole.slug, + projectId + } }); + handlePopUpToggle("role", false); } + + createNotification({ + text: `Successfully ${popUp?.role?.data ? "updated" : "created"} role`, + type: "success" + }); + + reset(); }; return ( diff --git a/frontend/src/pages/project/SettingsPage/components/DeleteProjectProtection/DeleteProjectProtection.tsx b/frontend/src/pages/project/SettingsPage/components/DeleteProjectProtection/DeleteProjectProtection.tsx index 7160e9adf..3cb0c9371 100644 --- a/frontend/src/pages/project/SettingsPage/components/DeleteProjectProtection/DeleteProjectProtection.tsx +++ b/frontend/src/pages/project/SettingsPage/components/DeleteProjectProtection/DeleteProjectProtection.tsx @@ -10,24 +10,16 @@ export const DeleteProjectProtection = () => { const { mutateAsync } = useUpdateProject(); const handleToggleDeleteProjectProtection = async (state: boolean) => { - try { - await mutateAsync({ - projectId, - hasDeleteProtection: state - }); + await mutateAsync({ + projectId, + hasDeleteProtection: state + }); - const text = `Successfully ${state ? "enabled" : "disabled"} delete protection`; - createNotification({ - text, - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to update delete protection", - type: "error" - }); - } + const text = `Successfully ${state ? "enabled" : "disabled"} delete protection`; + createNotification({ + text, + type: "success" + }); }; return ( diff --git a/frontend/src/pages/project/SettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx b/frontend/src/pages/project/SettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx index fb66f0a74..58db07484 100644 --- a/frontend/src/pages/project/SettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx +++ b/frontend/src/pages/project/SettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx @@ -68,12 +68,6 @@ export const DeleteProjectSection = () => { to: "/organization/projects" }); handlePopUpClose("deleteWorkspace"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete project", - type: "error" - }); } finally { setIsDeleting.off(); } @@ -118,12 +112,6 @@ export const DeleteProjectSection = () => { navigate({ to: "/organization/projects" }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to leave project", - type: "error" - }); } finally { setIsLeaving.off(); } From cc02d4d4d93e7748c6b894e5b55a8bbfa6bb7cfa Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Mon, 3 Nov 2025 22:43:05 +0530 Subject: [PATCH 017/294] fix: folder name sorting --- .../secret-folder/secret-folder-dal.ts | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/backend/src/services/secret-folder/secret-folder-dal.ts b/backend/src/services/secret-folder/secret-folder-dal.ts index 7dfeaddcf..c999b69d2 100644 --- a/backend/src/services/secret-folder/secret-folder-dal.ts +++ b/backend/src/services/secret-folder/secret-folder-dal.ts @@ -419,13 +419,18 @@ export const secretFolderDALFactory = (db: TDbClient) => { .select( selectAllTableCols(TableName.SecretFolder), db.raw( - `DENSE_RANK() OVER (ORDER BY ${TableName.SecretFolder}."name" ${ - orderDirection ?? OrderByDirection.ASC - }) as rank` + `DENSE_RANK() OVER (ORDER BY CASE WHEN ${TableName.SecretFolder}."name" LIKE '\\_%' THEN ${orderDirection === OrderByDirection.ASC ? "0" : "1"} ELSE ${orderDirection === OrderByDirection.ASC ? "1" : "0"} END, + LOWER(${TableName.SecretFolder}."name") ${orderDirection}, + ${TableName.SecretFolder}."name" ${orderDirection} + ) as rank` ), db.ref("slug").withSchema(TableName.Environment).as("environment") ) - .orderBy(`${TableName.SecretFolder}.${orderBy}`, orderDirection); + .orderByRaw( + `CASE WHEN ${TableName.SecretFolder}.${orderBy} LIKE '\\_%' THEN ${orderDirection === OrderByDirection.ASC ? "0" : "1"} ELSE ${orderDirection === OrderByDirection.ASC ? "1" : "0"} END` + ) + .orderByRaw(`LOWER(${TableName.SecretFolder}.${orderBy}) ${orderDirection}`) + .orderByRaw(`${TableName.SecretFolder}.${orderBy} ${orderDirection}`); if (limit) { const rankOffset = offset + 1; // ranks start from 1 @@ -434,7 +439,12 @@ export const secretFolderDALFactory = (db: TDbClient) => { .select("*") .from[number]>("w") .where("w.rank", ">=", rankOffset) - .andWhere("w.rank", "<", rankOffset + limit); + .andWhere("w.rank", "<", rankOffset + limit) + .orderByRaw( + `CASE WHEN "w"."${orderBy}" LIKE '\\_%' THEN ${orderDirection === OrderByDirection.ASC ? "0" : "1"} ELSE ${orderDirection === OrderByDirection.ASC ? "1" : "0"} END` + ) + .orderByRaw(`LOWER("w"."${orderBy}") ${orderDirection}`) + .orderByRaw(`"w"."${orderBy}" ${orderDirection}`); } const folders = await query; From c005f8a0fefdede8956e8fde3d0badd160a2dc82 Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Mon, 3 Nov 2025 15:11:10 -0300 Subject: [PATCH 018/294] refactor: streamline async logic and notification handling across various settings and integration components --- .../AuditLogsRetentionSection.tsx | 55 +-- .../components/ProjectAccessError.tsx | 20 +- .../components/ShareSecretForm.tsx | 68 ++- .../RollbackPreviewTab/RollbackPreviewTab.tsx | 21 +- .../IntegrationsDetailsByIDPage.tsx | 32 +- .../OverviewPage/OverviewPage.tsx | 164 +++---- .../CreateSecretForm/CreateSecretForm.tsx | 19 +- .../CreateSecretForm/CreateSecretForm.tsx | 93 ++-- .../SecretListView/SecretListView.tsx | 424 +++++++++--------- .../AutoCapitalizationSection.tsx | 28 +- .../EncryptionTab/EncryptionTab.tsx | 38 +- .../AddEnvironmentModal.tsx | 30 +- .../UpdateEnvironmentModal.tsx | 32 +- .../PointInTimeVersionLimitSection.tsx | 23 +- .../SecretSharingSection.tsx | 16 +- .../SecretTagsSection/AddSecretTagModal.tsx | 32 +- .../SecretTagsSection/SecretTagsSection.tsx | 26 +- .../BitbucketConfigurePage.tsx | 64 ++- .../CircleCIConfigurePage.tsx | 76 ++-- .../CloudflarePagesConfigurePage.tsx | 16 +- .../CloudflareWorkersConfigurePage.tsx | 16 +- .../DatabricksConfigurePage.tsx | 76 ++-- .../GithubConfigurePage.tsx | 16 +- .../HashicorpVaultAuthorizePage.tsx | 48 +- .../HashicorpVaultConfigurePage.tsx | 50 +-- .../OctopusDeployAuthorizePage.tsx | 39 +- .../OctopusDeployConfigurePage.tsx | 76 ++-- .../ChangeEmailSection/ChangeEmailSection.tsx | 30 +- .../ChangePasswordSection.tsx | 20 +- .../DeleteAccountSection.tsx | 22 +- .../components/SecuritySection/MFASection.tsx | 60 +-- .../SessionsSection/SessionsTable.tsx | 18 +- .../UserNameSection/UserNameSection.tsx | 22 +- 33 files changed, 709 insertions(+), 1061 deletions(-) diff --git a/frontend/src/pages/project/SettingsPage/components/AuditLogsRetentionSection/AuditLogsRetentionSection.tsx b/frontend/src/pages/project/SettingsPage/components/AuditLogsRetentionSection/AuditLogsRetentionSection.tsx index d9bbc80d8..1618c73aa 100644 --- a/frontend/src/pages/project/SettingsPage/components/AuditLogsRetentionSection/AuditLogsRetentionSection.tsx +++ b/frontend/src/pages/project/SettingsPage/components/AuditLogsRetentionSection/AuditLogsRetentionSection.tsx @@ -39,40 +39,33 @@ export const AuditLogsRetentionSection = () => { if (!currentProject) return null; const handleAuditLogsRetentionSubmit = async ({ auditLogsRetentionDays }: TForm) => { - try { - if (!subscription?.auditLogs) { - handlePopUpOpen("upgradePlan", { - description: - "You can only configure audit logs retention if you switch to Infisical's Pro plan." - }); - - return; - } - - if (subscription && auditLogsRetentionDays > subscription?.auditLogsRetentionDays) { - handlePopUpOpen("upgradePlan", { - description: - "To update your audit logs retention period to a higher value, switch to Infisical's Pro plan." - }); - - return; - } - - await updateAuditLogsRetention({ - auditLogsRetentionDays, - projectSlug: currentProject.slug + if (!subscription?.auditLogs) { + handlePopUpOpen("upgradePlan", { + description: + "You can only configure audit logs retention if you switch to Infisical's Pro plan." }); - createNotification({ - text: "Successfully updated audit logs retention period", - type: "success" - }); - } catch { - createNotification({ - text: "Failed updating audit logs retention period", - type: "error" - }); + return; } + + if (subscription && auditLogsRetentionDays > subscription?.auditLogsRetentionDays) { + handlePopUpOpen("upgradePlan", { + description: + "To update your audit logs retention period to a higher value, switch to Infisical's Pro plan." + }); + + return; + } + + await updateAuditLogsRetention({ + auditLogsRetentionDays, + projectSlug: currentProject.slug + }); + + createNotification({ + text: "Successfully updated audit logs retention period", + type: "success" + }); }; // render only for dedicated/self-hosted instances of Infisical diff --git a/frontend/src/pages/public/ErrorPage/components/ProjectAccessError.tsx b/frontend/src/pages/public/ErrorPage/components/ProjectAccessError.tsx index d6fd57d56..a07dd1674 100644 --- a/frontend/src/pages/public/ErrorPage/components/ProjectAccessError.tsx +++ b/frontend/src/pages/public/ErrorPage/components/ProjectAccessError.tsx @@ -2,7 +2,6 @@ import { faHome } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Link, useNavigate, useParams } from "@tanstack/react-router"; -import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; import { RequestProjectAccessModal } from "@app/components/projects"; import { AccessRestrictedBanner, Button } from "@app/components/v2"; @@ -35,19 +34,12 @@ export const ProjectAccessError = () => { const handleAccessProject = async () => { if (!project) return; - try { - await orgAdminAccessProject.mutateAsync({ - projectId: project.id - }); - await navigate({ - to: "." - }); - } catch { - createNotification({ - text: "Failed to access project", - type: "error" - }); - } + await orgAdminAccessProject.mutateAsync({ + projectId: project.id + }); + await navigate({ + to: "." + }); }; return ( diff --git a/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx b/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx index a34c08f9d..b8e93f0fe 100644 --- a/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx +++ b/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx @@ -131,52 +131,44 @@ export const ShareSecretForm = ({ emails, shouldLimitView }: FormData) => { - try { - const expiresAt = new Date(new Date().getTime() + Number(expiresIn)); + const expiresAt = new Date(new Date().getTime() + Number(expiresIn)); - const processedEmails = emails ? emails.split(",").map((e) => e.trim()) : undefined; + const processedEmails = emails ? emails.split(",").map((e) => e.trim()) : undefined; - const { id } = await createSharedSecret.mutateAsync({ - name, - password, - secretValue: secret, - expiresAt, - expiresAfterViews: shouldLimitView ? Number(viewLimit) : undefined, - accessType, - emails: processedEmails + const { id } = await createSharedSecret.mutateAsync({ + name, + password, + secretValue: secret, + expiresAt, + expiresAfterViews: shouldLimitView ? Number(viewLimit) : undefined, + accessType, + emails: processedEmails + }); + + if (processedEmails && processedEmails.length > 0) { + setSecretLink(""); + createNotification({ + text: `Shared secret link emailed to ${processedEmails.length} user(s).`, + type: "success" }); - - if (processedEmails && processedEmails.length > 0) { - setSecretLink(""); - createNotification({ - text: `Shared secret link emailed to ${processedEmails.length} user(s).`, - type: "success" - }); - } else { - const link = new URL(`${window.location.origin}/shared/secret/${id}`); - if (subOrganization) { - link.searchParams.set("subOrganization", subOrganization); - } - - setSecretLink(link.toString()); - - navigator.clipboard.writeText(link.toString()); - setCopyTextSecret("secret"); - - createNotification({ - text: "Shared secret link copied to clipboard.", - type: "success" - }); + } else { + const link = new URL(`${window.location.origin}/shared/secret/${id}`); + if (subOrganization) { + link.searchParams.set("subOrganization", subOrganization); } - reset(); - } catch (error) { - console.error(error); + setSecretLink(link.toString()); + + navigator.clipboard.writeText(link.toString()); + setCopyTextSecret("secret"); + createNotification({ - text: "Failed to create a shared secret.", - type: "error" + text: "Shared secret link copied to clipboard.", + type: "success" }); } + + reset(); }; if (secretLink === null) diff --git a/frontend/src/pages/secret-manager/CommitDetailsPage/components/RollbackPreviewTab/RollbackPreviewTab.tsx b/frontend/src/pages/secret-manager/CommitDetailsPage/components/RollbackPreviewTab/RollbackPreviewTab.tsx index 43992d73a..baf2b015b 100644 --- a/frontend/src/pages/secret-manager/CommitDetailsPage/components/RollbackPreviewTab/RollbackPreviewTab.tsx +++ b/frontend/src/pages/secret-manager/CommitDetailsPage/components/RollbackPreviewTab/RollbackPreviewTab.tsx @@ -140,22 +140,15 @@ export const RollbackPreviewTab = (): JSX.Element => { ); const handleRollback = async (): Promise => { - try { - await rollback(message); + await rollback(message); - createNotification({ - type: "success", - text: "Rollback completed successfully" - }); + createNotification({ + type: "success", + text: "Rollback completed successfully" + }); - handlePopUpClose("rollbackConfirm"); - goBackToHistory(); - } catch (error) { - createNotification({ - type: "error", - text: error instanceof Error ? error.message : "Failed to rollback changes" - }); - } + handlePopUpClose("rollbackConfirm"); + goBackToHistory(); }; const folderChanges: FolderChanges[] = rollbackChangesNested || []; diff --git a/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/IntegrationsDetailsByIDPage.tsx b/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/IntegrationsDetailsByIDPage.tsx index 3b5600665..9043c35ef 100644 --- a/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/IntegrationsDetailsByIDPage.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/IntegrationsDetailsByIDPage.tsx @@ -53,28 +53,20 @@ export const IntegrationDetailsByIDPage = () => { const navigate = useNavigate(); const handleIntegrationDelete = async (shouldDeleteIntegrationSecrets: boolean) => { - try { - await deleteIntegration({ - id: integrationId, - workspaceId: currentProject.id, - shouldDeleteIntegrationSecrets - }); + await deleteIntegration({ + id: integrationId, + workspaceId: currentProject.id, + shouldDeleteIntegrationSecrets + }); - createNotification({ - type: "success", - text: "Deleted integration" - }); + createNotification({ + type: "success", + text: "Deleted integration" + }); - await navigate({ - to: `/${ProjectType.SecretManager}/${projectId}/integrations` - }); - } catch (err) { - console.log(err); - createNotification({ - type: "error", - text: "Failed to delete integration" - }); - } + await navigate({ + to: `/${ProjectType.SecretManager}/${projectId}/integrations` + }); }; const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ diff --git a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx index 2dc370321..09fa1044d 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx @@ -488,55 +488,47 @@ export const OverviewPage = () => { }; const handleSecretCreate = async (env: string, key: string, value: string) => { - try { - // create folder if not existing - if (secretPath !== "/") { - // /hello/world -> [hello","world"] - const pathSegment = secretPath.split("/").filter(Boolean); - const parentPath = `/${pathSegment.slice(0, -1).join("/")}`; - const folderName = pathSegment.at(-1); - const canCreateFolder = permission.can( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.SecretFolders, { - environment: env, - secretPath: parentPath - }) - ); - if (folderName && parentPath && canCreateFolder) { - await getOrCreateFolder({ - projectId, - path: parentPath, - environment: env, - name: folderName - }); - } + // create folder if not existing + if (secretPath !== "/") { + // /hello/world -> [hello","world"] + const pathSegment = secretPath.split("/").filter(Boolean); + const parentPath = `/${pathSegment.slice(0, -1).join("/")}`; + const folderName = pathSegment.at(-1); + const canCreateFolder = permission.can( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.SecretFolders, { + environment: env, + secretPath: parentPath + }) + ); + if (folderName && parentPath && canCreateFolder) { + await getOrCreateFolder({ + projectId, + path: parentPath, + environment: env, + name: folderName + }); } - const result = await createSecretV3({ - environment: env, - projectId, - secretPath, - secretKey: key, - secretValue: value, - secretComment: "", - type: SecretType.Shared - }); + } + const result = await createSecretV3({ + environment: env, + projectId, + secretPath, + secretKey: key, + secretValue: value, + secretComment: "", + type: SecretType.Shared + }); - if ("approval" in result) { - createNotification({ - type: "info", - text: "Requested change has been sent for review" - }); - } else { - createNotification({ - type: "success", - text: "Successfully created secret" - }); - } - } catch (error) { - console.log(error); + if ("approval" in result) { createNotification({ - type: "error", - text: "Failed to create secret" + type: "info", + text: "Requested change has been sent for review" + }); + } else { + createNotification({ + type: "success", + text: "Successfully created secret" }); } }; @@ -565,63 +557,47 @@ export const OverviewPage = () => { secretValue = undefined; } - try { - const result = await updateSecretV3({ - environment: env, - projectId, - secretPath, - secretKey: key, - secretValue, - type - }); + const result = await updateSecretV3({ + environment: env, + projectId, + secretPath, + secretKey: key, + secretValue, + type + }); - if ("approval" in result) { - createNotification({ - type: "info", - text: "Requested change has been sent for review" - }); - } else { - createNotification({ - type: "success", - text: "Successfully updated secret" - }); - } - } catch (error) { - console.log(error); + if ("approval" in result) { createNotification({ - type: "error", - text: "Failed to update secret" + type: "info", + text: "Requested change has been sent for review" + }); + } else { + createNotification({ + type: "success", + text: "Successfully updated secret" }); } }; const handleSecretDelete = async (env: string, key: string, secretId?: string) => { - try { - const result = await deleteSecretV3({ - environment: env, - projectId, - secretPath, - secretKey: key, - secretId, - type: SecretType.Shared - }); + const result = await deleteSecretV3({ + environment: env, + projectId, + secretPath, + secretKey: key, + secretId, + type: SecretType.Shared + }); - if ("approval" in result) { - createNotification({ - type: "info", - text: "Requested change has been sent for review" - }); - } else { - createNotification({ - type: "success", - text: "Successfully deleted secret" - }); - } - } catch (error) { - console.log(error); + if ("approval" in result) { createNotification({ - type: "error", - text: "Failed to delete secret" + type: "info", + text: "Requested change has been sent for review" + }); + } else { + createNotification({ + type: "success", + text: "Successfully deleted secret" }); } }; diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx index fede4e44d..977943330 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx @@ -193,19 +193,12 @@ export const CreateSecretForm = ({ secretPath = "/", onClose }: Props) => { const slugSchema = z.string().trim().toLowerCase().min(1); const createNewTag = async (slug: string) => { // TODO: Replace with slugSchema generic - try { - const parsedSlug = slugSchema.parse(slug); - await createWsTag.mutateAsync({ - projectId, - tagSlug: parsedSlug, - tagColor: "" - }); - } catch { - createNotification({ - type: "error", - text: "Failed to create new tag" - }); - } + const parsedSlug = slugSchema.parse(slug); + await createWsTag.mutateAsync({ + projectId, + tagSlug: parsedSlug, + tagColor: "" + }); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/CreateSecretForm/CreateSecretForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/CreateSecretForm/CreateSecretForm.tsx index dd0561495..858fcde77 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/CreateSecretForm/CreateSecretForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/CreateSecretForm/CreateSecretForm.tsx @@ -77,70 +77,55 @@ export const CreateSecretForm = ({ const slugSchema = z.string().trim().toLowerCase().min(1); const createNewTag = async (slug: string) => { // TODO: Replace with slugSchema generic - try { - const parsedSlug = slugSchema.parse(slug); - await createWsTag.mutateAsync({ - projectId, - tagSlug: parsedSlug, - tagColor: "" - }); - } catch { - createNotification({ - type: "error", - text: "Failed to create new tag" - }); - } + const parsedSlug = slugSchema.parse(slug); + await createWsTag.mutateAsync({ + projectId, + tagSlug: parsedSlug, + tagColor: "" + }); }; const handleFormSubmit = async ({ key, value, tags }: TFormSchema) => { - try { - if (isBatchMode) { - const pendingSecretCreate: PendingSecretCreate = { - id: key, - type: PendingAction.Create, - secretKey: key, - secretValue: value || "", - secretComment: "", - tags: tags?.map((el) => ({ id: el.value, slug: el.label })), - timestamp: Date.now(), - resourceType: "secret" - }; - addPendingChange(pendingSecretCreate, { - projectId, - - environment, - secretPath - }); - closePopUp(PopUpNames.CreateSecretForm); - reset(); - return; - } - await createSecretV3({ - environment, - projectId, - secretPath, + if (isBatchMode) { + const pendingSecretCreate: PendingSecretCreate = { + id: key, + type: PendingAction.Create, secretKey: key, secretValue: value || "", secretComment: "", - type: SecretType.Shared, - tagIds: tags?.map((el) => el.value) + tags: tags?.map((el) => ({ id: el.value, slug: el.label })), + timestamp: Date.now(), + resourceType: "secret" + }; + addPendingChange(pendingSecretCreate, { + projectId, + + environment, + secretPath }); closePopUp(PopUpNames.CreateSecretForm); reset(); - - createNotification({ - type: isProtectedBranch ? "info" : "success", - text: isProtectedBranch - ? "Requested changes have been sent for review" - : "Successfully created secret" - }); - } catch (error) { - console.log(error); - createNotification({ - type: "error", - text: "Failed to create secret" - }); + return; } + await createSecretV3({ + environment, + projectId, + secretPath, + secretKey: key, + secretValue: value || "", + secretComment: "", + type: SecretType.Shared, + tagIds: tags?.map((el) => el.value) + }); + closePopUp(PopUpNames.CreateSecretForm); + reset(); + + createNotification({ + type: isProtectedBranch ? "info" : "success", + text: isProtectedBranch + ? "Requested changes have been sent for review" + : "Successfully created secret" + }); }; const handlePaste = (e: ClipboardEvent) => { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx index bd43e1271..be5dfaaed 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx @@ -293,174 +293,166 @@ export const SecretListView = ({ isSameTags && isSameRecipients; - try { - // personal secret change - let personalAction = false; - if (overrideAction === "deleted") { - await handleSecretOperation("delete", SecretType.Personal, oldKey, { - secretId: orgSecret.idOverride - }); - personalAction = true; - } else if (overrideAction && idOverride) { - await handleSecretOperation("update", SecretType.Personal, oldKey, { - value: valueOverride, - newKey: hasKeyChanged ? key : undefined, - secretId: orgSecret.idOverride, - skipMultilineEncoding: modSecret.skipMultilineEncoding - }); - personalAction = true; - } else if (overrideAction) { - await handleSecretOperation("create", SecretType.Personal, oldKey, { - value: valueOverride - }); - personalAction = true; - } + // personal secret change + let personalAction = false; + if (overrideAction === "deleted") { + await handleSecretOperation("delete", SecretType.Personal, oldKey, { + secretId: orgSecret.idOverride + }); + personalAction = true; + } else if (overrideAction && idOverride) { + await handleSecretOperation("update", SecretType.Personal, oldKey, { + value: valueOverride, + newKey: hasKeyChanged ? key : undefined, + secretId: orgSecret.idOverride, + skipMultilineEncoding: modSecret.skipMultilineEncoding + }); + personalAction = true; + } else if (overrideAction) { + await handleSecretOperation("create", SecretType.Personal, oldKey, { + value: valueOverride + }); + personalAction = true; + } - // shared secret change - if (!isSharedSecUnchanged && !personalAction) { - if (isBatchMode) { - const isEditingPendingCreation = isPending && pendingAction === PendingAction.Create; + // shared secret change + if (!isSharedSecUnchanged && !personalAction) { + if (isBatchMode) { + const isEditingPendingCreation = isPending && pendingAction === PendingAction.Create; - if (isEditingPendingCreation) { - const updatedCreate: PendingSecretCreate = { - id: orgSecret.id, - type: PendingAction.Create, - secretKey: key, - secretValue: value || "", - secretComment: comment || "", - skipMultilineEncoding: modSecret.skipMultilineEncoding || false, - tags: tags?.map((tag) => ({ id: tag.id, slug: tag.name || tag.slug || "" })) || [], - secretMetadata: secretMetadata || [], - timestamp: Date.now(), - resourceType: "secret", - originalKey: oldKey - }; + if (isEditingPendingCreation) { + const updatedCreate: PendingSecretCreate = { + id: orgSecret.id, + type: PendingAction.Create, + secretKey: key, + secretValue: value || "", + secretComment: comment || "", + skipMultilineEncoding: modSecret.skipMultilineEncoding || false, + tags: tags?.map((tag) => ({ id: tag.id, slug: tag.name || tag.slug || "" })) || [], + secretMetadata: secretMetadata || [], + timestamp: Date.now(), + resourceType: "secret", + originalKey: oldKey + }; - addPendingChange(updatedCreate, { - projectId, - environment, - secretPath - }); - } else { - const trueOriginalSecret = getTrueOriginalSecret( - orgSecret, - pendingChangesRef.current.secrets - ); + addPendingChange(updatedCreate, { + projectId, + environment, + secretPath + }); + } else { + const trueOriginalSecret = getTrueOriginalSecret( + orgSecret, + pendingChangesRef.current.secrets + ); - const updateChange: PendingSecretUpdate = { - id: orgSecret.id, - type: PendingAction.Update, - secretKey: trueOriginalSecret.key, - newSecretName: key, - originalValue: trueOriginalSecret.value, - secretValue: value, - originalComment: trueOriginalSecret.comment, - secretComment: comment, - originalSkipMultilineEncoding: trueOriginalSecret.skipMultilineEncoding, - skipMultilineEncoding: modSecret.skipMultilineEncoding, - originalTags: - trueOriginalSecret.tags?.map((tag) => ({ id: tag.id, slug: tag.slug })) || [], - tags: tags?.map((tag) => ({ id: tag.id, slug: tag.name || tag.slug || "" })) || [], - originalSecretMetadata: trueOriginalSecret.secretMetadata || [], - secretMetadata: secretMetadata || [], - timestamp: Date.now(), - resourceType: "secret", - existingSecret: orgSecret - }; + const updateChange: PendingSecretUpdate = { + id: orgSecret.id, + type: PendingAction.Update, + secretKey: trueOriginalSecret.key, + newSecretName: key, + originalValue: trueOriginalSecret.value, + secretValue: value, + originalComment: trueOriginalSecret.comment, + secretComment: comment, + originalSkipMultilineEncoding: trueOriginalSecret.skipMultilineEncoding, + skipMultilineEncoding: modSecret.skipMultilineEncoding, + originalTags: + trueOriginalSecret.tags?.map((tag) => ({ id: tag.id, slug: tag.slug })) || [], + tags: tags?.map((tag) => ({ id: tag.id, slug: tag.name || tag.slug || "" })) || [], + originalSecretMetadata: trueOriginalSecret.secretMetadata || [], + secretMetadata: secretMetadata || [], + timestamp: Date.now(), + resourceType: "secret", + existingSecret: orgSecret + }; - addPendingChange(updateChange, { - projectId, - environment, - secretPath - }); - } - - if (!isReminderEvent) { - handlePopUpClose("secretDetail"); - } - if (cb) cb(); - return; + addPendingChange(updateChange, { + projectId, + environment, + secretPath + }); } - await handleSecretOperation("update", SecretType.Shared, oldKey, { - value, - tags: tagIds, - comment, - reminderRepeatDays, - reminderNote, - reminderRecipients, - secretId: orgSecret.id, - newKey: hasKeyChanged ? key : undefined, - skipMultilineEncoding: modSecret.skipMultilineEncoding, - secretMetadata, - isRotatedSecret: orgSecret.isRotatedSecret, - secretValueHidden - }); + if (!isReminderEvent) { + handlePopUpClose("secretDetail"); + } if (cb) cb(); - } - queryClient.invalidateQueries({ - queryKey: dashboardKeys.getDashboardSecrets({ - projectId, - secretPath - }) - }); - queryClient.invalidateQueries({ - queryKey: secretKeys.getProjectSecret({ projectId, environment, secretPath }) - }); - queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.list({ - projectId, - environment, - directory: secretPath - }) - }); - queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.count({ - projectId, - environment, - directory: secretPath - }) - }); - queryClient.invalidateQueries({ - queryKey: commitKeys.count({ projectId, environment, directory: secretPath }) - }); - queryClient.invalidateQueries({ - queryKey: commitKeys.history({ - projectId, - environment, - directory: secretPath - }) - }); - queryClient.invalidateQueries({ - queryKey: secretApprovalRequestKeys.count({ projectId }) - }); - if (!isReminderEvent) { - handlePopUpClose("secretDetail"); + return; } - let successMessage; - if (isReminderEvent) { - successMessage = reminderRepeatDays - ? "Successfully saved secret reminder" - : "Successfully deleted secret reminder"; - } else { - successMessage = "Successfully saved secrets"; - } - - createNotification({ - type: isProtectedBranch && !personalAction ? "info" : "success", - text: - isProtectedBranch && !personalAction - ? "Requested changes have been sent for review" - : successMessage - }); - } catch (error) { - console.log(error); - createNotification({ - type: "error", - text: "Failed to save secret" + await handleSecretOperation("update", SecretType.Shared, oldKey, { + value, + tags: tagIds, + comment, + reminderRepeatDays, + reminderNote, + reminderRecipients, + secretId: orgSecret.id, + newKey: hasKeyChanged ? key : undefined, + skipMultilineEncoding: modSecret.skipMultilineEncoding, + secretMetadata, + isRotatedSecret: orgSecret.isRotatedSecret, + secretValueHidden }); + if (cb) cb(); } + queryClient.invalidateQueries({ + queryKey: dashboardKeys.getDashboardSecrets({ + projectId, + secretPath + }) + }); + queryClient.invalidateQueries({ + queryKey: secretKeys.getProjectSecret({ projectId, environment, secretPath }) + }); + queryClient.invalidateQueries({ + queryKey: secretSnapshotKeys.list({ + projectId, + environment, + directory: secretPath + }) + }); + queryClient.invalidateQueries({ + queryKey: secretSnapshotKeys.count({ + projectId, + environment, + directory: secretPath + }) + }); + queryClient.invalidateQueries({ + queryKey: commitKeys.count({ projectId, environment, directory: secretPath }) + }); + queryClient.invalidateQueries({ + queryKey: commitKeys.history({ + projectId, + environment, + directory: secretPath + }) + }); + queryClient.invalidateQueries({ + queryKey: secretApprovalRequestKeys.count({ projectId }) + }); + if (!isReminderEvent) { + handlePopUpClose("secretDetail"); + } + + let successMessage; + if (isReminderEvent) { + successMessage = reminderRepeatDays + ? "Successfully saved secret reminder" + : "Successfully deleted secret reminder"; + } else { + successMessage = "Successfully saved secrets"; + } + + createNotification({ + type: isProtectedBranch && !personalAction ? "info" : "success", + text: + isProtectedBranch && !personalAction + ? "Requested changes have been sent for review" + : successMessage + }); }, [environment, secretPath, isProtectedBranch, isBatchMode, projectId, addPendingChange] ); @@ -488,75 +480,67 @@ export const SecretListView = ({ value, secretValueHidden } = popUp.deleteSecret?.data as SecretV3RawSanitized; - try { - if (isBatchMode) { - const deleteChange: PendingSecretDelete = { - id: `${secretId}`, - type: PendingAction.Delete, - secretKey: key, - secretValue: value || "", - timestamp: Date.now(), - resourceType: "secret", - secretValueHidden - }; + if (isBatchMode) { + const deleteChange: PendingSecretDelete = { + id: `${secretId}`, + type: PendingAction.Delete, + secretKey: key, + secretValue: value || "", + timestamp: Date.now(), + resourceType: "secret", + secretValueHidden + }; - addPendingChange(deleteChange, { - projectId, - environment, - secretPath - }); + addPendingChange(deleteChange, { + projectId, + environment, + secretPath + }); - handlePopUpClose("deleteSecret"); - handlePopUpClose("secretDetail"); - return; - } - - await handleSecretOperation("delete", SecretType.Shared, key, { secretId }); - // wrap this in another function and then reuse - queryClient.invalidateQueries({ - queryKey: dashboardKeys.getDashboardSecrets({ projectId, secretPath }) - }); - queryClient.invalidateQueries({ - queryKey: secretKeys.getProjectSecret({ projectId, environment, secretPath }) - }); - queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.list({ - projectId, - environment, - directory: secretPath - }) - }); - queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.count({ - projectId, - environment, - directory: secretPath - }) - }); - queryClient.invalidateQueries({ - queryKey: commitKeys.count({ projectId, environment, directory: secretPath }) - }); - queryClient.invalidateQueries({ - queryKey: commitKeys.history({ projectId, environment, directory: secretPath }) - }); - queryClient.invalidateQueries({ - queryKey: secretApprovalRequestKeys.count({ projectId }) - }); handlePopUpClose("deleteSecret"); handlePopUpClose("secretDetail"); - createNotification({ - type: isProtectedBranch ? "info" : "success", - text: isProtectedBranch - ? "Requested changes have been sent for review" - : "Successfully deleted secret" - }); - } catch (error) { - console.log(error); - createNotification({ - type: "error", - text: "Failed to delete secret" - }); + return; } + + await handleSecretOperation("delete", SecretType.Shared, key, { secretId }); + // wrap this in another function and then reuse + queryClient.invalidateQueries({ + queryKey: dashboardKeys.getDashboardSecrets({ projectId, secretPath }) + }); + queryClient.invalidateQueries({ + queryKey: secretKeys.getProjectSecret({ projectId, environment, secretPath }) + }); + queryClient.invalidateQueries({ + queryKey: secretSnapshotKeys.list({ + projectId, + environment, + directory: secretPath + }) + }); + queryClient.invalidateQueries({ + queryKey: secretSnapshotKeys.count({ + projectId, + environment, + directory: secretPath + }) + }); + queryClient.invalidateQueries({ + queryKey: commitKeys.count({ projectId, environment, directory: secretPath }) + }); + queryClient.invalidateQueries({ + queryKey: commitKeys.history({ projectId, environment, directory: secretPath }) + }); + queryClient.invalidateQueries({ + queryKey: secretApprovalRequestKeys.count({ projectId }) + }); + handlePopUpClose("deleteSecret"); + handlePopUpClose("secretDetail"); + createNotification({ + type: isProtectedBranch ? "info" : "success", + text: isProtectedBranch + ? "Requested changes have been sent for review" + : "Successfully deleted secret" + }); }, [ (popUp.deleteSecret?.data as SecretV3RawSanitized)?.key, environment, diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/AutoCapitalizationSection/AutoCapitalizationSection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/AutoCapitalizationSection/AutoCapitalizationSection.tsx index 73bec81b9..7c9d03904 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/AutoCapitalizationSection/AutoCapitalizationSection.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/AutoCapitalizationSection/AutoCapitalizationSection.tsx @@ -13,26 +13,18 @@ export const AutoCapitalizationSection = () => { const { mutateAsync } = useUpdateProject(); const handleToggleCapitalizationToggle = async (state: boolean) => { - try { - if (!currentProject?.id) return; + if (!currentProject?.id) return; - await mutateAsync({ - projectId: currentProject.id, - autoCapitalization: state - }); + await mutateAsync({ + projectId: currentProject.id, + autoCapitalization: state + }); - const text = `Successfully ${state ? "enabled" : "disabled"} auto capitalization`; - createNotification({ - text, - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to update auto capitalization", - type: "error" - }); - } + const text = `Successfully ${state ? "enabled" : "disabled"} auto capitalization`; + createNotification({ + text, + type: "success" + }); }; return ( diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/EncryptionTab/EncryptionTab.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/EncryptionTab/EncryptionTab.tsx index 9e4627d26..e22719c83 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/EncryptionTab/EncryptionTab.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/EncryptionTab/EncryptionTab.tsx @@ -124,17 +124,13 @@ const LoadBackupModal = ({ return; } - try { - await loadKmsBackup(backupContent); - createNotification({ - text: "Successfully loaded KMS backup", - type: "success" - }); + await loadKmsBackup(backupContent); + createNotification({ + text: "Successfully loaded KMS backup", + type: "success" + }); - onOpenChange(false); - } catch (err) { - console.error(err); - } + onOpenChange(false); }; const parseFile = (file?: File) => { @@ -245,20 +241,16 @@ export const EncryptionTab = () => { }); const onUpdateProjectKms = async (data: TForm) => { - try { - await updateProjectKms( - data.kmsKeyId === INTERNAL_KMS_KEY_ID - ? { type: KmsType.Internal } - : { type: KmsType.External, kmsId: data.kmsKeyId } - ); + await updateProjectKms( + data.kmsKeyId === INTERNAL_KMS_KEY_ID + ? { type: KmsType.Internal } + : { type: KmsType.External, kmsId: data.kmsKeyId } + ); - createNotification({ - text: "Successfully updated project KMS", - type: "success" - }); - } catch (err) { - console.error(err); - } + createNotification({ + text: "Successfully updated project KMS", + type: "success" + }); }; return ( diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/AddEnvironmentModal.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/AddEnvironmentModal.tsx index 611504a14..70c486616 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/AddEnvironmentModal.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/AddEnvironmentModal.tsx @@ -36,28 +36,20 @@ const Content = ({ onComplete }: ContentProps) => { }); const onFormSubmit = async ({ environmentName, environmentSlug }: FormData) => { - try { - if (!currentProject?.id) return; + if (!currentProject?.id) return; - const env = await mutateAsync({ - projectId: currentProject.id, - name: environmentName, - slug: environmentSlug - }); + const env = await mutateAsync({ + projectId: currentProject.id, + name: environmentName, + slug: environmentSlug + }); - createNotification({ - text: "Successfully created environment", - type: "success" - }); + createNotification({ + text: "Successfully created environment", + type: "success" + }); - onComplete(env); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to create environment", - type: "error" - }); - } + onComplete(env); }; return ( diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/UpdateEnvironmentModal.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/UpdateEnvironmentModal.tsx index e2d64ee72..1c9d09101 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/UpdateEnvironmentModal.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/UpdateEnvironmentModal.tsx @@ -33,29 +33,21 @@ export const UpdateEnvironmentModal = ({ popUp, handlePopUpClose, handlePopUpTog const oldEnvId = (popUp?.updateEnv?.data as { id: string })?.id; const onFormSubmit = async ({ name, slug }: FormData) => { - try { - if (!currentProject?.id) return; + if (!currentProject?.id) return; - await mutateAsync({ - projectId: currentProject.id, - name, - slug, - id: oldEnvId - }); + await mutateAsync({ + projectId: currentProject.id, + name, + slug, + id: oldEnvId + }); - createNotification({ - text: "Successfully updated environment", - type: "success" - }); + createNotification({ + text: "Successfully updated environment", + type: "success" + }); - handlePopUpClose("updateEnv"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to update environment", - type: "error" - }); - } + handlePopUpClose("updateEnv"); }; return ( diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/PointInTimeVersionLimitSection/PointInTimeVersionLimitSection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/PointInTimeVersionLimitSection/PointInTimeVersionLimitSection.tsx index e647ff481..6345301a0 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/PointInTimeVersionLimitSection/PointInTimeVersionLimitSection.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/PointInTimeVersionLimitSection/PointInTimeVersionLimitSection.tsx @@ -34,22 +34,15 @@ export const PointInTimeVersionLimitSection = () => { if (!currentProject) return null; const handleVersionLimitSubmit = async ({ pitVersionLimit }: TForm) => { - try { - await updateProject({ - pitVersionLimit, - projectId - }); + await updateProject({ + pitVersionLimit, + projectId + }); - createNotification({ - text: "Successfully updated version limit", - type: "success" - }); - } catch { - createNotification({ - text: "Failed updating project's version limit", - type: "error" - }); - } + createNotification({ + text: "Successfully updated version limit", + type: "success" + }); }; const isAdmin = hasProjectRole(ProjectMembershipRole.Admin); diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/SecretSharingSection/SecretSharingSection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/SecretSharingSection/SecretSharingSection.tsx index bd8928823..ac1f735f9 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/SecretSharingSection/SecretSharingSection.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/SecretSharingSection/SecretSharingSection.tsx @@ -15,12 +15,12 @@ export const SecretSharingSection = () => { const handleToggle = async (state: boolean) => { setIsLoading(true); - try { - if (!currentProject?.id) { - setIsLoading(false); - return; - } + if (!currentProject?.id) { + setIsLoading(false); + return; + } + try { await updateProject({ projectId: currentProject.id, secretSharing: state @@ -30,12 +30,6 @@ export const SecretSharingSection = () => { text: `Successfully ${state ? "enabled" : "disabled"} secret sharing for this project`, type: "success" }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to update secret sharing for this project", - type: "error" - }); } finally { setIsLoading(false); } diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/SecretTagsSection/AddSecretTagModal.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/SecretTagsSection/AddSecretTagModal.tsx index 1d4c8e492..cfe7dff8e 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/SecretTagsSection/AddSecretTagModal.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/SecretTagsSection/AddSecretTagModal.tsx @@ -39,29 +39,21 @@ export const AddSecretTagModal = ({ popUp, handlePopUpClose, handlePopUpToggle } }); const onFormSubmit = async ({ slug }: FormData) => { - try { - if (!currentProject?.id) return; + if (!currentProject?.id) return; - await createWsTag.mutateAsync({ - projectId: currentProject?.id, - tagSlug: slug, - tagColor: "" - }); + await createWsTag.mutateAsync({ + projectId: currentProject?.id, + tagSlug: slug, + tagColor: "" + }); - handlePopUpClose("CreateSecretTag"); + handlePopUpClose("CreateSecretTag"); - createNotification({ - text: "Successfully created a tag", - type: "success" - }); - reset(); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to create a tag", - type: "error" - }); - } + createNotification({ + text: "Successfully created a tag", + type: "success" + }); + reset(); }; return ( diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/SecretTagsSection/SecretTagsSection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/SecretTagsSection/SecretTagsSection.tsx index 508ef2867..2964467ad 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/SecretTagsSection/SecretTagsSection.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/SecretTagsSection/SecretTagsSection.tsx @@ -29,25 +29,17 @@ export const SecretTagsSection = (): JSX.Element => { const deleteWsTag = useDeleteWsTag(); const onDeleteApproved = async () => { - try { - await deleteWsTag.mutateAsync({ - projectId: currentProject?.id || "", - tagID: (popUp?.deleteTagConfirmation?.data as DeleteModalData)?.id - }); + await deleteWsTag.mutateAsync({ + projectId: currentProject?.id || "", + tagID: (popUp?.deleteTagConfirmation?.data as DeleteModalData)?.id + }); - createNotification({ - text: "Successfully deleted tag", - type: "success" - }); + createNotification({ + text: "Successfully deleted tag", + type: "success" + }); - handlePopUpClose("deleteTagConfirmation"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete the tag", - type: "error" - }); - } + handlePopUpClose("deleteTagConfirmation"); }; return ( diff --git a/frontend/src/pages/secret-manager/integrations/BitbucketConfigurePage/BitbucketConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/BitbucketConfigurePage/BitbucketConfigurePage.tsx index 8d855f284..b66644bdc 100644 --- a/frontend/src/pages/secret-manager/integrations/BitbucketConfigurePage/BitbucketConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/BitbucketConfigurePage/BitbucketConfigurePage.tsx @@ -126,43 +126,35 @@ export const BitbucketConfigurePage = () => { }: TFormData) => { if (!targetRepo || !targetWorkspace) return; - try { - await createIntegration.mutateAsync({ - integrationAuthId, - isActive: true, - app: targetRepo.name, - appId: targetRepo.appId, - sourceEnvironment: sourceEnvironment.slug, - targetEnvironment: targetWorkspace.name, - targetEnvironmentId: targetWorkspace.slug, - ...(scope.value === BitbucketScope.Env && - targetEnvironment && { - targetService: targetEnvironment.name, - targetServiceId: targetEnvironment.uuid - }), - secretPath - }); + await createIntegration.mutateAsync({ + integrationAuthId, + isActive: true, + app: targetRepo.name, + appId: targetRepo.appId, + sourceEnvironment: sourceEnvironment.slug, + targetEnvironment: targetWorkspace.name, + targetEnvironmentId: targetWorkspace.slug, + ...(scope.value === BitbucketScope.Env && + targetEnvironment && { + targetService: targetEnvironment.name, + targetServiceId: targetEnvironment.uuid + }), + secretPath + }); - createNotification({ - type: "success", - text: "Successfully created integration" - }); - navigate({ - to: "/projects/secret-management/$projectId/integrations", - params: { - projectId: currentProject.id - }, - search: { - selectedTab: IntegrationsListPageTabs.NativeIntegrations - } - }); - } catch (err) { - createNotification({ - type: "error", - text: "Failed to create integration" - }); - console.error(err); - } + createNotification({ + type: "success", + text: "Successfully created integration" + }); + navigate({ + to: "/projects/secret-management/$projectId/integrations", + params: { + projectId: currentProject.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } + }); }; useEffect(() => { diff --git a/frontend/src/pages/secret-manager/integrations/CircleCIConfigurePage/CircleCIConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/CircleCIConfigurePage/CircleCIConfigurePage.tsx index 7df2ae35c..a7ecbca6a 100644 --- a/frontend/src/pages/secret-manager/integrations/CircleCIConfigurePage/CircleCIConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/CircleCIConfigurePage/CircleCIConfigurePage.tsx @@ -73,51 +73,43 @@ export const CircleCIConfigurePage = () => { : undefined; const onSubmit = async (data: TFormData) => { - try { - if (data.scope === CircleCiScope.Context) { - await mutateAsync({ - scope: data.scope, - integrationAuthId, - isActive: true, - sourceEnvironment: data.sourceEnvironment.slug, - app: data.targetContext.name, - appId: data.targetContext.id, - owner: data.targetOrg.name, - secretPath: data.secretPath - }); - } else { - await mutateAsync({ - scope: data.scope, - integrationAuthId, - isActive: true, - app: data.targetProject.name, // project name - owner: data.targetOrg.name, // organization name - appId: data.targetProject.id, // project id (used for syncing) - sourceEnvironment: data.sourceEnvironment.slug, - secretPath: data.secretPath - }); - } - - createNotification({ - type: "success", - text: "Successfully created integration" + if (data.scope === CircleCiScope.Context) { + await mutateAsync({ + scope: data.scope, + integrationAuthId, + isActive: true, + sourceEnvironment: data.sourceEnvironment.slug, + app: data.targetContext.name, + appId: data.targetContext.id, + owner: data.targetOrg.name, + secretPath: data.secretPath }); - navigate({ - to: "/projects/secret-management/$projectId/integrations", - params: { - projectId: currentProject.id - }, - search: { - selectedTab: IntegrationsListPageTabs.NativeIntegrations - } + } else { + await mutateAsync({ + scope: data.scope, + integrationAuthId, + isActive: true, + app: data.targetProject.name, // project name + owner: data.targetOrg.name, // organization name + appId: data.targetProject.id, // project id (used for syncing) + sourceEnvironment: data.sourceEnvironment.slug, + secretPath: data.secretPath }); - } catch (err) { - createNotification({ - type: "error", - text: "Failed to create integration" - }); - console.error(err); } + + createNotification({ + type: "success", + text: "Successfully created integration" + }); + navigate({ + to: "/projects/secret-management/$projectId/integrations", + params: { + projectId: currentProject.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } + }); }; if (isCircleCIOrganizationsLoading) diff --git a/frontend/src/pages/secret-manager/integrations/CloudflarePagesConfigurePage/CloudflarePagesConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/CloudflarePagesConfigurePage/CloudflarePagesConfigurePage.tsx index 0a0d08863..3948351c3 100644 --- a/frontend/src/pages/secret-manager/integrations/CloudflarePagesConfigurePage/CloudflarePagesConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/CloudflarePagesConfigurePage/CloudflarePagesConfigurePage.tsx @@ -1,8 +1,6 @@ import { useEffect, useState } from "react"; import { useNavigate, useSearch } from "@tanstack/react-router"; -import axios from "axios"; -import { createNotification } from "@app/components/notifications"; import { Button, Card, @@ -102,19 +100,7 @@ export const CloudflarePagesConfigurePage = () => { selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); - } catch (err) { - console.error(err); - - let errorMessage: string = "Something went wrong!"; - if (axios.isAxiosError(err)) { - const { message } = err?.response?.data as { message: string }; - errorMessage = message; - } - - createNotification({ - text: errorMessage, - type: "error" - }); + } catch { setIsLoading(false); } }; diff --git a/frontend/src/pages/secret-manager/integrations/CloudflareWorkersConfigurePage/CloudflareWorkersConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/CloudflareWorkersConfigurePage/CloudflareWorkersConfigurePage.tsx index 58bc8596b..417f0fe84 100644 --- a/frontend/src/pages/secret-manager/integrations/CloudflareWorkersConfigurePage/CloudflareWorkersConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/CloudflareWorkersConfigurePage/CloudflareWorkersConfigurePage.tsx @@ -1,8 +1,6 @@ import { useEffect, useState } from "react"; import { useNavigate, useSearch } from "@tanstack/react-router"; -import axios from "axios"; -import { createNotification } from "@app/components/notifications"; import { Button, Card, CardTitle, FormControl, Select, SelectItem } from "@app/components/v2"; import { SecretPathInput } from "@app/components/v2/SecretPathInput"; import { ROUTE_PATHS } from "@app/const/routes"; @@ -75,19 +73,7 @@ export const CloudflareWorkersConfigurePage = () => { selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); - } catch (err) { - console.error(err); - - let errorMessage: string = "Something went wrong!"; - if (axios.isAxiosError(err)) { - const { message } = err?.response?.data as { message: string }; - errorMessage = message; - } - - createNotification({ - text: errorMessage, - type: "error" - }); + } catch { setIsLoading(false); } }; diff --git a/frontend/src/pages/secret-manager/integrations/DatabricksConfigurePage/DatabricksConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/DatabricksConfigurePage/DatabricksConfigurePage.tsx index 1f5a3c40a..05c943fc5 100644 --- a/frontend/src/pages/secret-manager/integrations/DatabricksConfigurePage/DatabricksConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/DatabricksConfigurePage/DatabricksConfigurePage.tsx @@ -55,49 +55,45 @@ export const DatabricksConfigurePage = () => { const [secretPath, setSecretPath] = useState("/"); const handleButtonClick = async () => { - try { - if (!integrationAuth?.id) return; + if (!integrationAuth?.id) return; - if (!targetScope) { - createNotification({ - type: "error", - text: "Please select a scope" - }); - return; - } - - const selectedScope = integrationAuthScopes?.find( - (integrationAuthScope) => integrationAuthScope.name === targetScope - ); - - if (!selectedScope) { - createNotification({ - type: "error", - text: "Invalid scope selected" - }); - return; - } - - await mutateAsync({ - integrationAuthId: integrationAuth?.id, - isActive: true, - app: selectedScope.name, // scope name - sourceEnvironment: selectedSourceEnvironment, - secretPath + if (!targetScope) { + createNotification({ + type: "error", + text: "Please select a scope" }); - - navigate({ - to: "/projects/secret-management/$projectId/integrations", - params: { - projectId: currentProject.id - }, - search: { - selectedTab: IntegrationsListPageTabs.NativeIntegrations - } - }); - } catch (err) { - console.error(err); + return; } + + const selectedScope = integrationAuthScopes?.find( + (integrationAuthScope) => integrationAuthScope.name === targetScope + ); + + if (!selectedScope) { + createNotification({ + type: "error", + text: "Invalid scope selected" + }); + return; + } + + await mutateAsync({ + integrationAuthId: integrationAuth?.id, + isActive: true, + app: selectedScope.name, // scope name + sourceEnvironment: selectedSourceEnvironment, + secretPath + }); + + navigate({ + to: "/projects/secret-management/$projectId/integrations", + params: { + projectId: currentProject.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } + }); }; return integrationAuth && selectedSourceEnvironment && integrationAuthScopes ? ( diff --git a/frontend/src/pages/secret-manager/integrations/GithubConfigurePage/GithubConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/GithubConfigurePage/GithubConfigurePage.tsx index 5e9871ade..741c03032 100644 --- a/frontend/src/pages/secret-manager/integrations/GithubConfigurePage/GithubConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/GithubConfigurePage/GithubConfigurePage.tsx @@ -12,12 +12,10 @@ import { import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { useNavigate, useSearch } from "@tanstack/react-router"; -import axios from "axios"; import { motion } from "framer-motion"; import { twMerge } from "tailwind-merge"; import { z, ZodIssueCode } from "zod"; -import { createNotification } from "@app/components/notifications"; import { Button, Card, @@ -275,19 +273,7 @@ export const GithubConfigurePage = () => { selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); - } catch (err) { - console.error(err); - - let errorMessage: string = "Something went wrong!"; - if (axios.isAxiosError(err)) { - const { message } = err?.response?.data as { message: string }; - errorMessage = message; - } - - createNotification({ - text: errorMessage, - type: "error" - }); + } catch { setIsLoading(false); } }; diff --git a/frontend/src/pages/secret-manager/integrations/HashicorpVaultAuthorizePage/HashicorpVaultAuthorizePage.tsx b/frontend/src/pages/secret-manager/integrations/HashicorpVaultAuthorizePage/HashicorpVaultAuthorizePage.tsx index 4b922a9bc..93d892241 100644 --- a/frontend/src/pages/secret-manager/integrations/HashicorpVaultAuthorizePage/HashicorpVaultAuthorizePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/HashicorpVaultAuthorizePage/HashicorpVaultAuthorizePage.tsx @@ -4,10 +4,8 @@ import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-sv import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { useNavigate } from "@tanstack/react-router"; -import axios from "axios"; import { z } from "zod"; -import { createNotification } from "@app/components/notifications"; import { Button, Card, CardBody, CardTitle, FormControl, Input } from "@app/components/v2"; import { useProject } from "@app/context"; import { useSaveIntegrationAccessToken } from "@app/hooks/api"; @@ -41,37 +39,23 @@ export const HashicorpVaultAuthorizePage = () => { }); const handleFormSubmit = async (formData: TForm) => { - try { - const integrationAuth = await mutateAsync({ - workspaceId: currentProject.id, - integration: "hashicorp-vault", - accessId: formData.vaultRoleID, - accessToken: formData.vaultSecretID, - url: formData.vaultURL, - namespace: formData.vaultNamespace - }); - navigate({ - to: "/projects/secret-management/$projectId/integrations/hashicorp-vault/create", - params: { - projectId: currentProject.id - }, - search: { - integrationAuthId: integrationAuth.id - } - }); - } catch (err) { - console.error(err); - let errorMessage: string = "Something went wrong!"; - if (axios.isAxiosError(err)) { - const { message } = err?.response?.data as { message: string }; - errorMessage = message; + const integrationAuth = await mutateAsync({ + workspaceId: currentProject.id, + integration: "hashicorp-vault", + accessId: formData.vaultRoleID, + accessToken: formData.vaultSecretID, + url: formData.vaultURL, + namespace: formData.vaultNamespace + }); + navigate({ + to: "/projects/secret-management/$projectId/integrations/hashicorp-vault/create", + params: { + projectId: currentProject.id + }, + search: { + integrationAuthId: integrationAuth.id } - - createNotification({ - text: errorMessage, - type: "error" - }); - } + }); }; return ( diff --git a/frontend/src/pages/secret-manager/integrations/HashicorpVaultConfigurePage/HashicorpVaultConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/HashicorpVaultConfigurePage/HashicorpVaultConfigurePage.tsx index 0dfcbb0bf..0636625df 100644 --- a/frontend/src/pages/secret-manager/integrations/HashicorpVaultConfigurePage/HashicorpVaultConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/HashicorpVaultConfigurePage/HashicorpVaultConfigurePage.tsx @@ -10,10 +10,8 @@ import { import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { useNavigate, useSearch } from "@tanstack/react-router"; -import axios from "axios"; import { z } from "zod"; -import { createNotification } from "@app/components/notifications"; import { Button, Card, @@ -90,38 +88,24 @@ export const HashicorpVaultConfigurePage = () => { }); const handleFormSubmit = async (formData: TForm) => { - try { - if (!integrationAuth?.id) return; - await mutateAsync({ - integrationAuthId: integrationAuth?.id, - isActive: true, - app: formData.vaultEnginePath, - sourceEnvironment: formData.selectedSourceEnvironment, - path: formData.vaultSecretPath, - secretPath: formData.secretPath - }); - navigate({ - to: "/projects/secret-management/$projectId/integrations", - params: { - projectId: currentProject.id - }, - search: { - selectedTab: IntegrationsListPageTabs.NativeIntegrations - } - }); - } catch (err) { - console.error(err); - let errorMessage: string = "Something went wrong!"; - if (axios.isAxiosError(err)) { - const { message } = err?.response?.data as { message: string }; - errorMessage = message; + if (!integrationAuth?.id) return; + await mutateAsync({ + integrationAuthId: integrationAuth?.id, + isActive: true, + app: formData.vaultEnginePath, + sourceEnvironment: formData.selectedSourceEnvironment, + path: formData.vaultSecretPath, + secretPath: formData.secretPath + }); + navigate({ + to: "/projects/secret-management/$projectId/integrations", + params: { + projectId: currentProject.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } - - createNotification({ - text: errorMessage, - type: "error" - }); - } + }); }; return integrationAuth ? ( diff --git a/frontend/src/pages/secret-manager/integrations/OctopusDeployAuthorizePage/OctopusDeployAuthorizePage.tsx b/frontend/src/pages/secret-manager/integrations/OctopusDeployAuthorizePage/OctopusDeployAuthorizePage.tsx index 21851e4cd..9e50fd43d 100644 --- a/frontend/src/pages/secret-manager/integrations/OctopusDeployAuthorizePage/OctopusDeployAuthorizePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/OctopusDeployAuthorizePage/OctopusDeployAuthorizePage.tsx @@ -6,7 +6,6 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useNavigate } from "@tanstack/react-router"; import { z } from "zod"; -import { createNotification } from "@app/components/notifications"; import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2"; import { useProject } from "@app/context"; import { removeTrailingSlash } from "@app/helpers/string"; @@ -29,30 +28,22 @@ export const OctopusDeployAuthorizePage = () => { }); const onSubmit = async ({ instanceUrl, apiKey }: TForm) => { - try { - const integrationAuth = await mutateAsync({ - workspaceId: currentProject.id, - integration: "octopus-deploy", - url: removeTrailingSlash(instanceUrl), - accessToken: apiKey - }); + const integrationAuth = await mutateAsync({ + workspaceId: currentProject.id, + integration: "octopus-deploy", + url: removeTrailingSlash(instanceUrl), + accessToken: apiKey + }); - navigate({ - to: "/projects/secret-management/$projectId/integrations/octopus-deploy/create", - params: { - projectId: currentProject.id - }, - search: { - integrationAuthId: integrationAuth.id - } - }); - } catch (err: any) { - createNotification({ - type: "error", - text: err.message ?? "Error authorizing integration" - }); - console.error(err); - } + navigate({ + to: "/projects/secret-management/$projectId/integrations/octopus-deploy/create", + params: { + projectId: currentProject.id + }, + search: { + integrationAuthId: integrationAuth.id + } + }); }; return ( diff --git a/frontend/src/pages/secret-manager/integrations/OctopusDeployConfigurePage/OctopusDeployConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/OctopusDeployConfigurePage/OctopusDeployConfigurePage.tsx index 5b531ac2d..75f8f5031 100644 --- a/frontend/src/pages/secret-manager/integrations/OctopusDeployConfigurePage/OctopusDeployConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/OctopusDeployConfigurePage/OctopusDeployConfigurePage.tsx @@ -107,49 +107,41 @@ export const OctopusDeployConfigurePage = () => { targetRoles, scope }: TFormData) => { - try { - await createIntegration.mutateAsync({ - integrationAuthId, - isActive: true, - scope, - app: targetResource.name, - appId: targetResource.appId, - targetEnvironment: targetSpace.Name, - targetEnvironmentId: targetSpace.Id, - metadata: { - octopusDeployScopeValues: { - Environment: targetEnvironments?.map(({ Id }) => Id), - Action: targetActions?.map(({ Id }) => Id), - Channel: targetChannels?.map(({ Id }) => Id), - ProcessOwner: targetProcesses?.map(({ Id }) => Id), - Role: targetRoles?.map(({ Id }) => Id), - Machine: targetMachines?.map(({ Id }) => Id) - } - }, - sourceEnvironment: sourceEnvironment.slug, - secretPath - }); - - createNotification({ - type: "success", - text: "Successfully created integration" - }); - navigate({ - to: "/projects/secret-management/$projectId/integrations", - params: { - projectId: currentProject.id - }, - search: { - selectedTab: IntegrationsListPageTabs.NativeIntegrations + await createIntegration.mutateAsync({ + integrationAuthId, + isActive: true, + scope, + app: targetResource.name, + appId: targetResource.appId, + targetEnvironment: targetSpace.Name, + targetEnvironmentId: targetSpace.Id, + metadata: { + octopusDeployScopeValues: { + Environment: targetEnvironments?.map(({ Id }) => Id), + Action: targetActions?.map(({ Id }) => Id), + Channel: targetChannels?.map(({ Id }) => Id), + ProcessOwner: targetProcesses?.map(({ Id }) => Id), + Role: targetRoles?.map(({ Id }) => Id), + Machine: targetMachines?.map(({ Id }) => Id) } - }); - } catch (err) { - createNotification({ - type: "error", - text: "Failed to create integration" - }); - console.error(err); - } + }, + sourceEnvironment: sourceEnvironment.slug, + secretPath + }); + + createNotification({ + type: "success", + text: "Successfully created integration" + }); + navigate({ + to: "/projects/secret-management/$projectId/integrations", + params: { + projectId: currentProject.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } + }); }; useEffect(() => { diff --git a/frontend/src/pages/user/PersonalSettingsPage/components/ChangeEmailSection/ChangeEmailSection.tsx b/frontend/src/pages/user/PersonalSettingsPage/components/ChangeEmailSection/ChangeEmailSection.tsx index c9d457e44..bd37e5620 100644 --- a/frontend/src/pages/user/PersonalSettingsPage/components/ChangeEmailSection/ChangeEmailSection.tsx +++ b/frontend/src/pages/user/PersonalSettingsPage/components/ChangeEmailSection/ChangeEmailSection.tsx @@ -83,23 +83,14 @@ export const ChangeEmailSection = () => { return; } - try { - await requestEmailChangeOTP({ newEmail }); - setPendingEmail(newEmail); - setIsOTPModalOpen(true); + await requestEmailChangeOTP({ newEmail }); + setPendingEmail(newEmail); + setIsOTPModalOpen(true); - createNotification({ - text: "Verification code sent to your new email address. Check your inbox!", - type: "success" - }); - } catch (err: any) { - console.error(err); - const errorMessage = err?.response?.data?.message || "Failed to send verification code"; - createNotification({ - text: errorMessage, - type: "error" - }); - } + createNotification({ + text: "Verification code sent to your new email address. Check your inbox!", + type: "success" + }); }; const [typedOTP, setTypedOTP] = useState(""); @@ -135,8 +126,6 @@ export const ChangeEmailSection = () => { navigate({ to: "/login" }); }, 2000); } catch (err: any) { - console.error(err); - const errorMessage = err?.response?.data?.message || "Invalid verification code"; if (errorMessage.includes("Invalid verification code")) { // Reset to email step so user must request new OTP @@ -149,11 +138,6 @@ export const ChangeEmailSection = () => { text: "Invalid verification code. Please request a new one.", type: "error" }); - } else { - createNotification({ - text: errorMessage, - type: "error" - }); } } }; diff --git a/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx b/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx index 552eab741..1ae492162 100644 --- a/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx +++ b/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx @@ -97,21 +97,13 @@ export const ChangePasswordSection = () => { }; const onSetupPassword = async () => { - try { - await sendSetupPasswordEmail.mutateAsync(); + await sendSetupPasswordEmail.mutateAsync(); - createNotification({ - title: "Password setup verification email sent", - text: "Check your email to confirm password setup", - type: "info" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to send password setup email", - type: "error" - }); - } + createNotification({ + title: "Password setup verification email sent", + text: "Check your email to confirm password setup", + type: "info" + }); }; return ( diff --git a/frontend/src/pages/user/PersonalSettingsPage/components/DeleteAccountSection/DeleteAccountSection.tsx b/frontend/src/pages/user/PersonalSettingsPage/components/DeleteAccountSection/DeleteAccountSection.tsx index 5761a772a..301942346 100644 --- a/frontend/src/pages/user/PersonalSettingsPage/components/DeleteAccountSection/DeleteAccountSection.tsx +++ b/frontend/src/pages/user/PersonalSettingsPage/components/DeleteAccountSection/DeleteAccountSection.tsx @@ -15,23 +15,15 @@ export const DeleteAccountSection = () => { const { mutateAsync: deleteUserMutateAsync, isPending } = useDeleteMe(); const handleDeleteAccountSubmit = async () => { - try { - await deleteUserMutateAsync(); + await deleteUserMutateAsync(); - createNotification({ - text: "Successfully deleted account", - type: "success" - }); + createNotification({ + text: "Successfully deleted account", + type: "success" + }); - navigate({ to: "/login" }); - handlePopUpClose("deleteAccount"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete account", - type: "error" - }); - } + navigate({ to: "/login" }); + handlePopUpClose("deleteAccount"); }; return ( diff --git a/frontend/src/pages/user/PersonalSettingsPage/components/SecuritySection/MFASection.tsx b/frontend/src/pages/user/PersonalSettingsPage/components/SecuritySection/MFASection.tsx index ef29f3ff6..74f4ed99b 100644 --- a/frontend/src/pages/user/PersonalSettingsPage/components/SecuritySection/MFASection.tsx +++ b/frontend/src/pages/user/PersonalSettingsPage/components/SecuritySection/MFASection.tsx @@ -84,49 +84,27 @@ export const MFASection = () => { }, [totpRegistration, showMobileAuthSetup]); const handleTotpDeletion = async () => { - try { - await deleteTotpConfiguration(); + await deleteTotpConfiguration(); - await mutateAsync({ - selectedMfaMethod: MfaMethod.EMAIL - }); + await mutateAsync({ + selectedMfaMethod: MfaMethod.EMAIL + }); - createNotification({ - text: "Successfully deleted mobile authenticator and switched to email authentication", - type: "success" - }); + createNotification({ + text: "Successfully deleted mobile authenticator and switched to email authentication", + type: "success" + }); - handlePopUpClose("deleteTotpConfig"); - } catch (err) { - console.error(err); - const error = err as any; - const text = error?.response?.data?.message ?? "Failed to delete mobile authenticator"; - - createNotification({ - text, - type: "error" - }); - } + handlePopUpClose("deleteTotpConfig"); }; const handleGenerateMoreRecoveryCodes = async () => { - try { - await createTotpRecoveryCodes(); + await createTotpRecoveryCodes(); - createNotification({ - text: "Successfully generated new recovery codes", - type: "success" - }); - } catch (err) { - console.error(err); - const error = err as any; - const text = error?.response?.data?.message ?? "Failed to generate new recovery codes"; - - createNotification({ - text, - type: "error" - }); - } + createNotification({ + text: "Successfully generated new recovery codes", + type: "success" + }); }; const handleFormDataChange = async (field: string, value: any) => { @@ -200,10 +178,6 @@ export const MFASection = () => { await queryClient.invalidateQueries({ queryKey: userKeys.totpConfiguration }); } catch { - createNotification({ - text: "Failed to verify TOTP code. Please try again.", - type: "error" - }); setIsLoading(false); return; } @@ -249,12 +223,6 @@ export const MFASection = () => { setShowMobileAuthSetup(false); setTotpCode(""); setShouldShowRecoveryCodes.off(); - } catch (err) { - createNotification({ - text: "Something went wrong while updating two-factor authentication settings.", - type: "error" - }); - console.error(err); } finally { setIsLoading(false); } diff --git a/frontend/src/pages/user/PersonalSettingsPage/components/SessionsSection/SessionsTable.tsx b/frontend/src/pages/user/PersonalSettingsPage/components/SessionsSection/SessionsTable.tsx index 027f2ed46..f37be42a1 100644 --- a/frontend/src/pages/user/PersonalSettingsPage/components/SessionsSection/SessionsTable.tsx +++ b/frontend/src/pages/user/PersonalSettingsPage/components/SessionsSection/SessionsTable.tsx @@ -40,19 +40,11 @@ export const SessionsTable = () => { ] as const); const handleSignOut = async (sessionId: string) => { - try { - await revokeMySessionById(sessionId); - createNotification({ - text: "Session revoked successfully", - type: "success" - }); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to revoke session", - type: "error" - }); - } + await revokeMySessionById(sessionId); + createNotification({ + text: "Session revoked successfully", + type: "success" + }); handlePopUpClose("deleteSession"); }; diff --git a/frontend/src/pages/user/PersonalSettingsPage/components/UserNameSection/UserNameSection.tsx b/frontend/src/pages/user/PersonalSettingsPage/components/UserNameSection/UserNameSection.tsx index df0dae9cb..1e06d298f 100644 --- a/frontend/src/pages/user/PersonalSettingsPage/components/UserNameSection/UserNameSection.tsx +++ b/frontend/src/pages/user/PersonalSettingsPage/components/UserNameSection/UserNameSection.tsx @@ -27,22 +27,14 @@ export const UserNameSection = (): JSX.Element => { }, [user]); const onFormSubmit = async ({ name }: FormData) => { - try { - if (!user?.id) return; - if (name === "") return; + if (!user?.id) return; + if (name === "") return; - await mutateAsync({ newName: name }); - createNotification({ - text: "Successfully renamed user", - type: "success" - }); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to rename user", - type: "error" - }); - } + await mutateAsync({ newName: name }); + createNotification({ + text: "Successfully renamed user", + type: "success" + }); }; return ( From 0d3cb0c0180d672869175a9255b2c1b582febe13 Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Mon, 3 Nov 2025 23:46:08 +0530 Subject: [PATCH 019/294] refactor: enhance folder retrieval with dynamic sorting options --- .../secret-folder/secret-folder-dal.ts | 37 +++++++++++++------ .../secret-folder/secret-folder-service.ts | 29 ++++++++------- .../secret-folder/secret-folder-types.ts | 2 + 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/backend/src/services/secret-folder/secret-folder-dal.ts b/backend/src/services/secret-folder/secret-folder-dal.ts index c999b69d2..356de3099 100644 --- a/backend/src/services/secret-folder/secret-folder-dal.ts +++ b/backend/src/services/secret-folder/secret-folder-dal.ts @@ -426,11 +426,13 @@ export const secretFolderDALFactory = (db: TDbClient) => { ), db.ref("slug").withSchema(TableName.Environment).as("environment") ) - .orderByRaw( - `CASE WHEN ${TableName.SecretFolder}.${orderBy} LIKE '\\_%' THEN ${orderDirection === OrderByDirection.ASC ? "0" : "1"} ELSE ${orderDirection === OrderByDirection.ASC ? "1" : "0"} END` - ) - .orderByRaw(`LOWER(${TableName.SecretFolder}.${orderBy}) ${orderDirection}`) - .orderByRaw(`${TableName.SecretFolder}.${orderBy} ${orderDirection}`); + .orderByRaw(`CASE WHEN ${TableName.SecretFolder}.?? LIKE '\\_%' THEN ? ELSE ? END`, [ + orderBy, + orderDirection === OrderByDirection.ASC ? 0 : 1, + orderDirection === OrderByDirection.ASC ? 1 : 0 + ]) + .orderByRaw(`LOWER(${TableName.SecretFolder}.??) ${orderDirection}`, [orderBy]) + .orderByRaw(`${TableName.SecretFolder}.?? ${orderDirection}`, [orderBy]); if (limit) { const rankOffset = offset + 1; // ranks start from 1 @@ -440,11 +442,13 @@ export const secretFolderDALFactory = (db: TDbClient) => { .from[number]>("w") .where("w.rank", ">=", rankOffset) .andWhere("w.rank", "<", rankOffset + limit) - .orderByRaw( - `CASE WHEN "w"."${orderBy}" LIKE '\\_%' THEN ${orderDirection === OrderByDirection.ASC ? "0" : "1"} ELSE ${orderDirection === OrderByDirection.ASC ? "1" : "0"} END` - ) - .orderByRaw(`LOWER("w"."${orderBy}") ${orderDirection}`) - .orderByRaw(`"w"."${orderBy}" ${orderDirection}`); + .orderByRaw(`CASE WHEN "w".?? LIKE '\\_%' THEN ? ELSE ? END`, [ + orderBy, + orderDirection === OrderByDirection.ASC ? 0 : 1, + orderDirection === OrderByDirection.ASC ? 1 : 0 + ]) + .orderByRaw(`LOWER("w".??) ${orderDirection}`, [orderBy]) + .orderByRaw(`"w".?? ${orderDirection}`, [orderBy]); } const folders = await query; @@ -455,7 +459,10 @@ export const secretFolderDALFactory = (db: TDbClient) => { } }; - const findByEnvsDeep = async ({ parentIds }: TFindFoldersDeepByParentIdsDTO, tx?: Knex) => { + const findByEnvsDeep = async ( + { parentIds, orderBy = SecretsOrderBy.Name, orderDirection = OrderByDirection.ASC }: TFindFoldersDeepByParentIdsDTO, + tx?: Knex + ) => { try { const folders = await (tx || db.replicaNode()) .withRecursive("parents", (qb) => @@ -490,7 +497,13 @@ export const secretFolderDALFactory = (db: TDbClient) => { .select<(TSecretFolders & { path: string; depth: number; environment: string })[]>("*") .from("parents") .orderBy("depth") - .orderBy(`name`); + .orderByRaw(`CASE WHEN "parents".?? LIKE '\\_%' THEN ? ELSE ? END`, [ + orderBy, + orderDirection === OrderByDirection.ASC ? 0 : 1, + orderDirection === OrderByDirection.ASC ? 1 : 0 + ]) + .orderByRaw(`LOWER("parents".??) ${orderDirection}`, [orderBy]) + .orderByRaw(`"parents".?? ${orderDirection}`, [orderBy]); return folders; } catch (error) { diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts index c216ea3a8..033efdb14 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -14,6 +14,7 @@ import { PgSqlLock } from "@app/keystore/keystore"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { OrderByDirection, OrgServiceActor } from "@app/lib/types"; import { ActorType } from "@app/services/auth/auth-type"; +import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { buildFolderPath } from "@app/services/secret-folder/secret-folder-fns"; import { @@ -781,7 +782,11 @@ export const secretFolderServiceFactory = ({ if (!parentFolder) return []; if (recursive) { - const recursiveFolders = await folderDAL.findByEnvsDeep({ parentIds: [parentFolder.id] }); + const recursiveFolders = await folderDAL.findByEnvsDeep({ + parentIds: [parentFolder.id], + orderBy: orderBy || SecretsOrderBy.Name, + orderDirection: orderDirection || OrderByDirection.ASC + }); // remove the parent folder return recursiveFolders .filter((folder) => { @@ -800,19 +805,15 @@ export const secretFolderServiceFactory = ({ })); } - const folders = await folderDAL.find( - { - envId: env.id, - parentId: parentFolder.id, - isReserved: false, - $search: search ? { name: `%${search}%` } : undefined - }, - { - sort: orderBy ? [[orderBy, orderDirection ?? OrderByDirection.ASC]] : undefined, - limit, - offset - } - ); + const folders = await folderDAL.findByMultiEnv({ + environmentIds: [env.id], + parentIds: [parentFolder.id], + search, + orderBy: orderBy || SecretsOrderBy.Name, + orderDirection: orderDirection || OrderByDirection.ASC, + limit, + offset + }); if (lastSecretModified) { return folders.filter((el) => el.lastSecretModified ? el.lastSecretModified >= new Date(lastSecretModified) : false diff --git a/backend/src/services/secret-folder/secret-folder-types.ts b/backend/src/services/secret-folder/secret-folder-types.ts index da8be52a0..d220c29d2 100644 --- a/backend/src/services/secret-folder/secret-folder-types.ts +++ b/backend/src/services/secret-folder/secret-folder-types.ts @@ -64,6 +64,8 @@ export type TGetFoldersDeepByEnvsDTO = { export type TFindFoldersDeepByParentIdsDTO = { parentIds: string[]; + orderBy?: SecretsOrderBy; + orderDirection?: OrderByDirection; }; export type TCreateManyFoldersDTO = { From 9d215dff36d4635c6af6590ad5195b5538014b67 Mon Sep 17 00:00:00 2001 From: x032205 Date: Mon, 3 Nov 2025 13:40:21 -0500 Subject: [PATCH 020/294] don't notify "invited" admins about health alerts --- backend/src/ee/services/gateway-v2/gateway-v2-service.ts | 4 +++- backend/src/ee/services/relay/relay-service.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/services/gateway-v2/gateway-v2-service.ts b/backend/src/ee/services/gateway-v2/gateway-v2-service.ts index eaec31e50..c17f507ae 100644 --- a/backend/src/ee/services/gateway-v2/gateway-v2-service.ts +++ b/backend/src/ee/services/gateway-v2/gateway-v2-service.ts @@ -909,7 +909,9 @@ export const gatewayV2ServiceFactory = ({ for await (const [orgId, gateways] of Object.entries(gatewaysByOrg)) { try { - const admins = await orgDAL.findOrgMembersByRole(orgId, OrgMembershipRole.Admin); + const admins = (await orgDAL.findOrgMembersByRole(orgId, OrgMembershipRole.Admin)).filter( + (admin) => admin.status !== "invited" + ); if (admins.length === 0) { logger.warn({ orgId }, "Organization has no admins to notify about unhealthy gateway."); // eslint-disable-next-line no-continue diff --git a/backend/src/ee/services/relay/relay-service.ts b/backend/src/ee/services/relay/relay-service.ts index 096011765..833c2d420 100644 --- a/backend/src/ee/services/relay/relay-service.ts +++ b/backend/src/ee/services/relay/relay-service.ts @@ -1248,7 +1248,9 @@ export const relayServiceFactory = ({ }); } } else { - const admins = await orgDAL.findOrgMembersByRole(orgId, OrgMembershipRole.Admin); + const admins = (await orgDAL.findOrgMembersByRole(orgId, OrgMembershipRole.Admin)).filter( + (admin) => admin.status !== "invited" + ); if (admins.length === 0) { // eslint-disable-next-line no-continue continue; From f2f50f739e4c3e4547de84451338dcd21aef8e76 Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Mon, 3 Nov 2025 15:43:08 -0300 Subject: [PATCH 021/294] refactor: streamline async logic and notification handling across various settings and integration components --- .../NativeIntegrationsTab.tsx | 46 +++----- .../EnvironmentSection/EnvironmentSection.tsx | 28 ++--- .../EnvironmentSection/EnvironmentTable.tsx | 28 ++--- .../SecretDetectionIgnoreValuesSection.tsx | 23 ++-- .../SecretSnapshotsLegacySection.tsx | 6 - .../components/WebhooksTab/WebhooksTab.tsx | 104 ++++++------------ .../MicrosoftTeamsIntegrationForm.tsx | 53 ++++----- .../SecretScanningResourceSection.tsx | 25 ++--- .../SecretScanningDataSourcesTable.tsx | 52 ++++----- .../components/ProjectSshConfigCasSection.tsx | 26 ++--- .../pages/ssh/SshCaByIDPage/SshCaByIDPage.tsx | 34 +++--- .../components/SshCertificateModal.tsx | 100 ++++++++--------- .../SshCertificateTemplateModal.tsx | 76 ++++++------- .../SshCertificateTemplatesSection.tsx | 50 +++------ .../ssh/SshCasPage/components/SshCaModal.tsx | 66 +++++------ .../SshCasPage/components/SshCaSection.tsx | 40 ++----- .../SshHostGroupDetailsByIDPage.tsx | 34 +++--- .../components/AddHostGroupMemberModal.tsx | 33 +++--- .../components/SshHostGroupModal.tsx | 102 ++++++++--------- .../components/SshHostsSection.tsx | 20 +--- .../components/APIKeySection/APIKeyTable.tsx | 18 +-- .../APIKeySection/AddAPIKeyModal.tsx | 28 ++--- 22 files changed, 383 insertions(+), 609 deletions(-) diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/NativeIntegrationsTab/NativeIntegrationsTab.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/NativeIntegrationsTab/NativeIntegrationsTab.tsx index 97ebfbee5..db4c6356e 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/NativeIntegrationsTab/NativeIntegrationsTab.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/NativeIntegrationsTab/NativeIntegrationsTab.tsx @@ -108,43 +108,27 @@ export const NativeIntegrationsTab = () => { shouldDeleteIntegrationSecrets: boolean, cb: () => void ) => { - try { - await deleteIntegration({ id: integrationId, workspaceId, shouldDeleteIntegrationSecrets }); - if (cb) cb(); - createNotification({ - type: "success", - text: "Deleted integration" - }); - } catch (err) { - console.log(err); - createNotification({ - type: "error", - text: "Failed to delete integration" - }); - } + await deleteIntegration({ id: integrationId, workspaceId, shouldDeleteIntegrationSecrets }); + if (cb) cb(); + createNotification({ + type: "success", + text: "Deleted integration" + }); }; const handleIntegrationAuthRevoke = async (provider: string, cb?: () => void) => { const integrationAuthForProvider = integrationAuths?.[provider]; if (!integrationAuthForProvider) return; - try { - await deleteIntegrationAuths({ - integration: provider, - workspaceId - }); - if (cb) cb(); - createNotification({ - type: "success", - text: "Revoked provider authentication" - }); - } catch (err) { - console.error(err); - createNotification({ - type: "error", - text: "Failed to revoke provider authentication" - }); - } + await deleteIntegrationAuths({ + integration: provider, + workspaceId + }); + if (cb) cb(); + createNotification({ + type: "success", + text: "Revoked provider authentication" + }); }; const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/EnvironmentSection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/EnvironmentSection.tsx index 1105d60a2..e1c1cdeda 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/EnvironmentSection.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/EnvironmentSection.tsx @@ -39,27 +39,19 @@ export const EnvironmentSection = () => { ] as const); const onEnvDeleteSubmit = async (id: string) => { - try { - if (!currentProject?.id) return; + if (!currentProject?.id) return; - await deleteWsEnvironment.mutateAsync({ - projectId: currentProject.id, - id - }); + await deleteWsEnvironment.mutateAsync({ + projectId: currentProject.id, + id + }); - createNotification({ - text: "Successfully deleted environment", - type: "success" - }); + createNotification({ + text: "Successfully deleted environment", + type: "success" + }); - handlePopUpClose("deleteEnv"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete environment", - type: "error" - }); - } + handlePopUpClose("deleteEnv"); }; return ( diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/EnvironmentTable.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/EnvironmentTable.tsx index 0908d8377..6b5b3a8d2 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/EnvironmentTable.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/EnvironmentTable.tsx @@ -46,26 +46,18 @@ export const EnvironmentTable = ({ handlePopUpOpen }: Props) => { const updateEnvironment = useUpdateWsEnvironment(); const handleReorderEnv = async (id: string, position: number) => { - try { - if (!currentProject?.id) return; + if (!currentProject?.id) return; - await updateEnvironment.mutateAsync({ - projectId: currentProject.id, - id, - position - }); + await updateEnvironment.mutateAsync({ + projectId: currentProject.id, + id, + position + }); - createNotification({ - text: "Successfully re-ordered environments", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to re-order environments", - type: "error" - }); - } + createNotification({ + text: "Successfully re-ordered environments", + type: "success" + }); }; const isMoreEnvironmentsAllowed = diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx index e086834a1..5667afa76 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx @@ -55,22 +55,15 @@ export const SecretDetectionIgnoreValuesSection = () => { }, [currentProject?.secretDetectionIgnoreValues, reset]); const handleIgnoreValuesSubmit = async ({ ignoreValues }: TForm) => { - try { - await updateProject({ - projectId: currentProject.id, - secretDetectionIgnoreValues: ignoreValues.map((item) => item.value) - }); + await updateProject({ + projectId: currentProject.id, + secretDetectionIgnoreValues: ignoreValues.map((item) => item.value) + }); - createNotification({ - text: "Successfully updated secret detection ignore values", - type: "success" - }); - } catch { - createNotification({ - text: "Failed updating secret detection ignore values", - type: "error" - }); - } + createNotification({ + text: "Successfully updated secret detection ignore values", + type: "success" + }); }; const isAdmin = hasProjectRole(ProjectMembershipRole.Admin); diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/SecretSnapshotsLegacySection/SecretSnapshotsLegacySection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/SecretSnapshotsLegacySection/SecretSnapshotsLegacySection.tsx index 553f17d54..466b950f1 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/SecretSnapshotsLegacySection/SecretSnapshotsLegacySection.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/SecretSnapshotsLegacySection/SecretSnapshotsLegacySection.tsx @@ -30,12 +30,6 @@ export const SecretSnapshotsLegacySection = () => { text: `Successfully ${state ? "enabled" : "disabled"} secret snapshots legacy for this project`, type: "success" }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to update secret snapshots legacy for this project", - type: "error" - }); } finally { setIsLoading(false); } diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WebhooksTab/WebhooksTab.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WebhooksTab/WebhooksTab.tsx index 517f9e624..5296bc4ad 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/WebhooksTab/WebhooksTab.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WebhooksTab/WebhooksTab.tsx @@ -59,83 +59,51 @@ export const WebhooksTab = withProjectPermission( const { mutateAsync: deleteWebhook } = useDeleteWebhook(); const handleWebhookCreate = async (data: TFormSchema) => { - try { - await createWebhook({ - ...data, - projectId - }); - handlePopUpClose("addWebhook"); - createNotification({ - type: "success", - text: "Successfully created webhook" - }); - } catch (err) { - console.log(err); - createNotification({ - type: "error", - text: "Failed to create webhook" - }); - } + await createWebhook({ + ...data, + projectId + }); + handlePopUpClose("addWebhook"); + createNotification({ + type: "success", + text: "Successfully created webhook" + }); }; const handleWebhookDisable = async (webhookId: string, isDisabled: boolean) => { - try { - await updateWebhook({ - webhookId, - projectId, - isDisabled - }); - createNotification({ - type: "success", - text: "Successfully updated webhook" - }); - } catch (err) { - console.log(err); - createNotification({ - type: "error", - text: "Failed to update webhook" - }); - } + await updateWebhook({ + webhookId, + projectId, + isDisabled + }); + createNotification({ + type: "success", + text: "Successfully updated webhook" + }); }; const handleWebhookDelete = async () => { - try { - const webhookId = popUp?.deleteWebhook?.data as string; - await deleteWebhook({ - webhookId, - projectId - }); - handlePopUpClose("deleteWebhook"); - createNotification({ - type: "success", - text: "Successfully deleted webhook" - }); - } catch (err) { - console.log(err); - createNotification({ - type: "error", - text: "Failed to delete webhook" - }); - } + const webhookId = popUp?.deleteWebhook?.data as string; + await deleteWebhook({ + webhookId, + projectId + }); + handlePopUpClose("deleteWebhook"); + createNotification({ + type: "success", + text: "Successfully deleted webhook" + }); }; const handleWebhookTest = async (webhookId: string) => { - try { - await testWebhook({ - webhookId, - projectId - }); - createNotification({ - type: "success", - text: "Successfully triggered webhook" - }); - } catch (err) { - console.log(err); - createNotification({ - type: "error", - text: "Failed to trigger webhook" - }); - } + await testWebhook({ + webhookId, + projectId + }); + createNotification({ + type: "success", + text: "Successfully triggered webhook" + }); }; return ( diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsIntegrationForm.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsIntegrationForm.tsx index c638b65a7..707322264 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsIntegrationForm.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsIntegrationForm.tsx @@ -130,37 +130,30 @@ export const MicrosoftTeamsIntegrationForm = ({ onClose }: Props) => { }); const handleIntegrationSave = async (data: TMicrosoftTeamsConfigForm) => { - try { - if (!currentProject) { - return; - } - - await updateProjectMicrosoftTeamsConfig({ - projectId: currentProject.id, - isAccessRequestNotificationEnabled: data.isAccessRequestNotificationEnabled, - isSecretRequestNotificationEnabled: data.isSecretRequestNotificationEnabled, - ...(data.isAccessRequestNotificationEnabled && { - accessRequestChannels: data.accessRequestChannels - }), - ...(data.isSecretRequestNotificationEnabled && { - secretRequestChannels: data.secretRequestChannels - }), - integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS, - integrationId: data.microsoftTeamsIntegrationId - }); - - createNotification({ - type: "success", - text: "Successfully created microsoft teams integration" - }); - - onClose(); - } catch { - createNotification({ - type: "error", - text: "Failed to create microsoft teams integration" - }); + if (!currentProject) { + return; } + + await updateProjectMicrosoftTeamsConfig({ + projectId: currentProject.id, + isAccessRequestNotificationEnabled: data.isAccessRequestNotificationEnabled, + isSecretRequestNotificationEnabled: data.isSecretRequestNotificationEnabled, + ...(data.isAccessRequestNotificationEnabled && { + accessRequestChannels: data.accessRequestChannels + }), + ...(data.isSecretRequestNotificationEnabled && { + secretRequestChannels: data.secretRequestChannels + }), + integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS, + integrationId: data.microsoftTeamsIntegrationId + }); + + createNotification({ + type: "success", + text: "Successfully created microsoft teams integration" + }); + + onClose(); }; const selectedAccessRequestTeamId = watch("accessRequestChannels.teamId"); diff --git a/frontend/src/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/SecretScanningResourceSection.tsx b/frontend/src/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/SecretScanningResourceSection.tsx index 5616c956a..1e81357b8 100644 --- a/frontend/src/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/SecretScanningResourceSection.tsx +++ b/frontend/src/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/SecretScanningResourceSection.tsx @@ -22,23 +22,16 @@ export const SecretScanningResourceSection = ({ dataSource }: Props) => { const triggerDataSourceScan = useTriggerSecretScanningDataSource(); const handleTriggerScan = async () => { - try { - await triggerDataSourceScan.mutateAsync({ - dataSourceId: dataSource.id, - type: dataSource.type, - projectId: dataSource.projectId - }); + await triggerDataSourceScan.mutateAsync({ + dataSourceId: dataSource.id, + type: dataSource.type, + projectId: dataSource.projectId + }); - createNotification({ - text: `Successfully triggered scan for ${dataSource.name}`, - type: "success" - }); - } catch { - createNotification({ - text: `Failed to trigger scan for ${dataSource.name}`, - type: "error" - }); - } + createNotification({ + text: `Successfully triggered scan for ${dataSource.name}`, + type: "success" + }); }; const resourceDetails = RESOURCE_DESCRIPTION_HELPER[dataSource.type]; diff --git a/frontend/src/pages/secret-scanning/SecretScanningDataSourcesPage/components/SecretScanningDataSourcesTable.tsx b/frontend/src/pages/secret-scanning/SecretScanningDataSourcesPage/components/SecretScanningDataSourcesTable.tsx index 8e879acce..ac61c8f54 100644 --- a/frontend/src/pages/secret-scanning/SecretScanningDataSourcesPage/components/SecretScanningDataSourcesTable.tsx +++ b/frontend/src/pages/secret-scanning/SecretScanningDataSourcesPage/components/SecretScanningDataSourcesTable.tsx @@ -184,44 +184,30 @@ export const SecretScanningDataSourcesTable = ({ dataSources }: Props) => { const isAutoScanEnabled = !dataSource.isAutoScanEnabled; - try { - await updateDataSource.mutateAsync({ - dataSourceId: dataSource.id, - type: dataSource.type, - isAutoScanEnabled, - projectId: dataSource.projectId - }); + await updateDataSource.mutateAsync({ + dataSourceId: dataSource.id, + type: dataSource.type, + isAutoScanEnabled, + projectId: dataSource.projectId + }); - createNotification({ - text: `Successfully ${isAutoScanEnabled ? "enabled" : "disabled"} auto-scan for ${destinationName} Data Source`, - type: "success" - }); - } catch { - createNotification({ - text: `Failed to ${isAutoScanEnabled ? "enable" : "disable"} auto-scan for ${destinationName} Data Source`, - type: "error" - }); - } + createNotification({ + text: `Successfully ${isAutoScanEnabled ? "enabled" : "disabled"} auto-scan for ${destinationName} Data Source`, + type: "success" + }); }; const handleTriggerScan = async (dataSource: TSecretScanningDataSource) => { - try { - await triggerDataSourceScan.mutateAsync({ - dataSourceId: dataSource.id, - type: dataSource.type, - projectId: dataSource.projectId - }); + await triggerDataSourceScan.mutateAsync({ + dataSourceId: dataSource.id, + type: dataSource.type, + projectId: dataSource.projectId + }); - createNotification({ - text: "Successfully triggered scan", - type: "success" - }); - } catch { - createNotification({ - text: "Failed to trigger scan", - type: "error" - }); - } + createNotification({ + text: "Successfully triggered scan", + type: "success" + }); }; return ( diff --git a/frontend/src/pages/ssh/SettingsPage/components/ProjectSshTab/components/ProjectSshConfigCasSection.tsx b/frontend/src/pages/ssh/SettingsPage/components/ProjectSshTab/components/ProjectSshConfigCasSection.tsx index 86bdf3c34..aed25d46f 100644 --- a/frontend/src/pages/ssh/SettingsPage/components/ProjectSshTab/components/ProjectSshConfigCasSection.tsx +++ b/frontend/src/pages/ssh/SettingsPage/components/ProjectSshTab/components/ProjectSshConfigCasSection.tsx @@ -47,24 +47,16 @@ export const ProjectSshConfigCasSection = () => { }, [sshConfig]); const onFormSubmit = async ({ defaultUserSshCaId, defaultHostSshCaId }: FormData) => { - try { - await updateProjectSshConfig({ - projectId: currentProject.id, - defaultUserSshCaId: defaultUserSshCaId || undefined, - defaultHostSshCaId: defaultHostSshCaId || undefined - }); + await updateProjectSshConfig({ + projectId: currentProject.id, + defaultUserSshCaId: defaultUserSshCaId || undefined, + defaultHostSshCaId: defaultHostSshCaId || undefined + }); - createNotification({ - text: "Successfully updated SSH project settings", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to update SSH project settings", - type: "error" - }); - } + createNotification({ + text: "Successfully updated SSH project settings", + type: "success" + }); }; return ( diff --git a/frontend/src/pages/ssh/SshCaByIDPage/SshCaByIDPage.tsx b/frontend/src/pages/ssh/SshCaByIDPage/SshCaByIDPage.tsx index bb6790a92..41a69a7bd 100644 --- a/frontend/src/pages/ssh/SshCaByIDPage/SshCaByIDPage.tsx +++ b/frontend/src/pages/ssh/SshCaByIDPage/SshCaByIDPage.tsx @@ -43,30 +43,22 @@ const Page = () => { ] as const); const onRemoveCaSubmit = async (caIdToDelete: string) => { - try { - if (!projectId) return; + if (!projectId) return; - await deleteSshCa({ caId: caIdToDelete }); + await deleteSshCa({ caId: caIdToDelete }); - createNotification({ - text: "Successfully deleted SSH CA", - type: "success" - }); + createNotification({ + text: "Successfully deleted SSH CA", + type: "success" + }); - handlePopUpClose("deleteSshCa"); - navigate({ - to: "/projects/ssh/$projectId/overview", - params: { - projectId - } - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete SSH CA", - type: "error" - }); - } + handlePopUpClose("deleteSshCa"); + navigate({ + to: "/projects/ssh/$projectId/overview", + params: { + projectId + } + }); }; return ( diff --git a/frontend/src/pages/ssh/SshCaByIDPage/components/SshCertificateModal.tsx b/frontend/src/pages/ssh/SshCaByIDPage/components/SshCertificateModal.tsx index 6e0baa502..32470308a 100644 --- a/frontend/src/pages/ssh/SshCaByIDPage/components/SshCertificateModal.tsx +++ b/frontend/src/pages/ssh/SshCaByIDPage/components/SshCertificateModal.tsx @@ -122,65 +122,57 @@ export const SshCertificateModal = ({ popUp, handlePopUpToggle }: Props) => { ttl, keyId }: FormData) => { - try { - if (!templateData) return; - if (!projectId) return; + if (!templateData) return; + if (!projectId) return; - switch (operation) { - case SshCertificateOperation.SIGN_SSH_KEY: { - const { serialNumber, signedKey } = await signSshKey({ - projectId, - certificateTemplateId: templateData.id, - publicKey: existingPublicKey, - certType, - principals: principals.split(",").map((user) => user.trim()), - ttl, - keyId - }); + switch (operation) { + case SshCertificateOperation.SIGN_SSH_KEY: { + const { serialNumber, signedKey } = await signSshKey({ + projectId, + certificateTemplateId: templateData.id, + publicKey: existingPublicKey, + certType, + principals: principals.split(",").map((user) => user.trim()), + ttl, + keyId + }); - setCertificateDetails({ - serialNumber, - signedKey - }); - break; - } - case SshCertificateOperation.ISSUE_SSH_CREDS: { - const { serialNumber, publicKey, privateKey, signedKey } = await issueSshCreds({ - projectId, - certificateTemplateId: templateData.id, - keyAlgorithm, - certType, - principals: principals.split(",").map((user) => user.trim()), - ttl, - keyId - }); - - setCertificateDetails({ - serialNumber, - privateKey, - publicKey, - signedKey - }); - break; - } - default: { - break; - } + setCertificateDetails({ + serialNumber, + signedKey + }); + break; } + case SshCertificateOperation.ISSUE_SSH_CREDS: { + const { serialNumber, publicKey, privateKey, signedKey } = await issueSshCreds({ + projectId, + certificateTemplateId: templateData.id, + keyAlgorithm, + certType, + principals: principals.split(",").map((user) => user.trim()), + ttl, + keyId + }); - reset(); - - createNotification({ - text: "Successfully created SSH certificate", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to create SSH certificate", - type: "error" - }); + setCertificateDetails({ + serialNumber, + privateKey, + publicKey, + signedKey + }); + break; + } + default: { + break; + } } + + reset(); + + createNotification({ + text: "Successfully created SSH certificate", + type: "success" + }); }; return ( diff --git a/frontend/src/pages/ssh/SshCaByIDPage/components/SshCertificateTemplateModal.tsx b/frontend/src/pages/ssh/SshCaByIDPage/components/SshCertificateTemplateModal.tsx index 17c7a552e..926a1d022 100644 --- a/frontend/src/pages/ssh/SshCaByIDPage/components/SshCertificateTemplateModal.tsx +++ b/frontend/src/pages/ssh/SshCaByIDPage/components/SshCertificateTemplateModal.tsx @@ -138,52 +138,44 @@ export const SshCertificateTemplateModal = ({ popUp, handlePopUpToggle, sshCaId allowedHosts, allowCustomKeyIds }: FormData) => { - try { - if (certTemplate) { - await updateSshCertTemplate({ - id: certTemplate.id, - name, - ttl, - maxTTL, - allowedUsers: allowedUsers ? allowedUsers.split(",").map((user) => user.trim()) : [], - allowedHosts: allowedHosts ? allowedHosts.split(",").map((host) => host.trim()) : [], - allowUserCertificates, - allowHostCertificates, - allowCustomKeyIds - }); + if (certTemplate) { + await updateSshCertTemplate({ + id: certTemplate.id, + name, + ttl, + maxTTL, + allowedUsers: allowedUsers ? allowedUsers.split(",").map((user) => user.trim()) : [], + allowedHosts: allowedHosts ? allowedHosts.split(",").map((host) => host.trim()) : [], + allowUserCertificates, + allowHostCertificates, + allowCustomKeyIds + }); - createNotification({ - text: "Successfully updated SSH certificate template", - type: "success" - }); - } else { - await createSshCertTemplate({ - sshCaId, - name, - ttl, - maxTTL, - allowedUsers: allowedUsers ? allowedUsers.split(",").map((user) => user.trim()) : [], - allowedHosts: allowedHosts ? allowedHosts.split(",").map((host) => host.trim()) : [], - allowUserCertificates, - allowHostCertificates, - allowCustomKeyIds - }); - - createNotification({ - text: "Successfully created SSH certificate template", - type: "success" - }); - } - - reset(); - handlePopUpToggle("sshCertificateTemplate", false); - } catch (err) { - console.error(err); createNotification({ - text: "Failed to save changes", - type: "error" + text: "Successfully updated SSH certificate template", + type: "success" + }); + } else { + await createSshCertTemplate({ + sshCaId, + name, + ttl, + maxTTL, + allowedUsers: allowedUsers ? allowedUsers.split(",").map((user) => user.trim()) : [], + allowedHosts: allowedHosts ? allowedHosts.split(",").map((host) => host.trim()) : [], + allowUserCertificates, + allowHostCertificates, + allowCustomKeyIds + }); + + createNotification({ + text: "Successfully created SSH certificate template", + type: "success" }); } + + reset(); + handlePopUpToggle("sshCertificateTemplate", false); }; return ( diff --git a/frontend/src/pages/ssh/SshCaByIDPage/components/SshCertificateTemplatesSection.tsx b/frontend/src/pages/ssh/SshCaByIDPage/components/SshCertificateTemplatesSection.tsx index 792c6ad3b..5b021715f 100644 --- a/frontend/src/pages/ssh/SshCaByIDPage/components/SshCertificateTemplatesSection.tsx +++ b/frontend/src/pages/ssh/SshCaByIDPage/components/SshCertificateTemplatesSection.tsx @@ -33,24 +33,16 @@ export const SshCertificateTemplatesSection = ({ caId }: Props) => { const { mutateAsync: updateSshCertTemplate } = useUpdateSshCertTemplate(); const onRemoveSshCertificateTemplateSubmit = async (id: string) => { - try { - await deleteSshCertTemplate({ - id - }); + await deleteSshCertTemplate({ + id + }); - await createNotification({ - text: "Successfully deleted SSH certificate template", - type: "success" - }); + createNotification({ + text: "Successfully deleted SSH certificate template", + type: "success" + }); - handlePopUpClose("deleteSshCertificateTemplate"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete SSH certificate template", - type: "error" - }); - } + handlePopUpClose("deleteSshCertificateTemplate"); }; const onUpdateSshCaStatus = async ({ @@ -60,26 +52,16 @@ export const SshCertificateTemplatesSection = ({ caId }: Props) => { templateId: string; status: SshCertTemplateStatus; }) => { - try { - await updateSshCertTemplate({ id: templateId, status }); + await updateSshCertTemplate({ id: templateId, status }); - await createNotification({ - text: `Successfully ${ - status === SshCertTemplateStatus.ACTIVE ? "enabled" : "disabled" - } SSH certificate template`, - type: "success" - }); + createNotification({ + text: `Successfully ${ + status === SshCertTemplateStatus.ACTIVE ? "enabled" : "disabled" + } SSH certificate template`, + type: "success" + }); - handlePopUpClose("sshCertificateTemplateStatus"); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to ${ - status === SshCertTemplateStatus.ACTIVE ? "enabled" : "disabled" - } SSH certificate template`, - type: "error" - }); - } + handlePopUpClose("sshCertificateTemplateStatus"); }; return ( diff --git a/frontend/src/pages/ssh/SshCasPage/components/SshCaModal.tsx b/frontend/src/pages/ssh/SshCasPage/components/SshCaModal.tsx index 4c8ce4e19..53e0298ff 100644 --- a/frontend/src/pages/ssh/SshCasPage/components/SshCaModal.tsx +++ b/frontend/src/pages/ssh/SshCasPage/components/SshCaModal.tsx @@ -106,47 +106,39 @@ export const SshCaModal = ({ popUp, handlePopUpToggle }: Props) => { publicKey, privateKey }: FormData) => { - try { - if (!projectId) return; + if (!projectId) return; - if (ca) { - await updateMutateAsync({ - caId: ca.id, - friendlyName - }); - } else { - const { id: newCaId } = await createMutateAsync({ - projectId, - friendlyName, - keySource, - keyAlgorithm, - publicKey, - privateKey - }); - - navigate({ - to: "/projects/ssh/$projectId/ca/$caId", - params: { - projectId, - caId: newCaId - } - }); - } - - reset(); - handlePopUpToggle("sshCa", false); - - createNotification({ - text: `Successfully ${ca ? "updated" : "created"} SSH CA`, - type: "success" + if (ca) { + await updateMutateAsync({ + caId: ca.id, + friendlyName }); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to ${ca ? "update" : "create"} SSH CA`, - type: "error" + } else { + const { id: newCaId } = await createMutateAsync({ + projectId, + friendlyName, + keySource, + keyAlgorithm, + publicKey, + privateKey + }); + + navigate({ + to: "/projects/ssh/$projectId/ca/$caId", + params: { + projectId, + caId: newCaId + } }); } + + reset(); + handlePopUpToggle("sshCa", false); + + createNotification({ + text: `Successfully ${ca ? "updated" : "created"} SSH CA`, + type: "success" + }); }; return ( diff --git a/frontend/src/pages/ssh/SshCasPage/components/SshCaSection.tsx b/frontend/src/pages/ssh/SshCasPage/components/SshCaSection.tsx index b1d24e8f3..02eb37a38 100644 --- a/frontend/src/pages/ssh/SshCasPage/components/SshCaSection.tsx +++ b/frontend/src/pages/ssh/SshCasPage/components/SshCaSection.tsx @@ -23,41 +23,25 @@ export const SshCaSection = () => { ] as const); const onRemoveSshCaSubmit = async (caId: string) => { - try { - await deleteSshCa({ caId }); + await deleteSshCa({ caId }); - createNotification({ - text: "Successfully deleted SSH CA", - type: "success" - }); + createNotification({ + text: "Successfully deleted SSH CA", + type: "success" + }); - handlePopUpClose("deleteSshCa"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete SSH CA", - type: "error" - }); - } + handlePopUpClose("deleteSshCa"); }; const onUpdateSshCaStatus = async ({ caId, status }: { caId: string; status: SshCaStatus }) => { - try { - await updateSshCa({ caId, status }); + await updateSshCa({ caId, status }); - createNotification({ - text: `Successfully ${status === SshCaStatus.ACTIVE ? "enabled" : "disabled"} SSH CA`, - type: "success" - }); + createNotification({ + text: `Successfully ${status === SshCaStatus.ACTIVE ? "enabled" : "disabled"} SSH CA`, + type: "success" + }); - handlePopUpClose("sshCaStatus"); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to ${status === SshCaStatus.ACTIVE ? "enabled" : "disabled"} SSH CA`, - type: "error" - }); - } + handlePopUpClose("sshCaStatus"); }; return ( diff --git a/frontend/src/pages/ssh/SshHostGroupDetailsByIDPage/SshHostGroupDetailsByIDPage.tsx b/frontend/src/pages/ssh/SshHostGroupDetailsByIDPage/SshHostGroupDetailsByIDPage.tsx index d093003ac..ffb596ac2 100644 --- a/frontend/src/pages/ssh/SshHostGroupDetailsByIDPage/SshHostGroupDetailsByIDPage.tsx +++ b/frontend/src/pages/ssh/SshHostGroupDetailsByIDPage/SshHostGroupDetailsByIDPage.tsx @@ -44,30 +44,22 @@ const Page = () => { ] as const); const onRemoveSshGroupSubmit = async (groupIdToDelete: string) => { - try { - if (!projectId) return; + if (!projectId) return; - await deleteSshHostGroup({ sshHostGroupId: groupIdToDelete }); + await deleteSshHostGroup({ sshHostGroupId: groupIdToDelete }); - createNotification({ - text: "Successfully deleted SSH group", - type: "success" - }); + createNotification({ + text: "Successfully deleted SSH group", + type: "success" + }); - handlePopUpClose("deleteSshHostGroup"); - navigate({ - to: "/projects/ssh/$projectId/overview", - params: { - projectId - } - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete SSH group", - type: "error" - }); - } + handlePopUpClose("deleteSshHostGroup"); + navigate({ + to: "/projects/ssh/$projectId/overview", + params: { + projectId + } + }); }; return ( diff --git a/frontend/src/pages/ssh/SshHostGroupDetailsByIDPage/components/AddHostGroupMemberModal.tsx b/frontend/src/pages/ssh/SshHostGroupDetailsByIDPage/components/AddHostGroupMemberModal.tsx index 2374492a5..e45266fa0 100644 --- a/frontend/src/pages/ssh/SshHostGroupDetailsByIDPage/components/AddHostGroupMemberModal.tsx +++ b/frontend/src/pages/ssh/SshHostGroupDetailsByIDPage/components/AddHostGroupMemberModal.tsx @@ -42,30 +42,23 @@ export const AddHostGroupMemberModal = ({ popUp, handlePopUpToggle }: Props) => useAddHostToSshHostGroup(); const handleAddHost = async (sshHostId: string) => { - try { - if (!popUpData?.sshHostGroupId) { - createNotification({ - text: "Some data is missing, please refresh the page and try again", - type: "error" - }); - return; - } - - await addHostToSshHostGroup({ - sshHostGroupId: popUpData.sshHostGroupId, - sshHostId - }); - + if (!popUpData?.sshHostGroupId) { createNotification({ - text: "Successfully added host to the group", - type: "success" - }); - } catch { - createNotification({ - text: "Failed to add host to the group", + text: "Some data is missing, please refresh the page and try again", type: "error" }); + return; } + + await addHostToSshHostGroup({ + sshHostGroupId: popUpData.sshHostGroupId, + sshHostId + }); + + createNotification({ + text: "Successfully added host to the group", + type: "success" + }); }; return ( diff --git a/frontend/src/pages/ssh/SshHostsPage/components/SshHostGroupModal.tsx b/frontend/src/pages/ssh/SshHostsPage/components/SshHostGroupModal.tsx index 6e65dd532..d7411f889 100644 --- a/frontend/src/pages/ssh/SshHostsPage/components/SshHostGroupModal.tsx +++ b/frontend/src/pages/ssh/SshHostsPage/components/SshHostGroupModal.tsx @@ -121,67 +121,59 @@ export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => { }, [sshHostGroup]); const onFormSubmit = async ({ name, loginMappings }: FormData) => { - try { - if (!projectId) return; + if (!projectId) return; - // check if there is already a different host group with the same name - const existingNames = - sshHostGroups?.filter((h) => h.id !== sshHostGroup?.id).map((h) => h.name) || []; - - if (existingNames.includes(name.trim())) { - createNotification({ - text: "A host group with this name already exists.", - type: "error" - }); - return; - } - - const transformedLoginMappings = loginMappings.map(({ loginUser, allowedPrincipals }) => { - const usernames = allowedPrincipals - .filter((p) => p.type === "user" && p.value) - .map((p) => p.value); - - const groupNames = allowedPrincipals - .filter((p) => p.type === "group" && p.value) - .map((p) => p.value); - - return { - loginUser, - allowedPrincipals: { - usernames, - groups: groupNames - } - }; - }); - - if (sshHostGroup) { - await updateMutateAsync({ - sshHostGroupId: sshHostGroup.id, - name, - loginMappings: transformedLoginMappings - }); - } else { - await createMutateAsync({ - projectId, - name, - loginMappings: transformedLoginMappings - }); - } - - reset(); - handlePopUpToggle("sshHostGroup", false); + // check if there is already a different host group with the same name + const existingNames = + sshHostGroups?.filter((h) => h.id !== sshHostGroup?.id).map((h) => h.name) || []; + if (existingNames.includes(name.trim())) { createNotification({ - text: `Successfully ${sshHostGroup ? "updated" : "created"} SSH host group`, - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to ${sshHostGroup ? "update" : "create"} SSH host group`, + text: "A host group with this name already exists.", type: "error" }); + return; } + + const transformedLoginMappings = loginMappings.map(({ loginUser, allowedPrincipals }) => { + const usernames = allowedPrincipals + .filter((p) => p.type === "user" && p.value) + .map((p) => p.value); + + const groupNames = allowedPrincipals + .filter((p) => p.type === "group" && p.value) + .map((p) => p.value); + + return { + loginUser, + allowedPrincipals: { + usernames, + groups: groupNames + } + }; + }); + + if (sshHostGroup) { + await updateMutateAsync({ + sshHostGroupId: sshHostGroup.id, + name, + loginMappings: transformedLoginMappings + }); + } else { + await createMutateAsync({ + projectId, + name, + loginMappings: transformedLoginMappings + }); + } + + reset(); + handlePopUpToggle("sshHostGroup", false); + + createNotification({ + text: `Successfully ${sshHostGroup ? "updated" : "created"} SSH host group`, + type: "success" + }); }; const toggleMapping = (index: number) => { diff --git a/frontend/src/pages/ssh/SshHostsPage/components/SshHostsSection.tsx b/frontend/src/pages/ssh/SshHostsPage/components/SshHostsSection.tsx index 0f57cbeb2..53e8062ab 100644 --- a/frontend/src/pages/ssh/SshHostsPage/components/SshHostsSection.tsx +++ b/frontend/src/pages/ssh/SshHostsPage/components/SshHostsSection.tsx @@ -20,22 +20,14 @@ export const SshHostsSection = () => { ] as const); const onRemoveSshHostSubmit = async (sshHostId: string) => { - try { - const host = await deleteSshHost({ sshHostId }); + const host = await deleteSshHost({ sshHostId }); - createNotification({ - text: `Successfully deleted SSH host: ${host.hostname}`, - type: "success" - }); + createNotification({ + text: `Successfully deleted SSH host: ${host.hostname}`, + type: "success" + }); - handlePopUpClose("deleteSshHost"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete SSH host", - type: "error" - }); - } + handlePopUpClose("deleteSshHost"); }; return ( diff --git a/frontend/src/pages/user/PersonalSettingsPage/components/APIKeySection/APIKeyTable.tsx b/frontend/src/pages/user/PersonalSettingsPage/components/APIKeySection/APIKeyTable.tsx index 6fe6afd43..cf2960ada 100644 --- a/frontend/src/pages/user/PersonalSettingsPage/components/APIKeySection/APIKeyTable.tsx +++ b/frontend/src/pages/user/PersonalSettingsPage/components/APIKeySection/APIKeyTable.tsx @@ -22,19 +22,11 @@ export const APIKeyTable = () => { const { mutateAsync } = useDeleteAPIKey(); const handleDeleteAPIKeyDataClick = async (apiKeyDataId: string) => { - try { - await mutateAsync(apiKeyDataId); - createNotification({ - text: "Successfully deleted API key", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete API key", - type: "error" - }); - } + await mutateAsync(apiKeyDataId); + createNotification({ + text: "Successfully deleted API key", + type: "success" + }); }; return ( diff --git a/frontend/src/pages/user/PersonalSettingsPage/components/APIKeySection/AddAPIKeyModal.tsx b/frontend/src/pages/user/PersonalSettingsPage/components/APIKeySection/AddAPIKeyModal.tsx index 213479b47..f28c39339 100644 --- a/frontend/src/pages/user/PersonalSettingsPage/components/APIKeySection/AddAPIKeyModal.tsx +++ b/frontend/src/pages/user/PersonalSettingsPage/components/APIKeySection/AddAPIKeyModal.tsx @@ -76,27 +76,19 @@ export const AddAPIKeyModal = ({ popUp, handlePopUpToggle }: Props) => { }; const onFormSubmit = async ({ name, expiresIn }: FormData) => { - try { - const { apiKey } = await mutateAsync({ - name, - expiresIn: expirationMapping[expiresIn] - }); + const { apiKey } = await mutateAsync({ + name, + expiresIn: expirationMapping[expiresIn] + }); - setNewAPIKey(apiKey); + setNewAPIKey(apiKey); - createNotification({ - text: "Successfully created API key", - type: "success" - }); + createNotification({ + text: "Successfully created API key", + type: "success" + }); - reset(); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to create API key", - type: "error" - }); - } + reset(); }; const hasAPIKey = Boolean(newAPIKey); From 4eb24c6025aa7f1d39f40f52ec8372093f0eb970 Mon Sep 17 00:00:00 2001 From: x032205 Date: Mon, 3 Nov 2025 14:15:42 -0500 Subject: [PATCH 022/294] remove domain input from users --- .../components/GatewayCliDeploymentMethod.tsx | 26 +++---------------- .../components/RelayCliDeploymentMethod.tsx | 25 +++--------------- .../RelayTerraformDeploymentMethod.tsx | 23 +++------------- 3 files changed, 11 insertions(+), 63 deletions(-) diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliDeploymentMethod.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliDeploymentMethod.tsx index 7537d95a3..813094ae6 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliDeploymentMethod.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliDeploymentMethod.tsx @@ -36,7 +36,6 @@ import { RelayOption } from "./RelayOption"; const baseFormSchema = z.object({ name: slugSchema({ field: "name" }), - instanceDomain: z.string().url("Must be a valid URL").or(z.literal("")), relay: z .object( { @@ -78,7 +77,6 @@ export const GatewayCliDeploymentMethod = () => { const [autogenerateToken, setAutogenerateToken] = useState(true); const [step, setStep] = useState<"form" | "command">("form"); const [name, setName] = useState(""); - const [instanceDomain, setInstanceDomain] = useState(siteURL); const [relay, setRelay] = useState { const validation = formSchemaWithIdentity.safeParse({ name, relay, - identity, - instanceDomain + identity }); if (!validation.success) { setFormErrors(validation.error.issues); @@ -180,8 +177,7 @@ export const GatewayCliDeploymentMethod = () => { const validation = formSchemaWithToken.safeParse({ name, relay, - identityToken, - instanceDomain + identityToken }); if (!validation.success) { setFormErrors(validation.error.issues); @@ -192,11 +188,10 @@ export const GatewayCliDeploymentMethod = () => { }; const command = useMemo(() => { - const domainFlag = instanceDomain ? ` --domain=${instanceDomain}` : ""; return `infisical gateway start --name=${name} --relay=${ relay?.name || "" - }${domainFlag} --token=${identityToken}`; - }, [name, relay, identityToken, instanceDomain]); + } --domain=${siteURL} --token=${identityToken}`; + }, [name, relay, identityToken, siteURL]); if (step === "command") { return ( @@ -279,19 +274,6 @@ export const GatewayCliDeploymentMethod = () => { /> {errors.relay &&

{errors.relay}

} - - setInstanceDomain(e.target.value)} - placeholder="https://app.infisical.com" - isError={Boolean(errors.instanceDomain)} - /> - {errors.instanceDomain &&

{errors.instanceDomain}

} - {canCreateToken && autogenerateToken ? ( <> { const [name, setName] = useState(""); const [host, setHost] = useState(""); - const [instanceDomain, setInstanceDomain] = useState(siteURL); const [identity, setIdentity] = useState { setFormErrors([]); if (canCreateToken && autogenerateToken) { - const validation = formSchemaWithIdentity.safeParse({ name, host, instanceDomain, identity }); + const validation = formSchemaWithIdentity.safeParse({ name, host, identity }); if (!validation.success) { setFormErrors(validation.error.issues); return; @@ -153,7 +151,6 @@ export const RelayCliDeploymentMethod = () => { const validation = formSchemaWithToken.safeParse({ name, host, - instanceDomain, identityToken }); if (!validation.success) { @@ -174,9 +171,8 @@ export const RelayCliDeploymentMethod = () => { }; const command = useMemo(() => { - const domainFlag = instanceDomain ? ` --domain=${instanceDomain}` : ""; - return `infisical relay start --name=${name}${domainFlag} --host=${host} --token=${identityToken}`; - }, [name, instanceDomain, host, identityToken]); + return `infisical relay start --name=${name} --domain=${siteURL} --host=${host} --token=${identityToken}`; + }, [name, siteURL, host, identityToken]); if (step === "command") { return ( @@ -244,19 +240,6 @@ export const RelayCliDeploymentMethod = () => { /> {errors.host &&

{errors.host}

} - - setInstanceDomain(e.target.value)} - placeholder="https://app.infisical.com" - isError={Boolean(errors.instanceDomain)} - /> - {errors.instanceDomain &&

{errors.instanceDomain}

} - {canCreateToken && autogenerateToken ? ( <> { const [step, setStep] = useState<"form" | "command">("form"); const [name, setName] = useState(""); - const [instanceDomain, setInstanceDomain] = useState(siteURL); const [identity, setIdentity] = useState { } else { const validation = formSchemaWithToken.safeParse({ name, - instanceDomain, identityToken }); if (!validation.success) { @@ -202,7 +199,6 @@ export const RelayTerraformDeploymentMethod = () => { }; const terraformCommand = useMemo(() => { - const domain = instanceDomain || "https://app.infisical.com"; return `terraform { required_providers { aws = { @@ -293,7 +289,7 @@ module "infisical_relay_instance" { export INFISICAL_TOKEN="${identityToken}" sudo -E infisical relay systemd install \\ --name "${name}" \\ - --domain "${domain}" \\ + --domain "${siteURL}" \\ --host "\${aws_eip.infisical_relay_eip.public_ip}" # Start and enable the service to run on boot @@ -308,7 +304,7 @@ resource "aws_eip_association" "eip_assoc" { allocation_id = aws_eip.infisical_relay_eip.id } `; - }, [name, instanceDomain, identityToken, awsRegion, vpcId, ami, subnetId]); + }, [name, siteURL, identityToken, awsRegion, vpcId, ami, subnetId]); if (step === "command") { return ( @@ -358,19 +354,6 @@ resource "aws_eip_association" "eip_assoc" { /> {errors.name &&

{errors.name}

} - - setInstanceDomain(e.target.value)} - placeholder="https://app.infisical.com" - isError={Boolean(errors.instanceDomain)} - /> - {errors.instanceDomain &&

{errors.instanceDomain}

} - {canCreateToken && autogenerateToken ? ( <> Date: Mon, 3 Nov 2025 14:28:06 -0500 Subject: [PATCH 023/294] use dropdown for AWS regions --- .../RelayTerraformDeploymentMethod.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayTerraformDeploymentMethod.tsx b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayTerraformDeploymentMethod.tsx index 65f4c436e..94f7c5748 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayTerraformDeploymentMethod.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayTerraformDeploymentMethod.tsx @@ -29,6 +29,7 @@ import { useGetIdentityTokenAuth } from "@app/hooks/api"; import { slugSchema } from "@app/lib/schemas"; +import { AWS_REGIONS } from "@app/helpers/appConnections"; const baseFormSchema = z.object({ name: slugSchema({ field: "name" }) @@ -117,7 +118,7 @@ export const RelayTerraformDeploymentMethod = () => { setFormErrors([]); if (canCreateToken && autogenerateToken) { - const validation = formSchemaWithIdentity.safeParse({ name, instanceDomain, identity }); + const validation = formSchemaWithIdentity.safeParse({ name, identity }); if (!validation.success) { setFormErrors(validation.error.issues); return; @@ -443,11 +444,16 @@ resource "aws_eip_association" "eip_assoc" { - setAwsRegion(e.target.value)} - placeholder="us-east-1" - isError={Boolean(errors.awsRegion)} + r.slug === awsRegion)} + onChange={(selected) => { + if (selected) { + setAwsRegion((selected as SingleValue<{ slug: string; name: string }>)!.slug); + } + }} + options={AWS_REGIONS} + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.slug} /> {errors.awsRegion &&

{errors.awsRegion}

} From c3248a0c4c466242d679c97ff8b485601c98aa56 Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Mon, 3 Nov 2025 16:30:53 -0300 Subject: [PATCH 024/294] Refactor migration to use TableName constants for project_slack_configs schema changes --- ...ure-slack-secret-sync-error-notification.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/backend/src/db/migrations/20251024184713_feature-slack-secret-sync-error-notification.ts b/backend/src/db/migrations/20251024184713_feature-slack-secret-sync-error-notification.ts index 4b5bf54d3..203333fed 100644 --- a/backend/src/db/migrations/20251024184713_feature-slack-secret-sync-error-notification.ts +++ b/backend/src/db/migrations/20251024184713_feature-slack-secret-sync-error-notification.ts @@ -1,28 +1,30 @@ import { Knex } from "knex"; +import { TableName } from "@app/db/schemas"; + export async function up(knex: Knex): Promise { - if (!(await knex.schema.hasColumn("project_slack_configs", "isSecretSyncErrorNotificationEnabled"))) { - await knex.schema.alterTable("project_slack_configs", (table) => { + if (!(await knex.schema.hasColumn(TableName.ProjectSlackConfigs, "isSecretSyncErrorNotificationEnabled"))) { + await knex.schema.alterTable(TableName.ProjectSlackConfigs, (table) => { table.boolean("isSecretSyncErrorNotificationEnabled").notNullable().defaultTo(false); }); } - if (!(await knex.schema.hasColumn("project_slack_configs", "secretSyncErrorChannels"))) { - await knex.schema.alterTable("project_slack_configs", (table) => { + if (!(await knex.schema.hasColumn(TableName.ProjectSlackConfigs, "secretSyncErrorChannels"))) { + await knex.schema.alterTable(TableName.ProjectSlackConfigs, (table) => { table.text("secretSyncErrorChannels").notNullable().defaultTo(""); }); } } export async function down(knex: Knex): Promise { - if (await knex.schema.hasColumn("project_slack_configs", "isSecretSyncErrorNotificationEnabled")) { - await knex.schema.alterTable("project_slack_configs", (table) => { + if (await knex.schema.hasColumn(TableName.ProjectSlackConfigs, "isSecretSyncErrorNotificationEnabled")) { + await knex.schema.alterTable(TableName.ProjectSlackConfigs, (table) => { table.dropColumn("isSecretSyncErrorNotificationEnabled"); }); } - if (await knex.schema.hasColumn("project_slack_configs", "secretSyncErrorChannels")) { - await knex.schema.alterTable("project_slack_configs", (table) => { + if (await knex.schema.hasColumn(TableName.ProjectSlackConfigs, "secretSyncErrorChannels")) { + await knex.schema.alterTable(TableName.ProjectSlackConfigs, (table) => { table.dropColumn("secretSyncErrorChannels"); }); } From 7ddeab3bd2a452b65f29b4f7ac7086ccbd35f393 Mon Sep 17 00:00:00 2001 From: x032205 Date: Mon, 3 Nov 2025 14:32:08 -0500 Subject: [PATCH 025/294] lint --- .../RelayTab/components/RelayTerraformDeploymentMethod.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayTerraformDeploymentMethod.tsx b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayTerraformDeploymentMethod.tsx index 94f7c5748..a8e1693a5 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayTerraformDeploymentMethod.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayTerraformDeploymentMethod.tsx @@ -2,6 +2,7 @@ import { useMemo, useState } from "react"; import { SingleValue } from "react-select"; import { faCopy, faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Tab } from "@headlessui/react"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; @@ -15,13 +16,13 @@ import { ModalClose, Tooltip } from "@app/components/v2"; -import { Tab } from "@headlessui/react"; import { OrgPermissionIdentityActions, OrgPermissionSubjects, useOrganization, useOrgPermission } from "@app/context"; +import { AWS_REGIONS } from "@app/helpers/appConnections"; import { useAddIdentityTokenAuth, useCreateTokenIdentityTokenAuth, @@ -29,7 +30,6 @@ import { useGetIdentityTokenAuth } from "@app/hooks/api"; import { slugSchema } from "@app/lib/schemas"; -import { AWS_REGIONS } from "@app/helpers/appConnections"; const baseFormSchema = z.object({ name: slugSchema({ field: "name" }) From 70b2fb844d5b261561866f6e4b15d62d390d0869 Mon Sep 17 00:00:00 2001 From: x032205 Date: Mon, 3 Nov 2025 14:38:56 -0500 Subject: [PATCH 026/294] lint --- backend/src/ee/services/gateway-v2/gateway-v2-service.ts | 4 ++-- backend/src/ee/services/relay/relay-service.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/ee/services/gateway-v2/gateway-v2-service.ts b/backend/src/ee/services/gateway-v2/gateway-v2-service.ts index c17f507ae..22daa1cd1 100644 --- a/backend/src/ee/services/gateway-v2/gateway-v2-service.ts +++ b/backend/src/ee/services/gateway-v2/gateway-v2-service.ts @@ -3,7 +3,7 @@ import net from "node:net"; import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; -import { OrganizationActionScope, OrgMembershipRole, TRelays } from "@app/db/schemas"; +import { OrganizationActionScope, OrgMembershipRole, OrgMembershipStatus, TRelays } from "@app/db/schemas"; import { PgSqlLock } from "@app/keystore/keystore"; import { crypto } from "@app/lib/crypto"; import { DatabaseErrorCode } from "@app/lib/error-codes"; @@ -910,7 +910,7 @@ export const gatewayV2ServiceFactory = ({ for await (const [orgId, gateways] of Object.entries(gatewaysByOrg)) { try { const admins = (await orgDAL.findOrgMembersByRole(orgId, OrgMembershipRole.Admin)).filter( - (admin) => admin.status !== "invited" + (admin) => admin.status !== OrgMembershipStatus.Invited ); if (admins.length === 0) { logger.warn({ orgId }, "Organization has no admins to notify about unhealthy gateway."); diff --git a/backend/src/ee/services/relay/relay-service.ts b/backend/src/ee/services/relay/relay-service.ts index 833c2d420..749a3ca16 100644 --- a/backend/src/ee/services/relay/relay-service.ts +++ b/backend/src/ee/services/relay/relay-service.ts @@ -3,7 +3,7 @@ import { isIP } from "node:net"; import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; -import { OrganizationActionScope, OrgMembershipRole, TRelays } from "@app/db/schemas"; +import { OrganizationActionScope, OrgMembershipRole, OrgMembershipStatus, TRelays } from "@app/db/schemas"; import { PgSqlLock } from "@app/keystore/keystore"; import { crypto } from "@app/lib/crypto"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; @@ -1249,7 +1249,7 @@ export const relayServiceFactory = ({ } } else { const admins = (await orgDAL.findOrgMembersByRole(orgId, OrgMembershipRole.Admin)).filter( - (admin) => admin.status !== "invited" + (admin) => admin.status !== OrgMembershipStatus.Invited ); if (admins.length === 0) { // eslint-disable-next-line no-continue From 88e6c5badfd88ab9c6eb84c50825738a147d3d7f Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Mon, 3 Nov 2025 16:40:50 -0300 Subject: [PATCH 027/294] Update SlackConfigRow component to improve error notification display and styling --- .../components/SlackConfigRow.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackConfigRow.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackConfigRow.tsx index f4ba81823..afabe826c 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackConfigRow.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackConfigRow.tsx @@ -100,16 +100,19 @@ export const SlackConfigRow = ({ handlePopUpOpen, isSlackConfigLoading, slackCon {slackConfig.isSecretSyncErrorNotificationEnabled && !isLoadingConfig && slackConfig.secretSyncErrorChannels.length > 0 ? ( - +

{slackConfig.secretSyncErrorChannels .split(", ") .map((channel) => slackChannelIdToName[channel]) .join(", ")} - +

) : isLoadingConfig ? ( ) : ( - Disabled + + + Disabled + )} From edd34f5561cec61aa2f9b76579753231f582ba56 Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Mon, 3 Nov 2025 16:43:12 -0300 Subject: [PATCH 028/294] refactor: simplify user removal logic and enhance notification handling in UserProjectsSection --- .../UserProjectsSection.tsx | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserProjectsSection/UserProjectsSection.tsx b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserProjectsSection/UserProjectsSection.tsx index 843b5a63c..6e128036b 100644 --- a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserProjectsSection/UserProjectsSection.tsx +++ b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserProjectsSection/UserProjectsSection.tsx @@ -31,19 +31,11 @@ export const UserProjectsSection = ({ membershipId }: Props) => { ] as const); const handleRemoveUser = async (projectId: string, username: string) => { - try { - await removeUserFromWorkspace({ projectId, usernames: [username], orgId }); - createNotification({ - text: "Successfully removed user from project", - type: "success" - }); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to remove user from the project", - type: "error" - }); - } + await removeUserFromWorkspace({ projectId, usernames: [username], orgId }); + createNotification({ + text: "Successfully removed user from project", + type: "success" + }); handlePopUpClose("removeUserFromProject"); }; From a73ba232ab0f42c30032ab1d0abce80679ec4b42 Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Mon, 3 Nov 2025 18:24:35 -0300 Subject: [PATCH 029/294] refactor: enhance notification handling and streamline async logic across secret management components --- .../components/IPAllowlistModal.tsx | 51 +++--- .../components/IPAllowlistSection.tsx | 28 +-- .../SecretSyncTable/SecretSyncsTable.tsx | 52 ++---- .../SecretV2MigrationSection.tsx | 21 +-- .../components/EditAccessRequestModal.tsx | 34 ++-- .../components/ReviewAccessModal.tsx | 3 +- .../components/AccessPolicyModal.tsx | 140 +++++++------- .../components/RemoveApprovalPolicyModal.tsx | 33 ++-- .../SecretApprovalRequestAction.tsx | 52 ++---- .../SecretApprovalRequestChanges.tsx | 26 +-- .../SecretDashboardPage.tsx | 64 +++---- .../components/ActionBar/ActionBar.tsx | 140 ++++++-------- .../AwsElastiCacheInputForm.tsx | 32 ++-- .../AwsIamInputForm.tsx | 34 ++-- .../AzureEntraIdInputForm.tsx | 48 ++--- .../AzureSqlDatabaseInputForm.tsx | 40 ++-- .../CassandraInputForm.tsx | 32 ++-- .../CouchbaseInputForm.tsx | 32 ++-- .../ElasticSearchInputForm.tsx | 32 ++-- .../GcpIamInputForm.tsx | 38 ++-- .../GithubInputForm.tsx | 36 ++-- .../KubernetesInputForm.tsx | 45 ++--- .../CreateDynamicSecretForm/LdapInputForm.tsx | 32 ++-- .../MongoAtlasInputForm.tsx | 32 ++-- .../MongoDBInputForm.tsx | 46 ++--- .../RabbitMqInputForm.tsx | 32 ++-- .../RedisInputForm.tsx | 34 ++-- .../SapAseInputForm.tsx | 34 ++-- .../SapHanaInputForm.tsx | 34 ++-- .../SnowflakeInputForm.tsx | 34 ++-- .../SqlDatabaseInputForm.tsx | 34 ++-- .../CreateDynamicSecretForm/TotpInputForm.tsx | 28 +-- .../VerticaInputForm.tsx | 34 ++-- .../ActionBar/CreateSecretImportForm.tsx | 5 - .../CreateDynamicSecretLease.tsx | 72 +++----- .../DynamicSecretLease.tsx | 42 ++--- .../DynamicSecretListView.tsx | 38 ++-- ...ynamicSecretAwsElastiCacheProviderForm.tsx | 43 ++--- .../EditDynamicSecretAwsIamForm.tsx | 43 ++--- .../EditDynamicSecretAzureEntraIdForm.tsx | 41 ++--- .../EditDynamicSecretAzureSqlDatabaseForm.tsx | 47 ++--- .../EditDynamicSecretCassandraForm.tsx | 43 ++--- .../EditDynamicSecretCouchbaseForm.tsx | 51 +++--- .../EditDynamicSecretElasticSearchForm.tsx | 43 ++--- .../EditDynamicSecretGcpIamForm.tsx | 41 ++--- .../EditDynamicSecretGithubForm.tsx | 37 ++-- .../EditDynamicSecretKubernetesForm.tsx | 53 +++--- .../EditDynamicSecretLdapForm.tsx | 43 ++--- .../EditDynamicSecretMongoAtlasForm.tsx | 43 ++--- .../EditDynamicSecretMongoDBForm.tsx | 52 +++--- .../EditDynamicSecretRabbitMqForm.tsx | 43 ++--- .../EditDynamicSecretRedisProviderForm.tsx | 44 ++--- .../EditDynamicSecretSapAseForm.tsx | 43 ++--- .../EditDynamicSecretSapHanaForm.tsx | 43 ++--- .../EditDynamicSecretSnowflakeForm.tsx | 45 ++--- .../EditDynamicSecretSqlProviderForm.tsx | 53 +++--- .../EditDynamicSecretTotpForm.tsx | 37 ++-- .../EditDynamicSecretVertica.tsx | 50 +++-- .../RenewDynamicSecretLease.tsx | 34 ++-- .../FolderListView/FolderListView.tsx | 172 ++++++++---------- .../SecretImportListView/SecretImportItem.tsx | 28 +-- .../SecretImportListView.tsx | 30 ++- .../SecretListView/CreateReminderForm.tsx | 62 +++---- .../components/SnapshotView/SnapshotView.tsx | 30 ++- .../CreateRotationForm/CreateRotationForm.tsx | 35 ++-- 65 files changed, 1159 insertions(+), 1714 deletions(-) diff --git a/frontend/src/pages/secret-manager/IPAllowlistPage/components/IPAllowlistModal.tsx b/frontend/src/pages/secret-manager/IPAllowlistPage/components/IPAllowlistModal.tsx index 36f585850..ea6b604dc 100644 --- a/frontend/src/pages/secret-manager/IPAllowlistPage/components/IPAllowlistModal.tsx +++ b/frontend/src/pages/secret-manager/IPAllowlistPage/components/IPAllowlistModal.tsx @@ -64,39 +64,32 @@ export const IPAllowlistModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: }, [popUp?.trustedIp?.data]); const onIPAllowlistModalSubmit = async ({ ipAddress, comment }: FormData) => { - try { - if (!currentProject?.id) return; + if (!currentProject?.id) return; - if (popUp?.trustedIp?.data) { - await updateTrustedIp.mutateAsync({ - projectId: currentProject.id, - trustedIpId: (popUp?.trustedIp?.data as { trustedIpId: string })?.trustedIpId, - ipAddress, - comment, - isActive: true - }); - } else { - await addTrustedIp.mutateAsync({ - projectId: currentProject.id, - ipAddress, - comment, - isActive: true - }); - } - - createNotification({ - text: `Successfully ${popUp?.trustedIp?.data ? "updated" : "added"} trusted IP`, - type: "success" + if (popUp?.trustedIp?.data) { + await updateTrustedIp.mutateAsync({ + projectId: currentProject.id, + trustedIpId: (popUp?.trustedIp?.data as { trustedIpId: string })?.trustedIpId, + ipAddress, + comment, + isActive: true }); - - reset(); - handlePopUpClose("trustedIp"); - } catch { - createNotification({ - text: `Failed to ${popUp?.trustedIp?.data ? "update" : "add"} trusted IP`, - type: "error" + } else { + await addTrustedIp.mutateAsync({ + projectId: currentProject.id, + ipAddress, + comment, + isActive: true }); } + + createNotification({ + text: `Successfully ${popUp?.trustedIp?.data ? "updated" : "added"} trusted IP`, + type: "success" + }); + + reset(); + handlePopUpClose("trustedIp"); }; return ( diff --git a/frontend/src/pages/secret-manager/IPAllowlistPage/components/IPAllowlistSection.tsx b/frontend/src/pages/secret-manager/IPAllowlistPage/components/IPAllowlistSection.tsx index 470f09aef..b076618b0 100644 --- a/frontend/src/pages/secret-manager/IPAllowlistPage/components/IPAllowlistSection.tsx +++ b/frontend/src/pages/secret-manager/IPAllowlistPage/components/IPAllowlistSection.tsx @@ -29,27 +29,19 @@ export const IPAllowlistSection = () => { ] as const); const onDeleteTrustedIpSubmit = async (trustedIpId: string) => { - try { - if (!currentProject?.id) return; + if (!currentProject?.id) return; - await mutateAsync({ - projectId: currentProject.id, - trustedIpId - }); + await mutateAsync({ + projectId: currentProject.id, + trustedIpId + }); - createNotification({ - text: "Successfully deleted IP access range", - type: "success" - }); + createNotification({ + text: "Successfully deleted IP access range", + type: "success" + }); - handlePopUpClose("deleteTrustedIp"); - } catch (err) { - console.log(err); - createNotification({ - text: "Failed to delete IP access range", - type: "error" - }); - } + handlePopUpClose("deleteTrustedIp"); }; return ( diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncsTable.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncsTable.tsx index bdf92ef7f..93a22feb8 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncsTable.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncsTable.tsx @@ -235,46 +235,32 @@ export const SecretSyncsTable = ({ secretSyncs }: Props) => { const isAutoSyncEnabled = !secretSync.isAutoSyncEnabled; - try { - await updateSync.mutateAsync({ - syncId: secretSync.id, - destination: secretSync.destination, - isAutoSyncEnabled, - projectId: secretSync.projectId - }); + await updateSync.mutateAsync({ + syncId: secretSync.id, + destination: secretSync.destination, + isAutoSyncEnabled, + projectId: secretSync.projectId + }); - createNotification({ - text: `Successfully ${isAutoSyncEnabled ? "enabled" : "disabled"} auto-sync for ${destinationName} Sync`, - type: "success" - }); - } catch { - createNotification({ - text: `Failed to ${isAutoSyncEnabled ? "enable" : "disable"} auto-sync for ${destinationName} Sync`, - type: "error" - }); - } + createNotification({ + text: `Successfully ${isAutoSyncEnabled ? "enabled" : "disabled"} auto-sync for ${destinationName} Sync`, + type: "success" + }); }; const handleTriggerSync = async (secretSync: TSecretSync) => { const destinationName = SECRET_SYNC_MAP[secretSync.destination].name; - try { - await triggerSync.mutateAsync({ - syncId: secretSync.id, - destination: secretSync.destination, - projectId: secretSync.projectId - }); + await triggerSync.mutateAsync({ + syncId: secretSync.id, + destination: secretSync.destination, + projectId: secretSync.projectId + }); - createNotification({ - text: `Successfully triggered ${destinationName} Sync`, - type: "success" - }); - } catch { - createNotification({ - text: `Failed to trigger ${destinationName} Sync`, - type: "error" - }); - } + createNotification({ + text: `Successfully triggered ${destinationName} Sync`, + type: "success" + }); }; return ( diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretV2MigrationSection/SecretV2MigrationSection.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretV2MigrationSection/SecretV2MigrationSection.tsx index 98feebb7e..79a658717 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretV2MigrationSection/SecretV2MigrationSection.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretV2MigrationSection/SecretV2MigrationSection.tsx @@ -65,20 +65,13 @@ export const SecretV2MigrationSection = () => { const didProjectUpgradeFailed = workspaceDetails?.upgradeStatus === ProjectUpgradeStatus.Failed; const handleMigrationSecretV2 = async () => { - try { - handlePopUpToggle("migrationInfo"); - await migrateProjectToV3.mutateAsync({ projectId: currentProject?.id || "" }); - refetch(); - createNotification({ - text: "Project upgrade started", - type: "success" - }); - } catch { - createNotification({ - text: "Failed to upgrade project", - type: "error" - }); - } + handlePopUpToggle("migrationInfo"); + await migrateProjectToV3.mutateAsync({ projectId: currentProject?.id || "" }); + refetch(); + createNotification({ + text: "Project upgrade started", + type: "success" + }); }; const isAdmin = hasProjectRole(ProjectMembershipRole.Admin); diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/EditAccessRequestModal.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/EditAccessRequestModal.tsx index 430ba080e..922a5b8b1 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/EditAccessRequestModal.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/EditAccessRequestModal.tsx @@ -65,28 +65,20 @@ const Content = ({ accessRequest, onComplete, projectSlug }: ContentProps) => { }); const onSubmit = async (form: FormData) => { - try { - const request = await update.mutateAsync({ - requestId: accessRequest.id, - projectSlug, - ...form - }); - await queryClient.refetchQueries({ - queryKey: accessApprovalKeys.getAccessApprovalPolicies(projectSlug) - }); + const request = await update.mutateAsync({ + requestId: accessRequest.id, + projectSlug, + ...form + }); + await queryClient.refetchQueries({ + queryKey: accessApprovalKeys.getAccessApprovalPolicies(projectSlug) + }); - createNotification({ - type: "success", - text: "Access request updated successfully." - }); - onComplete(request); - } catch (e) { - console.error(e); - createNotification({ - type: "error", - text: "Failed to update access request" - }); - } + createNotification({ + type: "success", + text: "Access request updated successfully." + }); + onComplete(request); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx index 6a04225b4..a86ca8ebb 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx @@ -178,8 +178,7 @@ export const ReviewAccessRequestModal = ({ text: `The request has been ${status}`, type: status === "approved" ? "success" : "info" }); - } catch (error) { - console.error(error); + } catch { setIsLoading(null); return; } diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx index 30998aff4..67385a58b 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx @@ -237,48 +237,40 @@ const Form = ({ }: TFormSchema) => { if (!projectId) return; - try { - const bypassers = [...userBypassers, ...groupBypassers]; + const bypassers = [...userBypassers, ...groupBypassers]; - if (data.policyType === PolicyType.ChangePolicy) { - await createSecretApprovalPolicy({ - ...data, - approvers: [...userApprovers, ...groupApprovers], - bypassers: bypassers.length > 0 ? bypassers : undefined, - environments: environments.map((env) => env.slug), - projectId: currentProject?.id || "" - }); - } else { - await createAccessApprovalPolicy({ - ...data, - approvers: sequenceApprovers?.flatMap((approvers, index) => - approvers.user - .map( - (el) => ({ ...el, sequence: index + 1 }) as Omit - ) - .concat(approvers.group.map((el) => ({ ...el, sequence: index + 1 }))) - ), - approvalsRequired: sequenceApprovers?.map((el, index) => ({ - stepNumber: index + 1, - numberOfApprovals: el.approvals - })), - bypassers: bypassers.length > 0 ? bypassers : undefined, - environments: environments.map((env) => env.slug), - projectSlug - }); - } - createNotification({ - type: "success", - text: "Successfully created policy" + if (data.policyType === PolicyType.ChangePolicy) { + await createSecretApprovalPolicy({ + ...data, + approvers: [...userApprovers, ...groupApprovers], + bypassers: bypassers.length > 0 ? bypassers : undefined, + environments: environments.map((env) => env.slug), + projectId: currentProject?.id || "" }); - onToggle(false); - } catch (err) { - console.log(err); - createNotification({ - type: "error", - text: "Failed to create policy" + } else { + await createAccessApprovalPolicy({ + ...data, + approvers: sequenceApprovers?.flatMap((approvers, index) => + approvers.user + .map( + (el) => ({ ...el, sequence: index + 1 }) as Omit + ) + .concat(approvers.group.map((el) => ({ ...el, sequence: index + 1 }))) + ), + approvalsRequired: sequenceApprovers?.map((el, index) => ({ + stepNumber: index + 1, + numberOfApprovals: el.approvals + })), + bypassers: bypassers.length > 0 ? bypassers : undefined, + environments: environments.map((env) => env.slug), + projectSlug }); } + createNotification({ + type: "success", + text: "Successfully created policy" + }); + onToggle(false); }; const handleUpdatePolicy = async ({ @@ -293,50 +285,42 @@ const Form = ({ if (!projectId || !projectSlug) return; if (!editValues?.id) return; - try { - const bypassers = [...userBypassers, ...groupBypassers]; + const bypassers = [...userBypassers, ...groupBypassers]; - if (data.policyType === PolicyType.ChangePolicy) { - await updateSecretApprovalPolicy({ - id: editValues?.id, - ...data, - approvers: [...userApprovers, ...groupApprovers], - bypassers: bypassers.length > 0 ? bypassers : undefined, - projectId: currentProject?.id || "", - environments: environments.map((env) => env.slug) - }); - } else { - await updateAccessApprovalPolicy({ - id: editValues?.id, - ...data, - approvers: sequenceApprovers?.flatMap((approvers, index) => - approvers.user - .map( - (el) => ({ ...el, sequence: index + 1 }) as Omit - ) - .concat(approvers.group.map((el) => ({ ...el, sequence: index + 1 }))) - ), - approvalsRequired: sequenceApprovers?.map((el, index) => ({ - stepNumber: index + 1, - numberOfApprovals: el.approvals - })), - bypassers: bypassers.length > 0 ? bypassers : undefined, - environments: environments.map((env) => env.slug), - projectSlug - }); - } - createNotification({ - type: "success", - text: "Successfully updated policy" + if (data.policyType === PolicyType.ChangePolicy) { + await updateSecretApprovalPolicy({ + id: editValues?.id, + ...data, + approvers: [...userApprovers, ...groupApprovers], + bypassers: bypassers.length > 0 ? bypassers : undefined, + projectId: currentProject?.id || "", + environments: environments.map((env) => env.slug) }); - onToggle(false); - } catch (err) { - console.log(err); - createNotification({ - type: "error", - text: "failed to update policy" + } else { + await updateAccessApprovalPolicy({ + id: editValues?.id, + ...data, + approvers: sequenceApprovers?.flatMap((approvers, index) => + approvers.user + .map( + (el) => ({ ...el, sequence: index + 1 }) as Omit + ) + .concat(approvers.group.map((el) => ({ ...el, sequence: index + 1 }))) + ), + approvalsRequired: sequenceApprovers?.map((el, index) => ({ + stepNumber: index + 1, + numberOfApprovals: el.approvals + })), + bypassers: bypassers.length > 0 ? bypassers : undefined, + environments: environments.map((env) => env.slug), + projectSlug }); } + createNotification({ + type: "success", + text: "Successfully updated policy" + }); + onToggle(false); }; const handleFormSubmit = async (data: TFormSchema) => { diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/RemoveApprovalPolicyModal.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/RemoveApprovalPolicyModal.tsx index db2406335..96866ad31 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/RemoveApprovalPolicyModal.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/RemoveApprovalPolicyModal.tsx @@ -32,29 +32,22 @@ export const RemoveApprovalPolicyModal = ({ const { currentProject } = useProject(); const handleDeletePolicy = async () => { - try { - if (policyType === PolicyType.ChangePolicy) { - await deleteSecretApprovalPolicy({ - projectId: currentProject.id, - id: policyId - }); - } else { - await deleteAccessApprovalPolicy({ - projectSlug: currentProject.slug, - id: policyId - }); - } - createNotification({ - type: "success", - text: "Successfully deleted policy" + if (policyType === PolicyType.ChangePolicy) { + await deleteSecretApprovalPolicy({ + projectId: currentProject.id, + id: policyId }); - onOpenChange(false); - } catch { - createNotification({ - type: "error", - text: "Failed to delete policy" + } else { + await deleteAccessApprovalPolicy({ + projectSlug: currentProject.slug, + id: policyId }); } + createNotification({ + type: "success", + text: "Successfully deleted policy" + }); + onOpenChange(false); }; const deleteSecretApprovalData = useGetSecretApprovalRequestCount({ 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 c2eb4c61f..709e312bb 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 @@ -59,43 +59,27 @@ export const SecretApprovalRequestAction = ({ }; const handleSecretApprovalRequestMerge = async () => { - try { - await performSecretApprovalMerge({ - id: approvalRequestId, - projectId, - bypassReason: byPassApproval ? bypassReason : undefined - }); - createNotification({ - type: "success", - text: "Successfully merged the request" - }); - } catch (err) { - console.log(err); - createNotification({ - type: "error", - text: "Failed to update the request status" - }); - } + await performSecretApprovalMerge({ + id: approvalRequestId, + projectId, + bypassReason: byPassApproval ? bypassReason : undefined + }); + createNotification({ + type: "success", + text: "Successfully merged the request" + }); }; const handleSecretApprovalStatusChange = async (reqState: "open" | "close") => { - try { - await updateSecretStatusChange({ - id: approvalRequestId, - status: reqState, - projectId - }); - createNotification({ - type: "success", - text: "Successfully updated the request" - }); - } catch (err) { - console.log(err); - createNotification({ - type: "error", - text: "Failed to update the request status" - }); - } + await updateSecretStatusChange({ + id: approvalRequestId, + status: reqState, + projectId + }); + createNotification({ + type: "success", + text: "Successfully updated the request" + }); }; const isSoftEnforcement = enforcementLevel === EnforcementLevel.Soft; 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 24d306314..bd4a04fa3 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 @@ -174,23 +174,15 @@ export const SecretApprovalRequestChanges = ({ approvalRequestId, onGoBack }: Pr ); const handleSecretApprovalStatusUpdate = async (status: ApprovalStatus, comment: string) => { - try { - await updateSecretApprovalRequestStatus({ - id: approvalRequestId, - status, - comment - }); - createNotification({ - type: "success", - text: `Successfully ${status} the request` - }); - } catch (err) { - console.log(err); - createNotification({ - type: "error", - text: "Failed to update the request status" - }); - } + await updateSecretApprovalRequestStatus({ + id: approvalRequestId, + status, + comment + }); + createNotification({ + type: "success", + text: `Successfully ${status} the request` + }); handlePopUpToggle("reviewChanges", false); reset({ diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx index f1a7ffba8..a2ee814f8 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -338,45 +338,37 @@ const Page = () => { const isProtectedBranch = Boolean(boardPolicy); const handleCreateCommit = async (changes: PendingChanges, message: string) => { - try { - await createCommit({ - projectId, - environment, - secretPath, - pendingChanges: changes, - message - }); + await createCommit({ + projectId, + environment, + secretPath, + pendingChanges: changes, + message + }); - if (!isProtectedBranch) { - pendingChanges.secrets.forEach((secret) => { - if (secret.type === "update" && secret.secretValue !== undefined) { - queryClient.setQueryData( - dashboardKeys.getSecretValue({ - projectId, - environment, - secretPath, - secretKey: secret.newSecretName ?? secret.secretKey, - isOverride: false - }), - { value: secret.secretValue } - ); - } - }); - } - - createNotification({ - text: isProtectedBranch - ? "Requested changes have been sent for review" - : "Changes saved successfully", - type: "success" + if (!isProtectedBranch) { + pendingChanges.secrets.forEach((secret) => { + if (secret.type === "update" && secret.secretValue !== undefined) { + queryClient.setQueryData( + dashboardKeys.getSecretValue({ + projectId, + environment, + secretPath, + secretKey: secret.newSecretName ?? secret.secretKey, + isOverride: false + }), + { value: secret.secretValue } + ); + } }); - } catch (error) { - createNotification({ - text: "Failed to save changes", - type: "error" - }); - console.error(error); } + + createNotification({ + text: isProtectedBranch + ? "Requested changes have been sent for review" + : "Changes saved successfully", + type: "success" + }); }; const { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx index 0b8210f0d..a3f6f3e43 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx @@ -209,48 +209,40 @@ export const ActionBar = ({ const isOrgAdmin = hasOrgRole(OrgMembershipRole.Admin); const handleFolderCreate = async (folderName: string, description: string | null) => { - try { - if (isBatchMode) { - const folderId = `${folderName}`; - const pendingFolderCreate: PendingFolderCreate = { - id: folderId, - resourceType: "folder", - type: PendingAction.Create, - folderName, - description: description || undefined, - parentPath: secretPath, - timestamp: Date.now() - }; + if (isBatchMode) { + const folderId = `${folderName}`; + const pendingFolderCreate: PendingFolderCreate = { + id: folderId, + resourceType: "folder", + type: PendingAction.Create, + folderName, + description: description || undefined, + parentPath: secretPath, + timestamp: Date.now() + }; - addPendingChange(pendingFolderCreate, { - projectId, - environment, - secretPath - }); - - handlePopUpClose("addFolder"); - return; - } - - await createFolder({ - name: folderName, - path: secretPath, - environment, + addPendingChange(pendingFolderCreate, { projectId, - description + environment, + secretPath }); + handlePopUpClose("addFolder"); - createNotification({ - type: "success", - text: "Successfully created folder" - }); - } catch (error) { - console.log(error); - createNotification({ - type: "error", - text: "Failed to create folder" - }); + return; } + + await createFolder({ + name: folderName, + path: secretPath, + environment, + projectId, + description + }); + handlePopUpClose("addFolder"); + createNotification({ + type: "success", + text: "Successfully created folder" + }); }; const handleSecretDownload = async () => { @@ -323,26 +315,18 @@ export const ActionBar = ({ const handleSecretBulkDelete = async () => { const bulkDeletedSecrets = Object.values(selectedSecrets); - try { - await deleteBatchSecretV3({ - secretPath, - projectId, - environment, - secrets: bulkDeletedSecrets.map(({ key }) => ({ secretKey: key, type: SecretType.Shared })) - }); - resetSelectedSecret(); - handlePopUpClose("bulkDeleteSecrets"); - createNotification({ - type: "success", - text: "Successfully deleted secrets" - }); - } catch (error) { - console.log(error); - createNotification({ - type: "error", - text: "Failed to delete secrets" - }); - } + await deleteBatchSecretV3({ + secretPath, + projectId, + environment, + secrets: bulkDeletedSecrets.map(({ key }) => ({ secretKey: key, type: SecretType.Shared })) + }); + resetSelectedSecret(); + handlePopUpClose("bulkDeleteSecrets"); + createNotification({ + type: "success", + text: "Successfully deleted secrets" + }); }; const handleSecretsMove = async ({ @@ -678,35 +662,23 @@ export const ActionBar = ({ }; const handleVaultImport = async (vaultPath: string, namespace: string) => { - try { - const result = await importVaultSecrets({ - projectId, - environment, - secretPath, - vaultNamespace: namespace, - vaultSecretPath: vaultPath - }); - - if (result.status === VaultImportStatus.ApprovalRequired) { - createNotification({ - type: "info", - text: "Secret change request created successfully. Awaiting approval." - }); - } else { - createNotification({ - type: "success", - text: "Successfully imported secrets from HashiCorp Vault" - }); - } - } catch (err) { - console.error("Vault import error:", err); - const error = err as AxiosError<{ message?: string }>; - const errorMessage = - error.response?.data?.message || "Failed to import secrets from Vault. Please try again."; + const result = await importVaultSecrets({ + projectId, + environment, + secretPath, + vaultNamespace: namespace, + vaultSecretPath: vaultPath + }); + if (result.status === VaultImportStatus.ApprovalRequired) { createNotification({ - type: "error", - text: errorMessage + type: "info", + text: "Secret change request created successfully. Awaiting approval." + }); + } else { + createNotification({ + type: "success", + text: "Successfully imported secrets from HashiCorp Vault" }); } }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsElastiCacheInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsElastiCacheInputForm.tsx index c58f2a754..48bd2012b 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsElastiCacheInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsElastiCacheInputForm.tsx @@ -4,7 +4,6 @@ import ms from "ms"; import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; -import { createNotification } from "@app/components/notifications"; import { Accordion, AccordionContent, @@ -111,25 +110,18 @@ export const AwsElastiCacheInputForm = ({ if (createDynamicSecret.isPending) return; const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.AwsElastiCache, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment.slug, - usernameTemplate: - !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.AwsElastiCache, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug, + usernameTemplate: + !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate + }); + onCompleted(); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx index 35a416224..e4438d117 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx @@ -4,7 +4,6 @@ import ms from "ms"; import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; -import { createNotification } from "@app/components/notifications"; import { Button, FilterableSelect, @@ -166,26 +165,19 @@ export const AwsIamInputForm = ({ // wait till previous request is finished if (createDynamicSecret.isPending) return; - try { - const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.AwsIam, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment.slug, - usernameTemplate: - !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.AwsIam, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug, + usernameTemplate: + !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate + }); + onCompleted(); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx index 581c473b9..b4506c51a 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx @@ -6,7 +6,6 @@ import ms from "ms"; import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; -import { createNotification } from "@app/components/notifications"; import { Button, FilterableSelect, FormControl, Input } from "@app/components/v2"; import { DropdownMenu, @@ -112,34 +111,27 @@ export const AzureEntraIdInputForm = ({ }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - try { - selectedUsers.map(async (user: { id: string; name: string; email: string }) => { - await createDynamicSecret.mutateAsync({ - provider: { - type: DynamicSecretProviders.AzureEntraId, - inputs: { - userId: user.id, - tenantId: provider.tenantId, - email: user.email, - applicationId: provider.applicationId, - clientSecret: provider.clientSecret - } - }, - maxTTL, - name: `${name}-${user.name}`, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment.slug - }); + selectedUsers.map(async (user: { id: string; name: string; email: string }) => { + await createDynamicSecret.mutateAsync({ + provider: { + type: DynamicSecretProviders.AzureEntraId, + inputs: { + userId: user.id, + tenantId: provider.tenantId, + email: user.email, + applicationId: provider.applicationId, + clientSecret: provider.clientSecret + } + }, + maxTTL, + name: `${name}-${user.name}`, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + }); + onCompleted(); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureSqlDatabaseInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureSqlDatabaseInputForm.tsx index aece11f38..954aed3ba 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureSqlDatabaseInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureSqlDatabaseInputForm.tsx @@ -5,7 +5,6 @@ import ms from "ms"; import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; -import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; import { Accordion, @@ -171,29 +170,22 @@ export const AzureSqlDatabaseInputForm = ({ if (createDynamicSecret.isPending) return; const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - try { - await createDynamicSecret.mutateAsync({ - provider: { - type: DynamicSecretProviders.AzureSqlDatabase, - inputs: { ...provider, masterDatabase: "master" } - }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment.slug, - metadata, - usernameTemplate: - !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + await createDynamicSecret.mutateAsync({ + provider: { + type: DynamicSecretProviders.AzureSqlDatabase, + inputs: { ...provider, masterDatabase: "master" } + }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug, + metadata, + usernameTemplate: + !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate + }); + onCompleted(); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CassandraInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CassandraInputForm.tsx index 15f9387df..dd0c3e879 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CassandraInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CassandraInputForm.tsx @@ -4,7 +4,6 @@ import ms from "ms"; import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; -import { createNotification } from "@app/components/notifications"; import { Accordion, AccordionContent, @@ -112,25 +111,18 @@ export const CassandraInputForm = ({ if (createDynamicSecret.isPending) return; const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.Cassandra, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment.slug, - usernameTemplate: - !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.Cassandra, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug, + usernameTemplate: + !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate + }); + onCompleted(); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CouchbaseInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CouchbaseInputForm.tsx index 17bfeddca..894b31c1c 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CouchbaseInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CouchbaseInputForm.tsx @@ -7,7 +7,6 @@ import ms from "ms"; import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; -import { createNotification } from "@app/components/notifications"; import { Accordion, AccordionContent, @@ -386,25 +385,18 @@ export const CouchbaseInputForm = ({ const { useAdvancedBuckets, ...finalProvider } = transformedProvider; - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.Couchbase, inputs: finalProvider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment.slug, - usernameTemplate: - !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.Couchbase, inputs: finalProvider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug, + usernameTemplate: + !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate + }); + onCompleted(); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx index 1d0b6c268..d6477862b 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx @@ -6,7 +6,6 @@ import ms from "ms"; import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; -import { createNotification } from "@app/components/notifications"; import { Button, FilterableSelect, @@ -131,25 +130,18 @@ export const ElasticSearchInputForm = ({ // wait till previous request is finished if (createDynamicSecret.isPending) return; const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.ElasticSearch, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment.slug, - usernameTemplate: - !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.ElasticSearch, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug, + usernameTemplate: + !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate + }); + onCompleted(); }; const selectedAuthType = watch("provider.auth.type"); diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/GcpIamInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/GcpIamInputForm.tsx index a208b1973..a0134bfde 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/GcpIamInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/GcpIamInputForm.tsx @@ -4,7 +4,6 @@ import ms from "ms"; import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; -import { createNotification } from "@app/components/notifications"; import { Button, FilterableSelect, FormControl, Input } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; @@ -83,28 +82,21 @@ export const GcpIamInputForm = ({ }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - try { - await createDynamicSecret.mutateAsync({ - provider: { - type: DynamicSecretProviders.GcpIam, - inputs: { - ...provider - } - }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment.slug - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + await createDynamicSecret.mutateAsync({ + provider: { + type: DynamicSecretProviders.GcpIam, + inputs: { + ...provider + } + }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); + onCompleted(); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/GithubInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/GithubInputForm.tsx index 776511ac4..31256f5a1 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/GithubInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/GithubInputForm.tsx @@ -4,7 +4,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; -import { createNotification } from "@app/components/notifications"; import { Button, FilterableSelect, @@ -71,27 +70,20 @@ export const GithubInputForm = ({ const handleCreateDynamicSecret = async ({ name, provider, environment }: TForm) => { if (createDynamicSecret.isPending) return; - try { - await createDynamicSecret.mutateAsync({ - provider: { - type: DynamicSecretProviders.Github, - inputs: { - ...provider - } - }, - defaultTTL: "1h", // Github is limited to 1 hour tokens - name, - path: secretPath, - projectSlug, - environmentSlug: environment.slug - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + await createDynamicSecret.mutateAsync({ + provider: { + type: DynamicSecretProviders.Github, + inputs: { + ...provider + } + }, + defaultTTL: "1h", // Github is limited to 1 hour tokens + name, + path: secretPath, + projectSlug, + environmentSlug: environment.slug + }); + onCompleted(); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/KubernetesInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/KubernetesInputForm.tsx index d15bcc9b8..a439f1af2 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/KubernetesInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/KubernetesInputForm.tsx @@ -283,33 +283,26 @@ export const KubernetesInputForm = ({ // wait till previous request is finished if (createDynamicSecret.isPending) return; - try { - const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - await createDynamicSecret.mutateAsync({ - provider: { - type: DynamicSecretProviders.Kubernetes, - inputs: { - ...provider, - url: provider.url || undefined - } - }, - maxTTL: rest.maxTTL, - name: rest.name, - path: secretPath, - defaultTTL: rest.defaultTTL, - projectSlug, - environmentSlug: rest.environment.slug, - usernameTemplate: - !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate - }); + const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; + await createDynamicSecret.mutateAsync({ + provider: { + type: DynamicSecretProviders.Kubernetes, + inputs: { + ...provider, + url: provider.url || undefined + } + }, + maxTTL: rest.maxTTL, + name: rest.name, + path: secretPath, + defaultTTL: rest.defaultTTL, + projectSlug, + environmentSlug: rest.environment.slug, + usernameTemplate: + !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate + }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + onCompleted(); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx index 7b7397625..61273fc41 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx @@ -4,7 +4,6 @@ import ms from "ms"; import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; -import { createNotification } from "@app/components/notifications"; import { Button, FilterableSelect, @@ -139,25 +138,18 @@ export const LdapInputForm = ({ if (createDynamicSecret.isPending) return; const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.Ldap, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - usernameTemplate: - !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate, - environmentSlug: environment.slug - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.Ldap, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + usernameTemplate: + !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate, + environmentSlug: environment.slug + }); + onCompleted(); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx index 3b0c7bb3b..317febae8 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx @@ -6,7 +6,6 @@ import ms from "ms"; import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; -import { createNotification } from "@app/components/notifications"; import { Accordion, AccordionContent, @@ -143,25 +142,18 @@ export const MongoAtlasInputForm = ({ if (createDynamicSecret.isPending) return; const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.MongoAtlas, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment.slug, - usernameTemplate: - !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.MongoAtlas, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug, + usernameTemplate: + !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate + }); + onCompleted(); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx index ac60a77b0..8099b123c 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx @@ -6,7 +6,6 @@ import ms from "ms"; import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; -import { createNotification } from "@app/components/notifications"; import { Button, FilterableSelect, @@ -113,32 +112,25 @@ export const MongoDBDatabaseInputForm = ({ if (createDynamicSecret.isPending) return; const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - try { - await createDynamicSecret.mutateAsync({ - provider: { - type: DynamicSecretProviders.MongoDB, - inputs: { - ...provider, - port: provider?.port ? provider.port : undefined, - roles: provider.roles.map((el) => el.roleName) - } - }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment.slug, - usernameTemplate: - !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + await createDynamicSecret.mutateAsync({ + provider: { + type: DynamicSecretProviders.MongoDB, + inputs: { + ...provider, + port: provider?.port ? provider.port : undefined, + roles: provider.roles.map((el) => el.roleName) + } + }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug, + usernameTemplate: + !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate + }); + onCompleted(); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx index 7b2e56464..b142682dd 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx @@ -6,7 +6,6 @@ import ms from "ms"; import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; -import { createNotification } from "@app/components/notifications"; import { Button, FilterableSelect, @@ -121,25 +120,18 @@ export const RabbitMqInputForm = ({ if (createDynamicSecret.isPending) return; const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.RabbitMq, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment.slug, - usernameTemplate: - !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.RabbitMq, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug, + usernameTemplate: + !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate + }); + onCompleted(); }; const selectedTags = watch("provider.tags"); diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx index 472d30783..31745ae79 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx @@ -4,7 +4,6 @@ import ms from "ms"; import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; -import { createNotification } from "@app/components/notifications"; import { Accordion, AccordionContent, @@ -104,26 +103,19 @@ export const RedisInputForm = ({ }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - try { - const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.Redis, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment.slug, - usernameTemplate: - !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.Redis, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug, + usernameTemplate: + !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate + }); + onCompleted(); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx index 621451fda..42c06ebea 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx @@ -4,7 +4,6 @@ import ms from "ms"; import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; -import { createNotification } from "@app/components/notifications"; import { Accordion, AccordionContent, @@ -105,26 +104,19 @@ sp_droplogin '{{username}}';` }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - try { - const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.SapAse, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment.slug, - usernameTemplate: - !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.SapAse, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug, + usernameTemplate: + !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate + }); + onCompleted(); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapHanaInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapHanaInputForm.tsx index 034afee2c..8984d1f49 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapHanaInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapHanaInputForm.tsx @@ -4,7 +4,6 @@ import ms from "ms"; import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; -import { createNotification } from "@app/components/notifications"; import { Accordion, AccordionContent, @@ -105,26 +104,19 @@ DROP USER {{username}};`, }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - try { - const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.SapHana, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - usernameTemplate: - !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate, - environmentSlug: environment.slug - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.SapHana, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + usernameTemplate: + !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate, + environmentSlug: environment.slug + }); + onCompleted(); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SnowflakeInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SnowflakeInputForm.tsx index 11357e26a..91f638f39 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SnowflakeInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SnowflakeInputForm.tsx @@ -4,7 +4,6 @@ import ms from "ms"; import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; -import { createNotification } from "@app/components/notifications"; import { Accordion, AccordionContent, @@ -102,26 +101,19 @@ export const SnowflakeInputForm = ({ }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - try { - const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.Snowflake, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment.slug, - usernameTemplate: - !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate - }); - onCompleted(); - } catch (err) { - createNotification({ - type: "error", - text: err instanceof Error ? err.message : "Failed to create dynamic secret" - }); - } + const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.Snowflake, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug, + usernameTemplate: + !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate + }); + onCompleted(); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx index 0a836fec2..aaf2e43d1 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx @@ -5,7 +5,6 @@ import ms from "ms"; import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; -import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; import { Accordion, @@ -215,26 +214,19 @@ export const SqlDatabaseInputForm = ({ if (createDynamicSecret.isPending) return; const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.SqlDatabase, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment.slug, - metadata, - usernameTemplate: - !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.SqlDatabase, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug, + metadata, + usernameTemplate: + !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate + }); + onCompleted(); }; const handleDatabaseChange = (type: SqlProviders) => { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx index 7f5693e33..f86ac4017 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx @@ -2,7 +2,6 @@ import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; -import { createNotification } from "@app/components/notifications"; import { Button, FilterableSelect, @@ -100,23 +99,16 @@ export const TotpInputForm = ({ const handleCreateDynamicSecret = async ({ name, provider, environment }: TForm) => { // wait till previous request is finished if (createDynamicSecret.isPending) return; - try { - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.Totp, inputs: provider }, - maxTTL: "24h", - name, - path: secretPath, - defaultTTL: "1m", - projectSlug, - environmentSlug: environment.slug - }); - onCompleted(); - } catch (err) { - createNotification({ - type: "error", - text: err instanceof Error ? err.message : "Failed to create dynamic secret" - }); - } + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.Totp, inputs: provider }, + maxTTL: "24h", + name, + path: secretPath, + defaultTTL: "1m", + projectSlug, + environmentSlug: environment.slug + }); + onCompleted(); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/VerticaInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/VerticaInputForm.tsx index b48eab850..7bd66a466 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/VerticaInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/VerticaInputForm.tsx @@ -5,7 +5,6 @@ import ms from "ms"; import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; -import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; import { Accordion, @@ -145,26 +144,19 @@ GRANT CREATE ON SCHEMA public TO {{username}};`, usernameTemplate }: TForm) => { if (createDynamicSecret.isPending) return; - try { - const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - await createDynamicSecret.mutateAsync({ - provider: { type: DynamicSecretProviders.Vertica, inputs: provider }, - maxTTL, - name, - path: secretPath, - defaultTTL, - projectSlug, - environmentSlug: environment.slug, - usernameTemplate: - !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate - }); - onCompleted(); - } catch { - createNotification({ - type: "error", - text: "Failed to create dynamic secret" - }); - } + const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.Vertica, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug, + usernameTemplate: + !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate + }); + onCompleted(); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateSecretImportForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateSecretImportForm.tsx index 68d2d8cd0..73a9d938e 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateSecretImportForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateSecretImportForm.tsx @@ -101,11 +101,6 @@ export const CreateSecretImportForm = ({ text: "You do not have access to the selected environment/path", type: "error" }); - } else { - createNotification({ - type: "error", - text: "Failed to link secrets" - }); } } }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx index 1a16e6e4e..99e66e8b7 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx @@ -441,30 +441,22 @@ export const CreateKubernetesDynamicSecretLease = ({ const handleDynamicSecretLeaseCreate = async ({ ttl, namespace }: TKubernetesForm) => { if (createDynamicSecretLease.isPending) return; - try { - await createDynamicSecretLease.mutateAsync({ - environmentSlug: environment, - projectSlug, - path: secretPath, - ttl, - dynamicSecretName, - config: { - namespace: namespace || undefined - }, - provider - }); + await createDynamicSecretLease.mutateAsync({ + environmentSlug: environment, + projectSlug, + path: secretPath, + ttl, + dynamicSecretName, + config: { + namespace: namespace || undefined + }, + provider + }); - createNotification({ - type: "success", - text: "Successfully leased dynamic secret" - }); - } catch (error) { - console.log(error); - createNotification({ - type: "error", - text: "Failed to lease dynamic secret" - }); - } + createNotification({ + type: "success", + text: "Successfully leased dynamic secret" + }); }; const handleLeaseRegeneration = async (data: { ttl?: string }) => { @@ -590,29 +582,21 @@ export const CreateDynamicSecretLease = ({ const handleDynamicSecretLeaseCreate = async ({ ttl }: TForm) => { if (createDynamicSecretLease.isPending) return; - try { - await createDynamicSecretLease.mutateAsync({ - environmentSlug: environment, - projectSlug, - path: secretPath, - ttl, - dynamicSecretName, - provider - }); + await createDynamicSecretLease.mutateAsync({ + environmentSlug: environment, + projectSlug, + path: secretPath, + ttl, + dynamicSecretName, + provider + }); - createNotification({ - type: "success", - text: "Successfully leased dynamic secret" - }); + createNotification({ + type: "success", + text: "Successfully leased dynamic secret" + }); - setIsPreloading.off(); - } catch (error) { - console.log(error); - createNotification({ - type: "error", - text: "Failed to lease dynamic secret" - }); - } + setIsPreloading.off(); }; const handleLeaseRegeneration = async (data: { ttl?: string }) => { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/DynamicSecretLease.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/DynamicSecretLease.tsx index 085a06169..1f734de32 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/DynamicSecretLease.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/DynamicSecretLease.tsx @@ -69,31 +69,23 @@ export const DynamicSecretLease = ({ const deleteDynamicSecretLease = useRevokeDynamicSecretLease(); const handleDynamicSecretDeleteLease = async () => { - try { - const { leaseId, isForced } = popUp.deleteSecret.data as { - leaseId: string; - isForced?: boolean; - }; - await deleteDynamicSecretLease.mutateAsync({ - environmentSlug: environment, - projectSlug, - path: secretPath, - dynamicSecretName, - leaseId, - isForced - }); - handlePopUpClose("deleteSecret"); - createNotification({ - type: "success", - text: "Successfully deleted lease" - }); - } catch (error) { - console.log(error); - createNotification({ - type: "error", - text: "Failed to delete lease" - }); - } + const { leaseId, isForced } = popUp.deleteSecret.data as { + leaseId: string; + isForced?: boolean; + }; + await deleteDynamicSecretLease.mutateAsync({ + environmentSlug: environment, + projectSlug, + path: secretPath, + dynamicSecretName, + leaseId, + isForced + }); + handlePopUpClose("deleteSecret"); + createNotification({ + type: "success", + text: "Successfully deleted lease" + }); }; const canRenew = !DYNAMIC_SECRETS_WITHOUT_RENEWAL.includes(dynamicSecret.type); diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/DynamicSecretListView.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/DynamicSecretListView.tsx index 1ef140193..d444f8390 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/DynamicSecretListView.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/DynamicSecretListView.tsx @@ -54,29 +54,21 @@ export const DynamicSecretListView = ({ const deleteDynamicSecret = useDeleteDynamicSecret(); const handleDynamicSecretDelete = async () => { - try { - const { name, isForced } = popUp.deleteDynamicSecret.data as TDynamicSecret & { - isForced?: boolean; - }; - await deleteDynamicSecret.mutateAsync({ - environmentSlug: environment, - projectSlug, - path: secretPath, - name, - isForced - }); - handlePopUpClose("deleteDynamicSecret"); - createNotification({ - type: "success", - text: "Successfully deleted dynamic secret" - }); - } catch (error) { - console.log(error); - createNotification({ - type: "error", - text: "Failed to delete dynamic secret" - }); - } + const { name, isForced } = popUp.deleteDynamicSecret.data as TDynamicSecret & { + isForced?: boolean; + }; + await deleteDynamicSecret.mutateAsync({ + environmentSlug: environment, + projectSlug, + path: secretPath, + name, + isForced + }); + handlePopUpClose("deleteDynamicSecret"); + createNotification({ + type: "success", + text: "Successfully deleted dynamic secret" + }); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretAwsElastiCacheProviderForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretAwsElastiCacheProviderForm.tsx index 5740e2061..0015c0c0c 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretAwsElastiCacheProviderForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretAwsElastiCacheProviderForm.tsx @@ -101,31 +101,24 @@ export const EditDynamicSecretAwsElastiCacheProviderForm = ({ // wait till previous request is finished if (updateDynamicSecret.isPending) return; const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - try { - await updateDynamicSecret.mutateAsync({ - name: dynamicSecret.name, - path: secretPath, - projectSlug, - environmentSlug: environment, - data: { - maxTTL: maxTTL || undefined, - defaultTTL, - inputs, - newName: newName === dynamicSecret.name ? undefined : newName, - usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate - } - }); - onClose(); - createNotification({ - type: "success", - text: "Successfully updated dynamic secret" - }); - } catch { - createNotification({ - type: "error", - text: "Failed to update dynamic secret" - }); - } + await updateDynamicSecret.mutateAsync({ + name: dynamicSecret.name, + path: secretPath, + projectSlug, + environmentSlug: environment, + data: { + maxTTL: maxTTL || undefined, + defaultTTL, + inputs, + newName: newName === dynamicSecret.name ? undefined : newName, + usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate + } + }); + onClose(); + createNotification({ + type: "success", + text: "Successfully updated dynamic secret" + }); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretAwsIamForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretAwsIamForm.tsx index 555ed9eae..c9c9afb71 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretAwsIamForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretAwsIamForm.tsx @@ -141,31 +141,24 @@ export const EditDynamicSecretAwsIamForm = ({ // wait till previous request is finished if (updateDynamicSecret.isPending) return; const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - try { - await updateDynamicSecret.mutateAsync({ - name: dynamicSecret.name, - path: secretPath, - projectSlug, - environmentSlug: environment, - data: { - maxTTL: maxTTL || undefined, - defaultTTL, - inputs, - newName: newName === dynamicSecret.name ? undefined : newName, - usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate - } - }); - onClose(); - createNotification({ - type: "success", - text: "Successfully updated dynamic secret" - }); - } catch { - createNotification({ - type: "error", - text: "Failed to update dynamic secret" - }); - } + await updateDynamicSecret.mutateAsync({ + name: dynamicSecret.name, + path: secretPath, + projectSlug, + environmentSlug: environment, + data: { + maxTTL: maxTTL || undefined, + defaultTTL, + inputs, + newName: newName === dynamicSecret.name ? undefined : newName, + usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate + } + }); + onClose(); + createNotification({ + type: "success", + text: "Successfully updated dynamic secret" + }); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretAzureEntraIdForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretAzureEntraIdForm.tsx index c9bfc92cc..2799a95ed 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretAzureEntraIdForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretAzureEntraIdForm.tsx @@ -76,30 +76,23 @@ export const EditDynamicSecretAzureEntraIdForm = ({ const handleUpdateDynamicSecret = async ({ maxTTL, defaultTTL, newName, inputs }: TForm) => { // wait till previous request is finished if (updateDynamicSecret.isPending) return; - try { - await updateDynamicSecret.mutateAsync({ - name: dynamicSecret.name, - path: secretPath, - projectSlug, - environmentSlug: environment, - data: { - maxTTL: maxTTL || undefined, - defaultTTL, - newName: newName === dynamicSecret.name ? undefined : newName, - inputs - } - }); - onClose(); - createNotification({ - type: "success", - text: "Successfully updated dynamic secret" - }); - } catch { - createNotification({ - type: "error", - text: "Failed to update dynamic secret" - }); - } + await updateDynamicSecret.mutateAsync({ + name: dynamicSecret.name, + path: secretPath, + projectSlug, + environmentSlug: environment, + data: { + maxTTL: maxTTL || undefined, + defaultTTL, + newName: newName === dynamicSecret.name ? undefined : newName, + inputs + } + }); + onClose(); + createNotification({ + type: "success", + text: "Successfully updated dynamic secret" + }); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretAzureSqlDatabaseForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretAzureSqlDatabaseForm.tsx index 77bb7aa1f..fd370d755 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretAzureSqlDatabaseForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretAzureSqlDatabaseForm.tsx @@ -162,33 +162,26 @@ export const EditDynamicSecretAzureSqlDatabaseForm = ({ }: TForm) => { if (updateDynamicSecret.isPending) return; - try { - const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - await updateDynamicSecret.mutateAsync({ - projectSlug, - environmentSlug: environment, - path: secretPath, - name: dynamicSecret.name, - data: { - maxTTL: maxTTL || undefined, - defaultTTL, - inputs: inputs ? { ...inputs, masterDatabase: "master" } : undefined, - newName: newName === dynamicSecret.name ? undefined : newName, - metadata, - usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate - } - }); - onClose(); - createNotification({ - type: "success", - text: "Successfully updated dynamic secret" - }); - } catch { - createNotification({ - type: "error", - text: "Failed to update dynamic secret" - }); - } + const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; + await updateDynamicSecret.mutateAsync({ + projectSlug, + environmentSlug: environment, + path: secretPath, + name: dynamicSecret.name, + data: { + maxTTL: maxTTL || undefined, + defaultTTL, + inputs: inputs ? { ...inputs, masterDatabase: "master" } : undefined, + newName: newName === dynamicSecret.name ? undefined : newName, + metadata, + usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate + } + }); + onClose(); + createNotification({ + type: "success", + text: "Successfully updated dynamic secret" + }); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretCassandraForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretCassandraForm.tsx index 515ee19e3..fe1212330 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretCassandraForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretCassandraForm.tsx @@ -103,31 +103,24 @@ export const EditDynamicSecretCassandraForm = ({ // wait till previous request is finished if (updateDynamicSecret.isPending) return; const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - try { - await updateDynamicSecret.mutateAsync({ - name: dynamicSecret.name, - path: secretPath, - projectSlug, - environmentSlug: environment, - data: { - maxTTL: maxTTL || undefined, - defaultTTL, - inputs, - newName: newName === dynamicSecret.name ? undefined : newName, - usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate - } - }); - onClose(); - createNotification({ - type: "success", - text: "Successfully updated dynamic secret" - }); - } catch { - createNotification({ - type: "error", - text: "Failed to update dynamic secret" - }); - } + await updateDynamicSecret.mutateAsync({ + name: dynamicSecret.name, + path: secretPath, + projectSlug, + environmentSlug: environment, + data: { + maxTTL: maxTTL || undefined, + defaultTTL, + inputs, + newName: newName === dynamicSecret.name ? undefined : newName, + usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate + } + }); + onClose(); + createNotification({ + type: "success", + text: "Successfully updated dynamic secret" + }); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretCouchbaseForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretCouchbaseForm.tsx index e69b3460f..de38358f3 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretCouchbaseForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretCouchbaseForm.tsx @@ -386,37 +386,30 @@ export const EditDynamicSecretCouchbaseForm = ({ })() : transformedInputs; - try { - await updateDynamicSecret.mutateAsync({ - name: dynamicSecret.name, - path: secretPath, - projectSlug, - environmentSlug: environment, - data: { - defaultTTL, - maxTTL: maxTTL || undefined, - newName: newName === dynamicSecret.name ? undefined : newName, - metadata, - usernameTemplate: - !usernameTemplate || usernameTemplate === "{{randomUsername}}" - ? undefined - : usernameTemplate, - inputs: finalInputs - } - }); + await updateDynamicSecret.mutateAsync({ + name: dynamicSecret.name, + path: secretPath, + projectSlug, + environmentSlug: environment, + data: { + defaultTTL, + maxTTL: maxTTL || undefined, + newName: newName === dynamicSecret.name ? undefined : newName, + metadata, + usernameTemplate: + !usernameTemplate || usernameTemplate === "{{randomUsername}}" + ? undefined + : usernameTemplate, + inputs: finalInputs + } + }); - createNotification({ - type: "success", - text: "Successfully updated dynamic secret" - }); + createNotification({ + type: "success", + text: "Successfully updated dynamic secret" + }); - onClose(); - } catch (err) { - createNotification({ - type: "error", - text: `Failed to update dynamic secret: ${err}` - }); - } + onClose(); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretElasticSearchForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretElasticSearchForm.tsx index 000762bc5..015f394ea 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretElasticSearchForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretElasticSearchForm.tsx @@ -123,31 +123,24 @@ export const EditDynamicSecretElasticSearchForm = ({ // wait till previous request is finished if (updateDynamicSecret.isPending) return; const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - try { - await updateDynamicSecret.mutateAsync({ - name: dynamicSecret.name, - path: secretPath, - projectSlug, - environmentSlug: environment, - data: { - maxTTL: maxTTL || undefined, - defaultTTL, - inputs, - newName: newName === dynamicSecret.name ? undefined : newName, - usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate - } - }); - onClose(); - createNotification({ - type: "success", - text: "Successfully updated dynamic secret" - }); - } catch { - createNotification({ - type: "error", - text: "Failed to update dynamic secret" - }); - } + await updateDynamicSecret.mutateAsync({ + name: dynamicSecret.name, + path: secretPath, + projectSlug, + environmentSlug: environment, + data: { + maxTTL: maxTTL || undefined, + defaultTTL, + inputs, + newName: newName === dynamicSecret.name ? undefined : newName, + usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate + } + }); + onClose(); + createNotification({ + type: "success", + text: "Successfully updated dynamic secret" + }); }; const selectedAuthType = watch("inputs.auth.type"); diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretGcpIamForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretGcpIamForm.tsx index fb313f730..a2755e9c8 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretGcpIamForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretGcpIamForm.tsx @@ -77,30 +77,23 @@ export const EditDynamicSecretGcpIamForm = ({ const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => { // wait till previous request is finished if (updateDynamicSecret.isPending) return; - try { - await updateDynamicSecret.mutateAsync({ - name: dynamicSecret.name, - path: secretPath, - projectSlug, - environmentSlug: environment, - data: { - maxTTL: maxTTL || undefined, - defaultTTL, - inputs, - newName: newName === dynamicSecret.name ? undefined : newName - } - }); - onClose(); - createNotification({ - type: "success", - text: "Successfully updated dynamic secret" - }); - } catch { - createNotification({ - type: "error", - text: "Failed to update dynamic secret" - }); - } + await updateDynamicSecret.mutateAsync({ + name: dynamicSecret.name, + path: secretPath, + projectSlug, + environmentSlug: environment, + data: { + maxTTL: maxTTL || undefined, + defaultTTL, + inputs, + newName: newName === dynamicSecret.name ? undefined : newName + } + }); + onClose(); + createNotification({ + type: "success", + text: "Successfully updated dynamic secret" + }); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretGithubForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretGithubForm.tsx index 434bc9465..b5539d85d 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretGithubForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretGithubForm.tsx @@ -61,28 +61,21 @@ export const EditDynamicSecretGithubForm = ({ const handleUpdateDynamicSecret = async ({ inputs, newName }: TForm) => { if (updateDynamicSecret.isPending) return; - try { - await updateDynamicSecret.mutateAsync({ - name: dynamicSecret.name, - path: secretPath, - projectSlug, - environmentSlug: environment, - data: { - inputs, - newName: newName === dynamicSecret.name ? undefined : newName - } - }); - onClose(); - createNotification({ - type: "success", - text: "Successfully updated dynamic secret" - }); - } catch { - createNotification({ - type: "error", - text: "Failed to update dynamic secret" - }); - } + await updateDynamicSecret.mutateAsync({ + name: dynamicSecret.name, + path: secretPath, + projectSlug, + environmentSlug: environment, + data: { + inputs, + newName: newName === dynamicSecret.name ? undefined : newName + } + }); + onClose(); + createNotification({ + type: "success", + text: "Successfully updated dynamic secret" + }); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretKubernetesForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretKubernetesForm.tsx index 95f40f9f5..e599c2d6a 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretKubernetesForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretKubernetesForm.tsx @@ -189,38 +189,29 @@ export const EditDynamicSecretKubernetesForm = ({ // wait till previous request is finished if (updateDynamicSecret.isPending) return; const isDefaultUsernameTemplate = formData.usernameTemplate === "{{randomUsername}}"; - try { - await updateDynamicSecret.mutateAsync({ - name: dynamicSecret.name, - path: secretPath, - projectSlug, - environmentSlug: environment, - data: { - inputs: { - ...formData.inputs, - url: formData.inputs.url || undefined - }, - newName: formData.newName === dynamicSecret.name ? undefined : formData.newName, - defaultTTL: formData.defaultTTL, - maxTTL: formData.maxTTL, - usernameTemplate: - !formData.usernameTemplate || isDefaultUsernameTemplate - ? null - : formData.usernameTemplate - } - }); + await updateDynamicSecret.mutateAsync({ + name: dynamicSecret.name, + path: secretPath, + projectSlug, + environmentSlug: environment, + data: { + inputs: { + ...formData.inputs, + url: formData.inputs.url || undefined + }, + newName: formData.newName === dynamicSecret.name ? undefined : formData.newName, + defaultTTL: formData.defaultTTL, + maxTTL: formData.maxTTL, + usernameTemplate: + !formData.usernameTemplate || isDefaultUsernameTemplate ? null : formData.usernameTemplate + } + }); - onClose(); - createNotification({ - type: "success", - text: "Successfully updated dynamic secret" - }); - } catch (err) { - createNotification({ - type: "error", - text: err instanceof Error ? err.message : "Failed to update dynamic secret" - }); - } + onClose(); + createNotification({ + type: "success", + text: "Successfully updated dynamic secret" + }); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretLdapForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretLdapForm.tsx index 09d345d3d..e95d89561 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretLdapForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretLdapForm.tsx @@ -119,31 +119,24 @@ export const EditDynamicSecretLdapForm = ({ if (updateDynamicSecret.isPending) return; const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - try { - await updateDynamicSecret.mutateAsync({ - name: dynamicSecret.name, - path: secretPath, - projectSlug, - environmentSlug: environment, - data: { - maxTTL: maxTTL || undefined, - defaultTTL, - inputs, - newName: newName === dynamicSecret.name ? undefined : newName, - usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate - } - }); - onClose(); - createNotification({ - type: "success", - text: "Successfully updated dynamic secret" - }); - } catch { - createNotification({ - type: "error", - text: "Failed to update dynamic secret" - }); - } + await updateDynamicSecret.mutateAsync({ + name: dynamicSecret.name, + path: secretPath, + projectSlug, + environmentSlug: environment, + data: { + maxTTL: maxTTL || undefined, + defaultTTL, + inputs, + newName: newName === dynamicSecret.name ? undefined : newName, + usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate + } + }); + onClose(); + createNotification({ + type: "success", + text: "Successfully updated dynamic secret" + }); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretMongoAtlasForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretMongoAtlasForm.tsx index 5b577192a..905e00660 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretMongoAtlasForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretMongoAtlasForm.tsx @@ -142,31 +142,24 @@ export const EditDynamicSecretMongoAtlasForm = ({ if (updateDynamicSecret.isPending) return; const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - try { - await updateDynamicSecret.mutateAsync({ - name: dynamicSecret.name, - path: secretPath, - projectSlug, - environmentSlug: environment, - data: { - maxTTL: maxTTL || undefined, - defaultTTL, - inputs, - newName: newName === dynamicSecret.name ? undefined : newName, - usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate - } - }); - onClose(); - createNotification({ - type: "success", - text: "Successfully updated dynamic secret" - }); - } catch { - createNotification({ - type: "error", - text: "Failed to update dynamic secret" - }); - } + await updateDynamicSecret.mutateAsync({ + name: dynamicSecret.name, + path: secretPath, + projectSlug, + environmentSlug: environment, + data: { + maxTTL: maxTTL || undefined, + defaultTTL, + inputs, + newName: newName === dynamicSecret.name ? undefined : newName, + usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate + } + }); + onClose(); + createNotification({ + type: "success", + text: "Successfully updated dynamic secret" + }); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretMongoDBForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretMongoDBForm.tsx index 90c31e1f0..d5b63a1cf 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretMongoDBForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretMongoDBForm.tsx @@ -108,36 +108,28 @@ export const EditDynamicSecretMongoDBForm = ({ if (updateDynamicSecret.isPending) return; const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - try { - await updateDynamicSecret.mutateAsync({ - name: dynamicSecret.name, - path: secretPath, - projectSlug, - environmentSlug: environment, - data: { - maxTTL: maxTTL || undefined, - defaultTTL, - inputs: { - ...inputs, - port: inputs?.port ? inputs.port : undefined, - roles: inputs?.roles?.map((el) => el.roleName) - }, - usernameTemplate: - !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate, - newName: newName === dynamicSecret.name ? undefined : newName - } - }); - onClose(); - createNotification({ - type: "success", - text: "Successfully updated dynamic secret" - }); - } catch { - createNotification({ - type: "error", - text: "Failed to update dynamic secret" - }); - } + await updateDynamicSecret.mutateAsync({ + name: dynamicSecret.name, + path: secretPath, + projectSlug, + environmentSlug: environment, + data: { + maxTTL: maxTTL || undefined, + defaultTTL, + inputs: { + ...inputs, + port: inputs?.port ? inputs.port : undefined, + roles: inputs?.roles?.map((el) => el.roleName) + }, + usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate, + newName: newName === dynamicSecret.name ? undefined : newName + } + }); + onClose(); + createNotification({ + type: "success", + text: "Successfully updated dynamic secret" + }); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretRabbitMqForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretRabbitMqForm.tsx index 984e68831..dbf0d90d0 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretRabbitMqForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretRabbitMqForm.tsx @@ -100,31 +100,24 @@ export const EditDynamicSecretRabbitMqForm = ({ if (updateDynamicSecret.isPending) return; const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - try { - await updateDynamicSecret.mutateAsync({ - name: dynamicSecret.name, - path: secretPath, - projectSlug, - environmentSlug: environment, - data: { - maxTTL: maxTTL || undefined, - defaultTTL, - inputs, - newName: newName === dynamicSecret.name ? undefined : newName, - usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate - } - }); - onClose(); - createNotification({ - type: "success", - text: "Successfully updated dynamic secret" - }); - } catch { - createNotification({ - type: "error", - text: "Failed to update dynamic secret" - }); - } + await updateDynamicSecret.mutateAsync({ + name: dynamicSecret.name, + path: secretPath, + projectSlug, + environmentSlug: environment, + data: { + maxTTL: maxTTL || undefined, + defaultTTL, + inputs, + newName: newName === dynamicSecret.name ? undefined : newName, + usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate + } + }); + onClose(); + createNotification({ + type: "success", + text: "Successfully updated dynamic secret" + }); }; const selectedTags = watch("inputs.tags"); diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretRedisProviderForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretRedisProviderForm.tsx index bc21a17d7..add8d6042 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretRedisProviderForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretRedisProviderForm.tsx @@ -102,32 +102,24 @@ export const EditDynamicSecretRedisProviderForm = ({ // wait till previous request is finished if (updateDynamicSecret.isPending) return; const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - try { - await updateDynamicSecret.mutateAsync({ - name: dynamicSecret.name, - path: secretPath, - projectSlug, - environmentSlug: environment, - data: { - usernameTemplate: - !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate, - maxTTL: maxTTL || undefined, - defaultTTL, - inputs, - newName: newName === dynamicSecret.name ? undefined : newName - } - }); - onClose(); - createNotification({ - type: "success", - text: "Successfully updated dynamic secret" - }); - } catch { - createNotification({ - type: "error", - text: "Failed to update dynamic secret" - }); - } + await updateDynamicSecret.mutateAsync({ + name: dynamicSecret.name, + path: secretPath, + projectSlug, + environmentSlug: environment, + data: { + usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate, + maxTTL: maxTTL || undefined, + defaultTTL, + inputs, + newName: newName === dynamicSecret.name ? undefined : newName + } + }); + onClose(); + createNotification({ + type: "success", + text: "Successfully updated dynamic secret" + }); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSapAseForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSapAseForm.tsx index 37dbae5ee..2f55cca2b 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSapAseForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSapAseForm.tsx @@ -101,31 +101,24 @@ export const EditDynamicSecretSapAseForm = ({ if (updateDynamicSecret.isPending) return; const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - try { - await updateDynamicSecret.mutateAsync({ - name: dynamicSecret.name, - path: secretPath, - projectSlug, - environmentSlug: environment, - data: { - maxTTL: maxTTL || undefined, - defaultTTL, - inputs, - newName: newName === dynamicSecret.name ? undefined : newName, - usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate - } - }); - onClose(); - createNotification({ - type: "success", - text: "Successfully updated dynamic secret" - }); - } catch { - createNotification({ - type: "error", - text: "Failed to update dynamic secret" - }); - } + await updateDynamicSecret.mutateAsync({ + name: dynamicSecret.name, + path: secretPath, + projectSlug, + environmentSlug: environment, + data: { + maxTTL: maxTTL || undefined, + defaultTTL, + inputs, + newName: newName === dynamicSecret.name ? undefined : newName, + usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate + } + }); + onClose(); + createNotification({ + type: "success", + text: "Successfully updated dynamic secret" + }); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSapHanaForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSapHanaForm.tsx index 430f13e83..d0f6a9d90 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSapHanaForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSapHanaForm.tsx @@ -101,31 +101,24 @@ export const EditDynamicSecretSapHanaForm = ({ if (updateDynamicSecret.isPending) return; const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - try { - await updateDynamicSecret.mutateAsync({ - name: dynamicSecret.name, - path: secretPath, - projectSlug, - environmentSlug: environment, - data: { - maxTTL: maxTTL || undefined, - defaultTTL, - inputs, - newName: newName === dynamicSecret.name ? undefined : newName, - usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate - } - }); - onClose(); - createNotification({ - type: "success", - text: "Successfully updated dynamic secret" - }); - } catch { - createNotification({ - type: "error", - text: "Failed to update dynamic secret" - }); - } + await updateDynamicSecret.mutateAsync({ + name: dynamicSecret.name, + path: secretPath, + projectSlug, + environmentSlug: environment, + data: { + maxTTL: maxTTL || undefined, + defaultTTL, + inputs, + newName: newName === dynamicSecret.name ? undefined : newName, + usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate + } + }); + onClose(); + createNotification({ + type: "success", + text: "Successfully updated dynamic secret" + }); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSnowflakeForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSnowflakeForm.tsx index 1cbc20cfb..384278494 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSnowflakeForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSnowflakeForm.tsx @@ -97,32 +97,25 @@ export const EditDynamicSecretSnowflakeForm = ({ }: TForm) => { // wait till previous request is finished if (updateDynamicSecret.isPending) return; - try { - const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - await updateDynamicSecret.mutateAsync({ - name: dynamicSecret.name, - path: secretPath, - projectSlug, - environmentSlug: environment, - data: { - maxTTL: maxTTL || undefined, - defaultTTL, - inputs, - newName: newName === dynamicSecret.name ? undefined : newName, - usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate - } - }); - onClose(); - createNotification({ - type: "success", - text: "Successfully updated dynamic secret" - }); - } catch (err) { - createNotification({ - type: "error", - text: err instanceof Error ? err.message : "Failed to update dynamic secret" - }); - } + const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; + await updateDynamicSecret.mutateAsync({ + name: dynamicSecret.name, + path: secretPath, + projectSlug, + environmentSlug: environment, + data: { + maxTTL: maxTTL || undefined, + defaultTTL, + inputs, + newName: newName === dynamicSecret.name ? undefined : newName, + usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate + } + }); + onClose(); + createNotification({ + type: "success", + text: "Successfully updated dynamic secret" + }); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx index 929a1485d..d6da25f80 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx @@ -167,36 +167,29 @@ export const EditDynamicSecretSqlProviderForm = ({ }: TForm) => { // wait till previous request is finished if (updateDynamicSecret.isPending) return; - try { - const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - await updateDynamicSecret.mutateAsync({ - name: dynamicSecret.name, - path: secretPath, - projectSlug, - environmentSlug: environment, - data: { - maxTTL: maxTTL || undefined, - defaultTTL, - inputs: { - ...inputs, - gatewayId: isGatewayInActive ? null : inputs.gatewayId - }, - newName: newName === dynamicSecret.name ? undefined : newName, - metadata, - usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate - } - }); - onClose(); - createNotification({ - type: "success", - text: "Successfully updated dynamic secret" - }); - } catch { - createNotification({ - type: "error", - text: "Failed to update dynamic secret" - }); - } + const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; + await updateDynamicSecret.mutateAsync({ + name: dynamicSecret.name, + path: secretPath, + projectSlug, + environmentSlug: environment, + data: { + maxTTL: maxTTL || undefined, + defaultTTL, + inputs: { + ...inputs, + gatewayId: isGatewayInActive ? null : inputs.gatewayId + }, + newName: newName === dynamicSecret.name ? undefined : newName, + metadata, + usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate + } + }); + onClose(); + createNotification({ + type: "success", + text: "Successfully updated dynamic secret" + }); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretTotpForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretTotpForm.tsx index 7b4a4dac3..15dead8d0 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretTotpForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretTotpForm.tsx @@ -87,28 +87,21 @@ export const EditDynamicSecretTotpForm = ({ const handleUpdateDynamicSecret = async ({ inputs, newName }: TForm) => { // wait till previous request is finished if (updateDynamicSecret.isPending) return; - try { - await updateDynamicSecret.mutateAsync({ - name: dynamicSecret.name, - path: secretPath, - projectSlug, - environmentSlug: environment, - data: { - inputs, - newName: newName === dynamicSecret.name ? undefined : newName - } - }); - onClose(); - createNotification({ - type: "success", - text: "Successfully updated dynamic secret" - }); - } catch (err) { - createNotification({ - type: "error", - text: err instanceof Error ? err.message : "Failed to update dynamic secret" - }); - } + await updateDynamicSecret.mutateAsync({ + name: dynamicSecret.name, + path: secretPath, + projectSlug, + environmentSlug: environment, + data: { + inputs, + newName: newName === dynamicSecret.name ? undefined : newName + } + }); + onClose(); + createNotification({ + type: "success", + text: "Successfully updated dynamic secret" + }); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretVertica.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretVertica.tsx index e4ccda943..0579ba3b9 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretVertica.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretVertica.tsx @@ -146,34 +146,28 @@ export const EditDynamicSecretVerticaForm = ({ if (updateDynamicSecret.isPending) return; const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; - try { - await updateDynamicSecret.mutateAsync({ - name: dynamicSecret.name, - path: secretPath, - projectSlug, - environmentSlug: environment, - data: { - maxTTL: maxTTL || undefined, - defaultTTL, - inputs: { - ...inputs, - gatewayId: isGatewayInActive ? null : inputs.gatewayId - }, - newName: newName === dynamicSecret.name ? undefined : newName, - usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate - } - }); - onClose(); - createNotification({ - type: "success", - text: "Successfully updated dynamic secret" - }); - } catch { - createNotification({ - type: "error", - text: "Failed to update dynamic secret" - }); - } + + await updateDynamicSecret.mutateAsync({ + name: dynamicSecret.name, + path: secretPath, + projectSlug, + environmentSlug: environment, + data: { + maxTTL: maxTTL || undefined, + defaultTTL, + inputs: { + ...inputs, + gatewayId: isGatewayInActive ? null : inputs.gatewayId + }, + newName: newName === dynamicSecret.name ? undefined : newName, + usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate + } + }); + onClose(); + createNotification({ + type: "success", + text: "Successfully updated dynamic secret" + }); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/RenewDynamicSecretLease.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/RenewDynamicSecretLease.tsx index 85528a5bc..bdb5f9add 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/RenewDynamicSecretLease.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/RenewDynamicSecretLease.tsx @@ -64,27 +64,19 @@ export const RenewDynamicSecretLease = ({ const handleDynamicSecretLeaseCreate = async ({ ttl }: TForm) => { if (renewDynamicSecretLease.isPending) return; - try { - await renewDynamicSecretLease.mutateAsync({ - environmentSlug: environment, - projectSlug, - path: secretPath, - ttl, - dynamicSecretName, - leaseId - }); - onClose(); - createNotification({ - type: "success", - text: "Successfully renewed lease" - }); - } catch (error) { - console.log(error); - createNotification({ - type: "error", - text: "Failed to renew lease" - }); - } + await renewDynamicSecretLease.mutateAsync({ + environmentSlug: environment, + projectSlug, + path: secretPath, + ttl, + dynamicSecretName, + leaseId + }); + onClose(); + createNotification({ + type: "success", + text: "Successfully renewed lease" + }); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderListView/FolderListView.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderListView/FolderListView.tsx index bd812b837..6e39b8ffc 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderListView/FolderListView.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderListView/FolderListView.tsx @@ -69,73 +69,65 @@ export const FolderListView = ({ oldFolderName?: string, oldFolderDescription?: string ) => { - try { - const updateFolderData = popUp.updateFolder.data; - if (!updateFolderData) throw new Error("Update folder data is required"); - const { id: folderId, pendingAction, isPending } = updateFolderData as TSecretFolder; + const updateFolderData = popUp.updateFolder.data; + if (!updateFolderData) throw new Error("Update folder data is required"); + const { id: folderId, pendingAction, isPending } = updateFolderData as TSecretFolder; - if (isBatchMode) { - const isEditingPendingCreation = isPending && pendingAction === PendingAction.Create; + if (isBatchMode) { + const isEditingPendingCreation = isPending && pendingAction === PendingAction.Create; - if (isEditingPendingCreation) { - const updatedCreate: PendingFolderCreate = { - id: folderId, - type: PendingAction.Create, - folderName: newFolderName, - description: newFolderDescription || undefined, - parentPath: secretPath, - timestamp: Date.now(), - resourceType: "folder" - }; + if (isEditingPendingCreation) { + const updatedCreate: PendingFolderCreate = { + id: folderId, + type: PendingAction.Create, + folderName: newFolderName, + description: newFolderDescription || undefined, + parentPath: secretPath, + timestamp: Date.now(), + resourceType: "folder" + }; - addPendingChange(updatedCreate, { - projectId, - environment, - secretPath - }); - } else { - const updateChange: PendingFolderUpdate = { - id: folderId, - type: PendingAction.Update, - originalFolderName: oldFolderName || "", - folderName: newFolderName, - originalDescription: oldFolderDescription, - description: newFolderDescription || undefined, - timestamp: Date.now(), - resourceType: "folder" - }; + addPendingChange(updatedCreate, { + projectId, + environment, + secretPath + }); + } else { + const updateChange: PendingFolderUpdate = { + id: folderId, + type: PendingAction.Update, + originalFolderName: oldFolderName || "", + folderName: newFolderName, + originalDescription: oldFolderDescription, + description: newFolderDescription || undefined, + timestamp: Date.now(), + resourceType: "folder" + }; - addPendingChange(updateChange, { - projectId, - environment, - secretPath - }); - } - - handlePopUpClose("updateFolder"); - return; + addPendingChange(updateChange, { + projectId, + environment, + secretPath + }); } - await updateFolder({ - folderId, - name: newFolderName, - path: secretPath, - environment, - projectId, - description: newFolderDescription - }); handlePopUpClose("updateFolder"); - createNotification({ - type: "success", - text: "Successfully saved folder" - }); - } catch (error) { - console.log(error); - createNotification({ - type: "error", - text: "Failed to save folder" - }); + return; } + + await updateFolder({ + folderId, + name: newFolderName, + path: secretPath, + environment, + projectId, + description: newFolderDescription + }); + handlePopUpClose("updateFolder"); + createNotification({ + type: "success", + text: "Successfully saved folder" + }); }; const handleDeletePending = (id: string) => { @@ -147,48 +139,40 @@ export const FolderListView = ({ }; const handleFolderDelete = async () => { - try { - const folderData = popUp.deleteFolder?.data as TSecretFolder; + const folderData = popUp.deleteFolder?.data as TSecretFolder; - if (isBatchMode) { - const pendingFolderDelete: PendingFolderDelete = { - id: folderData.id, - folderName: folderData.name, - folderPath: secretPath, - resourceType: "folder", - type: PendingAction.Delete, - timestamp: Date.now() - }; + if (isBatchMode) { + const pendingFolderDelete: PendingFolderDelete = { + id: folderData.id, + folderName: folderData.name, + folderPath: secretPath, + resourceType: "folder", + type: PendingAction.Delete, + timestamp: Date.now() + }; - addPendingChange(pendingFolderDelete, { - projectId, - environment, - secretPath - }); - - handlePopUpClose("deleteFolder"); - return; - } - - await deleteFolder({ - folderId: folderData.id, - path: secretPath, + addPendingChange(pendingFolderDelete, { + projectId, environment, - projectId + secretPath }); handlePopUpClose("deleteFolder"); - createNotification({ - type: "success", - text: "Successfully deleted folder" - }); - } catch (error) { - console.log(error); - createNotification({ - type: "error", - text: "Failed to delete folder" - }); + return; } + + await deleteFolder({ + folderId: folderData.id, + path: secretPath, + environment, + projectId + }); + + handlePopUpClose("deleteFolder"); + createNotification({ + type: "success", + text: "Successfully deleted folder" + }); }; const handleFolderClick = (name: string, isPending?: boolean) => { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportItem.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportItem.tsx index d4e7d42d5..6a127d6b5 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportItem.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportItem.tsx @@ -118,24 +118,16 @@ export const SecretImportItem = ({ const handleResyncSecretReplication = async () => { if (resyncSecretReplication.isPending) return; - try { - await resyncSecretReplication.mutateAsync({ - id, - environment, - path: secretPath, - projectId: currentProject?.id || "" - }); - createNotification({ - text: "Please refresh the dashboard to view changes", - type: "success" - }); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to resync replication", - type: "error" - }); - } + await resyncSecretReplication.mutateAsync({ + id, + environment, + path: secretPath, + projectId: currentProject?.id || "" + }); + createNotification({ + text: "Please refresh the dashboard to view changes", + type: "success" + }); }; const handleRowClick = () => { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportListView.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportListView.tsx index 763872e9e..794fce88f 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportListView.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportListView.tsx @@ -147,25 +147,17 @@ export const SecretImportListView = ({ const handleSecretImportDelete = async () => { const { id: secretImportId } = popUp.deleteSecretImport?.data as { id: string }; - try { - await deleteSecretImport({ - projectId, - environment, - path: secretPath, - id: secretImportId - }); - handlePopUpClose("deleteSecretImport"); - createNotification({ - type: "success", - text: "Successfully removed secret link" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to remove secret link", - type: "error" - }); - } + await deleteSecretImport({ + projectId, + environment, + path: secretPath, + id: secretImportId + }); + handlePopUpClose("deleteSecretImport"); + createNotification({ + type: "success", + text: "Successfully removed secret link" + }); }; const handleSecretImportReorder = ({ over, active }: DragEndEvent) => { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CreateReminderForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CreateReminderForm.tsx index 3f7727290..531401998 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CreateReminderForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CreateReminderForm.tsx @@ -199,53 +199,37 @@ export const CreateReminderForm = ({ // Form submission handler const handleFormSubmit = async (data: TReminderFormSchema) => { - try { - await createReminder({ - repeatDays: data.repeatDays, - message: data.message, - recipients: data.recipients?.map((r) => r.value) || [], - secretId, - nextReminderDate: data.nextReminderDate, - fromDate: data.fromDate - }); + await createReminder({ + repeatDays: data.repeatDays, + message: data.message, + recipients: data.recipients?.map((r) => r.value) || [], + secretId, + nextReminderDate: data.nextReminderDate, + fromDate: data.fromDate + }); - invalidateQueries(); + invalidateQueries(); - createNotification({ - type: "success", - text: `Successfully ${isEditMode ? "updated" : "created"} secret reminder` - }); + createNotification({ + type: "success", + text: `Successfully ${isEditMode ? "updated" : "created"} secret reminder` + }); - reset(); - onOpenChange(); - } catch (error) { - console.error("Failed to save reminder:", error); - createNotification({ - type: "error", - text: "Failed to save reminder. Please try again." - }); - } + reset(); + onOpenChange(); }; // Delete reminder handler const handleDeleteReminder = async () => { - try { - await deleteReminder({ reminderId: reminder?.id || "", secretId }); - invalidateQueries(); - reset(); - onOpenChange(); + await deleteReminder({ reminderId: reminder?.id || "", secretId }); + invalidateQueries(); + reset(); + onOpenChange(); - createNotification({ - type: "success", - text: "Successfully deleted reminder" - }); - } catch (error) { - console.error("Failed to delete reminder:", error); - createNotification({ - type: "error", - text: "Failed to delete reminder. Please try again." - }); - } + createNotification({ + type: "success", + text: "Successfully deleted reminder" + }); }; // Handle reminder type change diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SnapshotView/SnapshotView.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SnapshotView/SnapshotView.tsx index eed335fa8..ca12e76a4 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SnapshotView/SnapshotView.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SnapshotView/SnapshotView.tsx @@ -128,25 +128,17 @@ export const SnapshotView = ({ }); return; } - try { - await performRollback({ - projectId, - snapshotId: snapshotData.id, - environment, - directory: secretPath - }); - createNotification({ - text: "Successfully rollback secrets", - type: "success" - }); - onGoBack(); - } catch (error) { - console.log(error); - createNotification({ - text: "Failed to rollback secrets", - type: "error" - }); - } + await performRollback({ + projectId, + snapshotId: snapshotData.id, + environment, + directory: secretPath + }); + createNotification({ + text: "Successfully rollback secrets", + type: "success" + }); + onGoBack(); }; if (isSnapshotLoading) { diff --git a/frontend/src/pages/secret-manager/SecretRotationPage/components/CreateRotationForm/CreateRotationForm.tsx b/frontend/src/pages/secret-manager/SecretRotationPage/components/CreateRotationForm/CreateRotationForm.tsx index 952c869db..cfc34df2c 100644 --- a/frontend/src/pages/secret-manager/SecretRotationPage/components/CreateRotationForm/CreateRotationForm.tsx +++ b/frontend/src/pages/secret-manager/SecretRotationPage/components/CreateRotationForm/CreateRotationForm.tsx @@ -1,7 +1,6 @@ import { useRef, useState } from "react"; import { AnimatePresence, motion } from "framer-motion"; -import { createNotification } from "@app/components/notifications"; import { Modal, ModalContent, Step, Stepper } from "@app/components/v2"; import { useCreateSecretRotation } from "@app/hooks/api"; import { TSecretRotationProviderTemplate } from "@app/hooks/api/types"; @@ -54,27 +53,19 @@ export const CreateRotationForm = ({ const handleFormSubmit = async () => { if (!wizardData.current.input || !wizardData.current.output) return; - try { - await createSecretRotation({ - workspaceId, - provider: provider.name, - customProvider, - secretPath: wizardData.current.output.secretPath, - environment: wizardData.current.output.environment, - interval: wizardData.current.output.interval, - inputs: wizardData.current.input, - outputs: wizardData.current.output.secrets - }); - setWizardStep(0); - onToggle(false); - wizardData.current = {}; - } catch (error) { - console.log(error); - createNotification({ - type: "error", - text: "Failed to create secret rotation" - }); - } + await createSecretRotation({ + workspaceId, + provider: provider.name, + customProvider, + secretPath: wizardData.current.output.secretPath, + environment: wizardData.current.output.environment, + interval: wizardData.current.output.interval, + inputs: wizardData.current.input, + outputs: wizardData.current.output.secrets + }); + setWizardStep(0); + onToggle(false); + wizardData.current = {}; }; return ( From 909a49d3efc3c9ca34cabda3631535e4de415219 Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Mon, 3 Nov 2025 18:42:11 -0300 Subject: [PATCH 030/294] refactor: update key usage mapping to improve readability and consistency in CertificateTemplateModal --- .../components/CertificateTemplateModal.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplateModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplateModal.tsx index 66e9ec57a..0ba3721e3 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplateModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplateModal.tsx @@ -171,10 +171,14 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro ttl, keyUsages: Object.entries(keyUsages) .filter(([, value]) => value) - .map(([key]) => key as CertKeyUsage), + .map(([key]) => + key === CertKeyUsage.CRL_SIGN + ? "cRLSign" + : key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()) + ), extendedKeyUsages: Object.entries(extendedKeyUsages) .filter(([, value]) => value) - .map(([key]) => key as CertExtendedKeyUsage) + .map(([key]) => key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())) }); createNotification({ @@ -192,10 +196,14 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro ttl, keyUsages: Object.entries(keyUsages) .filter(([, value]) => value) - .map(([key]) => key as CertKeyUsage), + .map(([key]) => + key === CertKeyUsage.CRL_SIGN + ? "cRLSign" + : key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()) + ), extendedKeyUsages: Object.entries(extendedKeyUsages) .filter(([, value]) => value) - .map(([key]) => key as CertExtendedKeyUsage) + .map(([key]) => key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())) }); createNotification({ From d1ea07959a7059c1d6ad00da1fa4621b23bbf214 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 3 Nov 2025 14:35:02 -0800 Subject: [PATCH 031/294] feat: add text filter support to audit log event and user agent filters --- .../AuditLogsPage/components/LogsFilter.tsx | 134 +++++------------- 1 file changed, 38 insertions(+), 96 deletions(-) diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx index 8e0124fb5..ece7b522a 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx @@ -1,7 +1,8 @@ /* eslint-disable no-nested-ternary */ import { useMemo } from "react"; import { Controller, useForm } from "react-hook-form"; -import { faCaretDown, faCheckCircle, faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons"; +import { MultiValue, SingleValue } from "react-select"; +import { faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { AnimatePresence, motion } from "framer-motion"; @@ -11,13 +12,10 @@ import { Button, DropdownMenu, DropdownMenuContent, - DropdownMenuItem, DropdownMenuTrigger, FilterableSelect, FormControl, - Input, - Select, - SelectItem + Input } from "@app/components/v2"; import { Badge } from "@app/components/v3"; import { useOrganization } from "@app/context"; @@ -132,7 +130,7 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
-
+
@@ -173,76 +171,24 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => { name="eventType" render={({ field }) => ( - - -
- {selectedEventTypes?.length === 1 - ? filteredEventTypes.find( - (eventType) => eventType.value === selectedEventTypes[0] - )?.label - : selectedEventTypes?.length === 0 - ? "All events" - : `${selectedEventTypes?.length} events selected`} - -
-
- -
- {filteredEventTypes.length > 0 ? ( - filteredEventTypes.map((eventType) => { - const isSelected = selectedEventTypes?.includes( - eventType.value as EventType - ); - - return ( - - filteredEventTypes.length > 1 && event.preventDefault() - } - onClick={() => { - if ( - selectedEventTypes?.includes(eventType.value as EventType) - ) { - field.onChange( - selectedEventTypes?.filter( - (e: string) => e !== eventType.value - ) - ); - } else { - field.onChange([ - ...(selectedEventTypes || []), - eventType.value - ]); - } - }} - key={`event-type-${eventType.value}`} - icon={ - isSelected ? ( - - ) : ( -
- ) - } - iconPos="left" - className="w-[28.4rem] text-sm" - > - {eventType.label} - - ); - }) - ) : ( -
- )} -
- - + + field.value.includes(eventType.value as EventType) + )} + isMulti + isClearable + onChange={(options) => + field.onChange( + (options as MultiValue<(typeof filteredEventTypes)[number]>).map( + (option) => option.value + ) + ) + } + placeholder="All events" + options={filteredEventTypes} + getOptionValue={(option) => option.value} + getOptionLabel={(option) => option.label} + /> )} /> @@ -256,31 +202,27 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => { ( + render={({ field: { onChange, value }, fieldState: { error } }) => ( - + value === (userAgentType.value as UserAgentType) + ) ?? null + } + isClearable + onChange={(option) => + onChange((option as SingleValue<(typeof userAgentTypes)[number]>)?.value) + } + placeholder="All sources" + options={userAgentTypes} + getOptionValue={(option) => option.value} + getOptionLabel={(option) => option.label} + /> )} /> From a3e8e5433f9ccb805940e1b0c97648fddc9f8a2c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 4 Nov 2025 04:46:09 +0400 Subject: [PATCH 032/294] docs: pkcs11 requirements --- .../platform/kms/hsm-integration.mdx | 89 ++++++++++++++++++- 1 file changed, 86 insertions(+), 3 deletions(-) diff --git a/docs/documentation/platform/kms/hsm-integration.mdx b/docs/documentation/platform/kms/hsm-integration.mdx index 7a8d15fe5..ea3918ae3 100644 --- a/docs/documentation/platform/kms/hsm-integration.mdx +++ b/docs/documentation/platform/kms/hsm-integration.mdx @@ -30,13 +30,96 @@ Using a hardware security module comes with the added benefit of having a secure Enabling HSM encryption has a set of key benefits: 1. **Root Key Wrapping**: The root KMS encryption key that is used to secure your Infisical instance will be encrypted using the HSM device rather than the standard software-protected key. + #### Caveats - **Performance**: Using an HSM device can have a performance impact on your Infisical instance. This is due to the additional latency introduced by the HSM device. This is however only noticeable when your instance(s) start up or when the encryption strategy is changed. - **Key Recovery**: If the HSM device is lost or destroyed, you will no longer be able to decrypt your data stored within Infisical. Most HSM providers offer recovery options, which you should consider when setting up an HSM device. -### Requirements -- An Infisical instance with a version number that is equal to or greater than `v0.91.0`. -- An HSM device from a provider such as [Thales Luna HSM](https://cpl.thalesgroup.com/encryption/data-protection-on-demand/services/luna-cloud-hsm), [AWS CloudHSM](https://aws.amazon.com/cloudhsm/), [Fortanix HSM](https://www.fortanix.com/platform/data-security-manager), or others. +## Requirements +- An HSM device _(PKCS#11 compatible library)_ from a compatible provider such as [Thales Luna HSM](https://cpl.thalesgroup.com/encryption/data-protection-on-demand/services/luna-cloud-hsm), [AWS CloudHSM](https://aws.amazon.com/cloudhsm/), [Fortanix HSM](https://www.fortanix.com/platform/data-security-manager), or others. + Infisical is validated to work with PKCS#11 2.40 and later. If your HSM device doesn't follow the >=2.40 PKCS#11 standard you may see degraded performance. + + +## Environment Variable Configuration +To configure your Infisical instance to use an HSM, you must set the required environment variables. Below you'll find an example of the required environment variables. +For further instructions on how to configure the HSM device for your Infisical instance, please see the [Setup Instructions](#setup-instructions) section. + + +```dotenv +HSM_LIB_PATH=/usr/local/lib/cloudhsm/cloudhsm.so +HSM_SLOT=1 +HSM_KEY_LABEL=infisical-key +HSM_PIN=your:pin +``` + +- `HSM_LIB_PATH`: The path to the PKCS#11 library provided by the HSM provider. This usually comes in the form of a `.so` for Linux and MacOS, or a `.dll` file for Windows. For Docker, you need to mount the library path as a volume. Further instructions can be found below. If you are using Docker, make sure to set the HSM_LIB_PATH environment variable to the path where the library is mounted in the container. +- `HSM_PIN`: The PKCS#11 PIN to use for authentication with the HSM device. +- `HSM_SLOT`: The slot number to use for the HSM device. This is typically between `0` and `5` for most HSM devices. +- `HSM_KEY_LABEL`: The label of the key to use for encryption. **Please note that if no key is found with the provided label, the HSM will create a new key with the provided label.** + +You can read more about the [default instance configurations](/self-hosting/configuration/envars) here. + +## PKCS#11 Key Attributes + +If no AES key or HMAC key already exists with the label you defined on the `HSM_KEY_LABEL` environment variable, then Infisical will create one for you automatically using the label specified on `HSM_KEY_LABEL`. +Below you'll find a list of the attributes each key will be created with. + +### AES Key + + + If you bring your own AES key and don't let Infisical create it for you it must have at least the following attributes: + + * `CKA_CLASS`: `CKO_PRIVATE_KEY` — Defines the key class _(secret key)_. + * `CKA_KEY_TYPE`: `CKO_AES` — Defines the key type _(AES key)_. + * `CKA_VALUE_LEN`: `32` — 256-bit key size. + * `CKA_ENCRYPT`: `true` — Encryption capabilities enabled. + * `CKA_DECRYPT`: `true` — Decryption capabilities enabled. + * `CKA_TOKEN`: `true` — The key material will persist in your HSM so it can be reused. + + + Note that for security reasons it is highly recommended to create an AES key with the full set of key attributes seen below if you're going to bring your own key. + + + +* `CKA_CLASS`: `CKO_PRIVATE_KEY` — Defines the key class _(secret key)_. +* `CKA_KEY_TYPE`: `CKO_AES` — Defines the key type _(AES key)_. +* `CKA_VALUE_LEN`: `32` — 256-bit key size. +* `CKA_LABEL`: Your specified label in the `HSM_KEY_LABEL` environment variable. +* `CKA_ENCRYPT`: `true` — Encryption capabilities enabled. +* `CKA_DECRYPT`: `true` — Decryption capabilities enabled. +* `CKA_TOKEN`: `true` — The key material will persist in your HSM so it can be reused. +* `CKA_EXTRACTABLE`: `false` — The key material is not extractable from the HSM. +* `CKA_SENSITIVE`: `true` — The key material is marked as sensitive. +* `CKA_PRIVATE`: `true` — The key material is marked as private to the slot and can't be accessed from other slots. + +### HMAC Key + + + If you bring your own AES key and don't let Infisical create it for you it must have at least the following attributes: + + * `CKA_CLASS`: `CKO_PRIVATE_KEY` — Defines the key class _(secret key)_. + * `CKA_KEY_TYPE`: `CKO_GENERIC_SECRET` — Defines the key class _(generic secret key)_. + * `CKA_VALUE_LEN`: `32` — 256-bit key size + * `CKA_SIGN`: `true` — Signing capabilities enabled + * `CKA_VERIFY`: `true` — Verifying capabilities enabled. + * `CKA_TOKEN`: `true` — The key material will persist in your HSM so it can be reused. + + + Note that for security reasons it is highly recommended to create an HMAC key with the full set of key attributes seen below if you're going to bring your own key. + + + +* `CKA_CLASS`: `CKO_PRIVATE_KEY` — Defines the key class _(secret key)_. +* `CKA_KEY_TYPE`: `CKO_GENERIC_SECRET` — Defines the key class _(generic secret key)_. +* `CKA_VALUE_LEN`: `32` — 256-bit key size. +* `CKA_LABEL`: Your specified label in the `HSM_KEY_LABEL` environment variable, suffixed with `_HMAC`. If you specify `infisical-key-v1`, then the HMAC key label will become `infisical-key-v1_HMAC`. +* `CKA_SIGN`: `true` — Signing capabilities enabled +* `CKA_VERIFY`: `true` — Verifying capabilities enabled. +* `CKA_TOKEN`: `true` — The key material will persist in your HSM so it can be reused. +* `CKA_EXTRACTABLE`: `false` — The key material is not extractable from the HSM. +* `CKA_SENSITIVE`: `true` — The key material is marked as sensitive. +* `CKA_PRIVATE`: `true` — The key material is marked as private to the slot and can't be accessed from other slots. + ## Setup Instructions From dab4c35ec9fd5056930d908f8926f34e4009dce7 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 4 Nov 2025 04:57:52 +0400 Subject: [PATCH 033/294] Update hsm-integration.mdx --- docs/documentation/platform/kms/hsm-integration.mdx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/documentation/platform/kms/hsm-integration.mdx b/docs/documentation/platform/kms/hsm-integration.mdx index ea3918ae3..a2e2dce16 100644 --- a/docs/documentation/platform/kms/hsm-integration.mdx +++ b/docs/documentation/platform/kms/hsm-integration.mdx @@ -37,7 +37,7 @@ Enabling HSM encryption has a set of key benefits: ## Requirements - An HSM device _(PKCS#11 compatible library)_ from a compatible provider such as [Thales Luna HSM](https://cpl.thalesgroup.com/encryption/data-protection-on-demand/services/luna-cloud-hsm), [AWS CloudHSM](https://aws.amazon.com/cloudhsm/), [Fortanix HSM](https://www.fortanix.com/platform/data-security-manager), or others. - Infisical is validated to work with PKCS#11 2.40 and later. If your HSM device doesn't follow the >=2.40 PKCS#11 standard you may see degraded performance. + Infisical is validated to work with PKCS#11 2.30 and newer. If your HSM device doesn't follow the >=2.30 PKCS#11 standard you may see degraded performance. ## Environment Variable Configuration @@ -69,7 +69,7 @@ Below you'll find a list of the attributes each key will be created with. If you bring your own AES key and don't let Infisical create it for you it must have at least the following attributes: - * `CKA_CLASS`: `CKO_PRIVATE_KEY` — Defines the key class _(secret key)_. + * `CKA_CLASS`: `CKO_SECRET_KEY` — Defines the key class _(secret key)_. * `CKA_KEY_TYPE`: `CKO_AES` — Defines the key type _(AES key)_. * `CKA_VALUE_LEN`: `32` — 256-bit key size. * `CKA_ENCRYPT`: `true` — Encryption capabilities enabled. @@ -81,7 +81,7 @@ Below you'll find a list of the attributes each key will be created with. -* `CKA_CLASS`: `CKO_PRIVATE_KEY` — Defines the key class _(secret key)_. +* `CKA_CLASS`: `CKO_SECRET_KEY` — Defines the key class _(secret key)_. * `CKA_KEY_TYPE`: `CKO_AES` — Defines the key type _(AES key)_. * `CKA_VALUE_LEN`: `32` — 256-bit key size. * `CKA_LABEL`: Your specified label in the `HSM_KEY_LABEL` environment variable. @@ -95,9 +95,9 @@ Below you'll find a list of the attributes each key will be created with. ### HMAC Key - If you bring your own AES key and don't let Infisical create it for you it must have at least the following attributes: + If you bring your own HMAC key and don't let Infisical create it for you it must have at least the following attributes: - * `CKA_CLASS`: `CKO_PRIVATE_KEY` — Defines the key class _(secret key)_. + * `CKA_CLASS`: `CKO_SECRET_KEY` — Defines the key class _(secret key)_. * `CKA_KEY_TYPE`: `CKO_GENERIC_SECRET` — Defines the key class _(generic secret key)_. * `CKA_VALUE_LEN`: `32` — 256-bit key size * `CKA_SIGN`: `true` — Signing capabilities enabled @@ -109,7 +109,7 @@ Below you'll find a list of the attributes each key will be created with. -* `CKA_CLASS`: `CKO_PRIVATE_KEY` — Defines the key class _(secret key)_. +* `CKA_CLASS`: `CKO_SECRET_KEY` — Defines the key class _(secret key)_. * `CKA_KEY_TYPE`: `CKO_GENERIC_SECRET` — Defines the key class _(generic secret key)_. * `CKA_VALUE_LEN`: `32` — 256-bit key size. * `CKA_LABEL`: Your specified label in the `HSM_KEY_LABEL` environment variable, suffixed with `_HMAC`. If you specify `infisical-key-v1`, then the HMAC key label will become `infisical-key-v1_HMAC`. From b9ebf2217e147871b30122f8d82dc7675281ff30 Mon Sep 17 00:00:00 2001 From: x032205 Date: Mon, 3 Nov 2025 22:48:41 -0500 Subject: [PATCH 034/294] docs: pam session recording docs --- docs/docs.json | 5 +- .../platform/pam/session-recording.mdx | 60 ++++++++++++++++++ .../individual-session-page-search.png | Bin 0 -> 425097 bytes .../individual-session-page.png | Bin 0 -> 472697 bytes .../sessions-page-search.png | Bin 0 -> 511727 bytes .../pam/session-recording/sessions-page.png | Bin 0 -> 583157 bytes 6 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 docs/documentation/platform/pam/session-recording.mdx create mode 100644 docs/images/pam/session-recording/individual-session-page-search.png create mode 100644 docs/images/pam/session-recording/individual-session-page.png create mode 100644 docs/images/pam/session-recording/sessions-page-search.png create mode 100644 docs/images/pam/session-recording/sessions-page.png diff --git a/docs/docs.json b/docs/docs.json index 6d8eb04cc..3fb6914af 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -784,7 +784,10 @@ "groups": [ { "group": "Infisical PAM", - "pages": ["documentation/platform/pam/overview"] + "pages": [ + "documentation/platform/pam/overview", + "documentation/platform/pam/session-recording" + ] } ] } diff --git a/docs/documentation/platform/pam/session-recording.mdx b/docs/documentation/platform/pam/session-recording.mdx new file mode 100644 index 000000000..7e560d5c2 --- /dev/null +++ b/docs/documentation/platform/pam/session-recording.mdx @@ -0,0 +1,60 @@ +--- +title: "Session Recording" +sidebarTitle: "Session Recording" +description: "Learn how Infisical records and stores session activity for auditing and monitoring." +--- + +Infisical's Privileged Access Management (PAM) provides robust session recording capabilities to help you audit and monitor user activity across your infrastructure. + +## How It Works + +When a user initiates a session through the Infisical Gateway, a recording of the session begins. The gateway securely caches all recording data in temporary encrypted files on its local system. + +Once the session concludes, the gateway transmits the complete recording to the Infisical platform for long-term, centralized storage. This asynchronous process ensures that sessions remain operational even if the connection to the Infisical platform is temporarily lost. After the upload is complete, administrators can search and review the session logs in the Infisical UI. + +## What's Captured + +The content captured during a session depends on the type of resource being accessed. + +### Database Sessions + +For database connections, Infisical captures all queries executed and their corresponding responses. + + +Support for additional resource types like SSH and RDP is coming soon. + + +## Viewing Recordings + +To review session recordings: + +1. Navigate to the **PAM Sessions** page in your project. +2. Click on a session from the list to view its details. + +![PAM Sessions](/images/pam/session-recording/sessions-page.png) + +The session details page provides key information, including the complete session logs, connection status, the user who initiated it, and more. + +![PAM Individual Session](/images/pam/session-recording/individual-session-page.png) + +### Searching Logs + +You can use the search bar to quickly find relevant information: + +- **On the main Sessions page:** Search across all session logs to locate specific queries or outputs. +- **On an individual session page:** Search within that specific session's logs to pinpoint activity. + +![PAM Sessions Search](/images/pam/session-recording/sessions-page-search.png) + +![PAM Individual Session Search](/images/pam/session-recording/individual-session-page-search.png) + +## FAQ + + + + Yes. All session recordings are encrypted at rest by default, ensuring your audit data is always secure. + + + Currently, Infisical uses an asynchronous approach where the gateway records the entire session locally before uploading it. This design makes your PAM sessions more resilient, as they don't depend on a constant, active connection to the Infisical platform. We may introduce live streaming capabilities in a future release. + + diff --git a/docs/images/pam/session-recording/individual-session-page-search.png b/docs/images/pam/session-recording/individual-session-page-search.png new file mode 100644 index 0000000000000000000000000000000000000000..ce369f515665c9be091efb09f951bf82a9c5e59f GIT binary patch literal 425097 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�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 literal 0 HcmV?d00001 diff --git a/docs/images/pam/session-recording/individual-session-page.png b/docs/images/pam/session-recording/individual-session-page.png new file mode 100644 index 0000000000000000000000000000000000000000..2926caf675c0943f22d6e5db1fc0a4a5c29016cf GIT binary patch literal 472697 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

BK?f8U!ElzbCyZzJ zHBUJ$lw=8Cx-5dAxwsP>;+A0s364NtTqC!MSs1z zt49`crm^GX>5tYwvFA4IVcEyL@<5okQue1x^k%9rOIY}qLK$X&#Uv=BywLL7b7(KL zVLhpxQiv*=L6FZkgN$ZE=~}KPjTAaSWx^XUdk{td>1ZFm0|LYi{T+S_BgZXE(usdj zcEj-DK@NG@m^%=87P@C&I zIq+TiR^w0s?Lk!)6q7V>sA1_6L%vM_g|M5bC?{iLGEeU6Ye=YYKc1D)vHt zBs#+yvsZOb6b-q8PT!N>>g&kJu=7v7|B zRbXog?p%A~uY>l-%?c7fbW%71E>FROKrMfD*&T1zb|voWU@*k}Xe}@4Ess{}mnxYm zhavgtkP&K3J}V4ipkN9p?Cjj0kHuF|HqX{tCd*cTHPQ8xkEpv6vi*3q5}`Pmtqk+r z!a+!-eU_!hhf7J85lq(62)zv14Zhu5Q(PFQ=$6(ikmf~nc3VLhT(!Wg)Jog%_83Xh$Ex4sWp4-bsD(} z-;)rCsKYFeZ{QAx^Y3ov_84WM^uZXK%>J+4WQBj%*HNOgzW=S1p@b&^6qr{UTGAjG zp|d<7Na|V?dH2v~wOpN3$O82SLI|!Wbo9uHyu8V;GzPQOJuawXC#U9MJ4-S6@}R_g ze?cQ|{egE+s$RCMd()0x_;N~>_U(xAmcX|63B}rm&l}+jo#8n#LP{?fkj+PN8tred z(eeAdJK9WOywf9eOC?gI5QqTTU&UJS#5hTMmguf1OdinSIWp>PK7)Yq^#KwlmZI!4 zVR=zsn{sDjsTv6KT`m*lR`p0${;mw)LzyMP`o}1nj>foRd)Gnh4gV-4sVrW5U}MiH zVXp)ly3#L5QZz!sCC7aIW^8)zU9A_ob<`6r*0#TvXOuq01RGMAWx_>y|K6;@Imoj| z9_x@DXrtZvaKpkc-zyOcmdmG&*p`C6t>f9FBRZWg!S5g>0>*3QizKEZ%lw8#0H$MD z(kLiodoH#Oqsj)tf<+#|gJI2Wi{1J?8KzTjnd<7X5v&?0m!?}`{0ay1^b|W^ zD*h;H7y01-J&fE)nu+d>Qt;s{Z_{Z{Ngnk1By}x%r_lqo<^QGLwDrvWZ7-}&R6nJ- z?>+rAQ+q=2)(r18*#SY{*lo;Qn(1a;UxKbU7Uysg;ROQUPDb%_j-{+jbP=d)=X#R*dzWcu- znSXl>!(bM`YL&E-5Bvp+B)!5c(O%9)_Dm(uI*JV}Z5y`^(Fx2Z#*Kcib$igp5}zph z^gzR-E!SFJrG3lh9~(bIaSbbYO!?gwLyUy=#LHNP;3N~~!qRYyz=LYdx%^U@ngpTk zsSQ{-yWg}-)6u*}o=)-4bDElGEK!JxoXzJ?y^b=kA77dNA$uTK5V_%Am)#at5~uCD zGv9&vfCCFppKc5*jnor}Xd>;~cpVvqkmIGK`t}5Dmp$AxHq&anRJdmbzYwc*O#wf_ zubC=dKV5ID#Uup|nIOG-qeAs+sd0w%{S9WLDmH*+T@mfE@_av?Kugue zMe6|XpuxwJVVL)+|8?#OqH{V?s23s;f7F+mdFkWfS|udlWX(R_No|ks$xSjHo6Zl{ zq^1MBZ?;W6kUu*P{c$)@x$+{d^7eqj*njHPW1O${opNuq?*P z_~Al@9}4@aKEn7$(KkWEk3lOL{$iNCBY{pQJ$l!X0v(+;{on|F9l5(diycKyjXyIq ztA1pmA5dIrDmTI@TD=?=hJldYXFvpCF5omjTIiEW@}I6#h^?H!tq>9^rl_Mei}XrAoO!VN&vg_vlZuKBKg-$tw!bS!f>n-T7ZH z7OoeLxRfN)6S$WkTk2LhegB_gh?suU`Nlp46B&O!&d>H^G%2 z)YEMRJSat3?WF8ziILKA31i~DgmAliKVsjj&27FX$us9`rsyTK-$K=DZH!~)-q6x0 z1Y;S1dgQH|DW~3tO4oj9rO68J2j@TnmQz+Jwkio&h4wTefq|vBrG7WbC{l@ZEIe6j z!GC)Yu-9#8=(n)X6WvE@Tph-WZS|hp`Z|nPIllYxjY%(6B=jkhtV=+^Ns7HPRmDSeJLHf`CF9O)tBCXvg6CU0%K zS4yqgBdnJDo?D%KV740k`WTa9pH-q~T;4Ds1*OZ1B|Ah5`)mz!Z#MM~0NHAP+j5%w z=eN4Nk#L)?aKPjg*+M)Cx9yc|c2XW&yM-(^f@x3W`{pdRJk2@D*3MX0e0{L{;JWum zjl@!ly=5p>rJazzBVX3UIM46hY2UA0y{hf{k5u!&j>GrPd)M3VWa(t3o=&tJ!M2ik zFj10QHK=6m+D^n1kK)d5_D{cy-~axFPq}pcbEGMGAOD?3x|Dx^!f0V^?Z78=bV@1g zw%y?OpH6N6o-c4)y7~cHVdd|6@i)ULAwG|bXj)sA#`S6^fA6vX>mkCXfL{kn!$FDoC++mQu2!QTK*hS|L2n|~V@F8rSd0~c;xJa8slZczN|%et(W zBvCGv+dFf#?W2w(?Gqw4Dll6N)-~Rs` zQ4D-D>pk6FSecZ=1GyW2f93e5^f8Jz9N0afJ)pO8Xpcf4KdgnJGlty=D^K^kZ~wot ze_hkd>*M2;-sXQT!dJ;_>7FN!^eyQ?uhd9R3lFRCm9#(q^}WBj1z}V7ft6>)6+53} zl`s?Da{O#FzmFEb5tIrov(;#Ic6v;o>VEV0p8vl#$EoBIaVp3P8vWN2jxvNP5;o~r z#2s|8OPVR7+-%^*XMcsk4Cjmg@vm+KvWRLTP2*o%IitGqF4^n+wXSDU zmrSain1l9LIRLVre!TQK9S<*kJ|HvaWS}5@ApiyAiQ7P3rC_4Twl|THRqQu+;a^Me zXzTj*^kR1W5a|E&jP$#@QUx%L|L_$2>+b^xfD1Sd{BAkX|8`man?rls=#dAXkxJHo z`s)01-T&tU{Lc#f&kFp{3jEIs{Qt26U%^;bV2I$~S9aJ(m9OVGxGSdd?a6@@Wfmft z!48pG?U|lj`JdhPK&;b$D6BfXw1l_0WnU_gsbm^2AnTS4``g1AEpGq;7_>SOt8{|c{_)D6 z;-f2{JJ9Q!wPpq!*|!$MEOAG`qN#AWpv4}ih{=uDL2Tmp<6|SjIGc>tBsM=Og-r*` zpVPnYv(tLChP7@pn)l01V`th7O{=dibjDESJf)(fWatz21)wc8g1JZKhGv6GSz1->!qsl!nueGql zJq<}B2EHfED=(prQ&mNn0hD{`(1!ESH`}9QkW@r}9H0IZ)VmuhQ8cciIKpPIuJr1s zcCi&yEe_JIflxAE^N!6filAn(J(SzY#a-0~nLKQjGg#z6mg)1RdyXGx+anZ|foU)! zbb#nfL5!H!n;6kL?QeAC(v$0xrhR--k#&hR<)2x_e~(y5jTc|*lN#}}33Rs~eI2K0 z8p0)rOOF8z_F%=>xYfX>FrQCzFs>OQ>;7|_D`n7tEm>hBda!<^K?|rWUE9$q0f*Vd z$Z>5p*uZ#$y-Ec1${cGQi_ zd9csrHvx%m+j!ad<$^>3bCb3drm5u1)(J(F;E?ANMGGgw!#A~SJaalRaak73XS)T# zZAb7Njby9Bdl#}k#u#4@;r%Mg0QpYtanCR9b1`y=JJ# zTJ4`74dY1nOgU#I!Z4h;f|MC4-gp015USpCV@0&1}yGy1tu zzHxa#hSUYDNqZ#qHGm?Z0Zub5yZ;n7o7d{+lqhT8jZsR*)74>*4H&G*W9|ULK54%I zAzt$!{`b%L_rcdq1cp34m#sNpn(sXP+n87W=Tv0849TJy57qt0(yKQ#Ixu`McXYeG zK2IJliJRUnGh`mU7M!7F^unek$yD4mu8oFeA3TM;2BhcA7x;riHfWZ;n!3Khia>GNOt>{O!Qdg^Dza=FYv3T@I5@ z8|4l|ZQC8CDOPL4+~LcMEk&EneKWNVtzlp)nT#0D@iTs{E68T?)AjD+RGL$ zQ%_cAT1zb$w*qTISYj2q54^LIrdJ+Dg!y4dxqlMGJ=>1@Dvf+dN1B~Z>Xh-t>I+>T zAM6~v18hy9C1fVlvc|=+KC60D>_%+0K;VFeSV>PMbL>#QD`D{%CBN64LOkQ^|F-JO z7>dqwe(c~hloU-7o?qd;Aac@r=p}vjHeO5xg)7(h8@bZjxi{lRps}j_c~sdQ&qvH) zqOMg_G=;nVIs++ypJZWrz>>f^umItNQBp%uzcTL8{OMw)#gz9gSaTJDqcwPbMM|-q zE)MZ1Fsr*R@#$}8TTUjxl;Zpj_So}->h#C$D-QXV!@pfy6K%Gx;s|)BG=}*IWcC_0!QRS%Lo>7Z7b_hPy!gDCJd!prKT;eaqdZALQ zOWGqjTR1b15qGU@wdd|$QH8dy#rUiXL+c6$4>uz=+*|Haf23mC_6wLFg4jys!Qjfh zR*?{eJ+O(_mjA4uN<-P5jHA0pblS2Kn`4bSHy2G9hocB(S{KdVT!CEYfUM$nT|Y0uf$ftb$_1tvvyrTjrzsnZkxZ17AQsdBL}r{%`HrORDa8cN#J zgIQ&Q&vcQ#^xC4Ba@xU_ME=+0~_-h5z(5+^1XLJ@FbPc z^nq1LRzBU}al>|87Nab_4p1H+QX_!2ZFYEzU@V=s%vNxCW^A3+aIQ{!DE%t-t6i`P(v?4MLomy>6ULWLZ3MhP+b(>!G`PeTdeDzuW z8~V+kD~mvJ#9~!W0n0+>PdsV43AoFA7MPCY7q@fbiLo>vk&(8Qksq^?NG(U2?|w{I zn1Ethxey*t6j^sdXcaaKJyvh1h6_px7rHQZEv(q+?xG7;vWPnGF*DPMN2fyI)6bB% zVgs_mn-$(kb-`21!s(nAnk{mQEc=gT^HqNPb!u@6Yp^`HCaw5aJo=UNz0sZVw63`e z+Q^&zNj8teiS%atvgrKV=V~39m-ki@WNWLayRtmgrB|Awqr&s&PnrH$uS8tHbedv& zF1b}W|9pjv%d@mYM+9U(F{Sdg<=^`0n&DksyEw2lYZJS_^(mG-j~L}+JvX0(oMtl@o*D_bv)f}%ZkX5_2b(CCp7L|(VXyWWEaH?Nr}@Z zy$A0E2{oUtYaW#M2(iBr-AQ?uomFS(7MwY~Z=ghoj+!^fMnX;3G;QYBA)$6q_xjV^rypbs`I;zuG+E`$O-owE#a}-1 z5#?U|Xwbo={N_8Gwm_x2I%7abQNG5Y)y|z{ky4bIkokyPR=9av^Jp?}#s18?yu4~0 zjM^I3+FSl&z9W#4Jn|ldAJ9_7qvbK9*4ahUg5X$=(0!@4yd?94N~6b8hHedu1+7$F z9RAjDKmayY{`jVl!j4?G|B_$|pT^~2=mzNE=r}0C#z7{Y|~RxY4SPzt5i_K#&8#AX8^JhU91qWQV zAHcTkc8HXWm@ZCi7NK23d8Xkq>DoY9)TlRpc5T|l9oMH=rW2>Wbu9V_GJV{&r6C!k9@4J>UvDEF`8_`p+{iM)` zE03>=+->Ut@sF6}SI6Ww>y5JZiIi%2@wKz7X)UoC#!Eag9j!nvlm=W8UzwD9YXYDu zks-G%z{m9u1GZo7hb$c8coVXtOl49&0cBN- z>|nETcm>j8|3vA`;YAS9eaYYv3is-bSb|N3bLR%>VLcjX!JxMcw#_;Y;6N)8Uf=5* z6OyZ*aHe|w^P!H5$ahA5P)C#~9dC@VWbNvs!zS-8jYj)=Z|g$-Ow-a?y4ZsOQ=&@g ze4^`4Wsv91;UZ1v*>AQyn{hL|nW`ZS79p6mhk?6q_k(X6UY9+Vv$rb%ok&Eq%pVJy zG(Fs#j9G^FXxC(%&Fr;tMM5d11pie2jN?U|sd9$uuBLpzG?3BC3N&g2x{shLJbnDUrSt%cQKz%YZV< z!BY+09+nPv9=OMlmLG7PZ7bHS2H+HG!@J{oO3@8i?km<6k29EFFpF6KXF_WM$@J4{v*M&a`8wU+ zG55G;YbY2eo_@GF5*u#(S)jJVCOu%-e>j_os-8IR+`c|mG6FYN*j(Rm?>G4T{*66p zt_w4srVcHSpIQ4HNwQcC?1@P_NL9c^O;>)~`1x=E=hi49g9EMQ8*8lrP;8*deJb}k z&vs(4khlYq9p7W(QCyb&GElm_OScY)OP@99*vgLNgBh^(8;PT* zOS!4TK)0%RgE*8BAV9iPIcl%Ax`Ov7@svKV6slt;w~=VTDvXC}`%mS0-%JjY5cB`> z`q-*-MNAoWPnRC`I5I>~{`ja>lsFAJT&?yup%;i)^2%B(R!p-G!8PscdTv>ww|mP2 zJi~OTew?m1pmVc-_;^Y99j&L?O=!m2^ zTvbji(N22zmoEl(H&#}tiEzlecF4rBFSTBeR^ABGZU# zu@7)x8NiI}*bIjjd!2?TwC^-{wZ9}&4B(aD`K2`C9g|_qC2YtVEeQ6{2-tPRD8({B zu87gaLFUE0y4>BbddK0iT%yA&v+jY)=KbAHQQXI~AH&2x{&wORGIsIul2FUKm}kge zG2jzn4!Ms|3R|Ok%5M>`hD|__LXW-}itOy|EE5Pv&uZ^sH7azRNFF;(1^=Cv0 zUU@f9yHi<)pS`TKo%PU7^BsM$bu|4elbR`6IYAk8P_`?oCIz3xi-~eU30~7)s>y6* zH0Waysq*5=CZiE31k;fTK2g!%kwz%&1(*c#;Opv%02sdtlRq6qpTnBz*0Z(DsuS(f z>5vjx#|sVqa*MeSj!(tR9)~3`0{KZJa1=d0GiBtUJCX-ciOWeT?an5kmVxP5g9q32@xO} zLdno~98Q;bgx7EWB%_fRM`;y1($g%0mL@-hB^f`yk2*X19tzMNp7KFNVwjR`fdRp} z@emd6o)z@80c)z3TrP@1Y{s}w)gt`b>vDrkh7Nb55fMoiL6WCvOE9Nf1JnalsSi8(qCu7xC=VO6|$j!e1#bm+krST2czE!-O1plD3$$`Qf?V_NK3LL(t z=t?qsZzyq(6DM#hH40^IyxpGu(7h{z@9enW-1PUWVg|JUx8fUZ@>ixP$rlE!SuD|fV28Fw^|G!e?A_f1!l3$D$Jjh@zE_E5HPLeNk9OgO3)P_H=uu%F+Um zqc^6P|G2AGo?CUeNJpDc^WbN(Ieb{p1rQM^m_h%Pp`}5Dm}M7puJhluHO(T@^!P;`!)VT9@Ns0gaafT(u;;dv}~~){kikyMg4) zmXBir>J-Gq*%BaxW)8$=0HBK2c^q28!q6Vi?$3969Fx#FZ!ds4X|<@ksRzpJZ}!S8 zGXCM(w>lq|6EHoL6OqK4*_UZ)Yhe>!W&9rMmJyUPW8Kljm<6d2)yVa3J@rzXQd+9Q zMb`NZF|_OKn0S>`b;!sq$-5&Ihyj4z91m;O)4k@Lb^vOtU@iIU?vnGL<_-?RU?8)W z0)t62OeMOuSwEPZW{SSdS*Pn)#l7(RD3U{wnZfRriQM#q{eDa@xqkl$v}5Gy)AhHm zL*n+sfWhzOXfU~5OcnZ!Jd*ZGQ#?ntcaodqoc&U9g4NH#*b!Iytty8}(Tpj@h&8Bul^ru} z*`>by&>?L=;+C(n<#nh*&Oh`%ERKfVj{?Ol1ZaII1?i&CB|k?mlZujTGT8w<3JfT* z<$E&*Ys5knGXlqH+a45TFe?H8lu5!mfJx5o4`aA>M}&YmCN#Cy;qw4-e+2H>nmdr2 z+w+_%pkq8Jbgbgrp7keTsX=|8V8vOTzM=_zA7bDh1)sKri-EM6fsY%OfJX=OZ??3o zo1D6w?(bTLPj=hetn!kz|H@6(rV0WgD35;<^*SO=4)&8c9#Oq^1P}}(;34h(0V2&W zw^*)Bj7nt0Oc3@nb5z0yp2cZ2@lZ}$P9j8v5N$mM20&%abkK&+79v04>^L^wt}mGZ z;G5zHU`_=9{s%OkmhoWnuAX50Y@h~QSN}<@*vswj9)nzw_W8B$t*&+0PE>oYS{6@@ zO5DneKX0D$6;35J+QsKN44&{)DgvaR{Qh8$qka=XUGT?P2OA z9~Mc0Bd$=SkU~$2d3k*(VQYA9bWrC?B<>!)o;5)Hn->BEEYB)jqrfc%&h#!y>4a0( z$Ad?jp4{epF+{JUc;)2#pKPpBc0M6R9?pPjutLyqHtiR4hCslT!4KErRRICPC{0dZCu8~eqr0~iC871oHKR;<_ zDU#cr&9lIJMoM7voDsBy+&dbELE6|#N!#7GqIl9~CDT_^#yZFGQjhO<8*SX?c;ymt z$R+;q&_i`u?dK@<)GT&ZB|o2lyG8rHzqnNGB9%+-83#;YED@(2d@?!lPSQYbY>+Gx zY9ecIv%ZhRd$FXiPvxkduwO^Ob#5N@)%ec_DWJ40OLkqB602qKG@v$1ClpSsL8k#6#{m(hRSpTwKl`c~%PJDw%+M(! zu1+lJPP0uf(9?nZy;mclWf30MSor?6-#r?Se6Qa6(p(rbR{D3bNK!d$(y~vzc>R_^ z#ac50y!PA=+aQ#Di+pw!?<8xHqMe~zAUZNqPt9`6VDVjcE<(xDh??8*>Lh$YDCHFr zNZ|YW5QdB_idDBijg)n{rMS+-Qy5H2N*9?`XA9Pfs@Wn7Eo&`o{k!{Mmatut{^Hkr zY~BNbj0sGsQ9sfgyJJZUmNg)IX@>y4gN;O17!4VDGpBWO84y7-0rgkgb_Oof+(*B< z2@yIDNS8s&sr6j%se_e!&A>URqf;@Rw@RmHH4p7Je_dCRt}g?^LAEKtLL#7bJewmc zucozFwUi8_OsfK{mP4#Tl{0ZKUb?_5%t~_^Oq-w=Z~&g`rGklB{1j9uBaAh_wn>171U9BwaHZwybe^e+!rEVjoh1X)2U~L@kLVzu zI8n{`Q%TL#BS%O5UN+KVd>Cf+|6=c}!=lcu=x>P`hmd;U9B&EAyq`T|AgX`}8Hq_nc{o{S!Xa6&1zH`6#o_o&cd``UzZf$zf z^}4Vhkto^L7JSCAE&_lDGzo%5X;$Rs*VOAEnBf|js_lNDrq~@SPZGAxAuCpov%M7=cjFoWf#sQ>(=WL z4*97gxZSy{?+LLr^}*H%hlhq`hk)JEFz0-~ZLqX*%)B+4sv~1JyGzD1`D$LipnO`y zbO3NDPpSFKJMD`G3dKv!$6lT`M)Ek)%aYL?x{M4(p`85isl3ajbDiBHM8-x#qN9i@k2r>v8>{D0lr;@&2 z;(e{yH-VylC6F*y(%=DkYra;e5w>$cjVdtiU=b8$No&EU`J!nSenIaM#A|b6s=dW6cR~S2#y9I*ouLGN2&IpZ zxnfA@v%be;;#D5h2hi=Va8WFD%ph(U-Z%|gV~&0>%&J~|DN}(YA$ahod!^Nwc8o6j zAuqCm44tgc<2Pkis&sF;3i-XrR8LL?1I0?QAJ06*|M46B^XXHmJfA2~`DlHeXoJ!C zf&~Qeh?O4oy9EzRa7QoBT(PWNQSZ+-LrnyW+;!}8Jo36(;X+4hmjP-G_>QWis@dz~ z2>S~mXEz@>26gF@Fl&=m^&8A(R&NeCHktVor|2f9neAOeIo+YLz3IRQU;p$jU+0tT zzO{P>VYqo}yt_l8#{5Q&Wt@r`lOUV>U}TO-GoopLY_k7q+M;)B zoa&W~fg-iNbYuGSS-Gh>m%O6>tTvVB0}u)zq4%aAdu#fR~}+xB}wpF-4} zjKmg`PW{I$iu7k_&S@+okM#q*a<)4ybSSJch(=HVe&~FI@ImAB>8`C!#W zU!P-bypT+&x$tU`0M7vPW`C*ufYtk2QJgbWHV=}PJQ0+lI;~daS*3y4cMPj9ESpX*{=r3uEhVHmPa#v|06(VAs}lZX42fY9S!DkE@PB*X$EcgTPMN1 zZb#`GxUoZ+B1J95Uo_2gjt&*I(@fMq`$F(vP_%o$6uWXDog7{r%tsG`1zi&F5Za%v z!<-bT=n7RgUf&9^7oDYHpAWscCKAJOJwvmAgR`3jL?Jx#| zT=4Et9|W$u722-njz}YN@|!D}H_1<`R{O$ZgA%N#OOg~7xYLbH3pRMh!f4^0dOh)= z;3l2||EQncCk0;OWWDPi!E(AQ`K{NhI|Pjm;u-PnIb)@+k@)m+L~;Vmlckdu>5@kp zBO+JVB@kh?{`#oH(fZmK{#$vb!H*9e|uJs+;H8@4ELs3vNHG z>vjgr^5eA{OHyy*Gv!+fVZCjR z0h{VPSK9aeuRxPwM|GRg9{?OTkFy3 z4){_{Z_SNXwWyID>1DM!eauA%BAT`n3ldh&J+s=wErtuab>9;(=BlnB&^zD zmD$n^DscYEwpWUwuR=#VJhFii3c;xK}gzgDNbP z&T{DTPE!FSuB2J6-;^{&4wVHhVSS>C?;Qk(&8P+7Bce zLc!=zJqXmxsz!Pry?j6HwkZXvui-y(8m-(8DN-30ZXEFJVJ)|t30ZEa_v(~nwe9Ws6s)$@m{6fU%_mKm?Z@S19#$-{$qx_Ov(;{D%$AWL~NqTILHP2rX|q?@hm z#zW)wc}aq}m`1-l(6b%1@)neqWz+%v2QQ~-cC*hvJ7xOTy%t~F(^GY9sdxgZY&6Xh zSL&NU*EJw%Y8tu}zjtIK_jn|m*mz~n#GS)2eWPoXMH+E4P>OLZkcyjw%W{~*Zn68S z*%0EA5U<1fo12a9u}brT%cIqyrVEi5MLDpF7yHuX$c|<{vn}K`z~(Xsyxlg16Wa{u zp8HYX?Rj$?4I$WhAa6rMAO~r4W5&XBoGUdY3-LY!%~U@p;4mM1O3hI!`>H!q?EUeMML*HuYz;PfwI4nf zntgV;flfrDQ4)VQRS=iy>Z4)$2jNE4%0z!F-Edr)15OO@@DbZfWcb2`~4!2H;yN zE$OMQgwoEpyJov}tVKtkawMS(ua-0g66i;;$5#$$Oe^{Gl>W@VuiqwA4C>)iuDd6# zeH>CzK}u~=*YX&OCe-pw;sSmNQT0Bpcz`JQ@M;>SoH|RXy{0M0!#TivT)`3e$jTK`djaic7$;oaa z;~uVyWm`cx@SV3;pc)%Rx$01h?X1@(p30*B#6OIa#jHI`YyF-cyTqRTkY$4-D2~=3 zhy&1zp4ZVf%6QESYHFB_i`#muk6hRQSMPP%7cOa%Th%UpL|o*erqOow>v4JI{#e?(R*f=Hzvox|!i%LdQf#)UcQB&FHxG^6-=fYn#=KJ7BV;Q1=EE$`U}?R)qm948r+Ck z1}ts+1T-bj#oklx(^`ubD#=5HkXnur&aAF!6G`toc(}Yb5Mp9tiCTQInGTUAAgvKA_Q+x=Mi3AmWVW3PfmuC0`4R3lNM=U&W0uq{*J!)X2umHBrO8Lu|fM265jS zv6)aF%ex-xZ-Gy|gIg14gUV!F9cglMTS4OF)=pGxehi4J^!RbnsXRT)HGt6H2!nmt zicEVPed9t+g(8|P3sN7^N|O&895ImR>bbC z59vp+B)QvUv>HkibcBp{`^K93$T1q~3T-+JIDSy#j}~z=iR9N)f86!Rq_jYiF%Ec; zB3#4e)~b<8CDG5lQ*`f+r5h2K7;;-{Gi#Vr6%J&HpKnv805zp#)6H>;M1GltdNruo z_SZM%&DdXWH8y%^rlCp^tp*?yTpYy3>4_iBdRyz=Np9_kuh{eMXys$D)qZuGfMAP( zIGU#$otE9SB{$b?&N)&+Btk{1`|!<6W=w7kh7r*Lvksme_cR)V=6xaQgiSRCy6j7| z&~9~*!B-hS=A^w4vNUDh4aInx5I7^z#zdo>BDo4igBYuZE0Jr0FtJ2T;E$*DnvjCB zEC&S~bh`T&+}vyBxyD+~We2c9o><09V#rf-TysTiS#w2YxK%VH4{iCTLLRXd`)5~^<> z7#mf{%-MCp#6_|44BX=@doF){rN4dV?-uYT7KEjy%A0;GY?aT%06E*F(LBa4b{bjs zr$TB$(Dewi*A4j}|KdAL;r~{^9&fQygjlG7OjUn@DLMtc`Xwu=k6e8qR9GgMEJ(o zvy|ISto}#47yM~c#43SzzRGO&YnjHMKJmYf+MhPkL=)tc{^Oe9-yZ4-$2luIrP2H2 zIU|4AgU(_g6M>zn;(z31RHVqMVH%rjvhL{P9n|u|DxWjhw+z>>r%||2$%q z`4V5aC2a0E0p?%4h5Q6?p1EZgAO2f0n$!SAlOt8#4FBXV{!XeB>wplM#15YLw_ErD z9d54l$^260`t@Lb{B^_vfe>}^m7M%dL;h}ge-M~Ii7@!RXO`d$;W+;wLjJ!`@gzs9 zXlt7@|Fn|*s~}t&0cW^FQurm8^q(#M_|&*rK;`VjB>q3G!2T+4|0x2D{}jQ0ir_y* z@Sh_1PZ6B`PZ9j52>w$9|0#n1dy3#Fp7~^7T{lCz8+H%*HZ$_%QM2HF5#glp7c`2Lo!AwtTJ{i^BX{(}S)F z&K^B^oPvUaYxB+?QPqnceqV9)4`BV@J2{@aB!qm*BUSzX+Vy#V`jjj;UkTB_^*rQ9 zFfdYQHB|S0+}ruzcjYR6_%02T!SUw5bw6h{#733ea7=$ZJNOU24<;!gU)SiQ?7wzB z+;GmuA}nDG|JDI+8wGnv9y^HjZ~f0VmM1(FH<&a2H48$n3HHz{RPgdwcj|x7=zllt zuRgQEe8~z?=~M8pJzy`ehbC4w4FA>vO$0=cY*$gv=D*!TBP+0nc(RM$|5lJYi+~{S zl63!T7Z{HP?BV-{xPR83?zfZre?l2#&h4H0MH@|BYXf0}wp{W0wo13XwiNT2Hqa%# z?aGemldp?TT*nBgdYN^1O8c%wgqe6G9d`YxUm$?Tnm|{7i?q~Gh!cVEwi*CM{>Eq0|>=9{-=I2zK zNn8<1)G%hl{`yoIu|&7?ZNo`=^=z&!^UG7DI^I6|lr45Sl=?Jx~!f021l`b%wTwx~Ax zsB93&zOLni81#L>qGkd8`P{pENDmF2iEbA3Vz|sbU&y#R-M)i<*JiCtBA@$R zcZO%M6(6NU?-jk@{<7JgHWwb-<`@BO|lo=SL5he zcbO1nVU16UvXSD9l46l@L;V&^_zR&(q&)R;>d>5D_4CQ&pRfQw5xBQIbDYttbhWKO zqle9Oy2)gd@z=*(gL?A$1=yA(M>Ww!q*U5|)2hj{U`$yZ6>2iAAB!4!Hqe1}t_$n2 zXu(6AGXir`j0RH7BZXa6xVbM7nNVJR`?VLqSCTL+T`ab`wnQ_wgE{a4O9@wJTW!d=Y=LGZtCaz!s8G!dl2cxCxK05}LpdBvgWT+<1T=w-+pZchLsby+B z*20+9wjnubG4?M{{I~VIYcKcE$fDo9g_fT{^vO?@YP%eF1uP&m8mV*xxm*^^CWv2U zRXzyAnuQc)Cv_DZ37$Rbn??+X$&2ZO0UPd5tmsoU(nK5oo{e@5^xxk2h{Y6|SMBet znch<*{`J(+SEYn6RfD`i)5iFX;IX*;&HkLgm}1P~S{iwg{=^*}q7$1q58zF4R>bge zw<%_H3r1oRCQa3VXvD9N2zwH6=ay$4hwr@bpeeaDI22%^a$oO>I-7Z_Z(HB;2fuCKuD_Oi$ z>00&h#2oMHxfyq@Q#^eSNo$<1yIlr=5O?+#dhXR$$r#9|9dexMH2B z`FHj~Z}{`FLMx;@`(OgYxfZ^O4EQi6`i643d1^Oq;M2J6-}55q)O`ECAwXbkeER$? zZuQ$|hTQiuVP1HcldkJM$YCNDahpdz?)=3+h<_=+gM|hV6>!V<&J^Z#r1^&UR=uJ8;5W( zGNf?~jFV|ZNmZPy*COL09vFELD#MXF(_f%bXBuTtc~b-P&ha#;XrG5A4>lr7cx<1{ z23t_N_7|FmM&Z9!FLWv$x7QLZ39M_AeI*SO+zj6KcNBRDE^xipmJ{|k z++Q9!0##KLo@aawS2>Pt`XCGsRr!0Lc?a&E%WKFXNz zUL{)}-{H1iwW6FbmB``tMAbNbuCUwgQ>$ifr15z@_Or)lAkTQhwxM4tTB>tcQs(=o zB4!JS->%r~%3QR2Cf0BoD4+>KC=R1E=#r!pDdOe=yXU?aYukcz5;G$~Ey*OWnI}zo zR7tbTWT9X$IbEeC#%R!;k-|&_8i_ac9-4Cbm{_n0rY2# z@hh=4^kdi4#sfM+S&tP>h=2hXvDuvZAH&q+9FxOUun+dOaE8hp325NUu(u&SWlxr# z!Uem`?G^{G=bQG3Aj_8F;J>9Fz39eiWE~MKU#<#(jn_Qcaly-FH)t{)OZ39W@}qJv z+OB1Fe{qCgyVNejt3~6`Zycd+{|cI|PZVPxV!e)SS)sgE@U%x*B$i6CK4>HhPO=7A z8k`lEIxaB(*93_)_L=mk+#Bku-kr-%(A+B-1G7T!W;LFfaj@`EPVniuiutK_F86hs z(#RCug5Sg!^_+`=#~on?9;9m9%w45!nn?_*$+go~wE{|%a+J0sl_N6l5gBpY`M9m3qKur`5W5&oLx;@i1^WZ7d%1F} zKOnp}0TA>dFFbyScjvJ)a&M1F;{bPC!>p|VOmZd<5?Wo}x1Ok%`(Ozj{0z3AZ{lf3 z6oZk2tY^`@(aMRzDlVW}z{tvCt3hi~JRSJTt+6b-Jy5NRE$&DCoHZB%iL?1= zyaOM*jeAyLqf@cUVZdBtMksgqwM1xDGcN&-gK9!H|i`lg-vnzQQBBN z_F%si(9E-^Z2>*x)D8f89)gvlPOtM~;4$i83ppYU4Re_Yns+#7YAQ8PAYX4%Q`%Fhcdo}9}Go^j|#3kV867tgSzbta(VMU zj_#H=(MhsLz;|aDK)$bedsZuY;vadw@%h0@J-)T34slT5Z!0b-iagw%TLvb~FMzd@ zR*SPYg22hAggy2r-83+i!dty3waGAp$)Nub1zI^E!#@C zLoXES#r)n)_-hH2zn7?Ai{bo5KP5VO9vxW1r$<>p!Rigxs4DEJb60PB15*q+a%`ZB zP=2!_!&BLClv{t>+q~$&JvlK=^=g*v1sZL)&q-(6AHHCukiZ5~S37y4F12d(z35#i z98r8zrK$ct0w_!VLHnMBQ03@;<-X2V_sx!FKhb>=I7)D&L5L|To$_RwZelz5SiFc* zG$ea5&s0}gx!-TO*I^#5+Q5+f{ktsKT;h^VuvC79uvRf zW}bo9wt0sKJy~GE1paFe^&_-+gnQHsDTvM&o_iX7~tMDZG#I$kNXDG6QczW8=x zl+9)`Z)Wy|GLMqH>X7Ek%IT23C8SRA{;Cw@v`j5atr)^>Ye*0)wlpWvPs%Yb4Br}m zb22p8kIIo!VQR2UFEwavHa#2r4iz^S=jYk!ndC#Q=lp57fAGNm%-R6<6_7N0*+2b+ zmW^x4VsI&sS~S$V*sYf8Q9CX$#XD%_Wi=7l1Kqg~x{HigBUWxSd%4@)uhlXp>|7or zP@-5?Sb1pQt;XOSy01nY&=HOuB(2imk5bq4C%CI^({M}TnM!J{a%R29^w!R7#io2z?Dl`aREwJCj48`~?4gjxN<;bzgN284X7pBI4$7Z!=j zrKU*ss~zpEasup1@U=s<_YjMeNf`#^CohCDoRrQFa$GZCS|}Q6m>`x?AgUM#A_Tz|KsCZjh5uKPozQyflW@E(@)_Qg2 zq#fn!hFwg}T&O@t>IB-+`RV1E)C2?k0jG%@ zGyeLy=vm@t&rmzb^X)Gu!N45pC>J@=18H^YEinJ9C|#?z>y<8r&kRtixT#=jw9P`1 zlnTiJ9oBN$bv?4|5ujPso$g8$N^NMdNEmW`p@Su9l1G@1>- zE4ZNb#Qs{I?Ym-yE^sa5(}+-Psc{as-;Sf`X9lmfG+=kEHnP$<^uqS4Ue;Rlpfovl zYmj`Fa9~c`t#w>xrp5piKy+q<(zrYYHF1)#{;`(93J=zIbKK zqGy`a6rgol8dWO@~5SOcibO%M@ zEx2G#^zH5O09G&@SwvvaVSom-S$2<1SC~S9Nz+Q#t!8S7v9uquW%g1T#;U%x-WtF(pzG+zur5P-5h2aU@mWcqG%(#1me&+G>?<3-2=t>WF;bP|gBZh0gGd(ZUyn8e5(!`}5# z$<7WL`>~aJlP89Iv(L8GjWKgP)bv`Vvs<5j1Rk2qLEC!TpJw|W6vSI%sM*+Ki*5mA zS!MP%x+8((hGI$FN>T@roDhG6jOgBm1|&4QxL?VhAhE71c6U|Vi# zUQc2J1z-lb1SWOLZ?U~nZ0hTO$|YM@jzo_NtTL;xtiP0B6`uXl8X&zjl<@v?HSi)Q zkVjO|WSK$I>>Fd)=dCvoRfW27{VCgoX5}vg=h^u1f*`mbA!d5p%jbOh*7xGCU%QcV zVPxvOojB1kf&=04+9$)qKtx8NAY#|fc0QYM8?F{OLhC6GhWf$Y# zVO*tK&xiToC@`IpK0Sk)5R+i|x>}k|#{=zh#}5EK94rhtKBn1G>iQUMjk;(U8=m#J z;L~jizaswRv-hcOSa1rx}DY;r! za6xruig0D?4vP`O$|^P1M*Z>HLbQVTLDrizg3a%f!ZJGOP%N_U_Hn>p>c%Jle6rLe zL+qN)waQ~1a-x~JX8I~WA9%J<=;jHn~fTW1HsWe`wRYoABSSz=6>F=h1QsgMa z67tBMegt{cKZZ!pdb5ssbX_sUyEs}=`FhqfWwK79x&4*kB031^yn5P^HC;v&r=dz$ zlWwDmmVLN%93;D(F_g8beD!1G^7}J=;bp+f>8WB2FC1~1YnIe6dE{{5#!@NhG)Yh6P0WmWO#>YwHB`TWEW$9jgZ%ex4O(e_50z7bj#lyor8d}zKkK_8vb&98>m8{Ke#BIy_h8L#6#|dm1 z_QZAl^P-t!g))`ABb$C|6U zZHR;qcT(>WdPSn4@%BnEL8~K3Nsyh{Gdz0D}o3n#lkSB04DE-1Cl@B@1P z%2VnCg-Tg19&%lli_lmEOKtf5iCEDH^`b)u_jriu<5mU1nh|23>zLbJVcH%Cfzb}B zd$W6vwYq|d-ycKRHhG$n)%4S(pV^l>uf(y_4Wi;c^-#rG@*VkkEj6=p3nB6{(Xbj`*a4>Qv3ZIin&PZTlos`h&c|2FOh(E z?dPvKu#^A<0G+jwvGv_va)$Y@Rae=qH5V!I`vuyM=Xzn{Nn)f2$zgm>kn=j&+n9qo zIh9hO>>1+hBn`qlHIutDHT;yJTHH0^;$XP-tLi?(gs1_nxmMpuDbRASqWeHi zomgt2t-_VZ=_t-|IMdpQ7)qLsuyH%edNUjQ@z+4y-~GXJ8Xci?*0Owc+{BE7P_@_! z<~06siJ5 z#atj4Ogda8lYEKG5NL*$r^(}9C7` z7Fm@mf#FlwxFz_-@Fks^<%wu&Nt66k&*xV$9X;ZmjXW%^o7VlQQ|HLVFx+$7nl25H zHgtGZ2;+ECXz?H*rK&>ZL`c*NGY$r`N?Pe`NmDw(+#jYU?l^bQhl@WnJ2H*T32N=p z)U&nZk7!AI`d!8ZpyXtaM=8!b7nzUIiRu+ksP;u7i;9gxt0s2QhN3)|_l$}(?%S|R zp(5+qv{V$2=?1?+Q+oJp?OL6mvYu5ZK%B(rsjGz0%d_otWgo?atbi|tb}Ptl2UEux zGh~ri)m=19_RuiPz12Saj1XF+4)Y*fSNCR(5(^gR*eaFzfSK3XJsHP|)?33fbk7iBcv0QKEUIk`k zl4ow+dB`5w9Lq~hNfYh9P~brex%m`pseDbAOmK675J`a!jy>+Fs8j?V^TyDfFG;b0 z@lYMqTt&xHVg(K<+@_-p4L|xf47%^pi0{t<;0^?1s9kIaINr=PVyRUZyecOA@1cD$ zj^M_uHZI_st>406FE#|c3f2JaS_0roc$xB=IKnaVBXrz%tQJcQ8EzbqlJMv(IM^mg zP2ZPXSe}Z|p@#&zmt9$mHqCl2s2rXUV^Jy*K1GZ(x8``O*0$lBnN7zWN3$Hj8f#_K zw~t%5ZO{^qJUZH)>${YkBpxj5C21ls6=NCHJiiE%97RO!XFCZ;s&tC0*zC zwuG3Wr?n2k*K*cb3yGjU0#jk3)o2RVc=O%Kt@I0g%Bq(!m5DJ`4nE&G<2?~1$V2qF z0H_BBPcCy)%_PPi1Na)e)|!Bl?;7YR&j?L!N0FO8pc#x6a<)kp2u!*pqB)qWST7Cr z!z<$sO<>o%P5;n%aj>L85?4-tw(|~s7UjK2-FMx@P)%jkFpK&l9gvU(ON|0#6qNia z#xaS|CWf)W9&HGZ$2QV@=@15U{IerR1;F zQS{`OeRKna(!D8(3*S#;X_idX&m+%#fBZ>LmkVee^cZrY_EPHL2o?h+vX^rac_b1g z09@>klMJ%4VGZ@fFHI!TVFb|qe}R)AdV040T{4<@t^vC?KChX&=VjI zh06C9%XZ!t=YC}Ddl*G_-@X?nijUkIDi{MgOjwcE00@3|!{Jp(cy@&YQc}N*_$f_- z8{69JQ1^RUU2Om|!hjhy*c9FDC5nRUeW-buxBbitwhO#D@qLIixCQDk9xS^70iZN4 z0wNH^h1_lewidF8pdWduu;UNOiaaImR`#e9@F57m8=kSqYwnJ^kCY4yPfP>>it~22 zeu40`#|N}Gawqlu=MwxnkH!gvY*%uz?lCKBNBjvf3tBV^&zGR6$38+>7_J61 za@ChSP^M3|1JUg>oT;C|EZ*I+D~j7koGV*Zz@@p@l_jkvzMu6WR{l8Z`F957Sk)Oh zVDzObMh`9HMINikFcmaXZ{aF_k9o4)Pma^)bksJ_L1y8{tVJbj{fD3nQ~u~=7= zLx>X?8xX5LeJDLaG=5_OE#_K{pB%8ZJ%l6MM+rwyrG|HrYIo|)c`~5uVGxIAsLs^{Vd7d9l)HS*4T|0HiaWx zHfF;y4+gF4<6x5??lpK7L+B@lg!ivuEi+16Pk91|n1rOO*lobvL;6vfJ0<9x6`Ae0 z{z*J^6M7dT*#e7*Ui$2@vIM=d88cto9UTyO7n_LlCx=6|`9YA|wo-~pAfG_kbyIpz zo39S;^I!tR)y|^LKI~qpFmN%Bzn4*>*%W@Pma`RRIr{8nPr6lT%OadhE?n zJS8UaaU5>Qx<)bpC``-j8TuHr*enbXLyc@pmWo(z;%rSJXmH<|3d?EY5N;Q#S=u~F zFi09CF#CDEj0l;a0V1g6iQ(yrX%lpe*SZ`_ejfVth1Qrj%0DXrg8L)85jxbNtpJ`?i~&(q6CtC=tlght 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

mTYWh_FPu{zKnd_w7me|JLC1G(HFFx4d-9L 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 literal 0 HcmV?d00001 diff --git a/docs/images/pam/session-recording/sessions-page-search.png b/docs/images/pam/session-recording/sessions-page-search.png new file mode 100644 index 0000000000000000000000000000000000000000..a90cda58714fb98604477bb962e34ced197901c6 GIT binary patch literal 511727 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�} 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

Bra7Uz1@>ZkG9f2p-K+n#GVz``1;P;GwfW{ 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

ocD%S_Rd#f8ItMbo4Ry%^*)3J&{REk-Dm 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 literal 0 HcmV?d00001 diff --git a/docs/images/pam/session-recording/sessions-page.png b/docs/images/pam/session-recording/sessions-page.png new file mode 100644 index 0000000000000000000000000000000000000000..8faab291dba977d30fd7d695fab24d645ac6dc82 GIT binary patch literal 583157 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%0A0sQk(xpu6l3ml^iIk^Nbgj7r?5 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

vc9>M(u3~>z~h|dW1`7o>$C&&P@BSw)}6L z{(tc+Z^D4v|EcK`%m2~WDdFPld><;6H8`)F|I?NJdx!fszs$k~JV*9bWXL~t9slxO zrSA~~d=RA;8prd{{a>%c-@L=${Bm&KW#Bp2pHTb*%E|xa4a@HU!O1<0e%1eRyFT3l zo}+a`+j#!h z?&tr9#xs;54K(Xy0mJJ6?o6D=glN(~03-hKr6g?=+>@bON%PksTu>$^YuLM=ub&;Z zHB+w1d8ZS&?uF02e z?)lehU2mWmM#)jDlvGm-^aht81BxZultAL3LY-peT^}BhBin2Vylu=EcvKfao{1{z zfAZT0wIQ?vZu(`!`O@QV0ty(u!TRbA;jegr@Lu_v!aTVVwEKeiZP z^zJ&2hX36rNvm${3B%aNEa|^rF&p}{-eL|UMJ9NvwSfAx-05? zc=QyT&Gc};BnJV>wafaY&<#)f@vf~&u;pQqp4zTNfCC{E3!2X_Yb|uXm@bs>oSXkxibPLN2o;NdYl;sGns!x}9hekY2x4#wB7>bB^G; zd?2-1=-&f1-H}xeiqcf=G^R@nE&^KNN6UAwQuN#Q6%>>f+c}ZR{P_L~87*ejArV;s~ z%>9f>$%;hJcmD9E$Wp4eT$0Jki-7p26&9R>EjcQn7=X*GV}+6Y>I}JKe9JLJv}t>{ z@Gv#us19her@=ICRdg@K#v4U(N?{jfQ?}epj9VK+4%P10rI(Y{9SBZVyN8n97?85L zn$Zeuybpoi-{W{JhXAl<6m83UVG#Hv79DX06C~JJ99Q4I+I=iMIUwk_#STYv+ez-{-~<8 z^q-3;IIRbh;gu5dhGLE7u7FDhM3h)_H1x#3i<7>|a~|`VZXy*C42w4mcxAJoSrv9D zb9S-`Izqt$DUKQSiDX0fj&w%IawB7mnoA?H=snLb_RCiXShC}dp@x2kTL@aM>sq{~ zvikFs%PN=64wYzv+|Iwc ze!Q-9C%Z%Nx7++3i1nobvnKk|B&^T(mBkO2PNitS{Ht3RSzm8LZJqpUWj8KtUW%v{ z;nfszrsh4DEHIaPrWKF($_yS6h5T9aNoT;`!*$RB!JMis!(mwVJ zla*#6#P^_qaukjLoo$oBT(Z3;2eEq3A_yINfdS!6{tJ@U(Kcc*|Bb*y^ec9sqF<2P zmgqxAjh8fv+s#ta4SYGlL^RZXDi0mG`1x#tfDylIdrS-nRnG~ zp;G%@_RbR~S-?2g?)O5^QE&rA1qS2|@o)Rg%5g55KvjVY?_~aNqZh(s8oi|1pMaH? zfRr|`!q2)4y(!#k*B|mvrLThYhOE7QZC|N(-$qpU4znMLL!H++413VLJgK7IvH6+- z9<{O!=`vc)5Ti|9L$o7Mwv7p8hHCKpC{Me#bW)tG-KIPq-3!P7EsmP}uC@J+Sc501 zSJjy+cbk0eWxCocX#5MR78Rvk}U!#d3BmPeGIO zX+_rjNfvymCsFf{8@{&(Cvm3Uy;0j!uRs2~MPgW1m4j!Z0iu(G$AK$U4K zSR5$UP)?~3MQo%`jyX;#0x-~v8ZqvapMGe`1|BVAwQ-oYkqEJ;Dxte^ zt$>-n#8K4VwR%Iq;cV2Np#VOtd(h`mG|6(`(X-x0D&aSPV6DcY#xxJUg@8f#2k(NV zebQ4dgKZ-z>UfO495vZ&j`aq}X!{W;(SYJm9I9os(SswvDDcGjSBCZ{{`3~5dNlW2 zZt>dNDo&*niO=ufuDbbNYK2xSdlMjK@nlNg$PcfnYOYAFw2}H+EhA*dT<)>XSCdA8 z4LIp+M^x{cRTb?d2b3s1H_)UmjT2UzjlmO5<5kyG`WP8-t(00YY4jXqr4 z)>kSkc;q^_RvA%OA~@6ou|g_7be+@ok>_5@qwvFU!g<$ZWA)gQ!0nc!ZlHU@<0)4% z_Kl>Pt76sEK#rrq>c`#Dco>>Md;Rs%lMhs>3F=CT6_yBVe(a#hFCeV!V#ITAO2`6tu@K3!YyNwAck0YLY41TYMH5A*E2DF6r8T2~sc>1S7b?fVHU!i-a1NDgA$=1B-?vOv79aR6^8A^SM$a_wHS zbF(>`0+2`sLyJO}-{!)qG1R$O<3J2s?Yv(&`Px{(RCclcWoyMoyCL<~Lkcu-8pvfQzlj3bTK6&>ED0vGN8vRt#6zWo=3 zf|#Sva#s+Kdt%+BJU4LZxN_uoyST*PZb0ia&6o8o# zY#crA$<*vNKt&mQSq`ayHaO1)x0^?aq4 zXZsH-Xm&sdAu!?LOWNV48X({C181xCWbl{dpat$AnH~^uYUe%UU1E*6DRPUW7geIy zTm>ume2|fbUhe~PuB!HdODkf^UiUdR;CW+1LN8Ik3msDe`)cf5!7puB0zz?ZxV1i`cwXiPOkOJf=PmQ{`0`MtH%O4_*@QcgEDftGutbYW1|t__k*oDz}3rb9C+|*W&k97LxG5S z-hCdA9cQzy=LGtC)^V}P*DL*XAEE!KHe#u;6z)N>8juzkz$TPuhdwL@!Xhh6AM0=O zoH|7>4aW5bevXzH%FQtg==&LYzfTeO9h$t8Uy; zeIzJc5g^xLI2)_i6EwJL?Dw7Gg{N-Ps}}^MU9B=9%1;W#HM+?H$nS~1v6x`^nti4s z)WOt`v(W070dLt^nbsH~d%Qz~Fpd=fle8SKxYSSH1OrWkwa9gS_|`7B#lXU6Bx+G2 z0!u@Adc;b%(>E;xRjl^giPdE@h+7~lq!$)k7@Jm34*(ydFh_or>-Tl}F@sOuC)dsE z{Z7s+LFu1I0F}a+pH{bE0CA`aF@o*7Q>cApfp2Juo8RO|$1&?OQM}GSB*+XB@X~Kli8BJJ7q-{ZPo@bci zJJlbzy@5fpmN6eC4eB8sCg4aVYq7|FSYV&0!8Kb3fF$u#l4COJV{Dhi8*9_fC|P7J zm$ttizo#q^4z(u=n>GpS6($*bMD`>dG?sXU69!#ZtCO;4FB{%@Zrg7RaWsids{)!d zymlYgUa|^?ztj!6=rs5p^jyL>(`7N=5=@P|pa}&54z_^r`d!CQUVMr)IItZd_a4ceW?J$BfWe(8e!76SJ7xb!*1B}`Q;1G#dck^&N;DeTY%u35n%pqMHE=CB?wqEWJ7nl%SsRVzYkSXIE`2DCXbc7m3p zMKn<_X4vok;dvh^PVgGZbFax|Q@5o zfaBRkfAAUYhLZ4}bY~LMW2*Y2_oxX|`G%7IRakQ=>L&#aFhA8;nPo(R&u0L^cihfA zlXp@cdnFlbeKA?SUNQfnQ~)FKw2#}h6aPjT(kdL53dSw9R0u&@ru zZMlS=XP{1HH5yW9!;9uk>4hHR$5*wkE7X`Mv=P(OrAY^~VrBZe?4tmo&wsMG3K5KO z|4<4O^G3S0*tPoS@h#K@A5JDXB|ivgLMwx`W^GD_iHWOF1(p%SW~ADD7F{gnQkZYE z9;{Z}FT8;G&Q?OAWDA6ffnMWc=nNdGKltv_T*l9B3cW*+KE28kaPo$l`S2aGe> zGK`fPaGX4sS4!l|K%p9nt$IaIOqEJf>QiLViDQCmGW)*W8869JPp~CuPl*|bi{!8_ z9Tq9y8AY`vj;WnL?PD$mu_?xtg=$0qbMy%rk!!3ddF2J#t#yktyhmDVhoO&+Vgw(C)g33>GF-`>HLm`*u2l7Da= zkNrpeAS%-8gPB7^4n$U^06Kwp|EJo1?bGP9069TYW{U6I*r}{qRj(G#jxt`Ht?g_q zYkU6UJ?hx?DmA%S;U_%Yo6ZchxU0~k?uThSNgkp$W_+yAy|+5M9~!}=&C~_Y3j%o2 z(1u#=n;sCCe%8acSt0CweZS#yreBPQjSF=2!?nYxKI2K{bg6e$IhsGM@1Q9%hg%D8 zeCNi0c0yF@xEL)m5cpzO$609oVwtJ)V0ux*x$FIefj_NySDpzm$3Ls91s>jx(|b%; zHzS4g` zjt$_x<>$da#6l;nfrYO^*REEXi%(T-9LKD0sOd{us)3HLy-K8DVBg4!VAfh-xxunD zC*$sfj(C}Fok{p2oK>e0Z*G4C%*B;w*%8qKDX`CY(>+)JBAZsM-l3BuEq=s=-_5;B zi8Bbv*n!sc*C7ex{JQk&Y6kO{&q}JFNa9>PCp^EL_16TdjQgesK=v)+L(ir-7kRl7 z&ojQGnX}-xeEPrN?M4L|S&tUe{)&?SJ<|O2n81(tUudC-GU&Pf8~zAt5QLCSX!p$a z&GG551@TIX*gP=KEt>U`jeij(NG6$c^)1ZkPzkqYd-ce5`2Kq~fWsDz34>@lQ^c?4 z{IZ3?J52(PFJ;%%X-Vx!BJLpVGu)GJp}rrOr}Rcw8*Dvso-&*_}Zagdj)e? zZDZNy+d6ua9jcEXII96$W8uu)TaF~4*Pp)eEUJXMH4afz8jkYhTRMuiic7UlnBGQ4 z22O!6!x4M9j@uTmb75hL-NZ=Vw(qTcf90R=i%4`ld&nQAw2cV=bz8aF{HbAhl-j5_ z_62WIUvi!fk5^so%{a@0ZN_mdEqRU;@`OP0Rp(PMF-4Oo)y>%8PC9+aj+LzL(H#`M zcv!9jy5!~DfJ6I%kLVA_5+z1wRweE$A|cTjyuU+J3zPA9oFp}mwv$&I|T zem&Z?7BCn4dJY8fD%x(pO0~zB*hzn)Xm+X;Ipk45eecLFeeI$fmBAlFb?guyOwKnK z<3Kkl3ovPW?)kn1l$I0mBiNcPH~-S0oWNsm!W%q$$$L?42GRAPYnsb4KBw(r*~Jz$ z9m$gjrJ;89x9&-ml>PZFeR=?32;78cA6&hT1*CRI^#5QP-h<3GG${O@c+QF1Sh4Oi zP1;~v$t;Dmo>w1JCQnlGg-kyL%8C8#j+*XHAeHWWdBP{l>v8y{=3-hr#&1v!9w7%C z9m{-6Nb0btW!DzxT=n^Q9-c5YFOReP^yQ!BA#tzk52PKzE|5ghOW*E)Lf5dMY4r*1 zmcfvkW$V=X@N=W%iMN06V(&rvpu^H%4)MPFSKn^KG6|2Pf-JcJ9l%iFif!HR$3Ct> zG6K%1Xb;d~(%4xt$RnZT#m$2}j$o%SwF zfbqTIb6nQDVWl~c?EB5dxuV)SgJo)R{wKvzF$WJLa8Zj;0`_K}wUrmwmUWFz;&|RA zgAyd?>0=sqe5{sbM|1+cVY(z7EYE!?w)``swRZG2Q$0u5m}_chc+fL@E_SV7b`(YAoNnwZIXu%J`bHJmdV zByN)BQl17eUi#S&x_9;JBeoc0*!^Xg{_BTil%DGSLlFJ%&vIUqNYxLtm`Ui)# z_9spqQS5MFu+T3^R=t$`HBICPPO5XCx#&Jd9IQ5>oWkHkKd_y_yL$kJ3huy@z5d4+ zf(BraNOiH*5k99VG4?Y`40QU*q&Q3Kkudj8hm3DXdb*USgAe6#mbSYFt2N*n)tta; zS+;+Z!Rk`B7&ZMlMNoX3$KsE89$0?FE+2^UA*I93hUZTG7bo6Z_H@GzYVjkxBez1B zrIg*HjNEWdpRs+~QF!F}1P577_)y~|c>LoMJf?)*G|`pyBd9YzSx$Lf`a}UEj#|!( zKW_3hyXyaRbRP@r4lm2kNB+^}#6I;7y*PbJYEKkKC?;cmU4L(jmK}NR+ z*V2zOd>qRyruEpvWzS6gCtjV&aNL#pidVDltmx&eQQ<2I&Bnr3j^mMG=lyJs`S)kf z?BZySoj$1q*v=H;8G4!4aBl^=abUt4_Dt=Sb=}h~55A*bRl-d4eR7U{z^AP@%*OKC zwJXXA(K=7@_9J8Gj8BVPVzvY-XAZzhU^Vbhc86EzMy0sztsB>y7mk+ny_mP+C&cM( zSbe^rhQde5N$-b&NZ#UpY2DPQ)>1haXtfjDMvS%edv#g%tbrpZubxO-KCeClGKqgh zYTowBGZwWJ5A526U5|#}DHOfLHZLH34rUma9nIOEQJqk4=gD_Ip&%okd=Isl^qImQ zn9mBn*eusDKnb$@8N4(_CHYJm&`1m!Ta4~6JqFBR1VCVTVg^sMkgXYgku;(@ZqJeW zKjGpLUv7gl)SyPx?|x~`-DgI|<2sJL%RO26&WHb=? zyDR5cWyg_oL`&ibULQ*=Sf%~hv|q?yxaCI_nUE#$2A-%-`dSLZ#CDs+BqWsqMt?d@?g27^5Fc& z)d8`G5PFc7@G7_R6=1+Pc@nE(LfU|sYgyQAUgtIU@k@_4eGb5^ z{YW|i*GxCuh{8U8S>|>-&1!tul_XNOMA&Q^pNLiaWw|vfB0a)1;oW9~Mj8MI29Et1 zQ|mh3yB_vGRx7jf{YJV|6g5`(-aTQmh^mNUCYB@5G838+AP^?#zC4ORm~4h1mTJ`} z`g;uELnn^aPULefM18I~uS(Z|TQ6sUPPMX8@39tMl(~#V5^FF4^U9?>-&mPar_Li; zEs(B=Ik7(JJi=WDBK1E=NXyHO0OroEKj)C=>-CI)he3BLZ+|gpUm{U`N-NjySI;HU zdiu0Ct07y*^IUDCVn@iyX$ou_B6ZRoW6Jjxjq}KNw#43f&l2ZB^+VJ z{QUuUk&i^NK`#^ji^HJZ`mBU0ug|W}(@68}=DHto@U-xXfOXBd7A2VA{-L6s=WEnj zMKtf~zQ#$$p|5PVY`5%{GV(6eTitP`s*T?Y;`{M-uoEQFtc4h^y+WeuNOMeXg*mv@ zbm@NL_<+-liLj0=!N8m`9=$4NnzlYs4jG;T*zSwH=2<7$uO{F5-%{%1?@yoHVprud zbxRt`R0G_Ey2%Ye3U*A_VxtZ{2%ROTPxuLhJdo(>r`uTFTCz`Qi}z?HSIzaYcWg>X z?QDT-f-V`Bfcq`_bQb@f;meyZw7*A}O+itxld|eHt^W7BAn;QH#PM35n2qGpuimK6fmC7bKce65+@uKOaZQv zubNL`C1Fm+5h;agGey?eiptA}+b3JMeJ}Bb9@iFk{1ZQrjK*1(N>%!P44#hg+O4Hj znx9OrJ9EKnMtal0S}Kjhv+tI(aF$A+eEo{e=yutWPW470vSh7n`A+qGmr9TV z&FcFIpw8$5KK1K%GeW@FH>}2W2RY1lXs{8B-0oJC6SrnB6xAs-p*J%+w(smDVq8z| z{WO|t%5GeSGA_XpEjY-zztA=F_#*fi_rX{m-xuegj?D1!sBb@FKMenU?GP;WQj=x8 zn`&il;SLs~%6z_u)!CNj2iiKItn;F4?5tL0o$G%HyyeqTtz)t+lpQwXq_%Goj*;(S zQLd?&lUkZE)v*G+hsbbgfTB}OW2KJAdDQOupp;S1>699?jKEn<9N6aE^Cf@~RENL7 ziAFxoa&pZs{&Ge9JZDNg`*I1%%C@G!lSCAP@AR%4v4t9OAOR5mn>5Tq<<()|&lIuA zXaGQeNSq{!_kCg109PzF=-FV37HR)A@eFo+2Qj^lz;}Sx;zak*8}~kR8s4`w?fUXd zU<0Wc|Lk3j`*HqMRg|=Po#*f3r&9A{Mg4tRQ{<|Duvf~?>sf&PLN{?>A|fo*iY4Ls zDjC_(1TRw>%Gk}?C1YatN@?An#*aYUPD##eAO*ZbNZKyvRSIrfz*9VV=VP*sTQ6{? znIe~e@!o;m>I?kw1T?8Z0a&}PFm(U}rKLi1q~=@6P9*xYYg2*WhFuS*N*6BoZYbE0 zX90$>&*lW!^A-)gyu3|LBhJO7kGQSn!#DCUl&b>%Qjq`aw$(LnT?}^jYzY;j((|Dr zQLa$vr*2a(-TMxk4mEnapD8Nxs?pvT<58}TEsOd>wxj$w$JdMiGt*(5;PuoR&#TQS z!LEFH5j5!YOZQOM{0X<(ret;&`yAn)CmjRhqT03I5j(t>fm==0<~-5fFn`okX66`` zM`3MrPR{0%*zs)>{ZA&r^Xo)^nlWH#;jlD0TpMCte=F{7L)PZb5Pfg5=4eVrwFln` z`G$m^?z>)!lKIVdC}BNv&7FwlYTpNQipd5Nr2DNl2mTSPS{p2@FYC#yBMR!`D5>6f zQpy@BTKj@grCR48rY=ztI4>EIa8{3n$_dQo?w>r|U(nbduGnJxU3>7<%QF-j@%K4h z@OgV2ZFiU#)Ely^t+bfR?*oA5+Y%oOu9gQH<-9R&ZgW z8h2M1^K8C$(TlAbiLzfASwj9smHo+4Ipf-{;qG^|+QEgkyEfIPbpRgcl%bZ3b7qZ&RXNis;MO_j1~$W^Jq2=r;{$ngoaEO7*XgCeX~gQ`jX%r~`_rKD15q;0 z>QL8&xiB?$LZ7ra$6yZ-mN1~)6)zAo$+a1fsP$>m5z(;`IhdayxURw*ZgVk2^wZ=A za0Z{Wxc^!v(Z1Gg&u2WS5i1L2x4OP@EJMa!)n#n%UDu2ZS**FdtOl;T^@&^l^m7G1 z40fiY`94b7O2||6FAKY+exYLHKC7b>bS;hfuht zbmACUB;x7-a+}+@01-n28E7tcAIn?SutR<-!+~ZrSnaQu=#pX$XB^j@?-gF#xZn6P zLpJWrh#=OO%~;E0t7V^cXd{$HHjMlhlJKRaG5OV$7%PB*JCiS;RAbp-%U&^0D>tK* zsSLA%I5=&L_gI(s8j^UPw;j+ABpKz+>po|%^ zCz6lce@nob8JU^2v_1DYqjrxnicNk!G0i$?V!}K|F2>FtOO1>W)FwF2G+@*tJ9`_8 zE9CkGm%npQ{az6|+@u;P)4(TrF_JE0UO8VrarT~#?C@c1T9eaF7@DHg;_+K{o!w=^ ze=PQg;X3VM9icCCVuKDM!p(pxXB_^br9J^38cGFLp!psz{t)^129lNaPH`1fN=2qT za-9*Z-2a4bAJu`mHlyBI*X(DXu3UI73SNu3R+u$y9`(XB1Sh|4u9(rG-$e(}b3H`o zA$R%Y4XEPcGxHIq%ERAZwR9F1>rczNW4FpYPSzhHi}Yi5(L(Yp6*Esru36ncFwKKZ z$n;6q*e~3>Ph~&ywO!8o0dc}09?vnio%i?*K6gEnm_?J0Q8{LYIVfY zV7jLAxLaQt7vWh^dE-%q4(%7e(5+e2aEC8~Vp{%2#7F^3{uk3|fsW2Q^1m!Wq7_)B zfg%K1d;ds1y|c0X9ck2Jl!lEaZ$*>$Ix%%jKQ59P7w@RcBPxeZ*-YiV^U<`fo9DW? zUKh9bz5Zd<=0Qe6M8es&%En_EurfaHb#XwO1Z{?Nej_`nsh>9b<$n58%wV$XBJ2%c zW_lW4d)X{D1v~da5H**$1E$s~d5&tu2LF7CW}KkK$1%CoVyh{>>p4$;{q?vbWU>i| zpqrO2a#c6wHks;v$ujf1*IOWq)%cv&sBsAh49C8(8;o?5TGg8zlNS{gc?!E!?63Z) zDsbeQ!@N;EpCNQnwyLit)NRCy$L(VaG8A8I_n(0qF5V6<=ym?0qkED%D=SXxnz9K+ zZ_a5N-ahW5kFhE10{cro+iQ^|lkfFbxySovw_(;K2WCEBJx^kE!Zq5+uH<)_6xSDH z?ekSz84AV>UW}q_LJ!KDa`r?&_D~+p`~ug85T)Q!_o~oPgWOboQ~&SINTse zwqh=PoM3l84!_{v>vd0O#v}nBDUeow&RfM49n4`lb2xF~_5-c*(fy5KRM9!g>%3pa z=AUs{nb;qw^JC+=L{(l;%GR2viZ#$=iF`yf)*F-io2jchfxXg#E z96?i-ox{QmpD_pYC)`|jKKw3LDX&0Km!P0lEjBTV<7|erz-j|~MwUyMuXrm@RT1DB~T{+^kEfh7{uy2`EU3oxU z=-3yBSlxnuoYGd!Bzz)&1&Pe0-lM~ zP>o!i{PdDEgq6*leSllmZ@F3bK$t!g^Dq3spQfECU!j&AW+D!n4<8?4RZl{$0yqCv z*}gj{m$QWFtm6VPy=8Rw^*`khdUGgZQIxfQH{^6Vu%4t#mh?2XX9Hm|RrR{=pk1`s z0y)5tzp%u0u>!9TvW@1Q{4lg$(fCvcotHbh=yWh;5AVvpSF`?#Wf0C2eEg6MG&Y&M z-X*U0MO;_so@1)RRdso~P zo*eZNkNh*$@P=r!v*2t05@kt?E+zWux;N~K*x0MhEmC>~!3q^t5n&7DnNuVRKE}rE zw`%u#UA}XF4W6oY>L5p;2%xJH(R+#333UUKgRunzA`D55O2H3I3+TsBc2}qf$f!y< z`6J*{x=r;$PEmI)g7R=@t^z9n(jdk-v&Vn?D~=&Z? z*VW6-3V|Atv4xHIl?Gp0x@6ymz|#Us6mEM5|NW_YRwjMZK<(Nz5EK$pX`2Eb)ae%p ziVFr3SO8L_-l*n6u zT1!4UT{Vv5y;a(y<1=e-QQB;W_3<77ghr8Gl_**p1Y_$Tl8`VmtCxqs_~<1{`*hV2rp%x1>|xcPAGx)caeJP+ zbPV{dWh;*4{w$IYn`H8C72Gk)rE5tEr z2jWI6c^kHw|MAV}NAsL=+icE=6x~r4Q{*x>@;_5TjOBKe81-guSPflCF)*;qG$~bX z(D%f#SGP`-L71gXQl*pH=H7GswP`4O(*TiS)vVb3n9v6Z;ug;C+B>Z=DY;yuRWCvkg{u81;c;CJkg@9+r8t$1laYm}HjM|(E4d-a!T>8!w( zVs2_~*$50whWcTJo!T;(xaQ`!G#7+Q_`TSno9@VX0we zv2OMXmP(#RwK4>c@3jKI;|n8i=mzzk3K{OS8LH05-5dj2#Ans+$+VPRk@_lKj#cRS zBH$rXS4&k>Se#P&b13O_7uRH!k`0{z-gHIrAy{#%k+EF{b2V{^&`qcmEeP)L}* zi$_#W*Q7hIuIPOI6xv~a;Q?(8^cvN1eo0(&4MAvUgZKBF{V!*d@~#A>1Us`@QA1L{ zLflODq-GqJnpDc0?=NGrw$DI1-MymTudH?d>`#L56rGR+iknsAHLhUQVJ9yZwl7|k z$k_F0UP}&CYOM(}E{h;uRPR<`iS6v0sQo<-L|}cf#XB%LS(&2hYT0amXPhs9aKk9z zsy6Agn#bY}1%J)}Sv(&S7M3ca(xX)i+zl(=GrB7(DN&cypDB;0mxlIc#hp1_!CT;5 znE70Oz`qn{9{%S84)JkM1>b4d!*SN4@6l4ZZ> z{FSTh*-}-zc_u~?u`%T)KC;iGR_75#v(C^p?s%Pb_2*}bpmdq&PGHe5H=|}_a`lq_ zUt8z@pI>^*W3VcQ8LHe?Nyg3hFOdi;b6NK_Ulku|X>C;+V@Av8Qz*?_Y8L5*Q&10A zIZAPMXV%o$19ItMSs-=QZRoReX!6uv+GVwtuuJ9{`Wpy^5| zRj3<{V(2N9hzJt^$HIT3Sd9px;5~Z0`Wx&2`a0rybKTYTsMs0(2oQxJ!f-&1%j%9$ zk~Opunw6EcaR-4Ov;Ee)YZ8+edBmXk_4#x4K5XM z2lxcWTP5^7zEN|32~IM-HK4qD^qO5g+`t+hnQGwYN7Xy-{O&IR^}jDFe;)2kDp916 zmv`6_R*R{r=|HtJT9UkGCY$+z@Nki0cC6R5t6eXzaU~2Ow+kN%7%XRate6-WLOy(G znCs4ncQf;)I#?Y=H1TN^!Q3gY<%k_@yh9|otj{}{c1k}tWSwEGotcAu&Cm@ z1Yn*CRaGHTNzKUkdiHmCO7y(>ETqcQBS7gf+2+Hs$Nj`^)76UsT?CD{4(NcVNRFh>PJSnnnIYg}bZqg0?`Ze1kcd zXUek*B8TgQq4YdNgRa;_G3kCDQ&>%tLZydp@~Qh<@3nS%+cr98hM?a9Q3R+YL>PxS zvAYg(iz@yfvj6tr{!^wiX|IEF&gQtH#CtILHY7OsF~~?A=!TL}X~&Uh8j3)PH4UE% z3CWf4_XJeP8IoPcko?tl7ula264Ef1&^i94w5ON}aBmuzAF4VVyfFT8s!dAT@Ayzt~i&OCi6Rew0_v8YG87|N7ZZ7rQ>*2XE*qUk#&r7dcj5v$APE#*tpA!U-OswqiN!ic}?&mSk@ zri$+HAhR0Zy5nP8g{*Zkhktm28~b2L7&GVh{Clnhd%FI1)8$(OrSEb*>T-pvO8rBP zaP>ROw4jDspMs^ROQueMhj5QT@ zavwA_m9dT^YIX}zR2+rbN+mMbp9nPG89 zo!s0;Tak@Ko&ak{1nO`L-3j98evbo7@zVRNk*Q;2|2*{{Ow4`hhgegN{+Z1Z0el{- zAsXCHYsXbCys_+8fcF2=Teitla%E*fn201T6xeZ-%^Y|N8M6GEsS!4f($8;6xD`;! z6237l3_H0M?bvGdWa&Gc#_b}l+6ZI@*Qy#>(uH|^Z)6xTtHcK3B)RMjsjcRaoPUli zWa`|lNpm4i{_r~T--ZiSFHIxwQu#eb9jEyoDbxr_{obvi7V+{g2~5J0`M&L{a^l5G=f_kAdddwF@;amj)gdQ}~7(%xlj=F=n^`3qCr!&#E?4T+5-n8-{6 zw=s*IO`thLxtjZdx*1x3cU9H8ydsA5!AC=G#_O+*&HM(Vv_Ou?^Ey@Q_Ak<-lv&n$ zZSy$;AbEu2jXG_cWJ>ct9g`YNips>2Mfs%?Hrt$`;2}uT#mqb{Z(1i1@PAZbFzQw` zlb>jlUp%|>m+|_qC%>VC{+au}1u&`2i3LgLrDjuaAZCypZ=WU&@N}hf&l`Oze=#Z* zI{|a{?QZryC54pV=~;ty#3z1A@h~UpF&R*_A58AXPkf_ZbmqNZ34lncb1#W1lMAer9v=$=~{8)>JuP2pM1il{FN(Lr#`6( z35;&jn)v>1g9%RoOTug`n074lIYv@0dAgQcLzFHr!1)Cp_j zuJbdogs3uTj>%i!W!Q9Zl~uK# zdi*?qoQIcJ&0^EZ(5IX+mhy^0xOuOTN#;41=+?c6?(Uv#<%Nj>^$S?Scvi|-J%WtW zEcF7y68!MY`E;M0-(Y7!TF7MSCcgbkr-?e4SXrR?0S=e-^n*Q5rvlxsh^1APkwRDm zZ54R;+Dt(VmyM?+Xgq0;OL+vZp47uM(0Gd7YTx*Cnw1!I4-9u9BBtIDc}}ax&x^KL zsKO0%8Ibidl3#Cg1csfE>b*)R;B0jRP0?3 zr&B|=6bm7gZgUo!smS`_MA{vAHBIst8bGIOAEP-ZkH%eN+wJ-UoWD+)Z2I?X5S&hH zplcx+{jzd$gzkOc{onTjBXc^y1C&oUk25*Cexg8^Ec~?tGgF>17COlq>wd4_dMe5W zdjIk*{r+g&0WdL#3g%io<#%2S0UI1EcfQt4M&UhQ0Px)tcf;QW0=uJUA~I@Pc%!s-lcU7sQ2yP!kdzzZOV z%TnDuF{Jm~eI=&%mwz+_k9@qk&exKGjwuM2jH{EZpgxKH4ZMJ&)V!H%y~e6$i+=z_ zJB*@BU|^D>#vx`23lu!1tMIeyGLCos6zkP^6mmCVrT|r>p8)~^_=_FQ4;$qK-1dVQ zS?V>_OZ!ja4Y?@})={jXV59$NhvabG1&5S0uRq@~O7goA`i+ ztLiMQ1jUyOIH~#s#L)V&k2@ZGv1}Qi zOv2DW*PI2YzVWwdIleWjV&y$W;mQqt_=2dJDm|IM2hDpv{(g}<=%d{_3^wMgJ}7|KZZiT{IR8xa)yo;LKE6h zVP!zAO;j~Qyl;Rx0C!~~%~s&5uE=9v=X%!S&%Id&iO$`kVOm-QPs5R&;xoUTwb2i5 zln49Towo8R$i&#Sw8=>yF_>P03(Uc$f`N$aM8K;z#JNABY z7W$j;Is(hbrzK#th!^kA6;P&?2(k|B(6t8@Q5m)NWew{yY`P8CDz$Iiyx|3RJIkGM zn=U3vEb}>9|%>-cQX|%neA*eY;21simDO zTe=huQe)cJohP+fZgxx%Zh-T<_nhGn<(1f;w~Mc<9f4 zbIsfAc9=TIHJc(BEzfAC=CUKrbfC)#n_COxVJ&2kj`60R^F|>*OU|9WJ_>m&&ydAD zOZ^somQnsCyf;tJaMiQt%lvCOQ~s?g=B2y~8U@{OgtmT}toW9Kz8GHxIF3L5=}NTh z3MV4l1sw2kXLcd(zq9ba6Gp`CG0I5u{K_{smLU*qWFkcuUT}Ds)@4;(cPRXhJ5tn* z7ssE`iSN0YT61uI!e+fuW2a)^V+nX&RApl)SZ$(5tcy>pwlZD1gohqXyVq+Hin+%w z19J#u1b3xc+PeB*j~g33>?%hzjq|kaXPlRaI&mji*-DWPCnw<46CdloQMvJIXM3At z*&{c>wkhw<(LCf~MruVFNJrU1Xv5Yyt*#Yfo7Lwdvy=%ka>j?R=__Ih~ zOem8vysCl+%gjcFe`ecV`#pcZ^J+J^o6lbz=Ap@69m22qPtWc;!T$?T7fOq12MsqHT#}BoImz( z2L=Y_WY6NuucexoQs`k!{_1>>fjPC(BO)?Pe0KvIs^+x-W+{8rR>bqJVh;`B$UV`^ z*Aihq!1i7p)!b`1FekCy)2+rbwxWRh4CH9V?Dp$!i}*l*Z`aq;v);kF2yADKqs^4S zd5v|svGWLwA#7`NuOItf8YKpA;vDOmGv&he;dg&qOk&GIP(mm zSmNypdU!N}vE+n&Urpifi0-ZbI8n6VKQG+^TQZFU{r(F?wS~ii!Iqg%+*G74ze~_# ztT-`U0k@*~LgQ90gL?`ZpXf`ssv;elwH>~-vopVAh#iutmP0VBf886Z4D}l48jgBL z*5cOl1t)T#Rj>9YgLzHv7)yu5^MQ)SU$ZU~0c|OehRSg4 zgcYVFU|`Oqah))G9^LjOCQH@BG+n3V(D-0k!UbTxt(8~NPYXck)v-;crEfV^*(hKd zi;)woPt4R~w&sTF5 zv@4Fa=00(ZfkYy=7lkffgOnb%q$rkgx#D8C9w<{L zUY-7`7rIXtYnfJ$CyB=b*R(5gH*fR&bwr|>?w6eW1lK?MX@d0WkfqcJdRh>6d{~Wy zaTKr~l;U-kCo(W{neW>72`(BpuS^_l3YOPq;cb`edG8Pl6K%9XXGs1O#y$B&%j$-M zQh@`v{U~Eb&7`T6!tU^Wfz!^Mg3e|?BA*-|=VC8z1U(L&da+AZeaH2nT#zb1Y8Y7R zPNJM^{p>BeK($CvG}SBRU~Q^v_*QQzvffLVAT}1{(c~efw=pxASBF9B*~$v{&-3rt zpIuuvWL&%rN{qc2bzr>1>&6EnOj1x!&$=A9Mqd)Q>WWcq0n9d?3X>v;UXO#wyAe6H zLfs@V&^~7*4kFT2N8H9zRuiahBghZ@#fyb*W4ru+G;~nuqV72bCMY1V*Vs$@LjE(p zKE%ObQzplOj$g%%yh<$H3<@J&khB=Q?`Ij3ECM}AE2Z0PXvj8O3dcJhYRLHr;ahno zq7o)99qTF6)>@fo``|uNhW<S9$h>DV zJJv+Er}8CT4ysI`hUfA;+^}cGA9>fRc3pWn3*Q;^(sPh&8iwxuj%T>s;uNrFhF#jo zWcCZ>(Z_UVL}S{hmUEoLC~0V|chSCl&m+w^ezUNcpPM-a=^da3EpWE{d%<1mdGx$5 z=ZSf^jxx(w$1y2b4;_wjLa`hOF9o+fa>TT`BqdYQi^6ImLh~ee9!KFxFK>KM%f&aqRGmH_t)FCxc)H@xSkTXO zvW6EO<6fqKf6{-t2Pl~of6HbS4*KRpK zoUpWL?(R(XsND;0s$%(^1E8j)64lMZ9?aMi1AwW&HiDPKqpINcb_&$ zb;*m=+T0rlF?j`5D-36o{I1ZcoSqM7$L0mlOr3m%`_|d+f+KVNNus;@Q%v~<)CH#0aIb`^`VWblYF}=nWDf3|ye=`g zM!o+y=HnP`jF;uOVCLq@88;dO=dtY@uia17ym~jR)h>!sWR*dGe%(6MWwb=BB>1W% z@*uH5O2|B$=$op=x2XrXOm~l4Y+0+&3NiAj#d=X(>!@*TY;2Z6gvx;o%+mlv9cVnZ zm5(m>HEg@e69nvw-I*F&XePEk)Wc1T7z&!(+A7hWXu$x)v^KE9TdQC1FY?LNDjBJj zlS8j)ug6Q71V!$V2%c_=!o9QAkhP=*_uQ3@&%XJN)8a5k#q4_ij8b@)9L|-88M^RM zTW;z6&kfI0iM@u(-nZ6|IpuQ6ZSBZY;Le{uCL~rO;)_qWiWM;aBN@7O-SM^XXI;gs zdOxFzqk0!Av7z0gJglY%^lv`{p!4`y$Xuors71+rdfr4#9H#1Uh;oZ+P&hG8v{~?d z#^cGRaGvkED{%xFm@hcS7zzNG;F1m1kBh34$mJNcfM%q%bfr(VB(tU){$fYyV%39XgM)p9<-YD}gof1?g^O?3n}y-U{{S3;4fGj!A*=bdy+V&y zrN$1b_Q)W2mI|U=A9V1LzaUiaCFdO2BeAI6!?t+yZq-NA>ufJ1$Hz_ubiHW3M%II= zi>cdtmb$Ay0$lozH71KRXu+{U7+S|M-+<4|sv3O)nV3jaY)s55yeW3#tEQ6sJ%}_+%=H)*?4H5A-&Xk zQBaZJ%Q_*z4HvHM1eR$gCP>E?4HBZX3Jm4kcYlg$ahEOu{ce1G< zi$@De=ffFQ#-x0UMKGI=&C0W#4Ar=DKTNIOu0s*kgvQky&J`C!X$iLdM;D6^cyHjh zZ{GWn=ChAeTw-tVqZOBEBhU3@>yLaw70_ZykM$ZTz=ka4~vi<~KGM zcEL`>XI%a%X7&2Vns*iY;=QHio%%Yv#7+4x@px@QB5lioe+n#+9*5QSqMx0t%5XpI z)68wHAXm5Nz3V<7Si8sYv`AK1Xj}Q57Ykc6CWYpd(sjLw&m+W1TNX%oB;d9J%Y9Q! zZ(jmPj!uCId;V>l%lUC8m87sC_WSV^QktD%V&}g5$nPl?p=Db$U~WrQDCh?>Iypc) z_obU5>FxObLLYq~xhLD0f)4I;NfFCeV@cxRIL>9A0br(=#m^l1TGCP2pyNEZ!Ec4~ zHa-2o8WdZ>&IZv;!x?;7KIItm=4FeSF7#!HQB^0r6<2RAxy~=-zBOrv^#8 zY+KOV8KU)M)rnZjZI!1o)hk&eA@(0C*mzsl8{GVH`@&bsmaI-9P&l5r12WIypInG) zc<0)ln+0y6KMrZTYYh>zJ=D!D8Yi|`U*lOgyk{leeWM(jKejzqG<&+9B%-WcYocRm zvl{vQ!p6thQW&^Tq0q&|s`BAInIkcKi23-nprj03TW_e-H?Yi7DSeAr z*l@x^{+kK}_u&#GW9+1$K7K(N0fZL#*~f>ofm6D=6a8sYB=ECNt8v`X#vfebW1ZpA zL`^ZAE}e57d?gom4mLrmxAp^7$C8*L6m;&YoxM5ahC!z5wzrfs4Zdu;F`=ohMeioE#NaF-8mco_u6SRFJ2l44{ zD_DZPhJ@xP*zKzsOMEh#%gt5wG6Ia~_Ov_t2!mhw0)RwzB2cF_6>ZpX&kGC3Jd|C> zQ+UNp0uD!Yzi|R=flXe^wuNY8@{P%6B{n@ayfGv8_jLZiHF}(YqL3tv-Tj&EL?*ms=??|cwV-9iynHq@ z@9i&ozoTrCX%_E;<94iik~en}E}GVlXA@2f*vW<7edr!M*MZKjmGs#{nG#pzqRYA< z49G>ViFJg;=&nQMX44KMma=cT>6tdpr;;0=(KROlOlmbldz`hNiDxpJX(Cb)EghFE z;@q8{T|=!!86Mpd4V3N4dOm7QgFQVO8636}AGl{-P|3G>Q1{lj{n5pSi?Lb4Dll(a zJg(r*u7p9jL``gp`i#TR$F?vsh_oMqDp!iqGKq#`C+$z&HW)3_mFmKkxJ#1ju?)s$lvVJ(?HFqYqZa>inF339 zat@Qk1lSXvLFJrck#=Qc;P0@nkhC_i_FzD)c_Z+(V*ZHW`@QvCAv2h2fy@8H-dBf3 z*{$mfQVK{ZD2Sj4NUL;%N_Tfi3Jf9LZ6F{>H%NC3-6bM9bi>eH0y6_c{a&`P2zUH=(z>>1_-92x(Csc~WSH#pS=bjmYr|h? zR}{3C8waZdSiaznYgBh?EBk=jEpDh4ZPqOTm6`~2O-P(dNyux%^z^a1Z~t0Vp$2= zqdmiy&8)T2B=^BO`Vk^5kq^eGsXxFk%2sia<{`YEX0L&&*2>2>qIvWADmCsx08lV8 z=6k{8iV_ocpfX6u^yNEGzb8o#*KmPDzP;t072m8qcJkqfC3LWeUmguJU8v=;6dQv| z0$X448h>>LsXsMX^Ln8p#goVz`}@X~80s2Nqqd;K6a7MrUG6H`i~nUMzvY+mCtS|!pds-vIkHFb7q`T1o=Rg1d&q)}pHW36c8Z|vH66wgOGwTB=m zt^ulvU3i)6V_(zM+9(b!u6tre;T{}t+#1wyDgccN)9Genkm;{S0&G>+Kr7L75cAKuxP$yqtl zag$EZea<(tu4Yb`sd&oQwyq&?7zp_j9lVR~OfL7hf`nBE*;n0u&Ns~_4$6|PuaCK6 z$ARb3g{wJ{UC-?&xHV&W9CF>t_2`U8pBjF*U(rWV#N%^r#Ln#;ihTzN5uqP#=p_Rh zjw{E<&e$BgBdFkf`&FBKeKOz3S=>Fae&cYBiw?WNMi>+L0P^ugP0P4?i6<0B2 z6CE}6g(4=^;az81&ag!!{JTH)lK9eRUMnX*Z~p*uC^SuIH(B#6%MIAAG^s@V4A}Fuq6^e%cUPeup%s94NDg_UW6mEchGqmE$Bd z`@m+ncdy`Ka$A|pW_><})beG<0TIh) zvk_4Pb=MX1cXaw0RpOi_CE-^6lzG$4fst?IRr=xVghWLO%FjxWag;xA1hW<9io?kn z!$L?%q*6WYED|Es8I^s`Cdz7GeS*~+XS&MyWs#a?(ql(S_I0aLxzUJ!Few>eqZ56O zxZUONaugPUPxbSyk5Vx--oc~yDWds1R=6(!@IxYp|u`7&fx z8+VA zU56h0V)??Vd0PbjN=Z9KI9)hRmqDULAI8Ll_spyTUchE^Oyor`Z|88WGx+4V)$-X* zGIgIztX#Qj6RJxCk{(~)$d3oap+mN6j`9#bBN-UQM4xcZ)3bd~lhH1-M|u+o4{Bim z%i-y80=M0i?QYQ;t4WoI? z&dqrAu4<(Z9M&UpO$q?`ay0rz`i9GdXY(nywcBc5`3COP&k82>r_dd(j8(=C-D|Ox z^*lvV(M)#2##s@@D{&lK79_w!MW%J!jf_fKzO|z9{=p>bPd~q(mbEZizf!aDpdol= z^X}9rPLV1-h?G=7F<@7eKe{18uj!C64-gFD!~TLtsO`mVhRdpkf~2k zrA38Qm#sO}ubFy+!;->czZsLlrdg6W-`HoSnVN>+0L|9QKP1nR$n~O z|DY29y)TS_29C`EA|zMnjH;%|5YSY-N}yQg(d{XU3o#@%q798gx!5nyBx%<8S+CSG zUJ#}KBjD!J8c;y(`)rpL~&uxkJc*vQy=JH&Lgfs6?1b+R{yQqa?n%o z=;!*0RvI#KbHA9YB7;098Y9tQwnCcGkI-hTPzx15rTVCpepS_-2GD)Use6p7{!LFc-7_Uo(oIo-}IZ>*n;( z6DZaRt3S+h3H+P+5GueImy@$wDKY9$M3AnHRsks{nGvtm>DtOEn^u^W{{981rSS}Q z9!bWRkd(B%t~#7){J`U&M6AuXZ1ce|D}-;u)W;rom4&5v8hJ}qPPW?ZSoC0dnyqXcWx&X8<3@Vbl`kp#J#aVKM#P&zEwsEm#2I z2J6jDP6=5X7@blB{&9&{Ugc=eT;;T59=3c9GA?iWn~zqd6)X>ctwNY~29M?EkZ7JF zI(G#Os(_N9c%^xNYFixLY1eFzl?!5#i4P6Q#1y2c6%EtXJ6I z9iQj+#Yl>D!FKyCiGYUd{iDK4S2RlwUVt8Habu%x6%<{$gcdV60d6YHX8o6DPx?>S-ivc3nv z4rmbPv*$6Gu;CSQ$_ zLY|ldU~}JZk2kvaWb9L3fkOxLRG0&7t zcY6*H# zaXQ?6%}l0R<;auac(bx*zyWZ#?Z&wZAcoB^`iZfVNV)ew5+CJ4FV4WVEKF2LmmEb!qWF*0g=g%%lUr!57=6 zefUrqV-=?D+Cgnavy9WD35S&xQ6){*lv@}d7XL^#T>Z>}Wd@*&0EEYSJsdLHX`EIC z;I2+^6u%ye?l%BNF*yspgWYmMxm3-5pn0x~{(h!mRugo-P-hPFjoO&crS{U0un$0y zt16wzIF0WMaEfEMptn3o>iatN640}Yx~XQWkyt07m}{b{UTCwN+E z>rp?uKEnBCvan#Xfl1mCjuCL9(A6`BF}HcE_<|vRQLAIt7w!qT5l~n(i%#Q415cc( zmNG>+_N=qdn4b?L9|Ka3p||T-0QBvQo;CK2C!obZS1E8PgLp8#gxn~y%L9rC)DXdQ zUy8EZ+K{NqNaL^I?g>W-Lz$ovQJ-_$Hs-r1Y-TQ?W5a_@RY=ll0fwr~je_|`IVq{| zLx1OvsR)gSlTonX&4%zuoAATK;Nt&^;lA(-Xv=}FII`;@$$@{X`DQbrX1I)p30l|P zN3OeYvpMA7M{Vt9(grQJNKyGBp8%IkiJ<@5m4{;B0^ z(_QRPo}D_JpBT;RB5vNW5iRR#;cxI66J)8Py85c%37`RYzmoZIwC}4=b7}L2`laTw z&jdpsHT%=-HjD&M03N#Jn+u33&0&1IzcsF<58zlBpQ~Rlf6;Z%G=ZQdw-aH({-ga9 zu6N0`oIV5LdEzHSqL>{(>4p(ZNzXvc7L}B=9c3ylm{8DJ(5F}Z0W~%=zsSgV@Vm@* zdF8F*lI9xSlBI((Azq1K(%NCi0&-qQn=U|u5s{lztcIP+vlJm57knTo_%a%{vD}kL zYkj(*w_OJc4RJomRN(A%94)t9>byB>D%^holHuEJ zI%VGFDSNStI7Z@rXk=lq7_?Xb0?6>sb0S6dUI0>ZnzjtTK*R;S2yy!Sxe=bUQ1G83QCdtKkE&|(|N z2-+5vHpZ%aJ=%u|LX5L&=H73#J+N}2W@jHx7IF47xwqR1Uh?r_sdqyVyL~kBvna6d zkM%98dbHcrk@pMx0(|d~l@H8VSdK%Snu)1LaC~vm$Yr(}^8_JQY6Gwfqv?K3-1Qh% zIeVLr_jWnfR2=evKX=+)S2t-4=bI`h^b<_0ZU9KF0m_TD)Gsj$riwHOUIZE!fS2FQki4o{`ujAxAV6^vaYXNc#tb`)N5oB>*Q!~p8_#i)B){=X0)zw~ z`~-Eow}1BTh*Z<1o33%%-^er{Dao@>G-Th9ojzq4p-{hXPtGBP{37TN z??rKxWo5%B54}kzw?Y9tx8)t5l-${~cTAWhUGi(w(7VqJssvU@A3Az{OA48Goh43BGW4NT5K^0^JuH4-D_B*dAi4wn6nNr+@UOnZu!*5Xf^ZVMh5P;iV zvtN#qGa|+{@!X_aaM2CwHU%Tt2A?grdOL=M=&|otqHq8Ns|>Qmy_0>GDnm^g zG&1*v$f5?MWH0y1eJ2mjY5TNF%^Bx?8Ly*P%m8IVPPIUH%XbWfu?Em+t06T37`I*8 zFtD{6i@Bq)R)hD)GcY9-VYqhp;WTG0&FB^P}W*nA&801v)lyl?Ri}@!W?8dzIt9iR~6e z_1l=XlRRAsW+)1(o9?aXW_r=_2N`l2 znLSIdearKEv>bran^!I8>+@bbHBf3KVum+CXFZs6#-@3!Vh8Z**FAj^lVx;D%e}a0 zdTtjv#3K8R5gE_3{bU#GuKI?(&OETWN(%HTke|h6-dU%ytp+gXr~33odd9-@jzs~# z5lw%jEnX2m5j2)g*UbN|7V$aF8(nrB0+i>zVYrT2?1FI*v62cZYqCu6zO)EGdYyKq z1#pLKlQ8Et2_{Bkl;oj@DF(Wt@C%d&MSXRy-}F(aMCrg7be zZB+^a%grma&$F|&T_kBd%&7bMJsxbd_Wa0Xgpu>wvRz`^A&ZwwC+=Cfz$idn_~zTe z7)o2E>>$(ZP`@PZ@eONQuu`jP;RL8weKP)93aR`~=>a^Wa}0r#RjOD{P9#VLUU@qwBFd7XFkLp?L*tiP zh>!~EjFY@Elr_T`5mZ+*vJ9YKbq3?vyIj(rE6~oCcvEj6)Ek?TAo!`}7HD)c67JDh z#~5E-uAmY>!=EYMo(DSRoe(U!hM2V|M#AeYJ-YgiZEj3(24p&4FRrhl@H#2ERpALS zt|@kyaolhl^M9q(fF)hCc!kyQK{C!xf52)GBXX~A6GptNtKoPS47<1az|YwKrdrfm zeauC8Z)8>hI<~4jdk1V{GI-)KoY%T5D{g#%VMJF_@{^lI_`E+ufPxnydWULrh;hl$ ztF@Q9YOGpaXn3r|aM}gkI?$2(;oNQsBiZnd*Ewd1o5&3Nq(gi-zacB$hTU&z@xM!@ zi=#f1iLbmV$ip@8;BpV7MBPj;lj=wD`e42`{#FyFL#csTbT;aBnpG=JcAJQAmwKw4 z^G#n9e>ND?M9aYjwAvKe=dp<*Z`z}4?59~7&F}3xAFv}O)nj7Jij5(NhCLnqJ1JO; z#+cHH(K6=!IX^$&R#=wZC6hQPJT7h>)dn;xEfdLMIU#O{iv#p0h+ni=&h5OYo-zWe zo^SnZ&r_~zKZ;b6C4W4sLl*Pp5Sv`yLKr@i@>TqJ2o4+ZvY4(ZXR{npt}wME>5_h% z-e!_k;Z1k!Rzg`V-X>tT!3sd9uUF?G-CN2B(`G08;S1ayxbw<|&t!;NPy8D{cO z^oA@1i1GSSRw_WQIRFq{cRLPwfA^We|Sd-=t=@;x{gZs5l58s3;l#oNrUmNrI6v zHa$_~4&$IIR~dKZ#Ut~+A91%7ye?MW_w!#|Kq(8$cREyy&Lsw<-s{ z&rYY-0qxCqliG$Go7H1Kk=wK>x!(miNMHP>cmK+?0Qz2(#p~%u+12vDvMc!6W+-Gs z7vAW{p5k}_j1-s5W@alx@g9Cvit^#1ehN@z*Dg@->(fe%8$La+_zaOj^hK|y#@;@6 z{-IyJ_le=LhF=z_!iWL=9Xi;0$Fp)ux>Rr+r(M<7_SP$Y_3^DHmdo!Gn&rY$KVJzC zAj-%t76<6z0Nb07*mC4YKtI-Hta4w?r$dQGDts{$(EIi!<=qBQ_w3oOMfm{G{QE@u z!>f`0c(#$DlpKh$NyM8n5HwjLn48Ee{z9lFQg0kx{wz6qtT4+Z8exbh&(qG+m(`Rzt zrFf0f^z1723pR3|Y9UYI3cQU(PaGfx84u?Ux5KLL<6MoAEv>&^W)?P3n5eF22CSnv z4f-tb`kjiUm6a~H9go?fklbUOm*Gl7j!^e`L!6G3{`cT)4$$q4a(?|A8*~ z%g6t_fa@|&{4b9$i8iW_6VROH1WX7Yiu^ z7DJ@%74?5M=l|dE{ImD>e`t6p-^CLv!h9{{_UUTFZBV1v+{*kT-g8Yq5f=L){B9It(C_eo^UBt)qmrg zs9Lb*f3|-ooT z`=|&QxF=t;-~XPN|Lp|+m+N3+7j|-A!7PpapUwFY#f6=On07?|$@Kk8uU#nO!cN{0xs(C;CpZ32 z-&(fdg@cw131j#V9keItfP+T($^1W?b9t`|2Td@UTJ1k|&;Z@3JJh#_|K0cUYayka zVF3=Bmw;3u+Mmwfe`(~@+kk^6`6Ty0bkP34;kmLj=gIZ^*NRfjk%x0K>WVbqqNg9jPVrRD|EPG7f&Ga5ReGK`oc-X1F}Id3$= zL&)*W=J+6da|g-iG?sXN5C0mdyMS+U1IxPUD-bVXB8&ilf8I3Z(llF=I^?&O*|MHzyd0+{tI~==5R}uZy8~^vd z$j6<~yO=&Xr4RT2?%`+Q53ju*-rCohm;Jw*Bw(C9iEAyn?!GTL|GS4jM$^{T1^|HH z7q_>q8n2rR|Ep^x1&h#z!PlZC?2o&i{rqURWj8uUwI|MA0L~Zu00t*_*s0REyoaj6 z8$d2GT$l7GxFs|)QiMv^)d$ekwZ7YxW!UhzQIXmVv3>rg#R)pK>+T}+bR(;Jab-p9 zo;%*@@j?1fL`2>DDS5Ye4uzpCJumilprMQC2RsT}9zEqr$Dvv|z2TKoIl-@n?VowJ zLPY;~>|!xWF0PpGp1ettN0F(kt4ar3RV8IkkOyu$`zoUqI+$<;q`bcV)9UJ4g%{fQ z0cMgF>G?F5q^ztX&xeo@JlOj5hjkH-l$&?x39IoFx$WZ4Cd&htA@Nc_f4-G%*Rxm< zOAh+_;|KCKncHVitur#c1=Cukx+A!_t{i%;<#Oag!#$~ExUOI=0ECqPaLk)Sx6~l$ ze9phWX4(1h;X}6dX+($q(=F~J#Ig*V$AogcC!8K&?drOo1kfI2LZi+lrJ@-}APSeu z+{c%4cWCfswngQ3L|mNccp80BD2@xP$@N$QyadS2F~3>tal$)Fp?ZM zX2O@xP6lzW=ji6qRM*zb?D8GO3i*n^Ql_RE1r01RPgK43@O-c9l>9z8esv0UJS3IR zUj8KRTC7f2Hg5ZMIz5y&FzAhi6*ETR z6^Y+rP#s`++1fb}v^MzT4cofI=bL^Qcq1MXYf6cnV%|Q#-jI+mVYW`~XF`>Z1ONx2 zTq`@h5vTiVHreBhI&vAR(G#?ISh=03khV7C7H_6AtbjJK2eiqg3U)u1nP^Sjv3SPG z$w|t+&Tl@I9?+I%$ zh!tizH8&spwb!?)#@)RG+c6Sc%4?{9{)ohTn`u>~){Exfx|(uIJXGeShDF^xT$kPU zS+W>0Yc{`M&+@lFZ=<6U%9D+!d~H+Q-Z7eRfTn^!CId2;1AulgUA&(nwa{`=FxjWW zPbGYR?At0w)GgjAYbt+Rjz9|2a$BS1praSbQoe}*ssDSdsFQ-EWG?9=0~j8+-3#$I z**&)(nJ4d_p4%&^Xo~WX3)sfoy3dso!lKX;*l=X^D3aOIi-^M1qO>8!$aQ_BG}@uO z)lNp4I#!xRuVHhS$b)BM?!F7V@^5f?mP50lBP2%C)PY(`Pij;@XSdA{M%F%iT!6ff-ui zaqJFzN&CkO`Qw=LNtFXoXP?fT{O|Z)E}0oLocu^xxrbDkE2Jk}vn_3rLHEpzq~&Nj zd%NBETrcAYIqVHaLab#>DA(qCtW6<(!^UK?JXCaRm zjvsA^_x9b^Xlx*j2dpjx*F(~acxh`(W~cs~V0a!Ua*vE_wWOWtguwLdn>yU^8zw=Z z;rYr~f7%crSnptUX37|t3lyb`a$g12+a48dR`MCmEY)n@@>vBO z6lwb?y|Iw2iSjQeUP~01c&mG^+IKH!=jLbcK%aNY9d9FCT=DKnVuWiQ)hue(5LQ>Y z-I)@(X-0#Ghj+{KNru1#wXdJw+8HK~Qh4n)jag+yXJlTVUm`^+aIUFy=Ga*Zxbebc z@j_GXt}(Fq+>b4rZuPnM+PI5n-9U+jC2=uh$m7IU%8_71y3>~5$;qkUrr6aT`tZhh zquGAykZpnnk>`c4^6IDP_U6t0*s)=gMFt8QY}2abD5{_ri3@!!m#`^BTe06fxOQ}m zsm{5vGEukO5W+UG4@=i|d|KheZ&o`@!~0CCFN9xksDFZwpFhVjv8L0D-4tl0x185` zw))nHB%}#IfV|YCq3OU`>ZTWY1=#w7pKxa3dRIirs({g|9r7qQ zlEFbCQW*f_51!X^YC>)k+|z*4^wo%La3Bwz#OhzhWlNF??iFwzz%zRo+eCp5cO$Gi zBCnKbeqK+EWUtV#CqgHO| z@P%KmY6FyeR-WcXa}>%b^QsJee*@#X+Nr|$Sh%=pA8H(wlz=`Z3Ky|>{qJ?$2!^dd>(?So31g%Z?TZdyMeOn(1 zEOQo}?@sP}r80ST8V&HIg;oq9dEEKu5l`j{a+wnZ<>dI`Pb_E3`fIuz5=kOLC=oyV zUg{(kzHVx*hU~mz#QRIH`^gO2)F;GViR;xYqde5_7N?FTJQ-MgL28^RD9Nl#&HZUO zUfYjBailHWNLngPSA=jxic0ru6q8m?Ic#S%Uf_J5!6TX{M;h`Y1p)Q=;J7`OgX;Q$ z@3<~$TzG=jxw48y@H_%gV`a(e%!D8%U|&e?@z*+h#Yt*Sl8NQl!dXrGLF8>jpQ%SD^{g#XEl`_WMIL|>Tu`3 zvB%}Gew&|nk#3*)$?pSc>NVP{S#pK(+s>}roA;2W8_>?8FPqnUb}WRU?yz@CkHUsK zW1(|zcKrhc^J(%P0ATB}+R!n6jlsE=UK?UMg1I+7ZM;sP%2lm~7a|TxQD0<<+Cxg;gcEgW*UUi#bByywatnaY zFJ(qQ4OF581sEep!kgA&V(fB3XSC0rDWJcQ>MM_q&M6vcFl^^oAMbj6Kq$z_gNzt4 zvb#NZ!unGBUcuvH>CoUzSoIljkPFpEP% z&f}=e3fmqGaa8|V;S`W2_E{bme*WzNIa^G_p?grhmHEPBJw1wHntKhoDg`E$6!cf# zo$HOd9EQmAXsXKZ5ms~;i3ocjW1ez1R8%TPh`itPrH~nlOC{Owq)H^kX6P7(HDFJ z;4V%)AqQmIh1%SJ56Df2Me#5W3N>@DEiN{nYe9F6el(uH4!))Z z@?maRR8x}>DZB;ypugP^aA4s`((-Z1w-tFi-@W97g-4S#%uf6z=$~j10gmmwo&>S~ z4WfodV-5aEr>f+JLvu!`QfSX`SNbVMuL(D2!uhzjeG^-mZByg1(XN&Zj3ar=}GJbl9!w(F>qR-9&XNL<; zNKkisna44QM-X4gK$0anuY1IEmsU!*3R@AYr+Ye1P>X(%uD-{y^!bzNX-3eY!q=Xb zX5qnsMl#@vXbr|9baAQp^7;;eP(CN3cx0o*$tGieO<<1;m*Dlm&BK5NdL{FHfPj!| z$s6CTrP*k~4}>sM6kwzN7l4Xn_RYu9of8fl6MUtEyzzG&LFa0gDUVfT;@LSZN*Lj+ zU5{&R+irG=CR~O9O}K=sQ%eu&^#rtgz;-LQu#=nnhltc8nC|6x>cFw7|Ou2y*p*2k1ic5gotZJfn*y{Mp$pAjzbYxk-^p*X5-vn35^rQ6WP zWYPM4VnB^J%7{e&*$qvPlN(C!vl=<9QaE+JB58uoWsQBq99>Wjc3ZD!WP?WX?`eMF z;Xh2#NXcY~yvZ~{J`@4`-BE{qQ7Se-72P_^L zZ6_aOa*BkFe&T39IV9J#`Oq_)aI+(oE|um6zQUQ8knr3+LvPClgk^;A5h!ZCDGxYxsViy? zlDen))#%fqdpGUD3Wb-teQ>t6h+s%?c@IYhJ0Mu2%mBty(PWWo=A&BHq7qZ^dyjhn zpD5qxYc~`ag+#~l?+?%$%YZpd#~A??MQMOKsQB_R(eVIvHKe!9Wt05n3mM2z`c_Zn z%-3FVHy1cnSYhHNoL5E4-w`e5?r zfdSOexG0bNYUui-hJt$bfW|CK$I#GFp)A=X8xCqfYWi>f?ra>^;o)H)ksN>8ccbDy zWE$QKWYn3!7!u1L2>s$A73MHCb^k_1&#`#5b~VtUu7ikn4G5IY=EC$=)8quwqTh$4 zaNCh&L$`$eCdy;pp= zNUH1uRzeUMn-ZJpihaI6M-JB=7_ExZ)9uzPQ-;E}b{aYR+bKL!CJ-l;TIgZjNvv`= z;yR;@6*NAR^Ek_+Z$;6od`h1QVP*JcSNyJ;9W?bSQL=A?MJjq-hMZR*CHjNF{$ou1 zc*li1Uxz3fixxnttaRLTK7`>iK5YZl;~qUQY4n^NvxCJ2Kdz_T8a<_3&;mB=nfr0y z?&UppzV~+r7Lb@?e0)=Z@^OmlKIfT%LNMB(xQRE<7nsEJ$9FZRjI7YtoVsIK13eR7 z6Q=J>Rz^F5poP9~$~HV;M|Y8lrxeeA78!9a=CFDyKT3D~QjJold0zLK@B}$T=Z6nUS(wt003#yuar+ffrl&s^1%L1XO`L7H5)VH-vW+&C(AAwG zJ~c2jTpN@VNGB+&D?Is*Cvc)n;t(IWVO*L_C3Z_Is0SFq%!>)QD@GmQ9>SAf=gNhS zpGsgjS%l{&%~K5*jC#|!BZ3|5l2*dTU%E9^81F+}&yVc9GKmxJN#)x`7n-+H)%VLb z&#u3~UWTZ|N@P^aK~c(HpSlJng~49bMj564T|UTpBT>QCC@t6St*K{}te19D_w^>m4c65&lh`yErA-OAwQr-4!uX;fpMK~xdj9bamy0o-( zCy1hq=2H+*qSAA{r#0!vn3KRy!XN zZ^CZAu2^bhE{#!f9G$FP5v<>irVfOGRYNJ&o{j6MI))8{w&I30zv!JE40kD3g}jr0 z!0mB9S>H8}5)|)NxXlNgs)26_hXyL0oLoV>X>5qBaaS$B1akL``7W04V|CYfo#2d2 z`fu;x{E@Zr_eQfauW)rJggkPnb=p>z*{+-P1oE$D(+7saNb>!ap23yKmhI`lIJXoJDWiW-276#^~*8>fPbG3jJR6TDe$Cx^*9UhQ|LW*B2sY@uYFe0`T zRyLMFU%52W^7dGD$1}&Ss$GDby&zfhF)Y9Aw)I)sc;}JuNg&IPQ7WnQ~NAoMMWX&B88W{J&2zurQHX1US|>%PJoKhj53{WVl<2Ogpq`` zz5Lj>cvA5wml8>W$n3|a*_@TVi88T8ue3Qf+GCext^%nhT!J@Ki&kMcqXv6!W~Wx0 z?`a_IvnjJvpqMaWZBQ399*9w?)=HmI3|4rZ_nWTm$*XyOUgFBDnfXafa&tf|^xF36 zF+7hqtS4l`sR}y3{dm9H1lS9spDttN+eE03GIP~S9sy#HZ-W9zUa5#2x`p>(F7bx# zNH(w@ZU1rBQOd`lF{X*!+?R|YyIKJ$HaG5;cEm)9GjHn}bW{#x|k*Ky!HWVdOLFw%aERcb0vM3AG>k;29K5mMMsB$sv;J z%O7EicK3BD9JxgV}034azlTYfE7>Z&b5&(=vI&HzefW^G{Yad)C)C zWd9DXtLkq|jn=)Z5&lU|%%b_`ZK>VLGR8;8t%>PW8QGhiJE4s6Irk6EEzQg_X9tE7 zD3vzg2lh&}&7ln9;O&J~_l+oR!LB;F7rthuP{Nt0@w?r}w^cQs3t(ZF0GyuzU+S*a zo=P!AAiGns%T!QAK}eXHs~!6i=mwtShIqh1;dMSWI|XFvH08efW`XDxP_Fzo`NvoX ztNr1n1Tp@L=b-5qVchkUjO^^A=X;P2&$TzYoOpv6FJd+B5$n(^XA3QSmrIXhmw}`` zCUXOUaIjN)6)~dQI-~KjZiIWL#Fd+C1odpZTHdzt5*wCr^gDY!@U8B>dYRz)Ccf)x z^KlNg*L#8eh|c>C`sbc;?E>3?FKYDgoVot|W3=92*AX2W2F5zHBa$K75>1bc)2c6P zZIZcFhA0!T={?tlNQfasGdBjnHh`t@t0hB?iC-Dj#<1fe_94ZI)o z*dARacdlXG#_*YnzzciNS*Ll%)6kzfoe3k6O;n&R=rGoN-?(NhpKByxL*8q@0_jx{|3_7cS_l%I9r{QRZ_S+)Fp zbC`SAB+@rzm10jy&`d!zqT0(nl*H0C~|8?ZyIGy0pVJoWE;BcPWLG`y$J0q(E za^Vxgk}~D46`6~K{}Izbg<-oRDXdqEc)V=p0r#gkj`|RO84~1dt6Yv=@`J}3bNhxn z4d*_jv>t*dv!^FGC6n!6ijGFeiu&jEq8KRXAF~Z@tBXZ0k3X*)r*J#%sbs2&TIA_@ zjD3m+l^c>Je_+}hBTBx*Y5eu0`DD^#t(>ve5S)Sb?s#4?mb9w^2&d#My63IM1;Yt- zlb`tO%$Sbqr3we0}J&0VNhtS1|Hp|8*iS65_wLi%pOY zt}cIahZA*=Jc-XG>{G6u;*_&N{crz13N5sxGYqr$(j`q%C;A~<_pK%OTCcS65s`Jb zG*#NiLdiHsO{Nh}7u>*P!5!0jr^Yk9JlmCCorBqM!I6T&$sX+1Br-`LcoG9T)+?Zt zYsNbT5DwLeNX7kJkV`PH3sdc>vkUB%#8=@?^W(V8SyZuNsqZ8~eLb)@Lbw9}LGjq+ zmYpA8U!M&yek>(fG!`)NsThfS-obU@?V0T_H$G`&REj>d0z_c42#Oj+ts7Yt8~s0g zpz{{Z3+={t@c=&$G}-;)i1)`nS|q#+RIqf0*i1)2dkDlz$~^ul(cQc0PPc9`7p$M2 z9=Rk$zIz|{Tsuy!{aezgTx(|>$9g|Q_swNU;ye){jF`aisAN}avpaT>ZEuUy64h1k zq!F7!%;49^cY+S&qv<`;-5I~JyMhuGd9x?j!9&0QV3t4m!q7`}b~7K-*T5FHa!DQ`sK zvG2(MH?KN}In#eZTk2(4co9JVY>4#_y^iA`G0Ew1$nPbFu ze;2}5Ri=l-8Y#svai8<#2veoNfRK2)Ydr7l)HH9Ms##3QeZ} zeQ}Dq^Q3w!Vh6x+3jjKN$f%5#s#)(H><+OifM%-tQ(0s$m)xu<7T-_-rc{J9FYE}3 zTy4yNFEc8%NLil)S+JVEN|lX~gS|ZKY+N+8A`KD4#*>yAn=FU+&-cG#a6_^rkzB;w zZgG~3;YmrEkTsyVz&oA(;x!V>&NEJA+BK$uwF4yZ_M|esexugrj4$!?=LQLA)E)h^T95qU@#pWoliIGi}ghLC$~}ebabo3;tA^HnZrUP(o!en zm6apbt3Pu(=vHnc+=E__T%|^RQl$YDOX|s%zU8IWvmr+dx?lVn0w!V!6H}MzWUC@9 z>06gA@v5+T=rZ@PwzpHZe2Htrdn{mq_Q|tnPVIVj4`ya&7MGW00kj7FyTb~v$}sm6 zE^ot;gyiACqUR>OKY`F%{6^m5JM}>m#1|v};)>%qsHx67yWe-g8zcmvPRva`tG8Ok zZVm2|Bsqx-JvZvyZUw5R^h%i(;Q}YOh``s6@m4p@hYPzJ;>nCnOx7Ki--Y(c^W&?1 zgAu+0Spp)XDh4Dhy6NcqE4h##3On>~>xVcE1?Buz3|0&cRf_H9&6HFzRm@;wj-kEt zDSBcX;PuZ+zwctw-l#=f5 zTBM8aknZm8M!Mnr_OsRfj_;iFp7;C5v1E+x*xc*Bub9`o=2U+!0yTz%`?n`o06nA8 z6oiS4QTphSzwexwn3(;~HUTiO1a`LMdO2k9JU}mTR{!Z2S&WhzX4B;{OwHMu11y(A z4gxC%b2}>_*~K6#hudE50_r64uvUYxG~7@xPc@f!_XmYQjLKzg)YsU=Upy`c1r$~2C=5#s| zj$YDP1UOG;wOjI~3u8GV_Fq8U_rA$^>=oKCB?syNbR%)uWg&Kp-z;In35{I-Rp{wR zPxt7+k3gU>tM!BvI={^N1M})+wAFprjwvV*bJs5q)US#CCZ+o~6ay(5d5LLg=D0s8 z7X3|J?JDPo1a%BaK}4%8W(3OT&sV%$T?XQZCUXkw&vyud(; zl#KXR2my=zkY;2TD=;%HM545`~_`cgjpED-t*=#zTzz-{y6Xf)^fTJ|;B^SjO!b8)V>ml+p?7)cz zpdU@i-2{c`zA1-0c6LtxMj7|1_W_GOwbogAdoZIe{c^E4vEvHG@R{9j`FWM3y3+?* zs>A6tD}MV_<3#uvUhkh9`a00tT-Ual9QPcsG)!fqC<4nwrKWbCP|8nk6O(bc#hFLX zUpU29>`$`9bCdkm0w_`dyt0K}zkaPm9}FYOd4f#ZIHs0z zh?ANeUE&YQW9OPO(sq|R-+q*YclJY6OHG6+|ztrRQ7yL-aVJYv=eew3;FP$2YLJkig>k5J`d8M3g32<7N9U}{`BC| zPyz1ax!%=WbK0deKAh1WJ+NxY^FPnZGT^A(V2eQDkj3-wml_&lJcNTZAb!eP`J$01W{O>+PF8Y>xK)pO>aUOl-H_xs`YV%44p zHI1p?*Rb7ldezwKbv6K5FCcNQ?Kfga7M~{FK0bz*i<3tAr_y?XOnC*0sVGY zwJ179!FZ~F<$~ktcUNckIl`Lb0A6amy|#c!eqvFx@7Yp|`#KS5mK?Y=V=j6~{3SR1zfZ1{@k zCg-p>-R#uj4`M2iQRtw?&NXIfzuOQa%9JNH8CYdD(=kY#(CSUZM1i{G z)?h}(u1YTj+Ygb*F!Y${=&5spA(0!aYdQo1aHJL5#6Lj$qAnB=>9*NO$wnwb2`+0zeJ_uk%rEsGk&xFm277`WurSuQnf&7Exa z#asYF&f9Y*u3W(8cZ#o{L@{E(^OM`arF6_r9nt4>BER-H%rjVO28J#(y_k(2nc2ma z(~bI+nOWv+s6Qx^%HIq}6?Z8W(qA!L1KOv=9yqEw8xe!@e!LVsuk<%$5(-ySm8=%^ z&6cgvF!e~zHz<$YPjhFrS~Ya;Y?kkvuiFEu`D=lla%S62;{&?CnVkyc&zUp0N#~(I z(s#_2`J#cw%oK~SKK+)3EV9ebiOX@L1oq>f@_;LXUm5eH?&ou--~-8%A5-YgA&k** zYCfQkRoph~mdu*-A%(5T=Ju594XhvCD<{Xa^~D;SAFkq(-wK>oo<(m})&qg)q2f_; zA_1Z1em-QfzdbVu&Bx=aQ2=nn>d%$>aTQlNvc&(XuLiwRW4QglBB6<^^vATtKRo3L`N&V9aJtC zRvFa`}-Q#kpZQIynwz0r~3`^V~sn9gRVf0O6h#{ zWY1kbso8YH%8Z&a7MSmg9RnQ3-xpM(T(dF+f8Bg1dFa0sJk}{_k&+-e3$>`mz=uXg zP;kIa)PKer)WdDu?28Dak>UhSS#5b3IGT09?EEwJ-CFg3C-8 z`N4(#`uR>q0WPJ!>FR=tXAA~K)Vp&G1b@neQexS<<&n|`8V#&pH}O~ps{2qfI#Cl# zdEy6>9r~PDLfC>EDelP&LUw zS>(*`v(*@neQG`|snB&f4)=S@GYL9LCTJz$qVO}pgbSFj#J%K#RF-=z<6d4KF7*uK zamBqQZy&gle5h@9yBb%Jc~f(+(w7L7F?qhhx#iVj@x`YAmNn9&V%QMv`JI8`Vt(KU z73)>%iNZo>c-Eau)aA%&XaK4AO zZ2FLxa0ws`FkAA$uP1}uCRMoQk~6rDr&Xz1jb#dye)K^-zIImZVbQ})>M0?pk#l*4 zN;@H$?b@h$<;tsO-rPSj3TZ_O&i|jn=Oecwr)HRn6)DFdJ) zZN_F6@35lo;92SJpW&ID*QysQsk`2znsS6jc7V?g_a7vYG8_i5Oa?{_+&s;@>5V5! zs&CSk27I95F3jbnl9|_}mioC4jw>txNmFKreZ|I%tpPhG1+LV;5Cf!j&Niv4fWbs` zPSJx~mNpvJA1*1S1{9s=lfVs|=Ne=8;Xhp#TQI+|D5Vjqc)gf1O+$iB@u)XM7b%+P zV=6=!$P3g=-@}V>T_%l%uzPAE8CaZp9<$xpURE;BiS6#(B5=fZ1OQNqs2{!uyEdAe z&9vWqwW$Jf0SBM`Pw%kA{xQ-osoBj+VQPY~SV;P}>LZpFy9ryQNywzVN>I-KlKKd( zb6}WTNl0k?{vIvf$Af4g;v&Cu=vc2p8vzF2fD`78ulunZs#Z#phx%L~+}i4zOW~oU8&=}B;8mizzr8JT*<3l+1^ku?`S`r% z>+Rqy=Brc>3~lTkSY-KikTng)Y&=)mVG@w?V>k8FmMTYhi?=oEtckjQ3Oml|iiBI! zq95j?{yd`=VE8_B#j`Z|a$Wb=FDwgem?ZF3{YB?b^$T$TVgb?%tZUTG^*xC);*tjI zZnr%&xHn2OraQywWVio@6#mQ# z__mW?veejE!RYpV;X`|S=Dr=ZqqD7nDueTUGrX@)e@>l{|LkE;*!>-f-3eeaCK@fd zLn||LZeA+7{08&ouPV2e!KbRK%B-K#1vjUy^cMo=O)TrjCXW{5W!2N!NzDLitg#U5 z1lbNsXplW)mZ{HF1zTC~joSlAFN?Oovx%oR6Ih)-8u$hVaEI`+fhQYNZYAb`jiP?E z(3mi7HEV3A3UFdg7g$4>Oao#X8v{+TQ09O#$l@+pd9qYJkeGy|uT_zX7|YL8TwFZ2 z+H7WYZ=!~Q(TBFhbfSSQ4xZ{W9bK90vrs5zicBcu?=TO{6X1b;D~#as7ooq`KcGl; ztjVzGC;U)RRIw{jZ7JArs19tI>jS2;B{5YEdd|b`9zJe4GWRX79&8jl9)_7NNH6IE z1IZ6+?9Yx(nu}=v{_B6Jg8$tIfQwY9SQMSS*5>24Pl*i{lyvy84%QQikk@~NMb~Q4 z$bv7fTA&?TFRiG6>?^{6V8d~vBZA5x&Y$C3pCF6x%0wcL(b3UM5GisPLYcJd$DrSG z7eci65Df!3tIZS!;!7`ZP;~p=5rhx8>}FY384j6ZI^j((NLYV8PUmSn==f0oHK;h( z3$WF`@a(Ea#+dq=ot-^-1_fr_(cHqGW{$b+(^QSlQsxpMs#o1Zj))o~A7@z{%(sgjV%E#LsSMjwsi=LjIz}-`h zBP#}aG5hsSvFO7oU0}!!18RM&@dYZ9u1~berjr#!EcFL;Qw4QT3|OE(o}e~rVq#x% znRIy#EBJrURR_3-YkbhZ>dm^LSNfV95|(i|a??r8q}atlPFY0DRKiRo2~71vtGI$? zc_frH+sN2V^3~f5+wbJ^AfEKJ6gjGV!?^(nSi26jxtZ{bmVI3|a=$Cs-{Z1Bsplz&~V^G5U?DT1*mkzBqU&kmOX%40Ujk~dt9o%q2c(n*Hac_7Lg)R zlP~Yv*Z;;RS#)3v5rp3Ivg9{`=yfE&<1cs2A z#TC$FKYXG*I4fE#QF85B2bA3pIlyXpI9;9Hqkt+SyNR;&beX!dIxx5S(t6~FiBD=Bg+~y3PvC6X5UCiiA@WE)HZye z1MQ;}t*r@#L^8fa#o@ZRx)#v^vSkwu`#sN33&jy*H1)`+sM*pvIWjhb5@P4%AnB1_ zS=AO_Ki?@A0Q;DfgMf~f8b|V<%hI?)t1I?zPSIGe?H_JzQaBa=JSKpF0tC786NKWI z|J(wfCU||LLUWKXrnCVXpRCO#VSl9JLVO3Y$q0~G7D%d2Uf& zp+0F55v;r|oe%#W{_ok{fBzE>nCNI!#iCri7g4ZOeCyQDe<^h5!wm1+m$I6SlVa*` z@qMLWp-_msbzoFE6l^+WJn0^sB4QnnPM17rm6-n^ogD3EV|_tx7N1>zP7#2H&?>WS6mf(YN_D= z{onsjpAj-8DHK2jvj@f;0P89jFykKWi@9RE7?RMyz{f9Bdgu1!PE>j!Tu?paZeS&h zg>_uesk+$zskAef)NM&|sS5#XYK%;zcdKHn>{AVRVHKh4!|yK^&Aa0D zGxo2)wOL5EB?2tUTNT{U(!~x_x$U{;SeJF}?8Ao>dp#bEf03U*6~+JK!>S{p^~J87 z={RX5U|)0~t3n10_%ycG)`S2PK{-n4jBq+eQC(eIA+0=)Mny@LL@#fqS=IR3-$x zC6Fh9(;MbBHURgoP!;54I$~i_CsWN>wz;JWxIzF`kmfL62Q$%TQY-TU^l1x;59+MSdRHea z1)a*$6m`L2fW=m9y5)RoJz&nms$8Z}P$QM5*x8*717sbg>V@TsFVJB5yw~*&4d5%g zo#CWAoBZPWYO}d-!QV=(6qMi75EB!N^8SlPEcpFJTx1n-g5vZK%|uoZ{+%y%MFp4~ zJI6^@$<$Xov^2iI2%-u7qpy^kkCweThEbu+Hf3y0GsqfjYoctbJrL9X-5cy5h~RQo z?)(WHHLTS~u58(w(xng<>)js^%@pLA(Vsc!haH>zC3iRq|6<_So--q|wCu~EWI@Tm z*rrkqxq;hVbShV4%a4Gp-|_eVai{*v7PKWA6gDHN6kH`(44Z|MEqzCDOPz`a)g3?H%LW)7^=&iKl%izlTclZHav9ca)LC5 zpGJ`)JXYvjSYHCw!Gjzlj54X7_fps~tfasRRIhUQ+#ZBVTp;q3gOu%~HX(g1h^t&!y zs!N6i2Vs2TpMD9^4-a|NMkSo+DMISf`m#7?6_7f0TTX;xh%;QGHQTVx)~Dj|+#L20#i~ZYRxw`_rXTJ>mFy|EXPLMa$kFY!Ix4WE+T$qklyfS4i z&(t6w>^{+YdHM<4Z>eXdPY*9sUBGAoU%ygj#Tci4>!wv}bWH2l1?5Wy0m|ZzDx4m86FAHTp|z_FpCZpGi9R1KnEg!=wAfjSSNR$f8Dcr2T2vewE< zH!W_+ftH*~m;wa_rOUWlh5C0~^yYz5BAUKx$o#XIyxIW&^K-zVKZ5>GBvLFZuk73b zQ|iw}A03$c>k*#cpZ~zT{OBj7a#qoy-=q*qb8^_xLH)Lt)|2q_1y*S8(^^Eoa#siS zGV@no55&!*wNGQp5aXrxH~s74b1)QPOsZn})YqwqT5nh*ByOnPn-4>K#5y$%ZOc** zHvVVT$DgYMd~hQ7DOIl>ueUQQQk}?0N7qOu=I{gQd)(6kky>P&kBVx}6ON)9AS-8| zZuD^NeOajT{2?42L6Y+EOY~Cy^?*LikT(E&Co`KbP`Fj86bal{Qm%3myC6b6;YzWOtRIE!En{JhhB1 zVv)G&_rShH`!4&0l5ktZ(z1ET#5FZa96dszpbptQ4T`dX^*l9nMk6GWI<8VhQ)*c0 zQQ!AQF{Z5_RO9?*fTIxd6gWeiRID%cC$;>izNyoE&AA^NUpw%Qo1X z;C$5ii;T2$B9=;_RPEde*n9X4uxDUORV$OPj+cy>L?2#zd3iNhulY))^}uL&v$eG{`20YbJ!M09A)J6unm9KkBt%{zY1pfE%!C2|_e5hA6R6wvF}+^Y zasTIGwOL5>rw{yuhSK9iD0GMF?OM%elo#s;AJ*CF4@&NE%Rlf9zuUq=mIk)gP{__L z>CSw8gj8Tx2yIU<@~Y0^f;ZTT91!1z1dn9b>5~`$7=iAm8dS-m9X`ipF;NKh(Ojfs zm59c)4b4$71wMt#XlBykv94#?l$ppm`aB9soapdrW`|$_V3jxtF+lJPa!3w2N8+1z z3j4$0;crKEK52FW$qwX-tti;r+qX+=y{Z(mw!S^vx-~FB)@1jJ0=m3DE<$q7=4hB( z-p7lHeKM0YP6y+|+3(W#J5wq^GmYv6@^xcAzr0jv?__huJ6%eKREX2^(zO@81)>#U zxGk_rdzt%M9*5^{$u|+z?oVMW#P913_%@5Mm`y8QJ8bo1+^vVwe>j$Vi0vWp`UV9J z4SKAxSh!e9|G;@B1~iz|nw*I*=bM|oPjgzMiq$+@XQQ_j zp%U7yUTxX;B*=7jc`D}tu<-Y|LyH075g7NO>$?fQ^j%#!)apf z=pS|;O*P(^YF2&)YE%=u(!xSkSHemuJ?So>H{3m(^Mzk^v`75)jL-?7&a9N^>C17o zehNKZ*SPD{hI=>fysgz`0g#{Jb@0libDsKlSPit_w-;DN06OjpZU)?6Sz#8ncQ_lU zrx`1(Ll${B{^|(RU`HmpH+>UY`XKcvLjh;z|IZD?}DGgbh@f&S_ls@`e->^EMBrc{)PjR6b?jx9zm((#k@3XFQL1Dul}QzBS?8b zrAhgvVc7=(mGrSA^hfW~&{C7yOG$~Z!=t0f$-Ew4g;HR|-r{k__Qz6{&I}-pF^a@d zgG_FM)|ih|Y^Y-brUklH>?Y=`a(pt}I)42xuSyd|W~_M0$jG?bkLF;m8yUF+-CS|9 z5PzQhX%@vRi zMnmbFMYSQaOPuf2rWb0h2uh2OM{W|Rg@E##RC^^lmBZa{nJ!>FZ$Kq=f$KYkVCBqP zhkw2zC)gK#O)AJ8U@kwUu@;G2y3lBj1?C3IQZ%za+~wXB@2!-~ru!VY-z7&I3+l&^ z3hGnT`uHMgI{am7XpBIJyko~p+bMsL<2ExH5!+*!OkX}(;{+x3lIWUG*=9Qf+}aCR zFtUDTrA;b|go4Z|x8l1tPS^q$l zb06w_i$!A>o>;|5LB=;C6k|LN+s^DGS7}nc_*o!P0xHsEmn|+;RzLcj)(Q7e|Jv4FMtqJhylt_{9{Av(_8~6EJ0F(dNrxipzp>dl}w^iXLvMkt{@{k>)V?FbIozXa%tQ@w9iFL zn6tY_tg-Rp^5*P&42JLD_c?-?kg83SAZEvWI;d7y{u#tGXat`gUZOb2q;ZBxr?Ly2 zE#_}L;sMi{<18?33E=IMkKiqm}I0?IUaF}%g9jlwM;#<-1qh@q%=4l z2m^dg)dm_gz9N~7Ww1L?eci1*lgGUGn5})s?2H_@Wv&YF)G|vC-4YQXK)RSr1co-F z54Q-CF{+Oke2Bjh2OJ;vVSh^8F!lv>p@UGpF*3rIVE2UoQW{adfqL(X`0qqi14@`N zpB(jj8!&(&sE3Uu({sg{>Dtj^6`9o6Z;yM>9#o98>)4^%6HE!$AB|2ksj?Qq`Mayb z2WB=uhPp^gqRSqrHjD_pto&FJsoK$H3`fxw8C}BrkhSTt&Q1rL!>1jY!ioL+2LM2; z00tEOiK{$K3jhPolBKH$1_*$W8IOV=H-C%{x>*bsC#on7!GSsaIJsSKPn3zr`=eoX za4Z2_UhMazg)w&l0f8n2?c?D##&QkqGU$JQ4K0{Xf~ZX8TMMTP;*n)54;`J%*K~a& zOCiswO01IbQK^?RWjVK8wpvB!+g+Hz^9BWP#~c6D$JPF17|LBeLtd*)Y&4=q zy;9@zG-*rB?R*nQp@=)JS7#x>_WMmdt;2^pYy9BJ@9xg+aXB^fHwiMugc_1T>Uh)p1+`t)9g6STP8TJPWHL2+ z@2n#L0c@0ntPuaw<(TEm0}Mrul+={jwCu|d8*!%e-ab6iWyH%3wBq|y0T@pbJsgWm3QC;03rWI^4S1>w6W^c~4?vmOne{z5 z!nOu-u{5DMsN>)ENf?tWZRSYTs8FQ)^q}7!A8)LEvM_a)RU z?vCXs%K1t**zc9V!4??h{_V$Zf(GYbWq~VH>f*$66c7%x_0&!8l`B>9Dd6=5;22eS zR<$>=*ZXC`{1r3-w&)Z`Az*VS^zttWBbTwMMi~Nd*9!*u>BBcC?6e2prPb-$^O{{R zv%yVz5X^Tp^i!ld`G5$BsmKQ3^CEw-a`||E$8Ux#f95MR0KYo8KF^nh+!JV}0&|<5HP4#wD$bG0&TUm5XBsOI-#0SgY_23>m1X$E+e6l_V zg3V!31w*XHoFjxziT^sq#XY~_@N4IIa5MBLv`<%18YkZJ(R}a%Gx$R~$ zT|#9f?Cd>XAw&A9q^@o;457;H4j>24$*ARYJFe5dh7?1BAdWW36&e#GBQn)uvL}4c zPsLFFxu2lI4Jb_PV$AwP%rADGK>XjE4@4VZ`RY3v&4sBl!RJ3Y%-P;0l;qcMh021l*WpJCOO?kph zJF~GZJ&2-8VKy0Dned<WTA!017WoAw6bd9kCn5TuQH;owShrT@qcOx8Jwh>n#5*Ehg{Xss zUxksB!Q~`vFdn^woZM$ea;n-1Ecg_F4tfAjuQ9;J3p`Ep$??b|otpxcU}L>2w4qHM zm$W6gi7BRj;1o89%W?CLXqltqbb#Z!G_3vi%}qs#T*0`-iy5*S(ql;t6Hp%`BphK7 zR5ZYoQD9aI1pNWr7wKRmWmqi}8rwa7{fr-ljF1?7xJi>*3WfqORr&4P&+#-{=1S3H z{j$`t5VYH^K>b?XFL9wkC9{Ie7^?<^;R&+a{r%wNtg`p3?YhN^Ko9Ip-5cnXPj8Yb zpIW+KCT?lJFOO-%LRNrF6?zYPwTZXQo)Xuu(^Y9LNdh;I8gSbv&ly@#llF1QgXy_H zG{$KeyT}MTC?o_@*A!Glx(bq<&n^EJ#Sa?V)PPaeoezCi`k`F!`NbnpqNkCv3p&1t zl_`+Ej^Tr&V>6pqM4Xx5+?s@b|BZ&;`RZ^k$8k5t8)zaerc08-D#YMsZN7nm*j3yR z0zQ)Y`j|2AdF@pQh}RPeGF44dT*Z|ZHsemJqIEN=vrjBDZyuK zYRq@aM9Bb@8I^A!DT8R#vBB-3l-N2EUoSHDZncMr3*PVsgP4{^)jBfrN5N6L8p9zq zo;DRm>P5Q~9>q#SsjVh_y;hmyYPpTCRWW`hfH%|NB5_<~p?2#NGcW*T3A`n6x~eQG zb-%Smmn(_}V|q#642S@-b~*`Zu|l~dK4QAbb(;NZX|Y2?51f$DGr)E0VjHlz#4x7y zG^(}K>whfz7FJM6}@ac{!3gv<_fqSZ{rL=t*+%yLv% z>a_mJSS9FA+Td|M(boO#TkadesWMFkhR74K7N&abzI0ae+4KEYfpZtxUz1{=10q5w zV>^_pW)4!jAJrDQ9MCSN&*)>qkIw(76)%Z7rNO zl?@-ADd7V67ztqqXY2zoA{W#0`o5ua=1f(Zb>vdp%gPqwUqA`1U_NAb%9G&S39q?W?oC zzd0tGI4V)!-XkaBVd<$+z}d<>k{gwa7aZnF=>J^DTk2QbVpaNtt)jAZ+?7-|-rF+~ zz&U+INYB>C-i2M#8_*B;Zg&%7IgX4sC0w&NJ4i=)UvcL1)V-}CSnZ3YeRLNnAqKK1 zYaWjZ;*lNsY*4%YQ3X>4+vLEeg%zPhqZEq6idC>r0|P})48CApRfKOKEFrIq?`dJ2Z!FxW)L~Z5+C_u5beAjE0Th9Y@I50}6=*^NB zr&NHwC>&tkubhM`$WLa0dQ+tO*ei;0LAwL2X3I_iA|VZhrU1r9{Z0jkTB!se_4Ivi zbmMSV@%8V*UoSzPA1DNf@MptMDT<>KVK^)*DlGg*v*n!Y|D4_5dx%jKgzbb&jSk8O zv43Io;pe^Vt}&Yl4-hI9^_A};h{O`wCSOkkSQo|Ll-8}7jc2_ZYz&D}RH}`~IzFjf z4l>EmFoeNC9xf(i?0>EjZ531*FQ;a+lN2zad2Rn(`%&e5s;gN+@<`xz$Up{;HXBLg zP%z+*Y+0`b#9YOKz2@y4vjAR4(Hlhoxso$8>tDA+p-NKEfkm9$Bf=C7W*4Rnd3kZR z&KUes6qhyefqafZ-e1TzBALZ>Hgc1Moa-aNJX1F~b>E6hh(qp~Cz$9~CFp{S^~UeB z1;{*wb=OBT$_5DyJL!#6$p!^G*DKF_EcTPTQ6YOBLZQZ7_g*flfNwPj3q{eRPqmR$ z!q5FVg8S|jC_E?=32%PxRdLAd{EOY3eyfr`Y=wZi%h~EOk%tBRC8_7;3~q-IOgfc9$C3DxYG9?b zzvrlDaEmJv@6(V}OUCDim9+>aFU4ywz`?dsdogKWWLjYfFre?o{!~-=T-FF_T`UAJ z$sYsZ^JmS!E9Xo~6yz1@fk+b>+0=~c=_0c`#7WPUa~u*+&`3HDh^>#1Y$J`WRkY@$qwn30$Ytv^d{goRe^Sk_xm z{^kV+dq33lwmO~PvC%vK0Y8Apzo-gho1<`Ca^Jo5iww{vMWiDcrrbdpj!CGuF4Qj_ zOrkfAF)Z$&s=1OF1#^Sc6u!sc*iekq zfd(Pjg4TgHnKFR?^u~){t1fV;pE=Ju|JKA zsrB;qiqUO^r@V=l(^x^m-afXmsdSHqNiyFXAehbh*^{@e z40$6Je8c9N#&d?aU0YrU7KLC?bAlFqS~*v1zl`(C7#SfjeNK!P?4)1#J2FH*uyn*NZsSiY$vmbBy>U%1^f0^6OKO15{w7E z+9qz(M2V_`x=5|rXe=hhtNyY?zT_;0a=C%YB88*IC#nSdw@ikTwMaOG4%#Db7P3?1 zF=ML}GJG*_d3Wj0w}KaG5yS6W*_I`%U4|loEj2Rrzhg(_%X6%T7Z3B>^sNuNaPSt3 zb?p`+6bPej^hXM=aGK>>)oMVQWN`qx)GxDs`$mt5WM-m6|9LzcyX9-L**HJThh#iG zAWm+t5Po-DRj4#<@tJir8Yg&M>G=8O$59)V<}@Mo( zUyN;yzq=^$*}$1Bhfcq2eB8%B*-ec$*`q}PRU%bp>Im`=X64yYI&+pDn<1CJ4X`IMiX@b=Vt^J2x! zmhNTujSCrQ&-NMRyEdHI*|o-BX^)K9=a^VR8w@Y6q^CDZM;QNidlh8Kwm0?pzwA=@ zmEb!qRB3}5$gTThI}?APT1{IpsDdTesl;Y$yI@q0zN?mW!hpJtR6f+7B~pSlP_}vG z&jc}$Lv$ z8`@+J1tW6DJA?-9dpR}cMAP9^4#&BBqq}d|SMN?(PtL$`fPjJ{2Q_tn%fVkNY0clA zZJrM%2-9H^_ZP4OFY&9=VWmtQNfEQDt$fpJEG`X58!` zsMjuBQ-i>>Z{%IH=KB&Tx#Xc(92`2{mh37SQjLd5yij=7$|lX2s)_ZOpU z0IFMT0dd5nwZjwIT@~zj^o{4S79OVN9%^_!vJAnZn~+1S8HI&}MCK+q;(d0e`Q5EP zwj9qq<+7#38#azV*H9}Lk&;Q=xsNbnlQGIO+VaHF+(Z2^OnFASoNp33!Wz`pft6=- z@VjcdcgT!eZmiukDtZ23xpaJQ)j3!2{9%v(^3!%}HE{p}SBlFo7sQ2EljS=p%tqDN zhbd9nFIJ>=?tpH3!u%6cmg$OQs3D)z-Irk>Kvg@ROpf5=SIXgQYg50d>J_J%C8v69 z*4B5NgG0jm%=by~;WLLvIL1ea57Q;5Z!cYXudbOVig;xvyiH#Jy7B(>vb9>8>bH?D z)95$fNEVgGCuFG1pN5tycRW~C#99{|&fZzs+#yQybu^BsN{jw{ROd^e;Hs}2xi7!osvpQ<>rX6oHRf9+YZ>HEFTTc_2jvqrkr_hiN0#2NUheA%qwwajq-nY z>fvVx%zy%N1o8gykz%AoZZ-0A)6X`{J1aRrLIgv)VC2R8Z`H!N)({}$0!XK~=CW~} zU9)XqRXC}SN?{nzi!C+v#Xf4NxW! z?8Tja(dXIaWnR!|)_t*e1&2S>mIs3$1jh#{>wYpyUpt}KNADc(ugNBo6vF+ zcERV1AMC%y7oGbu@$^|II3ygB^2y-y>>)hyTW+NpJ>QG4>|#(g=u(ZqCD*Fr$D&2@ z3xP@kbLL7+uE*GTEDgdHPp~<`I70xVK}Vlk4+;y*BbZZ z?HxFi&p_t9GoV437QQr{Ix-wOj7-nQ?x3ry$lDzp8TQVI1@gz);cE^LuU-Iw0FGL4841Euj7a@YVf~-P39;f}n`VoE@E3Qzu{~ECV2h zheY>MUa}c9c}l}fa})H<7pUq6YaK$Y_bxh*04_MsUmBe!?+M4{kk&GHb;Ok_MYU{o z;F!WyG@AHbPalxmq}n&GsESGIXjG`oK_X!7FwB~Hisn_Acp~|e5wHriSFl9!%%fAS z)q}&sA)tx<0%O*5U=AK^ST()U42$%B(=fgUF_Dj}HpPgOe0HEuWzYP&1&jVT<)=YH z5;vJ*kq-HX!?B#krP8_buWzuUvrRQLQqxo@W;Z!opYDp24hw2L90AJ(Jg8#%-S59c z-@j%|pWqX}%%J}=LStWoswX}d)8G1vs#+c3!V=S4NFLo&0$4ixLvO=L5r8_8m( z)OgRF!w;}`HUXIBVjyTsJ}&P z?aN@UcHORM*!lO8uM=%)QUVeQp+nBGej40<hF;tYs zXRR$<)?-3KMC5!G?4SD=)m?o!`pJLd1cw|ybeIDn1olPwn`mt2RJXE(*f8DdzT9** z6Jy_o^5$(Ebs^|P071p8x*JYw(J4X|faLDKaBY_Mu0gxNpOx(ipOKJ?;Q$SE2FK_> zHEoIoKplKln>oyrwyuiVV^q|vA^E3x~Ym?=B!sk)mR$bio}vKkcuhw#SBJ7|o+u6z&Rfio<%KkF(Ggq-ewbr~`-vpG{$%Ed1G^hCNK`Q4ZJ(J!sfu(hg&Uhufi#3`6^Gr?HS~Mw8QA9X49~m17GOat&!;}4xV)j? zaL3dF5-)Xcrq(N=-e?$>IzxQ1^UgUF$Wj@>RDM<=IOhMHz-lY~Xr=(P8(O2(Gr9G8 zn)OLp{~XAC3Rp@Bs+aIq#M|6A!Cc9fe0dMCQ5A}9FJ~lv{NyBu7>1P8o@*>qd zJpyfl_OdVzo25D1Ncv+0d`a>Q;SZYe5=g~@Xld<7Dwz1QybL$A7K23stZdiU{44U2*L?V0fWevhBN z;dRZA|3jU-ib1Fsr|f6&PwF(@dhQo@yjX8n;Qn|y z3Y^|AK z0JvXx!9d}5iKCJy1Xsk$>Z9hf<%>Y~YB6^E4hL;BM+k1&)LT(!jL-%Vo!SC+$2Ald z(*!vV6A>pgJiKOrDWAfq7=k;Pk<;UzlwVbzYq0S(?5ep2d$t(bEH!&zOs>W842xEu z?VuSLM7GnFt2Yi8xy~=M1nsBV;Ut;o9xcAnoG(wgKhe0oOR+k1*cwWiL1KLE5M>r~ z{27kq<-z%$(fzDmg!n@7{5<&ulQk@P1Rm2geW5swK56E#o5fx;K9A!@{!mRYz_!nQ z_0IlVRY$%Qt~t-sKaW2W?WO5Vk%g5RU_FoB!1EYBL+F60=6L7gg9s{7k!x@|FuaFK z764XMeW-l%ntIG;J)C(Vqse5v`M- z{N+aKLyPu~+c_w0&2g#W;_cTdy^EV~?^9ISa$hKlgyUBaY1)qkTzOm#s!}CeO>8r! zq=6+8hO?PobLIgA&*N`i2U77b3TQRA@1wv#$D8e^Y;Ro}53I1Nmo*Nvjdm3caQ%99 z`c^&;VgfRp?5sfB(X5j8{kkRny8eKo()#A-mlP_|g?*1A%K{t%ltY%lOrPaV~(j9eXFuvzUnkg;K6w&%sd*AZT2`x!cX^ z{`v`>T2a)pZVeU$k_2M=U+leQT$J6u2CRS}B4EOUN(eGYE8U?Y(nw09ba$7CjfA8_cX#)I zDBTSM3`oZ?^Z>)ayL<%qe)m3SpS{oXetXXsf5^bieXo11e_sFVTJ%AcJ5|epky;aU zH~a7x+=r(=BcDsWW9|7ti zY}a&5w?_}qo>?_--|8+GKfZB3Z|6pAzhWM$r(vx>=NW^1%sN%msrgE|t;oP=Yr0=F zXEt!LRxsDQjhNRl#C$Ldj_k5T;iR6Sk1tW7{CRNQ| zyt{&#+NmGIn4UgxT$hHIsgDU9JPakHj9F!=CMf(}08q!3MfXaaxMn4?&_wj}Ep$E_iucXiU!*zskHUN2qcu1tat`brafrqUVu& zLoC&*>5Ev}1LK5`4xP!8KG8oy$XT^nDDzQ^_zziif$~hklpG{j8n@X=|0ib(a%xOL z6x$J}Ofa>WpqCEf4+*xqy%!!nya>uTZH@85q}9FMn}7= z-V@%n|46dh>#b)#7pfp)*J*?5ZH!t;Ffg$V_Qs#7yJ-^LQ^sYRpS2{YXWuH(mn6aY zfg3{aC45K#z_|mVyAFe+ z>9HKU)hOAfEV(YGU*YBX*}G?n-wu5ujB)}};WsVUp&HohFf{8jkQEzOdk z1VnkmM|`26n~^a(=0K;=`AjjD$IhTSJQ7_21PfYW#|QWUF$a&EiR@n%NnyZcvdweoCNf5g&AF3a5z{(HWr|9veV{z$SfA zp_fp%i+|x3I%ylRdvHN5&TU@9rP$asU#mQj*tU<*WP6TPfU#?&(4aQjlE<3(eP0rq z%<>1-M0z*Fq}zgy&b7`E5rZbgF27P`kQP_Alc-KiV+_Yx;+Vc+AaeD{C#=^6RfMk|EBlto1mE;XshT%tLtxQ<#~L zYviOsAYnlh&KAVOiAycys+j%sBqp8`rAfdBp%E(#7}8q>>e=G#H&jyLK1cf|{O+sI zK2|?@FOzF?#WNm%_aen+W#^*=D+`1;=2EjP#t+|n7Ju6nBb*s4$u5oI27s-^9B!|B z39U8@}^>BpT?rSsx#jv%|O^$THVobTD68XNCqV@ z_F{|QofGVSKTT)2yXrIk62g!njRf){q!e4oKNiAnBL+QRwh9zJZ^Remdtw!nkj1K1 zE`5AN{c)EycJq4=yJ0xtVqcP%tZ+KXlfH}36Y&iMo81wS48*U8ywk3#q+!=*zub2Q-@bs+djCWN86ztSl&ZR)v;q@)hnU9-_w;C1_~`t%1Eqd7X_s;q{k25H@X(evrEJia@VMeT+}gWgTM zFE2*96W>tec3s!!y_trtu#KRj3++xF&6spwv4Wc@-$X_)%3vNH#%3{1dZ0Jj zXT5=d=Wg3*8tU_Nw25cKY**n`i0nPznVCn$&gORDY2Dt++w<9l*w9L*i=3R^zuUqu z#WZm$Kaw$1cskl@N)}Q7nGX9}LKO2X`8lW9=Uxt2IzzSPNv_|u#LpK2tRml3Xsw$A-B-vLVS?5I^aRm}RsAt@> zoDZQpb6R^ix`}f`F%|bYm#Zme7s+D_Vb0U`vtv@vv&6OK;G_Z7?>?m#ytjZxOwO@v zdZ3N9eEfw8^N@a2d1w8Y;#`cw%~s-APv|8Gw1NGGA?o6d3r@1mh&*h$QmV96Z}G>r zK&0R-=vPwI8O8=^;-+oG$caOURJS>YMUo>}jcHso;}r^=KVD-DnWtH>&2Ki-%hw6& zwdm{6Iw?ljSBX(?bv)(_K0a8nnXi;vpV}O_$ff++ICo%bi(Jo@u{4XJR3To$%trzi z&uuE0yP(YAPz0F6#~N!JrWyLFnrZ*^Ew+#g+8z_7_PYLLJWt#Tpo5h1lkRT?4xK`fs^wnv1^K`E_GMeP`Lt&Z(M7P|jviod3!yP{1d=(`Xgxe&T~| zm(OwCGX{Mi4euAx#Px>Iw$&sJlI?J{-ZNl`!yInTY0@QIdT{7QlwvmnQQ{WR1ap)0 zol(5y6avHfTJf~&vVK({!@3JX1^Ghu!O)l=v12|&!%Vp{{WN8^AHC--hVzKi&07R+ ztVgO5qTOSh9qnI+w(_l%G@b;K$LG#cKFJYR)g)up3`>-;R)oa~TZ1%%LO^8PZ+kxs zUnKtH1F^wO#ra#mN*X!|KnHi1ERt0xDp9NRleaV@}VS<)~Eg_75Xhi+m7gt34xH zk5Zu5OKu4`@-_N}Tj3u5mT4~%6T^s#9&h@8``*+Fcn3Hg-vY=L^r8~R5FkY;1mgTo z%sGw8g+u-g1xBTcjE-nt(TBWWD6nA{W51k!{&)KDzLsnt2jDq=Ss`>%L2IeQ0cE5x z&`qx(udv1*6yOtA6BL{VIp=4^d_goT)te-Nj0J>EEs@EOKh(Wbo<9;)k6_WIRo8J! zRmswpkiN#w=C(B-4q9l{?<^9A00PJekcF)Ako_*htkNb>+(}o6_Ew5ahElcex+=ZC z_36)MqFCUyo=*q{Wsc-{dya*LXD4^|Ysdb1^EA~!?LAyalp>Glv=vfY4{)%C^Enz< z$f?;ahI2lLGpoh*S;TRfg@oOG`amsD+ex~+z4iB>AHYjH+KCKf)>13UFEQ<<4b@l% zd>zWGg5HM}pKkI_PEcLG{2U|n;ll^}^(pZW0Rc|v-=W^=D?Vv`{_}t9ThGrjY33OI zXyvLe{PnRRxv#xZyOeB4;i^5qHo@ts{JpS$Z&Cm0y2l#60DIoDO9?+N9`1hgt8R8F zQ0x(3c!u9U{q#TI&%gSS**j-(vhDlQTWkO2E&Puc6?^uxm+<`&{Xf61zuR!X&CGxI zy%s5OnYk2&P>xkH7nOJLHUS0p9tX3;rLZ|Hm8uSBpav911SeCKV4spGJ-U z%j-QSy!7lk+12p>)x-RLavBD3neb2%xwTU__`kf~*DJuFu|Syq$<+PJ)%eHVDklH) zG7Mf(<3#`2hW^Jzd`%C`&@)+;2mjr+`1@D;do}*?hkax2fXm1qa8;83wM}?l=4E|W z!IzBJ|Iupv(+~4Yfy=~fo7uDfBn@32>wNR;Q0=aqrQxR;pm%f9`SGyiwD)c+Ztf3t;lU$s0orn0*5i*r z<;qW8pZQ-c(ErQ-j4IM7@YF!qeuu?;$#_6i>is~bjuz0hXY3Y5Z&Ns-##w${(WFWY z5OSm{AS78HdV1DuBm1?$0J&Ws%Q1SsT4_6nw#asxmV#@_FVvbn7yohK!Q+o)tW1UT zRMu;KQZE82WmbYi#}HYn7EAX1on-+o2yYp+TurFCM=rYsU2OHZ=}OIxk4+08VN=TO zmOr&cG+)Frl~Pj>?{MdGl1y4b2YRmY2y58-<9kEa+frl^kNOu8X>!J=Nnx`eI_Tb! z+zJNtmTmQCv2$Ru)Y}S_k93aEBQxG`B`tn$TD61iD$!CV#@$j2gl-g<6FzYk*|S5kh~JxpH|r+Q5Du!8z-n7@zL`skm5UYil-85ap;gw?HV`x`%46Tw(d; zMT4*7fW|_63Cgzi+qYYhJ_{WY!+IX{jp_U=ImIdHhomwAQKhRc2SD)05WN=5ap=!( zIFtkHw=6d+p6^TRoKq@RV`WsUd3|f7TtO{ggFci&K5VH!J=M+Aok`nqR40H?T25BhN>n!wu862Y%=}JYPN==i`;jZ zOu}7G*Zp^t#&-_H7O^;n^1i2bDFxCqJKm(!cd>&}HA~ifaOB;|C;W<25n48(pch)< zGBkZ4zu7sQJihaZZ6HTkix!aVElw{9`ZVbODFg!hJEh31ZVNc*8)MXDo|!wml$fkk!(p<(r2h|GizsLD!l z7Gx>S70;~}CyyJnh3C^W*8Rcs;e%QA;*@bZDuZ}{EA+PFujrvg+PBJSWi)|FSn}v_ zKgGKCfI>v4J%YS#MnTvBH$3L?X4J)*H$;-{#oaI(BC-xJ8+eg2$E&+mI8_Hj(v`TY z18dxIsv|twp9wU#%ZumMW?Z@bT?9*1UocD=j`X0Sw1^y zO6hKWysJ%1)cvUcZ!Q~3pMcA7BhDdD&%p`+nryJJ0($^95WLIKa8YTZ$f#I0es|bs zx~TH;>HeY7!HLxC<&mIKF(+PpIj%Stz>Bbh_npabS{aIxO(vQhDg49ZJ$^Rqpeb6Z zOq;*_17JgHnXL8b_pBtqV=-MX_usw@aA|==}n2Z@mP+6fbHFGoFLND zI0`RP;%Dq?Ij$MX74RLZ8gbP%lHUc~-{IO+*o8X^ZuZg&zpJY>%I9chbGAPigqY$l zJif+_%Vs|p@oi)##PvtUlLy*qaUYxh7Nc@VCaA8okLM_XE>8H5?=nxI97b~UenrOh zZGj#9Kx>-56dI_|OK*GQ_p52g+a$kBBTVgjZ*>=e5>?Dn&xQaXA`n_#WFsJ@vJJ{r zht&;at9R#F^gKloc(ffjt*pAgc54Ef9=KLL;rQyp1L^rfMjYy98g-UX$Lby9>RZd* z;p#f9weG@(E;eHot+~!9FLZk;a!kt8^Ht4GlLd#@0hbHn4Y`gb$Fn_r69B)|qH^22 zJzDEgUVR)WxE2z~>sYqJJMJ1P8?$Y)Sw}T7KBrHVDvO`j{Vt%O`o+wTS%i-mFqJv z(-;h9+Rd~4I(&wiVtMDUK5^Q9`OQj~<+6mdRGjnb*f&{sQjWfeq|v8DDj&2`{vF zX^uR;9Q#nmW~AWh2ySkvH+rpnC7KmG>Cvpg)jgV5*zmwjwqU4J2aAas)){WdMQ{4E z@10X!3cUSL!F1glbB#X7EBZ7$z5=}{yYA-bMI24v9??m#M{c&KClj9IV zo-!}=S^wh#j@hl}#`=Zq8Ou#wl$|t}oi@Xdg?C>scKCE9P)uYYSv4!fbUlwW`0ai? zQtF)L?PS|n-C`fpcoSOw(Gp0TAE8?sIYwgg6A0(WD(sTSeGn6qOe`me>3RxC!r94Q z)$FnA$ucMZLr#x0XD3>mbzNwFjarHzu8@0 z*BeO;7qwePO;oE>)f|gNtBPaHB1_F?NZuj`S8ns`7qR{fjGZmf#HLPx4r9X3ERO|T z%u$wPHhQSH@|2GVYE925`Qp?Oo+-7fD1(|q-5fFwXyI*es{ zGC%Gy0axwuB%Gr$bg;q0osthbGNX`seoy^fVmxU?y!S0su63lO_fDyBa}Lxe+It$` z%*TtNyqn*0k9@N;gI)PnBmRla=kvk=!^$BLx#%&X5%U8pAGz2&K(c|vOFC^^GjKoa{rfTyeQpB^>@;5DB%)U>&a$QHY(2~@0-s+rEUR}Tj4Wr|v)yXV_<)M-9MRuGsb62kke%gFizRcG|M+Ko zD>i@OPTV~|#TmH-g*BloFdP4GU>=VUQ$Dh)dRd~AB?r=9?~~}sH$?|aV5&7Hwp_-v z%=8uZ6DRq(IYC5hdK;R9gg3%EndU6@_-x{IN^n{7YP&{P(Yncx6o#a+DTjH&yFRopwH}Z-z^8HB{kUg=iqu_;lvU z_AQa)>PRc{rfeM7s5y&H(Ma7&g8u>Wjknh>& zF{(|v<;Pd1hcK?Gi~6U>d8_3qw6V?eEmmvRZXFRYf~&_*8gyf*gVptpMRkZZv!aho z7ua}H7aj0(J5HldK31G4x#~ z`!d6g7ZkkKIXc6l>UviXE913pn~8Jkn2Et6w%9A~nOU@zM+-p>e6aKtl5NQ&zY3>f zO?X@$6`{`o^RSz@*IPwa5)ScEy2lrb5i3DagxH$zgr-AQqF-b`H?$@C5y+ zik@RH6ZK~(ilz8BgSJYk9g7}^n-2DR^2MLtFv&k=MjGTkD`QdNJ{>tZO$BLQ)-zsO zr@riYLB56YG?lQx<4Rz1*^s8Kq`l)(cp=1fJK>v^4%=m8{s)}TpotepYZvv0jOO3@ zEyTG=AD{vM4^aUbWr=%ptP6Z@ssAX1fyze(pZJzOr})xi%60f4rw6j-bEBmW)?tu) zxT1aXO|*60cb%Q1dPlSDA*C`)w`v;zj^Hp{Pwz|CQr|c2IWro{g^BMvg3s&x9Zr+FVfuMs*PFW!Xb!^S9l{I4zaK~(MCS6 zC{~xlqOGe&HDouaMR{>lY1ndC zK0B$M5blHM{Mq&Add}ToPIGkiOyU?xX{{`@h(~#-fc@DP}^4)EMv zk}Nw;x8g&mitk63N*(1);CH^-^(tl2D0(D9%`sxw@fd|#Q zNMUHi&Nj;hbPe4De{V@0q%%E`Q?=Xn_-13uqz03Qust~OS;a$Kt{ z3pCEY!DDHfwet`;aFKKjJy7J@fCHv4)nZK?fdbS|Bsv_KiIsZtButfAeT->rQi5DB zCaG!{71K)W7>sTrUQr%+j{BE!vdR&Dns=KSvqNAx0$Z@j#IB}xaPLU=8{RA6K(?!^x`-n zA>rgh`thSC|49}dn;O~=QVreS0jWhHy+;~Ma#htF*{DL>Y0nQI_^si&8rf+-=k0~x z6(>Mpjt{6={mI$xiJ}e^Se6E6PN&%ZmY2x~h{Y*8+~_)OfoQM>+Zw`V*{twQW;N+5 zFI)@%<`qr9u}<}YFFR+JO+(_!i-?bRz8UQp#i(EaEm?#l_0mk)#sUh~T8;=HH_>13 zfO5|64C@wRoaSqQ_h;Ah<{rc6Qq9nR)fV#cX#Ho1!ry-QJ_YU{)p6lx9_I{4J4ADp zq68Lq-fhfGc&Y2Ul~#{$O;&fD`O)aCgPRIbh%znGAdXCyBx#*auJip~we>23yb5|@ z`&|e~2CeZS)TPZ>L6fpiRM>D&OAfWADWl&g+yq=XnP?h1R)K_}B^y~@ac+Sxk2&p! zEJmI<;!oB$&_hR2d&}9MIbM8Wr}0|qqvx<$({@9GAm3Evgfl-f|o7#F# zXL(7<@9jdx&Q~2i{R0CR#AqXu!K}%5u3fDK6iD==Gt^>(JwLigy8$aqADu_Du zCwLLAomSO|YSfdCUW2~*O=cs&-09r22B>9`N7ZXfW`WGlBag8zfk6DRf6Z>}xWAz) zNOT(Rtk+F2tDq1Zq1DS`dT*!#<>~7*iToWLE|F)bR2FGFm7@4R%O@p=uEkg~KJ6R! z>2c<<9{;0=A4zQ#qykOh;?iVz!-dhnu4>DpPc-4OVW+Sr;R`(7Dw)XDy;8bp7!c(sxE5&34fn z)M1;9X$K9e#$9DTvfQN1gEe?L^E$N|=05(_-J@y8i*ujn#~ey3tS>eDjhoOjIDGYV z+X87r;69G~gVItjxU-pQoS<3n0`27Hp@YKD=?SKt<>63=ZYvgpW-QH(JF&xfYW#C8 zpOe>YoVvqniC`D)SX1iQ^8Bv(O>*`}e_s$hqx1x!w!RBi<;& zFh?ayo67wN`1s&c$g{N&Fw=HY+ceA-d%b_0?!@u|UtpmL$)_J4dDzEGl(WX5ZYo}Z3YKg4?lPrEVd9|JKpV** zXZYY7Fm)G2o#ELqEf73Rm}Sp|oUDc8eW5PS2G-7&=N<5= zPFXB^b>r4diZn2+-B7h&WKdKB>#Hmr)n_LFYRvhX^&c|Uwmla4buqM4!gT8H5zxbC zMlIO{CG$B~r3gRIeNVrXhl5C4?2hrhFYJ5L#in4IBJ}4d04J1NX8)Wja=h}PuG_xE z&lD?4E|OVYm&+oZEIN1DKhc@L)@SoeEB^98wgqYbhfg)Q`7las0DSy=r1mmfdfB~3 z*%wpWg2_qADennyZAAcHyP-Vuh@j7OZ#`TJPhg5^7r4yE#<6+hC1%NZM?4X|q5Lo# zO5x?`d-qfX0Y{UA%XMb84xOPlUksWJmj_y%e^8se+wzFYTS2E7b0dn}ii-tT(V%1@ z6C{W#+$jy3l1=%;88_{P+IDx$mWeaMV5FxzHZ8UDx_VW6Rf6B1#bBmY`O>3DGLbC1 z@a{hinb;7a>AgWfxnOlPq1E^4eyf6_>d@18$994J1nKB{!{IzwG*Vy@$r(l+U!W*Pl2lEjtf=b0STA}egQ`N)1XYD%aRjQF6e})8V1~!se{`Wv_?gZr%PxykDwHf&p9NB*&+&i1b58KwnyB5^a#31Zej3IC z`HY>>;t-6+q`*6peGY%JyI-7n@KJWq!Wux?{gjAR;^Vc?_Yuc;6-2N(TvkKl`8E`; zd7u~%m#Yi_)!X6ronaT*4JI`fw*qT07ls>siBd@71d-so%GgtAz`&^9MgHWddsQO| z+RvH%?b{tR>?6Gq?BbwxTumL}>mNT19;z0?ZI;F+CJ?CF&~~*{;?8(jy>Wk^<@%W3 zpi+~e7Itx_dp07kd~9dnXnSp|pc>;5YSbo}__>ys?-zL1bGf@0N#nay-^}^%OqEp}o30;Ts z^%%f}dy(q4U`aF5r3H@Kbs!#A_wDrpKY(l6edY0a@S&J@oBfK5heE;mm__C1o)W?9yHh-6|r7R-!ohBhKy^Rk> zFKdTOCH#U9fdZoVxlO!@w$lV?PvOFyTcgaE-$*|>+@Nb6jt|x^Qp*jmabgRt$ZeM` zP}Sb7vn;ztP9DF;+(&Kq>N;_YCd@kak)(R~590*s)V6RSObPa`YNuxD>TLm{@lB(q zZ_0HC_9>mFsm*4+;>8DapX&mn8k!vz4(V|5w<8}CkXA{m-bLLsbzqqyxLf{8qld!J z>7I1P%Fd7x#YwT-KJO(-(p~Kl-t4$tBC6$n-r3IOGYYYt!#7RiG_Zk>h`)9#5>BEo zq%#t~xS)2W6Yp$#v1wB;L!5gPiD~ayk*En3y)hSfk(+;~ihx~T{2KkTNz$lN>FoU- zR1HSgMK9w`RH;S#c;)c{GB8NN2Qi#~_N4SCMk9mA720c6jxd7{t%lju9GR!Znj9Z_ zS!<0A6RC0;yy9HnbNzJu(gby1puR%C0mr%Dobz^X6nA!QHD`&CX+42=^Tb$g^thw4 zdNmU4zfqR-M4ZmH=$tfAJuPw?wBEaSOc-!@L*8I+*;kq={ zHE)OEyv;tMZVZyNjKgz8v9icYt2%qfDAM1UVX$wfY<7yP^Q-uYi_gY$k6sXyPiwU^ zXnvAmLQlc(v-;s#@2ZlSoVGIHPAcCQu6C&eZAgb)!09-U*BoU+)-AI0TFUa$+WlOl=*Y9ElA5od?WQ7ooQk;->+hBZtrj8Dni+ ztCQNHSl9LUEtTa>vA+;9w)2E{ZcQ5CWPgnAP&v}u_+Zye|I{z}$2__7Usuzfztz1v?HyU~W6f)=DKCb-G1X;jmr`&-mH5F0)tc^D5-NdPSyAJL_*{Z>e&|U%GQYaWdtPb4BR5Qo zGru0;`-&$SuxqGV+a#n#qbBaycn2suuSu^( zYdh_5cNx1Kd}uxd#h7N0Kc*_=VJ7vrTDw#X@u2D)kn0 zV3^n+mRrUtEjdR}hroJt&a%g$&@{t`fGs&!4Vd`DXi6Q1SB@fJs!Of)GhJLO2QK@k zKL@>iWv}-5JPgTX>gQ9?dU!zL9#0TcMl}8H^d-=pYk3wC}!c}`hWxbM^F5zeg0rS z=!5z@+BRKR*JWhw5=0o&zvmLg5jMgfp7bcU+5JHYFaKa%!K8Q(Hrp01^UCODhFY$B zi$fvqiDm1$<@H$DL@Xl zlP#`UCb|dQSer+}%c|PDz6K)UbAyq3McF{zE<}dSiuFdYo`4yCJ17k)I-dDtY%)^q zk{|ParxT~v3!_%K5(tkqcKsFVo$!c#oGrLRI8j&w-12aTK@PDH~-6{ zWCeV9Jq__dC1!W6>^Q-y18rE4qn&A=VJtV4{&ua-NQSH$lvHjCAnpnf21g8LtHUjq zCb5%qlcYPP-9Xmn^}dG2)F5a@mveQ4JGZl)Vq(?&AE=ImM`x983+Wo4VdmgQ+kx?|M46Asr zV8x z`*acY#-U>&p*x&wDpz%dM+x>E7k#Df26vy1@t^%M8jEu^XRD!hX-N_vpLlQCZ0_yh zUA^N0WEula9v7Efe*4lp0<%kRf=4`9IIiti&%+uP5i_-eE^Bl218E>}v(CJaKAj-bLanq@VLj$~tij$BG2 z)Jd!zv}otRw4FG`1u~YqZkvzV8vF<>FKhsG37OZ@Cnq}C!PwEdY7^(mIw*)+>4zWD zI3As2l_;jdi-;gEyEy($-~=}m^BhNDM47dNSXZb8FN6&9zzyT6`^RnTR%z%Nw>Lj1VsChpPVYR`souu{g#g^VtylUtd+ zbk^%=)=;bOWvo1}<7c3v(K0y!gu>?PeQ}+gjfkg)Ow{^uMEH+}foo)18XH5Vxw? z0J+w8Kq|cb-ebfC5lAAi5v+AP;EkFTZfMDv7kYAzcMJ}!ZJChT5b(7C05SpS?F_X|yZCH1y_SiB9tUhb0-{i>_*#g{) zRAfY^%0-6a*&rz3TL<^=TEQ)K7J@_?dy=3u59DYwFm$0DZVWnat&8YYd>TjNV-dwg zsEzOB55aqz^Z^6(GH*WC<)q$7AYH8Nri!hB- zb`j}1o!-!3)i)LmrKu#nqHyba>-F1}o3mG|V+Mym%Z*X<9})>Zkzii{wRUF$8Bf83 z>}%>7r220??~jwz2~oF3v@Z%G8zI%>#@4 z5u*dj2=VztPOU&n|I4ur8YaR=i=hgs3$^H!y@sM13cT^-hX^raVf+2Q*_SLju&q3g z*|A;9N5z!eD1?GH*6o$Gb32IeChD{N(%2Wn=Gr4i&-S~IGw0W_bqgWe-KSQE(P#Zc zIL>O#$z2+9cAK4kXLNkjFY1?sK6rMT15V*!F}3XboBJGpoXbvxvaZ}QDm_3W_>IL@ zSw@X;ieroHX7Hn*x3;EnS(4LkZf0D`l=JGmS%A@9r4X=AOZDdTu-t9f)6=+4&?cz z#cw>~Th9%-45iCOGg*ykTN6ifSy=PhOjJoYf0=SSDfMa4uP3{al?=(2glbm)Y_qQg z;S(3M`;#3&+i1WVrd=^N?hS(d+@l_CGx$it$ z{Is?wkvtnC@0*;TBmJ_@cw3g*L(Z?ABKQQer6Sy%ZL79)m}Yb+Z=0+ zXv(a9;kL;c6N-#?>%LwQGw4N%Y+nVH2{5|LT)Hyk?2{fNU?e@2TJYzl34VifgDa*t z>FSoQ>+~C?T6d@N56O~2Ul4Z#M_xCx~qL&i+d|99elZhXGam)uvvggB7jc>rI{OwF%+(oOGi(2=Ivm$NJY zNEwpd5u_*(of|>#PhR;>0kgusWQXAqJR^bj*<8A`KerHXcu->2H`Z}}d^gKtf&y>C z?}YQ}j4!9O7;c4AL3#uqC+_;4t9;>_`$HG4?QpWA%1o!#QW4WiPhe=oXMjm+BYDE8 zp7*MiMPTfrkS^0!$C-r(5*K6uyL>8eoog?tiNCo6#-fm$_hcO8CRPNUi{NY3$Ks4; z_}7m?6-{aEqI2cyX8T-GJDbbOG2fAgi`%+Me9Qhqd5Ol;ZvOJZ8f+e6+rgX()C?c! zRZFzi8qpW7F zBX1gwg|!y-aQ>mm47l=S^DKsYMGX2H)Z}4PSIH=#l_Tb^qN=58F)1eNi@tJjbvypv3uUy%U7vi3y8vh0gcY9>m$dwlkx|o%hy~+&R7t$ z9c{eZV9fI7Du7vz);yJtWTVpyxUp0&di%CGm&L3cuS+T1Lu5TcC0FgCapxg?fSq2) zW(6`9)SE=Xg^Ljq#qRkDR?1>XhGixWcIMdoBQ_!3UO{$n?7n=a;VHj?(Oku6zKbcBI8$~EdVq+yO?tpfEQ@!dU}F~Dm2le+xqCZ}C4;5SmrQZd7L>d$v* zKu3yvl+hC|`K<}DT1|nJbnx=ya)^LNxpnPv=a*6^Gg2rLYnKeTlr~q3yVEnWIZ-4OhndyFy?mkrE8?L@^Vio{6zjV-vlq7FwibI0LWVn> zW10=Q6HbvZP==mKJk%`5ak_+`PswqxCckOXRRAs-H708zhpKDF`1p1lc)S&6i}99x z^yu}?OL1dvN9`B@YE>nn7P-q_vmJ#rFcZxI7=qpFu6hFgn|^2nvj=)@2M#J!+M}TE zsgkGt9bf<4?sQCBXy?nB#&@KG*-3t{+|kR7ej-O5B$K_~$#qmA?AF^5slY(|AIBS*{H`pF_ZF+p z#3zz=ImkQWn?u!@2duY%TpCIvqC8Gy20&q7aoO0_P29>7P3>-sb6x#>?d*(6Jf#sv z+`V9kVkh;O#s*dSRl|CS#ZAfArt`I=S-4Fdg%NIc$z-^C88I`U5`1od@X}p72fC*Z zPH0^ZK^_9O7>%%sJY@zss)z>bnwplS!eZN2t%UmSJ6Wl@7<@ z3L;c@To4}d!ex(lZY1UF&$WN1GX%9#taIuRVmh>p)<_}miLr1&Ln`^Oq5T&P06u{H3+-0Y=Hp*p`e>V zV8*|fCgk?@W?M9a!cZW8YxFSr`&<~Okx_2~limi_IU; zQ@?bjWo)v0sfxMF|Icz^Jp~K!1`Gnmyz177MiO?3ub*qLJT1u?g$h@nv9l|jY?njZ zXPj*_=(y%~VG?DXRFWUbQ0s=H|6(@9&CQ{LyncYkP@aN zJ_m<2a7|vkLaZiV2L2O#-xRwJU=JYYHw$Zk6jP8u+aDc z-bsXT{%`@$@m`TK$AMR!{r3w?7)AD8@lsz2ztFlr1XL#7FrD=}&b5s@ycO`qR<$`b zlEQ@UeqHyCb3Y3`y{msrnwYx3-+jmRawU+eycO6Pr4oApAFYPhQD9Z0uAZ+5_hT90 z#%+kT-0*NlMNG^CnvA*#dDE0s!%QWm0mDSn^iM|+ep4WGSFEhe-?iEgO`r|0Bh;XXt7Ww%agZB`ldO%xb_~YA$Bvjp;RQmdA%%3$4&}b-nK%?IKf<9D|!sI)J>D6CB ziSnI48Df~TP?Foqj%}81Q9`O+pZAc=cak0%eLxhB8K!OGt;}}OxmoDY7Fw7{y^!7f zgc;p@%(0Q=2uTF}8Vi$=esKz*R6(4*a|l@xpAc~BO!(ASWG~^$dB&n~PVVEbKrNzW(j(`P;{ zejCPQ1i5n0D4VbhsM5Xpc8=o7wdEtg!^y&(JskGsYO@6HnnB)|lA)99=e&ziFkZiO%nC<{rIr{bpHj!jZ zqy|zJ^DA{WEENM_$=TJ6yZpBLupG@wCHWLi+lnwk%{;R>mmHnqCOoZk@y{UHDBs-^ zknlxcum1l&k!cOQ6Pk14Kw#3Sqj#@+BO4f*A!mF3G=bxbH!|)!+JaFeY-im3h zm4$>FhA4(iyG*)pl*f3WDiO~`9cmonwps#_6dVpiEc}jlVNF|RJd2;{6h7CEdd%(| z*F81RF);xWTXSED*h{jN)8)B`NrW0=mGuzZ*($*I>|gEIW2yDh$-PN?-yqzTuR)# z&H~*Z@6KQjy>IY#$;1%9M&9WrujM_1#JkNZG(2TcL*qfDgFqVb?2BIuee106)aE-X zUn08;rw|$*oaawF@3260X}Jo_yCI)HzGK*3hP1R96sf)8ws3lIPHn={xQn=-OE=AP zZ<=6N_=~7Ouo}U@Q0bzq&tT`S`mn>(Uo1#qqi0CH^lpA03!;k;#s%h2M&mx;KFG2d zEw_pHtk(C|Ax#!JNn2_3aVu|*z2ol#bc@BqUxNPo=I$rnha1k73@2~vf0{P;#lMj9 zzNLg8N}OzVaY_g*2x~8nK8}NVTJgxvy%O-MoUw)tW)j-K)+zt2%8w9$d$@ zn3YqbJ>QuvbbtKYhe&Cy(9AJLDg|h(=XFLPnBd##J!gsbqV9pgK2dNEO{DlfbC-4C zWLx;*&(P8VjgOwn+gUp@hCD>4NJUopYPk~==k*U{CNB7 zs26KjgkXX%h>Sn!;hnQtkS{mP8aR$_%JiO5Ia3R{cpm(-v;mWt^5rGcly;c z-`8ZU0BrhrbBnaq7neXgyb50(IUa6=ll=|vUa3>}<<$qp#)V31YEHL;nm-{KSy-~{ zqgq<-wzjv21Gd5T5j_kL1+VVroGQ~8s!ka6X}Dp8OP^JS!RDvLv` ztMrA~iQ8LoW69MX?2s5)03@J?-k)K0f!k@=;*1Bjf3LiIdUXG8{^4{f|9-W_oNx*u z{UKz;w>H_G2=m&yc|&QUuaJ{3_(W+A=hyC5nXd?;#~seXcMn#M!;@%jdIkX>yHRfv z%5ZzJFiBiJO1P~uK2F$U%)w@~NP^6(yrs4aurL{|4Lcz3uQh#qQ&J-Cyj1Ut-2|kA zRn-pm_Or-xM1)0N-I#Iy@@Vn99L>_71i*4Vs?1t8T``qBQ_||vGBQTfE(oc2DUX(b zu#_)0XVK%_dPF^{2HL}rMs)>Z5ki8U?}C*AWj%$LH`_5Anlhq{=&GRS5|c{ zs+0+$`}OLTR7*rzqekxdM>&t_NkN;)IAM1wW3@D(FWhzqnbk7pbjxO?3vLtsnv4)| zDmBcETwn|Muh#3o_}&!zl;Ov|RcT-8f(F&g0)$13UHdq(*&^_-jg48i)Sd;S@KyN* zhjTO*@TCAQ{4ocI^B0=zCxE@FRF#ZfcU8p+5y`H%L^Wg0MFhU-%uQzyO|jMY)g@Ag zjg7HTqi(F!%4As3Y1tCNtu+V+0J8@4${fu?Npi+&++@N%z`qXRRwsj+;sDO$Mkg`>RfPfNuM?s_`y@S%b(mNuEo1!SagS60*UIGMEr1vH@ zNN*w3011IRIl?~Yp7WjkopS%(?~e~Yz+$bLHOClpjJE)6c3>T~3P=S#F>#M{PGCfT zsfzyO9~L5V+SW2zHUgrH{7)K)fP)*V<41&5vA{r?T%y1Lk?|SP0NCeSDv@X>8M)8l zF@ez?*6pSxyf;{xv;A}!1wBIHD{v)EO+!uvw-RUmQJrs1qmANz#KCnrtf&zW!dw9t zg<`dh&0JG_FD=}Kq|VuC-o|>A`*%xVNEnusWE(%HcbHnl9gaDEx5a3I*h0?j8w0;K zn)wz11R4%Y?;dTR^89!=(I(SgNlnwZ)W2eJsl!xlO8(5m0y>!Z9~eXb!>9e1{q!P& z=-l~WV=&F!hS@VvSZ^6Xh=5?Lpw zhkT0q#HUBTIm0%?9>pjRO-Tlgva62_n?RiXJ-jN&TJ5(rU;E=%{RR-h*fsmLd&SP4 zca@Rvj;PxYCM^$I`JpJ|QV`M07A(=~m%RA>P&MzEdhyS0_pdSF;yD74!Xtiu9c>22 zgSljRy|_H~N)`bD^=cRNGv;#ZiRVa~ugUH08g4U)~`3)pEU%MGnyyZbm>lrl$x7fm@$fJxbo@_GvYF6lGfNuVCJPFU3uS z`8cqHh5Hg;5!swBa(OV%B$9QZy!pKsNH1R=uHbXw;bFHKYwb!CpbXmVPl7*g zdH3|zNmfe4*=@1sf1T}5dTL)j%-h+&IPlxU)Z1V0h(P25U=q?KDqU=lG&MJcDF4Rq zvYJFM2Mf;s5i@y5)Na|2SO< z;PAGzbmXt^8da5)mZ}B?*M>tWs5sMGGe|Bh7?{M{Hn(mN8*VQRLnjY@d!!IXC+z+N zRFC5bt08tf;qb*ApF?l8_tLU7x+Xd*V;?{^z@C_dh?$?gA11&hBDG3zw98UHp7XCj zSrX)8gP4p0Bd*51WrVPmvq99HCE|?(SDw>dvo>JKI0MNo%v&2qYbFxDA&$LTx4j&b* zbZ+Y-RkZJ!;5hFTs~9kc242@IT*MBP1*86XGXC2P0+!c5NK9Hf5I|_9I{Aze(qS-J zadF=k-LkTgQHR<v;-HxusXGUX5E(IG-z=nMpfwDx==$cSrN5Zf=b~(&7}9J48B) z`(cU(lo$K;dV70~f$TZPlUC4~>-4iULOM_avc&u5n&b|0dhfe!?9D?Z{Yp#Cl?Jk& z!bWC+x79P8sY;NzUf z9Q))4Rz<*(tp>YfHLzM}a};-;G`*J&`_L_;Ac}Y7N0mN>dG3!yfZnJ8b8lcYlffEX0 z77}W|C>fKIcR8?sMIG!+m3s%`YVH)(HY24S^6Sp#(Btw%6{yEFik>+xy;H?ptzVOB zvV2_o5e503W%Y%jn)P|oU^^SX*e|KiJ4#-k4Zq(?Bs(14l?TOMY!Gs_c$S3N;-bnO zG>L4f(2+@R{$>~%Yq{45`JB_IFdPEDDE81MkC8xcdYuu<^zCxlhWSl_FrKDZ@+96n@4MZg664WA>RRyFe zwWa0dZ64-lWFPPAi%;t9es@Y?u|C$g@pV5R^Y+|qmB$^Sf_MInWtaI&yOLOHOr&tD zJ=^y;ovi7~f4rrFoRy_lcMXosX8h8=@|++y^>$l3A=O7W$de_(&EmZrIq(T^JsxR% zE34}mW4yuz2$ly(om|rUSg7R&06&|mzc_xyUd?Y z%l$lhBZ-t^o0&=NBJU)9@m#+$E3H|g4k>4OeCx7@jMkw0`ppq^tt=(&)sOw7tE?RQ z7JgY%@~0w+H7!;EDLgW!6rBEqzYr;3jk~8#ve9NEEG(|@iMO8RAc58kgLtNnwJY8gE zWOCXQ_Nsucc}}bNNTK;aPUOmP39PkliN`;lBp7r~!@qoa*hf!V*L8)Sog{j71ikMb_j2J;vO0|!=5OKpqZYU zYTr?-nu$A1>Kv)nDYxr-;TN#v7CUXPsXn+WU{Fz7+CPTwObP8nZ_^6CE;Q&1^h0U% zJH8KCJrYMSXHau@uNIDU!3PH{voS(l6)0x$fD6b>&iz z{FwE4`^U-VE$z(VetUGx4?e05SIzPmwu&5*uEqO9a_y7I_|d-K%4nNP_II0jgG9_< z1a+=X!B?$aCcIS4VVRk~7i^|{$#noy@y#(0(mEdHp?V|Cc3+BEO`P0w^Esj2N&1zP zsBD!(HYzGADa?%2Piq0bO4(@kxu(|gVl$U=>+$M=_E#|)(~*uJ&>g%;Yo9x`_YGeO zVHeW42McY0P4&10E?1zkiFtjU)cO9)M}=u#6(DuU$aXf2QRC`6%R;}wC;AkugLNQ4oT&|`< zE3B|HbnU~{h5b+EYy#f}KQjslK>BUp;ZDXew;56hpO^9ZslKmg4&7zuS}@y~k>;6x znBpt|r8$@vJtU3M65m;7FrZCINcPEUK*{pr)d2oRTt+tPUJ+D*U8lUCn2~!+fP_3w z`YKBhxq^T?BWEk_NjI*P`^Omp#{~x7@hv04&C)(~vM~&sA2C@NGvt2D z4!nQSY;VvZD)ChQ5r|KIq>0Msz?I*r|1K=Zi(<%n=X8*1OTU$cs1#G*%T|BV>uqa4 zZzrgY8uYkJ2L^8R0igp>cUJf=+MX#%@k>Wn;}imeLN>;&N#v{oI@8l7+o)|BHN-XocZ!IwIOt=&Z9sHQ)34fy^9=h)0fZ<2~oU39w9s*KIGQ^+{ zV`6SpVXCcI&xj7qKn?4~;5vlAC#;Z>p#9r%4Jt3B!pstlmIN-c+#A@Xr|*4NqO z=GLlKz4gqIb&oQ(#aMj_dI$!L{*t@v1Aj}2y&fuII1u=0pc1Vt+V)-5GLEbIL0NeD z$*~+o5a~mjQ!B!KQboNdpd-s`9c8^l>n^M5n9w0+5>PDTwn`WHf*_Y5p)cNl495#g z(Vf75deR##95x1dnA#EJH#7LNx6Gz;MaZ1_XI}$ka7!hr zR3)XJhxnb1PC>M~faIe>D05dla&h~iQKmrdLFrNFERR&z*>q;L@S#TEmns~bFhg_cQZkYcr_5{-x1g+T zA|%-@pA1CpoI3`rY8pnRQj=4M`b7LO*SWHAz!fi=O{e^KkqZ8u8V8dZX4n%&O3iVE zkB?T-m(hwIqf(z4A~iO>YE@v4#n#x^m^xem1_WFdbk~`pe-pgw0k+eJtWN(&bB<}(Dm_6Ci*AMhk;CaE01>Y+RuPQwvp71F2wX5 zGtc-4NaP_BiRS5<(oYkrYk2if1Mdnm>EpNqLSn!xDtGylFqFnRc}ekE((k07&sjw= zT*?;=gjF_MxAA)p2c2B>mm-%q-8{xzc2h>fwl57Zb zxZXJ1Kx@-8GY6do;08YXz{t`W%Dq6yMH@0@uth>k`y`j+(3-SbvFlUeQjiL)de}{; zT<*S~vVbbTi_^Rnu!oC%gI;T|ch|42(|>zX8_LUy;cg2os3AskaldhNyLiR(*sM&3 z8Lq11IA{f(N`0Sj54aReZ@D-MGcql8O(&wXX8Oyi>q(Z0Us#bX8Ty-ruGx zZWqheg_^@NQ}Jr}ipz2c#bxvH_oi!4<-G>t3N%LRs!OH1GRQ}g;zkOcJVojCh7M}U zGdDP@^UVsAA~^$hho1f768mD$F6@+J2<<^PI{DC3Vz591GV7w#DS4F2+xA*y#~qsuv*H_p=ei(8oEutPGg!ee%df{_b7RH{ zDmF}Iokzv0@43#XgOK#BSzxe%nbS9rE@)S-3GOYm@niT3_ZZkY~VbKO&PY zCHa$g)QQHB*rfT{4@FDnS@y@`gZo56jAy2pTcX$e?~vMkG=7~!9t10G&Nq2j*q)+D z>3a35(PP@ju~|9xq`~(M`hH%@d@prNPD$0D^nEMa#*gTLo)sZYc52oPUlr^Rh7nae zI$FpyRzmh^c1(1VT6$%>5ys}uAW)?L%y;RBg^wO_T-+EzQ|EYT4Vbj$?MgGe!;RqCUf&8#&uK| z?dM__?E_-(w5y#_EJuX{e1HEuM{~foTFGy^UL6CyqZe41@811P z_^Pb2{nC@EE~d393_mlyIf&VXu?;n3gfl5dC7FIVHZ#mS$jT%%?YzO;*=fE% zpy4pP*(K_r(0F1^$MxLzG$C>Go9p^Y%s-@YRIKWGT+n^Q(?ya`f~(akBe7yRT_U)# zEe&mHZS6S0T_A^3w=>}p$w|srk!Z|$%bcExpMRLlxThgcVA(EZN_QT3Rzj)HxsMoL zLlG|45Az=23_N}}4Xj?63T_Yi_7-<%WECJ#n{lQ}@4{^+%SUZXhA%i??6?iN+lnwZ z-0Si=aDcmg521pb@4pPsYj`TKrr~qZV=mT=@%HVnhe|GbB+YWr9s8y1G&xiMO_PL0 zN(+X;2EVsV>@QXqfsqR*K0OxmV;vIwx-%zFT&t~}!LZi!;ltPCFh6&MPH8jw#a=Rj zNlmjb44XTfmxbG;ujr4{jlKnGIBtd~W-f!nF^Q>u*`oWCx3!E5rmxVNdwty=8pR#p z(ey8PMqSA0s<(PcF}iUtzc7QY_m#5ET7z*|GLA7?_qoUpyMh|7K~-@tF;?4AuUrRR7J~gqD(Q%|4sS;Vj{|5e`>zGmb!U$KMB?q zSQ`2roB^KjO-lHymNkUIMn@zcn8H1BXuwTz}? zNW`kXMpXtRKGL-GoL%glS+}_+iVj;a82-Z5+mD%g4kxEHjU|W^jdTm)E_+B?=}49t9*^2^SX^BiRcZR0Ss0f>-;h-9yM#oq`Ii>Lahs zzrA5*V1pK|-Z%&9{mZmBQ%g=QS?%v`rbkj$dV=m1=7m}E0iRPHRYx4OA+y|J-h3V_(A6z9&jP5B^&}L>#g(mF0?{C>rU~7 zKot|@c>T=w+o-0$j~u+V9jbfPc{9)J1G7rzlR8p}7Q(g`h)E)V;F4la|B(3I3go&< zg?`GLl1_m>{DY^3Dg|DuDqLUt73~hQ$vk_V#$n*yA)^)OCoH*I*m$5Saq za=0{k<8U(hDJ=GBMW*>Uz1(EfwS3rVl(uPS!Yujy!H{#X5Ftq{24y8!7!Q&=+G*kQ zZ^iu=GGl=X|4Z2tUkMG}_1j0ta^bh=Y<%eqXs!S+#hK82i5 zT6W^N*@nu5-3vZ_4T!@fP9_Z&4YsSPKomdrhH+|D^$Mny>$Kdv<0My_oL!FO&n&5W z3utFzK$yphY|EjyOAf730vmUmr^O&s&~C@9hZGS4>Qg8hBHD$UaaEt#GhEOUL(#8 ze#>-6&Y`ZBmPlZKr@B|$$^ZVC=c6h{=ujSq?u>%qJ{=^bBFgx>1}<6Z5@2b3Mb7th ztxPufl_&R9YCo|$i52}v&N^j^5x=?z=BflqVV5^qU)Qy!3-RjQ8gZlyrMjr)eZOy1 z0s29uRaPF8^PE|Z{jTlbJ*(8x1T~Rl^ zkw^M@qxnn6+z6ze<%=D|e9`0ncD3wmW}X)^h55kvktdh_aQE@JaWK1LJkR(zn%;P} z|2#&Iq(UJPYbWTcL9#Vr-^Rvc*^^x{Mo*)p;H7zk%d<6e!y7rm; z@}PZD%ekNXH@H0p??ForJp~ss6q7|DpO<6=jS%_G^v6ghFh3W#G1E&LJ>tA{>S_nw z?1d0vAgC;7*rVAao;^a}O&{bLOxy&Z93!Tsy$9c=!T6ID0{uv=`3`suD;o{~gF9&b*%AOfp0-Dj zQx>bbb_i*+kpB8xwNkJPw44%K8Iop(`e2e0-?(We3P%!8?d~)sOz$JhD6$CMx!RuU z>nAxO_Ji7NoU>IF_=n52%-j#A=?{%5@OVN*NhWl{Tc?f}j~HnMcy{ZnWLfS%j8VP! z$gq0`4k+$kVQw|hcFkk)RXdpP_bw#TU{OOgZ_D1w6T99nwnhF-|QUB40ef6`W=F zJqtq%4y(8wdh+fK;pD-AgSSa(Moo7B3C44Im%YMrKt@5t&3}A`s6!B!82ILL#||QV zTW@F4r9vN==88sFIn;QG&fhp`Iy1PQPF#2#tU#=pteouKI@w@h3+VuAF4fVfA1(2< ztrf_ht9hE;r+cV1BNxIKTM_TkSWuq4#z=2bMT=Z_2heCD` zcKaka?L^bD&yW6Tylv0nL_P}Pw*FV2CcYvf@=yi-qSbi#FB<4%%vslS4%UVdRBQ}M zHP*}=e0P%`;pEkGyfE0Lu%n?&P)vxrN$hgFeUg78;}gxz^2-xr#@w8~YE#V7bEc_y zTgbF(sPoU>S{r||Bo@xaEYm{vC|XG6Qux)z;$_3C7MZKRJ%Cud8`L!sJqyg^J+0xf zDYStJyESY@kjHgHHWXbj|G-x>ZZGGXajw9GBw@Cr7%KU2pQ%SYURzm3Wz0lxC^a^x zwBVZ8P<1COH+Q3TzD}$i=NV&T;~##GkovBUSLCo7ZleIfwL5}>qxfez4zNyzt1gp; z44=T{ta`qug@9{K78gkP{5)n1V74dhsfN+#XRlPsz}}mWy7&rp>+H0pY?b zC0SPLcE}cY&*hR|azy_x2oBNZ`;Q9#00fm1$++ZQRQyDmz}cu0!(?JQel0ws^-faQ z23}C^!+?H#ql}!An#CY7X)xDQ)n`BFgk^4}P08u1W#8meWua%|2shvCu$jt}F!~hK zk!<79meHPPWt!t6x2BrVqMFg5{1j3$@MGd>Z?;M0oo@ls-|INtaJXB6@6LrcpS7dh zLOYiTSEP(_F|MqApRS3Bivv)Z9IF6gL)z@(XJ3DUyqO^IVu+b)wC1!%C$@Tf*%-hB zc}8EA$dk~UnVLw4wMcN=&pd(taLFFefUY{N_P4Ni#qX4qCCJxu?e_Kmg$BWHXBb#t&kf`W7i9Nr!{+; zi#jjdawNNH4>P~gLa^VnDL@#II1XlsFLCP95J5jmvALDPHe9P(uC>yz^wr_2xla5D z@f8b5u_5Qh7fz7jk)ON8|zC-j4RB~9r(+<-)Tu@-j?AF_>{>XbtxE`bA;POtA$M+c(p z+WLB@>2Ve^=OQ2^ab@ZlHYCOk!p!R{C}ZjX7(-j}Q>;>%U8ZD7vLOEceEo|)gTARA zl8}Kb^B$F%4b3BH_#zW;6%~(x>wzRMO}4{`nf!|yQ@bHCD`dT02er@6kKcqZ>Ys zcf=*I2zw>8b0NN-obCT`mH^`o!=qC8aiG92xJSQAiZxn&=Siq(#)j4qEw!piFll<& zi_J1!?G>`(UU71h2f#W)=DdyJKm|YKMs8d&(l~1**~irC-pIZcfd#npbGGZ(uWERF6=0}R&K4@~8|U_!5W*3Lni_Wdc3=rRnPCamJInlz z-B~J*_w&i^2V?hiVzoS&6ynScs8cdxSv*TL8JRqwj~0pqS&458&iA0n1Ep>n;cAePUs!=Y&*QTOBOikIGvCObU_naC4w!!EF7NglRyRIwWf-K1dU&FJw z1~J*Lq}ml6V5;O?C?2m9PLnZIvwc5wOQi0Na6!4Ko10pROzy|0?u){lRLx($%VJR3UcBJ)iE`pbjMkqMBeg)KhJVjnXvOYp*_}&%b8%j4v+v8_TK4;ZeEWOm30Gc#HTirm?t^iOwFe7_4^)be{Zbf)(shFjg8Aa;AMI65%yenTe-t4)N8iM0zGBXUOwTUxG5JY;p!X&feHRdIf0a( z^Xblt+=K!nKb$*bp+oI4f_-G2gYV)RWk_@MnTVn5=!YSiIF35Kw@` zzf-{pQQx~_-7}e8WF#f4Y8>H+p3pmbfnDp2<`vdEvNGBP?n~i#mP+i9BH{TCZ(DY` zo?H$O%h4?WePXrp#_z6#x%Lj5IV*ztG+T8d`)iMIK|w)b;&a}e^r91{^D?|zZFNaF zr!x6b09gzX%D{H@9E#EI3t9iT57#2x29%OW8K@KzAaZPpnGcQpvQ&}r{1Xr9!`Ag1}%gfYhw0Ng||??)Rf?*S#6$iqbs?o_7cv-?c4 z?CsXdfJ&xZto06hIGn9ZXfuzdbjGK;=pm(T+#}&>T+31tqxK z&o5{>!vX6%`WaEW*mFRn*xO?U$d|Qt+Y)yq9`}A#4bie0~9Zf`*I$JoiB2vRI zTAYejXjM_xz6GXU5*hPSbpzXOkKTSGcRnmgRO`A*No$gO{Y-QFss~-vF{0{kQtoL3 zAUwZk`T1ATTmSO%+LgH{Jv-3!$y~E^!%i;dU91~(!eQ8z!d=bQ&Eim%+_*s#Ub_kV zG3pf}rA09$a9$a9o{4QJ6?9(7>WAPTEVe`Y8uuDxTvjJ)b@YAkPJ+K{4mkxpV*~7| z)s%36cK%^g=MzA&L!Puf{{6&?h)dtswq-)RNPusqq)R0Cis}FW&ugYux%wVM{MP zh8S|f6{xL?KkBcTF7$ej?*t>FhYoQDqXi2K=7P;x)sJm+Dk*3~u4Hw=l2IJq>kj;O z`)-&kSSmikaI+fjG-aho^(;lPM7z`Vd9P*eDEXkk~8)KUP~t0g7xI^pq!9@yy48>FYs?3P)gQ)Y*P)3Aw(gxM8G8GHL; z!D-~RaFcrGTy+nh^rg{?!iCAJw7R(&Iu*8HEIQ%KuAq0);7NIT-n!tddv8M#Gc&WT zk<<4H9Xt4do?Y`p&{ONUl0F~vqJ?qQsg&a7hy_drtzWz3i3Mnvo^}###}|1RorGQ> zbyv0C#4A%x1-O*44L=a(gDh-*BoyW1k25kdipMT|+2$CA#KVA}MxF_9W8zuR9rXeU zyyeVwU55EYQT^5GWd)adIpjpG38;@aUruE>>pXq>^qrsWN?>S1GEiu0n`sG`7wbP8 z%ViNJX#T`6@Nx1gifJhYmY0`jsgpJbckz%1FTwN0o|3mZyu-${@Nd?A!?Cm+Wc@D0 zA{4ale7pL1+sdCOs)c1b%5zvkyPPw|hS?~5T~#`hz$eu*lA#=%V+Ur=J)kM53+q1XC8?9yR9 zY0UeXrk$QFWn*xgU2rsX2Uh|4&}vapZgTMYj9T1Qj}*NueYl&rv#c~?TEE`7Y@ zjCq!y=Vy)3TYKj*HVU1HnkmrB(2M6u_0s^LEnOWcAM@T+zwktp9foTs*tDnT%%gm# z!k*L2*!RhcUh5#Z_>A}!m(^foOPi18Z1cf|fy&5r7MY@>+~ zcbpt>gHQ&LCt*A6ylq4^61_&4K|g%dIIs1Ey4$fXb~+K)4Q(jIeDKW$MBasl33EhH zh(>NM6_-~TsrsOmd5_3zZzM4b_1b=?;M)?}TFi)*^QjU#kAuDW(P;F`(Q=dKsm6)Z zZJo|!H(g{x>4axOeLTCh^0yAAf;n2i#s=OWvNT?gZnFd*`ov&l7X;i>q1kI?TzrEq7ET$g1NC<`m=dC_ySJ^YiQf zPMR}PsRh1oaG=Hul6TYKkd zYTG{KfQaf6Jsj&uodfPNhr}YgYmfH4U#<=3lpgBnt*frE=6+IeYAQZPjg1f`pi|=N z%!ICXg(OtFcZ&PM1>piij|#7?|7=V;tF{c+GqX@_C|S<-zd7 zvgD78(c&tjDW}x{4%ngO8J6yPyS-}vPb&+aoFf!Lc~>US2yag`CU8q@C@oZE4nz5x zpV#SEC_2l1`Vj199P6eV`%M2pphanN3KA9UqDM6cXHJf=1ve2e?{%Wo|z4x`5 z{vPx}6P-spu_XWKmN3i0K?rZm7(WJFmeY3{{ktiue+oSA=7q$W7nti4fj1MeC&%2s zZPb>L`ZJbV(zrUlu#qbMB^u%T81f+^#+Z0tjrPMxbDXrpLsf=}3;OthLT?N7YNUof zH=$@3*Tdof;02*=RmK`}acI|;ZgA7f0hh+g( zDK@+1tk?<6GV?dEMgkWf`@Q9aLTx77d(5kdr;j*PrxI>ceHgFv^02Ta_zHv~7zIXO z$j`R27M7M=wYuMd?5H1EYL$LO+%_XUA$Qgwrh$Xn7=3{{XEo+~ogU8ah=$@HMH&_v z%jo9jwryM7`8fZy3KlKtHUPsEHGjIyFmE_Iod8PJCi8f% z&1=VKYo+UzO+qC-7iN;Zv9WHplzf#n5WQV=OBzEq(v(K<*JQr*?3493Hylv^2m+{>O;xp~R7JNNhS2RAMz?^j9Uw@VWkH z_A~+7?5}RcViyU(IlC@DgHYe{d`@qWK(iY3B9V@x;Y+r;@n{*UZ&vtkBiSIN2;hYAbxF#to`uj#cIW&)08_ZO4mSf;_c2E57{(xN8&QYtsAc3+kuqA6}bV+W%GIi8^#|P zJ}^!>O&#`fke}jLW8y5=nbWjS^2F7&qc{xo3n3KLeD!n{Hp}JiwDc?DSWg+k0$aMs=?H|!cTLAqVfV7C_8 zYCX2?X)4{?qEd*-;3R+bCiG7cbI0-lo1o7w>iYWZg>{&?1&{kjSoby{XKS**>yRO9 zuVK+wT_zzVB@0Tx@vq!UR-gti20&03nm>5fVn&y`FNAH@B@Ly_nP*BB3-kQ`!SB~0 z`K|Bh|xWjjGb@}}8iaXA{p8mg*y z&^4P$aA6^#F^roKUFgir%oD&@hKC;WPXD@e|MV;BTNk6cbMj%oF->azxEsxf{*GDwZ~ynx z9iaYj*DG%9{6ALK|JYqUS-Zb(ij%L1j#MO+Pa{M z&wlz(NVl(#u5%Z{ax4^ga+m++@`&9f1v_3Q8M1SNga79X@TZp+Kf2Bp%EwF4lc5;y@dxGLKf0iA%F%U#0%iWgr#b$$i!#8B{7o;crS?ys{eS%(({{Q5$|HW6~|H96nEY$yp zVMl^kE%WXVW$}*^V7P&Vp#>ve`VX&g{A;hTfa^TDoAci-kJ!Rd4J(k$=o9!E`sI7_FMPWH^N;*r*!hz^^M7II&$iyh z|Nn&@F+<^gm=d6s9IZFXX-pw!=|RDh)edjKfPuAWO9D*=iAwht?*Fp zUC*+_$;hDtR-n_X{CxGNRJ459m)EaH$St}LUQYMU7PERAcdf4>4pgoM~^e47q!1^p2MH8RW5ShGYU z?A>VTcplUpP8MZ*3OZnS;`0HP%4cB(@-q#jth$A(xpqqMorFI z)y74hYX=@-iHUmC>)fc7iQ2abE(d3h)NE0dl#Coq8h~|u+o2AtJl7ek()Y<32|Pe= zeGN#ymz_P%SSNXYxI|D^{hq;Z$zIqZARLl8IBk`C5$K*#h+_S}NqG2SVF%074oF!m zq4tWytgFe%B6@XrXYqGUe1e7W19vL^B`!0ygY}BH#$N-rR|SrHIX6jtlgB)Bbvb`3 z0~yt@#^7tMhd6}j5Uv#P_9Nh{bsFy5t44_6;NT#6q+vzP)|d0A5r?a|VUBb_CK5M5 zhlh$rxFFq_7O?XPg8J-ZzbIJ;mX@XC;jSe5AeBExUuw zC06c182!`R6o0JxNkTbGdXzY)jAkImi8F=y`~@0L)sZ+~e$GWSB$y4 zGvV|W2<&9^9vG@W4yC${*d4ID)4rdq@?7rFB7*c@?@%`ET@>N~ap>f%=00Rth!EY+ za^&{augd_g+o^>Poq`&49y6Er_Dd8!U54ED5;i-J(aybBwfF7kUm=OtXh%oWe`N2Af{G9NBs^PA|!oJ`7vl<}n?FdCWP z_+5&}Zo-)J9eeoe*AD^5G4~K{Ko|BcJ)H&iL|Q&l3fPpA<*=|YN0HI+#>pvJ0lWLP zt=o_i_0$%gt68c;lP^&7jw$93S(4(C(cLY!GW(BEON06u|lk7 zfqZe)E`8o;KMdrK@(UZZ0cEdn!HHTK;0?lL-tIhzolI*N9?CFZ1FGx!R>BgZF36M| zZHkm*VAJ-+-DKpM16wjj7BY3qi5j;QywJC{0J{VnlyrRz z%?&@m-^*QB(^a|F1306Ma#>$x@xE;qG zTo%idxNSJGC`FVeLHqaNJ1(mJ=i)$|I^5z|Rrc)eIsbUg#VLaIn1-sKq^9+fn*Odb zLElsoIF$95biz&kxPtHFTld-Va=f|y2Y6h%(=t!*r$?!@r{W_c2cBqC=|cC-1jtwK z{`HtB|A?Vn3tEN32)?#mxg9EM0%e(TL)Fye{17{AOg~j zlt`y^N=bKZLQ1;3I~1h5JEXfCMCtA>>F(OFf#2=(6`%8a-*e8)J9GcB2WDX3pIC9N zwXTc*{f$p-yj#ZO?&)va*jh*AeM+9t&+_OIW<0KjME}(Xosi>&p*CPb2%j&QfsefMJ70d`!5}>TF%N=zY8d z81h3R@AWW_zRo_SgWHXP^2j-zLA&cILqWJ`N@tn6ubiGHL%p_9@O8oL!E+hSi!@)sYn6aMDD#YW2`2vPH~!4~8g z3JUcZ@c6EwyX^cDD-?pg_Om#L9CE#$3Q1KT2gK92hSH?owuAZYS>9hmLIN^Saq5$n zT@JBwT15u1FCN4B*i(ihF4tgWO6c^e|+mx67O8lt2SP!0=b^iw|&a! zPPj*_B)jrQu0U-u98lR?JXN*5f7=7*P20BsCLc{PmFq&!JYOK;3%%wjIcFU#MDqHA zfv)WH?B)OY2ozHIgo1K!*MF`FK7$bep6{aH2U7f%O(kOO9bUU8AX2X2%k_^;COc)P)-#pCAeKRea7dggS|=ak+dm;SzfeT zI3#kum^n}OBu?syE2q4yGOsM&msp6a0~Vr4fLPV5pt%+n21b6JLMHCf%xJ%;{R(6l z%08#iWL|$n=JoE?KkpALLFNXU%jFbDsK6R9mhY;WX%JhaksrQ?fvFtu;uV3x%;OkZ z#OrNuI<-%MMc>BtDygz!hmo-`CKjnjn?Nc1lQ$OFO09r4c0hq-g!lfXDySJ^W}jAC zUWWVq2L&1^NJMOg;`DiKkbIFOXyLqky(eNmsOZ}WprmKgaeS`qjK*ARy&{ML>d|h! z#oc~%yC&pOh+_>Uyn8S4tXi#hZ))mP&kJ6z&x8Exa3_jvjkS{Ky?c?quGi+qR*@Bl z3$a`7G?u(#i`%0}^YwJh!uLA2!_;h3C=_WW9{Y)=a?+2VTXV z)AnHPnH7yWh}Lz--SM0ZRy|jE*N4}kP1%9n*2iNp8I*I&xe`vrT0G80uDoFEkILjT zE!Rg`4$PvDWKdsF7P1B3m;dDqLXpEvv?U@rJ>XE8*L z;C9Ib$uhhkZvb!d1NZ!AX9kEEzGP=5AII>Vpv>Z+0lqsVv*Y=lt<6`PsyRf)R-vl% zjAc(bGsCv_daEpF*X=f-m#rlo{53G}rQ7+TvxE~C`vTBGyz9T6k;~-KN`h@q$YuA1 z$o=AtJx=VOEA@u6en*EGq6(&LdIXdLc7u9|K z!=~@px-DN^yuzPtuP>vhF6YSfCXUWDKqHBx)?aug04dHnJ-t}0mQxUI+FpI5KRzIj zsynf7-6DhalLr5qHX+IV~-9&{Cc|HJ!)-l%oCQv3C3{@ ztMx9`Z9jQ;d9<){!bJ(Wd47L;PL|J9@Z0G9^EBAOBy(8HmM+vNAaV|SE%aka6#@)i zRyYm^I||W08idjW^{z2mEBI{Yh3A6@boiG;s!o=6q)Ae$3_7yhDMH%}S|-!d8Y(YM ziNW4FMKXS!U+oN|38g&f(D;Kg7c5dZtiL@xw-937#US?XG$~Ul<=#ANvUgp)z1$CT zv)5%YZbKDpQ=2eis_EZ&D+4j*6{%E{f@A_5q{(0LHotwdmz*WH5biwM11FEo$-JrC zA1V^TeQdNNMeL)uZqY#cLi*wPWrNU1=YVnew<@ zPM%E_Ym9J;LH)5846we=%XbNGq9pd7HyPKvhF(@{eOCv|Lu)1rJO$OrE3R72b8T-8 zA%mt$H%S39qZaAM`d%6Ar`tMl8KV`t`t~tBXYYRoygteXghMX&W$}w#W%yt(x;CTY^6yUt6sE5$-10(c1yAK8<^vGq_(k)ijfJ_R;fw zzpOI3IkxVE{Ybm?{VfU!{8n7r%^*cb4`b_Daz9P(e!bOlN${SC_=tAX{E=F@w&w@s zxJa8tRFH&90MmN6lJRooG+j}22eS^q0%QXD5)?X_UT8pYa}e^Vzu2EyIYEl2*UB?Fm~E3GB>%1i4O&wF1wnSsvApym=)G;1Xw>mOFGct`<>>$3i^Z-6L2^2q29NRqG?dI{ zy3uQ0uBI}Q(!3&>;c*4KqLess-*0?6zi+L$(sH1;<&LG2;qj0gQy_WU`##*|!1ngq zn~xQxl6J<>+hH~HjVxcj7G>&RAF8PU9o@AXH?yp49!aNo3>4CR_oI-!v^|dSKGs=K z-yi2Zjsj$xVN7SdlKczY70&Hib<{Z+IM{<3#;MvpQ0+zl7Zvc}C`R-g53l>0k7HI$D zO^=6ttwQ{(Bks5H24nAZ7Iu^c1k)>nMkwmMSG=mKI0rpi-#9OWzQ3y50=p*FZ%8wH zH?X7y2~9WiFtU7YZYzNM&-(jjqoY<<4Zp+cSu?C2S+y#QqU(Ry&4+J^8X$0s;<#YZ z7*Ht6M$*g~hg#TRajRwW^%3MCt%^FE zXGQPCr_CQ7q=oRn%@*`stIBpbm1}9o%^hXt7$XeX74`-#T-XNrDGc}< z4?Kfbg@d{aTrFNtx50Ue5oFL&1>@wXjNQ)8C^90=3}mG8QrDY%QwEl!JA!`-OEpP+ z>Xb!RblDG^*#UTvNakSGr2SNmy;t9Wv@f?M-8MIIOgA`mVqiQ1CB7ePRF5TeK-NUyLJ;jK%7BtIQ>RYt*_6$NZUuM;6k- z7(-cTp{V4QCQVIUcaZMu3uT6*<80qtT&~F_WZ0j4^3%l)oVz{6Mn2)AmfIiKcaYJT zvTiqt((K&urr=|x9#`wbp47#llHBMI5pUdtX5*WCryq>)uO8Nl%c;MQ=v)GdQ~aKf zOH`YWeFV2{!m%s08V|;n(!H>Rm@UBI3qFU_E936l*av1q&v(}%cuyhIYCkIO=}n9% z$!SZYeVDNVY^l-l^81TZvB%uaf%rMcurjS4PPsuuGjCh)~rBEm<8n#56W9oK2V7#zsXI&hK*+j3gENrQRY)q0NO@uD`m{UcO3s z&xIra`)4ySeFf&K40nCM5-Gf`vbWp$t!y~CLvY07Ixg%bcI8c`-ZP}qCIst9vl0?+ zx#dMEm(DC}Se@c`aO58FTB~i<7$G{|zHN`%lqX4r{( zj^~4r5U4d@=j1K#nBeQDGw@#m@K|l=wp{03HLj=M`6T&}U-E*L*H&MZ-*15Oc-2P; z$<6!m&dN{)`E=R(qANf5M(YzPuJr?{=Q`n4I@5Hm%P(S9$8L*p-wyBl+T@4xj=Lhy zg%u4~v9>u@(=$wRliE}q{wFSgB9$-^L)W^m&Vw?NlGz9lbcwwIdn5mi4YH+@aDW&_ zK@8q4jm5og%xQqW{x!g!&f5#sNxi4}MRCAIY?W!<>s!qvVr;Y#B3*p<6(Ki%5&{|3 zIuIyse&1kavsw>uWHszJz#qaZzOd>jf@=SrO#JhdsY3&B935G({52y~?fi;bS2jL; zUQ}qRj9zk#fq10W&5#rIeRv;%$=w7@3W;Y=_+gFf1jX%Y`y&g(^;{2elz}iR#-pUu z2O^GNl;lEY*b_f?RA+_I@AOgLzgs+sF)&^QLehy8>-L;HlkpmL-TWqldq5k8nspMJ zwL0#z8Q-n77s(k;hL;eY>F(s=md4I%$8(y7t;xcey?GQqUcVlY@@5!;GUO_y{Ixmy z<8ila+77^2;206^l8T)o{83blArKdkKTfF_){J0&+Oh9(l=5$xqpg~ku2RL>$D(AM zgUzviT0DFnwBP7OtDgw+rXqEAjmN~@p}S>5A>g{@q0wMZBuo6r{~^Z7HGGrmfH{KJ z3>;R!%;XoPTBi2vqGZUHukYQ;Hwg@ct+2ovT&-Jz_xK}8^oz!q4yipJ%K#I*BaKBo zopwQgGjNPAAk)!dSjc2Ng}yQ!BS8TusXt`P(sf27rMx)LdqL{JvG6-=k@ens zc9p{2gTGDn$9)Od!na&?YwUp|2G5svqPnZ2*9Kp&5*Gv*e_QDMd9)?T87063S(K1Y z4{@nl_Pi}PR^5F&A{tCHc*R<8)lBdZmA>FXz-bd`%dOw7oJ04Z*#7W*vN5m$@eI5w zhPxyGPb#b^CGgO$sV5=4Lx75!x!e^ypVAnN!*JXK^qx|)v4OfC)#&1jZ>((1|5)~C z(B!^8&Jt=c(|;X+4gAXrlm3AoIhk1Zy=TN+FK3da8uO8(Rc$z3VPEL@g=j8QZ{4%c z96?u|FJD=Dzjm}Y8e;8lB4M=aG)Um;BgnKoCDpL;PembMRZ}h2hr<>EZ(rW0IH$7l z`sFynQC-V^whq4*<}es_m1;>6MyS*?{okU+*OJ zX>>K~MW8?8xhHnvB0f(3JjBDJ;I6bD@802;;w_<4zVz5c(K-MxgJI)j2pBqwJ_4Cn z8lPt}?AGpH4Pf`0nnruT=U(<_G1GQ+en9xnCT3tTBsAh!$bI|H*mA7aJf=mSZDB`Y zW%T<)D_8MM^*0gF_;QUY#uPmZPspg~Na%y;+oTtb7S1^e6S+54bKLV&xL+H}0TD&|PA1atm5^kI zLQ?`n!$Tgmk=Aiz1OkgBPkP8xQ2LF@GU7ap&gMG)nk#`-JM@YO?8a2pAB%qvWkQ0_ zl+g4b=Ghq{nK?ULi`}~th+4H`U$jTG2aTv5J8k(ubAaA(o9Vh+KAyu1IMZ1Ghg5QR zYofnV#UJ#oXn#0kks3&Z(f~4?NrlI~1@BCT@y-kiTtn&-o%U%?@0%#J)GRYzBwWr! z8c>nNBoFo;M+NN1-b)qxJb2ZsZONI-vfFIrzy5ZQVl-<#Z0|*vb0U|1)6&qx?fyu& z>+!ND;6pWO+CTV-Gml%6QopY!0Ghl>0ou39m`0g?b@e(Y>theWXnkVm?fvPkJM}F- z9EMruka+mB)UXcTEyXwt3=EJ>#h_}LCRZ}MIYaC1ZOY_U)Bd4{RL{PMurdh5H<%@t z(KcCc&7YqU8F|)S0q|Di8)`+aQsX`uMu9%E(Q+N{BE>EWGM#oSdWo$IDP24~@Pn$j z_LY?L`R(ljJ8FO5kkgFF)Ugp$y@4$(_O6kv-KlE3 zwaNb|XlS6Pp-nJ|n;F@mv7eq+$Z5~da$eHf*nIWQFe#B`fp2~L2kf~-)p$r(jfHx_ z_l7TDFk&NZG{d5Ntoj^sOCNm%MYq6R-jJ+fTbMubt{z6@wHjk9q$pXeO;yVh;OfE| zOL=*F$76k@mtO4ww(B+p$TGMh6m)5NtHY}dEl13*PVX9jXOkZpwNCAB#KMc4Tg6b; zn*pvc-sKM0%#3D0$n6rDonx*<1y?i~82SxJ|7nUr_b-qp zQRi%(W@L@@ssNJ0XoH$)TRn7E9!T5WlpH=BF`$2)-u7@y+Z35<5n7}&g>4dZ^b2;< z_xT+NHK_R#KPKy6n+(E;^chTlp5=P!{Ogg97MaI(k z9Ob=UO(X53^=u@&<-EyEY6=d-Ts7z^fqmvnE1O9GH@R%T^4yFtI_<3e5qwRqF#$f` z4sGn5u{Vwe2|~~-2VY%7jf_8>g@7Nvg}NW>5X(!SZ}r!^KJpWfNBZ6)kbjA8Bq&`E zOWPUiAz~>4C{FK%;6M?OTPgii%p)T9(12HsQd3B+`$W+eh5lG2eGF6dA8S~*fWe&S zOW*m8Ch(fSXjsgon4V_)!75DT%jEO`#f-YV=Ho05ZFiA4>()ajM@;V%oaGkyC$EtZ z{9Oq`ARZEDB%2|6lpd!!cv)3l)wePdfiT~YCmM#|ePJw{!Q3D2&vT4#r0jVK&KCtM z;xK&m-e~Ava9`9__CFU5W^BK0vSskEGCc1xa$a`CX}um^a5^lLT@`D{_T&D2Zm1ez zgcmg%tL}yzq>lP4nOUH~uTY5jq(91AS$+5$vOj|fmU@O)94v*p{s*VZgh8wLma3}@ zqB72_dJL}LGKdBVt8V^~8Z~2lRLqkFcQpVBi>2@D_wHBBPPrO&20>$147}i*m-uXF z=F3?}i3kN#mV$=;kub5+H@n8NY(%-mNzPtlO7PhoD`ex8uW6P4n36xq2H2 zlQS->Ngx>0ON-X1d zQWv+ct@nOy93NZ9=0*-d7y5bG+0!}c+dw}#Xn8yM7Vva)K!NF5rvxTC`j6Mg!npic zv;lY_C@AIM_C5&RxTbyv73vMxn3l z4ojP2$Na{z`XUPCv-~Ps;!Edo|NkzN2nzh@W>7@qnjxj2N|uoYc5g0NA0vKpLC;`# z(FD5x>%QiO`mn7-yPE38xo&$)F>I!XQ65JY8AwP{89F;Ky(!(vz!vH zPjpJXcO93rqRikiX77tQ&?IA2Xj-vDtZmIo>z7>ZX?|?$?x>C6HCSlnWcmdo5m3x5 z=u5`4)71Y=AzxOjen#l|lfU_1`+=mpX2J^@(N1$SYi)2EXmg&LlF_ORuj|G)% z#xM%Rbm8hn^7Vq|=cr<#CAZ7ay)PI&fU_i{ARBFeyiDgt0xB9ngdF|yjCS+z6EI&c zhGjvkjy0VAUP^tgPAd}wJ$wSl6Z7*s(4LlHm4;-rLgDQ?!x150>PAl*QnNE>-v}W4 zvzy+0278gyMwkQbLcTur;d-eU0+;l5IJSeiO7vT^DK!N}x5+}IXNm%Yg>F|BxpKWM z;S^8=nhqe5^o!`0y$OPbKz3&dq0mCg zN8T~2oF_xxMQ$UO;nnS2Y`{XyV&%6$JrE1eI{$b*<}JSL9JDmq$nySijn?dWhkt6? zX^0%Z!sVIy<3lTE_XumJUUL1nO9{wy=ME>5Uk?8rP?vaq5b)G2T-FvFnE)I)#eOyQa%E zq=1RAjZeWOfJ!$qXuIUO-dALz?ATb$1YAEqzuuXd2tagF8R0bS^>h@Zx6BEH@-bW6 z1H^vF-C}%AL|zh2=&?|HED(M9eT>`TU{%fE{;&Hs5A}gD6t@YOUG{X}oy$+RkrH4w z=$8d&WnqEx27i7guh*$c{;!ar#ilR@u6)m60K)WYWPG5&NNywe|?RLVI z{(g(%FFw59?@EPPg z0iiT~7;QX#Zp7nt*4T(>PP7jC`0-xib|loBCj<(TM==uSRJHu0RXb9LT?Kb zG7p(nyv>i+l;cCw+htP<*{Qf4nSF^GtQ(Yw3k!_;$2ZONNmW0SeUkj5s7I>2zp~(& zN886tLrU!`%JFGc;8qvn!ZtinPV9wS-YuS?VcnWm1S-#K%FoSjMiRefC7%BiqyteB zZg0|1>zpd;{$g%%T7}tE(UgPdf>Y695M}x|_pe-t#kZHlpA51kIcQd=*0S4A!49~S~dj2Ff;q(2BPx1bSscf4%=IJ{gH1-u%{Ja$yKV z#6Ub9``*`O$9jc;72OY&y7%0YawGDTfcr4PytDH1@_Iq3RnXr7=wrkXzict@VOaVW zj>3EMZYYr@qC527*TG21{wo0M;pO)oAesR@3;8ttR^7*scl!rs`w3F1%l!pUmt>2@kkd|O&0mrYec?ykX-BS7b?F(kh zIQEt1YnUDzr{-(TL*4omZFfcm*EJEM9oLE zorQ%#MB{y`UGHKET4 zohvC`pF15drvaigGMD=t^4Udz#wxe{7Hb;6cQ}XIfWiteI;ts#!fV6NW|f)&f2UU- zWP1dUxn1qRZ+;3Wcjbie!yElAEj47rSS*m~jFGRd{hKAH`b@3aNe=kOY=GEfV|)7z zBZ|wKYK2Q^G?cEADvY1E=OmgZ-WCp8>>% zT087s@L0Ge7qnM=FmrXi;fC#a8# z@b^2+U_znUNXZ8z!c;P_!RKrGCiZvG-@kaJEMqxe$JX@y_?`SHPKLJ-5Jo2>)RZdb z;v;V4+pL}<&ksTk8xFuKoo|&ESl!=}tx+3KpUF$9*E{-h^0>zE z8Z9PoKt%+CTGnkzP04=I+r#OGp>ZWhK(Cyg}=*4H3ozIz_hY{3sEPTEQq2PH{&u6`&i`?rRCkH0`AN~QF;{o zygRZvU{Xwt`;q&BQy2QODJR3NTNy~u@i`vEZg=yEsma;=n;edW2(4Pi60H{#nMBnQ z@KQx~dw>0;$tg4ty0;p1`PkVu5I@9zs(c<_S}0|q`ty;?e(P1z^YnV_1ySqPQ+&sk zLKd0rS80Gr@J&rxOHo+C%PtEImFBkFKwaYTXjsC(=sR79H!vcntI@gK+qWL4z!ogM z-)F?8Px}U_V+9{h5A~9o{R+!!dvvNv?_aN67y?5V7AmDa|336t1T!Nwbx?hKKuJvv zcPcRS0#~54XE>E9jmXR3%NecXk{m|&K56)$ZM2biDZWK1Hyc0 zCp$w3RE&XQGFp*xA+yYbYqnN%mtGbggx9n;&AIRvErMULc7IYyXjd}s$8MQ>0N$Eu zU#95;(F_Jsn>|$=HAaCf!1XS}{kh4?nb+-%p5V@nn=#yb!*rSFxT(kFcWEf+psa$8 zA^c;#X^w}VSBv2Iw;=weeY44*5%1(P{|TE^$$@TzJ-ZTdN^B)q8mOswdJU7AvL(pi zW6RS0bP*^fnblNM0pgX(@@PqnkKd11q`GiPy(3)3d%Zi6pJ@fT_ek`*JFJJk)3W#q z@SG{SxR75u-|Kjnw$7R&5M1x%MA&SZ6DRX|;**ojvhz~g$iwg2oUFddk0anUX?G1% zg8R3XC)NcTRVD5w;w9n}s6mR}yBR7OT_NeTs4!Q2)}s+E7wz0AX~5;ZSV-SKGo>0$ zGLw~;K)FBjuz5w%{;)^uc75+%oIru}n*AG~Kf6#^4ig_rc%G(UZ8$8)4~7Z5|3!(g zMiLuL5J@}3qQgQ@p9Xmj>EmFmn86K&{u_g{31)JHIA6%#kv^b3cEpg|JNfDNgezX| zu~B^=$(XM*mouL!QCUY=cXn}if9+=@H$y#S>0PENNKM^$G3-uEMJxrW00yp#iF@v% zpDj=lxqks-Z}XWN*K>;C2!5fcC^pcusvMMj9>#TnUqEo?DT(9KmY+@__*Trj+%LM* zIyNJJS3M^Jlg#Ai=i|dd88YQxtDwpcb-LaLIlxHZpb;D?Z@ne;yf`)P|5~Wr$o&3p z@m$tZB(UA&7j*vZjE<*2ANat2c{M+HzBiu-F=C@PC{phjLA4 zUHE`~k>Qmp5Qfk3Yslzh!M1$;vKwt*#GJu-2Tr>4a5D8A@rzfu77q7oEF*J84q~UY znqcgv)^|NX`@m6|?UExn1xsc#3=^>m;Mr1#FaEXyFvfqR(#3)V`W0jo{&vWcj8J{b zSPPpJXXhxHgM+WgjqE6&aeLgnL`)AaxQ3~$tlVqnA!)bi%rhDw8`XMmW_)+To20fT@m{_&_W$c$wnmjB`A z^Frh1=18^N`&~>2+UI2Nu7F|=tH-Q-zSb?-=vCKtdX^Gpn)5hU7tcB0?a6%}Rzq7$ z3#Xs&Q}InR4W`L%>zO5six}j`EW{PegS>1jX00!-3qh9Belejr9KP8fyzi_=Cz4U$ z(3W0&7q~!>@9_>^+sSaP-EyTajmYg&gVdvJcc{z8n_JIi-pzfk;Wz4KZQxtl+ncv{ z(yHT(OmBR(-5%5>rFnmGN~eW<+V}nS^X7Izv(DJ~JmBTKzmd73n1EkYt=7Rcm}vOf z>EeW(ooK;ZH=1>-A34O`5&3d_54{B}IKn6A-#xF#(Xq92^YW5Xl+|H2NfEqnI_}g% zHx`drS4nA0kVh&v8W-J<+dkxoddWl^{)EuUGQ9DcD${D-dH8ib3h;U$I!*zt{do86 zC5(#l)U4t>xdi zx?kQlm_(f~&u%>Cz}hEtc@y|`x6^jnXRO1`KHwFaN=Tn`9W8&CZZG+Jdcdlhy$MtQ zly|&6xI5vH?2W+e8t_)ns|`(EdQVT!4MY7$Y=>66(Nt@SVP{G$!F>zWUPDc8--x&@DL@Bpz;e8v#n8?O0& zsmG4ub;Z9|KH;;EDme1phz6NXc(I=BpA~O-1ygjwP>P3F_jM^j_g0Z5*Lny$Hl-q^ zg;rG2YK?DL+7NyJ<|f+va|+en_m5RZ8^;YgW>!a}I5a%)_sot>^J#M0>5DuEWkN%k zz3M3V9A<-zax`P zV}LqzHrO_r*pf+k)t=LE9^ZWmkjCb^UG6Kma9WdncS`M2albmqBf#+$J~}EFH6WWR z*T`V5VxF=Zz}xk|yAxuLpEe(j6cQ4mdN|QV7ML%DHeb|&yZrn9B$@EN=lKChujPHb zfw_^JOmyr0x*r6FV*3DKUr@GCZ_OBetds~2nE;ejo0OwiWDgfB50kHyLVlv^KOusg zFk~2R0H7c#$t~x8eUMdBGX4gWq0C#Ezz7%dpF3>hh+Y8np+s5HF;{V4?oFit>cuA0 zCF+vOpjJ^bLLPj%5bWmVolXy^zBP}ZNS3>UyGj=W- z%i6{`U7$)5d~eV-&XWkDFa_r8I8VovZynVdeBN?imM{Vv@At4{A z7_3-IwOhM4If;p_5lV4Iw_Ww)ezbjtFUOSKZGj41Cflm)AyTpjBsbE~UEVfl&$f9I zAi~~>!2=*#)|bv~Pua&EXj`VI!vO<>kidU|uK7~wZ<7y%1*QekAE_$v4jfpqHwl&9 z@|ALLkFKi9Zknc?`OQph#ojM5Vx?>v-IE1td&GM|agtJl3YwxmS?@BZJK8=!FC+mW&U2-KIZYQa)acD)QdXNtbO{;OaZPz%hS?j-kthWM6 z%A1p+q;>NhuPaK_!1)tc5D28ed`4}!087Hl>!Cim&SuZN%w)a>zN@oSUL1CK zZ)dYK8Q@ViIb9QRxG1m}9So*x+Z%7h0EHJwYisj~KH{+$CFlR!Gy=FB7^s)UlAS$1 zB7fHVJ2~0v;RkKsIyiFDs;VvR-i$@F+{NnC4UJl~&5GXs=v47e6s0oJm;-7ljq))OLGI|ziaq6%KqOz`0Aktu7yVEH9H-ui1k&G=@n%M79$_5 z$T1CR$Qy+1CCI4dV)wMuRJs(KO_}W}!+@3vILP=(oBC_jOr~h=!hQkGhf=z!x*t=f zk_VE?J$A1a+zSp=o0dH+#v7O%_Z(sr$c5CtUewP!H934~o&Tgly*;%z(2%ro%3J4KP?U#O9`g^Ng0Z{7){}ZIuF#pmZ zH%e6=wn~$;6h;LoP?BORQ?KW+y-(^=ncKj??ouyihy*%i7c8ke$|*2M+E19-EKC+e z#8+lIiMy4Yoa*?x{=%704IBTKLjQ-KVEUeI_D8jQKLbj%<3QmBiADrVm5@KG_Xmw0 zlu{y-m@cAWKa8SPnLR;7oW46VGjm$r>E&fkY%C(H;~uNFGyYV8LYP%sBM%Ug3(F+4 zR${0uIVe$nP_DJQ=blHe7wHil5sQ+O54Llx8Hs5YdpXRlH0Eq zHwO~(`8?f<>3*w8WC3CcN-8Sxd-)F1 zRoS4aiHM0!0Cay+Q=XqH|1Rtmu&`CzzAS%E57i}%-3TG&N`XqOy9JL#H_e)sNuYD^ zn9-&ka=k0u*C@GMlYW`m57P&)mOQ2ia`q?LkNh_cSWokbO>ZwuMVGLsO?(%y8aWvd z{=NQx|JnccVKrQU#P}mR#;?d6ED_DP`0f;vYFJoUE?K2jaE0!5(;j6-DZLg zHOmA=6Svz1r{!!#x>d_51^QT=LB#mAQoWTH+cmHzCO>bHx*Y+@J!#rU&BAWnI9LC~ zM57{y2M8CylkGc}nwl!jVlZ_AS7is0d%S0dRu> zF`tfHT(PkHP;YWKFV$b;=p!IO@=O;baf4a$;>m`7fuSoj?gK7r`~}$=GQt~1Y?YwI z{&&kAqRb$5GINl?JJ*DW%*AT4v+9p{gi_AG<|v8tn;iEPg!G^f7aKpvIBgmpHu=?G zZmdTr0x&Q@c)E*F2A%NVbhkdWjsCaS+lDNYMMT1hTvbKQHZH=#Z!mA$o^7UU($(Fa z3kiT8ScE}Mwlih#1H@@~TS&Wli1nl57=ihKA_hRpa8fI5eZ||F16VQu`wW!N2O1MN zw{lY(CE}{9szLGPClxl1+NB>I9b+v(3-!k~d+p>5Wy*-f$V9vw4sviPL=HtCp1RF^ z1a?V*RocC6srdMGyb^uLH4KZZs%-j&AOc-I8B`jImJ5g>-`-zOLY;+JeQ4Ycx3jnE zuusS)5(_7I1E!X6V&!WiqlYv9(=_qFzWaY&klweE$!>yi{f(%orsDo+s{UW*nc1~g zOJqfA0k>OCQhdV1i|3kDG&s{|REcrgJ<#%+MRlQ54VV;QZ%XC!?59dW#KYT7we63u zm6}OFL{{knD!nm~hUybZc3sfr9!NQ%hIP9{XWZuO+rD{oO8vK~kU+D(X!76Ke7q5l z&d2fipv%8!1~cl}7mx@{z|o&eAioB0V?<%@6kwU8ZYu0mwGi4`&_6Jf(Ly_2V_4l^ zmkZ5!dhHMUCxv(l;{X1+|MA0jL(<5oD2eUiwA|6rVC7;J8E%i+%YZi6pkmUuZ>JD2 z`xg6t2S&OIWm1ACzey<7|Db7OTV&Bliws*#Zp{bFiY05jHds_RUt%N zC)jdFZYkwPDI)P%u;qqaT)%4!qj%2C%gekg$NL>4tNkCId!VZ~-a3p3$S2wC3SP=d zcX-`+rF4Bk8(6r~+I*wxuQlcWw^jJ>4tj$Tyu~ts81`}|Ck;(7KD)(E)Mso}LVYOE zC4SUosk!RjWMK||wf#0SdD|Ih2mfqhOw0$M`DG|cqt0U2VUt1zpJz%!L@!&$2UKpQ zm2e1*-DU!yPGm$xgc-l>+Luyd<0iAkhNy|e#KbLG$J|O%Ztmp&8h-)Akz|)sr(A0c z|M8ZRR8Yqi$#f-V;?p-nu_?BKxm_w!o62*1Sa194U+~j$;)`+ZZZ{6?T`>rAf|{dI zmKLdeimH8zLfdg*PwH>{U(pis8^0m8YESP{DvkB&4;iltsw3*lOI@0ajk3&5XL?{Y zHpfnD`J^yYv1OoeH0N?wIvq&|)j_tMUUuxHYx4iPhQMsV7Xc?HcEHN}aXTLg0r`Dw zdirouF>-S&4}BTBLR?^RC(x$H**w8rug}Xi3nN*kA4pcyt5@0e;FA1PE5$(FY&7!? zqMw{Vt+wbl%T{bgy#ZZ`aoNBKn=!ka9I1f9-$mNUGM2zSV&1`vy`kgn^L@&IxnA9TWCk}MA7dKB{!%T zXSI2PxW*-({U{m-H|kFP=cwj>@sU#_a|Odi%Bj&;rSb`@3}cdSZ5~Rspo2z%{Az8Zle!_rmchDUN-m7N;r6t9mzs4e!#gpPQl;4*b17|I>#V zR4BBU{s94sz-UAPz;t*qqhk(`*M?ca-`v63y_5IS90p({MsAI%&nSF5SKL6xlOqx1 z<+PwJ{}TuZ3(clzl-oRBIv&hMaXXmw{Gg#VPO0)omlCD*4!37sh`Mq4Si2{8w4eZA z1+dxbV$0>)@q3?LUMgwD#Ej4?#1V0L-m>CzUofy)XYhGaCFn=Dw6@|#p@dXb^{G~A z*oqd9n}lc6G{P0WX4(2=+Co_Jw`@cb@!RC(KfNk2IpqAw6`Rg5#`zO0wgT)XVlnhU zl&VbuItwr_cvcNfEQ{NvM}4V3?3udr*>_=t^I zZ08D>D6TX7X;Z}eU_g00x`QS!fRU|iOUfMCKnahZ@yz*Vy57iG(Mk~-eA3h#;cXYg z_!eFo>HjgfdIp{Q`Fjzlm>R&b0FJ{C9E+pz$?TTWa979q>y+r~YOz9m1p651_yF7( zMUxY=7-vxQvDyd;2u${?77Y}tKeX?p zRNsVPKQzQeMacrOSa!!}IQ0FJ)G~bz32JUYjh;cIxJ&VGolq0a=4GYR6s+Ul!-XT` zLrkN_kV<--8N7joDHQ9bBD1NLRmA;qb1|en9IPASme@f`=c=a5vRv5p)!9i`H?G~9 zfj{!i$1c*sOGwTcsInrMBP{FWlQqKGb@q*YV~v3``=(v4g}zd{Z+GZhd*IWEV6~9 z@i<3b$jQjqFLiu41Y{a^QUI#;(VvZ`UH$wLUW;?RQ8`-dh6V;z4W@Z!z?|0LTBRnp z`&B6SO0{wM0Fxpf4o<$8ho`4{EGS7)aeM~?KDWA~BqJ+0_{dkXh>n(UF=ZhdX4lGI z9}|}u01)4El^Ovtimc^@C$^M4V_-|PB)N{EEH>IIw0 zXykOcj+lf**T^6~Hnu3o@@!O*YPl>IS7VLcCc=EBKIO5HddvO|7(9k_Xq5@H^8*Rg zrCCSB-CvJjLn2?ONRH3#K&90XDGi|V_9pa;rpL?9^2*Arzw=R`DEW(391C4{Z`w)>sy$o6#f|;~O@EU&|lNR)o7Zw)VoP5Zy&Cp60%; zVK$n3rJShR#6u$ZCEGjYvTPpi_^L!5qmR3Rf#HX?s!`Yncka^-32rymWmF`s4a2hD ziq6g$kv1z?S>%=DQ=y2M<4gS>gq4+vPX9|<-XVp>I$)2ZUlszi3scH<=eb>J*iU_K zz&z$V0Cy-mf%6&o{LfL<+=SU^e8o>vVN~f;WI^TXdRG*x99`KT?ci6!rJgDl{P+_u z$3y6CpWOqD0{-z9DI>1hmrX(fQEjM>SXVm^TL^`t)v6qPUt?^nQLcrGN?O||1wZOb zNKY3Jyo5?_2GVX0?OIhht?BRbYTxGz+2M5ePE+XaG9YBI64VYt% zBGx0!9dG#=@lf*Asrt`KFMD1SVofr;UDRU|6MLIdw!f;Du(1ilXS*>p#7*ONP5v#d z4v3Jrv_!*z0YRFdd*r5o;^8_-lKBk z8zPbCC(lDE+gNwEoh&#|p1I%PclC1I^69@rCBIRqxqeZUYBdCW* z3Xs^l)qqol>rS;1c;Be;}po64vn_}F^OWTebf|;b> z8`+@@1T&X(5@l)W>9V#Ob-*O7x4mXCH%^vwx%T@)0Iltw&(*y?T4DwCCncrq(^_-1 z@7*SgjpfjZMc`U9!j5RVp~oAYx5K00XLiwPigdY zgN&i$HLJ)dqd5@sdl`M6lX&!nJV&G3#!$1s(%WZ*rehp7A~(VompI8w;4sdv8qX%2 zQ*JL}bTr2lXvC%k+a?#a1~t~uzUg5f9W0Kjot?c+_E$Jt?-zbY&YynU z%W29e9=U1uP+U69{~ddF?U>l6b_xFo?x~++6&*FwM~#W_n;m>!%Z5_m4j`rWK(p>w z?6{YP1PoCfFjxE4NJW;zQ03?5V!L}*9+U?cNczPBEFmRMsf$(QvO&?o?rG-|Ermkq z5;o)fhHYMTVtz)Iv;2;hnos{mlH0dGj&=eFjxwFnr_Hf`)y*ZdnTtMGPc(R7nDUBm z%wRD0ogPl(0ma>L)-QRzuC<<#byg6w?sGc+K_-=_DFq>zlNQs+;B%HwVV~Jm+AJ4u z2PlIsX7%5i3?^1eCR#bH`oT!>UtYv)Z0{SFsX$FcXH`qay}bSP72e4bo$S3{=gT6^ z#%Kjb9^nB3pc&>J!Xj9B#CR$bZb%}k z+EI1Sa4zOwvziFV@t?DL_6s@od@@l#1{As-RDWi`7s~qrqUUk|a?5FdBblRx5Jv;5 z<8gLelMLJ{BHsH?mQscU9qoFm2-rTPkK}g@o7Hp@Tc#5`(KWIquuprK`3d^U+7IC< zg6QvY6CwMAr3(T^(F!#)S|Fd5U(g^Mk`MSRZRGfa7`AH2-_+mB%GQsTlqO-O80Pi{ z^KFbU!9`^)E3*uygj3pz(v_yKxq37XIhdt3V5sc6O`B<+^9|$t@3wq@0g4g}6LbXv{CJDSx)B!3ye2~M|AYSk)i4HtEgK8PU zR0*C0W6KHeP%2vpyXB&Rvl#lp`7dvNQZ$pph5C`QLp@nC{rFixpw?t-P~Axh^P0Q( zL9c>tlzd-q z5J~rlIIMVqKFGk0#v%IpWg!lLpvh5PZK=Ulf4**LYoQnSG@_<}hJhg*n^9XxU0t1Q zuAbXzN5Js2Ux9N5-^D&-ySTIA8YL1wt9;x@2EW{jH-Er}TD)h7`_~nzDNc=P`g(_a z#{>67SaqNeyNC$@+_MXn^5-b*Z|J@7pX%MLzGTB+s9uM;%wvjNcU5knqpk?^Nj=>o zATrWKh9+=BdNO>d4J|Mw21DX+^nBw~lK`TZk6|V1Nty|%-gCSPF>$b}XF5z8pk?@s zd}FlIE$NR9Z$ltl=5g)i0K8AG13JN?kX8G8u@I@YW4#>V@GbI&GaB3!`92^`3CkmZh!RtU5x`A0FoC$j9n#q}v z7_{kT50i#pO&`J8`mh7BbMv`MfqeRBMH-%=L#sA`#ckQ2(cAb_l4=>_jPKv7bSwk( zT7>v$u}6Kgd(`$`bDLgQFipd^ch0mAQd4H2IMhBRZ%@L*<6c)ls=%#p$4t}2gsLwB zX!wn9c8_LiifBuplmItwX8^b+0APM|Uh$0JyT4!ono-c{)hc`_Y_`4t2bK8#ZiRSM z5P1iUxIky6jqbbi74_z3GR*zXQt~E#6~$@VpgH1NCedjnfy{J{=li8cK-5Vz?dW)q zyGbUTS%uHonMb$(J(*qRBrNj$mBCthe|y}I8_#jETb0e#w{mSo`TJOtDzTTnnm+`m z*G=!odm!N6#+z$&=}i+w)p~8MW@*xJ&%bltI3EPs>bp?HWo7e5`0i4jj^__qW$3SB zI?H1Ptl>7F{*k9b4DboVfb(YLQ-b$Pc)uM7N!UC1eA zQsz5%a1<4HaFFGvJG!GQ^?rOI20hw!bdU+o&v%NA`jt8EPfwf~(oS-oijhghrLS)t zEnT8ITF9?B-P3D48oimu$1kiLSI`N04`y8L81WB>|?Yr$dV?tTyM+a@nO+%?upWd_4P|HXL5>ca@RIe z3csC3`>p8zkG;1Ji>htchb09SL{b`&ke2RJy1Ps1ZiYq#1L^J%$sr|0VgTtza%icc zhVCJL%g1>3`#$g9`;G6P?>P2%P}@UwK~Vh0)ZOQqeqRm#!m5>v6uN%dNx{ z6&;3q5Rj>iIGN^YvL3Gr!424S-gi(Er99s#x(3RN*0IhXq#`&to^w0Q$lO)!-1lk) zLBl$SAww_^l%Gimr+&OBh~f&dXt5h!)S$}WADb>6cm9Ikm2f92lYS+7TMrVi#Ih%I zE8y}1#*kwEWGS`;Bj+Ld+hrHXd>ccNnwo_B``^l}9Vxd>X(z71RT zjCm!&MBB>XWw$Um+k5V#nuvZCm;ChNVI{8lieBBU+={LkK^t5xKkPO~lX(tXZn>51 z(gT>Gsry8)yO8IuN}zgZ*2d+@wIr?;^~z0(w;qCdRNvw$PLHP@3$sy^4>!g}FzyWW z3}j2*CK|E9##o=?P7pZQ_C9|Vfx2=vz!s3BSPZ114;O;&7VMu=`e*t1q*p$e@f)1g z$PAMEIx?bpquecl$PncwR<;xw&O>wiPS4I~x{;<+E|{$3k4i>V1Ce@2Xf9DxTHX9L%FF=oh6e8vl+O`)RK?*9;zP!5N+#vbQWB zj8O(Kn+4DYKY#u_Yyl7Gp%(V3F{DHCo7Uue`0|twNF=!QQMVAV@|Gt7)2B#m!{LY2z zUIOHFF%;~5ez=~7W>d)+$|r{$u<~jWIOxiIdwUU2!(B0>h`*eCZSoTw2y+s2f0ii| ztRpLXMoP5iqu1oikB|)=ot)+ieYpi@&MsylLuH;|Hg0OLiOrfjBIPN#@t`p3g(*~a z%Fc!5?4Az-4$=?<2$W>%F&|RFZ@@OUo3?u#^Zi&lCASU7i9#HDNyA75Xr7|& zRhmG77~2^y?X?AvyIvdA4A{4f%0zp5zA;EIa`gy4Sr_zq-na5wXs__wawSxoli7ew z!&My2(eu|RU)y1?f2vNPkvS(nw^HX(1jN>I^2~hIE0!;`bqngMv5M;2SnpCPgftrc zfR$ zTILVt@p7OM37s4@&R?@x;3{^Q%0Rd9G1H2jR?Kiu z{S$(UoZa5L-45-ojBNLGKZaN_at)>O89hn9%Tz?7wfF7DT|#|x9fs6)^=IZ@mgg>W z8iAFXhD0m!(gVx5+Uj}<@h)}7{qGvSkXcr$5f^(BMp!S4>1Trk{RG7*s3yT)hw0B{B$;5t1Emce4*5JB^vwv{(5EDDv z@Wu+PX4)Ya2iw45zBHYrbb5JZ6iyb##O4W{-aL=B*cTR<78qfSJ6mm)_uazhDf9k z6d$f|()p*RSjMp$uHeU76K&RsI>p^@Wc7bqX`#r^sf-i8vOp}oIkQ5y!Y0#rid^AZ zeoBRM6FY~CFm`LUa^gPqV&WT0udYG}>U$v|43HAle&sOxnS>2*MSyTm6-x7E)HG5w zFId@y)qXL*OBvuW)Ctp6Hjlv_?n~h=ivJ}2GJuw^L-k^$6h(TrLUz7{c?LJupW2E@ zzsl<4JribPf~mQo1fWn-kT!v`b<*8fV>27G$d_aR1l4^kQkWq?yJM_WBt^;U z(cWR3(E=p^w+z<1VE@JjfE{p=>bG*3smU=Fa7PwB7&ZkMQu}?$p^{!}L+_pIqqgu*~Cm{{Xj z3oo#DTYM4^zPG^{f3(Ryc1Gc_GYoLSITYOQg3)9q&y<-d`|Nkqk3z%`gdY7!@Vsbl z$H76W(nW>aV0X``;1?M~nl_sJ5K5+#yv6~hakm*q%N%=AX`7b32;HqyxovA$1;`~E zO5i|;>gTRxu7?`BFQcm-&siyez+%X4RDff5T}y7HU5=H3CDS~hA_@}e?e(!&se;^75_$I5ib8N|1IPJ z@t0X$8b_f=9vp10@o_7Te(YWrW`v$aa#yrF<89he3MpfJ^`HGS>30VEc81&n_?0Pg+1?EXsd|OfKNtweFWr@#a@BfPr*xm27oq;8xnD z%3OX|J>jJRd9WNIl9x2q;FD!ed7vA}U#eU0ffmZIq`Fh}y%*&hHV6r4(H{$-eFt!| zrOs1#DYNRbrqN}-tM2MSicZKuofhDaie=_cYh(-ss{dVrQ6RS>gT!eaOn||N5h6C= z7B6d#(y*4$afQFeM4ZnTMBt?wuHq=*8v!zxtr-ebufr$bep<_Yq6RXQEmQsIPW(gr{ocq25!GO-1#$v8BNK@G@iNVo+2Yya6!2J=NfLoD(9r!;JO4 zLIo{bem{#vCd4e>`W)n>g>lJY)4kZb171Ha?lfWteSiVLVlclkDb6GMPDnV9h+Nl&jEO!x3$fB=8IiNs;Tmj}O~jq?DBDuKIv!hSmT**lQE%^LfB0kORp1 zGIp0D8J7YN2_P$gI#gfGOy#d1q6AvtIaGl7Qof{wJakYp$E-$+MXz8)m^lj9IZTK7 zHc$#TXS^MyRa$kXSYeuj5xPuKU2Ox6S(aAtFqx|`FX~x5YrQ?l^dRDcXkI5jMf=UT z7ZlwtaWOI3pYBp{=$I$kOdoq!TR_*(8c(irUpE~cr@mW1B<8fjLj<5f!+AZ_3?)>K z7%bs2%vk$p^PF20U~+QB*?N#Gt``3QgMZoEZ_Dl9My=KP1=~GVY;8nUt`rX~hGWc} zaY+RZmD*L3uA1Ei#AH2cn?RF~S9p1r!wOrso);Hv-n*_bHlBu@e36bAIn0d zY7LemU){D|xs|$;-?@PCTUtDkD~7|9?9e^gJ=p!_4p!Nv==y;OhB&cAuRo!&XA5*c z+zSo!E#WiGV1|)+Q_2G`WB*brkr1J!7})cvs>lbhFWYs}N5l3KywRtkOI}OVmoqF7 zEZUzgLixR@9l|r8+AN4y%k5v;9{};SFhw0jneJjg%ezhZ^Xsd7$m3yVv_O1|RFRiP zC-04`t3K$g1+LsUAKwD%Ax2b-VOn*VKA+N_7>TcR7untSN?a+HVlJjGdeyTpg{bCR z(nxo51e33RXj4y4i?Hb)U>4|D1fNY;tVzVYs#H5uxzU?9n5U~7ELoL^rfsQK%w_6I zm;Nj;{_RIIU;E@YuXJM07*hf7!5Z&DJnCrE^%X?sB~Lrhl@wNy7Al*^xXwf;yVOb68-uT%t?Inm6=B(%|eayG*D`ZK{43^xFAn}a&O->Tdu4D@1a z&zeaa*^7&dzkUlqeEAudUK}Rcvgv--2+4CZ)|S4P5c;Sy&0OxKoy~Zd#%!+rwaB!) zq*?Vl$@ze5``dGM+n;&Bk)xqjatfhYqnOt*chW1QJ`yl#6@0h=$AIn@X_u*yNl+cF zy_JqDV9eko@jaP6IRxNhKrT8poc%MEI0tnXZY+7^ZCj4ax8Sz#2PBk;Mb-S?Q5S1` z`$r(RTKEkZ4aajKdv=HJic2+QN#tTjnj$~(jxM*&sEQvUtAT|6Vhsthf3oY<#Tlc_ zT37U!4xtL&7Z{MXe6E{@JJa=%O6dY6=VLtPrH|1Gvz^#Ct$U%6HkvOJo<^}4V{J5o z9RLFF%rVN2;M8r_y++J#Ea^o4s&;n?2-z3n;v^+#uSszPds|dpSn00E{WBqA@hgdM zyG(u>QkAb5T#I(0&lk(OuzQJ2k2c0tzuY3_61EzmdN{HM(4m zmO~OBq$VR$g`f)cf`Ed>wF7VN))fmKoWXa8I*m}g+7}`}%Q9Q9p2|}K?^yV=KhX&l z0K#b=d!x*}N3X63ENWa1C z`S$R22gpsM$B5(G-|1+?$9{@z@YR)ES?QM+-9Nhh+)xucUAh0g zR{$(VG{CiT;GM+n_;yG{+D8@EOnD1%GIPeQMFmajg#D#HE#9${4>kl1g}zxt?V0_# z#Hkz%LxZ8des=Ov^U`%!Pd>j&T#PsNdL=(RJS-=+;y_H@{8^*M(fCT9GFcrNRNYSb z5wrC0EThqY;%L9fD!{jy&WQ!c9n^)N;8rTTJv(qx6Xe7_>N!T(aA+7+UX!uT7BD#wg; zZ=c!N0S{mHap6`ulQ;u4Mzrh4p?B|q=moO0IL7C#TMlB;+a@)pb( znyy4vYffV`|2`;19@!A6sTKF^GllWLP3OzViU1Jn8pPQW3I~@b^Q76?)@ertQsFWd z7L;NSpQl?H<%e-1Tx~drAZAA7kBd{67I{dUGzkU6LEDT@x4MHFo05~vL?`@{DAxL~Qj#<& z9)D)P6y9REuY1vSlhS*rtNiTCDW(*UnlSu?IU;cjPF=-w8$xS)F)}Drav|L9#yB`4 z@%YcY>?5JT4n>lp0isjg*d@-0)&VtqU~=g7Dh2zZDy>$`jt5kz!*0rP$UnGFF_DFE zdj8xfZ)^E;zi(XF_T~AJx~BB0;q1QoS?YmIu9-fiuy>VrDIF+SC%WpCHn-5u!znAmx&>sGxnF^(e@>wls|H`Oar~vy)mCb0*oI~w+r8m%B z5mHRk^?+9_1pJ8pDQ3|)UFH4W@bslSny|0?(|%r`nME2)3PoR``HYm}@#7O^&O2nR zO2nLPO)*-UThui^!<4DvL`*7uus#B7ZlFb)nM5^PcmmO&t5I31_P7d6;sX_& z0T??|(`5moM-P!%x;35+JSQNw?!EKCG!lmPaMNq~5nDb@l!k=C?@U4)ufw25X{D)b9YP@oPl%6`-tHbAZ< zULzgZA&6PTN;9p4dNWad>DGCC%wihLlb0{sATbE$E!g6=KyM;T{wg~=O(v)}fn_Wb z^>%1Mz(87^2eikaU$s`rP?IH@{N3}I2gQD!*ffHon=kUR!$1?YfP3cuz>B)n)ZwbX z+uBn+Nz+D?H^(8G$F7}MmIG;;PfE(2U^^zW+6W7vNc6d9-E*+qCrt5G zo?@od8*Flyjfc;z)010g9zj(D(spBep0HiT2&v;;-}~j_QK@($16kzY(NdmnhArmd zVO6lLxc@sd&VcSD-ldn9AXj3cqMe!lWf%>rZH&TsSBRceh8+#X2bPHj3}l%IkGWDe62f1g|X zw5;(MY$(OtxV(sdvf=d$Wd~pmTu>#}_PH#J<4m;Awi9Hrc+gJsP;B zi>s0LjkWXqY z4N^Kbn~Qmw(61K^K>{AQ1Qih@t9extGHD!a11MPobS|IIO{UZB#nOY%K7JYydDe|5 z(=s@g^K&VmNHE_H=^#f-qBe9&S`4+$xW+cv;UoBBKhuXkQw0rD2x}1Cd#s$y(QbYc zOK<QRqZPDx(5|2v^UPhcXvMxNE#HihWb&FaF4rG`%BNe%_uz#7x~ zG}~d#=h?Q;;_W66cCRZzl)IYvvtK&z2I9zJu;6&_7<%8s!O4}qiKbDWJ&EJvv(cPPH^-Q}Lfz2ue72c+7U9Q4xvnz7(XOBVna|y#W@m$O^Tf(fQ;Q*e{iv zpCV$f*UY&+RV^=ab*f%p=DD}X<0=V2z_!9#fWD7}WX@~$O1Qm8B#UlE&cx^KA@KsU zB9{=IAM@I%dT(nKYYg96d-QyoA3#}fnr@r%^6B&15HgV$$7EzDBLj5f7M0J?1eB92 z%#rPtJf=UpZ`$!F!4Is;RmvrU@~3L-S>D$@$yLjh`&woFeBo@|e!7kGLjvn&GZ}ok zU=RqJ6>A1c=$6zCLl{+CcU;=vNuJyH;w9YIabFvH2JuU-7T>YGINdvDYcK}-WXNv>9js5p>C;EmbjC{gV#d}UB zO~%!yDK160Wmu`Hp8(A)4*{BkgL;K^j*x5pnMQ;Lx-o>r?#G%JW{7}(y3)pw*}Q-y z!9AsNkanprzrOo$Hmxj$TCNSj_c`IS5Zeidv)wO|Gg-9rK{c5@PjZP_t9RC?S+P(- z?~Z!`vc3o+`7f`r;H&(uSKNV`0jFBd6HFQfRCP~WyMdm=%L;qz>vqg?YP2;z=xPGrYoTIaEJ9Iq!JS`+2YuNDs1{r#(`EHuuuCDdE}fOa7LFg|X_9H2_D{t_l6jRPE|1=mrShmn^q)#Z3|<&0F`sGT zV($#(Xr=2X8bbV^=5bGAW_=B8>QAjLhx6H?j{sSfu_DEDj>4yJju#uszXIUG?KsD# zvkq+Parg9RI`VdQ#e!=&_R~BbsW8!SQhMx+qnVDM{}{u}FmT@-?`HePLKF~j%`z2$ zoxZhth$(pMmj!C-xkcGL#Vc6R2PnSR>lxaQs_D9Olo0Tm_C5(HF&hebjPP9_GAXI$~v~Wj(;$)VuEQ_%uD&d8|U`=j~u=iG8zy1~jP!L`w zdc1bu`V~-Vn)1E~`eQtwm00r zEXMh^>0m(6zeO#h({AQdOX)_|iacB6sB2j7JtKctPsC{^cO~=C&EH>Rgjx$NxP)=hw(1 zF)E$!V;|$QDSpnLOJffNfT&Pnhlw)fOA{SosYvDCkHqdzNyOZj*HnEzkC(7jWddBY z#{kO*5vUwm2mo;C1wLe{_ap$LC{Alz%!r+ODpo?+3W03;0|8ITn-ew+K*i0#Lw8Y( zD4#X+r~ZwqMjOw(<^8qgBLSm^6KN6W@toU%;>@@wER=N0VfwmUSVJDz}Yv=t_1x@m#s%_K&xZe6~ec~)3 z9$xHL7OBr-<*qUi<>xxiH_=gnLx3DgCplM6i$n>V!WR8TR{q-B8jb#kRY4G43IJd< zWYjL)8fUSsh&XyY?dD<8DUWiL9yo|ant|P<4Y4gHjsa&i0X!=%z8;h_f0# zjB<_GG4}Co@;x&fiVw20(3;J<_JM3{6wxp<_ibPAK)jA)1AdGq>xw`&S30s!U^Pn1 zn=VqPcOE*7AQwc&PKgK$ivtp+EyuhODKNF2hV=UyxqdeWMF&G|wQlBT4rfcc9yAob zvJ#cdR5$b*i{}A`xn&AjSHPa}I5mW>M_LazBR-mTcp84G%Ycak6_%Q!61}Q&@oN%{ z(xVOD0{TYp<0=Ff(|8CTz0h4#;-6xphEw3h=w#KtPrxv$;0=f; z(hto6P@(^X3tims(iOt7RM-GPzeVLWLB#5r#TKX z11?gyp0$>#&6*hv5s883_-C4gdXky))BSXqHmRne*L^OL2>+QAwHgEd5N0(Aw_Uk> zrNk+L^K98rY$cC`*`S}M`$ix$LzXVOQd>s3IO~LQK)My-i!_THGQempdms1Rw=k;c zs<}4AP5{-HH}lUB7~bS?Ha-+XZhfuNv^q7v^pp7j7V~3apet zu3`Wp(Mbg9~HL{ z0)Pvh`pbwp+EO;NOf8+F{BMf-UIz)k%8*AnVN7mvovta#)vct%wLN~58Yp>wnX?+8 zZ$DL4Gq0_Q7o49wnns+hK0;wXnlff9ans11Bl)r0?kI3m6Nq-Y-xR}A)AgKL@zhrF ze#j5?TZ21fV%49NrAx$vFf+$Ema@(0@vIgK5g*Y`F)?p^O$dLuwqu`_jWj5F;UrK$ zx;Fd)e#1hz25=;f2M<0ertlUN2C5+b;I@^FM~dDu9lWdV7FM z*+R^NmQ-c7UYm_Ksb^huBg+PUp!##q2`oI#*PJ_sFaMru7^ zdeb;-AMiFlrE)*@DbeG$e-W$1_aPudlD?go*C7o?mm02*l3lgdh?eq6`egkeB+=Mk zl1Q>LXE5z7(l0`Tno*9zbHRTa=5j8n2a99~=wifcll%u0j9(H^nQE4+js6@V9au@1 z6{vpB>$3c4IFaLgy{@=LbG=(-rEhJ==c&LXZ6PjYCg15%PTC!v70O64a5&v&94yRd z00~H|MgujFLG02odjS=lN-Zm>`ElcZPto>*=4@V&**x!&dsZ>)W|kFJ&es)o3yTE+ zEN;Vn>zCq3L7uwygpW;_Uk9SpRFfQLi)((q1f)3rLubUsVr&>oolcoOP>gP^x2jq?a=T^#Xx?4!rq7HS{s2j!l~3$0hE~Fx zr)BAUT5Zc3S%Kzz9Jp(LIwI5RaWeY8XF}X|cV%g1ng%6%$abbM^N@Sf>Ba42PM9%o zRyQVCt-d>!z87Q)ab-K%Hx@d47%VPb_M* z+K(loAAcMW%Sf*b4^_@nQtPZB|3gg%#-XOoXD$0%DrIm;AgygWh+?qRRof33Aj}FgY@y>aE0mc_jA98Z7 zT`4?&T<5wKjESePaC$deSFoB7Sg+7idj5;0!L|^x^mAY87k?RW%z(N4_K^nH?%G0J zgqi3MsnS)*lF-29To?=xvmvyl>+jA({${ZY;GgC#ceZ=kARH(AJ$NsRR9r>=K$z_1dF z#||D|cuy|qG1;8VIqZVf!JuDLjMd!T^J&j=_a&8f}A?c?SiDg@$7I;{AvZNqw69v+^g zFJCq2xwuA+Y=JId%R>Qc8?u|f$H7A-sDKV&iVu(Nc_MxJn+-Ym8o31SQAHM17&U9ni=(4=j~o6}Rcu*->`Yq4d`m<%&>IQ~g<-5I zD`MAbj3im=Km(b+J-~_5t!4Z;;60b!qzt2G@uTnaZs~w@)17qEamU@;LL*tF0Fy*F zfLyvbruB)gY*0ryw&>-Ka$!Bcowo*R0)iS7FrBV6%9R>pOHC(#q!M;iMp)&5*Qvx0 z7uo=4D6ox0BiDRyvEACnWmcdX?wrcwAUCqtLXk6=&$WAIhmQ;%c+D~K<7jLnj}>WF z)_(GZR&dSQNFA!btSeVyOq#O1*^Q)U}EWL7faDtQUFE^WN6NAfPMu$`9J%WT^4oHYSnq{k3XjR;ev#=g=p*{}Yng=zr5^+H@xhh{K0Q3&|UgZ_SNY}p~ z!ct5OAqr7Sd*uAlxgy~Dip%iXx7K#LpivQk6+|&50!H=oUw367t6wEiqqROXyShNL z{@fC_mY=iUN*4T=V~b(t{C!=gL=p$TjN;}kjWQ!eux7~ykJIIAEO9InkGm#Fze0wt z*T?|GJQjck_*eo{koU*dhO;w)jv!Z9_g7kZV4SRct^(9WEyw9uce-FLKATaWumMUn z;DA#DGhAG2KmOJnW&}w6RA(n)kdZO6{zI9XPPcn5kAG}CR85T2KR@40LX=i`0;cn8 zKmQD$sfupl=)`M6h^JotT#>Gk32eZS!}i!NBs4P7-FBh}9A7kGlX`t|pv<6_^A>=u zoEIA<6q<1^xypZ9Ak{;L!Ex8G3-BlA<=A`ni_z;j~$sS72)>q3723mq+MZ1{hF>7R@I z^V32aL02q&+1A@R=Yg8c*AM^LL!2-%naA-3KVsRxzSrME^UE#%Z+>D%|NZ@s)OI)i zvse7%--)RpqiUhGOVIxHZT-2B|NT>!5^x*ye8*Uyf8Z^S)1AEh{oQW~{$H;Bo8O6J;^+|IHim$?4QT1BJ%sugVir9{rTO$^NC94`_T)|Ci$O4=Wp!JKU}^U<#(Gu23_fSE|KpxKXWHW@ISQqRA_+B$9-q>4;TGUFEz^VyUiDw zrv?3|HXm?O325-g{(imxn)646zyNH%Uz7Yc>VGDLFYy6mE%ONcAKLumd%$fFzKJuK z|Ctb?G62RpN=4&m;_KgBTj171;5Nr+taW(*nGj;A0UCqqeZlMBX7|6B`~Tr5|EGBV zhs^;fGyYHU{P%k3|4)ji3qO#I-$mxj>u?$_E-v-bLCJgntXh)^6)UYGYh(YVrJJGn zZy8meQ21QP`g8;3sIItfNM?VpE6c9~y%2^8WF(%xj{MY{Au1wbBy|U3sFcPRy}c=X z-O7ZkTKf5lv_)Px`)#EKFTvxbY z#WKT3{1y@GmbNyccXOg7mfzK)I}KV%3rtWdi*VQ#^0z!x@;VH-i&hM)X9v1DG6MF@ zF)?M+5?%T?0R9Sjwo@(BpGj!0+iN?k`a2DdXFm_en)rxRQ&HO+X@0ilYY2 z`xwW-4rcGUQfs}Q=jj8vg2(l1tskDuoMZ*x4-F^f?PB4*t9X3cp_tMGRD9!rUP1Zy zt&+>lO@0*?aGT`wPRjk0dCnMNo*|_{OSvY*T{1qZP(s!y;*tDrfPcUjH0?gWBudOJ zXEAt1Z(=wf9`xo;^eT&9T`GVWK?Cwu)%63&f3YNH8Gyr-Y&jVJQ>v(B0^cIvQ>(G# z*RQW2&SE!riMpxw+bQc>L`ErV=z+tm)P0UO9?tu73e1+2YE`}t2D(GG4^Ul^$zOoQ zHT4ctfCYU-QF+PFQ{v8QH9G0egiFj2^TZ^+rh8qPkU=l58C=!sO7`P%U?E~f<0Z3A zPhDyainLe&6LSmcc->^tXD2|#CF$B#E@?fp>T!_WQJHzs`2IzuvS9s|Wwj0WOPfen zm8OQXw03;ak*^f6oI0147~XmRi86c5z5~I_`eJD|PUmL(if5PJu&6AN&6F3M%9Mja zj2Z<%f)rGq%GJCP(n)bNW1lD-IU$mT-DXtnl4~qHk9EmLNww0e?ZgYi>|J85{9&NP zq%h(bHK#Uf=^hTOr#)nW^h&;yI=_q%gxx;$Qd2^oB0H=BI`E18Cxm$lNGY!0U(OSrx?}QQ zQJ;q%ik|NW@QPx2D$XVi!<|_VTckhCv`jkFRdxVP0%q;+2wV;|j=#>lf4{#$>L_35 z5`ss^--(7Oyp)Gs9gdt(r1IErTN13zXd7|!4Ou|!$pt(Pr#sMpXekc0tmnCM+v|rX zUk^Y7uKWVt_4;U! z*^_!~!i~f=>pixs1~Y{aiYeV2S2thwDy)N$D!=W1d$@hMGhM4)CAx%f4+|}8pd(<^ za^pd}81+|r|Kuu}$BzlrkQ1w|62Id={`apzjLH=aS=70EhR50c$Fo*cH_LKTiCr%) zEzz*DDk;&?Od`ettNHtTar_=+_huhdq$rm#S(v;U7yix*S%$*mHm)^GiHl2(xk~2U z6ddeXpXM3H|E2Kq!2^EUL@y*93k%(`^AOnJc;C#=!-GK;wSwlB=SN=@hduYm;L}@t zHVRr*RaFZod$$Bfh$RZ8_kzfvX2P$h4RqC`63TvIe?2GcW4^1264aE4Ocx51ekd9b z(3*7vRf6RjeTQR#<9FggTb#nPr(=o6Nsz(WOJMj(bbf}{9QG&ssp40iuHw;-(gEhz zGd)Oz6akCY)}x<3&T=kkWFeAmB8_sCX1!~6z`X!8Hs(FA0X;xkJEW-(G_@03PnJi+ zcK`+k(J?5^Aq@hkrnYF`l@t0_7~l#UU`aHXl6^fV)Tal~_NcjK?dQzXdygF$6ly~P z{Z_$)+Odp4dtAaxS=2UK_dC-+79~?62Aurar*poS1j!=yDfJm&TZd9#%hqns_9L26 zz4q!Xmvd#cXemU^2aRu(X2I- zUP+?rTFs;MHF-86`T)iB4T`Bxvg&8^rpl9j+WmS}ZngOy-^R3!V*u2`p+K?QI>+iv zWT?N5M`3=ZpAov@@O)9kPEQt7Swii+_bjM#!h1)=uVGyGFL763F|Ak!xcJC#h~&G> z1lln>?VG)9Ya>ep%xMx|As;3{NiQcs5DSn<$pCz~xn|vu7&OXXEMxauzsz4;KHdA^ za=3Pn6L0fDh_JK4KCJiQN>4%pPy>~Ha}4k6ebZqfHTe~nSXTmIexU^_(Fp*Xkdc(% z`KvJUlMi8+SJK2s-In{y>dNwEbB#G>A&bWi0Bs}>k{)|La!mz=XEe2>gz?+8=G$1v z^7ag>**R4CUnjqM`0!zWbNGN7#Q!oY zXyi};(4TG z?ZIPiV9dI@kkY2@HGROI8&&}Z|uhT7MGvl+Xv{(+UuQf zH)o6Q5*auJs&mg~3ha%tiC)tHBw`6ZKtD3FKEE!#%Ss=^Z2gJT=1sl(d~0r$2At+U zF%*1w$mTt9&dT-vm);=$q*qFYpQo~(UmB7w?A7OXW^~W5v;>tor=FY_vDNo}7iBja zafJ9?p~G^+5tvEJ3P)YGxZ>IVO0}j|i2<8G52z34`#|lIMydp0Ejq8T% z<-oIFTmauK%7QP>+sH^>am`MJa;0DcE2Bf>(W~c$MnxI;JV6w=N;V#h?3LgUGR{Da z`s~`CJa=38?LdG64T_Xb&Q_<9Qf>DznpRF$p}1Sl+-MutLrl%z&t6AkBu5w-r^31G zdbDb#+u)H}Eb7_Ukif@qwx1Bih*cz4B>!1{ty?FV7vXpYOc*yGNaLro+ALr0k5zus zcRQ(AB_&CB!vV5{om$vp9Xa=fnNxkH);IueC%1nvwdwGf^XTYtpmhj}aaA@2#HC%% zYqhY`d}prFsFQ~m&{!geZ)cCsZOi1oNQEm5>5N>;EU0~rIYxQfOeMvAZkfWE`2k|5FTrcI?X`wXU? zrjz&h+x@)E#&uJVsq3JLQ`AQWn`>$|bT$VC<%@U~n(l2?6zeS+5rZe_*X z`{J0mMlL9%%rxK?NiN8Gd7eg51%_FTswY) zG2M8MgLOXIZr}mofGdM_(+W=OVgCqN);9omoq7jv57!Z$5P!h?>^_g|2n^teoVGrRx!2?l`i)`K+Z9+jDLz^sHp1eYV zD^1iG(}yLdI@XQNfmRqbkuz)$@)YTsSw2F1lf4}H9A-dBo9j99?i*vZh~p$?-FvTY znI~K3A(8e;{h_*DwX!@lFl*uZwRX;lLAsNbKKXHvcQC7qjVh-$qd1T1XR7rN+c0`Bm4)pZ693KQ+oxL$UzOaO+a9iy6ZSY_y(1(w{C=)YpZZ4YX zPZ_JSi3e{B-2N2+{>~UgaevRek<6o%{1Hobbs%G6(y7OHi)iM`nG*{f(iQrP-`1`5 zgIw@9R@zNhN&v$BY)6C{#LA#SRff-IFx01~CGB80qIwI475#Z<)cjTrDWWx#PPbn$Z1DY_c(MsL4VbeaiDA#H@d;2r%+DGjqyL-0* z(lwZoXLqTO*3hM4-_{HIe$d{wV7_IS`$oh0d8$83diue5p}O{*H$u4XYN`d7lysA< zmHubYlqQYb=yT$XAX2|hv)S>5ex;@3v_KKO|MccpR^5h2S#K=_ypN7*X98I=;@za? z?~0I_52X7JXI&5(d-a!H=W?#ic_83)BTtYDEi2s43Da{=;2@DGz&&CDFlEQH>09T5 zJ56luq0(Ms!*+K_;tsd#DZ9c~(~^gIH#}wO>-}!OHA8Q@{;`=K&y(?rFy+I_2aL{}`rog}&q$U6ldLUSUc} z1(HF~eC5>DI;N)ywJ9M{X=+VR&@6{HU9-D+zyK&8Jqh$q7EOzvxP^rdRS4QEUOJY)rgx4mRl+>;eX| zXD`zQUdhbL<(jqjjd9q|k??!A1)yK2`(XEg!-qNiuJjo68=|;M42`CE3mL~0vc`C0 zVLhwl2s@{-z%QL<$Jy=`X8l^$DkWhCUrUSPm<{!eJhyeB73QhrzEMvfmUFzvXVw`) zWo_KT-k7a0uT)4DYDe|6k3S|CkXfSZm#{ok1iP(I^<#m*wmjs_Q;sbRP>7a`FY2OA zG7+<3LdU}JB*6ktSy~KT0$vTB;1}_Ab98~tEDQ2AM1yrIVfwSv&_NMy z&Z`djiC$J$%a%<~fIqew+?nawg9R3_;b5~GXCaRpuF`e2cK`OuLrbJ_m8qGvK2kG=Xj zW%h(+(vll?_=N1tkj_fVA?W}_zO!Tm=BAk&ST!QPHj!kfM z%@R&x%j+bL?nC0*DQ07qe$e|M*J@>GOWMy31=;gx85+m7nMo_nR{3t2p#az|Nlpt?Mt+yz zu4P$BZ?kxaE_Eq|OSll?FxFpF7^Scy;_P)Zqv(Sl=GQDv%ro%iu?7%X#SPDg9r?Vj zw1xeV4NJZ=k$S;e7jP{CIZ^eGkC}{0m|xiz_MU`^dG{P`PNZ5A&WX>r)WG)WGu#l2 zgQ6#KSdZkhlZ{f3ya7rZLO+Yc<&nXj*hovk;TZ;x)hIIR7t-#vtjMH1Dt)nbUh(|rX>>ta_jw>FkNd1Cm$Fc> zxZiu2yAPYubm{ABN-+ot507{l+q}v1oirz%$XWVw8`k`P}I&r4er}iDDnqU9kz#AW<{gR8vcwsWqH;+ zQeQ*;1a?7fyVGVt{BIJxR%zd02cMqC4))0>K4JjE`9nCUZq|ay^Wp=NF0VmAI2o_F zik_QP-CYrq6?8W^fVX}<>-GUrVR^h2upE+~@saJRODYkc0H>3iTa|E4LUPL|)g?L) zUCWXBtCSWxoy`}969@jMSM~cVYwR9FKvNol;;y#k*=4OU-2dvQUmtkzR1ASVB|3~t z-uS&u-7fL^%BN!;>bW__n=I}6>GH0A3-CaKDc=BS{>;#wq=6UVb=UpT?50f#CyE`6 zSUAdszL!UdbWRtX`T$jZE=Rzekk@J*(d-u^zjd^;gF(@)&)62kj>w@RS6jCQ@8>x* zqB}I25Md{Y3}a1TIZxNSb>r%O^v&%z0K*FGE4(w{xWvTP=OH1Ll5BdPh7^Q$!={5L zjJ={FnBx4C(ZwE$#hjTVq1A8 z52}fRrnQbl>35oOvFrETB;5y@@RxN|N0UorSu&bmr~gFS_Yz%sGe;;ql`A^+&D=sA zlVR`O>mBwJ;$#a$rwn>p(ANT7+8qC&HveRic{%$99~YRCyieMJbwKs}N(dg=I%oZa zkslNq|19(2_UBIFiGB0Hpb=m&ZuCw^L^M)RX_cQ&`e9dX+#}Vm{a`S(m}@fqhgoS( z#_fvbWq-u?=FI#S7kdQyy~fMAn?UAsa54myGn?G{(vZS!1?ObnvEle#eOk&Z3Splq zOq`?pe*W1|t{@f?ljGARYi)dkA_sAmDbF&^dlKT$ zvG6#bTQ?)JB?Bva-UJ7CrSVthgeR}bKt_7|C1fchN!G_AkWlbY8r+xG%9`S8h^kMf z27e*h|E#Z=VSTSQb)1MX{jrY~g<=YQ&IiwF47^ibWE6(-biaoC#A1QfUgJtW%rUJ@ zsqmksIJ3Piw`2PSmfn66eWTw4I_@zk+}e zc9+kQZ`nB>^oNG^-6{sFuhD5T(vQ1>l3w-DF~wtB z3}-{})3O-rzm1BX%!4AsF8G|Aof^-NP|&qljI1o;1PL@&$OM6qWEq{)M5`C#x_XLb zGfq|bI^PJ)^*D1%iKSl{-Kqg`tnVLW!qwYi8Y#le){w-Pl_MM5fvTm;|MR>oT-;j= zI1@XupjS*;X5DPfM!T&LFAuat{!K^3BI1Prd=mFb4o_)YVi4WMct zZBp{7=c_A=c192{)!Ez(S$h!&B;i7i`+=v<|GGU}zf#bh4MxbvGp^WSpSvyw@A<>W zUI1kD5%NGpmSYEPd+{+#ISGltHDg15qmWSxOd~+m;hL~?GVddOI9NN%9^_rs)|L^~ zd3930<1pMZB zKP1%>=hPfiWKf+M-LX{*TvwEat@<+2mXc5tbtt(J>a$U!5*f=WZ5t}NpK zz}e6B?)%cKGg)F-?RF^u@D1o!67^)W|N8<$E1cj2TYy*b zoA11aHEi9*sD$UF{fB1~&H;{+##cGZpCje@3mR-!?fGN{?J$ZvuV`ZYVtS)q9=)_s zORLD$=7Ky5el#EL#MT$hEFZ>ZAU&W8uWYvLkn&p1c(=1CiL=yA1-3x3ns=pmri*+5 zs>o$r|L{u37bVF^@@M_c_&RL=!2^r3FAn5CLD+{R6vPb;#8O10g?47fjv_1)4gSC- z6|F~pq*v#*;*XByF{Z>!UxNPJ-ns)`uks6|M6Z|j{L@< z9uH*C!2u94({bTaLu3{uq0Q{bbISD_xnKubD}DFw{aQ)&dSeTvgbhN?&xca+B|9RS1JMR#HxbpnvRHuUhmJpgQ&?upN$7XLobg;+9bwv%) z>5hUK9!Nt=BvXlNX8g8{SnGXuP-|7?#NI1+ALt8Ah&OJOvhm?Ft0cXBHm48^Kv7!( z7dJAoNm+rPbg|XNWowLB(BLdTJ9RN&7uQNctRAQo`y3y$?d(r}h@`NH)z*Wk6{=BLxJ z!wpllEgfFT<{n^7>wyGlply$K7K&F;-6%x$(Lc+gQLu<}WMR_Bl>&An6PS$1^Ba>? z+;Sn0>(1LeEJkSSS30nGV!Y}Ncm>JABzA1dM?E*DZ2RHzq495e z+NH%}?u47hN~ujajOi|WGd^YAYvR{R*V*HOKwN%p*VPr#l}QwWPv=RI|3U1|e3v;M zaN_QY_idFE>-|-RoeSUxl+@Aqj|&aIQHp;8L@0Z;ntcgTUbAl>ZC$w~Fn=jpn5#yM zTgOnyafUZ!++lYrdUXMVrRZVa!gC>KhBOmKICv|li+0824IPqhi864$BBS{jd5JdE z{XprZSL97>)xY%?6Ie_^Cn()lKmuk`5YT}g5-W3-D|P>kEexJd^*R( z+P18Ee`_$1lV_eNnJY7$ex>J)8OrLJX!1Wm2|*PNTJ~NQp*vODUL!6drFWx17|^7V zn&Y#hxAjvPX-oC${Jlzf>r>yg2m&p2{i3c-DdMnG!*7KO>JikD&U0Eh|>4zkw z(ja1S=W__JLExnOYG(W$@J0ggB*N!-XVShzuU>ns*eG({`%RH$4!XF-b+xS=)zI4hDeOavDLSnd zZ0%N4P^_+eq09Dq5FpT9rp^&*>E21IxG+79X602%LOuy#yBR%Q? z84=tjNTyMB9@VQ}a{2x0C8+^=T(a7X&|&b;6X!febp87GOl4Ggxs#uBMfQ!$NY0{F z?T;&D8u*WQ2KtI->g27s^>}qW?6{Q193$K08~7>a`EHgAxO-Sp@wY0gv%I+(cP;m2 z3hhxEcJHgAB1^l4uE;Qpyux>ttQ&>Y)>f^f>AZg>Oia#a7Y%*Co-0X&LWzf2bxsbO zA3XkWvg{UZidB1ZAs|(15Yadv|0I7oueH7l^VFB}^1_YCi=PW@7E9=g>F$irIA;jh z40QIDrUlr7RYAnH!zoT~AnMLZ^&Vo#;g^ENn4Wz4@jRYRVi9HhcOS@vBLF0V@&Jf; zHdv|bkrfU(Xe2Z;Av9(z66>}CNHC^HYEld=erypDBZm^SBMXL)-PK>;d?AM0=uSc_-m>M7VSr9Zp#n*vSlvvl>8vf&KU=u^;K0NpP{U z@W@x`uCc}aj$X@WFgYWM9uR$IaRhCfPd=2YOceK6kIQb!2j+I2JDRAhy+;6r`v}++ zJ)rNBr644XM;GYL?1EPSaU<|@h@wO0RB5^b9@4t zR0~to16$5>?H}@dS)Cnc1dIMN*Z;iWP#+h6>!z2o{QE^>rTuDe3GydqX$f@Znx6g3 zU$3vKS8EpnT7o;k4?1cJ;iA~kb6z$n+MPd`8?k%m1a z%VT5GW5%39nn?`nU=<63^`1m`N3)omeLN&6UW8b8In9h*We)UKz#TaED>)frY@Syyp zU7L3G4Nn_b2(=Pg=JRPIF~rTDc*nD9;ex_-d~q zw`m@kC{H_jZ^^yyx?CHJiEb_cNmU=XSQ^KgBJ*U+UWMFcLq2NB8gwr$I5*LA`g)bl4(8{&|jg$u&mn zipst1>6`S>8XE3J$+>a^6xYjFuUODL$U3gSr(^8e3w&F|&hXJYK94E2Cswp2)&T3V zY~O26l3Ow7V|ta`U0SS{m7hc^aGCLWjeym=%L*!2oTjDD^&C$p4S@b@~pHGD)5bk zSwHez&0O--W7|fY1_$)#%~$D|xWp22Ul>l2=^(1$o>qRGDMzAaU;mNgQ%-p>n**TLK~n;=*(}W=t7lJnYR9a+Lq1 zUb(;!2omS>a#AY&>XiQiOs*zOyN{oyVm+qL|3>@SNtfHs;8u^ZZza4m3hlq=-z~Z1 z7R+^v2PY=*aTB`sWYp#9^_$mtAl;9;22!}Q;E{G+DQsUX5UWG^F?&L-S_|ocB&*iwD_wxO%PYUC&MVy~#=(LsOxX0$|( z)qyKdIzI8mYW(ma!t~6?n&v;AD}6-Xdz)+1-;mO0vpgjFaVH%&Ja=1Ub?SP^sC(UG zuxeJ z3_h5B;qF6eweW}xsc0o}&mVL8^#^WOPg@_7U~rt;Ts1ujM(com;$8#eX~1p5c5rIV zot@<)^^BX)tBC(b(X<#oag z9z`KOuBm)vuwufoRWDs4>d{HMH0q zAl|t@Zo;Nx>>j;NsG z+V!Luu?#FQyz&QE6tO09Nv`0|`Sz#JBqX?f8_^r`#(k(3$|hcrz~82ymTf;4kaYX@ zb(%yb`#`k1d zlwD*`k}gpe=z;V+?((HHT+ri);o-2?F20&vB*@Pw;RDd#A+UZ)Z2Y9S(EXEi*L9TY z7t@z0q$o_uHCneJm(q7&a0aH)`Cv|B)Xh`3>KU+r;qpre=meI+k*ZeC4ko*I6hfLV z1FOiQnIAVBpS7M6m79Gf&3l^hIyrv1Eletg-D>fhbhdI#3{7>65)Wd;B1PerE*-#+ zeF6RAy(7WWU*ip~dug~L;39&GS(#PYD7mZC9!k&L2>3P-T4AN6Z`wM*|AFUWF^cEu zQx>SnTo8qP*B7vKnXxKWf$_UO&*{fZ#UDnB`*}H>T`T!;nU-=Q>aZiE(h^kc5z|*X93IU-o>{OguU~M~N zhR*B&QM;(7JRZ%z0P-5ydwyIg@PwIsZ72Q7`$NLco0a-xiFnhBNMVPG&c=6)Z~_;% z@LfEEuR4zavZ6iya@_Oh&mE1DeU?C1DsXdccM_TawHwYR=X=q4wI+1wb2U`_Cm<#% z`~>V`GQsD2Br-^Y*{tCJTb$H&@Drr5T6QTG#W#x%0eTX4$zk_2>1eIwI-FxIXX&5< zA>lM_xXU%7EMs}%ET(SI^IeXtu_>nl#Zdf9)a~qhDM{q{10lx=CO|2v+$sd@Fa&%o zEwW|=?7}Cj?U$^kqlL`fC6OzwQOP8A9H?Sf&E?teCxRW;uetH#r?{eXYa ziR$nMV4U2KY^JZ6^Cyj4hk&skcx#6MvOKvZNZ>7Z(EC2`uz6mVD~gZuD+t-)|C#{e ztD=6)b>!&s7E%4Q)QX7s`Up?wJywXal~f7tz_<;oN3u)gyr(-%j?ZMdFCzLiNcq*C zPi$+FbdfuVqO`fPzuBC%Z+Po>^K_0N{--QvvDOvxd;ERGq*I(E)R`TA$vK!S$|U{p zuCV#!lXq$&?C68_*sW|H0g7j;ajoSD%bwz(>#lc<&kUNL8rNooc_~~qBXPNKhs&|t zXo0_CrFi-VchC6NzZB116beO!{89|d-IvZEUX^q@zAk4uc{J09P>XB5@Q!}~LDm2L z^G~ce;tfN%b*f)$lTEIER{rH?*OxkEV~Mks!BmtacVHX;a+>%Y#CzpxPW6YRD(8Fj%P<6Q8pkoK2nQBeO{kV#T%s)?p@~LSCm%Fi5DfVXwf)+lJN8RU252x*VjPcl<4qH+0y*RI%RAz)F*J~`0e%8vVtZyh7qdHPvv=gHW-du4zxV64M zg*a$*q!-dUNN^}mu{=BVD)-uD1+F7WRcu=U@1v9`M1zPYb4#`QvO4bP?LLUQsIy68 zUC8;#$az`W&4IHX5-YQIy4Tb5P2wYmZvwh?Onh(8HPO-8jO)#0%bJbHtuL#x76EYk zKB`|2PhHCd`mFFW0^q?H0fs<@%_*>^1u^R81i74@ zfB{=E_vq{lmaUTL=3r23MS{&le^Pil$Zz*!*BS+aj>pHV;(;(;a5bbREL57fE`xr+)(6@blAeup|o z6GLo)Z^KLF)aJ*&NA?j86HUk~G51XJOEEOdsqi}jqH&U&Q}|KOlU+ZA z%VN|9OqE0%!cEEk^wn;cG=JZ$A!TngiocFauR*~We#|0jXSMf(TWn$<91u6*j<4-? zJ^i2UOB(J{vc8Yi4b$^ahSzOGhRJtHdQx}_9mCt(y_=JE3c^3Xj-#B04w>c|yunxm zt&YspER*G-of=V>y$|~DEEp-0nQT-2uA!@k^8rSEDNWjz_q3wiLo z|80+)y73%h2P5HmJ69ZwF*-SVV&aQbb4CAO3X=u>dGZABogW<*AU);)l`zb=Vem+P zD?Ea+$tkELX~X*aZYI(~0+{k>0K^T(zLDWW$&|aMT-hd?;{>~|Og%Po18x;h)xA7u zQa7^#+~_PuyQ6*kcKi~SbY7AF{)sc6%mM_$1tX}_V;CuGP@(2c4VSOB=rW_zssf|| zq!Bw$&Y>s5x@7pgajQjvVwo(v*2TSI#P*GY6~s-Nj$J02pr1GlQwUfN0qA0KGA%zYZf+-|>z}-v zNj^dOdqP5vQx@%o%KzRh!IyeM0J3ZgYD2=-b_G%@I!yXZWOnA|rRMD8hFfv>31}b5 zF+tjOU8sMrU&=qqguZVx3bG^y*m|a)v4OLn^oQDWXNRcTICm z!qcAOc()nCXL~^UE+j-azMQl(vaPNgW}ednR-#bc89kli!h;_!z+<;Fr4if5<#ycA z9~sX%+Iudd+_EQ;i=4!=^%Rl*x|KTlevaL!okqLGtpOTJG8(Slz4%!`pw9CCpbz?+ z=O}C9xkBAC<-M;r|Kn)Uc`@K)mg1AXRY4XZDeXmM;!-q(ow?tSCz%-#t~ILkA(4Fo z-9oU$OVwZxmbF4dEd90yz@ZvNih`oPciOEyMHra2rNiG@T`ACSe0_sMOF_tK%I3tU zr%RdkV(=de;G`e|vD;7k^sD;5Ji5~p!$Z!YG$Q&UQQ+@H!Y7*sU>sIL5AMPD@c@GB ze@UGLu0*C?yG2w_U6s8`DP;DEBS=R2l=bWVvNlKq{PVn~d6rniJKjmekXNr3B8m)z zdgj?R=Q|_j*#@|Q4<<0595vpuj_h;ZU2LIjI~K|XNa0YCfpGmXaK#e7S+0>X@TjA6 zUVxH&&Emz6AMYqP7rG)lDuruq#<~m~UOzw1eCN(1SNKs^fBy~v>s(h2&j!}HS{i?29L~f<0+7Qp^EZ}1(up)a z8Pg4380(S~SQ20s5D3yrbKzL=k9x=Pk4+6G#*`t-0qw5tZK9=S#ih&nM%gsl_^Gu5 z8Is0o?FL7N{P#O>vhxf%Y{Xf6n`e&RPIFKkh84nbpX&=;raQi-aKXe zqZ#Ty9}vl^G+Le&z^|S{*q8I}_g%mg{)))$P7|tvkV3crb6xSPWLTDy#R9~FKBkd36QUu|=H#(}}L z{T#2usK)9@5gT}{{FHq~A0L09ee&YPd6*=5_0T1=Y?VVi?lL)f^cDKa#3x}riZ(Wm z1}0nkz=u?+x(Ea+$AF}WC$}kY@rXWfz%a{S>4{>GwCthAdqU)tw;}Z*lL|j_zgvAy zNOHu1IFH+?wIoh){LviJH7l=@^3V4TysEH{x$rcCxhuyU^2DOdtZS_AZSvP%!O7~zPEc=UqGQ6tlZ2M@qc1h>aDx@iM{v06 zWg%>8?TNiQh)ku}Vn@@Mt@~B>qd%Q46+b~7zJ^KB(gRN1BLy=u-7)Ok^`0Aw`T8vzAekiM=;%T-oK20j z^T*tMU$pB2X;pxsd513>7)zr0EES&sMk8<~JtB&xhm}6d;4S5Xu5dLziJpE$RK`TjmBtjzJr?)R z?p?1luj_(?5@GM&mC0p1H5cYE3)A?(lzVZge$P1Ta7TKkLc9%#NF1N>meNSGl>k3y zQGVA&IJ|MuV)3+}*Z$Mb8FCC|jdfB2m@876(Ft6;WtEizz{OO9@%<>!{25d`AU0;2 zXT~+vqsyPMr>#b+NmT(IPwi~c^)Zn4um)1A$08yRGe9ceSdJQ_-+8e3;4jx<*>ZZ) z^IMg5@j|CAmBRLKm2b=Q>-UW6DxuTmU#WTH>_6B+e&}VGB`e<7Ww%sBydf>CPJpOz zI*~Rdh&frVlU}o*VZauRJ}GgqcJEEPwiwvkH0goI>=>9PBFvB@I9Q z4+-hN{Av3kjY5HXt70tA96^)^3XCcbLz!whn&S(`3j2}-5I|q`?fy=^GseAb{WYCq z=MT@h55fr3yz=`LyTHNnIE@2pSY`J?C4T2aNzQZG2&{nZ(3cXEYKM50W2<0_{Gs55 zo>=rtUe+V%AiffcZZDav+lOKIh9jhM6*F=X;%lf>e#6%OczVFB*STHx*(KfG4Gmufr=gSy$`kAGsB0|rgoGn^wy5P*Zx*P*L zoD4OB?K=GU2uWO@G3l?i=UsLf<;c-r`N&eJ`@?K3&+;`~*s~6f+qdux<#WuwSx4Yy zrt!C(|8ECsHi|}}i_}o#arUxC_-sCiT|2?_M2AQ|zIQTlj5D74fP2RSJ6s1gk>%Fq z3DSPgbh#xPgH3pVL4P!Rn!<;$)6*Td%%Rv-MBM&mp0%Rb)qGYCG8C@ z$y6aHh=M2u3eoCz_kziqUDs?v9qqj6wQAiDig{yI?r^Nx*r^g2?gYl?woAB`&nIo*#W;k@BjIqZ9*Cm zx8+irdrv{u&pW2t{4LQX-Q>EP*~t6m9SLLdG1tHHA5w(UAh=6_K;5hT3cXmAPL(ru zIv2cxB59^aC z{-~Z72UGYoG8sa{TRo~ot(!5qqCF*D+v$5>ev0q|WUU4jMZ{U?S0Xf)+dt?w3m2;? zC0*#5%v@0_--_y~DH8WUDKYv%8&#bg7K}_GkN1PCo%=7-tuZV|aU_2qc3vtMPyBDM z)PJhpQR~I^$*MLJd!pEQcH>Kdz+hSbCb3R5Z>l6eB&$36bW^%b zV$gk6N5kR%WbyB3(8<~^lgM)2~>xV4~W-1TOI`6eE!K24~^ygf%^NNEC9)a*W}u6UC?a)i}(UQK0d(e z5O#Qrz<22)95I8{TCS)paNVV5+#E#Zhu#wXUjBTea%Vvk+C7pd^ctH$Ss;6G!y>HiQHsl*hcNk9;U(l%?T`A2>{(GU#lz8rDLHYCTKke%rbJ~q> zZ>}(wvRs*1H@I=f~p2FIh~$%!r(MGQMExml3^ zxdQ*oT@M~35v6X!i?W$dkb*J9=06v7xhaYW49LzqXKx3q_@ffeAWCVTS|0Q?jf4PlLnJxg^ zh)q@=DGPF1fl_L2{dw=cvcQ6ViQUW1$ObQ0qv{+CVw2H6gCI&V>K?-`2w3?{eoRgf z0HS^^pqRMK*%v^IUCxhOrz#K?^6WQkZZga29xIt4(k{2?DoqK@2mRr*rBuHr(7pGT z%rwGkkSAi zq$d4~m35HyfOruQ5-T-F_(0l$S~V&%Z68pezU02J4lXv8>Ow5;|Uv z>~UP+(`T_?8D`sU{*Z=4O5+Jo1q7G>NKzFv&Od(2wn8Ca@w+Oh$?%s%Jz=*t@wq=J z7Irq$zv20+=RsA66|b>9m1fUmnyXq3?ZTDG&Py{BX$8avb5?ytiXZB#?b=ka<7|-| z+Y!uTMbK6&G4@oAZn3ID>u4jHX!`q{sTv?8x0&zjm5GKG%5@N!T@CPjJOUR-=|6uo71ec}4QJ z0>o*36EScpmikUiN)LUyc);wqKDhmDtF~7YS7_BoD>d-szAA9DC@(YbIOftGEf{xh z)kY!t!I9Rfsjaddo&=~z1yHzJ({GpdnUoHIcrD!&Sl{TWDhnAN$8G$6vg3BLsa zGCO1=g-?g~{8~a>1_^_W>OFL;_VN#QQ}I*!ZX#{eg%b|$_FC17Up*&Xk+`R`awKZ}aI# zu)0K60MvaL7)qOiUa1lVvL((7Gb`I2lYYC*Cx;6C|2_vy>Q@6mH0g~@@9H{m!Igo` z&SQ5Fl~W3MwdjPZHKX?IWbO3Y#Hm^rmY&YeQZU0~#k#fj7h+AUdZwc`{N>h_KG2SC zv;;h?28?5$nnH`Nk39DZ>Nchj=xRqy!jdnG;e1SBt(Fd|k<_3GyIKF4AzdBYL0UL; z-fuT?hI+MD&TABZX!5t=n9L_o>LE4{uKY1|IU@>2plrLNxt#!jT`A9S zT#yXg=7k806a+ue&QzFrgXpF4JoBmq|xJDS!^w@Z#nTv67A6SIsO36Htoq@ z(~wv~sa^7Mm8}g3rb!%z7yEd!Je~-*^*)PLt11P|(S${Lhw2F!g~%mSS|dBc>h& zLQWjo_u^kUvXT5Tg9?abRtIyWuz@QPy3#gsjq_Ho4zpOLlT%~A1o+n^T7%NpU8V-y zySXJLF+%lA-d-xDQRYho#XtydP^27x1NQ9|jRW8zh#sh_sJK(|NB54K=9yYc!n7Cx zb*kAj^4SKt_R?`49F#&x8hl6M9@X#OO+h0dO*##>a)iB0 z`NQahc{;?j|J=tiEq&5Vd9*fVC}Xb$h?0_01qSAUZY3na zoO?ASWcH;<9WyDNe2GzZiHoD190j%x?udvz*Dz2?sSv~_X#-EqN z{znD6-9~wY+1flLR7>$%)H6W1SVoWx_{W0CFn#Z;{OadztOiX!h{W^Ht&2Ct(q;@C z5nm7C232;JVUh>B#rwOHy3M}udNAo#?0?cU!i!y765VRpEvzRi)Ic>s-*6k%36w0} zwWu#%k(aJRQ4BSKX_C@K`N|)`P~P3NfI1L)0|hJV?bs}9SHI&PW>KCpYb3+i;Z&1$ zx1=Xo_iJ)GD7fyV?0rgk#;kS5NpiKhF+}!^{nZHDifoS`ay0S8m+c8yql?hhsEPaL z4wIEDsMU5XuqI-%AFuu}bNcnmy8{s8&|Y8%Xfy}7B7huPuUU7^c5{}vwXXJ^UQiKh z2?!!_c(oSRcy`hZSJVW`?C=Jh~ShvP@41ri7O9esNvWd3+v#%2GwxaowW08$XM zRbbaF2|JhoLEh_gVNzGFQ2KIA;11AEWX&D={|I{|eZa0)=qBp^Ub?nCp|SyCXylCt zw46ryl};TXm?4EZ!wJs>}HiH1wpbgO~?vnBbvWZd_{3Zxs-*jqQB z?4{1rdg0pH{BKSafJ*LSWi-H_<-O16gVDv?Fabmv&ojn4_6xtg@$5WCoOz3p2)YXV zPS*RPo$5WQM!7N^>7^qSzyg1Y|Ddh9jYzs=rnHrawn#bOKR=N6M)V0{KS))JZl6a! zKns4d_amqGw4eVi{_aPH82Ha$NUcJyiy(JF;X)%XEaRcmyUnjYO$T~4wCYb&% z;hE%lA4duK90x-@zCd-@Bcd(ynJ6wMjA%}7B>Q@dR|Z2)TabncZW_Y2X?F8W8hxAB zk0tA#1L*p)j-bOt%U&`G)$L3FaSV_#JWmdKiC(fVwIpf-#6&p2=b`c~RW0-&=BGPb zGrG=gQkZBu*eVkK-kO98pSDXq6hJ zl0E`a@Cls|Ur`p8uf$hwEvLTm1$7Bk&B-~QEZxL>S(S{q-u83eCWbEvY3#c~C4EtB z;;HQ6pV+UDDl&(7Uz&qm6we*BvBauz(QH=g>0a^n^gkn`{PdQ?#d;tNZEvWjb@f*2 zyM`KhMxX8Q)^GW`NQciK9B)V9==a|^y22xAufZc{4MC7vP3kmG6ZVCuRi4C`;veUC z0Kyi{UByz0X{!h$7U(^v96`qxnEHjSaF#khdubZkxmL#<_FI7?Gk9%4@&SIPveYt? ztbdeoVgAyinJqPKrS=clULqYhb*PS*-p1@7@dZ=3NI;TP;B`-j(7%@4h>Odu^%QZz zwCJ<|+7nJqR_w!MQ}uu=@q&6#9(iGVEqVUwes{t?)%X6O zC0zF#+oORhjX#*e>^$i`^w56W>*Iiy)ewi7vNjSyxf;crk%5JgYhxuf6w*fnBu_yW z;~?l0mnwLSV3?|d0legm#sYk?Z!qyb$6PBQ%i+Y}i~=@kCewB91(JtTyV=Mcp8C(F zYj5-l9yEBf;vVIz6a7KCji;E-6FMP>i{nK5 z=E-am*~m1t>m^%R_PSfd=JHo~LZ_s4c@#_(I!63-R)O(cN5+jzNwMZzq?0)#QYUzV zvJ-@Uj_hPa{G-qgcNgVfq$kD{{}by~5>V|iXD8$GP{0kmf9+(yzNKWmWpe54M-tL4 zjVZ4yvbkDKihiXgs}4JAo0ClKiXYPpmLup6o^}Oq3`&Q-i8@};{^v6jJ*LZykd?jV7xaJODN2cmDA}hbgC9B*WErbU9kX~tGN%`Qi_dn-P^&1!R$2%LA zfW>8hitp-5oXQYBl_Ic&Rqd=@3XgojcsbLWq+5f^!baq?5bnMY#5_ zm|NH1+zvd?O&f)M2b0Z!{xoi_NOWrI;%z<}QaZq-C!wysHWnXAK*+L!h0_m0fB1sL zne9aGCyX<0-(z<6Ja(MUY4T@TC;2k%IfMJwCS_DG85a?8?MnNYREGQq59ldhhn|)R z|Ca4oQ(Ep>2O|!htsH;7kW^aSebq)1xW@$9TcoX-)Ho)?@Bbi^{+G3Infe5wcU>^3 z)R+&q;iM+#xYKhujWyUh`b>teWKoJaa+f~qGK$$GejRp8w7ai(;tRfAER1-)0XJsQ z6iVD{?14JhtirnySpMRN8Qp!emoU?6NnI*m4`3Gpx_jzs1G>GT^^Kb8D@b~6tKW-~ zfJml+dEaGMPg~{CH_IE~dJFjUY zr7#AJQT9u+BG0YqY?;E&CRt~dRKGLC2L{RZE&4sZ8&QI73MEo;Ok#MV4 zUtHCA`SArb>5ksO!SY1MP!6DAME4~NkN1loPL=W&+pC)<6O1_4{&?*W|4<-qm0g%g z1@8EVsrQNs7-)r&F(hoJh&Ln_nNY(Gi7poafwg?bBpNiaSNPOCa~7m_`vt4z*zXqwbAn2t@;+dppNa!>eM zD{?wl$~g^h4^~o=ldyt&~e33;pv+ z7=2oZv_I^&LWD6lq)Rzo=zUz0t>C!K;8GRMq1P>-o9V_L8_|NZZU3g|IO`I&iGdfH zCKAURE)%@nyKV&xnSF_k8f7$E)WR4fT`vDKSm;mtqA(rhL#97c43b^1UwJ*&vXG|b z$$vcU+_wKssPAoItm3465M1!;(MHW1l2H}@n-n+s?w&Tm4jrYBmQhb69M-{hc$n1la^fULtJU zJGZ|5t=C`Hvi#^r*w$r|vhtrQs`_I``GxLB=)PKbx9a3*Nobr zH-?vrD@&>qQ0AaDWUP9X&QEJY0!P-BO7w$4nWt_7kzY==cW2gyZ#7amtn!G}g57AOPzrkVN{+>K2 zUqH!q+|j5=E`)lrHHOP*wWR7{$AJ)roK^c@FJQR~XBr&Up{V}3a%5!QN!2L!qcK_? zD>5esvR^84t&h!TH|`am1zCw}hpYZe_}pSF$256H^m}UO?v2W=thP?R@*9=BnE@l7 zbu;%wLJ5{F3(uhR!nw=E^_%5y7|&coLD#U<3q_=D43>pfsp(Z{lj9Tfajjb!_O+#c ze!#T(8#d@9c`tFrJ6dw1xZZaiPuIIa`FZRK8WqP4&$&86HYl0$}lmwY%RmA85Px;tUCAjqg3Zp`nN+brzp zc3#7~KcgkTK{32bcH;xrk8+#b(7>-+=|Ks@73B)t`f;v1w?|Dx{F*2V^L5$zOO4ik z;*D#P5^X9DndP@RU8C{e&j5&(ZZcKA50gIjFNf1V#~UoB2=W-#{=42mwEsuUns)D^ z1eLj2-LeN*c(j!arF}EKp4ch2yJ&eJ8HkrI?0S6X=V@Jg6)@_YpdNwVrJ9XYIZm%3LW@>p$IzsKXbO*%E3 zM%;~db#!l=2Y+sI*qAOK%>CjjF&$qJc=nEUd*B6`q)GPOF#l<)T))hj@c%$pw|E1l zX0*BXQ<* z%$jUK6uY+)xiLbTt-%^#nz(AmXnF2pa1-Z|BFhWC>kd6@ngMc1iQPiRhJy2rAh5I` zm0d>`lE8gf*1>RCab-;|nAm>xn|@9|J=`=pRMLI5MoKe~Ah%M5!ZmQH7>fRPdnJUI z^+EyJ>G8>03HtPy?6aPyz}u5*w9|ga^r}Wu^AVB;r`+!S%}QlI{h3SVj0Gjia%F_N?DIQPMv25bXaE z6e7b+-;|8DWBF)u?*-{zy_}6o0m<%gyt+!z`)EC7ekPN}V*A#>SE5!QdDaC>|0H%E zUmIvVEI%KAs7uZSIk*SU2kN46BI@J?lDt*p690nEbZp9|6X|trzq!4G9Q3)^c!eNu z*#2rXcj@Ywfzyvx0a}Nhb~>jcSn6g?W4}n-Nv%UHBKpPQ0-a-RD+y#}innKP`xbas!|0uWK=U64oPp$9S zhp;Mlt_$aFePUW;nu}R=x0#v7+j=EdXtwNuhRznhZz6pdAY!L!;itTBMzw!laQU%b zs7GCI3@wve^xDk53hVJ=BcH_RvTujYel1(RTkz)nZ02eBc#}J>xj|9(#P~-jWT0sv z{B}^|CuI{CTt=G7RHiK@od?EUYB=)bT?Z`)i9WcIt(G@q{b`M{>7={Bw^SPh4;pTt z%8eC6>5%)aB#fx}E)(q8VHQtr)5DI?rjneq6knY`{X>TV6JKNiF{+&-QK;@RNlrXb zkSp`r1;R+db0VE%HxQCz#;}e3e|I01eo*^;sy@o5>F@WpO2+tK4h>ave332dU4NPA---MlDXq=J}~JbM#mio|F$`1*j|y!z3I5uKQWOWn^RF=5eDu-q5X_$t+zy-7;g3THTAnJ0|4Anc}OS-Pz?GO+;Sj zGrrS9zCPhYw?@m=`@C^J;^OsRwO+VQFf6Q279d#P|5+_t9_L_L5I$XIvR7C!cp6|# z&TY9=5hk6i(K=E;gelc3Y^PIm9+z(2kh+l?a5mApeE*tuE6+F#!orIPrWxk zk2fEPq!)d+Q{uI!IIKn`eetHm=4d~A1N?pDVo9@NiGhWdms@{#`Vu#~DIcxrfbiiy+|qb!8k(-0&Y)BKqp+j0e(k-%qn&i+1N z#kwUD<4J)+=)7%z7lD=w%O0bAiYlU3)4=D^?hxb0d;zU*+)_Lf%3tnOd)jIejQ*_Fw-yTU3=suo?iL~b*xB^v!5G#Aeb-WQwn+6y zsQF#!tM*-$*e$)*<3Bmy=d`z$2KRi5FP_$;rkuqkBd$*Ysa;I-cx9Q5Q55EsPyswf z^4!gOynEMcjW;eR8Lua!en(3`^$914k?`YUiOw z>djs-uiY3hU=*YET0PSK1vFtGzu{?-zO=e|Z7qF7Q0F(u?PqGju^iC#71TaXB>k0e z^)tZ-FpncXusO)=ssnWHQPb=+CZYNJR`o}6F{Mx*<~ffA9jy*k%smS{VN>EUCbNV! z)ICLyXRa!=(dEi#uV=7`Sfa@|CR%*MZN; zAM$t5pDSw=ci*YSYG&7~9JcTu6^lrpOWPAwbd<;|V&&#!w0k%ZHR+jx2!PaN#ptgi$X|f+Lr=WKN;zhW65* z(_&vwwDj_{@XlRAdhg}2OdHpf&DJV1vbgIC093bUgu1CP;R)yikRfNJG@?pe(Gn$P z)%$8Pg@Hry!-YJRNMKVim)~kMHXGbcQ0aB_rNakbVU?GaSy>*fo%mkY+c`_B=3h{Q zM272TGEn$Q|64koUS<3!j8G<8Nr*a=W?o{3zzaQmPiK{N`GIGJ;#}e?hR>zg(Q|6O zJe=eBtyGKs)Sn#m;gJ7m-)t_74T)y9a7cj~+Rav;2iQ8}v?6r<)Und@*D$C{^dsZe z%4LRsdgO@1k1%SME8hYHyaYZ(EIv+n=1CXnxEc8eimi}R0;hp zpYz_hPXwaQHkz(^vsS#+qtp|6Y9&%C2eFyV*Ur6ziN589z7Y<$9WM1lk!wlYkG&~u z{(R$t4#wYmFAY`Axjj*pug(>)$5ijv*`*qmB`glv)q+Nm-^sEGXMu)N)I!_`>ibdB z{&r9Y##@C2OVGJ1Ggo{kR`$IEZk2wZIWq|2KQSOVJ)*1Ov6}DpP2#dpT;2ZK*IVdu zxfTnvW1g?bA3`~C^9xVE8dgUGVK6L^J_wi%*gF#8xeV#WostRNr;s|x*r~mg;R~GU zF!VfHvyCICs|e|I`lLsrEeh51`~c`B*IZh}dnRXm@}moe)+;?d$~@R9bQ@ZuL#qa= zIp$M9KIx(eQ=g|>Nsg$F{m*IJ#qaxma>1KbS$N2uR$(5fPdwmbXHz$@7AolP!VlZ= zPIlQXQ&N8h+Ikj_3#|flqIj4fcG9;=x+;_I!dlZYRG_8~>)*Qcz#yiKy>jtaWrv@YW(Fv5jwVJxK9c-Sx8Y+&Idg56 z7*uqcLF(pwP=SHFl^8sBGqVG1E!poTiZo`dH4BWGNr!3-QQH@62`#G3De)xx2`t#; z_>_0B!Y0ho4Ikz2LTW_!)3{|H==O+IbIo&GeU}t%ks`atU8;?yQt!I=y=LbgmZYl6 zwU^HLQIOAPN~dBJO|+wT-i~D1OM}32XhG5M84dEHTu*BNluC+jL$w`(mcq#H5I!E4CxVvbD|AObV91$qfXD>E+!3x#MImkh%aJR+1x$3p)%xedm1UFm0 z^;K6g4I+g36$@c++6(=_);9JaJSvF_CL8HBVzz>}80XgbazoU@dYuz)@TzKSk9g6E z)gGCEW^Rs0w{X?o*2)3B%@G>_M9t|m5O~122c_0s=kmeh<5)i6M&wK#mj#VPfa?Tm z)Mfp`?UP$F{JiP=o-YgNKGh4dHqY9^toS5>d(K1$l<3-q!{^;49H3r6G{4hsF+xHG zk~n!Hm& zvtGa;(T>gD_&LLK%v_aev(Z zg?c>FFPSF~oM2FtzjdkNe7oG!y6Hx`F|o(uS!4s8i(6RJtJlm%Q`y^j1X&hVHmtC8 z0yG_gi7!RO>1SMQ2>pgbN~s$GqKZE0@u}>03zEbQjTh>7FUBMn=Uf(T^h(Qv=X`Ll@0U;`ozwM#% zxk&g;2K13H3dZG%LR?&KNVDoz_fD;fM06SbVQ>A-=7WBMAEjUyX)eE0UaR=II#uyF zzA!4?a`!SE>Zjj0xA`i8?iw?dM}&J3L!C3gX>MM=qG3Kb7LGSL@R~qlRacBG1>?@W zCCrka=~WA@?eV$_i0^*m<+9O|oxRv8*U(eKlUmG$7DAl$0VNgl@>EM@g+>B_X-@zq za-Jh9ylZeT$*yQY-aCH45KW)EAQ>q_B{16Ts9i|cx7;oeP+y5~oHUwj!{rZqs(Je6 zh#-y&i~19;eQ@#DGkvr;|8q+tyVWY0gK{4bJl1-|wxhYKhu_(T1b{yzp(FQG>`u=e^oQRbDQesYQ?%T|si? zn#q=z6mK<p1xx;{kacF;LUa_{8SsL!^L z0cb)Ui4XAOkB6kAmYEpT%jt)JwmZpIP0%-3{&;c$7;%vo{mH_QwTMqnj{Ddv%-PiP zduv~(%$!=qMc$Iq)G$;8g*jjBa%x0G_W~%8H+sR7JD&TEC%DW9<;mNZgTd_0QAJJp zktv*0X7-w{IKs@T)#}<;(h0I8emE49Fk4x5V^i)a9)?>2U*#WtO{K4`1`OHjbklxI zo2(NIJ$?a+y)`)2t?)TF-OFzHLfQP-sJsP;telg1Y`7{kF|V8Hkm~!V7Pue0ovrV| zWZ#BB4zjNu0@-pQD87f!)EVH3XOw;bb+o;wJ8J((Z20nGX>;@Isx{wl$dK3)CbM!I zj(w3~YF8MCnNn2}+Q*6^L#sF^V+6X5Z_zXYlqJe>db{5(R z)pomWJ_tBV4@htQ4!Q=q_;V|^IrY6+7rVj4Issh69D9ARB73+$f$41q&1St)Cir-8 zG3_R!UR;;IKSqPgo^>AEH5O4RE_Ca|FmrJ@P^pE}roeY)sD7V96*I-!rE%I*^N&^@ zPk()4Qf@}HIb9}^hE@PDvCJ3hJHUaH1Ko|`q^tEjFG|W^=d)*s?ZHXVl^9F&qjplP zKP$wt#rbXYHj~HIiT7>4$OS8RoW15m8_;O5O|UDJ2CCPJIfY3hk2=nL)d9swvZk#} z(2x>@*nZKlZ%SwHHn+c>WRaIilHA|Ly% zB-e!mYfbC{f3JKFK zewxrKENN7iXz~dtHP;$x160;ni;Uy#Tn}1|(Jf+`^n0hqZpK~(X@A?2@%gK6a*kb3 z)S3D=@w*V{*o(bsCa2TUtC3}04=b>bX3Bvbto7wU#YsEnTVD2lBD{4Ym-}OuZp}XO zo6rL7vKAK!fTnsu%6=3$+?Vmj9s{yDTJK`R=U7(&hy^S#}YK8b?wu|)Y^k|w3UC=VsRTl(!2pvtWJZ7yT z3{R>WO?y-gmDqa(U3w`WQr4MZ@*Tos!SMI`!w3G3F8Gy`*$Q)C{q&j!cg@OgbDF&q zD*p}NY`IO4o1!w5@;tyqCX_syHCOIJ3CEVzR>def2={J>?v8&djc~iJuFN| zT5w>$UR5M_OsCzmwr^1~RWXY2&wWY6J;`CiyYiec+`Is5`(QCL&WdR_oW*dNugWpt zJ$ETTgO2@lY8onDm93&A!v@Fvwn6+v;H~YHkfBs?6r@?Rke39=eM4wBQQ__ul3~}s zbrC3&&J_Ds%fP5#q;MW5UU8F@$I*Fhr@*7tYJ-hfC;91o!K2`EErq|tfS$x5N$h^+Xrk1JuVOc8< zn?|ZuVS4k5wnfq!-#{YIoXC%*0Ii8Om%a+9qssyh1_*4Ff#Fr-;&%dw0}wpvXBO*&NgJ^yh>W-8JApVSt;S~+H>T-!Tz02>fYmzmwI1v zLLN5t@uT1;6+B7@@@#dh#$d&RRPM6p$^;+>NTZcWGwj~Bh2;5TAbmA*2B2Am?{HA? zI~qCmEN`r7>QRf3YDb@iF;8pdC{q1Q_W8gj?dAa4s${Hi;_*-L#?#Rk-l8U(Ra4T% z=Y87&Hwl>soya(^*4AIOY~Y}4efx9S_Bd3y?pNEi>o)0D?S^+H*#eddY2RHk|4w5&-nZRM-RJs#KV7Unkk<5swwtS8OPN`FR1#$(wEH4-wo zx-zj;FG1I$N!WxW3p9!frGnRek}w83aBdWh5-qmgu*T@~r zJcl;`PxFVx{T=-epT_jTIRJF9d8R&48bf&^U%yZ3NBRZ_vtCTkX8QGUQRGpQ4;|IQ zT#DDe$4`{B?;xSJ=G%mPwTAjkz4-;w|WkSoL`L2N{8 z8oUDPzDIp2VU$*57X964dKCH)mAKr~T{%z^slkt&15qX~pxp-us25-+h_d@Q#)y)k zejB>-P4rXyNA9BO76Q68J`NCXu8z^s(L$6ILP30*$wWH5XA&w2nS*#8pp6+8j*d6^ zs+N9%J{SQ-0Fl1`i7!Cm+!@Je@O8-SvF!{)^n=k$5s$`M^I^!{C8>25$-Sn?T^lFesRXUE) zp^Tq%kpR`WUhc?O=zJfA7`V&_?_joh*K7)n(Z&GBiH|_J27slC&VhZwDU1TSB}A8}iJEyG`{$(}c!yh*CwTJ{|z0?Z=7rGTuIY<<`%xnM6I52Jk% zYhc=RvG29K|5F}UNdz$t)pzux5F2aTZ#XdUO#%dVd2x(1N3PkKZC`Oe9XW3j$yTA> zHssXzzvP2>M}n229nl2*VKw_};*)7@l?QPa$yE&p$UK_Lf|JeUJXk3*inYM>(-(A+ zp`Rz4#n=hVktY}RV)W?In07vM=q56;+FTZMYq9#&7!m`?!a}=Fl9T#d(7WW7>DM^{ z9H6f^L;`?|qH`O`qJ8o|zT8Ne1Maa^;sDeV0w&Ho!AN4Zl>Ybr!1=eTiWYSLtcs|1 zsvCVK#YKaCXSS*WR)hzA%ZCs_zjWAmnQ(*jbYfL}paID?pe#G85;-YHG;NoK02(+USN0tBp1{3T+rTTGo#I?ogUbRsD zvY`E;pGfpF-Zh(~Fe+25B;m)d>xHz+dYHEJwPgt81vuDaH`dVS5v5k)2JZvukVEnH z?G@|q37eeuyFghu%`QGl=7adi2?L^o34A%=&^~d{bde04CZ|P9>H+40(^D^<{Tovv z@WYH!5$xYF*u%&cAgo*tC^|z8u>(?JDAE}upz=El$eFdPckL`vC#PP+?kWIh>>kAH zDS@&8Y6=Y8ZEn>@btXaAH@s7flk$9P;me|12U9#H{VlpNSh@ZF<;y*ts(AsyOK`WF z-u4ROB!wMnp;;EzA$V0ay&_`#L}3~D?W&bMJa8F)bM-sfH`mc+gb!{Ma?2V6`XESl zjnb&eJOgFTl(Ox$&NNb*vLOGr-=hIZdzgZ~;?h_3=8#*Nqf4RP+DFFw${(Y#j6Jv3 zV(kERDIeM}3`)#^a?KKL6JO+sq5O3%agVcTO}pb&hc@vlaX_0vDx8d~bCC?Gs6658 z^JE!2cFU-8$UTi1;QF5L)Y#9N(}3Ww` z5ODxHQIsx~#=Bq3;=~R2Yhw*OKb2(k>*75

    ;q1lyh`)}2pG8N!y1N9?87WoSVC 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 - ); - } - - return ( -
    - -
    - ); -}; diff --git a/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.tsx b/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.tsx index 0fbccbc9b..7a4945540 100644 --- a/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.tsx +++ b/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.tsx @@ -7,7 +7,6 @@ import { Input } from "@app/components/v2"; import { HighlightText } from "@app/components/v2/HighlightText"; import { TPamSession } from "@app/hooks/api/pam"; -import { PamSessionLogOutput } from "./PamSessionLogOutput"; import { formatLogContent } from "./PamSessionLogsSection.utils"; type Props = { @@ -104,20 +103,9 @@ export const PamSessionLogsSection = ({ session }: Props) => { >
    {log.output && ( - <> -
    -
    - OUTPUT -
    -
    -
    - -
    - +
    + +
    )}
    From eff4bcb886c94835a860ac92fb8c1b5c5742cb5e Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Wed, 5 Nov 2025 03:21:06 +0530 Subject: [PATCH 038/294] refactor: fix conditional ordering logic in folder sorting to ensure correct rank assignment --- backend/src/services/secret-folder/secret-folder-dal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/services/secret-folder/secret-folder-dal.ts b/backend/src/services/secret-folder/secret-folder-dal.ts index 1923add8e..f4bb18005 100644 --- a/backend/src/services/secret-folder/secret-folder-dal.ts +++ b/backend/src/services/secret-folder/secret-folder-dal.ts @@ -419,7 +419,7 @@ export const secretFolderDALFactory = (db: TDbClient) => { .select( selectAllTableCols(TableName.SecretFolder), db.raw( - `DENSE_RANK() OVER (ORDER BY ${TableName.SecretFolder}."name" COLLATE "en-x-icu" ${orderDirection}) as rank` + `DENSE_RANK() OVER (ORDER BY ${TableName.SecretFolder}."name" COLLATE "en-x-icu" ${orderDirection === OrderByDirection.ASC ? "ASC" : "DESC"} as rank` ), db.ref("slug").withSchema(TableName.Environment).as("environment") ) From b1e7cbf10ad76d3517d473fe72ab5f7e38c0a2e7 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Tue, 4 Nov 2025 15:49:34 -0800 Subject: [PATCH 039/294] fix: handle null for skip multiline encoding on commit endpoint --- backend/src/ee/routes/v1/pit-router.ts | 5 ++++- .../components/SecretApprovalRequestChangeItem.tsx | 9 +++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/src/ee/routes/v1/pit-router.ts b/backend/src/ee/routes/v1/pit-router.ts index 14a82bce4..26909d294 100644 --- a/backend/src/ee/routes/v1/pit-router.ts +++ b/backend/src/ee/routes/v1/pit-router.ts @@ -468,7 +468,10 @@ export const registerPITRouter = async (server: FastifyZodProvider) => { .transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())) .optional(), secretComment: z.string().trim().optional().default(""), - skipMultilineEncoding: z.boolean().optional(), + skipMultilineEncoding: z + .boolean() + .nullish() + .transform((val) => (val === null ? false : val)), metadata: z.record(z.string()).optional(), secretMetadata: ResourceMetadataSchema.optional(), tagIds: z.string().array().optional() 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 c54ee8502..cbab034ed 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 @@ -221,9 +221,7 @@ export const SecretApprovalRequestChangeItem = ({
    Multi-line Encoding
    - {secretVersion?.skipMultilineEncoding?.toString() || ( - - - )}{" "} + {secretVersion?.skipMultilineEncoding?.toString() || "false"}
    @@ -366,9 +364,8 @@ export const SecretApprovalRequestChangeItem = ({
    Multi-line Encoding
    {newVersion?.skipMultilineEncoding?.toString() ?? - secretVersion?.skipMultilineEncoding?.toString() ?? ( - - - )}{" "} + secretVersion?.skipMultilineEncoding?.toString() ?? + "false"}

    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%0Djg#2wIm$vD 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 literal 0 HcmV?d00001 From c407acebe4370b89ba1d1b04b8a74ccf6b72fdc0 Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Tue, 4 Nov 2025 21:37:29 +0530 Subject: [PATCH 035/294] refactor: simplify folder sorting logic with ICU collation --- .../secret-folder/secret-folder-dal.ts | 29 +++---------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/backend/src/services/secret-folder/secret-folder-dal.ts b/backend/src/services/secret-folder/secret-folder-dal.ts index 356de3099..b8d7e1de0 100644 --- a/backend/src/services/secret-folder/secret-folder-dal.ts +++ b/backend/src/services/secret-folder/secret-folder-dal.ts @@ -419,20 +419,11 @@ export const secretFolderDALFactory = (db: TDbClient) => { .select( selectAllTableCols(TableName.SecretFolder), db.raw( - `DENSE_RANK() OVER (ORDER BY CASE WHEN ${TableName.SecretFolder}."name" LIKE '\\_%' THEN ${orderDirection === OrderByDirection.ASC ? "0" : "1"} ELSE ${orderDirection === OrderByDirection.ASC ? "1" : "0"} END, - LOWER(${TableName.SecretFolder}."name") ${orderDirection}, - ${TableName.SecretFolder}."name" ${orderDirection} - ) as rank` + `DENSE_RANK() OVER (ORDER BY ${TableName.SecretFolder}."name" COLLATE "en-x-icu" ${orderDirection}) as rank` ), db.ref("slug").withSchema(TableName.Environment).as("environment") ) - .orderByRaw(`CASE WHEN ${TableName.SecretFolder}.?? LIKE '\\_%' THEN ? ELSE ? END`, [ - orderBy, - orderDirection === OrderByDirection.ASC ? 0 : 1, - orderDirection === OrderByDirection.ASC ? 1 : 0 - ]) - .orderByRaw(`LOWER(${TableName.SecretFolder}.??) ${orderDirection}`, [orderBy]) - .orderByRaw(`${TableName.SecretFolder}.?? ${orderDirection}`, [orderBy]); + .orderByRaw(`${TableName.SecretFolder}.?? COLLATE "en-x-icu" ${orderDirection}`, [orderBy]); if (limit) { const rankOffset = offset + 1; // ranks start from 1 @@ -442,13 +433,7 @@ export const secretFolderDALFactory = (db: TDbClient) => { .from[number]>("w") .where("w.rank", ">=", rankOffset) .andWhere("w.rank", "<", rankOffset + limit) - .orderByRaw(`CASE WHEN "w".?? LIKE '\\_%' THEN ? ELSE ? END`, [ - orderBy, - orderDirection === OrderByDirection.ASC ? 0 : 1, - orderDirection === OrderByDirection.ASC ? 1 : 0 - ]) - .orderByRaw(`LOWER("w".??) ${orderDirection}`, [orderBy]) - .orderByRaw(`"w".?? ${orderDirection}`, [orderBy]); + .orderByRaw(`"w".?? COLLATE "en-x-icu" ${orderDirection}`, [orderBy]); } const folders = await query; @@ -497,13 +482,7 @@ export const secretFolderDALFactory = (db: TDbClient) => { .select<(TSecretFolders & { path: string; depth: number; environment: string })[]>("*") .from("parents") .orderBy("depth") - .orderByRaw(`CASE WHEN "parents".?? LIKE '\\_%' THEN ? ELSE ? END`, [ - orderBy, - orderDirection === OrderByDirection.ASC ? 0 : 1, - orderDirection === OrderByDirection.ASC ? 1 : 0 - ]) - .orderByRaw(`LOWER("parents".??) ${orderDirection}`, [orderBy]) - .orderByRaw(`"parents".?? ${orderDirection}`, [orderBy]); + .orderByRaw(`"parents".?? COLLATE "en-x-icu" ${orderDirection}`, [orderBy]); return folders; } catch (error) { From 0229732a6c0aca7711d8a1af642b117e5d9ae2c9 Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Tue, 4 Nov 2025 23:11:01 +0530 Subject: [PATCH 036/294] refactor: enhance folder sorting logic to use conditional ordering based on direction --- .../src/services/secret-folder/secret-folder-dal.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/backend/src/services/secret-folder/secret-folder-dal.ts b/backend/src/services/secret-folder/secret-folder-dal.ts index b8d7e1de0..1923add8e 100644 --- a/backend/src/services/secret-folder/secret-folder-dal.ts +++ b/backend/src/services/secret-folder/secret-folder-dal.ts @@ -423,7 +423,10 @@ export const secretFolderDALFactory = (db: TDbClient) => { ), db.ref("slug").withSchema(TableName.Environment).as("environment") ) - .orderByRaw(`${TableName.SecretFolder}.?? COLLATE "en-x-icu" ${orderDirection}`, [orderBy]); + .orderByRaw( + `${TableName.SecretFolder}.?? COLLATE "en-x-icu" ${orderDirection === OrderByDirection.ASC ? "ASC" : "DESC"}`, + [orderBy] + ); if (limit) { const rankOffset = offset + 1; // ranks start from 1 @@ -433,7 +436,9 @@ export const secretFolderDALFactory = (db: TDbClient) => { .from[number]>("w") .where("w.rank", ">=", rankOffset) .andWhere("w.rank", "<", rankOffset + limit) - .orderByRaw(`"w".?? COLLATE "en-x-icu" ${orderDirection}`, [orderBy]); + .orderByRaw(`"w".?? COLLATE "en-x-icu" ${orderDirection === OrderByDirection.ASC ? "ASC" : "DESC"}`, [ + orderBy + ]); } const folders = await query; @@ -482,7 +487,9 @@ export const secretFolderDALFactory = (db: TDbClient) => { .select<(TSecretFolders & { path: string; depth: number; environment: string })[]>("*") .from("parents") .orderBy("depth") - .orderByRaw(`"parents".?? COLLATE "en-x-icu" ${orderDirection}`, [orderBy]); + .orderByRaw(`"parents".?? COLLATE "en-x-icu" ${orderDirection === OrderByDirection.ASC ? "ASC" : "DESC"}`, [ + orderBy + ]); return folders; } catch (error) { From c73abc8608ceac797e5575e58c71e9884d27534f Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 4 Nov 2025 15:26:35 -0500 Subject: [PATCH 037/294] pam: simplify log output --- .../components/PamSessionLogOutput.tsx | 116 ------------------ .../components/PamSessionLogsSection.tsx | 18 +-- 2 files changed, 3 insertions(+), 131 deletions(-) delete mode 100644 frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogOutput.tsx diff --git a/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogOutput.tsx b/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogOutput.tsx deleted file mode 100644 index 99e30627d..000000000 --- a/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogOutput.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { useState } from "react"; - -import { HighlightText } from "@app/components/v2/HighlightText"; -import { PamResourceType } from "@app/hooks/api/pam"; - -type TableLog = { - command?: string; - data_rows: Record[]; - total_rows?: number; -}; - -export const PamSessionLogOutput = ({ - content, - resourceType, - search -}: { - content: string; - resourceType: PamResourceType; - search: string; -}) => { - const [isRawView, setIsRawView] = useState(false); - - let parsedContent: TableLog | null = null; - - if (resourceType === PamResourceType.Postgres || resourceType === PamResourceType.MySQL) { - try { - const parsed = JSON.parse(content); - - if ( - parsed && - typeof parsed === "object" && - !Array.isArray(parsed) && - parsed.data_rows && - Array.isArray(parsed.data_rows) && - parsed.data_rows.length > 0 && - typeof parsed.data_rows[0] === "object" && - parsed.data_rows[0] !== null - ) { - parsedContent = parsed; - } - } catch { - // Not a valid JSON or doesn't match structure, will render as plain text - } - } - - if (parsedContent) { - const headers = Object.keys(parsedContent.data_rows[0]); - return ( -

WD4Y;)qzZ@VoP=;YVI#fbDq4)!h@h*uLyec65 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�<*!)$O{g>+p+jqkW27luM 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

From 5d51280071d759d6b5d789256cb8fc1592ffeaec Mon Sep 17 00:00:00 2001 From: ArshBallagan Date: Tue, 4 Nov 2025 16:33:12 -0800 Subject: [PATCH 040/294] Update organization-structure.mdx --- docs/documentation/guides/organization-structure.mdx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/documentation/guides/organization-structure.mdx b/docs/documentation/guides/organization-structure.mdx index 3cba64678..752c06ccc 100644 --- a/docs/documentation/guides/organization-structure.mdx +++ b/docs/documentation/guides/organization-structure.mdx @@ -24,9 +24,11 @@ Infisical is designed to provide comprehensive, centralized, and efficient manag ### 2. Projects - **Definition and Role**: [Projects](/documentation/platform/project) are the highest-level construct within an [organization](/documentation/platform/organization) in Infisical. They serve as the primary container for all functionalities. -- **Correspondence to Code Repositories**: Projects typically align with specific code repositories. +- **Common Project Mappings**: Projects typically align with applications, services, or code repositories — each being a valid and common approach depending on your organizational structure. - **Functional Capabilities**: Each project encompasses features for managing secrets, certificates, and encryption keys, serving as the central hub for these resources. +Projects are isolated from one another. Secrets, certificates, and other resources cannot be shared or referenced across different projects. Each project maintains its own separate set of resources. + ### 3. Environments - **Purpose**: Environments are designed for organizing and compartmentalizing secrets within projects. @@ -40,8 +42,9 @@ Infisical is designed to provide comprehensive, centralized, and efficient manag ### 5. Imports -- **Purpose and Benefits**: To promote reusability and avoid redundancy, Infisical supports the use of imports. This allows secrets, folders, or entire environments to be referenced across multiple projects as needed. -- **Best Practice**: Utilizing [secret imports](/documentation/platform/secret-reference#secret-imports) or [references](/documentation/platform/secret-reference#secret-referencing) ensures consistency and minimizes manual overhead. +- **Purpose and Benefits**: To promote reusability and avoid redundancy within a project, Infisical supports the use of imports and references. This allows secrets, folders, or entire environments to be referenced within the same project as needed. +- **Project Isolation**: Imports and references only work within a single project. Secrets cannot be imported or referenced across different projects, as projects are isolated from one another. +- **Best Practice**: Utilizing [secret imports](/documentation/platform/secret-reference#secret-imports) or [references](/documentation/platform/secret-reference#secret-referencing) ensures consistency and minimizes manual overhead when managing secrets within a project. ### 6. Approval Workflows From cab093b805f51f8f2ff265b9308c644817e44bf4 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Tue, 4 Nov 2025 16:42:23 -0800 Subject: [PATCH 041/294] fix: include group members in change request author filter --- .../components/SecretApprovalRequest/SecretApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx index 0b8e6f03f..a62d2ec27 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx @@ -107,7 +107,7 @@ export const SecretApprovalRequest = () => { }); const { permission } = useProjectPermission(); - const { data: members } = useGetWorkspaceUsers(projectId); + const { data: members } = useGetWorkspaceUsers(projectId, true); const isSecretApprovalScreen = Boolean(selectedApprovalId); const { requestId } = search; From b0c8bcf2a33fe4cbf1e58b701d7e05287c558c63 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Tue, 4 Nov 2025 18:20:43 -0800 Subject: [PATCH 042/294] fix: correct secret approval query filters column references, correct count query and handle pagination reset on frontend --- .../secret-approval-request-dal.ts | 27 ++++++++++--------- .../SecretApprovalRequest.tsx | 8 +++++- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts index 7597dcfd4..db300720f 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts @@ -670,6 +670,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { .select( db.ref("projectId").withSchema(TableName.Environment), db.ref("slug").withSchema(TableName.Environment).as("environment"), + db.ref("name").withSchema(TableName.Environment).as("environmentName"), db.ref("id").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerId"), db.ref("reviewerUserId").withSchema(TableName.SecretApprovalRequestReviewer), db.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"), @@ -699,30 +700,30 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { ) .as("inner"); - const countQuery = (await (tx || db) - .select(db.raw("count(*) OVER() as total_count")) - .from(innerQuery.clone().distinctOn(`${TableName.SecretApprovalRequest}.id`))) as Array<{ - total_count: number; - }>; - const query = (tx || db).select("*").from(innerQuery).orderBy("createdAt", "desc") as typeof innerQuery; if (search) { void query.where((qb) => { void qb .whereRaw(`CONCAT_WS(' ', ??, ??) ilike ?`, [ - db.ref("firstName").withSchema("committerUser"), - db.ref("lastName").withSchema("committerUser"), + db.ref("committerUserFirstName"), + db.ref("committerUserLastName"), `%${search}%` ]) - .orWhereRaw(`?? ilike ?`, [db.ref("username").withSchema("committerUser"), `%${search}%`]) - .orWhereRaw(`?? ilike ?`, [db.ref("email").withSchema("committerUser"), `%${search}%`]) - .orWhereILike(`${TableName.Environment}.name`, `%${search}%`) - .orWhereILike(`${TableName.Environment}.slug`, `%${search}%`) - .orWhereILike(`${TableName.SecretApprovalPolicy}.secretPath`, `%${search}%`); + .orWhereRaw(`?? ilike ?`, [db.ref("committerUserUsername"), `%${search}%`]) + .orWhereRaw(`?? ilike ?`, [db.ref("committerUserEmail"), `%${search}%`]) + .orWhereILike(`environmentName`, `%${search}%`) + .orWhereILike(`environment`, `%${search}%`) + .orWhereILike(`policySecretPath`, `%${search}%`); }); } + const countQuery = (await (tx || db) + .select(db.raw("count(*) OVER() as total_count")) + .from(query.clone().as("outer"))) as Array<{ + total_count: number; + }>; + const rankOffset = offset + 1; const docs = await (tx || db) .with("w", query) diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx index a62d2ec27..c730e4622 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx @@ -40,7 +40,7 @@ import { PreferenceKey, setUserTablePreference } from "@app/helpers/userTablePreferences"; -import { usePagination } from "@app/hooks"; +import { usePagination, useResetPageHelper } from "@app/hooks"; import { useGetSecretApprovalRequestCount, useGetSecretApprovalRequests, @@ -99,6 +99,12 @@ export const SecretApprovalRequest = () => { const totalApprovalCount = data?.totalCount ?? 0; const secretApprovalRequests = data?.approvals ?? []; + useResetPageHelper({ + totalCount: totalApprovalCount, + offset, + setPage + }); + const { data: secretApprovalRequestCount, isSuccess: isSecretApprovalReqCountSuccess } = useGetSecretApprovalRequestCount({ projectId }); const { user: userSession } = useUser(); From fc395bf661001d158796121b8de8b84af240fb74 Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 4 Nov 2025 21:29:05 -0500 Subject: [PATCH 043/294] pam: return metadata for pam access requests --- .../pam-account-routers/pam-account-router.ts | 3 ++- .../pam-account/pam-account-service.ts | 27 ++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts b/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts index d2e0183ff..286e0896f 100644 --- a/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts +++ b/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts @@ -92,7 +92,8 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => { gatewayClientCertificate: z.string(), gatewayClientPrivateKey: z.string(), gatewayServerCertificateChain: z.string(), - relayHost: z.string() + relayHost: z.string(), + metadata: z.record(z.string(), z.string()).optional() }) } }, diff --git a/backend/src/ee/services/pam-account/pam-account-service.ts b/backend/src/ee/services/pam-account/pam-account-service.ts index 00b84943f..0316ecebf 100644 --- a/backend/src/ee/services/pam-account/pam-account-service.ts +++ b/backend/src/ee/services/pam-account/pam-account-service.ts @@ -480,6 +480,30 @@ export const pamAccountServiceFactory = ({ throw new NotFoundError({ message: `Gateway connection details for gateway '${gatewayId}' not found.` }); } + let metadata; + + switch (resourceType) { + case PamResource.Postgres: + case PamResource.MySQL: { + const connectionCredentials = await decryptResourceConnectionDetails({ + encryptedConnectionDetails: resource.encryptedConnectionDetails, + kmsService, + projectId: account.projectId + }); + + const credentials = await decryptAccountCredentials({ + encryptedCredentials: account.encryptedCredentials, + kmsService, + projectId: account.projectId + }); + + metadata = { + username: credentials.username, + database: connectionCredentials.database + }; + } + } + return { sessionId: session.id, resourceType, @@ -491,7 +515,8 @@ export const pamAccountServiceFactory = ({ gatewayServerCertificateChain: gatewayConnectionDetails.gateway.serverCertificateChain, relayHost: gatewayConnectionDetails.relayHost, projectId: account.projectId, - account + account, + metadata }; }; From 43c936e633d025e00b114cc28725151007d74cc4 Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 4 Nov 2025 21:57:02 -0500 Subject: [PATCH 044/294] added default case to switch --- .../pam-account/pam-account-service.ts | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/backend/src/ee/services/pam-account/pam-account-service.ts b/backend/src/ee/services/pam-account/pam-account-service.ts index 0316ecebf..7d766a532 100644 --- a/backend/src/ee/services/pam-account/pam-account-service.ts +++ b/backend/src/ee/services/pam-account/pam-account-service.ts @@ -484,24 +484,28 @@ export const pamAccountServiceFactory = ({ switch (resourceType) { case PamResource.Postgres: - case PamResource.MySQL: { - const connectionCredentials = await decryptResourceConnectionDetails({ - encryptedConnectionDetails: resource.encryptedConnectionDetails, - kmsService, - projectId: account.projectId - }); + case PamResource.MySQL: + { + const connectionCredentials = await decryptResourceConnectionDetails({ + encryptedConnectionDetails: resource.encryptedConnectionDetails, + kmsService, + projectId: account.projectId + }); - const credentials = await decryptAccountCredentials({ - encryptedCredentials: account.encryptedCredentials, - kmsService, - projectId: account.projectId - }); + const credentials = await decryptAccountCredentials({ + encryptedCredentials: account.encryptedCredentials, + kmsService, + projectId: account.projectId + }); - metadata = { - username: credentials.username, - database: connectionCredentials.database - }; - } + metadata = { + username: credentials.username, + database: connectionCredentials.database + }; + } + break; + default: + break; } return { From 3463d0d03c38b1f19a28b4b5638cf7e303a0a15f Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 4 Nov 2025 23:03:42 -0500 Subject: [PATCH 045/294] add account info to metadata --- .../src/ee/services/pam-account/pam-account-service.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/services/pam-account/pam-account-service.ts b/backend/src/ee/services/pam-account/pam-account-service.ts index 7d766a532..41a049deb 100644 --- a/backend/src/ee/services/pam-account/pam-account-service.ts +++ b/backend/src/ee/services/pam-account/pam-account-service.ts @@ -498,9 +498,17 @@ export const pamAccountServiceFactory = ({ projectId: account.projectId }); + const accountPath = await getFullPamFolderPath({ + pamFolderDAL, + folderId: account.folderId, + projectId: resource.projectId + }); + metadata = { username: credentials.username, - database: connectionCredentials.database + database: connectionCredentials.database, + accountName: account.name, + accountPath }; } break; From b54cdba82d53d9fac1de7afb8f9fc050b07e4bbc Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 4 Nov 2025 23:18:40 -0500 Subject: [PATCH 046/294] fix issue --- backend/src/ee/services/pam-account/pam-account-service.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/backend/src/ee/services/pam-account/pam-account-service.ts b/backend/src/ee/services/pam-account/pam-account-service.ts index 41a049deb..2f66d28d7 100644 --- a/backend/src/ee/services/pam-account/pam-account-service.ts +++ b/backend/src/ee/services/pam-account/pam-account-service.ts @@ -498,12 +498,6 @@ export const pamAccountServiceFactory = ({ projectId: account.projectId }); - const accountPath = await getFullPamFolderPath({ - pamFolderDAL, - folderId: account.folderId, - projectId: resource.projectId - }); - metadata = { username: credentials.username, database: connectionCredentials.database, From a19b2d4b0937072f4b21fb7eb3370ee9c275fcff Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Wed, 5 Nov 2025 16:00:21 +0530 Subject: [PATCH 047/294] fix: e2e --- backend/src/services/secret-folder/secret-folder-dal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/services/secret-folder/secret-folder-dal.ts b/backend/src/services/secret-folder/secret-folder-dal.ts index f4bb18005..f2befbe1a 100644 --- a/backend/src/services/secret-folder/secret-folder-dal.ts +++ b/backend/src/services/secret-folder/secret-folder-dal.ts @@ -419,7 +419,7 @@ export const secretFolderDALFactory = (db: TDbClient) => { .select( selectAllTableCols(TableName.SecretFolder), db.raw( - `DENSE_RANK() OVER (ORDER BY ${TableName.SecretFolder}."name" COLLATE "en-x-icu" ${orderDirection === OrderByDirection.ASC ? "ASC" : "DESC"} as rank` + `DENSE_RANK() OVER (ORDER BY ${TableName.SecretFolder}."name" COLLATE "en-x-icu" ${orderDirection === OrderByDirection.ASC ? "ASC" : "DESC"}) as rank` ), db.ref("slug").withSchema(TableName.Environment).as("environment") ) From 100c1b07862e5f6ce997d513a4967433fde6ae9b Mon Sep 17 00:00:00 2001 From: x032205 Date: Wed, 5 Nov 2025 13:13:57 -0500 Subject: [PATCH 048/294] fix relays with same identity repalcing each other, throw error instead --- backend/src/ee/services/relay/relay-service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/services/relay/relay-service.ts b/backend/src/ee/services/relay/relay-service.ts index 749a3ca16..2cf090dff 100644 --- a/backend/src/ee/services/relay/relay-service.ts +++ b/backend/src/ee/services/relay/relay-service.ts @@ -996,7 +996,9 @@ export const relayServiceFactory = ({ ); if (existingRelay && (existingRelay.host !== host || existingRelay.name !== name)) { - return relayDAL.updateById(existingRelay.id, { host, name }, tx); + throw new BadRequestError({ + message: `Machine identity already has an existing relay with the name "${existingRelay.name} and host "${existingRelay.host}". Delete the existing relay or use a different machine identity.` + }); } if (!existingRelay) { From d251faf45137d59b61e65da27e924e94f99c6b61 Mon Sep 17 00:00:00 2001 From: Andre <120525481+x032205@users.noreply.github.com> Date: Wed, 5 Nov 2025 13:20:46 -0500 Subject: [PATCH 049/294] Apply suggestion from @greptile-apps[bot] Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- backend/src/ee/services/relay/relay-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/relay/relay-service.ts b/backend/src/ee/services/relay/relay-service.ts index 2cf090dff..ae7758e67 100644 --- a/backend/src/ee/services/relay/relay-service.ts +++ b/backend/src/ee/services/relay/relay-service.ts @@ -997,7 +997,7 @@ export const relayServiceFactory = ({ if (existingRelay && (existingRelay.host !== host || existingRelay.name !== name)) { throw new BadRequestError({ - message: `Machine identity already has an existing relay with the name "${existingRelay.name} and host "${existingRelay.host}". Delete the existing relay or use a different machine identity.` + message: `Machine identity already has an existing relay with the name "${existingRelay.name}" and host "${existingRelay.host}". Delete the existing relay or use a different machine identity.` }); } From 977694d3677416cf2e62dcfe4b68ad1702867e4f Mon Sep 17 00:00:00 2001 From: = Date: Wed, 5 Nov 2025 23:54:34 +0530 Subject: [PATCH 050/294] feat: resolved project membership auto adding to orgnization --- .../server/routes/v1/project-membership-router.ts | 14 ++++++++++++++ .../MembersTab/components/AddMemberModal.tsx | 10 ---------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/backend/src/server/routes/v1/project-membership-router.ts b/backend/src/server/routes/v1/project-membership-router.ts index 57fdad031..dc76efa92 100644 --- a/backend/src/server/routes/v1/project-membership-router.ts +++ b/backend/src/server/routes/v1/project-membership-router.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { AccessScope, + OrgMembershipRole, ProjectMembershipRole, ProjectMembershipsSchema, ProjectUserMembershipRolesSchema, @@ -266,6 +267,19 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const usernamesAndEmails = [...req.body.emails, ...req.body.usernames]; + + await server.services.membershipUser.createMembership({ + permission: req.permission, + scopeData: { + scope: AccessScope.Organization, + orgId: req.permission.orgId + }, + data: { + roles: [{ isTemporary: false, role: OrgMembershipRole.NoAccess }], + usernames: usernamesAndEmails + } + }); + const { memberships } = await server.services.membershipUser.createMembership({ permission: req.permission, scopeData: { diff --git a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/AddMemberModal.tsx b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/AddMemberModal.tsx index cb6934240..7498b3181 100644 --- a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/AddMemberModal.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/AddMemberModal.tsx @@ -25,14 +25,12 @@ import { useProject } from "@app/context"; import { - useAddUsersToOrg, useAddUserToWsNonE2EE, useGetOrgUsers, useGetProjectRoles, useGetWorkspaceUsers } from "@app/hooks/api"; import { ProjectVersion } from "@app/hooks/api/projects/types"; -import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; const addMemberFormSchema = z.object({ @@ -86,7 +84,6 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => { defaultValues: { orgMemberships: [], projectRoleSlugs: [] } }); - const { mutateAsync: addMemberToOrg } = useAddUsersToOrg(); const { mutateAsync: addUserToProject } = useAddUserToWsNonE2EE(); useEffect(() => { @@ -140,13 +137,6 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => { return; } - if (newInvitees.length) { - await addMemberToOrg({ - inviteeEmails: newInvitees, - organizationId: orgId, - organizationRoleSlug: ProjectMembershipRole.Member // only applies to new invites - }); - } if (newInvitees.length || inviteeEmails.length) { await addUserToProject({ usernames: [...inviteeEmails, ...newInvitees], From 76d0f1a419b979e6da8c3561b1ef7e8d44a29997 Mon Sep 17 00:00:00 2001 From: = Date: Thu, 6 Nov 2025 00:54:06 +0530 Subject: [PATCH 051/294] feat: resolved metadata interpolation failing in project due to group only membership --- backend/src/ee/services/permission/permission-dal.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index 95480a54a..88b52be22 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -201,11 +201,11 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { if (actorType === ActorType.USER) { void queryBuilder - .on(`${TableName.Membership}.actorUserId`, `${TableName.IdentityMetadata}.userId`) + .on(`${TableName.IdentityMetadata}.userId`, db.raw("?", [actorId])) .andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`); } else if (actorType === ActorType.IDENTITY) { void queryBuilder - .on(`${TableName.Membership}.actorIdentityId`, `${TableName.IdentityMetadata}.identityId`) + .on(`${TableName.IdentityMetadata}.identityId`, db.raw("?", [actorId])) .andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`); } }) @@ -488,7 +488,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { }) .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { void queryBuilder - .on(`${TableName.Membership}.actorUserId`, `${TableName.IdentityMetadata}.userId`) + .on(`${TableName.Users}.id`, `${TableName.IdentityMetadata}.userId`) .andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`); }) .where(`${TableName.Membership}.scopeOrgId`, orgId) From 08cb45f9af5586fa6f53fb8eb452e90751c4a812 Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Thu, 6 Nov 2025 01:37:17 +0530 Subject: [PATCH 052/294] fix: status code when token auth not found --- .../identity-token-auth/identity-token-auth-service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/services/identity-token-auth/identity-token-auth-service.ts b/backend/src/services/identity-token-auth/identity-token-auth-service.ts index 2d3e11cd8..e6969da61 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-service.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-service.ts @@ -244,8 +244,8 @@ export const identityTokenAuthServiceFactory = ({ } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { - throw new BadRequestError({ - message: "The identity does not have Token Auth attached" + throw new NotFoundError({ + message: "Token Auth configuration not found for identity" }); } From 27ac81ef5cc7b78f63f4c0f2c2330c152b99a79c Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Wed, 5 Nov 2025 16:55:41 -0800 Subject: [PATCH 053/294] fix: handle group memberships in secret versions history and fix secret versions query to always return all versions --- backend/src/server/routes/sanitizedSchemas.ts | 3 +- .../secret-v2-bridge/secret-v2-bridge-fns.ts | 4 +- .../secret-v2-bridge/secret-version-dal.ts | 23 ++++---- frontend/src/hooks/api/secrets/types.ts | 1 + .../GroupMembersSection/GroupMembersTable.tsx | 19 ++++++- .../route-secret-manager.tsx | 7 +++ .../SecretListView/SecretVersionItem.tsx | 53 ++++++++++++++++--- 7 files changed, 90 insertions(+), 20 deletions(-) diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index 47fbb0e07..17cfcb5ce 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -141,7 +141,8 @@ export const secretRawSchema = z.object({ actorId: z.string().nullable().optional(), actorType: z.string().nullable().optional(), name: z.string().nullable().optional(), - membershipId: z.string().nullable().optional() + membershipId: z.string().nullable().optional(), + groupId: z.string().nullable().optional() }) .optional() .nullable(), diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts index 6b284ddee..01a7f6210 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts @@ -793,6 +793,7 @@ export const reshapeBridgeSecret = ( userActorId?: string | null; identityActorId?: string | null; membershipId?: string | null; + groupId?: string | null; actorType?: string | null; tags?: { id: string; @@ -823,7 +824,8 @@ export const reshapeBridgeSecret = ( actorType: secret.actorType, actorId: secret.userActorId || secret.identityActorId, name: secret.identityActorName || secret.userActorName, - membershipId: secret.membershipId + membershipId: secret.membershipId, + groupId: secret.groupId } : undefined, tags: secret.tags, diff --git a/backend/src/services/secret-v2-bridge/secret-version-dal.ts b/backend/src/services/secret-v2-bridge/secret-version-dal.ts index 7d25dac86..33ac43d09 100644 --- a/backend/src/services/secret-v2-bridge/secret-version-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-version-dal.ts @@ -182,7 +182,6 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { const findVersionsBySecretIdWithActors = async ({ secretId, - projectId, secretVersions, findOpt = {}, tx @@ -196,11 +195,19 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { try { const { offset, limit, sort = [["createdAt", "desc"]] } = findOpt; const query = (tx || db.replicaNode())(TableName.SecretVersionV2) + .leftJoin(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.SecretVersionV2}.folderId`) + .leftJoin(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretFolder}.envId`) .leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SecretVersionV2}.userActorId`) + .leftJoin(TableName.UserGroupMembership, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) .leftJoin(TableName.Membership, (qb) => { void qb - .on(`${TableName.Membership}.actorUserId`, `${TableName.SecretVersionV2}.userActorId`) - .andOn(`${TableName.Membership}.scope`, db.raw("?", [AccessScope.Project])); + .on(`${TableName.Membership}.scope`, db.raw("?", [AccessScope.Project])) + .andOn(`${TableName.Membership}.scopeProjectId`, `${TableName.Environment}.projectId`) + .andOn((sqb) => { + void sqb + .on(`${TableName.Membership}.actorUserId`, `${TableName.SecretVersionV2}.userActorId`) + .orOn(`${TableName.Membership}.actorGroupId`, `${TableName.UserGroupMembership}.groupId`); + }); }) .leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.SecretVersionV2}.identityActorId`) .leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`) @@ -216,12 +223,6 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { ) .where((qb) => { void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId); - void qb.where(`${TableName.Membership}.scopeProjectId`, projectId); - if (secretVersions?.length) void qb.whereIn(`${TableName.SecretVersionV2}.version`, secretVersions); - }) - .orWhere((qb) => { - void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId); - void qb.whereNull(`${TableName.Membership}.scopeProjectId`); if (secretVersions?.length) void qb.whereIn(`${TableName.SecretVersionV2}.version`, secretVersions); }) .select( @@ -229,6 +230,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { db.ref("username").withSchema(TableName.Users).as("userActorName"), db.ref("name").withSchema(TableName.Identity).as("identityActorName"), db.ref("id").withSchema(TableName.Membership).as("membershipId"), + db.ref("actorGroupId").withSchema(TableName.Membership).as("groupId"), db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug") @@ -256,7 +258,8 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { ...SecretVersionsV2Schema.parse(el), userActorName: el.userActorName, identityActorName: el.identityActorName, - membershipId: el.membershipId + membershipId: el.membershipId, + groupId: el.groupId }), childrenMapper: [ { diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index 4f912fad2..f4bf2da98 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -109,6 +109,7 @@ export type SecretVersions = { actorType?: string | null; name?: string | null; membershipId?: string | null; + groupId?: string | null; } | null; }; diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx index 1df989602..7ced08534 100644 --- a/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx +++ b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx @@ -1,4 +1,4 @@ -import { useMemo } from "react"; +import { useEffect, useMemo } from "react"; import { faArrowDown, faArrowUp, @@ -7,6 +7,7 @@ import { faSearch } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useNavigate, useSearch } from "@tanstack/react-router"; import { createNotification } from "@app/components/notifications"; import { @@ -48,6 +49,7 @@ enum GroupMembersOrderBy { } export const GroupMembersTable = ({ groupMembership }: Props) => { + const navigate = useNavigate(); const { search, setSearch, @@ -62,6 +64,21 @@ export const GroupMembersTable = ({ groupMembership }: Props) => { initPerPage: getUserTablePreference("projectGroupMembersTable", PreferenceKey.PerPage, 20) }); + // this handles links from secret versions when the actor is in a group membership + const { username, ...restSearch } = useSearch({ + strict: false + }); + useEffect(() => { + if (username) { + setSearch(username); + navigate({ + to: ".", + replace: true, + search: restSearch + }); + } + }, [username]); + const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["assumePrivileges"] as const); const handlePerPageChange = (newPerPage: number) => { diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/route-secret-manager.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/route-secret-manager.tsx index 01349a83b..c7c1e6e85 100644 --- a/frontend/src/pages/project/GroupDetailsByIDPage/route-secret-manager.tsx +++ b/frontend/src/pages/project/GroupDetailsByIDPage/route-secret-manager.tsx @@ -1,4 +1,6 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { zodValidator } from "@tanstack/zod-adapter"; +import { z } from "zod"; import { ProjectAccessControlTabs } from "@app/types/project"; @@ -8,6 +10,11 @@ export const Route = createFileRoute( "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/groups/$groupId" )({ component: GroupDetailsByIDPage, + validateSearch: zodValidator( + z.object({ + username: z.string().optional().catch(undefined) + }) + ), beforeLoad: ({ context, params }) => { return { breadcrumbs: [ diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretVersionItem.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretVersionItem.tsx index a2c25b13a..b6b7175c6 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretVersionItem.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretVersionItem.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { faEye } from "@fortawesome/free-regular-svg-icons"; import { faArrowRotateRight, + faBan, faDesktop, faEyeSlash, faServer, @@ -68,10 +69,14 @@ export const SecretVersionItem = ({ const getLinkToModifyHistoryEntity = ( actorId: string, actorType: string, - membershipId: string | null = "" + membershipId: string | null = "", + groupId: string | null = "", + actorName: string | null = "" ) => { switch (actorType) { case ActorType.USER: + if (groupId) + return `/projects/secret-management/${currentProject.id}/groups/${groupId}?username=${actorName}`; return `/projects/secret-management/${currentProject.id}/members/${membershipId}`; case ActorType.IDENTITY: return `/projects/secret-management/${currentProject.id}/identities/${actorId}`; @@ -83,10 +88,26 @@ export const SecretVersionItem = ({ const onModifyHistoryClick = ( actorId: string | undefined | null, actorType: string | undefined | null, - membershipId: string | undefined | null + membershipId: string | undefined | null, + groupId: string | undefined | null, + actorName: string | undefined | null ) => { + if (!membershipId) { + createNotification({ + type: "info", + text: `This ${actorType === ActorType.USER ? "user" : "identity"} is no longer a member of this project.` + }); + return; + } + if (actorType && actorId && actorType !== ActorType.PLATFORM) { - const redirectLink = getLinkToModifyHistoryEntity(actorId, actorType, membershipId); + const redirectLink = getLinkToModifyHistoryEntity( + actorId, + actorType, + membershipId, + groupId, + actorName + ); if (redirectLink) { navigate({ to: redirectLink }); } @@ -157,15 +178,33 @@ export const SecretVersionItem = ({
Modified by: - + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
- onModifyHistoryClick(actor.actorId, actor.actorType, actor.membershipId) + onClick={ + actor.membershipId + ? () => + onModifyHistoryClick( + actor.actorId, + actor.actorType, + actor.membershipId, + actor.groupId, + actor.name + ) + : undefined } - className="cursor-pointer" + className={actor.membershipId ? "cursor-pointer" : undefined} > + {!actor.membershipId && ( + + )}
From 283d7a702bba20a5ae4a54cffdba1d008c4c36d6 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Wed, 5 Nov 2025 22:01:38 -0300 Subject: [PATCH 054/294] Add new option on legacy templates to issue new certificates --- .../api/certificateTemplates/mutations.tsx | 21 +++++++++-- .../components/CertificateModal.tsx | 27 ++++++++++---- .../components/CertificatesSection.tsx | 15 +------- .../components/CertificatesTable.tsx | 4 +- .../PkiTemplateListPage.tsx | 37 ++++++++++++++++++- 5 files changed, 78 insertions(+), 26 deletions(-) diff --git a/frontend/src/hooks/api/certificateTemplates/mutations.tsx b/frontend/src/hooks/api/certificateTemplates/mutations.tsx index 0355d9d87..998194ddc 100644 --- a/frontend/src/hooks/api/certificateTemplates/mutations.tsx +++ b/frontend/src/hooks/api/certificateTemplates/mutations.tsx @@ -90,7 +90,12 @@ export const useCreateCertTemplateV2 = () => { return data.certificateTemplate; }, onSuccess: (_, { projectId }) => { - queryClient.invalidateQueries({ queryKey: certTemplateKeys.listTemplates({ projectId }) }); + queryClient.invalidateQueries({ + predicate: (query) => { + const [firstKey, queryProjectId] = query.queryKey; + return firstKey === "list-template" && queryProjectId === projectId; + } + }); } }); }; @@ -107,7 +112,12 @@ export const useUpdateCertTemplateV2 = () => { return data.certificateTemplate; }, onSuccess: (_, { projectId }) => { - queryClient.invalidateQueries({ queryKey: certTemplateKeys.listTemplates({ projectId }) }); + queryClient.invalidateQueries({ + predicate: (query) => { + const [firstKey, queryProjectId] = query.queryKey; + return firstKey === "list-template" && queryProjectId === projectId; + } + }); } }); }; @@ -127,7 +137,12 @@ export const useDeleteCertTemplateV2 = () => { return data.certificateTemplate; }, onSuccess: (_, { projectId }) => { - queryClient.invalidateQueries({ queryKey: certTemplateKeys.listTemplates({ projectId }) }); + queryClient.invalidateQueries({ + predicate: (query) => { + const [firstKey, queryProjectId] = query.queryKey; + return firstKey === "list-template" && queryProjectId === projectId; + } + }); } }); }; diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateModal.tsx index f56f0fde8..90aee7bd0 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateModal.tsx @@ -75,6 +75,7 @@ export type FormData = z.infer; type Props = { popUp: UsePopUpState<["certificate"]>; handlePopUpToggle: (popUpName: keyof UsePopUpState<["certificate"]>, state?: boolean) => void; + preselectedTemplate?: { id: string; name: string }; }; type TCertificateDetails = { @@ -86,7 +87,7 @@ type TCertificateDetails = { const CERT_TEMPLATE_NONE_VALUE = "none"; -export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { +export const CertificateModal = ({ popUp, handlePopUpToggle, preselectedTemplate }: Props) => { const [certificateDetails, setCertificateDetails] = useState(null); const { currentProject } = useProject(); const { data: cert } = useGetCert( @@ -147,13 +148,15 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { (cert.extendedKeyUsages || []).map((name) => [name, true]) ) }); - } else { + } else if (popUp?.certificate?.isOpen) { + const templateId = preselectedTemplate?.id || CERT_TEMPLATE_NONE_VALUE; + reset({ caId: "", commonName: "", subjectAltNames: "", ttl: "", - certificateTemplateId: CERT_TEMPLATE_NONE_VALUE, + certificateTemplateId: templateId, keyUsages: { [CertKeyUsage.DIGITAL_SIGNATURE]: true, [CertKeyUsage.KEY_ENCIPHERMENT]: true @@ -161,7 +164,7 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { extendedKeyUsages: {} }); } - }, [cert]); + }, [cert, preselectedTemplate, popUp?.certificate?.isOpen]); useEffect(() => { if (!cert && selectedCertTemplate) { @@ -269,15 +272,25 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { isRequired > )} From 5b8b133756cf8c0b5a1e086a75db79a278c151d3 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 27 Oct 2025 19:26:40 -0700 Subject: [PATCH 085/294] Add acme option --- .../components/CertificateProfilesTab/CreateProfileModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx index 267f5e780..245eadacc 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx @@ -45,7 +45,7 @@ const createSchema = z .trim() .max(1000, "Description must be less than 1000 characters") .optional(), - enrollmentType: z.enum(["api", "est"]), + enrollmentType: z.enum(["api", "est", "acme"]), certificateAuthorityId: z.string().min(1, "Certificate Authority is required"), certificateTemplateId: z.string().min(1, "Certificate Template is required"), estConfig: z From 5366136693db5ad95e400998de31c4594346c842 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 28 Oct 2025 09:48:02 -0700 Subject: [PATCH 086/294] Try to fix constraint --- .../migrations/20251027234547_add-pki-acme.ts | 37 ++++++++++++++----- .../utils/dropConstraintIfExists.ts | 2 +- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/backend/src/db/migrations/20251027234547_add-pki-acme.ts b/backend/src/db/migrations/20251027234547_add-pki-acme.ts index 01483938b..5352f71fa 100644 --- a/backend/src/db/migrations/20251027234547_add-pki-acme.ts +++ b/backend/src/db/migrations/20251027234547_add-pki-acme.ts @@ -1,6 +1,9 @@ import { Knex } from "knex"; import { TableName } from "../schemas"; import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; +import { dropConstraintIfExists } from "@app/db/migrations/utils/dropConstraintIfExists"; + +const ENROLLMENT_TYPE_CHECK_CONSTRAINT = "pki_certificate_profiles_enrollmentType_check"; export async function up(knex: Knex): Promise { // Create PkiAcmeEnrollmentConfig table @@ -21,8 +24,13 @@ export async function up(knex: Knex): Promise { t.foreign("acmeConfigId").references("id").inTable(TableName.PkiAcmeEnrollmentConfig).onDelete("SET NULL"); t.index("acmeConfigId"); }); - // TODO: should update (or add?) the constraints to check at least one of - // the enrollment config id is set and it matches the enrollment type? + } + + await dropConstraintIfExists(TableName.PkiCertificateProfile, ENROLLMENT_TYPE_CHECK_CONSTRAINT, knex); + if (await knex.schema.hasColumn(TableName.PkiCertificateProfile, "enrollmentType")) { + await knex.schema.alterTable(TableName.PkiCertificateProfile, (t) => { + t.string("enrollmentType").checkIn(["api", "est", "acme"], ENROLLMENT_TYPE_CHECK_CONSTRAINT).alter(); + }); } // Create PkiAcmeAccount table @@ -121,14 +129,6 @@ export async function up(knex: Knex): Promise { export async function down(knex: Knex): Promise { // Drop tables in reverse dependency order - if (await knex.schema.hasColumn(TableName.PkiCertificateProfile, "acmeConfigId")) { - await knex.schema.alterTable(TableName.PkiCertificateProfile, (t) => { - t.dropForeign(["acmeConfigId"]); - t.dropIndex("acmeConfigId"); - t.dropColumn("acmeConfigId"); - }); - } - // Drop PkiAcmeChallenge first (depends on PkiAcmeAuth) if (await knex.schema.hasTable(TableName.PkiAcmeChallenge)) { await knex.schema.dropTable(TableName.PkiAcmeChallenge); @@ -153,6 +153,23 @@ export async function down(knex: Knex): Promise { await dropOnUpdateTrigger(knex, TableName.PkiAcmeAccount); } + // Change enrollmentType check constraint to only allow api and est + await dropConstraintIfExists(TableName.PkiCertificateProfile, ENROLLMENT_TYPE_CHECK_CONSTRAINT, knex); + if (await knex.schema.hasColumn(TableName.PkiCertificateProfile, "enrollmentType")) { + await knex.schema.alterTable(TableName.PkiCertificateProfile, (t) => { + t.string("enrollmentType").checkIn(["api", "est"], ENROLLMENT_TYPE_CHECK_CONSTRAINT).alter(); + }); + } + + // Drop acmeConfigId column + if (await knex.schema.hasColumn(TableName.PkiCertificateProfile, "acmeConfigId")) { + await knex.schema.alterTable(TableName.PkiCertificateProfile, (t) => { + t.dropForeign(["acmeConfigId"]); + t.dropIndex("acmeConfigId"); + t.dropColumn("acmeConfigId"); + }); + } + // Drop PkiAcmeEnrollmentConfig if (await knex.schema.hasTable(TableName.PkiAcmeEnrollmentConfig)) { await knex.schema.dropTable(TableName.PkiAcmeEnrollmentConfig); diff --git a/backend/src/db/migrations/utils/dropConstraintIfExists.ts b/backend/src/db/migrations/utils/dropConstraintIfExists.ts index bfe487d49..93985ca76 100644 --- a/backend/src/db/migrations/utils/dropConstraintIfExists.ts +++ b/backend/src/db/migrations/utils/dropConstraintIfExists.ts @@ -3,4 +3,4 @@ import { Knex } from "knex"; import { TableName } from "@app/db/schemas"; export const dropConstraintIfExists = (tableName: TableName, constraintName: string, knex: Knex) => - knex.raw(`ALTER TABLE ${tableName} DROP CONSTRAINT IF EXISTS ${constraintName};`); + knex.raw("ALTER TABLE ?? DROP CONSTRAINT IF EXISTS ??;", [tableName, constraintName]); From 9c199cdbb8799af643dc643b7af7523e3d88f81b Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 28 Oct 2025 10:30:09 -0700 Subject: [PATCH 087/294] Fix db migration --- .../migrations/20251027234547_add-pki-acme.ts | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/backend/src/db/migrations/20251027234547_add-pki-acme.ts b/backend/src/db/migrations/20251027234547_add-pki-acme.ts index 5352f71fa..3b012c9ed 100644 --- a/backend/src/db/migrations/20251027234547_add-pki-acme.ts +++ b/backend/src/db/migrations/20251027234547_add-pki-acme.ts @@ -3,7 +3,12 @@ import { TableName } from "../schemas"; import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; import { dropConstraintIfExists } from "@app/db/migrations/utils/dropConstraintIfExists"; -const ENROLLMENT_TYPE_CHECK_CONSTRAINT = "pki_certificate_profiles_enrollmentType_check"; +// Notice: the old constraint name is "enrollmentType_check" instead of "enrollment_type_check" +// with psql, if there's no quote around an identifier, it will be lowercased. +// this may cause issues in migrations as Knex sometimes generates identifiers without quotes. +// to avoid this, we use a new constraint name that contains only lowercase letters and underscores. +const OLD_ENROLLMENT_TYPE_CHECK_CONSTRAINT = "pki_certificate_profiles_enrollmentType_check"; +const NEW_ENROLLMENT_TYPE_CHECK_CONSTRAINT = "pki_certificate_profiles_enrollment_type_check"; export async function up(knex: Knex): Promise { // Create PkiAcmeEnrollmentConfig table @@ -26,10 +31,11 @@ export async function up(knex: Knex): Promise { }); } - await dropConstraintIfExists(TableName.PkiCertificateProfile, ENROLLMENT_TYPE_CHECK_CONSTRAINT, knex); + await dropConstraintIfExists(TableName.PkiCertificateProfile, OLD_ENROLLMENT_TYPE_CHECK_CONSTRAINT, knex); if (await knex.schema.hasColumn(TableName.PkiCertificateProfile, "enrollmentType")) { + // Notice: it's okay to use `.checkIn(...).alter();` here because the constraint name is all lowercase. await knex.schema.alterTable(TableName.PkiCertificateProfile, (t) => { - t.string("enrollmentType").checkIn(["api", "est", "acme"], ENROLLMENT_TYPE_CHECK_CONSTRAINT).alter(); + t.string("enrollmentType").checkIn(["api", "est", "acme"], NEW_ENROLLMENT_TYPE_CHECK_CONSTRAINT).alter(); }); } @@ -153,12 +159,20 @@ export async function down(knex: Knex): Promise { await dropOnUpdateTrigger(knex, TableName.PkiAcmeAccount); } - // Change enrollmentType check constraint to only allow api and est - await dropConstraintIfExists(TableName.PkiCertificateProfile, ENROLLMENT_TYPE_CHECK_CONSTRAINT, knex); + // Change enrollmentType check constraint to allow acme + await dropConstraintIfExists(TableName.PkiCertificateProfile, NEW_ENROLLMENT_TYPE_CHECK_CONSTRAINT, knex); if (await knex.schema.hasColumn(TableName.PkiCertificateProfile, "enrollmentType")) { - await knex.schema.alterTable(TableName.PkiCertificateProfile, (t) => { - t.string("enrollmentType").checkIn(["api", "est"], ENROLLMENT_TYPE_CHECK_CONSTRAINT).alter(); - }); + // Notice: DO NOT USE + // + // `t.string("enrollmentType").checkIn(["api", "est"], OLD_ENROLLMENT_TYPE_CHECK_CONSTRAINT).alter();` + // + // here because knex will generate a constraint name without quotes, and it will be treated as lowercased and causing problems. + await knex.raw( + `ALTER TABLE ?? + ADD CONSTRAINT ?? CHECK (enrollmentType IN ('api', 'est')); + `, + [TableName.PkiCertificateProfile, OLD_ENROLLMENT_TYPE_CHECK_CONSTRAINT] + ); } // Drop acmeConfigId column From 3916d48ba41d3e80628f7b8fd8ad233e5c98f569 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 28 Oct 2025 10:32:14 -0700 Subject: [PATCH 088/294] Fix rollback constraint --- backend/src/db/migrations/20251027234547_add-pki-acme.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/db/migrations/20251027234547_add-pki-acme.ts b/backend/src/db/migrations/20251027234547_add-pki-acme.ts index 3b012c9ed..a81b377e3 100644 --- a/backend/src/db/migrations/20251027234547_add-pki-acme.ts +++ b/backend/src/db/migrations/20251027234547_add-pki-acme.ts @@ -169,9 +169,9 @@ export async function down(knex: Knex): Promise { // here because knex will generate a constraint name without quotes, and it will be treated as lowercased and causing problems. await knex.raw( `ALTER TABLE ?? - ADD CONSTRAINT ?? CHECK (enrollmentType IN ('api', 'est')); + ADD CONSTRAINT ?? CHECK (?? IN ('api', 'est')); `, - [TableName.PkiCertificateProfile, OLD_ENROLLMENT_TYPE_CHECK_CONSTRAINT] + [TableName.PkiCertificateProfile, OLD_ENROLLMENT_TYPE_CHECK_CONSTRAINT, "enrollmentType"] ); } From 2d8126c0111f34c9ad260b6042f1182d5c231e19 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 28 Oct 2025 11:00:50 -0700 Subject: [PATCH 089/294] Add more missing ACME stuff # Conflicts: # frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileRow.tsx # Conflicts: # frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx --- .../certificate-profile-service.ts | 7 ++--- .../hooks/api/certificateProfiles/types.ts | 8 +++-- .../CreateProfileModal.tsx | 31 ++++++++++++++++--- .../CertificateProfilesTab/ProfileRow.tsx | 3 +- 4 files changed, 36 insertions(+), 13 deletions(-) diff --git a/backend/src/services/certificate-profile/certificate-profile-service.ts b/backend/src/services/certificate-profile/certificate-profile-service.ts index d9b9565e1..0de9d6c55 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.ts @@ -225,11 +225,8 @@ export const certificateProfileServiceFactory = ({ message: "API enrollment requires API configuration" }); } - if (data.enrollmentType === EnrollmentType.ACME && !data.acmeConfig) { - throw new ForbiddenRequestError({ - message: "ACME enrollment requires ACME configuration" - }); - } + // TODO: acme type currently doesn't require config obj, but add a check in the future if + // we have options // Create enrollment configs and profile const profile = await certificateProfileDAL.transaction(async (tx) => { diff --git a/frontend/src/hooks/api/certificateProfiles/types.ts b/frontend/src/hooks/api/certificateProfiles/types.ts index f3584b12d..94d8d0c6d 100644 --- a/frontend/src/hooks/api/certificateProfiles/types.ts +++ b/frontend/src/hooks/api/certificateProfiles/types.ts @@ -5,7 +5,7 @@ export type TCertificateProfile = { certificateTemplateId: string; slug: string; description?: string; - enrollmentType: "api" | "est"; + enrollmentType: "api" | "est" | "acme"; estConfigId?: string; apiConfigId?: string; createdAt: string; @@ -44,7 +44,7 @@ export type TCreateCertificateProfileDTO = { certificateTemplateId: string; slug: string; description?: string; - enrollmentType: "api" | "est"; + enrollmentType: "api" | "est" | "acme"; estConfig?: { disableBootstrapCaValidation?: boolean; passphrase: string; @@ -54,6 +54,7 @@ export type TCreateCertificateProfileDTO = { autoRenew?: boolean; renewBeforeDays?: number; }; + acmeConfig?: {}; }; export type TUpdateCertificateProfileDTO = { @@ -69,6 +70,7 @@ export type TUpdateCertificateProfileDTO = { autoRenew?: boolean; renewBeforeDays?: number; }; + acmeConfig?: {}; }; export type TDeleteCertificateProfileDTO = { @@ -81,7 +83,7 @@ export type TListCertificateProfilesDTO = { offset?: number; search?: string; includeConfigs?: boolean; - enrollmentType?: "api" | "est"; + enrollmentType?: "api" | "est" | "acme"; }; export type TGetCertificateProfileByIdDTO = { diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx index 245eadacc..eefc76a1d 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx @@ -72,7 +72,8 @@ const createSchema = z autoRenew: z.boolean().optional(), renewBeforeDays: z.number().min(1).max(365).optional() }) - .optional() + .optional(), + acmeConfig: z.object({}).optional() }) .refine( (data) => { @@ -82,6 +83,9 @@ const createSchema = z if (data.enrollmentType === "api" && !data.apiConfig) { return false; } + if (data.enrollmentType === "acme" && !data.acmeConfig) { + return false; + } return true; }, { @@ -188,7 +192,8 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" } autoRenew: profile.apiConfig?.autoRenew || false, renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30 } - : undefined + : undefined, + acmeConfig: profile.enrollmentType === "acme" ? {} : undefined } : { slug: "", @@ -199,7 +204,8 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" } apiConfig: { autoRenew: false, renewBeforeDays: 30 - } + }, + acmeConfig: {} } }); @@ -230,7 +236,8 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" } autoRenew: profile.apiConfig?.autoRenew || false, renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30 } - : undefined + : undefined, + acmeConfig: profile.enrollmentType === "acme" ? {} : undefined }); } }, [isEdit, profile, reset]); @@ -249,6 +256,8 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" } updateData.estConfig = data.estConfig; } else if (data.enrollmentType === "api" && data.apiConfig) { updateData.apiConfig = data.apiConfig; + } else if (data.enrollmentType === "acme" && data.acmeConfig) { + updateData.acmeConfig = data.acmeConfig; } await updateProfile.mutateAsync(updateData); @@ -549,6 +558,20 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
)} + {/* ACME Configuration */} + {watchedEnrollmentType === "acme" && ( +
+ ( + +
FIXME: ACME configuration
+
+ )} + /> +
+ )} {watchedAutoRenew && (
{ const config = { api: { variant: "ghost" as const, label: "API" }, - est: { variant: "ghost" as const, label: "EST" } + est: { variant: "ghost" as const, label: "EST" }, + acme: { variant: "ghost" as const, label: "ACME" } } as const; const configKey = Object.keys(config).includes(enrollmentType) From 806e11a5b21d2538f0dbc2c33b386738c4a1f584 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 28 Oct 2025 11:09:49 -0700 Subject: [PATCH 090/294] Make column not nullable --- backend/bdd/features/steps/pki_acme.py | 2 +- backend/src/db/migrations/20251027234547_add-pki-acme.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 9a84b0dc8..0f04849e6 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -36,7 +36,7 @@ def step_impl(context: Context, profile_var: str): # TODO: Fixed value for now, just to make test much easier, # we should call infisical API to create such profile instead # in the future - profile_id = "c051e74c-48a7-4724-832c-d5b496698546" + profile_id = "40dd4794-66e3-4bd3-9875-c1f819804bb2" context.vars[profile_var] = AcmeProfile(profile_id) diff --git a/backend/src/db/migrations/20251027234547_add-pki-acme.ts b/backend/src/db/migrations/20251027234547_add-pki-acme.ts index a81b377e3..a2e1ab777 100644 --- a/backend/src/db/migrations/20251027234547_add-pki-acme.ts +++ b/backend/src/db/migrations/20251027234547_add-pki-acme.ts @@ -35,7 +35,10 @@ export async function up(knex: Knex): Promise { if (await knex.schema.hasColumn(TableName.PkiCertificateProfile, "enrollmentType")) { // Notice: it's okay to use `.checkIn(...).alter();` here because the constraint name is all lowercase. await knex.schema.alterTable(TableName.PkiCertificateProfile, (t) => { - t.string("enrollmentType").checkIn(["api", "est", "acme"], NEW_ENROLLMENT_TYPE_CHECK_CONSTRAINT).alter(); + t.string("enrollmentType") + .notNullable() + .checkIn(["api", "est", "acme"], NEW_ENROLLMENT_TYPE_CHECK_CONSTRAINT) + .alter(); }); } From a37f8445ad40299e0248e063bc0e8168fafd7b3b Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 28 Oct 2025 11:13:55 -0700 Subject: [PATCH 091/294] Extract common stuff --- .../ee/services/pki-acme/pki-acme-service.ts | 65 ++++++++++++------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 9e9fce099..d95f1f11a 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -3,6 +3,10 @@ import { NotFoundError } from "@app/lib/errors"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; +import { + EnrollmentType, + TCertificateProfileWithConfigs +} from "@app/services/certificate-profile/certificate-profile-types"; import { TCreateAcmeAccountPayload, TCreateAcmeAccountResponse, @@ -10,7 +14,6 @@ import { TCreateAcmeOrderResponse, TDeactivateAcmeAccountPayload, TDeactivateAcmeAccountResponse, - TDownloadAcmeCertificateDTO, TFinalizeAcmeOrderPayload, TFinalizeAcmeOrderResponse, TGetAcmeAuthorizationResponse, @@ -28,26 +31,39 @@ type TPkiAcmeServiceFactoryDep = { export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => { const appCfg = getConfig(); + const validateAcmeProfile = async (profileId: string): Promise => { + const profile = await certificateProfileDAL.findById(profileId); + if (!profile) { + throw new NotFoundError({ message: "Certificate profile not found" }); + } + if (profile.enrollmentType !== EnrollmentType.ACME) { + throw new NotFoundError({ message: "Certificate profile is not configured for ACME enrollment" }); + } + return profile; + }; + + const buildUrl = (path: string): string => { + const baseUrl = appCfg.SITE_URL ?? ""; + return `${baseUrl}${path}`; + }; + const getAcmeDirectory = async (profileId: string): Promise => { // FIXME: Implement ACME directory endpoint // Validate profile exists and is for ACME enrollment - // const profile = await certificateProfileDAL.findById(profileId); - // if (!profile) { - // throw new NotFoundError({ message: "Certificate profile not found" }); - // } + const profile = await validateAcmeProfile(profileId); // FIXME: Validate profile is configured for ACME enrollment // Return absolute URLs using SITE_URL - const baseUrl = appCfg.SITE_URL ?? ""; return { - newNonce: `${baseUrl}/api/v1/pki/acme/profiles/${profileId}/new-nonce`, - newAccount: `${baseUrl}/api/v1/pki/acme/profiles/${profileId}/new-account`, - newOrder: `${baseUrl}/api/v1/pki/acme/profiles/${profileId}/new-order` + newNonce: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/new-nonce`), + newAccount: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/new-account`), + newOrder: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/new-order`) }; }; const getAcmeNewNonce = async (profileId: string): Promise => { + const profile = await validateAcmeProfile(profileId); // FIXME: Implement ACME new nonce generation // Generate a new nonce, store it, and return it return "FIXME-generate-nonce"; @@ -57,16 +73,16 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService profileId: string, body: TCreateAcmeAccountPayload ): Promise => { + const profile = await validateAcmeProfile(profileId); // FIXME: Implement ACME new account registration // Use EAB authentication to find corresponding Infisical machine identity // Check permissions and return account information - const baseUrl = appCfg.SITE_URL || ""; const accountId = "FIXME-account-id"; return { status: "valid", - accountUrl: `${baseUrl}/api/v1/pki/acme/profiles/${profileId}/accounts/${accountId}`, + accountUrl: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${accountId}`), contact: [], - orders: `${baseUrl}/api/v1/pki/acme/profiles/${profileId}/accounts/${accountId}/orders` + orders: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${accountId}/orders`) }; }; @@ -74,15 +90,15 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService profileId: string, body: TCreateAcmeOrderPayload ): Promise => { + const profile = await validateAcmeProfile(profileId); // FIXME: Implement ACME new order creation const orderId = "FIXME-order-id"; - const baseUrl = appCfg.SITE_URL || ""; return { status: "pending", expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), identifiers: [], authorizations: [], - finalize: `${baseUrl}/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/finalize` + finalize: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/finalize`) }; }; @@ -91,6 +107,7 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService accountId: string, body?: TDeactivateAcmeAccountPayload ): Promise => { + const profile = await validateAcmeProfile(profileId); // FIXME: Implement ACME account deactivation return { status: "deactivated" @@ -98,6 +115,7 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService }; const listAcmeOrders = async (profileId: string, accountId: string): Promise => { + const profile = await validateAcmeProfile(profileId); // FIXME: Implement ACME list orders return { orders: [] @@ -105,14 +123,14 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService }; const getAcmeOrder = async (profileId: string, orderId: string): Promise => { + const profile = await validateAcmeProfile(profileId); // FIXME: Implement ACME get order - const baseUrl = appCfg.SITE_URL || ""; return { status: "pending", expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), identifiers: [], authorizations: [], - finalize: `${baseUrl}/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/finalize` + finalize: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/finalize`) }; }; @@ -121,28 +139,29 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService orderId: string, body: TFinalizeAcmeOrderPayload ): Promise => { + const profile = await validateAcmeProfile(profileId); const { csr } = body; // FIXME: Implement ACME finalize order - const baseUrl = appCfg.SITE_URL || ""; return { status: "processing", expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), identifiers: [], authorizations: [], - finalize: `${baseUrl}/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/finalize`, - certificate: `${baseUrl}/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/certificate` + finalize: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/finalize`), + certificate: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/certificate`) }; }; const downloadAcmeCertificate = async (profileId: string, orderId: string): Promise => { + const profile = await validateAcmeProfile(profileId); // FIXME: Implement ACME certificate download // Return the certificate in PEM format return "FIXME-certificate-pem"; }; const getAcmeAuthorization = async (profileId: string, authzId: string): Promise => { + const profile = await validateAcmeProfile(profileId); // FIXME: Implement ACME authorization retrieval - const baseUrl = appCfg.SITE_URL || ""; return { status: "pending", expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), @@ -153,7 +172,7 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService challenges: [ { type: "http-01", - url: `${baseUrl}/api/v1/pki/acme/profiles/${profileId}/authorizations/${authzId}/challenges/http-01`, + url: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/authorizations/${authzId}/challenges/http-01`), status: "pending", token: "FIXME-challenge-token" } @@ -165,12 +184,12 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService profileId: string, authzId: string ): Promise => { + const profile = await validateAcmeProfile(profileId); // FIXME: Implement ACME challenge response // Trigger verification process - const baseUrl = appCfg.SITE_URL || ""; return { type: "http-01", - url: `${baseUrl}/api/v1/pki/acme/profiles/${profileId}/authorizations/${authzId}/challenges/http-01`, + url: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/authorizations/${authzId}/challenges/http-01`), status: "pending", token: "FIXME-challenge-token" }; From efabf9ee05b78f61ab0de2522e591930fe4901ed Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 28 Oct 2025 13:55:41 -0700 Subject: [PATCH 092/294] Add more scheme --- .../ee/services/pki-acme/pki-acme-errors.ts | 436 ++++++++++++++++++ .../ee/services/pki-acme/pki-acme-schemas.ts | 20 + .../ee/services/pki-acme/pki-acme-service.ts | 54 ++- .../ee/services/pki-acme/pki-acme-types.ts | 7 + 4 files changed, 501 insertions(+), 16 deletions(-) create mode 100644 backend/src/ee/services/pki-acme/pki-acme-errors.ts diff --git a/backend/src/ee/services/pki-acme/pki-acme-errors.ts b/backend/src/ee/services/pki-acme/pki-acme-errors.ts new file mode 100644 index 000000000..85de5cb1c --- /dev/null +++ b/backend/src/ee/services/pki-acme/pki-acme-errors.ts @@ -0,0 +1,436 @@ +/** + * ACME Error Classes based on RFC 8555 Section 6.2 + * https://datatracker.ietf.org/doc/html/rfc8555#section-6.2 + */ + +export interface IAcmeError { + type: string; + detail: string; + status: number; + subproblems?: Array<{ type: string; detail: string; identifier?: { type: string; value: string } }>; +} + +export class AcmeError extends Error implements IAcmeError { + type: string; + + detail: string; + + status: number; + + subproblems?: Array<{ type: string; detail: string; identifier?: { type: string; value: string } }>; + + error?: unknown; + + constructor({ + type, + detail, + status, + subproblems, + error, + message + }: { + type: string; + detail: string; + status: number; + subproblems?: Array<{ type: string; detail: string; identifier?: { type: string; value: string } }>; + error?: unknown; + message?: string; + }) { + super(message || detail); + this.type = type; + this.detail = detail; + this.status = status; + this.subproblems = subproblems; + this.error = error; + this.name = "AcmeError"; + } + + toAcmeResponse(): IAcmeError { + return { + type: this.type, + detail: this.detail, + status: this.status, + subproblems: this.subproblems + }; + } +} + +/** + * malformed - The request message was malformed (RFC 8555 Section 6.7.1) + */ +export class AcmeMalformedError extends AcmeError { + constructor({ + detail = "The request message was malformed", + error, + message + }: { + detail?: string; + error?: unknown; + message?: string; + } = {}) { + super({ + type: "malformed", + detail, + status: 400, + error, + message + }); + this.name = "AcmeMalformedError"; + } +} + +/** + * unauthorized - The client lacks sufficient authorization (RFC 8555 Section 6.7.2) + */ +export class AcmeUnauthorizedError extends AcmeError { + constructor({ + detail = "The client lacks sufficient authorization", + error, + message + }: { + detail?: string; + error?: unknown; + message?: string; + } = {}) { + super({ + type: "unauthorized", + detail, + status: 403, + error, + message + }); + this.name = "AcmeUnauthorizedError"; + } +} + +/** + * accountDoesNotExist - The request specified an account that does not exist + * (RFC 8555 Section 6.7.3) + */ +export class AcmeAccountDoesNotExistError extends AcmeError { + constructor({ + detail = "The request specified an account that does not exist", + error, + message + }: { + detail?: string; + error?: unknown; + message?: string; + } = {}) { + super({ + type: "accountDoesNotExist", + detail, + status: 400, + error, + message + }); + this.name = "AcmeAccountDoesNotExistError"; + } +} + +/** + * badNonce - The client sent an unacceptable anti-replay nonce (RFC 8555 Section 6.7.4) + */ +export class AcmeBadNonceError extends AcmeError { + constructor({ + detail = "The client sent an unacceptable anti-replay nonce", + error, + message + }: { + detail?: string; + error?: unknown; + message?: string; + } = {}) { + super({ + type: "badNonce", + detail, + status: 400, + error, + message + }); + this.name = "AcmeBadNonceError"; + } +} + +/** + * badSignature - The JWS signature is invalid (RFC 8555 Section 6.7.5) + */ +export class AcmeBadSignatureError extends AcmeError { + constructor({ + detail = "The JWS signature is invalid", + error, + message + }: { + detail?: string; + error?: unknown; + message?: string; + } = {}) { + super({ + type: "badSignature", + detail, + status: 401, + error, + message + }); + this.name = "AcmeBadSignatureError"; + } +} + +/** + * badPublicKey - The public key is not acceptable (RFC 8555 Section 6.7.6) + */ +export class AcmeBadPublicKeyError extends AcmeError { + constructor({ + detail = "The public key is not acceptable", + error, + message + }: { + detail?: string; + error?: unknown; + message?: string; + } = {}) { + super({ + type: "badPublicKey", + detail, + status: 400, + error, + message + }); + this.name = "AcmeBadPublicKeyError"; + } +} + +/** + * badCSR - The CSR is unacceptable (RFC 8555 Section 6.7.7) + */ +export class AcmeBadCsrError extends AcmeError { + constructor({ + detail = "The CSR is unacceptable", + error, + message + }: { + detail?: string; + error?: unknown; + message?: string; + } = {}) { + super({ + type: "badCSR", + detail, + status: 400, + error, + message + }); + this.name = "AcmeBadCsrError"; + } +} + +/** + * badRevocationReason - The revocation reason provided is not allowed + * (RFC 8555 Section 6.7.8) + */ +export class AcmeBadRevocationReasonError extends AcmeError { + constructor({ + detail = "The revocation reason provided is not allowed", + error, + message + }: { + detail?: string; + error?: unknown; + message?: string; + } = {}) { + super({ + type: "badRevocationReason", + detail, + status: 400, + error, + message + }); + this.name = "AcmeBadRevocationReasonError"; + } +} + +/** + * rateLimited - The client has exceeded a rate limit (RFC 8555 Section 6.7.9) + */ +export class AcmeRateLimitedError extends AcmeError { + constructor({ + detail = "The client has exceeded a rate limit", + error, + message + }: { + detail?: string; + error?: unknown; + message?: string; + } = {}) { + super({ + type: "rateLimited", + detail, + status: 429, + error, + message + }); + this.name = "AcmeRateLimitedError"; + } +} + +/** + * rejectedIdentifier - The server will not issue certificates for the identifier + * (RFC 8555 Section 6.7.10) + */ +export class AcmeRejectedIdentifierError extends AcmeError { + constructor({ + detail = "The server will not issue certificates for the identifier", + subproblems, + error, + message + }: { + detail?: string; + subproblems?: Array<{ type: string; detail: string; identifier?: { type: string; value: string } }>; + error?: unknown; + message?: string; + } = {}) { + super({ + type: "rejectedIdentifier", + detail, + status: 400, + subproblems, + error, + message + }); + this.name = "AcmeRejectedIdentifierError"; + } +} + +/** + * serverInternal - An internal error occurred (RFC 8555 Section 6.7.11) + */ +export class AcmeServerInternalError extends AcmeError { + constructor({ + detail = "An internal error occurred", + error, + message + }: { + detail?: string; + error?: unknown; + message?: string; + } = {}) { + super({ + type: "serverInternal", + detail, + status: 500, + error, + message + }); + this.name = "AcmeServerInternalError"; + } +} + +/** + * serviceUnavailable - The service is unavailable (RFC 8555 Section 6.7.12) + */ +export class AcmeServiceUnavailableError extends AcmeError { + constructor({ + detail = "The service is unavailable", + error, + message + }: { + detail?: string; + error?: unknown; + message?: string; + } = {}) { + super({ + type: "serviceUnavailable", + detail, + status: 503, + error, + message + }); + this.name = "AcmeServiceUnavailableError"; + } +} + +/** + * unsupportedContact - A contact URL is of an unsupported type (RFC 8555 Section 6.7.13) + */ +export class AcmeUnsupportedContactError extends AcmeError { + constructor({ + detail = "A contact URL is of an unsupported type", + error, + message + }: { + detail?: string; + error?: unknown; + message?: string; + } = {}) { + super({ + type: "unsupportedContact", + detail, + status: 400, + error, + message + }); + this.name = "AcmeUnsupportedContactError"; + } +} + +/** + * unsupportedIdentifier - An identifier is of an unsupported type + * (RFC 8555 Section 6.7.14) + */ +export class AcmeUnsupportedIdentifierError extends AcmeError { + constructor({ + detail = "An identifier is of an unsupported type", + error, + message + }: { + detail?: string; + error?: unknown; + message?: string; + } = {}) { + super({ + type: "unsupportedIdentifier", + detail, + status: 400, + error, + message + }); + this.name = "AcmeUnsupportedIdentifierError"; + } +} + +/** + * userActionRequired - Visit the "instance" URL and take actions specified there + * (RFC 8555 Section 6.7.15) + */ +export class AcmeUserActionRequiredError extends AcmeError { + instance?: string; + + constructor({ + detail = "Visit the instance URL and take actions specified there", + instance, + error, + message + }: { + detail?: string; + instance?: string; + error?: unknown; + message?: string; + } = {}) { + super({ + type: "userActionRequired", + detail, + status: 403, + error, + message + }); + this.instance = instance; + this.name = "AcmeUserActionRequiredError"; + } + + toAcmeResponse(): IAcmeError & { instance?: string } { + return { + ...super.toAcmeResponse(), + instance: this.instance + }; + } +} diff --git a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts index cdce6e9b8..8530ad16f 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts @@ -1,5 +1,25 @@ import { z } from "zod"; +export const ProtectedHeaderSchema = z.object({ + alg: z.string(), + nonce: z.string(), + url: z.string(), + kid: z.string().optional(), + jwk: z.record(z.string(), z.string()).optional() +}); + +// Raw JWS payload schema before parsing and verification +export const RawJwsPayloadSchema = z.object({ + protected: z.string(), + payload: z.string(), + signature: z.string() +}); + +export const JwsPayloadSchema = z.object({ + protectedHeader: ProtectedHeaderSchema, + payload: z.any() +}); + // Directory endpoint export const GetAcmeDirectorySchema = z.object({ params: z.object({ diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index d95f1f11a..2ce87b37a 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -1,8 +1,10 @@ import { getConfig } from "@app/lib/config/env"; -import { NotFoundError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; +import { AcmeMalformedError, AcmeBadPublicKeyError } from "./pki-acme-errors"; + import { EnrollmentType, TCertificateProfileWithConfigs @@ -19,18 +21,20 @@ import { TGetAcmeAuthorizationResponse, TGetAcmeDirectoryResponse, TGetAcmeOrderResponse, + TRawJwsPayload, TListAcmeOrdersResponse, TPkiAcmeServiceFactory, - TRespondToAcmeChallengeResponse + TRespondToAcmeChallengeResponse, + TJwsPayload, + TProtectedHeader } from "./pki-acme-types"; +import { flattenedVerify, importJWK, JWK, JWSHeaderParameters } from "jose"; type TPkiAcmeServiceFactoryDep = { certificateProfileDAL: Pick; }; export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => { - const appCfg = getConfig(); - const validateAcmeProfile = async (profileId: string): Promise => { const profile = await certificateProfileDAL.findById(profileId); if (!profile) { @@ -43,18 +47,35 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService }; const buildUrl = (path: string): string => { + const appCfg = getConfig(); const baseUrl = appCfg.SITE_URL ?? ""; return `${baseUrl}${path}`; }; + const validateCreateAcmeAccountJwsPayload = async (rawPayload: TRawJwsPayload): Promise => { + const { payload, protectedHeader } = await flattenedVerify( + rawPayload, + async (protectedHeader: JWSHeaderParameters | undefined) => { + if (protectedHeader === undefined) { + throw new AcmeMalformedError({ detail: "Protected header is required" }); + } + if (protectedHeader.jwk === undefined) { + throw new AcmeBadPublicKeyError({ detail: "JWK is required in the protected header" }); + } + // For the create account request, the JWK is provided in the protected header. + // Let use it to verify the signature. + const imported = await importJWK(protectedHeader.jwk as JWK, protectedHeader.alg); + return imported; + } + ); + const decoder = new TextDecoder(); + const parsedPayload = JSON.parse(decoder.decode(payload)) as TCreateAcmeAccountPayload; + // TODO: also consume the nonce here + return { payload: parsedPayload, protectedHeader: protectedHeader as TProtectedHeader }; + }; + const getAcmeDirectory = async (profileId: string): Promise => { - // FIXME: Implement ACME directory endpoint - // Validate profile exists and is for ACME enrollment - const profile = await validateAcmeProfile(profileId); - - // FIXME: Validate profile is configured for ACME enrollment - - // Return absolute URLs using SITE_URL + await validateAcmeProfile(profileId); return { newNonce: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/new-nonce`), newAccount: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/new-account`), @@ -71,7 +92,7 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService const createAcmeAccount = async ( profileId: string, - body: TCreateAcmeAccountPayload + payload: TCreateAcmeAccountPayload ): Promise => { const profile = await validateAcmeProfile(profileId); // FIXME: Implement ACME new account registration @@ -88,7 +109,7 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService const createAcmeOrder = async ( profileId: string, - body: TCreateAcmeOrderPayload + payload: TCreateAcmeOrderPayload ): Promise => { const profile = await validateAcmeProfile(profileId); // FIXME: Implement ACME new order creation @@ -105,7 +126,7 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService const deactivateAcmeAccount = async ( profileId: string, accountId: string, - body?: TDeactivateAcmeAccountPayload + payload?: TDeactivateAcmeAccountPayload ): Promise => { const profile = await validateAcmeProfile(profileId); // FIXME: Implement ACME account deactivation @@ -137,10 +158,10 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService const finalizeAcmeOrder = async ( profileId: string, orderId: string, - body: TFinalizeAcmeOrderPayload + payload: TFinalizeAcmeOrderPayload ): Promise => { const profile = await validateAcmeProfile(profileId); - const { csr } = body; + const { csr } = payload; // FIXME: Implement ACME finalize order return { status: "processing", @@ -196,6 +217,7 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService }; return { + validateCreateAcmeAccountJwsPayload, getAcmeDirectory, getAcmeNewNonce, createAcmeAccount, diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index f004e5102..ffe5df0d3 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -12,7 +12,10 @@ import { GetAcmeAuthorizationResponseSchema, GetAcmeDirectoryResponseSchema, GetAcmeOrderResponseSchema, + JwsPayloadSchema, ListAcmeOrdersResponseSchema, + ProtectedHeaderSchema, + RawJwsPayloadSchema, RespondToAcmeChallengeResponseSchema } from "./pki-acme-schemas"; @@ -28,12 +31,16 @@ export type TGetAcmeAuthorizationResponse = z.infer; // Payload types +export type TRawJwsPayload = z.infer; +export type TJwsPayload = z.infer; +export type TProtectedHeader = z.infer; export type TCreateAcmeAccountPayload = z.infer; export type TCreateAcmeOrderPayload = z.infer; export type TDeactivateAcmeAccountPayload = z.infer; export type TFinalizeAcmeOrderPayload = z.infer; export type TPkiAcmeServiceFactory = { + validateCreateAcmeAccountJwsPayload(body: TRawJwsPayload): Promise; getAcmeDirectory: (profileId: string) => Promise; getAcmeNewNonce: (profileId: string) => Promise; createAcmeAccount: (profileId: string, body: TCreateAcmeAccountPayload) => Promise; From f06f6108f4fa09a7edc38eccd5d38aef724a3ff4 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 28 Oct 2025 14:04:06 -0700 Subject: [PATCH 093/294] More for validate jws payload --- .../ee/services/pki-acme/pki-acme-schemas.ts | 19 ++++++++----- .../ee/services/pki-acme/pki-acme-service.ts | 28 +++++++++++-------- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts index 8530ad16f..16862ca4a 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts @@ -1,12 +1,17 @@ import { z } from "zod"; -export const ProtectedHeaderSchema = z.object({ - alg: z.string(), - nonce: z.string(), - url: z.string(), - kid: z.string().optional(), - jwk: z.record(z.string(), z.string()).optional() -}); +export const ProtectedHeaderSchema = z + .object({ + alg: z.string(), + nonce: z.string(), + url: z.string(), + kid: z.string().optional(), + jwk: z.record(z.string(), z.string()).optional() + }) + .refine((data) => data.kid || data.jwk, { + message: "Either kid or jwk must be provided", + path: ["kid", "jwk"] + }); // Raw JWS payload schema before parsing and verification export const RawJwsPayloadSchema = z.object({ diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 2ce87b37a..78ed98369 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -1,14 +1,16 @@ import { getConfig } from "@app/lib/config/env"; -import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { NotFoundError } from "@app/lib/errors"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; -import { AcmeMalformedError, AcmeBadPublicKeyError } from "./pki-acme-errors"; +import { AcmeBadPublicKeyError, AcmeMalformedError } from "./pki-acme-errors"; import { EnrollmentType, TCertificateProfileWithConfigs } from "@app/services/certificate-profile/certificate-profile-types"; +import { flattenedVerify, importJWK, JWK, JWSHeaderParameters } from "jose"; +import { ProtectedHeaderSchema } from "./pki-acme-schemas"; import { TCreateAcmeAccountPayload, TCreateAcmeAccountResponse, @@ -21,14 +23,12 @@ import { TGetAcmeAuthorizationResponse, TGetAcmeDirectoryResponse, TGetAcmeOrderResponse, - TRawJwsPayload, + TJwsPayload, TListAcmeOrdersResponse, TPkiAcmeServiceFactory, - TRespondToAcmeChallengeResponse, - TJwsPayload, - TProtectedHeader + TRawJwsPayload, + TRespondToAcmeChallengeResponse } from "./pki-acme-types"; -import { flattenedVerify, importJWK, JWK, JWSHeaderParameters } from "jose"; type TPkiAcmeServiceFactoryDep = { certificateProfileDAL: Pick; @@ -52,9 +52,9 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService return `${baseUrl}${path}`; }; - const validateCreateAcmeAccountJwsPayload = async (rawPayload: TRawJwsPayload): Promise => { - const { payload, protectedHeader } = await flattenedVerify( - rawPayload, + const validateCreateAcmeAccountJwsPayload = async (rawJwsPayload: TRawJwsPayload): Promise => { + const { payload: rawPayload, protectedHeader: rawProtectedHeader } = await flattenedVerify( + rawJwsPayload, async (protectedHeader: JWSHeaderParameters | undefined) => { if (protectedHeader === undefined) { throw new AcmeMalformedError({ detail: "Protected header is required" }); @@ -68,10 +68,14 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService return imported; } ); + const { success, data: protectedHeader } = ProtectedHeaderSchema.safeParse(rawProtectedHeader); + if (!success) { + throw new AcmeMalformedError({ detail: "Invalid protected header" }); + } const decoder = new TextDecoder(); - const parsedPayload = JSON.parse(decoder.decode(payload)) as TCreateAcmeAccountPayload; + const payload = JSON.parse(decoder.decode(rawPayload)) as TCreateAcmeAccountPayload; // TODO: also consume the nonce here - return { payload: parsedPayload, protectedHeader: protectedHeader as TProtectedHeader }; + return { payload, protectedHeader }; }; const getAcmeDirectory = async (profileId: string): Promise => { From a29029c192eacbbad5ad5b5118f7fc8b097bbf2a Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 28 Oct 2025 15:30:51 -0700 Subject: [PATCH 094/294] Keep public key as a jwk json obj instead --- .../db/migrations/20251027234547_add-pki-acme.ts | 4 ++-- backend/src/db/schemas/pki-acme-accounts.ts | 2 +- backend/src/ee/routes/v1/pki-acme-router.ts | 14 +++++++++++--- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/backend/src/db/migrations/20251027234547_add-pki-acme.ts b/backend/src/db/migrations/20251027234547_add-pki-acme.ts index a2e1ab777..c0cf0db4f 100644 --- a/backend/src/db/migrations/20251027234547_add-pki-acme.ts +++ b/backend/src/db/migrations/20251027234547_add-pki-acme.ts @@ -54,8 +54,8 @@ export async function up(knex: Knex): Promise { // Multi-value emails array t.specificType("emails", "text[]").notNullable(); - // Public key (PEM format) - t.text("publicKey").notNullable(); + // Public key (JWK format) + t.jsonb("publicKey").notNullable(); t.timestamps(true, true, true); }); diff --git a/backend/src/db/schemas/pki-acme-accounts.ts b/backend/src/db/schemas/pki-acme-accounts.ts index 739e7057b..b599cec07 100644 --- a/backend/src/db/schemas/pki-acme-accounts.ts +++ b/backend/src/db/schemas/pki-acme-accounts.ts @@ -11,7 +11,7 @@ export const PkiAcmeAccountsSchema = z.object({ id: z.string().uuid(), profileId: z.string().uuid(), emails: z.string().array(), - publicKey: z.string(), + publicKey: z.unknown(), createdAt: z.date(), updatedAt: z.date() }); diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index 5f19359a6..c8ca31186 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -20,12 +20,14 @@ import { GetAcmeOrderSchema, ListAcmeOrdersResponseSchema, ListAcmeOrdersSchema, + RawJwsPayloadSchema, RespondToAcmeChallengeResponseSchema, RespondToAcmeChallengeSchema } from "@app/ee/services/pki-acme/pki-acme-schemas"; import { ApiDocsTags } from "@app/lib/api-docs"; import { getConfig } from "@app/lib/config/env"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { TRawJwsPayload } from "@app/ee/services/pki-acme/pki-acme-types"; export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { const appCfg = getConfig(); @@ -104,16 +106,22 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME New Account - register a new account or find existing one", - ...CreateAcmeAccountSchema.shape, + ...RawJwsPayloadSchema.shape, response: { 201: CreateAcmeAccountResponseSchema } }, handler: async (req, res) => { // TODO: check nonce here - // TODO: check signature here + const { payload, protectedHeader, jwk } = await server.services.pkiAcme.validateCreateAcmeAccountJwsPayload( + req.body as TRawJwsPayload + ); - const account = await server.services.pkiAcme.createAcmeAccount(req.params.profileId, req.body); + const account = await server.services.pkiAcme.createAcmeAccount( + req.params.profileId, + jwk, + payload as TCreateAcmeAccountPayload + ); // TODO: deal with existing account case here res.code(201); res.header( From 4883c3ead33c76a0a7eafcf9300ec76a2be5b8b6 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 28 Oct 2025 16:04:26 -0700 Subject: [PATCH 095/294] Implement account creation --- .../services/pki-acme/pki-acme-account-dal.ts | 105 ++++++++++++++++++ .../ee/services/pki-acme/pki-acme-schemas.ts | 5 +- .../ee/services/pki-acme/pki-acme-service.ts | 64 ++++++++--- .../ee/services/pki-acme/pki-acme-types.ts | 18 ++- 4 files changed, 174 insertions(+), 18 deletions(-) create mode 100644 backend/src/ee/services/pki-acme/pki-acme-account-dal.ts diff --git a/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts new file mode 100644 index 000000000..93f7b7055 --- /dev/null +++ b/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts @@ -0,0 +1,105 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { TPkiAcmeAccountsInsert, TPkiAcmeAccountsUpdate } from "@app/db/schemas/pki-acme-accounts"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify } from "@app/lib/knex"; + +export type TPkiAcmeAccountDALFactory = ReturnType; + +export const pkiAcmeAccountDALFactory = (db: TDbClient) => { + const pkiAcmeAccountOrm = ormify(db, TableName.PkiAcmeAccount); + + const create = async (data: TPkiAcmeAccountsInsert, tx?: Knex) => { + try { + const result = await (tx || db)(TableName.PkiAcmeAccount).insert(data).returning("*"); + const [account] = result; + + if (!account) { + throw new Error("Failed to create PKI ACME account"); + } + + return account; + } catch (error) { + throw new DatabaseError({ error, name: "Create PKI ACME account" }); + } + }; + + const updateById = async (id: string, data: TPkiAcmeAccountsUpdate, tx?: Knex) => { + try { + const result = await (tx || db)(TableName.PkiAcmeAccount).where({ id }).update(data).returning("*"); + const [account] = result; + + if (!account) { + return null; + } + + return account; + } catch (error) { + throw new DatabaseError({ error, name: "Update PKI ACME account" }); + } + }; + + const findById = async (id: string, tx?: Knex) => { + try { + const account = await (tx || db)(TableName.PkiAcmeAccount).where({ id }).first(); + + return account || null; + } catch (error) { + throw new DatabaseError({ error, name: "Find PKI ACME account by id" }); + } + }; + + const findByProfileId = async (profileId: string, tx?: Knex) => { + try { + const account = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId }).first(); + + return account || null; + } catch (error) { + throw new DatabaseError({ error, name: "Find PKI ACME account by profile id" }); + } + }; + + const findByPublicKey = async (publicKey: unknown, tx?: Knex) => { + try { + const account = await (tx || db)(TableName.PkiAcmeAccount).where({ publicKey }).first(); + + return account || null; + } catch (error) { + throw new DatabaseError({ error, name: "Find PKI ACME account by public key" }); + } + }; + + const findManyByProfileId = async (profileId: string, tx?: Knex) => { + try { + const accounts = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId }); + + return accounts; + } catch (error) { + throw new DatabaseError({ error, name: "Find many PKI ACME accounts by profile id" }); + } + }; + + const deleteById = async (id: string, tx?: Knex) => { + try { + const result = await (tx || db)(TableName.PkiAcmeAccount).where({ id }).delete().returning("*"); + const [account] = result; + + return account || null; + } catch (error) { + throw new DatabaseError({ error, name: "Delete PKI ACME account by id" }); + } + }; + + return { + ...pkiAcmeAccountOrm, + create, + updateById, + findById, + findByProfileId, + findByPublicKey, + findManyByProfileId, + deleteById + }; +}; diff --git a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts index 16862ca4a..599203dae 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts @@ -22,7 +22,7 @@ export const RawJwsPayloadSchema = z.object({ export const JwsPayloadSchema = z.object({ protectedHeader: ProtectedHeaderSchema, - payload: z.any() + payload: z.unknown() }); // Directory endpoint @@ -71,8 +71,7 @@ export const CreateAcmeAccountSchema = z.object({ export const CreateAcmeAccountResponseSchema = z.object({ status: z.string(), contact: z.array(z.string()).optional(), - orders: z.string().optional(), - accountUrl: z.string() + orders: z.string().optional() }); // New Order payload schema diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 78ed98369..7f1c07e7f 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -3,7 +3,7 @@ import { NotFoundError } from "@app/lib/errors"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; -import { AcmeBadPublicKeyError, AcmeMalformedError } from "./pki-acme-errors"; +import { AcmeAccountDoesNotExistError, AcmeBadPublicKeyError, AcmeMalformedError } from "./pki-acme-errors"; import { EnrollmentType, @@ -12,6 +12,7 @@ import { import { flattenedVerify, importJWK, JWK, JWSHeaderParameters } from "jose"; import { ProtectedHeaderSchema } from "./pki-acme-schemas"; import { + TAcmeResponse, TCreateAcmeAccountPayload, TCreateAcmeAccountResponse, TCreateAcmeOrderPayload, @@ -24,17 +25,23 @@ import { TGetAcmeDirectoryResponse, TGetAcmeOrderResponse, TJwsPayload, + TJwsPayloadWithJwk, TListAcmeOrdersResponse, TPkiAcmeServiceFactory, TRawJwsPayload, TRespondToAcmeChallengeResponse } from "./pki-acme-types"; +import { TPkiAcmeAccountDALFactory } from "./pki-acme-account-dal"; type TPkiAcmeServiceFactoryDep = { certificateProfileDAL: Pick; + pkiAcmeAccountDAL: Pick; }; -export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => { +export const pkiAcmeServiceFactory = ({ + certificateProfileDAL, + pkiAcmeAccountDAL +}: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => { const validateAcmeProfile = async (profileId: string): Promise => { const profile = await certificateProfileDAL.findById(profileId); if (!profile) { @@ -52,7 +59,7 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService return `${baseUrl}${path}`; }; - const validateCreateAcmeAccountJwsPayload = async (rawJwsPayload: TRawJwsPayload): Promise => { + const validateCreateAcmeAccountJwsPayload = async (rawJwsPayload: TRawJwsPayload): Promise => { const { payload: rawPayload, protectedHeader: rawProtectedHeader } = await flattenedVerify( rawJwsPayload, async (protectedHeader: JWSHeaderParameters | undefined) => { @@ -72,10 +79,11 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService if (!success) { throw new AcmeMalformedError({ detail: "Invalid protected header" }); } + const decoder = new TextDecoder(); const payload = JSON.parse(decoder.decode(rawPayload)) as TCreateAcmeAccountPayload; // TODO: also consume the nonce here - return { payload, protectedHeader }; + return { payload, protectedHeader, jwk: protectedHeader.jwk as JsonWebKey }; }; const getAcmeDirectory = async (profileId: string): Promise => { @@ -96,18 +104,47 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService const createAcmeAccount = async ( profileId: string, + jwk: JWK, payload: TCreateAcmeAccountPayload - ): Promise => { + ): Promise> => { const profile = await validateAcmeProfile(profileId); - // FIXME: Implement ACME new account registration - // Use EAB authentication to find corresponding Infisical machine identity - // Check permissions and return account information - const accountId = "FIXME-account-id"; + // TODO: the jwk as json obj may not be the best idea for indexing. + // Maybe we should find a way to serialize the jwk deterministically. + let account = await pkiAcmeAccountDAL.findByPublicKey(jwk); + if (payload.onlyReturnExisting && !account) { + throw new AcmeAccountDoesNotExistError({ message: "ACME account not found" }); + } + if (account) { + // With the same public key, we found an existing account, just return it + return { + status: 200, + payload: { + status: "valid", + contact: account.emails, + orders: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${account.id}/orders`) + }, + headers: { + Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${account.id}`) + } + }; + } + + account = await pkiAcmeAccountDAL.create({ + profileId, + publicKey: jwk, + emails: payload.contact ?? [] + }); + // TODO: check EAB authentication here return { - status: "valid", - accountUrl: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${accountId}`), - contact: [], - orders: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${accountId}/orders`) + status: 201, + payload: { + status: "valid", + contact: account.emails, + orders: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${account.id}/orders`) + }, + headers: { + Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${account.id}`) + } }; }; @@ -116,6 +153,7 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService payload: TCreateAcmeOrderPayload ): Promise => { const profile = await validateAcmeProfile(profileId); + // FIXME: Implement ACME new order creation const orderId = "FIXME-order-id"; return { diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index ffe5df0d3..9009bc0a7 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +import { JWK } from "jose"; import { CreateAcmeAccountBodySchema, CreateAcmeAccountResponseSchema, @@ -39,11 +40,24 @@ export type TCreateAcmeOrderPayload = z.infer; export type TDeactivateAcmeAccountPayload = z.infer; export type TFinalizeAcmeOrderPayload = z.infer; +export type TJwsPayloadWithJwk = TJwsPayload & { + jwk: JsonWebKey; +}; +export type TAcmeResponse = { + status: number; + headers: Record; + payload: TPayload; +}; + export type TPkiAcmeServiceFactory = { - validateCreateAcmeAccountJwsPayload(body: TRawJwsPayload): Promise; + validateCreateAcmeAccountJwsPayload(body: TRawJwsPayload): Promise; getAcmeDirectory: (profileId: string) => Promise; getAcmeNewNonce: (profileId: string) => Promise; - createAcmeAccount: (profileId: string, body: TCreateAcmeAccountPayload) => Promise; + createAcmeAccount: ( + profileId: string, + jwk: JsonWebKey, + body: TCreateAcmeAccountPayload + ) => Promise>; createAcmeOrder: (profileId: string, body: TCreateAcmeOrderPayload) => Promise; deactivateAcmeAccount: ( profileId: string, From a84303588d2abec35bc802c1203465335a9e0f86 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 28 Oct 2025 16:26:02 -0700 Subject: [PATCH 096/294] More on the account --- backend/src/ee/routes/v1/pki-acme-router.ts | 22 ++++++++----------- .../ee/services/pki-acme/pki-acme-types.ts | 3 +-- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index c8ca31186..e0762b7f3 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -3,7 +3,6 @@ import { z } from "zod"; import { CreateAcmeAccountResponseSchema, - CreateAcmeAccountSchema, CreateAcmeOrderResponseSchema, CreateAcmeOrderSchema, DeactivateAcmeAccountResponseSchema, @@ -24,10 +23,10 @@ import { RespondToAcmeChallengeResponseSchema, RespondToAcmeChallengeSchema } from "@app/ee/services/pki-acme/pki-acme-schemas"; +import { TCreateAcmeAccountPayload, TRawJwsPayload } from "@app/ee/services/pki-acme/pki-acme-types"; import { ApiDocsTags } from "@app/lib/api-docs"; import { getConfig } from "@app/lib/config/env"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; -import { TRawJwsPayload } from "@app/ee/services/pki-acme/pki-acme-types"; export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { const appCfg = getConfig(); @@ -112,28 +111,25 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { } }, handler: async (req, res) => { - // TODO: check nonce here - const { payload, protectedHeader, jwk } = await server.services.pkiAcme.validateCreateAcmeAccountJwsPayload( + const { payload, jwk } = await server.services.pkiAcme.validateCreateAcmeAccountJwsPayload( req.body as TRawJwsPayload ); - - const account = await server.services.pkiAcme.createAcmeAccount( + const { status, body, headers } = await server.services.pkiAcme.createAcmeAccount( req.params.profileId, jwk, payload as TCreateAcmeAccountPayload ); - // TODO: deal with existing account case here - res.code(201); - res.header( - "Location", - `${appCfg.SITE_URL}/api/v1/pki/acme/profiles/${req.params.profileId}/accounts/${account.accountUrl}` - ); + // TODO: DRY + res.code(status); + for (const [key, value] of Object.entries(headers)) { + res.header(key, value); + } // TODO: DRY const nonce = await server.services.pkiAcme.getAcmeNewNonce(req.params.profileId); res.header("Replay-Nonce", nonce); res.header("Cache-Control", "no-store"); - return account; + return body; } }); diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index 9009bc0a7..fe1105ff8 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -1,6 +1,5 @@ import { z } from "zod"; -import { JWK } from "jose"; import { CreateAcmeAccountBodySchema, CreateAcmeAccountResponseSchema, @@ -46,7 +45,7 @@ export type TJwsPayloadWithJwk = TJwsPayload & { export type TAcmeResponse = { status: number; headers: Record; - payload: TPayload; + body: TPayload; }; export type TPkiAcmeServiceFactory = { From 23d233659496e68a3012e0d0c3e7c60699daa809 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 28 Oct 2025 16:39:13 -0700 Subject: [PATCH 097/294] Bind ACME stuff --- backend/bdd/features/steps/pki_acme.py | 2 +- .../ee/services/pki-acme/pki-acme-service.ts | 13 +- backend/src/server/routes/index.ts | 120 +++++++++--------- 3 files changed, 70 insertions(+), 65 deletions(-) diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 0f04849e6..eb8a04315 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -36,7 +36,7 @@ def step_impl(context: Context, profile_var: str): # TODO: Fixed value for now, just to make test much easier, # we should call infisical API to create such profile instead # in the future - profile_id = "40dd4794-66e3-4bd3-9875-c1f819804bb2" + profile_id = "0e96a01b-017e-4660-8b3d-ff26018fe0ce" context.vars[profile_var] = AcmeProfile(profile_id) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 7f1c07e7f..0040f143b 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -31,6 +31,7 @@ import { TRawJwsPayload, TRespondToAcmeChallengeResponse } from "./pki-acme-types"; +import { TPkiAcmeAccount } from "@app/db/schemas/pki-acme-accounts"; import { TPkiAcmeAccountDALFactory } from "./pki-acme-account-dal"; type TPkiAcmeServiceFactoryDep = { @@ -105,20 +106,20 @@ export const pkiAcmeServiceFactory = ({ const createAcmeAccount = async ( profileId: string, jwk: JWK, - payload: TCreateAcmeAccountPayload + { onlyReturnExisting, contact }: TCreateAcmeAccountPayload ): Promise> => { const profile = await validateAcmeProfile(profileId); // TODO: the jwk as json obj may not be the best idea for indexing. // Maybe we should find a way to serialize the jwk deterministically. - let account = await pkiAcmeAccountDAL.findByPublicKey(jwk); - if (payload.onlyReturnExisting && !account) { + let account: TPkiAcmeAccount | null = await pkiAcmeAccountDAL.findByPublicKey(jwk); + if (onlyReturnExisting && !account) { throw new AcmeAccountDoesNotExistError({ message: "ACME account not found" }); } if (account) { // With the same public key, we found an existing account, just return it return { status: 200, - payload: { + body: { status: "valid", contact: account.emails, orders: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${account.id}/orders`) @@ -132,12 +133,12 @@ export const pkiAcmeServiceFactory = ({ account = await pkiAcmeAccountDAL.create({ profileId, publicKey: jwk, - emails: payload.contact ?? [] + emails: contact ?? [] }); // TODO: check EAB authentication here return { status: 201, - payload: { + body: { status: "valid", contact: account.emails, orders: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${account.id}/orders`) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index e9ab34b17..b1e4b9b7c 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -17,31 +17,30 @@ import { accessApprovalRequestDALFactory } from "@app/ee/services/access-approva import { accessApprovalRequestReviewerDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-reviewer-dal"; import { accessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service"; import { assumePrivilegeServiceFactory } from "@app/ee/services/assume-privilege/assume-privilege-service"; +import { auditLogStreamDALFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-dal"; +import { auditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service"; import { auditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal"; import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue"; import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; -import { auditLogStreamDALFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-dal"; -import { auditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service"; import { certificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; import { certificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-service"; import { certificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service"; -import { pkiAcmeServiceFactory } from "@app/ee/services/pki-acme/pki-acme-service"; -import { dynamicSecretDALFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-dal"; -import { dynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service"; -import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/providers"; import { dynamicSecretLeaseDALFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal"; import { dynamicSecretLeaseQueueServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue"; import { dynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service"; +import { dynamicSecretDALFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-dal"; +import { dynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service"; +import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/providers"; import { eventBusFactory } from "@app/ee/services/event/event-bus-service"; import { sseServiceFactory } from "@app/ee/services/event/event-sse-service"; import { externalKmsDALFactory } from "@app/ee/services/external-kms/external-kms-dal"; import { externalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service"; -import { gatewayDALFactory } from "@app/ee/services/gateway/gateway-dal"; -import { gatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; -import { orgGatewayConfigDALFactory } from "@app/ee/services/gateway/org-gateway-config-dal"; import { gatewayV2DalFactory } from "@app/ee/services/gateway-v2/gateway-v2-dal"; import { gatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; import { orgGatewayConfigV2DalFactory } from "@app/ee/services/gateway-v2/org-gateway-config-v2-dal"; +import { gatewayDALFactory } from "@app/ee/services/gateway/gateway-dal"; +import { gatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; +import { orgGatewayConfigDALFactory } from "@app/ee/services/gateway/org-gateway-config-dal"; import { githubOrgSyncDALFactory } from "@app/ee/services/github-org-sync/github-org-sync-dal"; import { githubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service"; import { groupDALFactory } from "@app/ee/services/group/group-dal"; @@ -75,6 +74,7 @@ import { pamSessionServiceFactory } from "@app/ee/services/pam-session/pam-sessi import { permissionDALFactory } from "@app/ee/services/permission/permission-dal"; import { permissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { pitServiceFactory } from "@app/ee/services/pit/pit-service"; +import { pkiAcmeServiceFactory } from "@app/ee/services/pki-acme/pki-acme-service"; import { projectTemplateDALFactory } from "@app/ee/services/project-template/project-template-dal"; import { projectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service"; import { rateLimitDALFactory } from "@app/ee/services/rate-limit/rate-limit-dal"; @@ -99,39 +99,39 @@ import { secretApprovalRequestReviewerDALFactory } from "@app/ee/services/secret import { secretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal"; import { secretApprovalRequestServiceFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-service"; import { secretReplicationServiceFactory } from "@app/ee/services/secret-replication/secret-replication-service"; -import { secretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal"; -import { secretRotationQueueFactory } from "@app/ee/services/secret-rotation/secret-rotation-queue"; -import { secretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service"; import { secretRotationV2DALFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-dal"; import { secretRotationV2QueueServiceFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-queue"; import { secretRotationV2ServiceFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-service"; +import { secretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal"; +import { secretRotationQueueFactory } from "@app/ee/services/secret-rotation/secret-rotation-queue"; +import { secretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service"; +import { secretScanningV2DALFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-dal"; +import { secretScanningV2QueueServiceFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-queue"; +import { secretScanningV2ServiceFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-service"; import { gitAppDALFactory } from "@app/ee/services/secret-scanning/git-app-dal"; import { gitAppInstallSessionDALFactory } from "@app/ee/services/secret-scanning/git-app-install-session-dal"; import { secretScanningDALFactory } from "@app/ee/services/secret-scanning/secret-scanning-dal"; import { secretScanningQueueFactory } from "@app/ee/services/secret-scanning/secret-scanning-queue"; import { secretScanningServiceFactory } from "@app/ee/services/secret-scanning/secret-scanning-service"; -import { secretScanningV2DALFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-dal"; -import { secretScanningV2QueueServiceFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-queue"; -import { secretScanningV2ServiceFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-service"; import { secretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; import { snapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal"; import { snapshotFolderDALFactory } from "@app/ee/services/secret-snapshot/snapshot-folder-dal"; import { snapshotSecretDALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-dal"; import { snapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-v2-dal"; -import { sshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal"; -import { sshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal"; -import { sshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh-certificate-authority-service"; -import { sshCertificateBodyDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-body-dal"; -import { sshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal"; import { sshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal"; import { sshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service"; +import { sshCertificateBodyDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-body-dal"; +import { sshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal"; +import { sshHostGroupDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-dal"; +import { sshHostGroupMembershipDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-membership-dal"; +import { sshHostGroupServiceFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-service"; import { sshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal"; import { sshHostLoginUserMappingDALFactory } from "@app/ee/services/ssh-host/ssh-host-login-user-mapping-dal"; import { sshHostServiceFactory } from "@app/ee/services/ssh-host/ssh-host-service"; import { sshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login-user-dal"; -import { sshHostGroupDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-dal"; -import { sshHostGroupMembershipDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-membership-dal"; -import { sshHostGroupServiceFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-service"; +import { sshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal"; +import { sshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal"; +import { sshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh-certificate-authority-service"; import { subOrgServiceFactory } from "@app/ee/services/sub-org/sub-org-service"; import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal"; import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service"; @@ -151,16 +151,12 @@ import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal"; import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service"; import { appConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { appConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; +import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal"; +import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { authDALFactory } from "@app/services/auth/auth-dal"; import { authLoginServiceFactory } from "@app/services/auth/auth-login-service"; import { authPaswordServiceFactory } from "@app/services/auth/auth-password-service"; import { authSignupServiceFactory } from "@app/services/auth/auth-signup-service"; -import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal"; -import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service"; -import { certificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; -import { certificateDALFactory } from "@app/services/certificate/certificate-dal"; -import { certificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; -import { certificateServiceFactory } from "@app/services/certificate/certificate-service"; import { certificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal"; import { certificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue"; @@ -173,39 +169,39 @@ import { internalCertificateAuthorityServiceFactory } from "@app/services/certif import { certificateEstV3ServiceFactory } from "@app/services/certificate-est-v3/certificate-est-v3-service"; import { certificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; import { certificateProfileServiceFactory } from "@app/services/certificate-profile/certificate-profile-service"; +import { certificateTemplateV2DALFactory } from "@app/services/certificate-template-v2/certificate-template-v2-dal"; +import { certificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service"; import { certificateSyncDALFactory } from "@app/services/certificate-sync/certificate-sync-dal"; import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal"; import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal"; import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; -import { certificateTemplateV2DALFactory } from "@app/services/certificate-template-v2/certificate-template-v2-dal"; -import { certificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service"; import { certificateV3QueueServiceFactory } from "@app/services/certificate-v3/certificate-v3-queue"; import { certificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service"; +import { certificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; +import { certificateDALFactory } from "@app/services/certificate/certificate-dal"; +import { certificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; +import { certificateServiceFactory } from "@app/services/certificate/certificate-service"; import { cmekServiceFactory } from "@app/services/cmek/cmek-service"; import { convertorServiceFactory } from "@app/services/convertor/convertor-service"; +import { acmeEnrollmentConfigDALFactory } from "@app/services/enrollment-config/acme-enrollment-config-dal"; import { apiEnrollmentConfigDALFactory } from "@app/services/enrollment-config/api-enrollment-config-dal"; import { estEnrollmentConfigDALFactory } from "@app/services/enrollment-config/est-enrollment-config-dal"; -import { acmeEnrollmentConfigDALFactory } from "@app/services/enrollment-config/acme-enrollment-config-dal"; import { externalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal"; import { externalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service"; import { externalMigrationQueueFactory } from "@app/services/external-migration/external-migration-queue"; import { externalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service"; import { vaultExternalMigrationConfigDALFactory } from "@app/services/external-migration/vault-external-migration-config-dal"; -import { folderCheckpointDALFactory } from "@app/services/folder-checkpoint/folder-checkpoint-dal"; import { folderCheckpointResourcesDALFactory } from "@app/services/folder-checkpoint-resources/folder-checkpoint-resources-dal"; +import { folderCheckpointDALFactory } from "@app/services/folder-checkpoint/folder-checkpoint-dal"; +import { folderCommitChangesDALFactory } from "@app/services/folder-commit-changes/folder-commit-changes-dal"; import { folderCommitDALFactory } from "@app/services/folder-commit/folder-commit-dal"; import { folderCommitQueueServiceFactory } from "@app/services/folder-commit/folder-commit-queue"; import { folderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service"; -import { folderCommitChangesDALFactory } from "@app/services/folder-commit-changes/folder-commit-changes-dal"; -import { folderTreeCheckpointDALFactory } from "@app/services/folder-tree-checkpoint/folder-tree-checkpoint-dal"; import { folderTreeCheckpointResourcesDALFactory } from "@app/services/folder-tree-checkpoint-resources/folder-tree-checkpoint-resources-dal"; +import { folderTreeCheckpointDALFactory } from "@app/services/folder-tree-checkpoint/folder-tree-checkpoint-dal"; import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service"; import { healthAlertServiceFactory } from "@app/services/health-alert/health-alert-queue"; -import { identityDALFactory } from "@app/services/identity/identity-dal"; -import { identityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal"; -import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal"; -import { identityServiceFactory } from "@app/services/identity/identity-service"; import { identityAccessTokenDALFactory } from "@app/services/identity-access-token/identity-access-token-dal"; import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service"; import { identityAliCloudAuthDALFactory } from "@app/services/identity-alicloud-auth/identity-alicloud-auth-dal"; @@ -235,23 +231,27 @@ import { identityTokenAuthServiceFactory } from "@app/services/identity-token-au import { identityUaClientSecretDALFactory } from "@app/services/identity-ua/identity-ua-client-secret-dal"; import { identityUaDALFactory } from "@app/services/identity-ua/identity-ua-dal"; import { identityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service"; -import { integrationDALFactory } from "@app/services/integration/integration-dal"; -import { integrationServiceFactory } from "@app/services/integration/integration-service"; +import { identityDALFactory } from "@app/services/identity/identity-dal"; +import { identityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal"; +import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal"; +import { identityServiceFactory } from "@app/services/identity/identity-service"; import { integrationAuthDALFactory } from "@app/services/integration-auth/integration-auth-dal"; import { integrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service"; +import { integrationDALFactory } from "@app/services/integration/integration-dal"; +import { integrationServiceFactory } from "@app/services/integration/integration-service"; import { internalKmsDALFactory } from "@app/services/kms/internal-kms-dal"; import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal"; import { TKmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; import { kmsServiceFactory } from "@app/services/kms/kms-service"; import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types"; -import { membershipDALFactory } from "@app/services/membership/membership-dal"; -import { membershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; import { membershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal"; import { membershipGroupServiceFactory } from "@app/services/membership-group/membership-group-service"; import { membershipIdentityDALFactory } from "@app/services/membership-identity/membership-identity-dal"; import { membershipIdentityServiceFactory } from "@app/services/membership-identity/membership-identity-service"; import { membershipUserDALFactory } from "@app/services/membership-user/membership-user-dal"; import { membershipUserServiceFactory } from "@app/services/membership-user/membership-user-service"; +import { membershipDALFactory } from "@app/services/membership/membership-dal"; +import { membershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; import { microsoftTeamsIntegrationDALFactory } from "@app/services/microsoft-teams/microsoft-teams-integration-dal"; import { microsoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service"; import { projectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal"; @@ -260,11 +260,11 @@ import { notificationServiceFactory } from "@app/services/notification/notificat import { userNotificationDALFactory } from "@app/services/notification/user-notification-dal"; import { offlineUsageReportDALFactory } from "@app/services/offline-usage-report/offline-usage-report-dal"; import { offlineUsageReportServiceFactory } from "@app/services/offline-usage-report/offline-usage-report-service"; +import { orgAdminServiceFactory } from "@app/services/org-admin/org-admin-service"; +import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { incidentContactDALFactory } from "@app/services/org/incident-contacts-dal"; import { orgDALFactory } from "@app/services/org/org-dal"; import { orgServiceFactory } from "@app/services/org/org-service"; -import { orgAdminServiceFactory } from "@app/services/org-admin/org-admin-service"; -import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { pamAccountRotationServiceFactory } from "@app/services/pam-account-rotation/pam-account-rotation-queue"; import { dailyExpiringPkiItemAlertQueueServiceFactory } from "@app/services/pki-alert/expiring-pki-item-alert-queue"; import { pkiAlertDALFactory } from "@app/services/pki-alert/pki-alert-dal"; @@ -281,10 +281,6 @@ import { pkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue"; import { pkiSyncServiceFactory } from "@app/services/pki-sync/pki-sync-service"; import { pkiTemplatesDALFactory } from "@app/services/pki-templates/pki-templates-dal"; import { pkiTemplatesServiceFactory } from "@app/services/pki-templates/pki-templates-service"; -import { projectDALFactory } from "@app/services/project/project-dal"; -import { projectQueueFactory } from "@app/services/project/project-queue"; -import { projectServiceFactory } from "@app/services/project/project-service"; -import { projectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal"; import { projectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { projectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { projectEnvDALFactory } from "@app/services/project-env/project-env-dal"; @@ -293,19 +289,18 @@ import { projectKeyDALFactory } from "@app/services/project-key/project-key-dal" import { projectKeyServiceFactory } from "@app/services/project-key/project-key-service"; import { projectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { projectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service"; +import { projectDALFactory } from "@app/services/project/project-dal"; +import { projectQueueFactory } from "@app/services/project/project-queue"; +import { projectServiceFactory } from "@app/services/project/project-service"; +import { projectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal"; +import { reminderRecipientDALFactory } from "@app/services/reminder-recipients/reminder-recipient-dal"; import { reminderDALFactory } from "@app/services/reminder/reminder-dal"; import { dailyReminderQueueServiceFactory } from "@app/services/reminder/reminder-queue"; import { reminderServiceFactory } from "@app/services/reminder/reminder-service"; -import { reminderRecipientDALFactory } from "@app/services/reminder-recipients/reminder-recipient-dal"; import { dailyResourceCleanUpQueueServiceFactory } from "@app/services/resource-cleanup/resource-cleanup-queue"; import { resourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal"; import { roleDALFactory } from "@app/services/role/role-dal"; import { roleServiceFactory } from "@app/services/role/role-service"; -import { secretDALFactory } from "@app/services/secret/secret-dal"; -import { secretQueueFactory } from "@app/services/secret/secret-queue"; -import { secretServiceFactory } from "@app/services/secret/secret-service"; -import { secretVersionDALFactory } from "@app/services/secret/secret-version-dal"; -import { secretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal"; import { secretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal"; import { secretBlindIndexServiceFactory } from "@app/services/secret-blind-index/secret-blind-index-service"; import { secretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; @@ -325,6 +320,11 @@ import { secretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret- import { secretV2BridgeServiceFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-service"; import { secretVersionV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-version-dal"; import { secretVersionV2TagBridgeDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal"; +import { secretDALFactory } from "@app/services/secret/secret-dal"; +import { secretQueueFactory } from "@app/services/secret/secret-queue"; +import { secretServiceFactory } from "@app/services/secret/secret-service"; +import { secretVersionDALFactory } from "@app/services/secret/secret-version-dal"; +import { secretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal"; import { serviceTokenDALFactory } from "@app/services/service-token/service-token-dal"; import { serviceTokenServiceFactory } from "@app/services/service-token/service-token-service"; import { projectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal"; @@ -340,15 +340,16 @@ import { telemetryServiceFactory } from "@app/services/telemetry/telemetry-servi import { totpConfigDALFactory } from "@app/services/totp/totp-config-dal"; import { totpServiceFactory } from "@app/services/totp/totp-service"; import { upgradePathServiceFactory } from "@app/services/upgrade-path/upgrade-path-service"; -import { userDALFactory } from "@app/services/user/user-dal"; -import { userServiceFactory } from "@app/services/user/user-service"; import { userAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; import { userEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service"; +import { userDALFactory } from "@app/services/user/user-dal"; +import { userServiceFactory } from "@app/services/user/user-service"; import { webhookDALFactory } from "@app/services/webhook/webhook-dal"; import { webhookServiceFactory } from "@app/services/webhook/webhook-service"; import { workflowIntegrationDALFactory } from "@app/services/workflow-integration/workflow-integration-dal"; import { workflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service"; +import { pkiAcmeAccountDALFactory } from "@app/ee/services/pki-acme/pki-acme-account-dal"; import { injectAuditLogInfo } from "../plugins/audit-log"; import { injectAssumePrivilege } from "../plugins/auth/inject-assume-privilege"; import { injectIdentity } from "../plugins/auth/inject-identity"; @@ -1064,6 +1065,7 @@ export const registerRoutes = async ( const apiEnrollmentConfigDAL = apiEnrollmentConfigDALFactory(db); const estEnrollmentConfigDAL = estEnrollmentConfigDALFactory(db); const acmeEnrollmentConfigDAL = acmeEnrollmentConfigDALFactory(db); + const pkiAcmeAccountDAL = pkiAcmeAccountDALFactory(db); const certificateDAL = certificateDALFactory(db); const certificateBodyDAL = certificateBodyDALFactory(db); @@ -1159,13 +1161,15 @@ export const registerRoutes = async ( certificateTemplateV2DAL, apiEnrollmentConfigDAL, estEnrollmentConfigDAL, + acmeEnrollmentConfigDAL, permissionService, kmsService, projectDAL }); const pkiAcmeService = pkiAcmeServiceFactory({ - certificateProfileDAL + certificateProfileDAL, + pkiAcmeAccountDAL }); const pkiAlertService = pkiAlertServiceFactory({ From 5585621a065f78bf0185e780866b2090c4cd517c Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 28 Oct 2025 16:39:24 -0700 Subject: [PATCH 098/294] Add acme type --- backend/src/@types/knex.d.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 2be2af84c..7dcd63c0e 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -717,6 +717,11 @@ declare module "knex/types/tables" { TPkiAcmeEnrollmentConfigsInsert, TPkiAcmeEnrollmentConfigsUpdate >; + [TableName.PkiAcmeAccount]: KnexOriginal.CompositeTableType< + TPkiAcmeAccounts, + TPkiAcmeAccountsInsert, + TPkiAcmeAccountsUpdate + >; [TableName.CertificateTemplateEstConfig]: KnexOriginal.CompositeTableType< TCertificateTemplateEstConfigs, TCertificateTemplateEstConfigsInsert, From f0d2c30384b18d670756347fa58dfad3ed18ddaa Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 28 Oct 2025 16:44:10 -0700 Subject: [PATCH 099/294] Use profile obj instead --- backend/src/ee/services/pki-acme/pki-acme-service.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 0040f143b..b73195282 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -109,8 +109,6 @@ export const pkiAcmeServiceFactory = ({ { onlyReturnExisting, contact }: TCreateAcmeAccountPayload ): Promise> => { const profile = await validateAcmeProfile(profileId); - // TODO: the jwk as json obj may not be the best idea for indexing. - // Maybe we should find a way to serialize the jwk deterministically. let account: TPkiAcmeAccount | null = await pkiAcmeAccountDAL.findByPublicKey(jwk); if (onlyReturnExisting && !account) { throw new AcmeAccountDoesNotExistError({ message: "ACME account not found" }); @@ -122,16 +120,16 @@ export const pkiAcmeServiceFactory = ({ body: { status: "valid", contact: account.emails, - orders: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${account.id}/orders`) + orders: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/accounts/${account.id}/orders`) }, headers: { - Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${account.id}`) + Location: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/accounts/${account.id}`) } }; } account = await pkiAcmeAccountDAL.create({ - profileId, + profileId: profile.id, publicKey: jwk, emails: contact ?? [] }); @@ -141,10 +139,10 @@ export const pkiAcmeServiceFactory = ({ body: { status: "valid", contact: account.emails, - orders: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${account.id}/orders`) + orders: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/accounts/${account.id}/orders`) }, headers: { - Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${account.id}`) + Location: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/accounts/${account.id}`) } }; }; From ebd2c8ae45dfd439863f6acc555bf932b7c3c9ad Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 28 Oct 2025 17:10:30 -0700 Subject: [PATCH 100/294] Refine code --- .../ee/services/pki-acme/pki-acme-service.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index b73195282..7a08a95f7 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -31,7 +31,7 @@ import { TRawJwsPayload, TRespondToAcmeChallengeResponse } from "./pki-acme-types"; -import { TPkiAcmeAccount } from "@app/db/schemas/pki-acme-accounts"; +import { TPkiAcmeAccounts } from "@app/db/schemas/pki-acme-accounts"; import { TPkiAcmeAccountDALFactory } from "./pki-acme-account-dal"; type TPkiAcmeServiceFactoryDep = { @@ -109,26 +109,26 @@ export const pkiAcmeServiceFactory = ({ { onlyReturnExisting, contact }: TCreateAcmeAccountPayload ): Promise> => { const profile = await validateAcmeProfile(profileId); - let account: TPkiAcmeAccount | null = await pkiAcmeAccountDAL.findByPublicKey(jwk); - if (onlyReturnExisting && !account) { + const existingAccount: TPkiAcmeAccounts | null = await pkiAcmeAccountDAL.findByPublicKey(jwk); + if (onlyReturnExisting && !existingAccount) { throw new AcmeAccountDoesNotExistError({ message: "ACME account not found" }); } - if (account) { + if (existingAccount) { // With the same public key, we found an existing account, just return it return { status: 200, body: { status: "valid", - contact: account.emails, - orders: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/accounts/${account.id}/orders`) + contact: existingAccount.emails, + orders: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/accounts/${existingAccount.id}/orders`) }, headers: { - Location: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/accounts/${account.id}`) + Location: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/accounts/${existingAccount.id}`) } }; } - account = await pkiAcmeAccountDAL.create({ + const newAccount = await pkiAcmeAccountDAL.create({ profileId: profile.id, publicKey: jwk, emails: contact ?? [] @@ -138,11 +138,11 @@ export const pkiAcmeServiceFactory = ({ status: 201, body: { status: "valid", - contact: account.emails, - orders: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/accounts/${account.id}/orders`) + contact: newAccount.emails, + orders: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/accounts/${newAccount.id}/orders`) }, headers: { - Location: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/accounts/${account.id}`) + Location: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/accounts/${newAccount.id}`) } }; }; From 4f0bb7fb8ac4a14baf15f24ff62262302f561f0c Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 28 Oct 2025 17:37:24 -0700 Subject: [PATCH 101/294] Add alg column and index --- .../migrations/20251027234547_add-pki-acme.ts | 5 + .../services/pki-acme/pki-acme-account-dal.ts | 6 +- .../ee/services/pki-acme/pki-acme-auth-dal.ts | 138 ++++++++++++++++++ .../services/pki-acme/pki-acme-order-dal.ts | 116 +++++++++++++++ .../ee/services/pki-acme/pki-acme-service.ts | 59 ++++++-- .../ee/services/pki-acme/pki-acme-types.ts | 6 +- 6 files changed, 313 insertions(+), 17 deletions(-) create mode 100644 backend/src/ee/services/pki-acme/pki-acme-auth-dal.ts create mode 100644 backend/src/ee/services/pki-acme/pki-acme-order-dal.ts diff --git a/backend/src/db/migrations/20251027234547_add-pki-acme.ts b/backend/src/db/migrations/20251027234547_add-pki-acme.ts index c0cf0db4f..781fe040e 100644 --- a/backend/src/db/migrations/20251027234547_add-pki-acme.ts +++ b/backend/src/db/migrations/20251027234547_add-pki-acme.ts @@ -10,6 +10,8 @@ import { dropConstraintIfExists } from "@app/db/migrations/utils/dropConstraintI const OLD_ENROLLMENT_TYPE_CHECK_CONSTRAINT = "pki_certificate_profiles_enrollmentType_check"; const NEW_ENROLLMENT_TYPE_CHECK_CONSTRAINT = "pki_certificate_profiles_enrollment_type_check"; +const PUBLIC_KEY_ALG_INDEX = "pki_acme_accounts_publicKey_alg_index"; + export async function up(knex: Knex): Promise { // Create PkiAcmeEnrollmentConfig table if (!(await knex.schema.hasTable(TableName.PkiAcmeEnrollmentConfig))) { @@ -56,6 +58,9 @@ export async function up(knex: Knex): Promise { // Public key (JWK format) t.jsonb("publicKey").notNullable(); + // The JWS algorithm used to sign the public key when creating the account, e.g. "RS256", "ES256", "PS256", etc. + t.string("alg").notNullable(); + t.index(["publicKey", "alg"], PUBLIC_KEY_ALG_INDEX); t.timestamps(true, true, true); }); diff --git a/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts index 93f7b7055..0050a1139 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts @@ -61,13 +61,13 @@ export const pkiAcmeAccountDALFactory = (db: TDbClient) => { } }; - const findByPublicKey = async (publicKey: unknown, tx?: Knex) => { + const findByPublicKey = async (publicKey: unknown, alg: string, tx?: Knex) => { try { - const account = await (tx || db)(TableName.PkiAcmeAccount).where({ publicKey }).first(); + const account = await (tx || db)(TableName.PkiAcmeAccount).where({ publicKey, alg }).first(); return account || null; } catch (error) { - throw new DatabaseError({ error, name: "Find PKI ACME account by public key" }); + throw new DatabaseError({ error, name: "Find PKI ACME account by public key and alg" }); } }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-auth-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-auth-dal.ts new file mode 100644 index 000000000..c06954f4a --- /dev/null +++ b/backend/src/ee/services/pki-acme/pki-acme-auth-dal.ts @@ -0,0 +1,138 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { TPkiAcmeAuthsInsert, TPkiAcmeAuthsUpdate } from "@app/db/schemas/pki-acme-auths"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify } from "@app/lib/knex"; + +export type TPkiAcmeAuthDALFactory = ReturnType; + +export const pkiAcmeAuthDALFactory = (db: TDbClient) => { + const pkiAcmeAuthOrm = ormify(db, TableName.PkiAcmeAuth); + + const create = async (data: TPkiAcmeAuthsInsert, tx?: Knex) => { + try { + const result = await (tx || db)(TableName.PkiAcmeAuth).insert(data).returning("*"); + const [auth] = result; + + if (!auth) { + throw new Error("Failed to create PKI ACME auth"); + } + + return auth; + } catch (error) { + throw new DatabaseError({ error, name: "Create PKI ACME auth" }); + } + }; + + const updateById = async (id: string, data: TPkiAcmeAuthsUpdate, tx?: Knex) => { + try { + const result = await (tx || db)(TableName.PkiAcmeAuth).where({ id }).update(data).returning("*"); + const [auth] = result; + + if (!auth) { + return null; + } + + return auth; + } catch (error) { + throw new DatabaseError({ error, name: "Update PKI ACME auth" }); + } + }; + + const findById = async (id: string, tx?: Knex) => { + try { + const auth = await (tx || db)(TableName.PkiAcmeAuth).where({ id }).first(); + + return auth || null; + } catch (error) { + throw new DatabaseError({ error, name: "Find PKI ACME auth by id" }); + } + }; + + const findByAccountId = async (accountId: string, tx?: Knex) => { + try { + const auths = await (tx || db)(TableName.PkiAcmeAuth).where({ accountId }); + + return auths; + } catch (error) { + throw new DatabaseError({ error, name: "Find PKI ACME auths by account id" }); + } + }; + + const findByStatus = async (status: string, tx?: Knex) => { + try { + const auths = await (tx || db)(TableName.PkiAcmeAuth).where({ status }); + + return auths; + } catch (error) { + throw new DatabaseError({ error, name: "Find PKI ACME auths by status" }); + } + }; + + const findByAccountIdAndStatus = async (accountId: string, status: string, tx?: Knex) => { + try { + const auths = await (tx || db)(TableName.PkiAcmeAuth).where({ accountId, status }); + + return auths; + } catch (error) { + throw new DatabaseError({ error, name: "Find PKI ACME auths by account id and status" }); + } + }; + + const findByIdentifier = async (identifierType: string, identifierValue: string, tx?: Knex) => { + try { + const auths = await (tx || db)(TableName.PkiAcmeAuth).where({ identifierType, identifierValue }); + + return auths; + } catch (error) { + throw new DatabaseError({ error, name: "Find PKI ACME auths by identifier" }); + } + }; + + const findByCertificateId = async (certificateId: string, tx?: Knex) => { + try { + const auths = await (tx || db)(TableName.PkiAcmeAuth).where({ certificateId }); + + return auths; + } catch (error) { + throw new DatabaseError({ error, name: "Find PKI ACME auths by certificate id" }); + } + }; + + const deleteById = async (id: string, tx?: Knex) => { + try { + const result = await (tx || db)(TableName.PkiAcmeAuth).where({ id }).delete().returning("*"); + const [auth] = result; + + return auth || null; + } catch (error) { + throw new DatabaseError({ error, name: "Delete PKI ACME auth by id" }); + } + }; + + const deleteByAccountId = async (accountId: string, tx?: Knex) => { + try { + const result = await (tx || db)(TableName.PkiAcmeAuth).where({ accountId }).delete().returning("*"); + + return result; + } catch (error) { + throw new DatabaseError({ error, name: "Delete PKI ACME auths by account id" }); + } + }; + + return { + ...pkiAcmeAuthOrm, + create, + updateById, + findById, + findByAccountId, + findByStatus, + findByAccountIdAndStatus, + findByIdentifier, + findByCertificateId, + deleteById, + deleteByAccountId + }; +}; diff --git a/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts new file mode 100644 index 000000000..fabaaf2a1 --- /dev/null +++ b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts @@ -0,0 +1,116 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { TPkiAcmeOrdersInsert, TPkiAcmeOrdersUpdate } from "@app/db/schemas/pki-acme-orders"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify } from "@app/lib/knex"; + +export type TPkiAcmeOrderDALFactory = ReturnType; + +export const pkiAcmeOrderDALFactory = (db: TDbClient) => { + const pkiAcmeOrderOrm = ormify(db, TableName.PkiAcmeOrder); + + const create = async (data: TPkiAcmeOrdersInsert, tx?: Knex) => { + try { + const result = await (tx || db)(TableName.PkiAcmeOrder).insert(data).returning("*"); + const [order] = result; + + if (!order) { + throw new Error("Failed to create PKI ACME order"); + } + + return order; + } catch (error) { + throw new DatabaseError({ error, name: "Create PKI ACME order" }); + } + }; + + const updateById = async (id: string, data: TPkiAcmeOrdersUpdate, tx?: Knex) => { + try { + const result = await (tx || db)(TableName.PkiAcmeOrder).where({ id }).update(data).returning("*"); + const [order] = result; + + if (!order) { + return null; + } + + return order; + } catch (error) { + throw new DatabaseError({ error, name: "Update PKI ACME order" }); + } + }; + + const findById = async (id: string, tx?: Knex) => { + try { + const order = await (tx || db)(TableName.PkiAcmeOrder).where({ id }).first(); + + return order || null; + } catch (error) { + throw new DatabaseError({ error, name: "Find PKI ACME order by id" }); + } + }; + + const findByAccountId = async (accountId: string, tx?: Knex) => { + try { + const orders = await (tx || db)(TableName.PkiAcmeOrder).where({ accountId }); + + return orders; + } catch (error) { + throw new DatabaseError({ error, name: "Find PKI ACME orders by account id" }); + } + }; + + const findByStatus = async (status: string, tx?: Knex) => { + try { + const orders = await (tx || db)(TableName.PkiAcmeOrder).where({ status }); + + return orders; + } catch (error) { + throw new DatabaseError({ error, name: "Find PKI ACME orders by status" }); + } + }; + + const findByAccountIdAndStatus = async (accountId: string, status: string, tx?: Knex) => { + try { + const orders = await (tx || db)(TableName.PkiAcmeOrder).where({ accountId, status }); + + return orders; + } catch (error) { + throw new DatabaseError({ error, name: "Find PKI ACME orders by account id and status" }); + } + }; + + const deleteById = async (id: string, tx?: Knex) => { + try { + const result = await (tx || db)(TableName.PkiAcmeOrder).where({ id }).delete().returning("*"); + const [order] = result; + + return order || null; + } catch (error) { + throw new DatabaseError({ error, name: "Delete PKI ACME order by id" }); + } + }; + + const deleteByAccountId = async (accountId: string, tx?: Knex) => { + try { + const result = await (tx || db)(TableName.PkiAcmeOrder).where({ accountId }).delete().returning("*"); + + return result; + } catch (error) { + throw new DatabaseError({ error, name: "Delete PKI ACME orders by account id" }); + } + }; + + return { + ...pkiAcmeOrderOrm, + create, + updateById, + findById, + findByAccountId, + findByStatus, + findByAccountIdAndStatus, + deleteById, + deleteByAccountId + }; +}; diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 7a08a95f7..cf239b16a 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -5,11 +5,14 @@ import { TCertificateProfileDALFactory } from "@app/services/certificate-profile import { AcmeAccountDoesNotExistError, AcmeBadPublicKeyError, AcmeMalformedError } from "./pki-acme-errors"; +import { TPkiAcmeAccounts } from "@app/db/schemas/pki-acme-accounts"; import { EnrollmentType, TCertificateProfileWithConfigs } from "@app/services/certificate-profile/certificate-profile-types"; import { flattenedVerify, importJWK, JWK, JWSHeaderParameters } from "jose"; +import { TPkiAcmeAccountDALFactory } from "./pki-acme-account-dal"; +import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal"; import { ProtectedHeaderSchema } from "./pki-acme-schemas"; import { TAcmeResponse, @@ -24,24 +27,22 @@ import { TGetAcmeAuthorizationResponse, TGetAcmeDirectoryResponse, TGetAcmeOrderResponse, - TJwsPayload, TJwsPayloadWithJwk, TListAcmeOrdersResponse, TPkiAcmeServiceFactory, TRawJwsPayload, TRespondToAcmeChallengeResponse } from "./pki-acme-types"; -import { TPkiAcmeAccounts } from "@app/db/schemas/pki-acme-accounts"; -import { TPkiAcmeAccountDALFactory } from "./pki-acme-account-dal"; type TPkiAcmeServiceFactoryDep = { certificateProfileDAL: Pick; - pkiAcmeAccountDAL: Pick; + acmeAccountDAL: Pick; + acmeOrderDAL: Pick; }; export const pkiAcmeServiceFactory = ({ certificateProfileDAL, - pkiAcmeAccountDAL + acmeAccountDAL }: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => { const validateAcmeProfile = async (profileId: string): Promise => { const profile = await certificateProfileDAL.findById(profileId); @@ -81,6 +82,32 @@ export const pkiAcmeServiceFactory = ({ throw new AcmeMalformedError({ detail: "Invalid protected header" }); } + const decoder = new TextDecoder(); + const payload = JSON.parse(decoder.decode(rawPayload)) as TCreateAcmeAccountPayload; + // TODO: also consume the nonce here + return { payload, protectedHeader, jwk: protectedHeader.jwk as JsonWebKey, alg: protectedHeader.alg }; + }; + + const validateJwsPayload = async (rawJwsPayload: TRawJwsPayload): Promise => { + const { payload: rawPayload, protectedHeader: rawProtectedHeader } = await flattenedVerify( + rawJwsPayload, + async (protectedHeader: JWSHeaderParameters | undefined) => { + if (protectedHeader === undefined) { + throw new AcmeMalformedError({ detail: "Protected header is required" }); + } + if (protectedHeader.kid === undefined) { + throw new AcmeBadPublicKeyError({ detail: "Kid is required in the protected header" }); + } + + const imported = await importJWK(protectedHeader.jwk as JWK, protectedHeader.alg); + return imported; + } + ); + const { success, data: protectedHeader } = ProtectedHeaderSchema.safeParse(rawProtectedHeader); + if (!success) { + throw new AcmeMalformedError({ detail: "Invalid protected header" }); + } + const decoder = new TextDecoder(); const payload = JSON.parse(decoder.decode(rawPayload)) as TCreateAcmeAccountPayload; // TODO: also consume the nonce here @@ -109,7 +136,7 @@ export const pkiAcmeServiceFactory = ({ { onlyReturnExisting, contact }: TCreateAcmeAccountPayload ): Promise> => { const profile = await validateAcmeProfile(profileId); - const existingAccount: TPkiAcmeAccounts | null = await pkiAcmeAccountDAL.findByPublicKey(jwk); + const existingAccount: TPkiAcmeAccounts | null = await acmeAccountDAL.findByPublicKey(jwk, alg); if (onlyReturnExisting && !existingAccount) { throw new AcmeAccountDoesNotExistError({ message: "ACME account not found" }); } @@ -128,7 +155,7 @@ export const pkiAcmeServiceFactory = ({ }; } - const newAccount = await pkiAcmeAccountDAL.create({ + const newAccount = await acmeAccountDAL.create({ profileId: profile.id, publicKey: jwk, emails: contact ?? [] @@ -150,17 +177,23 @@ export const pkiAcmeServiceFactory = ({ const createAcmeOrder = async ( profileId: string, payload: TCreateAcmeOrderPayload - ): Promise => { + ): Promise> => { const profile = await validateAcmeProfile(profileId); // FIXME: Implement ACME new order creation const orderId = "FIXME-order-id"; return { - status: "pending", - expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), - identifiers: [], - authorizations: [], - finalize: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/finalize`) + status: 201, + body: { + status: "pending", + expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + identifiers: [], + authorizations: [], + finalize: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/orders/${orderId}/finalize`) + }, + headers: { + Location: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/orders/${orderId}`) + } }; }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index fe1105ff8..2006f38a2 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -41,6 +41,7 @@ export type TFinalizeAcmeOrderPayload = z.infer = { status: number; @@ -57,7 +58,10 @@ export type TPkiAcmeServiceFactory = { jwk: JsonWebKey, body: TCreateAcmeAccountPayload ) => Promise>; - createAcmeOrder: (profileId: string, body: TCreateAcmeOrderPayload) => Promise; + createAcmeOrder: ( + profileId: string, + body: TCreateAcmeOrderPayload + ) => Promise>; deactivateAcmeAccount: ( profileId: string, accountId: string, From 540521588dc65e5ce290110f848fee70d61d8fd4 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 28 Oct 2025 19:02:17 -0700 Subject: [PATCH 102/294] Refactor validate jws method --- .../services/pki-acme/pki-acme-account-dal.ts | 4 +- .../ee/services/pki-acme/pki-acme-service.ts | 89 ++++++++++--------- .../ee/services/pki-acme/pki-acme-types.ts | 1 + backend/src/server/routes/index.ts | 7 +- 4 files changed, 55 insertions(+), 46 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts index 0050a1139..15c07b0e1 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts @@ -61,9 +61,9 @@ export const pkiAcmeAccountDALFactory = (db: TDbClient) => { } }; - const findByPublicKey = async (publicKey: unknown, alg: string, tx?: Knex) => { + const findByPublicKey = async (profileId: string, alg: string, publicKey: unknown, tx?: Knex) => { try { - const account = await (tx || db)(TableName.PkiAcmeAccount).where({ publicKey, alg }).first(); + const account = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId, alg, publicKey }).first(); return account || null; } catch (error) { diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index cf239b16a..57aa504b3 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -3,14 +3,19 @@ import { NotFoundError } from "@app/lib/errors"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; -import { AcmeAccountDoesNotExistError, AcmeBadPublicKeyError, AcmeMalformedError } from "./pki-acme-errors"; +import { + AcmeAccountDoesNotExistError, + AcmeBadPublicKeyError, + AcmeMalformedError, + AcmeServerInternalError +} from "./pki-acme-errors"; import { TPkiAcmeAccounts } from "@app/db/schemas/pki-acme-accounts"; import { EnrollmentType, TCertificateProfileWithConfigs } from "@app/services/certificate-profile/certificate-profile-types"; -import { flattenedVerify, importJWK, JWK, JWSHeaderParameters } from "jose"; +import { flattenedVerify, FlattenedVerifyResult, importJWK, JWK, JWSHeaderParameters } from "jose"; import { TPkiAcmeAccountDALFactory } from "./pki-acme-account-dal"; import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal"; import { ProtectedHeaderSchema } from "./pki-acme-schemas"; @@ -27,12 +32,16 @@ import { TGetAcmeAuthorizationResponse, TGetAcmeDirectoryResponse, TGetAcmeOrderResponse, + TJwsPayload, TJwsPayloadWithJwk, TListAcmeOrdersResponse, TPkiAcmeServiceFactory, TRawJwsPayload, TRespondToAcmeChallengeResponse } from "./pki-acme-types"; +import { JWSInvalid } from "jose/dist/types/util/errors"; +import { logger } from "@app/lib/logger"; +import { z, ZodError } from "zod"; type TPkiAcmeServiceFactoryDep = { certificateProfileDAL: Pick; @@ -61,57 +70,52 @@ export const pkiAcmeServiceFactory = ({ return `${baseUrl}${path}`; }; - const validateCreateAcmeAccountJwsPayload = async (rawJwsPayload: TRawJwsPayload): Promise => { - const { payload: rawPayload, protectedHeader: rawProtectedHeader } = await flattenedVerify( - rawJwsPayload, - async (protectedHeader: JWSHeaderParameters | undefined) => { + const validateJwsPayload = async ( + rawJwsPayload: TRawJwsPayload, + getJWK: (protectedHeader: JWSHeaderParameters) => Promise, + schema: z.ZodSchema + ): Promise => { + let result: FlattenedVerifyResult; + try { + result = await flattenedVerify(rawJwsPayload, async (protectedHeader: JWSHeaderParameters | undefined) => { if (protectedHeader === undefined) { throw new AcmeMalformedError({ detail: "Protected header is required" }); } - if (protectedHeader.jwk === undefined) { - throw new AcmeBadPublicKeyError({ detail: "JWK is required in the protected header" }); - } - // For the create account request, the JWK is provided in the protected header. - // Let use it to verify the signature. - const imported = await importJWK(protectedHeader.jwk as JWK, protectedHeader.alg); - return imported; + ProtectedHeaderSchema.parse(protectedHeader); + const jwk = await getJWK(protectedHeader); + return await importJWK(jwk, protectedHeader.alg); + }); + } catch (error) { + if (error instanceof ZodError) { + throw new AcmeMalformedError({ detail: `Invalid JWS payload: ${error.message}` }); } - ); + if (error instanceof JWSInvalid) { + throw new AcmeBadPublicKeyError({ detail: "Invalid JWS payload" }); + } + logger.error(error, "Unexpected error while verifying JWS payload"); + throw new AcmeServerInternalError({ detail: "Failed to verify JWS payload" }); + } + const { payload: rawPayload, protectedHeader: rawProtectedHeader } = result!; const { success, data: protectedHeader } = ProtectedHeaderSchema.safeParse(rawProtectedHeader); if (!success) { throw new AcmeMalformedError({ detail: "Invalid protected header" }); } const decoder = new TextDecoder(); - const payload = JSON.parse(decoder.decode(rawPayload)) as TCreateAcmeAccountPayload; - // TODO: also consume the nonce here - return { payload, protectedHeader, jwk: protectedHeader.jwk as JsonWebKey, alg: protectedHeader.alg }; - }; - - const validateJwsPayload = async (rawJwsPayload: TRawJwsPayload): Promise => { - const { payload: rawPayload, protectedHeader: rawProtectedHeader } = await flattenedVerify( - rawJwsPayload, - async (protectedHeader: JWSHeaderParameters | undefined) => { - if (protectedHeader === undefined) { - throw new AcmeMalformedError({ detail: "Protected header is required" }); - } - if (protectedHeader.kid === undefined) { - throw new AcmeBadPublicKeyError({ detail: "Kid is required in the protected header" }); - } - - const imported = await importJWK(protectedHeader.jwk as JWK, protectedHeader.alg); - return imported; + const jsonPayload = JSON.parse(decoder.decode(rawPayload)); + try { + const payload = schema.parse(jsonPayload); + return { + protectedHeader, + payload + }; + } catch (error) { + if (error instanceof ZodError) { + throw new AcmeMalformedError({ detail: `Invalid JWS payload: ${error.message}` }); } - ); - const { success, data: protectedHeader } = ProtectedHeaderSchema.safeParse(rawProtectedHeader); - if (!success) { - throw new AcmeMalformedError({ detail: "Invalid protected header" }); + logger.error(error, "Unexpected error while parsing JWS payload"); + throw new AcmeServerInternalError({ detail: "Failed to verify JWS payload" }); } - - const decoder = new TextDecoder(); - const payload = JSON.parse(decoder.decode(rawPayload)) as TCreateAcmeAccountPayload; - // TODO: also consume the nonce here - return { payload, protectedHeader, jwk: protectedHeader.jwk as JsonWebKey }; }; const getAcmeDirectory = async (profileId: string): Promise => { @@ -132,11 +136,12 @@ export const pkiAcmeServiceFactory = ({ const createAcmeAccount = async ( profileId: string, + alg: string, jwk: JWK, { onlyReturnExisting, contact }: TCreateAcmeAccountPayload ): Promise> => { const profile = await validateAcmeProfile(profileId); - const existingAccount: TPkiAcmeAccounts | null = await acmeAccountDAL.findByPublicKey(jwk, alg); + const existingAccount: TPkiAcmeAccounts | null = await acmeAccountDAL.findByPublicKey(profileId, alg, jwk); if (onlyReturnExisting && !existingAccount) { throw new AcmeAccountDoesNotExistError({ message: "ACME account not found" }); } diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index 2006f38a2..4bf7411c4 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -55,6 +55,7 @@ export type TPkiAcmeServiceFactory = { getAcmeNewNonce: (profileId: string) => Promise; createAcmeAccount: ( profileId: string, + alg: string, jwk: JsonWebKey, body: TCreateAcmeAccountPayload ) => Promise>; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index b1e4b9b7c..49ac7750d 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -361,6 +361,7 @@ import { initializeOauthConfigSync } from "./v1/sso-router"; import { registerV2Routes } from "./v2"; import { registerV3Routes } from "./v3"; import { registerV4Routes } from "./v4"; +import { pkiAcmeOrderDALFactory } from "@app/ee/services/pki-acme/pki-acme-order-dal"; const histogram = monitorEventLoopDelay({ resolution: 20 }); histogram.enable(); @@ -1065,7 +1066,8 @@ export const registerRoutes = async ( const apiEnrollmentConfigDAL = apiEnrollmentConfigDALFactory(db); const estEnrollmentConfigDAL = estEnrollmentConfigDALFactory(db); const acmeEnrollmentConfigDAL = acmeEnrollmentConfigDALFactory(db); - const pkiAcmeAccountDAL = pkiAcmeAccountDALFactory(db); + const acmeAccountDAL = pkiAcmeAccountDALFactory(db); + const acmeOrderDAL = pkiAcmeOrderDALFactory(db); const certificateDAL = certificateDALFactory(db); const certificateBodyDAL = certificateBodyDALFactory(db); @@ -1169,7 +1171,8 @@ export const registerRoutes = async ( const pkiAcmeService = pkiAcmeServiceFactory({ certificateProfileDAL, - pkiAcmeAccountDAL + acmeAccountDAL, + acmeOrderDAL }); const pkiAlertService = pkiAlertServiceFactory({ From 223835d1411c4e90d9fe7a0d0d73b576f3b9cdcf Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 28 Oct 2025 19:08:51 -0700 Subject: [PATCH 103/294] New approach to validate jws --- backend/src/@types/knex.d.ts | 11 +++++++++++ .../migrations/20251027234547_add-pki-acme.ts | 1 + backend/src/ee/routes/v1/pki-acme-router.ts | 19 ++++++++++++++++--- .../ee/services/pki-acme/pki-acme-schemas.ts | 5 ----- .../ee/services/pki-acme/pki-acme-service.ts | 3 ++- .../ee/services/pki-acme/pki-acme-types.ts | 16 ++++++++++------ 6 files changed, 40 insertions(+), 15 deletions(-) diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 7dcd63c0e..8a19670f0 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -722,6 +722,17 @@ declare module "knex/types/tables" { TPkiAcmeAccountsInsert, TPkiAcmeAccountsUpdate >; + [TableName.PkiAcmeOrder]: KnexOriginal.CompositeTableType< + TPkiAcmeOrders, + TPkiAcmeOrdersInsert, + TPkiAcmeOrdersUpdate + >; + [TableName.PkiAcmeAuth]: KnexOriginal.CompositeTableType; + [TableName.PkiAcmeChallenge]: KnexOriginal.CompositeTableType< + TPkiAcmeChallenges, + TPkiAcmeChallengesInsert, + TPkiAcmeChallengesUpdate + >; [TableName.CertificateTemplateEstConfig]: KnexOriginal.CompositeTableType< TCertificateTemplateEstConfigs, TCertificateTemplateEstConfigsInsert, diff --git a/backend/src/db/migrations/20251027234547_add-pki-acme.ts b/backend/src/db/migrations/20251027234547_add-pki-acme.ts index 781fe040e..285676c8f 100644 --- a/backend/src/db/migrations/20251027234547_add-pki-acme.ts +++ b/backend/src/db/migrations/20251027234547_add-pki-acme.ts @@ -56,6 +56,7 @@ export async function up(knex: Knex): Promise { // Multi-value emails array t.specificType("emails", "text[]").notNullable(); + // TODO: make public key a string instead of jsonb to make indexing much easier // Public key (JWK format) t.jsonb("publicKey").notNullable(); // The JWS algorithm used to sign the public key when creating the account, e.g. "RS256", "ES256", "PS256", etc. diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index e0762b7f3..8361fd330 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { + CreateAcmeAccountBodySchema, CreateAcmeAccountResponseSchema, CreateAcmeOrderResponseSchema, CreateAcmeOrderSchema, @@ -27,6 +28,7 @@ import { TCreateAcmeAccountPayload, TRawJwsPayload } from "@app/ee/services/pki- import { ApiDocsTags } from "@app/lib/api-docs"; import { getConfig } from "@app/lib/config/env"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { AcmeBadPublicKeyError } from "@app/ee/services/pki-acme/pki-acme-errors"; export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { const appCfg = getConfig(); @@ -111,13 +113,24 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { } }, handler: async (req, res) => { - const { payload, jwk } = await server.services.pkiAcme.validateCreateAcmeAccountJwsPayload( - req.body as TRawJwsPayload + const { payload, protectedHeader, jwk } = await server.services.pkiAcme.validateJwsPayload( + req.body as TRawJwsPayload, + async (protectedHeader) => { + if (!protectedHeader.jwk) { + throw new AcmeBadPublicKeyError({ detail: "JWK is required in the protected header" }); + } + return protectedHeader.jwk as unknown as JsonWebKey; + }, + CreateAcmeAccountBodySchema ); + if (!jwk) { + throw new AcmeBadPublicKeyError({ detail: "JWK is required in the protected header" }); + } const { status, body, headers } = await server.services.pkiAcme.createAcmeAccount( req.params.profileId, + protectedHeader.alg, jwk, - payload as TCreateAcmeAccountPayload + payload ); // TODO: DRY res.code(status); diff --git a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts index 599203dae..7e50b2108 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts @@ -20,11 +20,6 @@ export const RawJwsPayloadSchema = z.object({ signature: z.string() }); -export const JwsPayloadSchema = z.object({ - protectedHeader: ProtectedHeaderSchema, - payload: z.unknown() -}); - // Directory endpoint export const GetAcmeDirectorySchema = z.object({ params: z.object({ diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 57aa504b3..b75a76979 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -81,7 +81,8 @@ export const pkiAcmeServiceFactory = ({ if (protectedHeader === undefined) { throw new AcmeMalformedError({ detail: "Protected header is required" }); } - ProtectedHeaderSchema.parse(protectedHeader); + const parsedHeader = ProtectedHeaderSchema.parse(protectedHeader); + // TODO: consume the nonce here const jwk = await getJWK(protectedHeader); return await importJWK(jwk, protectedHeader.alg); }); diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index 4bf7411c4..35571b0cb 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +import { JWSHeaderParameters } from "jose"; import { CreateAcmeAccountBodySchema, CreateAcmeAccountResponseSchema, @@ -12,7 +13,6 @@ import { GetAcmeAuthorizationResponseSchema, GetAcmeDirectoryResponseSchema, GetAcmeOrderResponseSchema, - JwsPayloadSchema, ListAcmeOrdersResponseSchema, ProtectedHeaderSchema, RawJwsPayloadSchema, @@ -32,16 +32,16 @@ export type TRespondToAcmeChallengeResponse = z.infer; -export type TJwsPayload = z.infer; export type TProtectedHeader = z.infer; export type TCreateAcmeAccountPayload = z.infer; export type TCreateAcmeOrderPayload = z.infer; export type TDeactivateAcmeAccountPayload = z.infer; export type TFinalizeAcmeOrderPayload = z.infer; -export type TJwsPayloadWithJwk = TJwsPayload & { - jwk: JsonWebKey; - alg: string; +export type TJwsPayload = { + protectedHeader: TProtectedHeader; + jwk?: JsonWebKey; + payload: T; }; export type TAcmeResponse = { status: number; @@ -50,7 +50,11 @@ export type TAcmeResponse = { }; export type TPkiAcmeServiceFactory = { - validateCreateAcmeAccountJwsPayload(body: TRawJwsPayload): Promise; + validateJwsPayload: ( + rawJwsPayload: TRawJwsPayload, + getJWK: (protectedHeader: JWSHeaderParameters) => Promise, + schema: z.ZodSchema + ) => Promise>; getAcmeDirectory: (profileId: string) => Promise; getAcmeNewNonce: (profileId: string) => Promise; createAcmeAccount: ( From 6064b39c05783c40eb429eb8cfeb4ca4ee887085 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 28 Oct 2025 19:14:37 -0700 Subject: [PATCH 104/294] Refactor --- backend/src/ee/routes/v1/pki-acme-router.ts | 7 ++----- .../ee/services/pki-acme/pki-acme-service.ts | 20 +++++++------------ 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index 8361fd330..cda0fc478 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-floating-promises */ import { z } from "zod"; +import { AcmeBadPublicKeyError } from "@app/ee/services/pki-acme/pki-acme-errors"; import { CreateAcmeAccountBodySchema, CreateAcmeAccountResponseSchema, @@ -24,15 +25,11 @@ import { RespondToAcmeChallengeResponseSchema, RespondToAcmeChallengeSchema } from "@app/ee/services/pki-acme/pki-acme-schemas"; -import { TCreateAcmeAccountPayload, TRawJwsPayload } from "@app/ee/services/pki-acme/pki-acme-types"; +import { TRawJwsPayload } from "@app/ee/services/pki-acme/pki-acme-types"; import { ApiDocsTags } from "@app/lib/api-docs"; -import { getConfig } from "@app/lib/config/env"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; -import { AcmeBadPublicKeyError } from "@app/ee/services/pki-acme/pki-acme-errors"; export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { - const appCfg = getConfig(); - server.addContentTypeParser("application/jose+json", { parseAs: "string" }, (_, body, done) => { try { const strBody = body instanceof Buffer ? body.toString() : body; diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index b75a76979..316328784 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -33,7 +33,6 @@ import { TGetAcmeDirectoryResponse, TGetAcmeOrderResponse, TJwsPayload, - TJwsPayloadWithJwk, TListAcmeOrdersResponse, TPkiAcmeServiceFactory, TRawJwsPayload, @@ -74,15 +73,13 @@ export const pkiAcmeServiceFactory = ({ rawJwsPayload: TRawJwsPayload, getJWK: (protectedHeader: JWSHeaderParameters) => Promise, schema: z.ZodSchema - ): Promise => { + ): Promise> => { let result: FlattenedVerifyResult; try { result = await flattenedVerify(rawJwsPayload, async (protectedHeader: JWSHeaderParameters | undefined) => { if (protectedHeader === undefined) { throw new AcmeMalformedError({ detail: "Protected header is required" }); } - const parsedHeader = ProtectedHeaderSchema.parse(protectedHeader); - // TODO: consume the nonce here const jwk = await getJWK(protectedHeader); return await importJWK(jwk, protectedHeader.alg); }); @@ -96,15 +93,12 @@ export const pkiAcmeServiceFactory = ({ logger.error(error, "Unexpected error while verifying JWS payload"); throw new AcmeServerInternalError({ detail: "Failed to verify JWS payload" }); } - const { payload: rawPayload, protectedHeader: rawProtectedHeader } = result!; - const { success, data: protectedHeader } = ProtectedHeaderSchema.safeParse(rawProtectedHeader); - if (!success) { - throw new AcmeMalformedError({ detail: "Invalid protected header" }); - } - - const decoder = new TextDecoder(); - const jsonPayload = JSON.parse(decoder.decode(rawPayload)); + const { protectedHeader: rawProtectedHeader, payload: rawPayload } = result; try { + const protectedHeader = ProtectedHeaderSchema.parse(rawProtectedHeader); + // TODO: consume the nonce here + const decoder = new TextDecoder(); + const jsonPayload = JSON.parse(decoder.decode(rawPayload)); const payload = schema.parse(jsonPayload); return { protectedHeader, @@ -297,7 +291,7 @@ export const pkiAcmeServiceFactory = ({ }; return { - validateCreateAcmeAccountJwsPayload, + validateJwsPayload, getAcmeDirectory, getAcmeNewNonce, createAcmeAccount, From 457c5ea9c82d5e66e6e94cf74c262132830bf490 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 28 Oct 2025 19:16:25 -0700 Subject: [PATCH 105/294] Fix import --- backend/src/ee/services/pki-acme/pki-acme-service.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 316328784..1c191b206 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -11,11 +11,13 @@ import { } from "./pki-acme-errors"; import { TPkiAcmeAccounts } from "@app/db/schemas/pki-acme-accounts"; +import { logger } from "@app/lib/logger"; import { EnrollmentType, TCertificateProfileWithConfigs } from "@app/services/certificate-profile/certificate-profile-types"; -import { flattenedVerify, FlattenedVerifyResult, importJWK, JWK, JWSHeaderParameters } from "jose"; +import { errors, flattenedVerify, FlattenedVerifyResult, importJWK, JWK, JWSHeaderParameters } from "jose"; +import { z, ZodError } from "zod"; import { TPkiAcmeAccountDALFactory } from "./pki-acme-account-dal"; import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal"; import { ProtectedHeaderSchema } from "./pki-acme-schemas"; @@ -38,9 +40,6 @@ import { TRawJwsPayload, TRespondToAcmeChallengeResponse } from "./pki-acme-types"; -import { JWSInvalid } from "jose/dist/types/util/errors"; -import { logger } from "@app/lib/logger"; -import { z, ZodError } from "zod"; type TPkiAcmeServiceFactoryDep = { certificateProfileDAL: Pick; @@ -87,7 +86,7 @@ export const pkiAcmeServiceFactory = ({ if (error instanceof ZodError) { throw new AcmeMalformedError({ detail: `Invalid JWS payload: ${error.message}` }); } - if (error instanceof JWSInvalid) { + if (error instanceof errors.JWSInvalid) { throw new AcmeBadPublicKeyError({ detail: "Invalid JWS payload" }); } logger.error(error, "Unexpected error while verifying JWS payload"); From 9c748bf0ee3c07d0ed4475a54b8ced5cf9a9032b Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 28 Oct 2025 19:24:53 -0700 Subject: [PATCH 106/294] Fix new account again --- backend/src/db/schemas/pki-acme-accounts.ts | 1 + backend/src/ee/routes/v1/pki-acme-router.ts | 10 ++++------ backend/src/ee/services/pki-acme/pki-acme-service.ts | 1 + backend/src/ee/services/pki-acme/pki-acme-types.ts | 1 - 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/backend/src/db/schemas/pki-acme-accounts.ts b/backend/src/db/schemas/pki-acme-accounts.ts index b599cec07..275ff7e7e 100644 --- a/backend/src/db/schemas/pki-acme-accounts.ts +++ b/backend/src/db/schemas/pki-acme-accounts.ts @@ -12,6 +12,7 @@ export const PkiAcmeAccountsSchema = z.object({ profileId: z.string().uuid(), emails: z.string().array(), publicKey: z.unknown(), + alg: z.string(), createdAt: z.date(), updatedAt: z.date() }); diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index cda0fc478..b60b13dde 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -110,7 +110,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { } }, handler: async (req, res) => { - const { payload, protectedHeader, jwk } = await server.services.pkiAcme.validateJwsPayload( + const { payload, protectedHeader } = await server.services.pkiAcme.validateJwsPayload( req.body as TRawJwsPayload, async (protectedHeader) => { if (!protectedHeader.jwk) { @@ -120,13 +120,11 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { }, CreateAcmeAccountBodySchema ); - if (!jwk) { - throw new AcmeBadPublicKeyError({ detail: "JWK is required in the protected header" }); - } + const { alg, jwk } = protectedHeader; const { status, body, headers } = await server.services.pkiAcme.createAcmeAccount( req.params.profileId, - protectedHeader.alg, - jwk, + alg, + jwk!, payload ); // TODO: DRY diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 1c191b206..1ef8f4c6e 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -156,6 +156,7 @@ export const pkiAcmeServiceFactory = ({ const newAccount = await acmeAccountDAL.create({ profileId: profile.id, + alg, publicKey: jwk, emails: contact ?? [] }); diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index 35571b0cb..f5908a922 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -40,7 +40,6 @@ export type TFinalizeAcmeOrderPayload = z.infer = { protectedHeader: TProtectedHeader; - jwk?: JsonWebKey; payload: T; }; export type TAcmeResponse = { From ac82e8071d48e332b7683bff537d542cdef0843a Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 28 Oct 2025 19:48:27 -0700 Subject: [PATCH 107/294] Refactor --- .../ee/services/pki-acme/pki-acme-service.ts | 41 +++++++++++++++---- .../ee/services/pki-acme/pki-acme-types.ts | 18 +++++--- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 1ef8f4c6e..46f67ac17 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -16,11 +16,11 @@ import { EnrollmentType, TCertificateProfileWithConfigs } from "@app/services/certificate-profile/certificate-profile-types"; -import { errors, flattenedVerify, FlattenedVerifyResult, importJWK, JWK, JWSHeaderParameters } from "jose"; +import { errors, flattenedVerify, FlattenedVerifyResult, importJWK, JWSHeaderParameters } from "jose"; import { z, ZodError } from "zod"; import { TPkiAcmeAccountDALFactory } from "./pki-acme-account-dal"; import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal"; -import { ProtectedHeaderSchema } from "./pki-acme-schemas"; +import { CreateAcmeAccountBodySchema, ProtectedHeaderSchema } from "./pki-acme-schemas"; import { TAcmeResponse, TCreateAcmeAccountPayload, @@ -49,7 +49,8 @@ type TPkiAcmeServiceFactoryDep = { export const pkiAcmeServiceFactory = ({ certificateProfileDAL, - acmeAccountDAL + acmeAccountDAL, + acmeOrderDAL }: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => { const validateAcmeProfile = async (profileId: string): Promise => { const profile = await certificateProfileDAL.findById(profileId); @@ -112,6 +113,21 @@ export const pkiAcmeServiceFactory = ({ } }; + const validateNewAccountJwsPayload = async ( + rawJwsPayload: TRawJwsPayload + ): Promise> => { + return await validateJwsPayload( + rawJwsPayload, + async (protectedHeader) => { + if (!protectedHeader.jwk) { + throw new AcmeBadPublicKeyError({ detail: "JWK is required in the protected header" }); + } + return protectedHeader.jwk as unknown as JsonWebKey; + }, + CreateAcmeAccountBodySchema + ); + }; + const getAcmeDirectory = async (profileId: string): Promise => { await validateAcmeProfile(profileId); return { @@ -128,12 +144,17 @@ export const pkiAcmeServiceFactory = ({ return "FIXME-generate-nonce"; }; - const createAcmeAccount = async ( - profileId: string, - alg: string, - jwk: JWK, - { onlyReturnExisting, contact }: TCreateAcmeAccountPayload - ): Promise> => { + const createAcmeAccount = async ({ + profileId, + alg, + jwk, + payload: { onlyReturnExisting, contact } + }: { + profileId: string; + alg: string; + jwk: JsonWebKey; + payload: TCreateAcmeAccountPayload; + }): Promise> => { const profile = await validateAcmeProfile(profileId); const existingAccount: TPkiAcmeAccounts | null = await acmeAccountDAL.findByPublicKey(profileId, alg, jwk); if (onlyReturnExisting && !existingAccount) { @@ -176,6 +197,7 @@ export const pkiAcmeServiceFactory = ({ const createAcmeOrder = async ( profileId: string, + account: TPkiAcmeAccounts, payload: TCreateAcmeOrderPayload ): Promise> => { const profile = await validateAcmeProfile(profileId); @@ -292,6 +314,7 @@ export const pkiAcmeServiceFactory = ({ return { validateJwsPayload, + validateNewAccountJwsPayload, getAcmeDirectory, getAcmeNewNonce, createAcmeAccount, diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index f5908a922..3bb03f6a6 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -54,14 +54,20 @@ export type TPkiAcmeServiceFactory = { getJWK: (protectedHeader: JWSHeaderParameters) => Promise, schema: z.ZodSchema ) => Promise>; + validateNewAccountJwsPayload: (rawJwsPayload: TRawJwsPayload) => Promise>; getAcmeDirectory: (profileId: string) => Promise; getAcmeNewNonce: (profileId: string) => Promise; - createAcmeAccount: ( - profileId: string, - alg: string, - jwk: JsonWebKey, - body: TCreateAcmeAccountPayload - ) => Promise>; + createAcmeAccount: ({ + profileId, + alg, + jwk, + payload + }: { + profileId: string; + alg: string; + jwk: JsonWebKey; + payload: TCreateAcmeAccountPayload; + }) => Promise>; createAcmeOrder: ( profileId: string, body: TCreateAcmeOrderPayload From 2b8a6b801deea31fc67ff3a074c345e16337da63 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 28 Oct 2025 20:13:38 -0700 Subject: [PATCH 108/294] Work on order --- backend/bdd/features/steps/pki_acme.py | 2 +- backend/src/ee/routes/v1/pki-acme-router.ts | 39 ++++++----- .../services/pki-acme/pki-acme-account-dal.ts | 54 +-------------- .../ee/services/pki-acme/pki-acme-service.ts | 68 ++++++++++++++++--- .../ee/services/pki-acme/pki-acme-types.ts | 21 ++++-- 5 files changed, 100 insertions(+), 84 deletions(-) diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index eb8a04315..8546cb217 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -36,7 +36,7 @@ def step_impl(context: Context, profile_var: str): # TODO: Fixed value for now, just to make test much easier, # we should call infisical API to create such profile instead # in the future - profile_id = "0e96a01b-017e-4660-8b3d-ff26018fe0ce" + profile_id = "dd6e09c8-d5b8-4bfd-b436-4ab5c93d5d7e" context.vars[profile_var] = AcmeProfile(profile_id) diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index b60b13dde..13bad0a6a 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -1,12 +1,10 @@ /* eslint-disable @typescript-eslint/no-floating-promises */ import { z } from "zod"; -import { AcmeBadPublicKeyError } from "@app/ee/services/pki-acme/pki-acme-errors"; import { - CreateAcmeAccountBodySchema, CreateAcmeAccountResponseSchema, + CreateAcmeOrderBodySchema, CreateAcmeOrderResponseSchema, - CreateAcmeOrderSchema, DeactivateAcmeAccountResponseSchema, DeactivateAcmeAccountSchema, DownloadAcmeCertificateSchema, @@ -25,7 +23,6 @@ import { RespondToAcmeChallengeResponseSchema, RespondToAcmeChallengeSchema } from "@app/ee/services/pki-acme/pki-acme-schemas"; -import { TRawJwsPayload } from "@app/ee/services/pki-acme/pki-acme-types"; import { ApiDocsTags } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; @@ -104,29 +101,23 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME New Account - register a new account or find existing one", - ...RawJwsPayloadSchema.shape, + params: z.object({ + profileId: z.string().uuid() + }), + body: RawJwsPayloadSchema, response: { 201: CreateAcmeAccountResponseSchema } }, handler: async (req, res) => { - const { payload, protectedHeader } = await server.services.pkiAcme.validateJwsPayload( - req.body as TRawJwsPayload, - async (protectedHeader) => { - if (!protectedHeader.jwk) { - throw new AcmeBadPublicKeyError({ detail: "JWK is required in the protected header" }); - } - return protectedHeader.jwk as unknown as JsonWebKey; - }, - CreateAcmeAccountBodySchema - ); + const { payload, protectedHeader } = await server.services.pkiAcme.validateNewAccountJwsPayload(req.body); const { alg, jwk } = protectedHeader; - const { status, body, headers } = await server.services.pkiAcme.createAcmeAccount( - req.params.profileId, + const { status, body, headers } = await server.services.pkiAcme.createAcmeAccount({ + profileId: req.params.profileId, alg, - jwk!, + jwk: jwk!, payload - ); + }); // TODO: DRY res.code(status); for (const [key, value] of Object.entries(headers)) { @@ -153,7 +144,10 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME New Order - apply for a new certificate", - ...CreateAcmeOrderSchema.shape, + params: z.object({ + profileId: z.string().uuid() + }), + body: RawJwsPayloadSchema, response: { 201: CreateAcmeOrderResponseSchema } @@ -161,6 +155,11 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { + const { payload, protectedHeader, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload( + req.params.profileId, + req.body, + CreateAcmeOrderBodySchema + ); const order = await server.services.pkiAcme.createAcmeOrder(req.params.profileId, req.body); res.code(201); return order; diff --git a/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts index 15c07b0e1..713cb4350 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts @@ -26,24 +26,9 @@ export const pkiAcmeAccountDALFactory = (db: TDbClient) => { } }; - const updateById = async (id: string, data: TPkiAcmeAccountsUpdate, tx?: Knex) => { + const findById = async (profileId: string, id: string, tx?: Knex) => { try { - const result = await (tx || db)(TableName.PkiAcmeAccount).where({ id }).update(data).returning("*"); - const [account] = result; - - if (!account) { - return null; - } - - return account; - } catch (error) { - throw new DatabaseError({ error, name: "Update PKI ACME account" }); - } - }; - - const findById = async (id: string, tx?: Knex) => { - try { - const account = await (tx || db)(TableName.PkiAcmeAccount).where({ id }).first(); + const account = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId, id }).first(); return account || null; } catch (error) { @@ -51,16 +36,6 @@ export const pkiAcmeAccountDALFactory = (db: TDbClient) => { } }; - const findByProfileId = async (profileId: string, tx?: Knex) => { - try { - const account = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId }).first(); - - return account || null; - } catch (error) { - throw new DatabaseError({ error, name: "Find PKI ACME account by profile id" }); - } - }; - const findByPublicKey = async (profileId: string, alg: string, publicKey: unknown, tx?: Knex) => { try { const account = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId, alg, publicKey }).first(); @@ -71,35 +46,12 @@ export const pkiAcmeAccountDALFactory = (db: TDbClient) => { } }; - const findManyByProfileId = async (profileId: string, tx?: Knex) => { - try { - const accounts = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId }); - - return accounts; - } catch (error) { - throw new DatabaseError({ error, name: "Find many PKI ACME accounts by profile id" }); - } - }; - - const deleteById = async (id: string, tx?: Knex) => { - try { - const result = await (tx || db)(TableName.PkiAcmeAccount).where({ id }).delete().returning("*"); - const [account] = result; - - return account || null; - } catch (error) { - throw new DatabaseError({ error, name: "Delete PKI ACME account by id" }); - } - }; - return { ...pkiAcmeAccountOrm, create, updateById, findById, findByProfileId, - findByPublicKey, - findManyByProfileId, - deleteById + findByPublicKey }; }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 46f67ac17..a6d4b63ac 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -23,6 +23,7 @@ import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal"; import { CreateAcmeAccountBodySchema, ProtectedHeaderSchema } from "./pki-acme-schemas"; import { TAcmeResponse, + TAuthenciatedJwsPayload, TCreateAcmeAccountPayload, TCreateAcmeAccountResponse, TCreateAcmeOrderPayload, @@ -43,7 +44,7 @@ import { type TPkiAcmeServiceFactoryDep = { certificateProfileDAL: Pick; - acmeAccountDAL: Pick; + acmeAccountDAL: Pick; acmeOrderDAL: Pick; }; @@ -69,6 +70,14 @@ export const pkiAcmeServiceFactory = ({ return `${baseUrl}${path}`; }; + const extractAccountIdFromKid = (kid: string, profileId: string): string => { + const kidPrefix = buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/`); + if (!kid.startsWith(kidPrefix)) { + throw new AcmeMalformedError({ detail: "KID must start with the profile account URL" }); + } + return kid.slice(kidPrefix.length); + }; + const validateJwsPayload = async ( rawJwsPayload: TRawJwsPayload, getJWK: (protectedHeader: JWSHeaderParameters) => Promise, @@ -120,7 +129,7 @@ export const pkiAcmeServiceFactory = ({ rawJwsPayload, async (protectedHeader) => { if (!protectedHeader.jwk) { - throw new AcmeBadPublicKeyError({ detail: "JWK is required in the protected header" }); + throw new AcmeMalformedError({ detail: "JWK is required in the protected header" }); } return protectedHeader.jwk as unknown as JsonWebKey; }, @@ -128,6 +137,36 @@ export const pkiAcmeServiceFactory = ({ ); }; + const validateExistingAccountJwsPayload = async ( + profileId: string, + rawJwsPayload: TRawJwsPayload, + schema: z.ZodSchema + ): Promise> => { + const profile = await validateAcmeProfile(profileId); + const result = await validateJwsPayload( + rawJwsPayload, + async (protectedHeader) => { + if (!protectedHeader.kid) { + throw new AcmeMalformedError({ detail: "KID is required in the protected header" }); + } + const accountId = extractAccountIdFromKid(protectedHeader.kid, profileId); + const account = await acmeAccountDAL.findById(profile.id, accountId); + if (!account) { + throw new AcmeAccountDoesNotExistError({ message: "ACME account not found" }); + } + if (account.alg !== protectedHeader.alg) { + throw new AcmeMalformedError({ detail: "ACME account algorithm mismatch" }); + } + return account.publicKey as JsonWebKey; + }, + schema + ); + return { + ...result, + accountId: extractAccountIdFromKid(result.protectedHeader.kid!, profileId) + }; + }; + const getAcmeDirectory = async (profileId: string): Promise => { await validateAcmeProfile(profileId); return { @@ -195,13 +234,25 @@ export const pkiAcmeServiceFactory = ({ }; }; - const createAcmeOrder = async ( - profileId: string, - account: TPkiAcmeAccounts, - payload: TCreateAcmeOrderPayload - ): Promise> => { - const profile = await validateAcmeProfile(profileId); + const createAcmeOrder = async ({ + profileId, + accountId, + payload + }: { + profileId: string; + accountId: string; + payload: TCreateAcmeOrderPayload; + }): Promise> => { + const account = await acmeAccountDAL.findById(profileId, accountId)!; + // TODO: check and see if we have existing orders for this account that meet the criteria + // if we do, return the existing order + + orders = await acmeOrderDAL.create({ + profileId, + accountId, + status: "pending" + }); // FIXME: Implement ACME new order creation const orderId = "FIXME-order-id"; return { @@ -315,6 +366,7 @@ export const pkiAcmeServiceFactory = ({ return { validateJwsPayload, validateNewAccountJwsPayload, + validateExistingAccountJwsPayload, getAcmeDirectory, getAcmeNewNonce, createAcmeAccount, diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index 3bb03f6a6..7051222fc 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -42,6 +42,9 @@ export type TJwsPayload = { protectedHeader: TProtectedHeader; payload: T; }; +export type TAuthenciatedJwsPayload = TJwsPayload & { + accountId: string; +}; export type TAcmeResponse = { status: number; headers: Record; @@ -55,6 +58,11 @@ export type TPkiAcmeServiceFactory = { schema: z.ZodSchema ) => Promise>; validateNewAccountJwsPayload: (rawJwsPayload: TRawJwsPayload) => Promise>; + validateExistingAccountJwsPayload: ( + profileId: string, + rawJwsPayload: TRawJwsPayload, + schema: z.ZodSchema + ) => Promise>; getAcmeDirectory: (profileId: string) => Promise; getAcmeNewNonce: (profileId: string) => Promise; createAcmeAccount: ({ @@ -68,10 +76,15 @@ export type TPkiAcmeServiceFactory = { jwk: JsonWebKey; payload: TCreateAcmeAccountPayload; }) => Promise>; - createAcmeOrder: ( - profileId: string, - body: TCreateAcmeOrderPayload - ) => Promise>; + createAcmeOrder: ({ + profileId, + accountId, + payload + }: { + profileId: string; + accountId: string; + payload: TCreateAcmeOrderPayload; + }) => Promise>; deactivateAcmeAccount: ( profileId: string, accountId: string, From 3a7ba8d1382d759358c54d152e6f1841cd7012cc Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Wed, 29 Oct 2025 12:17:25 -0700 Subject: [PATCH 109/294] Remove unused stuff --- backend/src/ee/services/pki-acme/pki-acme-account-dal.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts index 713cb4350..e7979826d 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts @@ -49,9 +49,7 @@ export const pkiAcmeAccountDALFactory = (db: TDbClient) => { return { ...pkiAcmeAccountOrm, create, - updateById, findById, - findByProfileId, findByPublicKey }; }; From 80904b1dd1253881b4f34d0a8d3de72542c4020b Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Wed, 29 Oct 2025 14:13:07 -0700 Subject: [PATCH 110/294] Refine routers --- backend/src/ee/routes/v1/pki-acme-router.ts | 121 +++++++++++------- .../ee/services/pki-acme/pki-acme-schemas.ts | 112 ++++------------ .../ee/services/pki-acme/pki-acme-service.ts | 49 +++++-- backend/src/server/routes/index.ts | 8 +- 4 files changed, 144 insertions(+), 146 deletions(-) diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index 13bad0a6a..24ae75ff5 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -1,32 +1,38 @@ /* eslint-disable @typescript-eslint/no-floating-promises */ +import type { TAcmeResponse } from "@app/ee/services/pki-acme/pki-acme-types"; +import { FastifyReply } from "fastify"; import { z } from "zod"; import { CreateAcmeAccountResponseSchema, CreateAcmeOrderBodySchema, CreateAcmeOrderResponseSchema, + DeactivateAcmeAccountBodySchema, DeactivateAcmeAccountResponseSchema, - DeactivateAcmeAccountSchema, - DownloadAcmeCertificateSchema, - FinalizeAcmeOrderResponseSchema, - FinalizeAcmeOrderSchema, + FinalizeAcmeOrderBodySchema, GetAcmeAuthorizationResponseSchema, - GetAcmeAuthorizationSchema, GetAcmeDirectoryResponseSchema, - GetAcmeDirectorySchema, - GetAcmeNewNonceSchema, GetAcmeOrderResponseSchema, - GetAcmeOrderSchema, ListAcmeOrdersResponseSchema, - ListAcmeOrdersSchema, RawJwsPayloadSchema, - RespondToAcmeChallengeResponseSchema, - RespondToAcmeChallengeSchema + RespondToAcmeChallengeResponseSchema } from "@app/ee/services/pki-acme/pki-acme-schemas"; import { ApiDocsTags } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { + const sendAcmeResponse = async (res: FastifyReply, profileId: string, response: TAcmeResponse): Promise => { + res.code(response.status); + for (const [key, value] of Object.entries(response.headers)) { + res.header(key, value); + } + + const nonce = await server.services.pkiAcme.getAcmeNewNonce(profileId); + res.header("Replay-Nonce", nonce); + res.header("Cache-Control", "no-store"); + return response.body; + }; + server.addContentTypeParser("application/jose+json", { parseAs: "string" }, (_, body, done) => { try { const strBody = body instanceof Buffer ? body.toString() : body; @@ -53,7 +59,9 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME Directory - provides URLs for the client to make API calls to", - ...GetAcmeDirectorySchema.shape, + params: z.object({ + profileId: z.string().uuid() + }), response: { 200: GetAcmeDirectoryResponseSchema } @@ -77,15 +85,17 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME New Nonce - generate a new nonce and return in Replay-Nonce header", - ...GetAcmeNewNonceSchema.shape, + params: z.object({ + profileId: z.string().uuid() + }), response: { - 200: z.object({}) + 200: z.string().length(0) } }, handler: async (req, res) => { const nonce = await server.services.pkiAcme.getAcmeNewNonce(req.params.profileId); res.header("Replay-Nonce", nonce); - return {}; + return ""; } }); @@ -112,23 +122,16 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { handler: async (req, res) => { const { payload, protectedHeader } = await server.services.pkiAcme.validateNewAccountJwsPayload(req.body); const { alg, jwk } = protectedHeader; - const { status, body, headers } = await server.services.pkiAcme.createAcmeAccount({ - profileId: req.params.profileId, - alg, - jwk: jwk!, - payload - }); - // TODO: DRY - res.code(status); - for (const [key, value] of Object.entries(headers)) { - res.header(key, value); - } - - // TODO: DRY - const nonce = await server.services.pkiAcme.getAcmeNewNonce(req.params.profileId); - res.header("Replay-Nonce", nonce); - res.header("Cache-Control", "no-store"); - return body; + return sendAcmeResponse( + res, + req.params.profileId, + await server.services.pkiAcme.createAcmeAccount({ + profileId: req.params.profileId, + alg, + jwk: jwk!, + payload + }) + ); } }); @@ -155,14 +158,20 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { - const { payload, protectedHeader, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload( + const { payload, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload( req.params.profileId, req.body, CreateAcmeOrderBodySchema ); - const order = await server.services.pkiAcme.createAcmeOrder(req.params.profileId, req.body); - res.code(201); - return order; + return sendAcmeResponse( + res, + req.params.profileId, + await server.services.pkiAcme.createAcmeOrder({ + profileId: req.params.profileId, + accountId, + payload + }) + ); } }); @@ -178,7 +187,11 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME Account Deactivation", - ...DeactivateAcmeAccountSchema.shape, + params: z.object({ + profileId: z.string().uuid(), + accountId: z.string() + }), + body: DeactivateAcmeAccountBodySchema, response: { 200: DeactivateAcmeAccountResponseSchema } @@ -203,7 +216,10 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME List Orders - get existing orders from current account", - ...ListAcmeOrdersSchema.shape, + params: z.object({ + profileId: z.string().uuid(), + accountId: z.string() + }), response: { 200: ListAcmeOrdersResponseSchema } @@ -228,7 +244,10 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME Get Order - return status and details of the order", - ...GetAcmeOrderSchema.shape, + params: z.object({ + profileId: z.string().uuid(), + orderId: z.string().uuid() + }), response: { 200: GetAcmeOrderResponseSchema } @@ -253,10 +272,11 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME Finalize Order - finalize cert order by providing CSR", - ...FinalizeAcmeOrderSchema.shape, - response: { - 200: FinalizeAcmeOrderResponseSchema - } + params: z.object({ + profileId: z.string().uuid(), + orderId: z.string().uuid() + }), + body: FinalizeAcmeOrderBodySchema }, // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), @@ -278,7 +298,10 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME Download Certificate - download certificate when ready", - ...DownloadAcmeCertificateSchema.shape, + params: z.object({ + profileId: z.string().uuid(), + orderId: z.string().uuid() + }), response: { 200: z.string() } @@ -307,7 +330,10 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME Identifier Authorization - get authorization info (challenges)", - ...GetAcmeAuthorizationSchema.shape, + params: z.object({ + profileId: z.string().uuid(), + authzId: z.string().uuid() + }), response: { 200: GetAcmeAuthorizationResponseSchema } @@ -332,7 +358,10 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME Respond to Challenge - let ACME server know challenge is ready", - ...RespondToAcmeChallengeSchema.shape, + params: z.object({ + profileId: z.string().uuid(), + authzId: z.string().uuid() + }), response: { 200: RespondToAcmeChallengeResponseSchema } diff --git a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts index 7e50b2108..935776a8e 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts @@ -1,5 +1,26 @@ import { z } from "zod"; +export enum AcmeIdentifierType { + DNS = "dns" +} + +export enum AcmeOrderStatus { + Pending = "pending", + Processing = "processing", + Ready = "ready", + Valid = "valid", + Invalid = "invalid" +} + +export enum AcmeAuthStatus { + Pending = "pending", + Valid = "valid", + Invalid = "invalid", + Deactivated = "deactivated", + Expired = "expired", + Revoked = "revoked" +} + export const ProtectedHeaderSchema = z .object({ alg: z.string(), @@ -20,13 +41,6 @@ export const RawJwsPayloadSchema = z.object({ signature: z.string() }); -// Directory endpoint -export const GetAcmeDirectorySchema = z.object({ - params: z.object({ - profileId: z.string().uuid() - }) -}); - export const GetAcmeDirectoryResponseSchema = z.object({ newNonce: z.string(), newAccount: z.string(), @@ -34,13 +48,6 @@ export const GetAcmeDirectoryResponseSchema = z.object({ revokeCert: z.string().optional() }); -// New Nonce endpoint -export const GetAcmeNewNonceSchema = z.object({ - params: z.object({ - profileId: z.string().uuid() - }) -}); - // New Account payload schema export const CreateAcmeAccountBodySchema = z.object({ contact: z.array(z.string()).optional(), @@ -73,22 +80,16 @@ export const CreateAcmeAccountResponseSchema = z.object({ export const CreateAcmeOrderBodySchema = z.object({ identifiers: z.array( z.object({ - type: z.string(), - value: z.string() + type: z + .string() + .regex(/^(?!-)[A-Za-z0-9-]{1,63}(?; acmeAccountDAL: Pick; acmeOrderDAL: Pick; + acmeAuthDAL: Pick; }; export const pkiAcmeServiceFactory = ({ certificateProfileDAL, acmeAccountDAL, - acmeOrderDAL + acmeOrderDAL, + acmeAuthDAL }: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => { const validateAcmeProfile = async (profileId: string): Promise => { const profile = await certificateProfileDAL.findById(profileId); @@ -168,11 +177,11 @@ export const pkiAcmeServiceFactory = ({ }; const getAcmeDirectory = async (profileId: string): Promise => { - await validateAcmeProfile(profileId); + const profile = await validateAcmeProfile(profileId); return { - newNonce: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/new-nonce`), - newAccount: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/new-account`), - newOrder: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/new-order`) + newNonce: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/new-nonce`), + newAccount: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/new-account`), + newOrder: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/new-order`) }; }; @@ -244,15 +253,29 @@ export const pkiAcmeServiceFactory = ({ payload: TCreateAcmeOrderPayload; }): Promise> => { const account = await acmeAccountDAL.findById(profileId, accountId)!; - // TODO: check and see if we have existing orders for this account that meet the criteria // if we do, return the existing order - orders = await acmeOrderDAL.create({ - profileId, - accountId, - status: "pending" + const order = await acmeOrderDAL.create({ + accountId: account.id, + status: AcmeOrderStatus.Pending }); + payload.identifiers.forEach(async (identifier) => { + if (identifier.type === AcmeIdentifierType.DNS) { + // TODO: reuse existing authorizations for this identifier if they exist + const auth = await acmeAuthDAL.create({ + accountId: account.id, + status: AcmeAuthStatus.Pending, + identifierType: identifier.type, + identifierValue: identifier.value, + // TODO: read config from the profile to get the expiration time instead + expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) + }); + } else { + throw new AcmeMalformedError({ detail: "Only DNS identifiers are supported" }); + } + }); + // FIXME: Implement ACME new order creation const orderId = "FIXME-order-id"; return { @@ -262,10 +285,10 @@ export const pkiAcmeServiceFactory = ({ expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), identifiers: [], authorizations: [], - finalize: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/orders/${orderId}/finalize`) + finalize: buildUrl(`/api/v1/pki/acme/profiles/${account.profileId}/orders/${orderId}/finalize`) }, headers: { - Location: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/orders/${orderId}`) + Location: buildUrl(`/api/v1/pki/acme/profiles/${account.profileId}/orders/${orderId}`) } }; }; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 49ac7750d..376baaaae 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -74,6 +74,7 @@ import { pamSessionServiceFactory } from "@app/ee/services/pam-session/pam-sessi import { permissionDALFactory } from "@app/ee/services/permission/permission-dal"; import { permissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { pitServiceFactory } from "@app/ee/services/pit/pit-service"; +import { pkiAcmeAuthDALFactory } from "@app/ee/services/pki-acme/pki-acme-auth-dal"; import { pkiAcmeServiceFactory } from "@app/ee/services/pki-acme/pki-acme-service"; import { projectTemplateDALFactory } from "@app/ee/services/project-template/project-template-dal"; import { projectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service"; @@ -350,6 +351,7 @@ import { workflowIntegrationDALFactory } from "@app/services/workflow-integratio import { workflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service"; import { pkiAcmeAccountDALFactory } from "@app/ee/services/pki-acme/pki-acme-account-dal"; +import { pkiAcmeOrderDALFactory } from "@app/ee/services/pki-acme/pki-acme-order-dal"; import { injectAuditLogInfo } from "../plugins/audit-log"; import { injectAssumePrivilege } from "../plugins/auth/inject-assume-privilege"; import { injectIdentity } from "../plugins/auth/inject-identity"; @@ -361,7 +363,6 @@ import { initializeOauthConfigSync } from "./v1/sso-router"; import { registerV2Routes } from "./v2"; import { registerV3Routes } from "./v3"; import { registerV4Routes } from "./v4"; -import { pkiAcmeOrderDALFactory } from "@app/ee/services/pki-acme/pki-acme-order-dal"; const histogram = monitorEventLoopDelay({ resolution: 20 }); histogram.enable(); @@ -1068,7 +1069,7 @@ export const registerRoutes = async ( const acmeEnrollmentConfigDAL = acmeEnrollmentConfigDALFactory(db); const acmeAccountDAL = pkiAcmeAccountDALFactory(db); const acmeOrderDAL = pkiAcmeOrderDALFactory(db); - + const acmeAuthDAL = pkiAcmeAuthDALFactory(db); const certificateDAL = certificateDALFactory(db); const certificateBodyDAL = certificateBodyDALFactory(db); const certificateSecretDAL = certificateSecretDALFactory(db); @@ -1172,7 +1173,8 @@ export const registerRoutes = async ( const pkiAcmeService = pkiAcmeServiceFactory({ certificateProfileDAL, acmeAccountDAL, - acmeOrderDAL + acmeOrderDAL, + acmeAuthDAL }); const pkiAlertService = pkiAlertServiceFactory({ From d880c4c6c9e97834f6c9669760841dee33241937 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Wed, 29 Oct 2025 14:17:06 -0700 Subject: [PATCH 111/294] Get all authorizations --- .../ee/services/pki-acme/pki-acme-service.ts | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 20cc5f21d..9e1167aec 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -260,21 +260,23 @@ export const pkiAcmeServiceFactory = ({ accountId: account.id, status: AcmeOrderStatus.Pending }); - payload.identifiers.forEach(async (identifier) => { - if (identifier.type === AcmeIdentifierType.DNS) { - // TODO: reuse existing authorizations for this identifier if they exist - const auth = await acmeAuthDAL.create({ - accountId: account.id, - status: AcmeAuthStatus.Pending, - identifierType: identifier.type, - identifierValue: identifier.value, - // TODO: read config from the profile to get the expiration time instead - expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) - }); - } else { - throw new AcmeMalformedError({ detail: "Only DNS identifiers are supported" }); - } - }); + const authorizations = await Promise.all( + payload.identifiers.map(async (identifier) => { + if (identifier.type === AcmeIdentifierType.DNS) { + // TODO: reuse existing authorizations for this identifier if they exist + return await acmeAuthDAL.create({ + accountId: account.id, + status: AcmeAuthStatus.Pending, + identifierType: identifier.type, + identifierValue: identifier.value, + // TODO: read config from the profile to get the expiration time instead + expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) + }); + } else { + throw new AcmeMalformedError({ detail: "Only DNS identifiers are supported" }); + } + }) + ); // FIXME: Implement ACME new order creation const orderId = "FIXME-order-id"; From 76505939f3389d98adb395125f9ffe4271c880a2 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Wed, 29 Oct 2025 16:13:51 -0700 Subject: [PATCH 112/294] Add acme order table --- backend/src/@types/knex.d.ts | 5 ++++ ...acme.ts => 20251029234547_add-pki-acme.ts} | 25 +++++++++++++++++ backend/src/db/schemas/index.ts | 1 + backend/src/db/schemas/models.ts | 1 + .../src/db/schemas/pki-acme-order-auths.ts | 20 ++++++++++++++ .../pki-acme/pki-acme-order-auth-dal.ts | 27 +++++++++++++++++++ .../ee/services/pki-acme/pki-acme-service.ts | 15 +++++++++-- backend/src/server/routes/index.ts | 5 +++- 8 files changed, 96 insertions(+), 3 deletions(-) rename backend/src/db/migrations/{20251027234547_add-pki-acme.ts => 20251029234547_add-pki-acme.ts} (89%) create mode 100644 backend/src/db/schemas/pki-acme-order-auths.ts create mode 100644 backend/src/ee/services/pki-acme/pki-acme-order-auth-dal.ts diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 8a19670f0..1daf722a1 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -728,6 +728,11 @@ declare module "knex/types/tables" { TPkiAcmeOrdersUpdate >; [TableName.PkiAcmeAuth]: KnexOriginal.CompositeTableType; + [TableName.PkiAcmeOrderAuth]: KnexOriginal.CompositeTableType< + TPkiAcmeOrderAuths, + TPkiAcmeOrderAuthsInsert, + TPkiAcmeOrderAuthsUpdate + >; [TableName.PkiAcmeChallenge]: KnexOriginal.CompositeTableType< TPkiAcmeChallenges, TPkiAcmeChallengesInsert, diff --git a/backend/src/db/migrations/20251027234547_add-pki-acme.ts b/backend/src/db/migrations/20251029234547_add-pki-acme.ts similarity index 89% rename from backend/src/db/migrations/20251027234547_add-pki-acme.ts rename to backend/src/db/migrations/20251029234547_add-pki-acme.ts index 285676c8f..6390e5812 100644 --- a/backend/src/db/migrations/20251027234547_add-pki-acme.ts +++ b/backend/src/db/migrations/20251029234547_add-pki-acme.ts @@ -116,6 +116,25 @@ export async function up(knex: Knex): Promise { await createOnUpdateTrigger(knex, TableName.PkiAcmeAuth); } + // Create PkiAcmeOrderAuth table + if (!(await knex.schema.hasTable(TableName.PkiAcmeOrderAuth))) { + await knex.schema.createTable(TableName.PkiAcmeOrderAuth, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + // Foreign key to PkiAcmeOrder + t.uuid("orderId").notNullable(); + t.foreign("orderId").references("id").inTable(TableName.PkiAcmeOrder).onDelete("CASCADE"); + + // Foreign key to PkiAcmeAuth + t.uuid("authId").notNullable(); + t.foreign("authId").references("id").inTable(TableName.PkiAcmeAuth).onDelete("CASCADE"); + + t.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.PkiAcmeOrderAuth); + } + // Create PkiAcmeChallenge table if (!(await knex.schema.hasTable(TableName.PkiAcmeChallenge))) { await knex.schema.createTable(TableName.PkiAcmeChallenge, (t) => { @@ -150,6 +169,12 @@ export async function down(knex: Knex): Promise { await dropOnUpdateTrigger(knex, TableName.PkiAcmeChallenge); } + // Drop PkiAcmeOrderAuth (depends on PkiAcmeOrder and PkiAcmeAuth) + if (await knex.schema.hasTable(TableName.PkiAcmeOrderAuth)) { + await knex.schema.dropTable(TableName.PkiAcmeOrderAuth); + await dropOnUpdateTrigger(knex, TableName.PkiAcmeOrderAuth); + } + // Drop PkiAcmeAuth (depends on PkiAcmeAccount and Certificate) if (await knex.schema.hasTable(TableName.PkiAcmeAuth)) { await knex.schema.dropTable(TableName.PkiAcmeAuth); diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 3eef75d16..e3db789ac 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -97,6 +97,7 @@ export * from "./pki-acme-auths"; export * from "./pki-acme-challenges"; export * from "./pki-acme-enrollment-configs"; export * from "./pki-acme-orders"; +export * from "./pki-acme-order-auths"; export * from "./pki-alerts"; export * from "./pki-api-enrollment-configs"; export * from "./pki-certificate-profiles"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 0fcda08d7..a031c83b9 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -30,6 +30,7 @@ export enum TableName { PkiAcmeEnrollmentConfig = "pki_acme_enrollment_configs", PkiAcmeAccount = "pki_acme_accounts", PkiAcmeOrder = "pki_acme_orders", + PkiAcmeOrderAuth = "pki_acme_order_auths", PkiAcmeAuth = "pki_acme_auths", PkiAcmeChallenge = "pki_acme_challenges", PkiSubscriber = "pki_subscribers", diff --git a/backend/src/db/schemas/pki-acme-order-auths.ts b/backend/src/db/schemas/pki-acme-order-auths.ts new file mode 100644 index 000000000..66f8704f2 --- /dev/null +++ b/backend/src/db/schemas/pki-acme-order-auths.ts @@ -0,0 +1,20 @@ +// 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 { TImmutableDBKeys } from "./models"; + +export const PkiAcmeOrderAuthsSchema = z.object({ + id: z.string().uuid(), + orderId: z.string().uuid(), + authId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TPkiAcmeOrderAuths = z.infer; +export type TPkiAcmeOrderAuthsInsert = Omit, TImmutableDBKeys>; +export type TPkiAcmeOrderAuthsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/ee/services/pki-acme/pki-acme-order-auth-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-order-auth-dal.ts new file mode 100644 index 000000000..87b6f8211 --- /dev/null +++ b/backend/src/ee/services/pki-acme/pki-acme-order-auth-dal.ts @@ -0,0 +1,27 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { TPkiAcmeOrderAuthsInsert } from "@app/db/schemas/pki-acme-order-auths"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify } from "@app/lib/knex"; + +export type TPkiAcmeOrderAuthDALFactory = ReturnType; + +export const pkiAcmeOrderAuthDALFactory = (db: TDbClient) => { + const pkiAcmeOrderAuthOrm = ormify(db, TableName.PkiAcmeOrderAuth); + + const insertMany = async (rows: TPkiAcmeOrderAuthsInsert[], tx?: Knex) => { + try { + const result = await (tx || db)(TableName.PkiAcmeOrderAuth).insert(rows).returning("*"); + return result; + } catch (error) { + throw new DatabaseError({ error, name: "Insert many PKI ACME order auths" }); + } + }; + + return { + ...pkiAcmeOrderAuthOrm, + insertMany + }; +}; diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 9e1167aec..e8967a9ac 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -11,6 +11,7 @@ import { } from "./pki-acme-errors"; import { TPkiAcmeAccounts } from "@app/db/schemas/pki-acme-accounts"; +import { TPkiAcmeAuths } from "@app/db/schemas/pki-acme-auths"; import { logger } from "@app/lib/logger"; import { EnrollmentType, @@ -21,6 +22,7 @@ import { z, ZodError } from "zod"; import { TPkiAcmeAccountDALFactory } from "./pki-acme-account-dal"; import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal"; import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal"; +import { TPkiAcmeOrderAuthDALFactory } from "./pki-acme-order-auth-dal"; import { AcmeAuthStatus, AcmeIdentifierType, @@ -54,13 +56,15 @@ type TPkiAcmeServiceFactoryDep = { acmeAccountDAL: Pick; acmeOrderDAL: Pick; acmeAuthDAL: Pick; + acmeOrderAuthDAL: Pick; }; export const pkiAcmeServiceFactory = ({ certificateProfileDAL, acmeAccountDAL, acmeOrderDAL, - acmeAuthDAL + acmeAuthDAL, + acmeOrderAuthDAL }: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => { const validateAcmeProfile = async (profileId: string): Promise => { const profile = await certificateProfileDAL.findById(profileId); @@ -260,7 +264,7 @@ export const pkiAcmeServiceFactory = ({ accountId: account.id, status: AcmeOrderStatus.Pending }); - const authorizations = await Promise.all( + const authorizations: TPkiAcmeAuths[] = await Promise.all( payload.identifiers.map(async (identifier) => { if (identifier.type === AcmeIdentifierType.DNS) { // TODO: reuse existing authorizations for this identifier if they exist @@ -278,6 +282,13 @@ export const pkiAcmeServiceFactory = ({ }) ); + await acmeOrderAuthDAL.insertMany( + authorizations.map((auth) => ({ + orderId: order.id, + authId: auth.id + })) + ); + // FIXME: Implement ACME new order creation const orderId = "FIXME-order-id"; return { diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 376baaaae..40ffe4f5d 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -76,6 +76,7 @@ import { permissionServiceFactory } from "@app/ee/services/permission/permission import { pitServiceFactory } from "@app/ee/services/pit/pit-service"; import { pkiAcmeAuthDALFactory } from "@app/ee/services/pki-acme/pki-acme-auth-dal"; import { pkiAcmeServiceFactory } from "@app/ee/services/pki-acme/pki-acme-service"; +import { pkiAcmeOrderAuthDALFactory } from "@app/ee/services/pki-acme/pki-acme-order-auth-dal"; import { projectTemplateDALFactory } from "@app/ee/services/project-template/project-template-dal"; import { projectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service"; import { rateLimitDALFactory } from "@app/ee/services/rate-limit/rate-limit-dal"; @@ -1070,6 +1071,7 @@ export const registerRoutes = async ( const acmeAccountDAL = pkiAcmeAccountDALFactory(db); const acmeOrderDAL = pkiAcmeOrderDALFactory(db); const acmeAuthDAL = pkiAcmeAuthDALFactory(db); + const acmeOrderAuthDAL = pkiAcmeOrderAuthDALFactory(db); const certificateDAL = certificateDALFactory(db); const certificateBodyDAL = certificateBodyDALFactory(db); const certificateSecretDAL = certificateSecretDALFactory(db); @@ -1174,7 +1176,8 @@ export const registerRoutes = async ( certificateProfileDAL, acmeAccountDAL, acmeOrderDAL, - acmeAuthDAL + acmeAuthDAL, + acmeOrderAuthDAL }); const pkiAlertService = pkiAlertServiceFactory({ From e2916cb0f7cd916f9336b17b36f37bb45c047e3d Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Wed, 29 Oct 2025 16:40:46 -0700 Subject: [PATCH 113/294] Fix acme config creation --- backend/bdd/features/steps/pki_acme.py | 2 +- .../routes/v1/certificate-profiles-router.ts | 22 +++++++++++++++++-- .../certificate-profile-service.ts | 2 +- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 8546cb217..7e1cad457 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -36,7 +36,7 @@ def step_impl(context: Context, profile_var: str): # TODO: Fixed value for now, just to make test much easier, # we should call infisical API to create such profile instead # in the future - profile_id = "dd6e09c8-d5b8-4bfd-b436-4ab5c93d5d7e" + profile_id = "322be4ee-fe20-41c0-ba7c-bdbdfeee2ba8" context.vars[profile_var] = AcmeProfile(profile_id) diff --git a/backend/src/server/routes/v1/certificate-profiles-router.ts b/backend/src/server/routes/v1/certificate-profiles-router.ts index 08f532bc4..7ad7aaeb1 100644 --- a/backend/src/server/routes/v1/certificate-profiles-router.ts +++ b/backend/src/server/routes/v1/certificate-profiles-router.ts @@ -44,7 +44,8 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid autoRenew: z.boolean().default(false), renewBeforeDays: z.number().min(1).max(30).optional() }) - .optional() + .optional(), + acmeConfig: z.object({}).optional() }) .refine( (data) => { @@ -55,6 +56,9 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid if (data.apiConfig) { return false; } + if (data.acmeConfig) { + return false; + } } if (data.enrollmentType === EnrollmentType.API) { if (!data.apiConfig) { @@ -63,12 +67,26 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid if (data.estConfig) { return false; } + if (data.acmeConfig) { + return false; + } + } + if (data.enrollmentType === EnrollmentType.ACME) { + if (!data.acmeConfig) { + return false; + } + if (data.estConfig) { + return false; + } + if (data.apiConfig) { + return false; + } } return true; }, { message: - "EST enrollment type requires EST configuration and cannot have API configuration. API enrollment type requires API configuration and cannot have EST configuration." + "EST enrollment type requires EST configuration and cannot have API or ACME configuration. API enrollment type requires API configuration and cannot have EST or ACME configuration. ACME enrollment type requires ACME configuration and cannot have EST or API configuration." } ), response: { diff --git a/backend/src/services/certificate-profile/certificate-profile-service.ts b/backend/src/services/certificate-profile/certificate-profile-service.ts index 0de9d6c55..527ed3c96 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.ts @@ -275,7 +275,7 @@ export const certificateProfileServiceFactory = ({ } // Create the profile with the created config IDs - const { estConfig, apiConfig, ...profileData } = data; + const { estConfig, apiConfig, acmeConfig, ...profileData } = data; const profileResult = await certificateProfileDAL.create( { ...profileData, From 53555892d4c1efb4ab031cb9fcd7bd8c894d14ca Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Wed, 29 Oct 2025 17:35:57 -0700 Subject: [PATCH 114/294] Add new order feature --- .../bdd/features/pki/acme/new-order.feature | 17 ++++++ backend/bdd/features/steps/pki_acme.py | 53 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 backend/bdd/features/pki/acme/new-order.feature diff --git a/backend/bdd/features/pki/acme/new-order.feature b/backend/bdd/features/pki/acme/new-order.feature new file mode 100644 index 000000000..5ec9620e4 --- /dev/null +++ b/backend/bdd/features/pki/acme/new-order.feature @@ -0,0 +1,17 @@ +Feature: New Order + + Scenario: Create a new order +# Given I have an ACME cert profile as "acme_profile" +# When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory +# # TODO: make it I have an account already instead? +# Then I register a new ACME account with email fangpen@infisical.com and EAB key id {acme_profile.eab_kid} with secret {acme_profile.eab_secret} as acme_account + When I create certificate signing request as csr + Then I add names to certificate signing request csr + """ + { + "ORGANIZATION_NAME": "Infisical Inc", + "COMMON_NAME": "localhost" + } + """ + Then I create a RSA private key pair as cert_key + Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 7e1cad457..d3f0c109e 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -9,6 +9,9 @@ from behave import then from josepy.jwk import JWKRSA from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography import x509 +from cryptography.x509.oid import NameOID +from cryptography.hazmat.primitives import hashes ACC_KEY_BITS = 2048 ACC_KEY_PUBLIC_EXPONENT = 65537 @@ -93,3 +96,53 @@ def step_impl(context: Context, email: str, kid: str, secret: str, account_var: # TODO: add EAB info here registration = messages.NewRegistration.from_data(email=email) context.vars[account_var] = context.acme_client.new_account(registration) + + +@when("I create certificate signing request as {csr_var}") +def step_impl(context: Context, csr_var: str): + context.vars[csr_var] = x509.CertificateSigningRequestBuilder() + + +@then("I add names to certificate signing request {csr_var}") +def step_impl(context: Context, csr_var: str): + names = json.loads(context.text) + builder: x509.CertificateSigningRequestBuilder = context.vars[csr_var] + builder.subject_name( + x509.Name( + [ + x509.NameAttribute(getattr(NameOID, name), value) + for name, value in names.items() + ] + ) + ) + + +@then("I add subject alternative name to certificate signing request {csr_var}") +def step_impl(context: Context, csr_var: str): + names = json.loads(context.text) + builder: x509.CertificateSigningRequestBuilder = context.vars[csr_var] + builder.add_extension( + x509.SubjectAlternativeName([x509.DNSName(name) for name in names]), + critical=False, + ) + + +@then("I create a RSA private key pair as {rsa_key_var}") +def step_impl(context: Context, rsa_key_var: str): + context.vars[rsa_key_var] = rsa.generate_private_key( + # TODO: make them configurable if we need to + public_exponent=65537, + key_size=2048, + ) + + +@then( + "I sign the certificate signing request {csr_var} with private key {pk_var} and output it as {pem_var} in PEM format" +) +def step_impl(context: Context, csr_var: str, pk_var: str, pem_var: str): + context.vars[pem_var] = ( + context.vars[csr_var] + .sign(context.vars[pk_var], hashes.SHA256()) + .public_bytes(serialization.Encoding.PEM) + .decode("utf-8") + ) From 9ee9aa32615d46181ac46139d13a5d52d730c70e Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Wed, 29 Oct 2025 17:42:34 -0700 Subject: [PATCH 115/294] More for new order --- .../bdd/features/pki/acme/new-order.feature | 7 +- backend/bdd/features/steps/pki_acme.py | 8 +- .../ee/services/pki-acme/pki-acme-service.ts | 85 +++++++++++-------- 3 files changed, 61 insertions(+), 39 deletions(-) diff --git a/backend/bdd/features/pki/acme/new-order.feature b/backend/bdd/features/pki/acme/new-order.feature index 5ec9620e4..ebc0961ee 100644 --- a/backend/bdd/features/pki/acme/new-order.feature +++ b/backend/bdd/features/pki/acme/new-order.feature @@ -1,10 +1,10 @@ Feature: New Order Scenario: Create a new order -# Given I have an ACME cert profile as "acme_profile" -# When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + Given I have an ACME cert profile as "acme_profile" + When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory # # TODO: make it I have an account already instead? -# Then I register a new ACME account with email fangpen@infisical.com and EAB key id {acme_profile.eab_kid} with secret {acme_profile.eab_secret} as acme_account + Then I register a new ACME account with email fangpen@infisical.com and EAB key id {acme_profile.eab_kid} with secret {acme_profile.eab_secret} as acme_account When I create certificate signing request as csr Then I add names to certificate signing request csr """ @@ -15,3 +15,4 @@ Feature: New Order """ Then I create a RSA private key pair as cert_key Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format + Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index d3f0c109e..254ea4af4 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -98,6 +98,13 @@ def step_impl(context: Context, email: str, kid: str, secret: str, account_var: context.vars[account_var] = context.acme_client.new_account(registration) +@then( + "I submit the certificate signing request PEM {pem_var} certificate order to the ACME server" +) +def step_impl(context: Context, pem_var: str): + context.acme_order = context.acme_client.new_order(context.vars[pem_var]) + + @when("I create certificate signing request as {csr_var}") def step_impl(context: Context, csr_var: str): context.vars[csr_var] = x509.CertificateSigningRequestBuilder() @@ -144,5 +151,4 @@ def step_impl(context: Context, csr_var: str, pk_var: str, pem_var: str): context.vars[csr_var] .sign(context.vars[pk_var], hashes.SHA256()) .public_bytes(serialization.Encoding.PEM) - .decode("utf-8") ) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index e8967a9ac..098046ffc 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -54,7 +54,7 @@ import { type TPkiAcmeServiceFactoryDep = { certificateProfileDAL: Pick; acmeAccountDAL: Pick; - acmeOrderDAL: Pick; + acmeOrderDAL: Pick; acmeAuthDAL: Pick; acmeOrderAuthDAL: Pick; }; @@ -256,52 +256,67 @@ export const pkiAcmeServiceFactory = ({ accountId: string; payload: TCreateAcmeOrderPayload; }): Promise> => { - const account = await acmeAccountDAL.findById(profileId, accountId)!; // TODO: check and see if we have existing orders for this account that meet the criteria // if we do, return the existing order - const order = await acmeOrderDAL.create({ - accountId: account.id, - status: AcmeOrderStatus.Pending + const order = await acmeOrderDAL.transaction(async (tx) => { + const account = await acmeAccountDAL.findById(profileId, accountId)!; + const createdOrder = await acmeOrderDAL.create( + { + accountId: account.id, + status: AcmeOrderStatus.Pending + }, + tx + ); + const authorizations: TPkiAcmeAuths[] = await Promise.all( + payload.identifiers.map(async (identifier) => { + if (identifier.type === AcmeIdentifierType.DNS) { + // TODO: reuse existing authorizations for this identifier if they exist + return await acmeAuthDAL.create({ + accountId: account.id, + status: AcmeAuthStatus.Pending, + identifierType: identifier.type, + identifierValue: identifier.value, + // TODO: read config from the profile to get the expiration time instead + expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) + }); + } else { + throw new AcmeMalformedError({ detail: "Only DNS identifiers are supported" }); + } + }) + ); + + await acmeOrderAuthDAL.insertMany( + authorizations.map((auth) => ({ + orderId: order.id, + authId: auth.id + })) + ); + return { ...createdOrder, authorizations, account }; }); - const authorizations: TPkiAcmeAuths[] = await Promise.all( - payload.identifiers.map(async (identifier) => { - if (identifier.type === AcmeIdentifierType.DNS) { - // TODO: reuse existing authorizations for this identifier if they exist - return await acmeAuthDAL.create({ - accountId: account.id, - status: AcmeAuthStatus.Pending, - identifierType: identifier.type, - identifierValue: identifier.value, - // TODO: read config from the profile to get the expiration time instead - expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) - }); - } else { - throw new AcmeMalformedError({ detail: "Only DNS identifiers are supported" }); - } - }) - ); - await acmeOrderAuthDAL.insertMany( - authorizations.map((auth) => ({ - orderId: order.id, - authId: auth.id - })) - ); - - // FIXME: Implement ACME new order creation - const orderId = "FIXME-order-id"; return { status: 201, body: { status: "pending", + // TODO: read config from the profile to get the expiration time instead expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), - identifiers: [], - authorizations: [], - finalize: buildUrl(`/api/v1/pki/acme/profiles/${account.profileId}/orders/${orderId}/finalize`) + identifiers: order.authorizations.map((auth) => ({ + type: auth.identifierType, + value: auth.identifierValue + })), + authorizations: order.authorizations.map((auth) => ({ + id: auth.id, + status: auth.status, + identifier: { + type: auth.identifierType, + value: auth.identifierValue + } + })), + finalize: buildUrl(`/api/v1/pki/acme/profiles/${order.account.profileId}/orders/${order.id}/finalize`) }, headers: { - Location: buildUrl(`/api/v1/pki/acme/profiles/${account.profileId}/orders/${orderId}`) + Location: buildUrl(`/api/v1/pki/acme/profiles/${order.account.profileId}/orders/${order.id}`) } }; }; From 3d58702a4f097b891e25ffde0506a332e86d220a Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Wed, 29 Oct 2025 17:48:29 -0700 Subject: [PATCH 116/294] Fix bdd --- backend/bdd/features/steps/pki_acme.py | 4 ++-- backend/src/ee/services/pki-acme/pki-acme-schemas.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 254ea4af4..e08212b9e 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -114,7 +114,7 @@ def step_impl(context: Context, csr_var: str): def step_impl(context: Context, csr_var: str): names = json.loads(context.text) builder: x509.CertificateSigningRequestBuilder = context.vars[csr_var] - builder.subject_name( + context.vars[csr_var] = builder.subject_name( x509.Name( [ x509.NameAttribute(getattr(NameOID, name), value) @@ -128,7 +128,7 @@ def step_impl(context: Context, csr_var: str): def step_impl(context: Context, csr_var: str): names = json.loads(context.text) builder: x509.CertificateSigningRequestBuilder = context.vars[csr_var] - builder.add_extension( + context[csr_var] = builder.add_extension( x509.SubjectAlternativeName([x509.DNSName(name) for name in names]), critical=False, ) diff --git a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts index 935776a8e..ec2197466 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts @@ -80,10 +80,10 @@ export const CreateAcmeAccountResponseSchema = z.object({ export const CreateAcmeOrderBodySchema = z.object({ identifiers: z.array( z.object({ - type: z + type: z.enum(Object.values(AcmeIdentifierType) as [string, ...string[]]), + value: z .string() - .regex(/^(?!-)[A-Za-z0-9-]{1,63}(? Date: Wed, 29 Oct 2025 18:05:24 -0700 Subject: [PATCH 117/294] Add error handling --- backend/src/ee/services/pki-acme/pki-acme-service.ts | 2 +- backend/src/server/plugins/error-handler.ts | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 098046ffc..bcb3f07c7 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -288,7 +288,7 @@ export const pkiAcmeServiceFactory = ({ await acmeOrderAuthDAL.insertMany( authorizations.map((auth) => ({ - orderId: order.id, + orderId: createdOrder.id, authId: auth.id })) ); diff --git a/backend/src/server/plugins/error-handler.ts b/backend/src/server/plugins/error-handler.ts index 8d10a8630..50251377d 100644 --- a/backend/src/server/plugins/error-handler.ts +++ b/backend/src/server/plugins/error-handler.ts @@ -5,6 +5,7 @@ import fastifyPlugin from "fastify-plugin"; import jwt from "jsonwebtoken"; import { ZodError } from "zod"; +import { AcmeError } from "@app/ee/services/pki-acme/pki-acme-errors"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, @@ -242,6 +243,16 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider error: "TokenError", message: errorMessage }); + } else if (error instanceof AcmeError) { + void res + .type("application/problem+json") + .status(error.status) + .send({ + status: error.status, + type: `urn:ietf:params:acme:error:${error.type}`, + detail: error.detail + // TODO: add subproblems if they exist + }); } else { void res.status(HttpStatusCodes.InternalServerError).send({ reqId: req.id, From 96cb47319283b6e2a527ae880310b77771460c3b Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Wed, 29 Oct 2025 18:25:26 -0700 Subject: [PATCH 118/294] Fix new order resp --- .../ee/services/pki-acme/pki-acme-service.ts | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index bcb3f07c7..f6400e181 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -208,6 +208,7 @@ export const pkiAcmeServiceFactory = ({ payload: TCreateAcmeAccountPayload; }): Promise> => { const profile = await validateAcmeProfile(profileId); + // TODO: ensure unique account per public key const existingAccount: TPkiAcmeAccounts | null = await acmeAccountDAL.findByPublicKey(profileId, alg, jwk); if (onlyReturnExisting && !existingAccount) { throw new AcmeAccountDoesNotExistError({ message: "ACME account not found" }); @@ -272,14 +273,17 @@ export const pkiAcmeServiceFactory = ({ payload.identifiers.map(async (identifier) => { if (identifier.type === AcmeIdentifierType.DNS) { // TODO: reuse existing authorizations for this identifier if they exist - return await acmeAuthDAL.create({ - accountId: account.id, - status: AcmeAuthStatus.Pending, - identifierType: identifier.type, - identifierValue: identifier.value, - // TODO: read config from the profile to get the expiration time instead - expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) - }); + return await acmeAuthDAL.create( + { + accountId: account.id, + status: AcmeAuthStatus.Pending, + identifierType: identifier.type, + identifierValue: identifier.value, + // TODO: read config from the profile to get the expiration time instead + expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) + }, + tx + ); } else { throw new AcmeMalformedError({ detail: "Only DNS identifiers are supported" }); } @@ -290,7 +294,8 @@ export const pkiAcmeServiceFactory = ({ authorizations.map((auth) => ({ orderId: createdOrder.id, authId: auth.id - })) + })), + tx ); return { ...createdOrder, authorizations, account }; }); @@ -301,18 +306,13 @@ export const pkiAcmeServiceFactory = ({ status: "pending", // TODO: read config from the profile to get the expiration time instead expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), - identifiers: order.authorizations.map((auth) => ({ + identifiers: order.authorizations.map((auth: TPkiAcmeAuths) => ({ type: auth.identifierType, value: auth.identifierValue })), - authorizations: order.authorizations.map((auth) => ({ - id: auth.id, - status: auth.status, - identifier: { - type: auth.identifierType, - value: auth.identifierValue - } - })), + authorizations: order.authorizations.map((auth: TPkiAcmeAuths) => + buildUrl(`/api/v1/pki/acme/profiles/${order.account.profileId}/authorizations/${auth.id}`) + ), finalize: buildUrl(`/api/v1/pki/acme/profiles/${order.account.profileId}/orders/${order.id}/finalize`) }, headers: { From 61af9734b4359c8e539b54ae28915e0aad1bd8ee Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Wed, 29 Oct 2025 18:33:41 -0700 Subject: [PATCH 119/294] Refactor other endpoints and service methods --- .../ee/services/pki-acme/pki-acme-service.ts | 183 ++++++++++++------ .../ee/services/pki-acme/pki-acme-types.ts | 54 +++++- 2 files changed, 173 insertions(+), 64 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index f6400e181..a72ef2b0f 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -321,96 +321,171 @@ export const pkiAcmeServiceFactory = ({ }; }; - const deactivateAcmeAccount = async ( - profileId: string, - accountId: string, - payload?: TDeactivateAcmeAccountPayload - ): Promise => { + const deactivateAcmeAccount = async ({ + profileId, + accountId, + payload: { status } = { status: "deactivated" } + }: { + profileId: string; + accountId: string; + payload?: TDeactivateAcmeAccountPayload; + }): Promise> => { const profile = await validateAcmeProfile(profileId); // FIXME: Implement ACME account deactivation return { - status: "deactivated" + status: 200, + body: { + status: "deactivated" + }, + headers: { + Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${accountId}`) + } }; }; - const listAcmeOrders = async (profileId: string, accountId: string): Promise => { + const listAcmeOrders = async ({ + profileId, + accountId + }: { + profileId: string; + accountId: string; + }): Promise> => { const profile = await validateAcmeProfile(profileId); // FIXME: Implement ACME list orders return { - orders: [] + status: 200, + body: { + orders: [] + }, + headers: { + Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${accountId}/orders`) + } }; }; - const getAcmeOrder = async (profileId: string, orderId: string): Promise => { + const getAcmeOrder = async ({ + profileId, + orderId + }: { + profileId: string; + orderId: string; + }): Promise> => { const profile = await validateAcmeProfile(profileId); // FIXME: Implement ACME get order return { - status: "pending", - expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), - identifiers: [], - authorizations: [], - finalize: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/finalize`) + status: 200, + body: { + status: "pending", + expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + identifiers: [], + authorizations: [], + finalize: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/finalize`) + }, + headers: { Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}`) } }; }; - const finalizeAcmeOrder = async ( - profileId: string, - orderId: string, - payload: TFinalizeAcmeOrderPayload - ): Promise => { + const finalizeAcmeOrder = async ({ + profileId, + orderId, + payload + }: { + profileId: string; + orderId: string; + payload: TFinalizeAcmeOrderPayload; + }): Promise> => { const profile = await validateAcmeProfile(profileId); const { csr } = payload; // FIXME: Implement ACME finalize order return { - status: "processing", - expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), - identifiers: [], - authorizations: [], - finalize: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/finalize`), - certificate: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/certificate`) + status: 200, + body: { + status: "processing", + expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + identifiers: [], + authorizations: [], + finalize: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/finalize`), + certificate: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/certificate`) + }, + headers: { + Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}`) + } }; }; - const downloadAcmeCertificate = async (profileId: string, orderId: string): Promise => { + const downloadAcmeCertificate = async ({ + profileId, + orderId + }: { + profileId: string; + orderId: string; + }): Promise> => { const profile = await validateAcmeProfile(profileId); // FIXME: Implement ACME certificate download // Return the certificate in PEM format - return "FIXME-certificate-pem"; - }; - - const getAcmeAuthorization = async (profileId: string, authzId: string): Promise => { - const profile = await validateAcmeProfile(profileId); - // FIXME: Implement ACME authorization retrieval return { - status: "pending", - expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), - identifier: { - type: "dns", - value: "FIXME-domain-name" - }, - challenges: [ - { - type: "http-01", - url: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/authorizations/${authzId}/challenges/http-01`), - status: "pending", - token: "FIXME-challenge-token" - } - ] + status: 200, + body: "FIXME-certificate-pem", + headers: { + Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/certificate`) + } }; }; - const respondToAcmeChallenge = async ( - profileId: string, - authzId: string - ): Promise => { + const getAcmeAuthorization = async ({ + profileId, + authzId + }: { + profileId: string; + authzId: string; + }): Promise> => { + const profile = await validateAcmeProfile(profileId); + // FIXME: Implement ACME authorization retrieval + return { + status: 200, + body: { + status: "pending", + expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + identifier: { + type: "dns", + value: "FIXME-domain-name" + }, + challenges: [ + { + type: "http-01", + url: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/authorizations/${authzId}/challenges/http-01`), + status: "pending", + token: "FIXME-challenge-token" + } + ] + }, + headers: { + Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/authorizations/${authzId}`) + } + }; + }; + + const respondToAcmeChallenge = async ({ + profileId, + authzId + }: { + profileId: string; + authzId: string; + }): Promise> => { const profile = await validateAcmeProfile(profileId); // FIXME: Implement ACME challenge response // Trigger verification process return { - type: "http-01", - url: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/authorizations/${authzId}/challenges/http-01`), - status: "pending", - token: "FIXME-challenge-token" + status: 200, + body: { + type: "http-01", + url: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/authorizations/${authzId}/challenges/http-01`), + status: "pending", + token: "FIXME-challenge-token" + }, + headers: { + Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/authorizations/${authzId}/challenges/http-01`) + } }; }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index 7051222fc..2a923d1a5 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -90,14 +90,48 @@ export type TPkiAcmeServiceFactory = { accountId: string, body?: TDeactivateAcmeAccountPayload ) => Promise; - listAcmeOrders: (profileId: string, accountId: string) => Promise; - getAcmeOrder: (profileId: string, orderId: string) => Promise; - finalizeAcmeOrder: ( - profileId: string, - orderId: string, - body: TFinalizeAcmeOrderPayload - ) => Promise; - downloadAcmeCertificate: (profileId: string, orderId: string) => Promise; - getAcmeAuthorization: (profileId: string, authzId: string) => Promise; - respondToAcmeChallenge: (profileId: string, authzId: string) => Promise; + listAcmeOrders: ({ + profileId, + accountId + }: { + profileId: string; + accountId: string; + }) => Promise>; + getAcmeOrder: ({ + profileId, + orderId + }: { + profileId: string; + orderId: string; + }) => Promise>; + finalizeAcmeOrder: ({ + profileId, + orderId, + payload + }: { + profileId: string; + orderId: string; + payload: TFinalizeAcmeOrderPayload; + }) => Promise>; + downloadAcmeCertificate: ({ + profileId, + orderId + }: { + profileId: string; + orderId: string; + }) => Promise>; + getAcmeAuthorization: ({ + profileId, + authzId + }: { + profileId: string; + authzId: string; + }) => Promise>; + respondToAcmeChallenge: ({ + profileId, + authzId + }: { + profileId: string; + authzId: string; + }) => Promise>; }; From dd5bd35ad405a1620390cb01bdf4c41bdb6c42ac Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Wed, 29 Oct 2025 18:36:32 -0700 Subject: [PATCH 120/294] Fix ts --- backend/src/ee/routes/v1/pki-acme-router.ts | 13 ++++++++++--- .../src/ee/services/pki-acme/pki-acme-service.ts | 5 ++--- backend/src/ee/services/pki-acme/pki-acme-types.ts | 14 +++++++++----- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index 24ae75ff5..e576eb306 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -198,9 +198,16 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { }, // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req) => { - const result = await server.services.pkiAcme.deactivateAcmeAccount(req.params.profileId, req.params.accountId); - return result; + handler: async (req, res) => { + return sendAcmeResponse( + res, + req.params.profileId, + await server.services.pkiAcme.deactivateAcmeAccount({ + profileId: req.params.profileId, + accountId: req.params.accountId, + payload: req.body + }) + ); } }); diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index a72ef2b0f..6f35aa15d 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -303,9 +303,8 @@ export const pkiAcmeServiceFactory = ({ return { status: 201, body: { - status: "pending", - // TODO: read config from the profile to get the expiration time instead - expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + status: order.status, + expires: order.expiresAt.toISOString(), identifiers: order.authorizations.map((auth: TPkiAcmeAuths) => ({ type: auth.identifierType, value: auth.identifierValue diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index 2a923d1a5..811d121ad 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -85,11 +85,15 @@ export type TPkiAcmeServiceFactory = { accountId: string; payload: TCreateAcmeOrderPayload; }) => Promise>; - deactivateAcmeAccount: ( - profileId: string, - accountId: string, - body?: TDeactivateAcmeAccountPayload - ) => Promise; + deactivateAcmeAccount: ({ + profileId, + accountId, + payload + }: { + profileId: string; + accountId: string; + payload?: TDeactivateAcmeAccountPayload; + }) => Promise>; listAcmeOrders: ({ profileId, accountId From ea4b36b0aef1afe6b4b68f8a2ed02c5b9348344d Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Wed, 29 Oct 2025 18:50:49 -0700 Subject: [PATCH 121/294] Refactor --- .../services/pki-acme/pki-acme-account-dal.ts | 4 +-- .../ee/services/pki-acme/pki-acme-schemas.ts | 2 ++ .../ee/services/pki-acme/pki-acme-service.ts | 25 +++++++++++++------ .../ee/services/pki-acme/pki-acme-types.ts | 16 ++++++++---- 4 files changed, 32 insertions(+), 15 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts index e7979826d..a460ddacc 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts @@ -26,7 +26,7 @@ export const pkiAcmeAccountDALFactory = (db: TDbClient) => { } }; - const findById = async (profileId: string, id: string, tx?: Knex) => { + const findByProjectIdAndAccountId = async (profileId: string, id: string, tx?: Knex) => { try { const account = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId, id }).first(); @@ -49,7 +49,7 @@ export const pkiAcmeAccountDALFactory = (db: TDbClient) => { return { ...pkiAcmeAccountOrm, create, - findById, + findByProjectIdAndAccountId, findByPublicKey }; }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts index ec2197466..a10476ca4 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts @@ -114,6 +114,8 @@ export const DeactivateAcmeAccountResponseSchema = z.object({ }); // List Orders endpoint +export const ListAcmeOrdersPayloadSchema = z.object({}).strict(); + export const ListAcmeOrdersResponseSchema = z.object({ orders: z.array(z.string()) }); diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 6f35aa15d..ba9197b33 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -53,7 +53,7 @@ import { type TPkiAcmeServiceFactoryDep = { certificateProfileDAL: Pick; - acmeAccountDAL: Pick; + acmeAccountDAL: Pick; acmeOrderDAL: Pick; acmeAuthDAL: Pick; acmeOrderAuthDAL: Pick; @@ -150,11 +150,17 @@ export const pkiAcmeServiceFactory = ({ ); }; - const validateExistingAccountJwsPayload = async ( - profileId: string, - rawJwsPayload: TRawJwsPayload, - schema: z.ZodSchema - ): Promise> => { + const validateExistingAccountJwsPayload = async ({ + profileId, + rawJwsPayload, + schema, + expectedAccountId + }: { + profileId: string; + rawJwsPayload: TRawJwsPayload; + schema: z.ZodSchema; + expectedAccountId?: string; + }): Promise> => { const profile = await validateAcmeProfile(profileId); const result = await validateJwsPayload( rawJwsPayload, @@ -163,7 +169,10 @@ export const pkiAcmeServiceFactory = ({ throw new AcmeMalformedError({ detail: "KID is required in the protected header" }); } const accountId = extractAccountIdFromKid(protectedHeader.kid, profileId); - const account = await acmeAccountDAL.findById(profile.id, accountId); + if (expectedAccountId && accountId !== expectedAccountId) { + throw new AcmeAccountDoesNotExistError({ message: "ACME account ID mismatch" }); + } + const account = await acmeAccountDAL.findByProjectIdAndAccountId(profile.id, accountId); if (!account) { throw new AcmeAccountDoesNotExistError({ message: "ACME account not found" }); } @@ -261,7 +270,7 @@ export const pkiAcmeServiceFactory = ({ // if we do, return the existing order const order = await acmeOrderDAL.transaction(async (tx) => { - const account = await acmeAccountDAL.findById(profileId, accountId)!; + const account = await acmeAccountDAL.findByProjectIdAndAccountId(profileId, accountId)!; const createdOrder = await acmeOrderDAL.create( { accountId: account.id, diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index 811d121ad..827e28c7e 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -58,11 +58,17 @@ export type TPkiAcmeServiceFactory = { schema: z.ZodSchema ) => Promise>; validateNewAccountJwsPayload: (rawJwsPayload: TRawJwsPayload) => Promise>; - validateExistingAccountJwsPayload: ( - profileId: string, - rawJwsPayload: TRawJwsPayload, - schema: z.ZodSchema - ) => Promise>; + validateExistingAccountJwsPayload: ({ + profileId, + rawJwsPayload, + schema, + expectedAccountId + }: { + profileId: string; + rawJwsPayload: TRawJwsPayload; + schema: z.ZodSchema; + expectedAccountId?: string; + }) => Promise>; getAcmeDirectory: (profileId: string) => Promise; getAcmeNewNonce: (profileId: string) => Promise; createAcmeAccount: ({ From c6f51cbd106353e934469b81ded04082722e5de6 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Wed, 29 Oct 2025 20:31:41 -0700 Subject: [PATCH 122/294] Implement get acme order --- .../services/pki-acme/pki-acme-order-dal.ts | 81 +++++++------------ .../ee/services/pki-acme/pki-acme-service.ts | 31 ++++--- .../ee/services/pki-acme/pki-acme-types.ts | 2 + 3 files changed, 52 insertions(+), 62 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts index fabaaf2a1..2c7ea6aee 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts @@ -1,10 +1,10 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; +import { TableName, TPkiAcmeAuths } from "@app/db/schemas"; import { TPkiAcmeOrdersInsert, TPkiAcmeOrdersUpdate } from "@app/db/schemas/pki-acme-orders"; import { DatabaseError } from "@app/lib/errors"; -import { ormify } from "@app/lib/knex"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; export type TPkiAcmeOrderDALFactory = ReturnType; @@ -51,54 +51,35 @@ export const pkiAcmeOrderDALFactory = (db: TDbClient) => { } }; - const findByAccountId = async (accountId: string, tx?: Knex) => { + const findByIdWithAuthorizations = async (id: string, tx?: Knex) => { try { - const orders = await (tx || db)(TableName.PkiAcmeOrder).where({ accountId }); + const order = await (tx || db)(TableName.PkiAcmeOrder) + .join(TableName.PkiAcmeOrderAuth, `${TableName.PkiAcmeOrderAuth}.orderId`, `${TableName.PkiAcmeOrder}.id`) + .join(TableName.PkiAcmeAuth, `${TableName.PkiAcmeOrderAuth}.authId`, `${TableName.PkiAcmeAuth}.id`) + .select( + selectAllTableCols(TableName.PkiAcmeOrder), + db.ref("id").withSchema(TableName.PkiAcmeAuth).as("authId"), + db.ref("identifierType").withSchema(TableName.PkiAcmeAuth).as("identifierType"), + db.ref("identifierValue").withSchema(TableName.PkiAcmeAuth).as("identifierValue"), + db.ref("expiresAt").withSchema(TableName.PkiAcmeAuth).as("expiresAt") + ) + .where(`${TableName.PkiAcmeOrder}.id`, id) + .first(); - return orders; + if (!order) { + return null; + } + return { + ...order, + authorizations: order.authorizations.map((auth: TPkiAcmeAuths) => ({ + id: auth.id, + identifierType: auth.identifierType, + identifierValue: auth.identifierValue, + expiresAt: auth.expiresAt + })) + }; } catch (error) { - throw new DatabaseError({ error, name: "Find PKI ACME orders by account id" }); - } - }; - - const findByStatus = async (status: string, tx?: Knex) => { - try { - const orders = await (tx || db)(TableName.PkiAcmeOrder).where({ status }); - - return orders; - } catch (error) { - throw new DatabaseError({ error, name: "Find PKI ACME orders by status" }); - } - }; - - const findByAccountIdAndStatus = async (accountId: string, status: string, tx?: Knex) => { - try { - const orders = await (tx || db)(TableName.PkiAcmeOrder).where({ accountId, status }); - - return orders; - } catch (error) { - throw new DatabaseError({ error, name: "Find PKI ACME orders by account id and status" }); - } - }; - - const deleteById = async (id: string, tx?: Knex) => { - try { - const result = await (tx || db)(TableName.PkiAcmeOrder).where({ id }).delete().returning("*"); - const [order] = result; - - return order || null; - } catch (error) { - throw new DatabaseError({ error, name: "Delete PKI ACME order by id" }); - } - }; - - const deleteByAccountId = async (accountId: string, tx?: Knex) => { - try { - const result = await (tx || db)(TableName.PkiAcmeOrder).where({ accountId }).delete().returning("*"); - - return result; - } catch (error) { - throw new DatabaseError({ error, name: "Delete PKI ACME orders by account id" }); + throw new DatabaseError({ error, name: "Find PKI ACME order by id" }); } }; @@ -107,10 +88,6 @@ export const pkiAcmeOrderDALFactory = (db: TDbClient) => { create, updateById, findById, - findByAccountId, - findByStatus, - findByAccountIdAndStatus, - deleteById, - deleteByAccountId + findByIdWithAuthorizations }; }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index ba9197b33..9b6f9d4e5 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -7,7 +7,9 @@ import { AcmeAccountDoesNotExistError, AcmeBadPublicKeyError, AcmeMalformedError, - AcmeServerInternalError + AcmeServerInternalError, + AcmeUnauthorizedError, + AcmeUnsupportedIdentifierError } from "./pki-acme-errors"; import { TPkiAcmeAccounts } from "@app/db/schemas/pki-acme-accounts"; @@ -54,7 +56,7 @@ import { type TPkiAcmeServiceFactoryDep = { certificateProfileDAL: Pick; acmeAccountDAL: Pick; - acmeOrderDAL: Pick; + acmeOrderDAL: Pick; acmeAuthDAL: Pick; acmeOrderAuthDAL: Pick; }; @@ -170,7 +172,7 @@ export const pkiAcmeServiceFactory = ({ } const accountId = extractAccountIdFromKid(protectedHeader.kid, profileId); if (expectedAccountId && accountId !== expectedAccountId) { - throw new AcmeAccountDoesNotExistError({ message: "ACME account ID mismatch" }); + throw new NotFoundError({ message: "ACME resource not found" }); } const account = await acmeAccountDAL.findByProjectIdAndAccountId(profile.id, accountId); if (!account) { @@ -294,7 +296,7 @@ export const pkiAcmeServiceFactory = ({ tx ); } else { - throw new AcmeMalformedError({ detail: "Only DNS identifiers are supported" }); + throw new AcmeUnsupportedIdentifierError({ detail: "Only DNS identifiers are supported" }); } }) ); @@ -373,20 +375,29 @@ export const pkiAcmeServiceFactory = ({ const getAcmeOrder = async ({ profileId, + accountId, orderId }: { profileId: string; + accountId: string; orderId: string; }): Promise> => { - const profile = await validateAcmeProfile(profileId); - // FIXME: Implement ACME get order + const order = await acmeOrderDAL.findByIdWithAuthorizations(orderId); + if (!order || order.accountId !== accountId) { + throw new NotFoundError({ message: "ACME order not found" }); + } return { status: 200, body: { - status: "pending", - expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), - identifiers: [], - authorizations: [], + status: order.status, + expires: order.expiresAt.toISOString(), + identifiers: order.authorizations.map((auth: TPkiAcmeAuths) => ({ + type: auth.identifierType, + value: auth.identifierValue + })), + authorizations: order.authorizations.map((auth: TPkiAcmeAuths) => + buildUrl(`/api/v1/pki/acme/profiles/${profileId}/authorizations/${auth.id}`) + ), finalize: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/finalize`) }, headers: { Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}`) } diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index 827e28c7e..e9fa4ffa0 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -109,9 +109,11 @@ export type TPkiAcmeServiceFactory = { }) => Promise>; getAcmeOrder: ({ profileId, + accountId, orderId }: { profileId: string; + accountId: string; orderId: string; }) => Promise>; finalizeAcmeOrder: ({ From 47075852944d328f360fe614fe8c46419d5ced4a Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Wed, 29 Oct 2025 20:57:18 -0700 Subject: [PATCH 123/294] Implement more stuff --- backend/src/ee/routes/v1/pki-acme-router.ts | 133 ++++++++++++++---- .../ee/services/pki-acme/pki-acme-service.ts | 29 +++- .../ee/services/pki-acme/pki-acme-types.ts | 17 ++- 3 files changed, 138 insertions(+), 41 deletions(-) diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index e576eb306..362c8b38e 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -10,15 +10,18 @@ import { DeactivateAcmeAccountBodySchema, DeactivateAcmeAccountResponseSchema, FinalizeAcmeOrderBodySchema, + FinalizeAcmeOrderResponseSchema, GetAcmeAuthorizationResponseSchema, GetAcmeDirectoryResponseSchema, GetAcmeOrderResponseSchema, + ListAcmeOrdersPayloadSchema, ListAcmeOrdersResponseSchema, RawJwsPayloadSchema, RespondToAcmeChallengeResponseSchema } from "@app/ee/services/pki-acme/pki-acme-schemas"; import { ApiDocsTags } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { AcmeAccountDoesNotExistError, AcmeMalformedError } from "@app/ee/services/pki-acme/pki-acme-errors"; export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { const sendAcmeResponse = async (res: FastifyReply, profileId: string, response: TAcmeResponse): Promise => { @@ -158,16 +161,16 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { - const { payload, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload( - req.params.profileId, - req.body, - CreateAcmeOrderBodySchema - ); + const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + profileId: req.params.profileId, + rawJwsPayload: req.body, + schema: CreateAcmeOrderBodySchema + }); return sendAcmeResponse( res, - req.params.profileId, + profileId, await server.services.pkiAcme.createAcmeOrder({ - profileId: req.params.profileId, + profileId, accountId, payload }) @@ -191,7 +194,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { profileId: z.string().uuid(), accountId: z.string() }), - body: DeactivateAcmeAccountBodySchema, + body: RawJwsPayloadSchema, response: { 200: DeactivateAcmeAccountResponseSchema } @@ -199,13 +202,19 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { + const { payload, profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + profileId: req.params.profileId, + rawJwsPayload: req.body, + schema: DeactivateAcmeAccountBodySchema, + expectedAccountId: req.params.accountId + }); return sendAcmeResponse( res, - req.params.profileId, + profileId, await server.services.pkiAcme.deactivateAcmeAccount({ - profileId: req.params.profileId, - accountId: req.params.accountId, - payload: req.body + profileId, + accountId, + payload }) ); } @@ -227,15 +236,28 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { profileId: z.string().uuid(), accountId: z.string() }), + body: RawJwsPayloadSchema, response: { 200: ListAcmeOrdersResponseSchema } }, // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req) => { - const orders = await server.services.pkiAcme.listAcmeOrders(req.params.profileId, req.params.accountId); - return orders; + handler: async (req, res) => { + const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + profileId: req.params.profileId, + rawJwsPayload: req.body, + schema: ListAcmeOrdersPayloadSchema, + expectedAccountId: req.params.accountId + }); + return sendAcmeResponse( + res, + profileId, + await server.services.pkiAcme.listAcmeOrders({ + profileId, + accountId + }) + ); } }); @@ -255,15 +277,27 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { profileId: z.string().uuid(), orderId: z.string().uuid() }), + body: RawJwsPayloadSchema, response: { 200: GetAcmeOrderResponseSchema } }, // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req) => { - const order = await server.services.pkiAcme.getAcmeOrder(req.params.profileId, req.params.orderId); - return order; + handler: async (req, res) => { + const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + profileId: req.params.profileId, + rawJwsPayload: req.body + }); + return sendAcmeResponse( + res, + profileId, + await server.services.pkiAcme.getAcmeOrder({ + profileId, + accountId, + orderId: req.params.orderId + }) + ); } }); @@ -283,13 +317,29 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { profileId: z.string().uuid(), orderId: z.string().uuid() }), - body: FinalizeAcmeOrderBodySchema + body: RawJwsPayloadSchema, + response: { + 200: FinalizeAcmeOrderResponseSchema + } }, // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req) => { - const order = await server.services.pkiAcme.finalizeAcmeOrder(req.params.profileId, req.params.orderId, req.body); - return order; + handler: async (req, res) => { + const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + profileId: req.params.profileId, + rawJwsPayload: req.body + schema: FinalizeAcmeOrderBodySchema, + }); + return sendAcmeResponse( + res, + profileId, + await server.services.pkiAcme.finalizeAcmeOrder({ + profileId, + accountId, + orderId: req.params.orderId, + payload + }) + ); } }); @@ -309,6 +359,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { profileId: z.string().uuid(), orderId: z.string().uuid() }), + body: RawJwsPayloadSchema, response: { 200: z.string() } @@ -316,12 +367,19 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { - const certificate = await server.services.pkiAcme.downloadAcmeCertificate( - req.params.profileId, - req.params.orderId + const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + profileId: req.params.profileId, + rawJwsPayload: req.body + }); + return sendAcmeResponse( + res, + profileId, + await server.services.pkiAcme.downloadAcmeCertificate({ + profileId, + accountId, + orderId: req.params.orderId + }) ); - res.header("Content-Type", "application/pem-certificate-chain"); - return certificate; } }); @@ -341,15 +399,30 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { profileId: z.string().uuid(), authzId: z.string().uuid() }), + body: RawJwsPayloadSchema, response: { 200: GetAcmeAuthorizationResponseSchema } }, // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req) => { - const authz = await server.services.pkiAcme.getAcmeAuthorization(req.params.profileId, req.params.authzId); - return authz; + handler: async (req, res) => { + const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + profileId: req.params.profileId, + rawJwsPayload: req.body + }); + if (payload !== "") { + throw new AcmeMalformedError({ detail: "Payload should be empty" }); + } + return sendAcmeResponse( + res, + profileId, + await server.services.pkiAcme.getAcmeAuthorization({ + profileId, + accountId, + authzId: req.params.authzId + }) + ); } }); diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 9b6f9d4e5..4d51e31dd 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -93,10 +93,13 @@ export const pkiAcmeServiceFactory = ({ return kid.slice(kidPrefix.length); }; - const validateJwsPayload = async ( + const validateJwsPayload = async < + TSchema extends z.ZodSchema | undefined = undefined, + T = TSchema extends z.ZodSchema ? R : string + >( rawJwsPayload: TRawJwsPayload, getJWK: (protectedHeader: JWSHeaderParameters) => Promise, - schema: z.ZodSchema + schema?: TSchema ): Promise> => { let result: FlattenedVerifyResult; try { @@ -122,8 +125,8 @@ export const pkiAcmeServiceFactory = ({ const protectedHeader = ProtectedHeaderSchema.parse(rawProtectedHeader); // TODO: consume the nonce here const decoder = new TextDecoder(); - const jsonPayload = JSON.parse(decoder.decode(rawPayload)); - const payload = schema.parse(jsonPayload); + const textPayload = decoder.decode(rawPayload); + const payload = schema ? schema.parse(JSON.parse(textPayload)) : textPayload; return { protectedHeader, payload @@ -152,7 +155,10 @@ export const pkiAcmeServiceFactory = ({ ); }; - const validateExistingAccountJwsPayload = async ({ + const validateExistingAccountJwsPayload = async < + TSchema extends z.ZodSchema | undefined = undefined, + T = TSchema extends z.ZodSchema ? R : string + >({ profileId, rawJwsPayload, schema, @@ -160,7 +166,7 @@ export const pkiAcmeServiceFactory = ({ }: { profileId: string; rawJwsPayload: TRawJwsPayload; - schema: z.ZodSchema; + schema?: TSchema; expectedAccountId?: string; }): Promise> => { const profile = await validateAcmeProfile(profileId); @@ -187,7 +193,8 @@ export const pkiAcmeServiceFactory = ({ ); return { ...result, - accountId: extractAccountIdFromKid(result.protectedHeader.kid!, profileId) + accountId: extractAccountIdFromKid(result.protectedHeader.kid!, profileId), + profileId }; }; @@ -406,10 +413,12 @@ export const pkiAcmeServiceFactory = ({ const finalizeAcmeOrder = async ({ profileId, + accountId, orderId, payload }: { profileId: string; + accountId: string; orderId: string; payload: TFinalizeAcmeOrderPayload; }): Promise> => { @@ -453,12 +462,18 @@ export const pkiAcmeServiceFactory = ({ const getAcmeAuthorization = async ({ profileId, + accountId, authzId }: { profileId: string; + accountId: string; authzId: string; }): Promise> => { const profile = await validateAcmeProfile(profileId); + const order = await acmeOrderDAL.findByIdWithAuthorizations(orderId); + if (!order || order.accountId !== accountId) { + throw new NotFoundError({ message: "ACME order not found" }); + } // FIXME: Implement ACME authorization retrieval return { status: 200, diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index e9fa4ffa0..a1f65d8f8 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -43,6 +43,7 @@ export type TJwsPayload = { payload: T; }; export type TAuthenciatedJwsPayload = TJwsPayload & { + profileId: string; accountId: string; }; export type TAcmeResponse = { @@ -52,13 +53,19 @@ export type TAcmeResponse = { }; export type TPkiAcmeServiceFactory = { - validateJwsPayload: ( + validateJwsPayload: < + TSchema extends z.ZodSchema | undefined = undefined, + T = TSchema extends z.ZodSchema ? R : string + >( rawJwsPayload: TRawJwsPayload, getJWK: (protectedHeader: JWSHeaderParameters) => Promise, - schema: z.ZodSchema + schema?: TSchema ) => Promise>; validateNewAccountJwsPayload: (rawJwsPayload: TRawJwsPayload) => Promise>; - validateExistingAccountJwsPayload: ({ + validateExistingAccountJwsPayload: < + TSchema extends z.ZodSchema | undefined = undefined, + T = TSchema extends z.ZodSchema ? R : string + >({ profileId, rawJwsPayload, schema, @@ -66,7 +73,7 @@ export type TPkiAcmeServiceFactory = { }: { profileId: string; rawJwsPayload: TRawJwsPayload; - schema: z.ZodSchema; + schema?: TSchema; expectedAccountId?: string; }) => Promise>; getAcmeDirectory: (profileId: string) => Promise; @@ -118,10 +125,12 @@ export type TPkiAcmeServiceFactory = { }) => Promise>; finalizeAcmeOrder: ({ profileId, + accountId, orderId, payload }: { profileId: string; + accountId: string; orderId: string; payload: TFinalizeAcmeOrderPayload; }) => Promise>; From 68e4e6b36ab5d7742d9ec4a7b47de6f64e94a525 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Wed, 29 Oct 2025 20:59:10 -0700 Subject: [PATCH 124/294] Add missing stuff --- backend/src/ee/services/pki-acme/pki-acme-service.ts | 3 +-- backend/src/ee/services/pki-acme/pki-acme-types.ts | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 4d51e31dd..c7138bc27 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -8,7 +8,6 @@ import { AcmeBadPublicKeyError, AcmeMalformedError, AcmeServerInternalError, - AcmeUnauthorizedError, AcmeUnsupportedIdentifierError } from "./pki-acme-errors"; @@ -23,8 +22,8 @@ import { errors, flattenedVerify, FlattenedVerifyResult, importJWK, JWSHeaderPar import { z, ZodError } from "zod"; import { TPkiAcmeAccountDALFactory } from "./pki-acme-account-dal"; import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal"; -import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal"; import { TPkiAcmeOrderAuthDALFactory } from "./pki-acme-order-auth-dal"; +import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal"; import { AcmeAuthStatus, AcmeIdentifierType, diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index a1f65d8f8..93f14de17 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -143,9 +143,11 @@ export type TPkiAcmeServiceFactory = { }) => Promise>; getAcmeAuthorization: ({ profileId, + accountId, authzId }: { profileId: string; + accountId: string; authzId: string; }) => Promise>; respondToAcmeChallenge: ({ From 011b0df4145d4c40504e11761788478754ed9976 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Wed, 29 Oct 2025 21:10:42 -0700 Subject: [PATCH 125/294] Implement a bit of get auth --- .../ee/services/pki-acme/pki-acme-auth-dal.ts | 80 +------------------ .../ee/services/pki-acme/pki-acme-service.ts | 19 +++-- 2 files changed, 10 insertions(+), 89 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-auth-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-auth-dal.ts index c06954f4a..97d258450 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-auth-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-auth-dal.ts @@ -51,88 +51,10 @@ export const pkiAcmeAuthDALFactory = (db: TDbClient) => { } }; - const findByAccountId = async (accountId: string, tx?: Knex) => { - try { - const auths = await (tx || db)(TableName.PkiAcmeAuth).where({ accountId }); - - return auths; - } catch (error) { - throw new DatabaseError({ error, name: "Find PKI ACME auths by account id" }); - } - }; - - const findByStatus = async (status: string, tx?: Knex) => { - try { - const auths = await (tx || db)(TableName.PkiAcmeAuth).where({ status }); - - return auths; - } catch (error) { - throw new DatabaseError({ error, name: "Find PKI ACME auths by status" }); - } - }; - - const findByAccountIdAndStatus = async (accountId: string, status: string, tx?: Knex) => { - try { - const auths = await (tx || db)(TableName.PkiAcmeAuth).where({ accountId, status }); - - return auths; - } catch (error) { - throw new DatabaseError({ error, name: "Find PKI ACME auths by account id and status" }); - } - }; - - const findByIdentifier = async (identifierType: string, identifierValue: string, tx?: Knex) => { - try { - const auths = await (tx || db)(TableName.PkiAcmeAuth).where({ identifierType, identifierValue }); - - return auths; - } catch (error) { - throw new DatabaseError({ error, name: "Find PKI ACME auths by identifier" }); - } - }; - - const findByCertificateId = async (certificateId: string, tx?: Knex) => { - try { - const auths = await (tx || db)(TableName.PkiAcmeAuth).where({ certificateId }); - - return auths; - } catch (error) { - throw new DatabaseError({ error, name: "Find PKI ACME auths by certificate id" }); - } - }; - - const deleteById = async (id: string, tx?: Knex) => { - try { - const result = await (tx || db)(TableName.PkiAcmeAuth).where({ id }).delete().returning("*"); - const [auth] = result; - - return auth || null; - } catch (error) { - throw new DatabaseError({ error, name: "Delete PKI ACME auth by id" }); - } - }; - - const deleteByAccountId = async (accountId: string, tx?: Knex) => { - try { - const result = await (tx || db)(TableName.PkiAcmeAuth).where({ accountId }).delete().returning("*"); - - return result; - } catch (error) { - throw new DatabaseError({ error, name: "Delete PKI ACME auths by account id" }); - } - }; - return { ...pkiAcmeAuthOrm, create, updateById, - findById, - findByAccountId, - findByStatus, - findByAccountIdAndStatus, - findByIdentifier, - findByCertificateId, - deleteById, - deleteByAccountId + findById }; }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index c7138bc27..d15992c99 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -56,7 +56,7 @@ type TPkiAcmeServiceFactoryDep = { certificateProfileDAL: Pick; acmeAccountDAL: Pick; acmeOrderDAL: Pick; - acmeAuthDAL: Pick; + acmeAuthDAL: Pick; acmeOrderAuthDAL: Pick; }; @@ -468,22 +468,21 @@ export const pkiAcmeServiceFactory = ({ accountId: string; authzId: string; }): Promise> => { - const profile = await validateAcmeProfile(profileId); - const order = await acmeOrderDAL.findByIdWithAuthorizations(orderId); - if (!order || order.accountId !== accountId) { - throw new NotFoundError({ message: "ACME order not found" }); + const auth = await acmeAuthDAL.findById(authzId); + if (!auth || auth.accountId !== accountId) { + throw new NotFoundError({ message: "ACME authorization not found" }); } - // FIXME: Implement ACME authorization retrieval return { status: 200, body: { - status: "pending", - expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + status: auth.status, + expires: auth.expiresAt.toISOString(), identifier: { - type: "dns", - value: "FIXME-domain-name" + type: auth.identifierType, + value: auth.identifierValue }, challenges: [ + // TODO: fixme { type: "http-01", url: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/authorizations/${authzId}/challenges/http-01`), From 6b85ab54a4f12b57ed3bcf121805b55ed7484470 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 09:45:15 -0700 Subject: [PATCH 126/294] Refactor code --- backend/src/db/schemas/pki-acme-orders.ts | 4 +- backend/src/ee/routes/v1/pki-acme-router.ts | 119 ++++++----- .../ee/services/pki-acme/pki-acme-schemas.ts | 36 +--- .../ee/services/pki-acme/pki-acme-service.ts | 188 ++++++++++-------- .../ee/services/pki-acme/pki-acme-types.ts | 38 ++-- 5 files changed, 188 insertions(+), 197 deletions(-) diff --git a/backend/src/db/schemas/pki-acme-orders.ts b/backend/src/db/schemas/pki-acme-orders.ts index d52c30662..5f18a3b8c 100644 --- a/backend/src/db/schemas/pki-acme-orders.ts +++ b/backend/src/db/schemas/pki-acme-orders.ts @@ -12,7 +12,9 @@ export const PkiAcmeOrdersSchema = z.object({ accountId: z.string().uuid(), status: z.string(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + notBefore: z.date().nullable().optional(), + notAfter: z.date().nullable().optional() }); export type TPkiAcmeOrders = z.infer; diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index 362c8b38e..0cb39657d 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -4,16 +4,14 @@ import { FastifyReply } from "fastify"; import { z } from "zod"; import { + AcmeOrderResourceSchema, CreateAcmeAccountResponseSchema, CreateAcmeOrderBodySchema, - CreateAcmeOrderResponseSchema, DeactivateAcmeAccountBodySchema, DeactivateAcmeAccountResponseSchema, FinalizeAcmeOrderBodySchema, - FinalizeAcmeOrderResponseSchema, GetAcmeAuthorizationResponseSchema, GetAcmeDirectoryResponseSchema, - GetAcmeOrderResponseSchema, ListAcmeOrdersPayloadSchema, ListAcmeOrdersResponseSchema, RawJwsPayloadSchema, @@ -138,46 +136,6 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { } }); - // POST /api/v1/pki/acme/profiles//new-order - // New Certificate Order (RFC 8555 Section 7.4) - server.route({ - method: "POST", - url: "/profiles/:profileId/new-order", - config: { - rateLimit: writeLimit - }, - schema: { - hide: false, - tags: [ApiDocsTags.PkiAcme], - description: "ACME New Order - apply for a new certificate", - params: z.object({ - profileId: z.string().uuid() - }), - body: RawJwsPayloadSchema, - response: { - 201: CreateAcmeOrderResponseSchema - } - }, - // TODO: replace with verify ACME signature here instead - // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req, res) => { - const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ - profileId: req.params.profileId, - rawJwsPayload: req.body, - schema: CreateAcmeOrderBodySchema - }); - return sendAcmeResponse( - res, - profileId, - await server.services.pkiAcme.createAcmeOrder({ - profileId, - accountId, - payload - }) - ); - } - }); - // POST /api/v1/pki/acme/profiles//accounts/ // Account Deactivation (RFC 8555 Section 7.3.6) server.route({ @@ -220,42 +178,41 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { } }); - // POST /api/v1/pki/acme/profiles//accounts//orders - // List Orders (RFC 8555 Section 7.1.2.1) + // POST /api/v1/pki/acme/profiles//new-order + // New Certificate Order (RFC 8555 Section 7.4) server.route({ method: "POST", - url: "/profiles/:profileId/accounts/:accountId/orders", + url: "/profiles/:profileId/new-order", config: { - rateLimit: readLimit + rateLimit: writeLimit }, schema: { hide: false, tags: [ApiDocsTags.PkiAcme], - description: "ACME List Orders - get existing orders from current account", + description: "ACME New Order - apply for a new certificate", params: z.object({ - profileId: z.string().uuid(), - accountId: z.string() + profileId: z.string().uuid() }), body: RawJwsPayloadSchema, response: { - 200: ListAcmeOrdersResponseSchema + 201: AcmeOrderResourceSchema } }, // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { - const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ profileId: req.params.profileId, rawJwsPayload: req.body, - schema: ListAcmeOrdersPayloadSchema, - expectedAccountId: req.params.accountId + schema: CreateAcmeOrderBodySchema }); return sendAcmeResponse( res, profileId, - await server.services.pkiAcme.listAcmeOrders({ + await server.services.pkiAcme.createAcmeOrder({ profileId, - accountId + accountId, + payload }) ); } @@ -279,7 +236,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { }), body: RawJwsPayloadSchema, response: { - 200: GetAcmeOrderResponseSchema + 200: AcmeOrderResourceSchema } }, // TODO: replace with verify ACME signature here instead @@ -319,16 +276,16 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { }), body: RawJwsPayloadSchema, response: { - 200: FinalizeAcmeOrderResponseSchema + 200: AcmeOrderResourceSchema } }, // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { - const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ profileId: req.params.profileId, - rawJwsPayload: req.body - schema: FinalizeAcmeOrderBodySchema, + rawJwsPayload: req.body, + schema: FinalizeAcmeOrderBodySchema }); return sendAcmeResponse( res, @@ -342,6 +299,46 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { ); } }); + // POST /api/v1/pki/acme/profiles//accounts//orders + // List Orders (RFC 8555 Section 7.1.2.1) + server.route({ + method: "POST", + url: "/profiles/:profileId/accounts/:accountId/orders", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiAcme], + description: "ACME List Orders - get existing orders from current account", + params: z.object({ + profileId: z.string().uuid(), + accountId: z.string() + }), + body: RawJwsPayloadSchema, + response: { + 200: ListAcmeOrdersResponseSchema + } + }, + // TODO: replace with verify ACME signature here instead + // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req, res) => { + const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + profileId: req.params.profileId, + rawJwsPayload: req.body, + schema: ListAcmeOrdersPayloadSchema, + expectedAccountId: req.params.accountId + }); + return sendAcmeResponse( + res, + profileId, + await server.services.pkiAcme.listAcmeOrders({ + profileId, + accountId + }) + ); + } + }); // POST /api/v1/pki/acme/profiles//orders//certificate // Download Certificate (RFC 8555 Section 7.4.2) diff --git a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts index a10476ca4..ad40ea17b 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts @@ -90,9 +90,11 @@ export const CreateAcmeOrderBodySchema = z.object({ notAfter: z.string().optional() }); -export const CreateAcmeOrderResponseSchema = z.object({ - status: z.string(), - expires: z.string(), +export const AcmeOrderResourceSchema = z.object({ + status: z.enum(Object.values(AcmeOrderStatus) as [string, ...string[]]), + expires: z.string().optional(), + notBefore: z.string().optional(), + notAfter: z.string().optional(), identifiers: z.array( z.object({ type: z.string(), @@ -120,39 +122,11 @@ export const ListAcmeOrdersResponseSchema = z.object({ orders: z.array(z.string()) }); -export const GetAcmeOrderResponseSchema = z.object({ - status: z.enum(Object.values(AcmeOrderStatus) as [string, ...string[]]), - expires: z.string().optional(), - identifiers: z.array( - z.object({ - type: z.string(), - value: z.string() - }) - ), - authorizations: z.array(z.string()), - finalize: z.string(), - certificate: z.string().optional() -}); - // Finalize Order payload schema export const FinalizeAcmeOrderBodySchema = z.object({ csr: z.string() }); -export const FinalizeAcmeOrderResponseSchema = z.object({ - status: z.enum(Object.values(AcmeOrderStatus) as [string, ...string[]]), - expires: z.string().optional(), - identifiers: z.array( - z.object({ - type: z.string(), - value: z.string() - }) - ), - authorizations: z.array(z.string()), - finalize: z.string(), - certificate: z.string().optional() -}); - export const GetAcmeAuthorizationResponseSchema = z.object({ status: z.enum(Object.values(AcmeAuthStatus) as [string, ...string[]]), expires: z.string().optional(), diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index d15992c99..49dfb72ef 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -37,16 +37,14 @@ import { TCreateAcmeAccountPayload, TCreateAcmeAccountResponse, TCreateAcmeOrderPayload, - TCreateAcmeOrderResponse, TDeactivateAcmeAccountPayload, TDeactivateAcmeAccountResponse, TFinalizeAcmeOrderPayload, - TFinalizeAcmeOrderResponse, TGetAcmeAuthorizationResponse, TGetAcmeDirectoryResponse, - TGetAcmeOrderResponse, TJwsPayload, TListAcmeOrdersResponse, + TAcmeOrderResource, TPkiAcmeServiceFactory, TRawJwsPayload, TRespondToAcmeChallengeResponse @@ -206,6 +204,36 @@ export const pkiAcmeServiceFactory = ({ }; }; + const buildAcmeOrderResource = ({ + profileId, + order + }: { + order: { + id: string; + status: string; + expiresAt: Date; + notBefore?: Date | null; + notAfter?: Date | null; + authorizations: TPkiAcmeAuths[]; + }; + profileId: string; + }) => { + return { + status: order.status, + expires: order.expiresAt.toISOString(), + notBefore: order.notBefore?.toISOString(), + notAfter: order.notAfter?.toISOString(), + identifiers: order.authorizations.map((auth: TPkiAcmeAuths) => ({ + type: auth.identifierType, + value: auth.identifierValue + })), + authorizations: order.authorizations.map((auth: TPkiAcmeAuths) => + buildUrl(`/api/v1/pki/acme/profiles/${profileId}/authorizations/${auth.id}`) + ), + finalize: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${order.id}/finalize`) + }; + }; + const getAcmeNewNonce = async (profileId: string): Promise => { const profile = await validateAcmeProfile(profileId); // FIXME: Implement ACME new nonce generation @@ -213,6 +241,9 @@ export const pkiAcmeServiceFactory = ({ return "FIXME-generate-nonce"; }; + /** -------------------------------------------------------------- + * ACME Account + * -------------------------------------------------------------- */ const createAcmeAccount = async ({ profileId, alg, @@ -251,6 +282,7 @@ export const pkiAcmeServiceFactory = ({ publicKey: jwk, emails: contact ?? [] }); + // TODO: create audit log here // TODO: check EAB authentication here return { status: 201, @@ -265,6 +297,31 @@ export const pkiAcmeServiceFactory = ({ }; }; + const deactivateAcmeAccount = async ({ + profileId, + accountId, + payload: { status } = { status: "deactivated" } + }: { + profileId: string; + accountId: string; + payload?: TDeactivateAcmeAccountPayload; + }): Promise> => { + const profile = await validateAcmeProfile(profileId); + // FIXME: Implement ACME account deactivation + return { + status: 200, + body: { + status: "deactivated" + }, + headers: { + Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${accountId}`) + } + }; + }; + + /** -------------------------------------------------------------- + * ACME Order + * -------------------------------------------------------------- */ const createAcmeOrder = async ({ profileId, accountId, @@ -273,7 +330,7 @@ export const pkiAcmeServiceFactory = ({ profileId: string; accountId: string; payload: TCreateAcmeOrderPayload; - }): Promise> => { + }): Promise> => { // TODO: check and see if we have existing orders for this account that meet the criteria // if we do, return the existing order @@ -314,47 +371,68 @@ export const pkiAcmeServiceFactory = ({ })), tx ); + // TODO: create audit log here return { ...createdOrder, authorizations, account }; }); return { status: 201, - body: { - status: order.status, - expires: order.expiresAt.toISOString(), - identifiers: order.authorizations.map((auth: TPkiAcmeAuths) => ({ - type: auth.identifierType, - value: auth.identifierValue - })), - authorizations: order.authorizations.map((auth: TPkiAcmeAuths) => - buildUrl(`/api/v1/pki/acme/profiles/${order.account.profileId}/authorizations/${auth.id}`) - ), - finalize: buildUrl(`/api/v1/pki/acme/profiles/${order.account.profileId}/orders/${order.id}/finalize`) - }, + body: buildAcmeOrderResource({ + profileId, + order + }), headers: { Location: buildUrl(`/api/v1/pki/acme/profiles/${order.account.profileId}/orders/${order.id}`) } }; }; - const deactivateAcmeAccount = async ({ + const getAcmeOrder = async ({ profileId, accountId, - payload: { status } = { status: "deactivated" } + orderId }: { profileId: string; accountId: string; - payload?: TDeactivateAcmeAccountPayload; - }): Promise> => { + orderId: string; + }): Promise> => { + const order = await acmeOrderDAL.findByIdWithAuthorizations(orderId); + if (!order || order.accountId !== accountId) { + throw new NotFoundError({ message: "ACME order not found" }); + } + return { + status: 200, + body: buildAcmeOrderResource({ profileId, order }), + headers: { Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}`) } + }; + }; + + const finalizeAcmeOrder = async ({ + profileId, + accountId, + orderId, + payload + }: { + profileId: string; + accountId: string; + orderId: string; + payload: TFinalizeAcmeOrderPayload; + }): Promise> => { const profile = await validateAcmeProfile(profileId); - // FIXME: Implement ACME account deactivation + const { csr } = payload; + // FIXME: Implement ACME finalize order return { status: 200, body: { - status: "deactivated" + status: "processing", + expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + identifiers: [], + authorizations: [], + finalize: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/finalize`), + certificate: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/certificate`) }, headers: { - Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${accountId}`) + Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}`) } }; }; @@ -379,67 +457,6 @@ export const pkiAcmeServiceFactory = ({ }; }; - const getAcmeOrder = async ({ - profileId, - accountId, - orderId - }: { - profileId: string; - accountId: string; - orderId: string; - }): Promise> => { - const order = await acmeOrderDAL.findByIdWithAuthorizations(orderId); - if (!order || order.accountId !== accountId) { - throw new NotFoundError({ message: "ACME order not found" }); - } - return { - status: 200, - body: { - status: order.status, - expires: order.expiresAt.toISOString(), - identifiers: order.authorizations.map((auth: TPkiAcmeAuths) => ({ - type: auth.identifierType, - value: auth.identifierValue - })), - authorizations: order.authorizations.map((auth: TPkiAcmeAuths) => - buildUrl(`/api/v1/pki/acme/profiles/${profileId}/authorizations/${auth.id}`) - ), - finalize: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/finalize`) - }, - headers: { Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}`) } - }; - }; - - const finalizeAcmeOrder = async ({ - profileId, - accountId, - orderId, - payload - }: { - profileId: string; - accountId: string; - orderId: string; - payload: TFinalizeAcmeOrderPayload; - }): Promise> => { - const profile = await validateAcmeProfile(profileId); - const { csr } = payload; - // FIXME: Implement ACME finalize order - return { - status: 200, - body: { - status: "processing", - expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), - identifiers: [], - authorizations: [], - finalize: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/finalize`), - certificate: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/certificate`) - }, - headers: { - Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}`) - } - }; - }; - const downloadAcmeCertificate = async ({ profileId, orderId @@ -459,6 +476,9 @@ export const pkiAcmeServiceFactory = ({ }; }; + /** -------------------------------------------------------------- + * ACME Authorization + * -------------------------------------------------------------- */ const getAcmeAuthorization = async ({ profileId, accountId, diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index 93f14de17..672300f18 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -2,17 +2,15 @@ import { z } from "zod"; import { JWSHeaderParameters } from "jose"; import { + AcmeOrderResourceSchema, CreateAcmeAccountBodySchema, CreateAcmeAccountResponseSchema, CreateAcmeOrderBodySchema, - CreateAcmeOrderResponseSchema, DeactivateAcmeAccountBodySchema, DeactivateAcmeAccountResponseSchema, FinalizeAcmeOrderBodySchema, - FinalizeAcmeOrderResponseSchema, GetAcmeAuthorizationResponseSchema, GetAcmeDirectoryResponseSchema, - GetAcmeOrderResponseSchema, ListAcmeOrdersResponseSchema, ProtectedHeaderSchema, RawJwsPayloadSchema, @@ -21,11 +19,9 @@ import { export type TGetAcmeDirectoryResponse = z.infer; export type TCreateAcmeAccountResponse = z.infer; -export type TCreateAcmeOrderResponse = z.infer; +export type TAcmeOrderResource = z.infer; export type TDeactivateAcmeAccountResponse = z.infer; export type TListAcmeOrdersResponse = z.infer; -export type TGetAcmeOrderResponse = z.infer; -export type TFinalizeAcmeOrderResponse = z.infer; export type TDownloadAcmeCertificateDTO = string; export type TGetAcmeAuthorizationResponse = z.infer; export type TRespondToAcmeChallengeResponse = z.infer; @@ -89,15 +85,6 @@ export type TPkiAcmeServiceFactory = { jwk: JsonWebKey; payload: TCreateAcmeAccountPayload; }) => Promise>; - createAcmeOrder: ({ - profileId, - accountId, - payload - }: { - profileId: string; - accountId: string; - payload: TCreateAcmeOrderPayload; - }) => Promise>; deactivateAcmeAccount: ({ profileId, accountId, @@ -107,13 +94,15 @@ export type TPkiAcmeServiceFactory = { accountId: string; payload?: TDeactivateAcmeAccountPayload; }) => Promise>; - listAcmeOrders: ({ + createAcmeOrder: ({ profileId, - accountId + accountId, + payload }: { profileId: string; accountId: string; - }) => Promise>; + payload: TCreateAcmeOrderPayload; + }) => Promise>; getAcmeOrder: ({ profileId, accountId, @@ -122,7 +111,7 @@ export type TPkiAcmeServiceFactory = { profileId: string; accountId: string; orderId: string; - }) => Promise>; + }) => Promise>; finalizeAcmeOrder: ({ profileId, accountId, @@ -133,12 +122,21 @@ export type TPkiAcmeServiceFactory = { accountId: string; orderId: string; payload: TFinalizeAcmeOrderPayload; - }) => Promise>; + }) => Promise>; + listAcmeOrders: ({ + profileId, + accountId + }: { + profileId: string; + accountId: string; + }) => Promise>; downloadAcmeCertificate: ({ profileId, + accountId, orderId }: { profileId: string; + accountId: string; orderId: string; }) => Promise>; getAcmeAuthorization: ({ From 0ed464308f49c5e631795887bc7f3f8b6d204d67 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 09:55:59 -0700 Subject: [PATCH 127/294] Use the same find by account and order id to always have valid obj ownership check --- backend/src/ee/routes/v1/pki-acme-router.ts | 6 +- .../services/pki-acme/pki-acme-order-dal.ts | 7 ++- .../ee/services/pki-acme/pki-acme-service.ts | 63 ++++++++++--------- .../ee/services/pki-acme/pki-acme-types.ts | 14 ++--- 4 files changed, 44 insertions(+), 46 deletions(-) diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index 0cb39657d..4794a125a 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -371,11 +371,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { return sendAcmeResponse( res, profileId, - await server.services.pkiAcme.downloadAcmeCertificate({ - profileId, - accountId, - orderId: req.params.orderId - }) + await server.services.pkiAcme.downloadAcmeCertificate({ profileId, accountId, orderId: req.params.orderId }) ); } }); diff --git a/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts index 2c7ea6aee..78a46b665 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts @@ -51,7 +51,7 @@ export const pkiAcmeOrderDALFactory = (db: TDbClient) => { } }; - const findByIdWithAuthorizations = async (id: string, tx?: Knex) => { + const findByAccountAndOrderIdWithAuthorizations = async (accountId: string, orderId: string, tx?: Knex) => { try { const order = await (tx || db)(TableName.PkiAcmeOrder) .join(TableName.PkiAcmeOrderAuth, `${TableName.PkiAcmeOrderAuth}.orderId`, `${TableName.PkiAcmeOrder}.id`) @@ -63,7 +63,8 @@ export const pkiAcmeOrderDALFactory = (db: TDbClient) => { db.ref("identifierValue").withSchema(TableName.PkiAcmeAuth).as("identifierValue"), db.ref("expiresAt").withSchema(TableName.PkiAcmeAuth).as("expiresAt") ) - .where(`${TableName.PkiAcmeOrder}.id`, id) + .where(`${TableName.PkiAcmeOrder}.id`, orderId) + .where(`${TableName.PkiAcmeOrder}.accountId`, accountId) .first(); if (!order) { @@ -88,6 +89,6 @@ export const pkiAcmeOrderDALFactory = (db: TDbClient) => { create, updateById, findById, - findByIdWithAuthorizations + findByAccountAndOrderIdWithAuthorizations }; }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 49dfb72ef..c68bb3895 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -53,7 +53,7 @@ import { type TPkiAcmeServiceFactoryDep = { certificateProfileDAL: Pick; acmeAccountDAL: Pick; - acmeOrderDAL: Pick; + acmeOrderDAL: Pick; acmeAuthDAL: Pick; acmeOrderAuthDAL: Pick; }; @@ -396,8 +396,8 @@ export const pkiAcmeServiceFactory = ({ accountId: string; orderId: string; }): Promise> => { - const order = await acmeOrderDAL.findByIdWithAuthorizations(orderId); - if (!order || order.accountId !== accountId) { + const order = await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId); + if (!order) { throw new NotFoundError({ message: "ACME order not found" }); } return { @@ -418,25 +418,45 @@ export const pkiAcmeServiceFactory = ({ orderId: string; payload: TFinalizeAcmeOrderPayload; }): Promise> => { - const profile = await validateAcmeProfile(profileId); + const order = await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId); + if (!order) { + throw new NotFoundError({ message: "ACME order not found" }); + } const { csr } = payload; // FIXME: Implement ACME finalize order return { status: 200, - body: { - status: "processing", - expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), - identifiers: [], - authorizations: [], - finalize: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/finalize`), - certificate: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/certificate`) - }, + body: buildAcmeOrderResource({ profileId, order }), headers: { Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}`) } }; }; + const downloadAcmeCertificate = async ({ + profileId, + accountId, + orderId + }: { + profileId: string; + accountId: string; + orderId: string; + }): Promise> => { + const order = await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId); + if (!order) { + throw new NotFoundError({ message: "ACME order not found" }); + } + // FIXME: Implement ACME certificate download + // Return the certificate in PEM format + return { + status: 200, + body: "FIXME-certificate-pem", + headers: { + Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/certificate`) + } + }; + }; + const listAcmeOrders = async ({ profileId, accountId @@ -457,25 +477,6 @@ export const pkiAcmeServiceFactory = ({ }; }; - const downloadAcmeCertificate = async ({ - profileId, - orderId - }: { - profileId: string; - orderId: string; - }): Promise> => { - const profile = await validateAcmeProfile(profileId); - // FIXME: Implement ACME certificate download - // Return the certificate in PEM format - return { - status: 200, - body: "FIXME-certificate-pem", - headers: { - Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/certificate`) - } - }; - }; - /** -------------------------------------------------------------- * ACME Authorization * -------------------------------------------------------------- */ diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index 672300f18..8fded06d4 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -123,13 +123,6 @@ export type TPkiAcmeServiceFactory = { orderId: string; payload: TFinalizeAcmeOrderPayload; }) => Promise>; - listAcmeOrders: ({ - profileId, - accountId - }: { - profileId: string; - accountId: string; - }) => Promise>; downloadAcmeCertificate: ({ profileId, accountId, @@ -139,6 +132,13 @@ export type TPkiAcmeServiceFactory = { accountId: string; orderId: string; }) => Promise>; + listAcmeOrders: ({ + profileId, + accountId + }: { + profileId: string; + accountId: string; + }) => Promise>; getAcmeAuthorization: ({ profileId, accountId, From 9196e74437a3325309db98a6e2291f23199ec1ef Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 10:07:31 -0700 Subject: [PATCH 128/294] Add more missing columns --- backend/src/db/migrations/20251029234547_add-pki-acme.ts | 5 +++++ backend/src/db/schemas/pki-acme-orders.ts | 3 ++- backend/src/ee/services/pki-acme/pki-acme-service.ts | 6 +++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/backend/src/db/migrations/20251029234547_add-pki-acme.ts b/backend/src/db/migrations/20251029234547_add-pki-acme.ts index 6390e5812..fdc481a04 100644 --- a/backend/src/db/migrations/20251029234547_add-pki-acme.ts +++ b/backend/src/db/migrations/20251029234547_add-pki-acme.ts @@ -78,6 +78,11 @@ export async function up(knex: Knex): Promise { t.uuid("accountId").notNullable(); t.foreign("accountId").references("id").inTable(TableName.PkiAcmeAccount).onDelete("CASCADE"); + t.timestamp("notBefore").nullable(); + t.timestamp("notAfter").nullable(); + + t.timestamp("expiresAt").notNullable(); + // Order status t.string("status").notNullable(); // pending, ready, processing, valid, invalid diff --git a/backend/src/db/schemas/pki-acme-orders.ts b/backend/src/db/schemas/pki-acme-orders.ts index 5f18a3b8c..5ed58e4e9 100644 --- a/backend/src/db/schemas/pki-acme-orders.ts +++ b/backend/src/db/schemas/pki-acme-orders.ts @@ -14,7 +14,8 @@ export const PkiAcmeOrdersSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), notBefore: z.date().nullable().optional(), - notAfter: z.date().nullable().optional() + notAfter: z.date().nullable().optional(), + expiresAt: z.date().nullable().optional() }); export type TPkiAcmeOrders = z.infer; diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index c68bb3895..e5336b69c 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -339,7 +339,11 @@ export const pkiAcmeServiceFactory = ({ const createdOrder = await acmeOrderDAL.create( { accountId: account.id, - status: AcmeOrderStatus.Pending + status: AcmeOrderStatus.Pending, + notBefore: payload.notBefore ? new Date(payload.notBefore) : undefined, + notAfter: payload.notAfter ? new Date(payload.notAfter) : undefined, + // TODO: read config from the profile to get the expiration time instead + expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) }, tx ); From a7f773b78fa00da56bfe13f164143e9226a848e3 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 10:10:09 -0700 Subject: [PATCH 129/294] expires should be required --- backend/src/db/schemas/pki-acme-orders.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/db/schemas/pki-acme-orders.ts b/backend/src/db/schemas/pki-acme-orders.ts index 5ed58e4e9..6d5274f06 100644 --- a/backend/src/db/schemas/pki-acme-orders.ts +++ b/backend/src/db/schemas/pki-acme-orders.ts @@ -15,7 +15,7 @@ export const PkiAcmeOrdersSchema = z.object({ updatedAt: z.date(), notBefore: z.date().nullable().optional(), notAfter: z.date().nullable().optional(), - expiresAt: z.date().nullable().optional() + expiresAt: z.date() }); export type TPkiAcmeOrders = z.infer; From 1319ccc0d3fb527ced418bd9d5d886bf95bd50fb Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 12:02:39 -0700 Subject: [PATCH 130/294] Add challenge token --- .../migrations/20251029234547_add-pki-acme.ts | 3 ++ backend/src/db/schemas/index.ts | 2 +- backend/src/db/schemas/pki-acme-auths.ts | 3 +- .../ee/services/pki-acme/pki-acme-service.ts | 31 ++++++++++--------- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/backend/src/db/migrations/20251029234547_add-pki-acme.ts b/backend/src/db/migrations/20251029234547_add-pki-acme.ts index fdc481a04..aa334cfba 100644 --- a/backend/src/db/migrations/20251029234547_add-pki-acme.ts +++ b/backend/src/db/migrations/20251029234547_add-pki-acme.ts @@ -104,6 +104,9 @@ export async function up(knex: Knex): Promise { // Authorization status t.string("status").notNullable(); // pending, valid, invalid, deactivated, expired, revoked + // Token used to validate the authorization through ACME challenge + t.timestamp("token").nullable(); + // Identifier type and value t.string("identifierType").notNullable(); // dns t.string("identifierValue").notNullable(); // domain name diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index e3db789ac..3513b95a7 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -96,8 +96,8 @@ export * from "./pki-acme-accounts"; export * from "./pki-acme-auths"; export * from "./pki-acme-challenges"; export * from "./pki-acme-enrollment-configs"; -export * from "./pki-acme-orders"; export * from "./pki-acme-order-auths"; +export * from "./pki-acme-orders"; export * from "./pki-alerts"; export * from "./pki-api-enrollment-configs"; export * from "./pki-certificate-profiles"; diff --git a/backend/src/db/schemas/pki-acme-auths.ts b/backend/src/db/schemas/pki-acme-auths.ts index de883356d..15c7a7c55 100644 --- a/backend/src/db/schemas/pki-acme-auths.ts +++ b/backend/src/db/schemas/pki-acme-auths.ts @@ -16,7 +16,8 @@ export const PkiAcmeAuthsSchema = z.object({ expiresAt: z.date(), certificateId: z.string().uuid().nullable().optional(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + token: z.string().nullable().optional() }); export type TPkiAcmeAuths = z.infer; diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index e5336b69c..35f668a8d 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -1,19 +1,11 @@ -import { getConfig } from "@app/lib/config/env"; -import { NotFoundError } from "@app/lib/errors"; - -import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; - -import { - AcmeAccountDoesNotExistError, - AcmeBadPublicKeyError, - AcmeMalformedError, - AcmeServerInternalError, - AcmeUnsupportedIdentifierError -} from "./pki-acme-errors"; - import { TPkiAcmeAccounts } from "@app/db/schemas/pki-acme-accounts"; import { TPkiAcmeAuths } from "@app/db/schemas/pki-acme-auths"; +import { getConfig } from "@app/lib/config/env"; +import { crypto } from "@app/lib/crypto/cryptography"; +import { NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; +import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; + import { EnrollmentType, TCertificateProfileWithConfigs @@ -22,6 +14,13 @@ import { errors, flattenedVerify, FlattenedVerifyResult, importJWK, JWSHeaderPar import { z, ZodError } from "zod"; import { TPkiAcmeAccountDALFactory } from "./pki-acme-account-dal"; import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal"; +import { + AcmeAccountDoesNotExistError, + AcmeBadPublicKeyError, + AcmeMalformedError, + AcmeServerInternalError, + AcmeUnsupportedIdentifierError +} from "./pki-acme-errors"; import { TPkiAcmeOrderAuthDALFactory } from "./pki-acme-order-auth-dal"; import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal"; import { @@ -32,6 +31,7 @@ import { ProtectedHeaderSchema } from "./pki-acme-schemas"; import { + TAcmeOrderResource, TAcmeResponse, TAuthenciatedJwsPayload, TCreateAcmeAccountPayload, @@ -44,7 +44,6 @@ import { TGetAcmeDirectoryResponse, TJwsPayload, TListAcmeOrdersResponse, - TAcmeOrderResource, TPkiAcmeServiceFactory, TRawJwsPayload, TRespondToAcmeChallengeResponse @@ -357,6 +356,10 @@ export const pkiAcmeServiceFactory = ({ status: AcmeAuthStatus.Pending, identifierType: identifier.type, identifierValue: identifier.value, + // RFC 8555 suggests a token with at least 128 bits of entropy + // We are using 256 bits of entropy here, should be enough for now + // ref: https://datatracker.ietf.org/doc/html/rfc8555#section-11.3 + token: crypto.randomBytes(32).toString("base64"), // TODO: read config from the profile to get the expiration time instead expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) }, From a3f2ee47f6b31f8cf73c5f6458c01746a3e01111 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 12:23:22 -0700 Subject: [PATCH 131/294] Return auth token --- backend/bdd/features/pki/acme/new-order.feature | 2 +- backend/bdd/features/steps/pki_acme.py | 6 +++--- backend/src/ee/services/pki-acme/pki-acme-service.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/bdd/features/pki/acme/new-order.feature b/backend/bdd/features/pki/acme/new-order.feature index ebc0961ee..e165fafee 100644 --- a/backend/bdd/features/pki/acme/new-order.feature +++ b/backend/bdd/features/pki/acme/new-order.feature @@ -15,4 +15,4 @@ Feature: New Order """ Then I create a RSA private key pair as cert_key Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format - Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server + Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index e08212b9e..9180d00fa 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -99,10 +99,10 @@ def step_impl(context: Context, email: str, kid: str, secret: str, account_var: @then( - "I submit the certificate signing request PEM {pem_var} certificate order to the ACME server" + "I submit the certificate signing request PEM {pem_var} certificate order to the ACME server as {order_var}" ) -def step_impl(context: Context, pem_var: str): - context.acme_order = context.acme_client.new_order(context.vars[pem_var]) +def step_impl(context: Context, pem_var: str, order_var: str): + context.vars[order_var] = context.acme_client.new_order(context.vars[pem_var]) @when("I create certificate signing request as {csr_var}") diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 35f668a8d..fe823dceb 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -515,7 +515,7 @@ export const pkiAcmeServiceFactory = ({ type: "http-01", url: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/authorizations/${authzId}/challenges/http-01`), status: "pending", - token: "FIXME-challenge-token" + token: auth.token } ] }, From 6b1147aef9c1027468e268f6dab39c480e8d8e02 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 12:33:04 -0700 Subject: [PATCH 132/294] Improve var replacement --- backend/bdd/features/steps/pki_acme.py | 29 ++++++++++++++------------ 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 9180d00fa..0974771c3 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -22,16 +22,18 @@ class AcmeProfile: self.id = id -def replace_vars(payload: dict, vars: dict): - for key, value in payload.items(): - if isinstance(value, dict): - replace_vars(value, vars) - elif isinstance(value, list): - payload[key] = [replace_vars(item, vars) for item in value] - elif isinstance(value, str): - payload[key] = value.format(**vars) - else: - payload[key] = value +def replace_vars(payload: dict | list | int | float | str, vars: dict): + if isinstance(payload, dict): + return { + replace_vars(key, vars): replace_vars(value, vars) + for key, value in payload.items() + } + elif isinstance(payload, list): + return [replace_vars(item, vars) for item in payload] + elif isinstance(payload, str): + return payload.format(**vars) + else: + return payload @given('I have an ACME cert profile as "{profile_var}"') @@ -85,8 +87,8 @@ def step_impl(context: Context, header: str): def step_impl(context: Context): payload = context.response.json() expected = json.loads(context.text) - replace_vars(expected, context.vars) - assert payload == expected, f"{payload} != {expected}" + replaced = replace_vars(expected, context.vars) + assert payload == replaced, f"{payload} != {replaced}" @then( @@ -102,7 +104,8 @@ def step_impl(context: Context, email: str, kid: str, secret: str, account_var: "I submit the certificate signing request PEM {pem_var} certificate order to the ACME server as {order_var}" ) def step_impl(context: Context, pem_var: str, order_var: str): - context.vars[order_var] = context.acme_client.new_order(context.vars[pem_var]) + order = context.acme_client.new_order(context.vars[pem_var]) + context.vars[order_var] = order @when("I create certificate signing request as {csr_var}") From 3a965bb43fd8cad88b53794936c58d2cff5070c6 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 12:42:46 -0700 Subject: [PATCH 133/294] Make bdd assert --- backend/bdd/features/pki/acme/new-order.feature | 1 + backend/bdd/features/steps/pki_acme.py | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/backend/bdd/features/pki/acme/new-order.feature b/backend/bdd/features/pki/acme/new-order.feature index e165fafee..215933bf0 100644 --- a/backend/bdd/features/pki/acme/new-order.feature +++ b/backend/bdd/features/pki/acme/new-order.feature @@ -16,3 +16,4 @@ Feature: New Order Then I create a RSA private key pair as cert_key Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order + Then the value order.uri should be true for startswith("foo") diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 0974771c3..2a845d95b 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -1,5 +1,6 @@ import json +import jq from acme import client from acme import messages from behave.runner import Context @@ -104,8 +105,7 @@ def step_impl(context: Context, email: str, kid: str, secret: str, account_var: "I submit the certificate signing request PEM {pem_var} certificate order to the ACME server as {order_var}" ) def step_impl(context: Context, pem_var: str, order_var: str): - order = context.acme_client.new_order(context.vars[pem_var]) - context.vars[order_var] = order + context.vars[order_var] = context.acme_client.new_order(context.vars[pem_var]) @when("I create certificate signing request as {csr_var}") @@ -155,3 +155,13 @@ def step_impl(context: Context, csr_var: str, pk_var: str, pem_var: str): .sign(context.vars[pk_var], hashes.SHA256()) .public_bytes(serialization.Encoding.PEM) ) + + +@then("the value {var_path} should be true for {query}") +def step_impl(context: Context, var_path: str, query: str): + parts = var_path.split(".") + value = context.vars[parts[0]] + for part in parts[1:]: + value = getattr(value, part) + result = jq.compile(query).input_value(value).first() + assert result, f"{value} does not match {query}" From 1b827439b34227dc3ecd6ecb770316111b2704ef Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 12:44:23 -0700 Subject: [PATCH 134/294] Add some asserts --- backend/bdd/features/pki/acme/new-order.feature | 2 +- backend/bdd/features/steps/pki_acme.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/bdd/features/pki/acme/new-order.feature b/backend/bdd/features/pki/acme/new-order.feature index 215933bf0..a80c3ed58 100644 --- a/backend/bdd/features/pki/acme/new-order.feature +++ b/backend/bdd/features/pki/acme/new-order.feature @@ -16,4 +16,4 @@ Feature: New Order Then I create a RSA private key pair as cert_key Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order - Then the value order.uri should be true for startswith("foo") + Then the value order.uri should be true for startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/") diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 2a845d95b..b06a36f9c 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -163,5 +163,5 @@ def step_impl(context: Context, var_path: str, query: str): value = context.vars[parts[0]] for part in parts[1:]: value = getattr(value, part) - result = jq.compile(query).input_value(value).first() + result = jq.compile(replace_vars(query, context.vars)).input_value(value).first() assert result, f"{value} does not match {query}" From 69f93db5196637b0a56ba893586344058663926d Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 12:50:56 -0700 Subject: [PATCH 135/294] Use glom instead --- backend/bdd/features/steps/pki_acme.py | 7 ++-- backend/bdd/pyproject.toml | 1 + backend/bdd/uv.lock | 46 ++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index b06a36f9c..7cf5c4980 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -7,6 +7,7 @@ from behave.runner import Context from behave import given from behave import when from behave import then +import glom from josepy.jwk import JWKRSA from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa @@ -159,9 +160,9 @@ def step_impl(context: Context, csr_var: str, pk_var: str, pem_var: str): @then("the value {var_path} should be true for {query}") def step_impl(context: Context, var_path: str, query: str): - parts = var_path.split(".") + parts = var_path.split(".", 1) value = context.vars[parts[0]] - for part in parts[1:]: - value = getattr(value, part) + if len(parts) == 2: + value = glom.glom(value, parts[1]) result = jq.compile(replace_vars(query, context.vars)).input_value(value).first() assert result, f"{value} does not match {query}" diff --git a/backend/bdd/pyproject.toml b/backend/bdd/pyproject.toml index 98b1c2f89..80e1979a1 100644 --- a/backend/bdd/pyproject.toml +++ b/backend/bdd/pyproject.toml @@ -7,6 +7,7 @@ requires-python = ">=3.12" dependencies = [ "acme>=5.1.0", "behave>=1.3.3", + "glom>=24.11.0", "httpx>=0.28.1", "josepy>=2.2.0", "jq>=1.10.0", diff --git a/backend/bdd/uv.lock b/backend/bdd/uv.lock index a05b55420..beceb0f26 100644 --- a/backend/bdd/uv.lock +++ b/backend/bdd/uv.lock @@ -32,6 +32,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, ] +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + [[package]] name = "bdd" version = "0.1.0" @@ -39,6 +48,7 @@ source = { virtual = "." } dependencies = [ { name = "acme" }, { name = "behave" }, + { name = "glom" }, { name = "httpx" }, { name = "josepy" }, { name = "jq" }, @@ -48,6 +58,7 @@ dependencies = [ requires-dist = [ { name = "acme", specifier = ">=5.1.0" }, { name = "behave", specifier = ">=1.3.3" }, + { name = "glom", specifier = ">=24.11.0" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "josepy", specifier = ">=2.2.0" }, { name = "jq", specifier = ">=1.10.0" }, @@ -70,6 +81,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/71/06f74ffed6d74525c5cd6677c97bd2df0b7649e47a249cf6a0c2038083b2/behave-1.3.3-py2.py3-none-any.whl", hash = "sha256:89bdb62af8fb9f147ce245736a5de69f025e5edfb66f1fbe16c5007493f842c0", size = 223594, upload-time = "2025-09-04T12:12:00.3Z" }, ] +[[package]] +name = "boltons" +version = "25.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/54/71a94d8e02da9a865587fb3fff100cb0fc7aa9f4d5ed9ed3a591216ddcc7/boltons-25.0.0.tar.gz", hash = "sha256:e110fbdc30b7b9868cb604e3f71d4722dd8f4dcb4a5ddd06028ba8f1ab0b5ace", size = 246294, upload-time = "2025-02-03T05:57:59.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/7f/0e961cf3908bc4c1c3e027de2794f867c6c89fb4916fc7dba295a0e80a2d/boltons-25.0.0-py3-none-any.whl", hash = "sha256:dc9fb38bf28985715497d1b54d00b62ea866eca3938938ea9043e254a3a6ca62", size = 194210, upload-time = "2025-02-03T05:57:56.705Z" }, +] + [[package]] name = "certifi" version = "2025.10.5" @@ -276,6 +296,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/51/51ae3ab3b8553ec61f6558e9a0a9e8c500a9db844f9cf00a732b19c9a6ea/cucumber_tag_expressions-8.0.0-py3-none-any.whl", hash = "sha256:bfe552226f62a4462ee91c9643582f524af84ac84952643fb09057580cbb110a", size = 9726, upload-time = "2025-10-14T17:01:26.098Z" }, ] +[[package]] +name = "face" +version = "24.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boltons" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/79/2484075a8549cd64beae697a8f664dee69a5ccf3a7439ee40c8f93c1978a/face-24.0.0.tar.gz", hash = "sha256:611e29a01ac5970f0077f9c577e746d48c082588b411b33a0dd55c4d872949f6", size = 62732, upload-time = "2024-11-02T05:24:26.095Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/47/21867c2e5fd006c8d36a560df9e32cb4f1f566b20c5dd41f5f8a2124f7de/face-24.0.0-py3-none-any.whl", hash = "sha256:0e2c17b426fa4639a4e77d1de9580f74a98f4869ba4c7c8c175b810611622cd3", size = 54742, upload-time = "2024-11-02T05:24:24.939Z" }, +] + +[[package]] +name = "glom" +version = "24.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "boltons" }, + { name = "face" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/89/b57cfbc448189426f2e01b244fbe9226b059ef5423a9d49c1d335a1f1026/glom-24.11.0.tar.gz", hash = "sha256:4325f96759a912044af7b6c6bd0dba44ad8c1eb6038aab057329661d2021bb27", size = 195120, upload-time = "2024-11-02T23:17:50.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/a2/75fd80784ec33da8d39cf885e8811a4fbc045a90db5e336b8e345e66dbb2/glom-24.11.0-py3-none-any.whl", hash = "sha256:991db7fcb4bfa9687010aa519b7b541bbe21111e70e58fdd2d7e34bbaa2c1fbd", size = 102690, upload-time = "2024-11-02T23:17:46.468Z" }, +] + [[package]] name = "h11" version = "0.16.0" From e4655532a8cb21d547ea7781a2f48b2b0fe3eaba Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 12:55:24 -0700 Subject: [PATCH 136/294] More asserts --- .../bdd/features/pki/acme/new-order.feature | 3 ++- backend/bdd/features/steps/pki_acme.py | 22 ++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/backend/bdd/features/pki/acme/new-order.feature b/backend/bdd/features/pki/acme/new-order.feature index a80c3ed58..dec1c7c42 100644 --- a/backend/bdd/features/pki/acme/new-order.feature +++ b/backend/bdd/features/pki/acme/new-order.feature @@ -16,4 +16,5 @@ Feature: New Order Then I create a RSA private key pair as cert_key Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order - Then the value order.uri should be true for startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/") + Then the value order.uri should be true for jq startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/") + Then the value order.body.status.name should be "pending" diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 7cf5c4980..cb8ee88c9 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -38,6 +38,14 @@ def replace_vars(payload: dict | list | int | float | str, vars: dict): return payload +def eval_var(context: Context, var_path: str): + parts = var_path.split(".", 1) + value = context.vars[parts[0]] + if len(parts) == 2: + value = glom.glom(value, parts[1]) + return value + + @given('I have an ACME cert profile as "{profile_var}"') def step_impl(context: Context, profile_var: str): # TODO: Fixed value for now, just to make test much easier, @@ -158,11 +166,15 @@ def step_impl(context: Context, csr_var: str, pk_var: str, pem_var: str): ) -@then("the value {var_path} should be true for {query}") +@then("the value {var_path} should be true for jq {query}") def step_impl(context: Context, var_path: str, query: str): - parts = var_path.split(".", 1) - value = context.vars[parts[0]] - if len(parts) == 2: - value = glom.glom(value, parts[1]) + value = eval_var(context, var_path) result = jq.compile(replace_vars(query, context.vars)).input_value(value).first() assert result, f"{value} does not match {query}" + + +@then("the value {var_path} should be {expected}") +def step_impl(context: Context, var_path: str, expected: str): + value = eval_var(context, var_path) + expected_value = json.loads(expected) + assert value == expected_value, f"{value:!r} does not match {expected_value:!r}" From dca819615f5c92c519cc02b4885d2d00fd50d506 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 14:02:03 -0700 Subject: [PATCH 137/294] Provide more asserts --- backend/bdd/features/steps/pki_acme.py | 33 +++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index cb8ee88c9..8e82a95a0 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -9,6 +9,7 @@ from behave import when from behave import then import glom from josepy.jwk import JWKRSA +from josepy import JSONObjectWithFields from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa from cryptography import x509 @@ -38,11 +39,14 @@ def replace_vars(payload: dict | list | int | float | str, vars: dict): return payload -def eval_var(context: Context, var_path: str): +def eval_var(context: Context, var_path: str, as_json: bool = True): parts = var_path.split(".", 1) value = context.vars[parts[0]] if len(parts) == 2: value = glom.glom(value, parts[1]) + if as_json: + if isinstance(value, JSONObjectWithFields): + value = value.to_json() return value @@ -173,8 +177,31 @@ def step_impl(context: Context, var_path: str, query: str): assert result, f"{value} does not match {query}" -@then("the value {var_path} should be {expected}") +def match_value_with_jq(context: Context, var_path: str, jq_query: str, expected: str): + value = eval_var(context, var_path) + result = jq.compile(replace_vars(jq_query, context.vars)).input_value(value).first() + expected_value = json.loads(expected) + assert result == expected_value, ( + f"{json.dumps(value)!r} with jq {jq_query!r}, the result {json.dumps(result)!r} does not match {json.dumps(expected_value)!r}" + ) + + +@then("the value {var_path} with jq {jq_query} should be equal to json") +def step_impl(context: Context, var_path: str, jq_query: str): + return match_value_with_jq( + context=context, var_path=var_path, jq_query=jq_query, expected=context.text + ) + + +@then("the value {var_path} with jq {jq_query} should be equal to {expected}") +def step_impl(context: Context, var_path: str, jq_query: str, expected: str): + return match_value_with_jq( + context=context, var_path=var_path, jq_query=jq_query, expected=expected + ) + + +@then("the value {var_path} should be equal to {expected}") def step_impl(context: Context, var_path: str, expected: str): value = eval_var(context, var_path) expected_value = json.loads(expected) - assert value == expected_value, f"{value:!r} does not match {expected_value:!r}" + assert value == expected_value, f"{value!r} does not match {expected_value!r}" From bdc8d783f1a0e425f9afa61af6017b0c431f06c2 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 14:05:11 -0700 Subject: [PATCH 138/294] More asserts --- backend/bdd/features/pki/acme/new-order.feature | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/bdd/features/pki/acme/new-order.feature b/backend/bdd/features/pki/acme/new-order.feature index dec1c7c42..35d1f23fe 100644 --- a/backend/bdd/features/pki/acme/new-order.feature +++ b/backend/bdd/features/pki/acme/new-order.feature @@ -17,4 +17,6 @@ Feature: New Order Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order Then the value order.uri should be true for jq startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/") - Then the value order.body.status.name should be "pending" + Then the value order.body with jq .status should be equal to "pending" + Then the value order.body with jq .identifiers should be equal to [{"type": "dns", "value": "localhost"}] + Then the value order.body with jq all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/")) should be equal to true From 0f3dce40265bfe8e5798f13443cbeb46a9d69b0c Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 16:19:25 -0700 Subject: [PATCH 139/294] Add test for fetch order --- .../bdd/features/pki/acme/new-order.feature | 8 ++- backend/bdd/features/steps/pki_acme.py | 62 +++++++++++++++---- 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/backend/bdd/features/pki/acme/new-order.feature b/backend/bdd/features/pki/acme/new-order.feature index 35d1f23fe..fee66245f 100644 --- a/backend/bdd/features/pki/acme/new-order.feature +++ b/backend/bdd/features/pki/acme/new-order.feature @@ -16,7 +16,13 @@ Feature: New Order Then I create a RSA private key pair as cert_key Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order - Then the value order.uri should be true for jq startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/") + Then the value order.uri with jq . should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+) Then the value order.body with jq .status should be equal to "pending" Then the value order.body with jq .identifiers should be equal to [{"type": "dns", "value": "localhost"}] + Then the value order.body with jq .finalize should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize Then the value order.body with jq all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/")) should be equal to true + Then I send an ACME post-as-get to order.uri as fetched_order + Then the value fetched_order with jq .status should be equal to "pending" + Then the value fetched_order with jq .identifiers should be equal to [{"type": "dns", "value": "localhost"}] + Then the value fetched_order with jq .finalize should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize + Then the value fetched_order with jq all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/")) should be equal to true diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 8e82a95a0..6167faaa9 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -1,6 +1,8 @@ import json +import re import jq +import requests from acme import client from acme import messages from behave.runner import Context @@ -47,6 +49,8 @@ def eval_var(context: Context, var_path: str, as_json: bool = True): if as_json: if isinstance(value, JSONObjectWithFields): value = value.to_json() + elif isinstance(value, requests.Response): + value = value.json() return value @@ -121,6 +125,12 @@ def step_impl(context: Context, pem_var: str, order_var: str): context.vars[order_var] = context.acme_client.new_order(context.vars[pem_var]) +@then("I send an ACME post-as-get to {uri_path} as {res_var}") +def step_impl(context: Context, uri_path: str, res_var: str): + uri_value = eval_var(context, uri_path) + context.vars[res_var] = context.acme_client._post_as_get(uri_value) + + @when("I create certificate signing request as {csr_var}") def step_impl(context: Context, csr_var: str): context.vars[csr_var] = x509.CertificateSigningRequestBuilder() @@ -177,26 +187,48 @@ def step_impl(context: Context, var_path: str, query: str): assert result, f"{value} does not match {query}" -def match_value_with_jq(context: Context, var_path: str, jq_query: str, expected: str): +def apply_value_with_jq(context: Context, var_path: str, jq_query: str): value = eval_var(context, var_path) - result = jq.compile(replace_vars(jq_query, context.vars)).input_value(value).first() + return value, jq.compile(replace_vars(jq_query, context.vars)).input_value( + value + ).first() + + +@then("the value {var_path} with jq {jq_query} should be equal to json") +def step_impl(context: Context, var_path: str, jq_query: str): + value, result = apply_value_with_jq( + context=context, + var_path=var_path, + jq_query=jq_query, + ) + expected_value = json.loads(context.text) + assert result == expected_value, ( + f"{json.dumps(value)!r} with jq {jq_query!r}, the result {json.dumps(result)!r} does not match {json.dumps(expected_value)!r}" + ) + + +@then("the value {var_path} with jq {jq_query} should be equal to {expected}") +def step_impl(context: Context, var_path: str, jq_query: str, expected: str): + value, result = apply_value_with_jq( + context=context, + var_path=var_path, + jq_query=jq_query, + ) expected_value = json.loads(expected) assert result == expected_value, ( f"{json.dumps(value)!r} with jq {jq_query!r}, the result {json.dumps(result)!r} does not match {json.dumps(expected_value)!r}" ) -@then("the value {var_path} with jq {jq_query} should be equal to json") -def step_impl(context: Context, var_path: str, jq_query: str): - return match_value_with_jq( - context=context, var_path=var_path, jq_query=jq_query, expected=context.text +@then("the value {var_path} with jq {jq_query} should match pattern {regex}") +def step_impl(context: Context, var_path: str, jq_query: str, regex: str): + value, result = apply_value_with_jq( + context=context, + var_path=var_path, + jq_query=jq_query, ) - - -@then("the value {var_path} with jq {jq_query} should be equal to {expected}") -def step_impl(context: Context, var_path: str, jq_query: str, expected: str): - return match_value_with_jq( - context=context, var_path=var_path, jq_query=jq_query, expected=expected + assert re.match(replace_vars(regex, context.vars), result), ( + f"{json.dumps(value)!r} with jq {jq_query!r}, the result {json.dumps(result)!r} does not match {regex!r}" ) @@ -205,3 +237,9 @@ def step_impl(context: Context, var_path: str, expected: str): value = eval_var(context, var_path) expected_value = json.loads(expected) assert value == expected_value, f"{value!r} does not match {expected_value!r}" + + +@then("I print the value {var_path}") +def step_impl(context: Context, var_path: str): + value = eval_var(context, var_path) + print(json.dumps(value.json(), indent=2)) From 3442ef190593f0abef6bd7202194c3de6994669c Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 16:22:51 -0700 Subject: [PATCH 140/294] Fix DAL --- .../bdd/features/pki/acme/new-order.feature | 17 +++++++++ .../services/pki-acme/pki-acme-order-dal.ts | 38 +++++++++++-------- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/backend/bdd/features/pki/acme/new-order.feature b/backend/bdd/features/pki/acme/new-order.feature index fee66245f..2b8115580 100644 --- a/backend/bdd/features/pki/acme/new-order.feature +++ b/backend/bdd/features/pki/acme/new-order.feature @@ -21,6 +21,23 @@ Feature: New Order Then the value order.body with jq .identifiers should be equal to [{"type": "dns", "value": "localhost"}] Then the value order.body with jq .finalize should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize Then the value order.body with jq all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/")) should be equal to true + + Scenario: Fetch an order + Given I have an ACME cert profile as "acme_profile" + When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory +# # TODO: make it I have an account already instead? + Then I register a new ACME account with email fangpen@infisical.com and EAB key id {acme_profile.eab_kid} with secret {acme_profile.eab_secret} as acme_account + When I create certificate signing request as csr + Then I add names to certificate signing request csr + """ + { + "ORGANIZATION_NAME": "Infisical Inc", + "COMMON_NAME": "localhost" + } + """ + Then I create a RSA private key pair as cert_key + Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format + Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order Then I send an ACME post-as-get to order.uri as fetched_order Then the value fetched_order with jq .status should be equal to "pending" Then the value fetched_order with jq .identifiers should be equal to [{"type": "dns", "value": "localhost"}] diff --git a/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts index 78a46b665..70089c757 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts @@ -1,10 +1,10 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TPkiAcmeAuths } from "@app/db/schemas"; +import { TableName } from "@app/db/schemas"; import { TPkiAcmeOrdersInsert, TPkiAcmeOrdersUpdate } from "@app/db/schemas/pki-acme-orders"; import { DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; export type TPkiAcmeOrderDALFactory = ReturnType; @@ -53,7 +53,7 @@ export const pkiAcmeOrderDALFactory = (db: TDbClient) => { const findByAccountAndOrderIdWithAuthorizations = async (accountId: string, orderId: string, tx?: Knex) => { try { - const order = await (tx || db)(TableName.PkiAcmeOrder) + const rows = await (tx || db)(TableName.PkiAcmeOrder) .join(TableName.PkiAcmeOrderAuth, `${TableName.PkiAcmeOrderAuth}.orderId`, `${TableName.PkiAcmeOrder}.id`) .join(TableName.PkiAcmeAuth, `${TableName.PkiAcmeOrderAuth}.authId`, `${TableName.PkiAcmeAuth}.id`) .select( @@ -61,24 +61,32 @@ export const pkiAcmeOrderDALFactory = (db: TDbClient) => { db.ref("id").withSchema(TableName.PkiAcmeAuth).as("authId"), db.ref("identifierType").withSchema(TableName.PkiAcmeAuth).as("identifierType"), db.ref("identifierValue").withSchema(TableName.PkiAcmeAuth).as("identifierValue"), - db.ref("expiresAt").withSchema(TableName.PkiAcmeAuth).as("expiresAt") + db.ref("expiresAt").withSchema(TableName.PkiAcmeAuth).as("authExpiresAt") ) .where(`${TableName.PkiAcmeOrder}.id`, orderId) .where(`${TableName.PkiAcmeOrder}.accountId`, accountId) - .first(); + .orderBy(`${TableName.PkiAcmeAuth}.identifierValue`, "asc"); - if (!order) { + if (rows.length === 0) { return null; } - return { - ...order, - authorizations: order.authorizations.map((auth: TPkiAcmeAuths) => ({ - id: auth.id, - identifierType: auth.identifierType, - identifierValue: auth.identifierValue, - expiresAt: auth.expiresAt - })) - }; + return sqlNestRelationships({ + data: rows, + key: "id", + parentMapper: (row) => row, + childrenMapper: [ + { + key: "authId", + label: "authorizations" as const, + mapper: ({ authId, identifierType, identifierValue, authExpiresAt }) => ({ + id: authId, + identifierType: identifierType, + identifierValue: identifierValue, + expiresAt: authExpiresAt + }) + } + ] + })?.[0]; } catch (error) { throw new DatabaseError({ error, name: "Find PKI ACME order by id" }); } From 8c2e1379fe77c8e0ca60f39b723f0e0f2149ddfc Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 16:24:29 -0700 Subject: [PATCH 141/294] Rename --- .../bdd/features/pki/acme/{new-order.feature => order.feature} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename backend/bdd/features/pki/acme/{new-order.feature => order.feature} (99%) diff --git a/backend/bdd/features/pki/acme/new-order.feature b/backend/bdd/features/pki/acme/order.feature similarity index 99% rename from backend/bdd/features/pki/acme/new-order.feature rename to backend/bdd/features/pki/acme/order.feature index 2b8115580..bf16c2b03 100644 --- a/backend/bdd/features/pki/acme/new-order.feature +++ b/backend/bdd/features/pki/acme/order.feature @@ -1,4 +1,4 @@ -Feature: New Order +Feature: Order Scenario: Create a new order Given I have an ACME cert profile as "acme_profile" From a1835c0624e969af095d959d207e3b7f5dbf5eab Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 16:29:26 -0700 Subject: [PATCH 142/294] More features --- backend/bdd/features/pki/acme/order.feature | 32 +++++++++++++++++++++ backend/bdd/features/steps/pki_acme.py | 7 +++++ 2 files changed, 39 insertions(+) diff --git a/backend/bdd/features/pki/acme/order.feature b/backend/bdd/features/pki/acme/order.feature index bf16c2b03..6632b39a8 100644 --- a/backend/bdd/features/pki/acme/order.feature +++ b/backend/bdd/features/pki/acme/order.feature @@ -22,6 +22,38 @@ Feature: Order Then the value order.body with jq .finalize should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize Then the value order.body with jq all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/")) should be equal to true + Scenario: Create a new order with SANs + Given I have an ACME cert profile as "acme_profile" + When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory +# # TODO: make it I have an account already instead? + Then I register a new ACME account with email fangpen@infisical.com and EAB key id {acme_profile.eab_kid} with secret {acme_profile.eab_secret} as acme_account + When I create certificate signing request as csr + Then I add names to certificate signing request csr + """ + { + "ORGANIZATION_NAME": "Infisical Inc", + "COMMON_NAME": "localhost" + } + """ + Then I add subject alternative name to certificate signing request csr + """ + [ + "example.com", + "infisical.com" + ] + """ + Then I create a RSA private key pair as cert_key + Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format + Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order + Then the value order.body with jq .identifiers should be equal to json + """ + [ + {"type": "dns", "value": "localhost"}, + {"type": "dns", "value": "example.com"}, + {"type": "dns", "value": "infisical.com"} + ] + """ + Scenario: Fetch an order Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 6167faaa9..9e364e247 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -232,6 +232,13 @@ def step_impl(context: Context, var_path: str, jq_query: str, regex: str): ) +@then("the value {var_path} should be equal to json") +def step_impl(context: Context, var_path: str): + value = eval_var(context, var_path) + expected_value = json.loads(context.text) + assert value == expected_value, f"{value!r} does not match {expected_value!r}" + + @then("the value {var_path} should be equal to {expected}") def step_impl(context: Context, var_path: str, expected: str): value = eval_var(context, var_path) From 51e6170abf895cad80c31006150735a9ddbcc908 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 16:31:25 -0700 Subject: [PATCH 143/294] Fix tests --- backend/bdd/features/steps/pki_acme.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 9e364e247..2df1adfa0 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -154,7 +154,7 @@ def step_impl(context: Context, csr_var: str): def step_impl(context: Context, csr_var: str): names = json.loads(context.text) builder: x509.CertificateSigningRequestBuilder = context.vars[csr_var] - context[csr_var] = builder.add_extension( + context.vars[csr_var] = builder.add_extension( x509.SubjectAlternativeName([x509.DNSName(name) for name in names]), critical=False, ) From 8cab927cf2f79cc5458eed1a74651f6a72a4180b Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 16:32:50 -0700 Subject: [PATCH 144/294] Sort values --- backend/bdd/features/pki/acme/order.feature | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/bdd/features/pki/acme/order.feature b/backend/bdd/features/pki/acme/order.feature index 6632b39a8..d9cf5c268 100644 --- a/backend/bdd/features/pki/acme/order.feature +++ b/backend/bdd/features/pki/acme/order.feature @@ -45,12 +45,12 @@ Feature: Order Then I create a RSA private key pair as cert_key Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order - Then the value order.body with jq .identifiers should be equal to json + Then the value order.body with jq .identifiers | sort_by(.value) should be equal to json """ [ - {"type": "dns", "value": "localhost"}, {"type": "dns", "value": "example.com"}, - {"type": "dns", "value": "infisical.com"} + {"type": "dns", "value": "infisical.com"}, + {"type": "dns", "value": "localhost"} ] """ From 4970428f1d95e642088e03f019ac6f3f92718189 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 16:33:44 -0700 Subject: [PATCH 145/294] Rename --- .../features/pki/acme/{new-account.feature => account.feature} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename backend/bdd/features/pki/acme/{new-account.feature => account.feature} (94%) diff --git a/backend/bdd/features/pki/acme/new-account.feature b/backend/bdd/features/pki/acme/account.feature similarity index 94% rename from backend/bdd/features/pki/acme/new-account.feature rename to backend/bdd/features/pki/acme/account.feature index 09e616412..1a291bfdb 100644 --- a/backend/bdd/features/pki/acme/new-account.feature +++ b/backend/bdd/features/pki/acme/account.feature @@ -1,4 +1,4 @@ -Feature: New Account +Feature: Account Scenario: Create a new account Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory From 31a70bc781985228280eb721c642aefd28abf8b7 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 16:46:52 -0700 Subject: [PATCH 146/294] Add url check --- backend/src/ee/routes/v1/pki-acme-router.ts | 12 +++- .../ee/services/pki-acme/pki-acme-service.ts | 66 ++++++++++++------- .../ee/services/pki-acme/pki-acme-types.ts | 26 ++++++-- 3 files changed, 73 insertions(+), 31 deletions(-) diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index 4794a125a..a35e54cc1 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -121,7 +121,10 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { } }, handler: async (req, res) => { - const { payload, protectedHeader } = await server.services.pkiAcme.validateNewAccountJwsPayload(req.body); + const { payload, protectedHeader } = await server.services.pkiAcme.validateNewAccountJwsPayload({ + url: req.url, + rawJwsPayload: req.body + }); const { alg, jwk } = protectedHeader; return sendAcmeResponse( res, @@ -161,6 +164,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { const { payload, profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + url: req.url, profileId: req.params.profileId, rawJwsPayload: req.body, schema: DeactivateAcmeAccountBodySchema, @@ -202,6 +206,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + url: req.url, profileId: req.params.profileId, rawJwsPayload: req.body, schema: CreateAcmeOrderBodySchema @@ -243,6 +248,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + url: req.url, profileId: req.params.profileId, rawJwsPayload: req.body }); @@ -283,6 +289,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + url: req.url, profileId: req.params.profileId, rawJwsPayload: req.body, schema: FinalizeAcmeOrderBodySchema @@ -324,6 +331,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + url: req.url, profileId: req.params.profileId, rawJwsPayload: req.body, schema: ListAcmeOrdersPayloadSchema, @@ -365,6 +373,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + url: req.url, profileId: req.params.profileId, rawJwsPayload: req.body }); @@ -401,6 +410,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + url: req.url, profileId: req.params.profileId, rawJwsPayload: req.body }); diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index fe823dceb..97e08d757 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -19,6 +19,7 @@ import { AcmeBadPublicKeyError, AcmeMalformedError, AcmeServerInternalError, + AcmeUnauthorizedError, AcmeUnsupportedIdentifierError } from "./pki-acme-errors"; import { TPkiAcmeOrderAuthDALFactory } from "./pki-acme-order-auth-dal"; @@ -92,11 +93,17 @@ export const pkiAcmeServiceFactory = ({ const validateJwsPayload = async < TSchema extends z.ZodSchema | undefined = undefined, T = TSchema extends z.ZodSchema ? R : string - >( - rawJwsPayload: TRawJwsPayload, - getJWK: (protectedHeader: JWSHeaderParameters) => Promise, - schema?: TSchema - ): Promise> => { + >({ + url, + rawJwsPayload, + getJWK, + schema + }: { + url: string; + rawJwsPayload: TRawJwsPayload; + getJWK: (protectedHeader: JWSHeaderParameters) => Promise; + schema?: TSchema; + }): Promise> => { let result: FlattenedVerifyResult; try { result = await flattenedVerify(rawJwsPayload, async (protectedHeader: JWSHeaderParameters | undefined) => { @@ -119,6 +126,9 @@ export const pkiAcmeServiceFactory = ({ const { protectedHeader: rawProtectedHeader, payload: rawPayload } = result; try { const protectedHeader = ProtectedHeaderSchema.parse(rawProtectedHeader); + if (protectedHeader.url !== url) { + throw new AcmeUnauthorizedError({ detail: "URL mismatch in the protected header" }); + } // TODO: consume the nonce here const decoder = new TextDecoder(); const textPayload = decoder.decode(rawPayload); @@ -136,39 +146,47 @@ export const pkiAcmeServiceFactory = ({ } }; - const validateNewAccountJwsPayload = async ( - rawJwsPayload: TRawJwsPayload - ): Promise> => { - return await validateJwsPayload( + const validateNewAccountJwsPayload = async ({ + url, + rawJwsPayload + }: { + url: string; + rawJwsPayload: TRawJwsPayload; + }): Promise> => { + return await validateJwsPayload({ + url, rawJwsPayload, - async (protectedHeader) => { + getJWK: async (protectedHeader) => { if (!protectedHeader.jwk) { throw new AcmeMalformedError({ detail: "JWK is required in the protected header" }); } return protectedHeader.jwk as unknown as JsonWebKey; }, - CreateAcmeAccountBodySchema - ); + schema: CreateAcmeAccountBodySchema + }); }; const validateExistingAccountJwsPayload = async < TSchema extends z.ZodSchema | undefined = undefined, T = TSchema extends z.ZodSchema ? R : string >({ + url, profileId, rawJwsPayload, schema, expectedAccountId }: { + url: string; profileId: string; rawJwsPayload: TRawJwsPayload; schema?: TSchema; expectedAccountId?: string; }): Promise> => { const profile = await validateAcmeProfile(profileId); - const result = await validateJwsPayload( + const result = await validateJwsPayload({ + url, rawJwsPayload, - async (protectedHeader) => { + getJWK: async (protectedHeader) => { if (!protectedHeader.kid) { throw new AcmeMalformedError({ detail: "KID is required in the protected header" }); } @@ -186,7 +204,7 @@ export const pkiAcmeServiceFactory = ({ return account.publicKey as JsonWebKey; }, schema - ); + }); return { ...result, accountId: extractAccountIdFromKid(result.protectedHeader.kid!, profileId), @@ -194,15 +212,6 @@ export const pkiAcmeServiceFactory = ({ }; }; - const getAcmeDirectory = async (profileId: string): Promise => { - const profile = await validateAcmeProfile(profileId); - return { - newNonce: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/new-nonce`), - newAccount: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/new-account`), - newOrder: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/new-order`) - }; - }; - const buildAcmeOrderResource = ({ profileId, order @@ -233,6 +242,15 @@ export const pkiAcmeServiceFactory = ({ }; }; + const getAcmeDirectory = async (profileId: string): Promise => { + const profile = await validateAcmeProfile(profileId); + return { + newNonce: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/new-nonce`), + newAccount: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/new-account`), + newOrder: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/new-order`) + }; + }; + const getAcmeNewNonce = async (profileId: string): Promise => { const profile = await validateAcmeProfile(profileId); // FIXME: Implement ACME new nonce generation diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index 8fded06d4..1f962a220 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -52,21 +52,35 @@ export type TPkiAcmeServiceFactory = { validateJwsPayload: < TSchema extends z.ZodSchema | undefined = undefined, T = TSchema extends z.ZodSchema ? R : string - >( - rawJwsPayload: TRawJwsPayload, - getJWK: (protectedHeader: JWSHeaderParameters) => Promise, - schema?: TSchema - ) => Promise>; - validateNewAccountJwsPayload: (rawJwsPayload: TRawJwsPayload) => Promise>; + >({ + url, + rawJwsPayload, + getJWK, + schema + }: { + url: string; + rawJwsPayload: TRawJwsPayload; + getJWK: (protectedHeader: JWSHeaderParameters) => Promise; + schema?: z.ZodSchema; + }) => Promise>; + validateNewAccountJwsPayload: ({ + url, + rawJwsPayload + }: { + url: string; + rawJwsPayload: TRawJwsPayload; + }) => Promise>; validateExistingAccountJwsPayload: < TSchema extends z.ZodSchema | undefined = undefined, T = TSchema extends z.ZodSchema ? R : string >({ + url, profileId, rawJwsPayload, schema, expectedAccountId }: { + url: string; profileId: string; rawJwsPayload: TRawJwsPayload; schema?: TSchema; From 1d19fb4788dde06045f6cb92a7e0578f4cdec283 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 16:54:09 -0700 Subject: [PATCH 147/294] Remove not needed dal methods --- .../services/pki-acme/pki-acme-account-dal.ts | 17 ----- .../ee/services/pki-acme/pki-acme-auth-dal.ts | 68 +++++++++---------- .../pki-acme/pki-acme-order-auth-dal.ts | 16 +---- .../services/pki-acme/pki-acme-order-dal.ts | 44 ------------ 4 files changed, 32 insertions(+), 113 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts index a460ddacc..bbfe042ee 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts @@ -2,7 +2,6 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; -import { TPkiAcmeAccountsInsert, TPkiAcmeAccountsUpdate } from "@app/db/schemas/pki-acme-accounts"; import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; @@ -11,21 +10,6 @@ export type TPkiAcmeAccountDALFactory = ReturnType { const pkiAcmeAccountOrm = ormify(db, TableName.PkiAcmeAccount); - const create = async (data: TPkiAcmeAccountsInsert, tx?: Knex) => { - try { - const result = await (tx || db)(TableName.PkiAcmeAccount).insert(data).returning("*"); - const [account] = result; - - if (!account) { - throw new Error("Failed to create PKI ACME account"); - } - - return account; - } catch (error) { - throw new DatabaseError({ error, name: "Create PKI ACME account" }); - } - }; - const findByProjectIdAndAccountId = async (profileId: string, id: string, tx?: Knex) => { try { const account = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId, id }).first(); @@ -48,7 +32,6 @@ export const pkiAcmeAccountDALFactory = (db: TDbClient) => { return { ...pkiAcmeAccountOrm, - create, findByProjectIdAndAccountId, findByPublicKey }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-auth-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-auth-dal.ts index 97d258450..d03275f9b 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-auth-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-auth-dal.ts @@ -2,59 +2,53 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; -import { TPkiAcmeAuthsInsert, TPkiAcmeAuthsUpdate } from "@app/db/schemas/pki-acme-auths"; import { DatabaseError } from "@app/lib/errors"; -import { ormify } from "@app/lib/knex"; +import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; export type TPkiAcmeAuthDALFactory = ReturnType; export const pkiAcmeAuthDALFactory = (db: TDbClient) => { const pkiAcmeAuthOrm = ormify(db, TableName.PkiAcmeAuth); - const create = async (data: TPkiAcmeAuthsInsert, tx?: Knex) => { + const findByAccountIdAndAuthIdWithChallenges = async (accountId: string, authId: string, tx?: Knex) => { try { - const result = await (tx || db)(TableName.PkiAcmeAuth).insert(data).returning("*"); - const [auth] = result; + const rows = await (tx || db)(TableName.PkiAcmeAuth) + .join(TableName.PkiAcmeChallenge, `${TableName.PkiAcmeChallenge}.authId`, `${TableName.PkiAcmeAuth}.id`) + .select( + selectAllTableCols(TableName.PkiAcmeAuth), + db.ref("id").withSchema(TableName.PkiAcmeChallenge).as("challengeId"), + db.ref("token").withSchema(TableName.PkiAcmeChallenge).as("challengeToken"), + db.ref("status").withSchema(TableName.PkiAcmeChallenge).as("challengeStatus") + ) + .where(`${TableName.PkiAcmeAuth}.accountId`, accountId) + .where(`${TableName.PkiAcmeAuth}.id`, authId); - if (!auth) { - throw new Error("Failed to create PKI ACME auth"); - } - - return auth; - } catch (error) { - throw new DatabaseError({ error, name: "Create PKI ACME auth" }); - } - }; - - const updateById = async (id: string, data: TPkiAcmeAuthsUpdate, tx?: Knex) => { - try { - const result = await (tx || db)(TableName.PkiAcmeAuth).where({ id }).update(data).returning("*"); - const [auth] = result; - - if (!auth) { + if (rows.length === 0) { return null; } - - return auth; + return sqlNestRelationships({ + data: rows, + key: "id", + parentMapper: (row) => row, + childrenMapper: [ + { + key: "challengeId", + label: "challenges" as const, + mapper: ({ challengeId, challengeToken, challengeStatus }) => ({ + id: challengeId, + token: challengeToken, + status: challengeStatus + }) + } + ] + })?.[0]; } catch (error) { - throw new DatabaseError({ error, name: "Update PKI ACME auth" }); - } - }; - - const findById = async (id: string, tx?: Knex) => { - try { - const auth = await (tx || db)(TableName.PkiAcmeAuth).where({ id }).first(); - - return auth || null; - } catch (error) { - throw new DatabaseError({ error, name: "Find PKI ACME auth by id" }); + throw new DatabaseError({ error, name: "Find PKI ACME auth by account id and auth id with challenges" }); } }; return { ...pkiAcmeAuthOrm, - create, - updateById, - findById + findByAccountIdAndAuthIdWithChallenges }; }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-order-auth-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-order-auth-dal.ts index 87b6f8211..5a91e6fea 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-order-auth-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-order-auth-dal.ts @@ -1,9 +1,5 @@ -import { Knex } from "knex"; - import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; -import { TPkiAcmeOrderAuthsInsert } from "@app/db/schemas/pki-acme-order-auths"; -import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; export type TPkiAcmeOrderAuthDALFactory = ReturnType; @@ -11,17 +7,7 @@ export type TPkiAcmeOrderAuthDALFactory = ReturnType { const pkiAcmeOrderAuthOrm = ormify(db, TableName.PkiAcmeOrderAuth); - const insertMany = async (rows: TPkiAcmeOrderAuthsInsert[], tx?: Knex) => { - try { - const result = await (tx || db)(TableName.PkiAcmeOrderAuth).insert(rows).returning("*"); - return result; - } catch (error) { - throw new DatabaseError({ error, name: "Insert many PKI ACME order auths" }); - } - }; - return { - ...pkiAcmeOrderAuthOrm, - insertMany + ...pkiAcmeOrderAuthOrm }; }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts index 70089c757..d3b7466e4 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts @@ -2,7 +2,6 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; -import { TPkiAcmeOrdersInsert, TPkiAcmeOrdersUpdate } from "@app/db/schemas/pki-acme-orders"; import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; @@ -11,46 +10,6 @@ export type TPkiAcmeOrderDALFactory = ReturnType; export const pkiAcmeOrderDALFactory = (db: TDbClient) => { const pkiAcmeOrderOrm = ormify(db, TableName.PkiAcmeOrder); - const create = async (data: TPkiAcmeOrdersInsert, tx?: Knex) => { - try { - const result = await (tx || db)(TableName.PkiAcmeOrder).insert(data).returning("*"); - const [order] = result; - - if (!order) { - throw new Error("Failed to create PKI ACME order"); - } - - return order; - } catch (error) { - throw new DatabaseError({ error, name: "Create PKI ACME order" }); - } - }; - - const updateById = async (id: string, data: TPkiAcmeOrdersUpdate, tx?: Knex) => { - try { - const result = await (tx || db)(TableName.PkiAcmeOrder).where({ id }).update(data).returning("*"); - const [order] = result; - - if (!order) { - return null; - } - - return order; - } catch (error) { - throw new DatabaseError({ error, name: "Update PKI ACME order" }); - } - }; - - const findById = async (id: string, tx?: Knex) => { - try { - const order = await (tx || db)(TableName.PkiAcmeOrder).where({ id }).first(); - - return order || null; - } catch (error) { - throw new DatabaseError({ error, name: "Find PKI ACME order by id" }); - } - }; - const findByAccountAndOrderIdWithAuthorizations = async (accountId: string, orderId: string, tx?: Knex) => { try { const rows = await (tx || db)(TableName.PkiAcmeOrder) @@ -94,9 +53,6 @@ export const pkiAcmeOrderDALFactory = (db: TDbClient) => { return { ...pkiAcmeOrderOrm, - create, - updateById, - findById, findByAccountAndOrderIdWithAuthorizations }; }; From e2799b934ad3c246c15d1a50b5680e650c716da2 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 16:59:21 -0700 Subject: [PATCH 148/294] Implement getting auth --- .../ee/services/pki-acme/pki-acme-auth-dal.ts | 6 ++--- .../ee/services/pki-acme/pki-acme-service.ts | 24 ++++++++++--------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-auth-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-auth-dal.ts index d03275f9b..880d54dc9 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-auth-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-auth-dal.ts @@ -17,7 +17,7 @@ export const pkiAcmeAuthDALFactory = (db: TDbClient) => { .select( selectAllTableCols(TableName.PkiAcmeAuth), db.ref("id").withSchema(TableName.PkiAcmeChallenge).as("challengeId"), - db.ref("token").withSchema(TableName.PkiAcmeChallenge).as("challengeToken"), + db.ref("type").withSchema(TableName.PkiAcmeChallenge).as("challengeType"), db.ref("status").withSchema(TableName.PkiAcmeChallenge).as("challengeStatus") ) .where(`${TableName.PkiAcmeAuth}.accountId`, accountId) @@ -34,9 +34,9 @@ export const pkiAcmeAuthDALFactory = (db: TDbClient) => { { key: "challengeId", label: "challenges" as const, - mapper: ({ challengeId, challengeToken, challengeStatus }) => ({ + mapper: ({ challengeId, challengeType, challengeStatus }) => ({ id: challengeId, - token: challengeToken, + type: challengeType, status: challengeStatus }) } diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 97e08d757..0658886a4 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -49,12 +49,13 @@ import { TRawJwsPayload, TRespondToAcmeChallengeResponse } from "./pki-acme-types"; +import { TPkiAcmeChallenges } from "@app/db/schemas"; type TPkiAcmeServiceFactoryDep = { certificateProfileDAL: Pick; acmeAccountDAL: Pick; acmeOrderDAL: Pick; - acmeAuthDAL: Pick; + acmeAuthDAL: Pick; acmeOrderAuthDAL: Pick; }; @@ -514,8 +515,8 @@ export const pkiAcmeServiceFactory = ({ accountId: string; authzId: string; }): Promise> => { - const auth = await acmeAuthDAL.findById(authzId); - if (!auth || auth.accountId !== accountId) { + const auth = await acmeAuthDAL.findByAccountIdAndAuthIdWithChallenges(accountId, authzId); + if (!auth) { throw new NotFoundError({ message: "ACME authorization not found" }); } return { @@ -527,15 +528,16 @@ export const pkiAcmeServiceFactory = ({ type: auth.identifierType, value: auth.identifierValue }, - challenges: [ - // TODO: fixme - { - type: "http-01", - url: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/authorizations/${authzId}/challenges/http-01`), - status: "pending", + challenges: auth.challenges.map((challenge: TPkiAcmeChallenges) => { + return { + type: challenge.type, + url: buildUrl( + `/api/v1/pki/acme/profiles/${profileId}/authorizations/${authzId}/challenges/${challenge.id}` + ), + status: challenge.status, token: auth.token - } - ] + }; + }) }, headers: { Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/authorizations/${authzId}`) From 744b02ee005dba58febfb4ce77a3198e2c736dd9 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 17:33:42 -0700 Subject: [PATCH 149/294] Refactor the code a bit --- backend/bdd/features/pki/acme/auth.feature | 77 +++++++++++++++++++ backend/src/ee/routes/v1/pki-acme-router.ts | 76 +++++++++--------- .../ee/services/pki-acme/pki-acme-service.ts | 17 ++-- .../ee/services/pki-acme/pki-acme-types.ts | 10 +-- backend/src/server/plugins/error-handler.ts | 1 + 5 files changed, 136 insertions(+), 45 deletions(-) create mode 100644 backend/bdd/features/pki/acme/auth.feature diff --git a/backend/bdd/features/pki/acme/auth.feature b/backend/bdd/features/pki/acme/auth.feature new file mode 100644 index 000000000..d9cf5c268 --- /dev/null +++ b/backend/bdd/features/pki/acme/auth.feature @@ -0,0 +1,77 @@ +Feature: Order + + Scenario: Create a new order + Given I have an ACME cert profile as "acme_profile" + When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory +# # TODO: make it I have an account already instead? + Then I register a new ACME account with email fangpen@infisical.com and EAB key id {acme_profile.eab_kid} with secret {acme_profile.eab_secret} as acme_account + When I create certificate signing request as csr + Then I add names to certificate signing request csr + """ + { + "ORGANIZATION_NAME": "Infisical Inc", + "COMMON_NAME": "localhost" + } + """ + Then I create a RSA private key pair as cert_key + Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format + Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order + Then the value order.uri with jq . should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+) + Then the value order.body with jq .status should be equal to "pending" + Then the value order.body with jq .identifiers should be equal to [{"type": "dns", "value": "localhost"}] + Then the value order.body with jq .finalize should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize + Then the value order.body with jq all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/")) should be equal to true + + Scenario: Create a new order with SANs + Given I have an ACME cert profile as "acme_profile" + When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory +# # TODO: make it I have an account already instead? + Then I register a new ACME account with email fangpen@infisical.com and EAB key id {acme_profile.eab_kid} with secret {acme_profile.eab_secret} as acme_account + When I create certificate signing request as csr + Then I add names to certificate signing request csr + """ + { + "ORGANIZATION_NAME": "Infisical Inc", + "COMMON_NAME": "localhost" + } + """ + Then I add subject alternative name to certificate signing request csr + """ + [ + "example.com", + "infisical.com" + ] + """ + Then I create a RSA private key pair as cert_key + Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format + Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order + Then the value order.body with jq .identifiers | sort_by(.value) should be equal to json + """ + [ + {"type": "dns", "value": "example.com"}, + {"type": "dns", "value": "infisical.com"}, + {"type": "dns", "value": "localhost"} + ] + """ + + Scenario: Fetch an order + Given I have an ACME cert profile as "acme_profile" + When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory +# # TODO: make it I have an account already instead? + Then I register a new ACME account with email fangpen@infisical.com and EAB key id {acme_profile.eab_kid} with secret {acme_profile.eab_secret} as acme_account + When I create certificate signing request as csr + Then I add names to certificate signing request csr + """ + { + "ORGANIZATION_NAME": "Infisical Inc", + "COMMON_NAME": "localhost" + } + """ + Then I create a RSA private key pair as cert_key + Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format + Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order + Then I send an ACME post-as-get to order.uri as fetched_order + Then the value fetched_order with jq .status should be equal to "pending" + Then the value fetched_order with jq .identifiers should be equal to [{"type": "dns", "value": "localhost"}] + Then the value fetched_order with jq .finalize should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize + Then the value fetched_order with jq all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/")) should be equal to true diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index a35e54cc1..37df5aa36 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -1,8 +1,9 @@ /* eslint-disable @typescript-eslint/no-floating-promises */ -import type { TAcmeResponse } from "@app/ee/services/pki-acme/pki-acme-types"; -import { FastifyReply } from "fastify"; +import type { TAcmeResponse, TAuthenciatedJwsPayload, TRawJwsPayload } from "@app/ee/services/pki-acme/pki-acme-types"; +import { FastifyReply, FastifyRequest } from "fastify"; import { z } from "zod"; +import { AcmeMalformedError } from "@app/ee/services/pki-acme/pki-acme-errors"; import { AcmeOrderResourceSchema, CreateAcmeAccountResponseSchema, @@ -19,9 +20,27 @@ import { } from "@app/ee/services/pki-acme/pki-acme-schemas"; import { ApiDocsTags } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; -import { AcmeAccountDoesNotExistError, AcmeMalformedError } from "@app/ee/services/pki-acme/pki-acme-errors"; export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { + const validateExistingAccount = async < + TSchema extends z.ZodSchema | undefined = undefined, + T = TSchema extends z.ZodSchema ? R : string + >({ + req, + schema + }: { + req: FastifyRequest<{ Params: { profileId: string; accountId?: string }; Body: TRawJwsPayload }>; + schema?: TSchema; + }): Promise> => { + return await server.services.pkiAcme.validateExistingAccountJwsPayload({ + url: new URL(req.url, `${req.protocol}://${req.hostname}`), + profileId: req.params.profileId, + rawJwsPayload: req.body, + schema, + expectedAccountId: req.params.accountId + }); + }; + const sendAcmeResponse = async (res: FastifyReply, profileId: string, response: TAcmeResponse): Promise => { res.code(response.status); for (const [key, value] of Object.entries(response.headers)) { @@ -163,12 +182,9 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { - const { payload, profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ - url: req.url, - profileId: req.params.profileId, - rawJwsPayload: req.body, - schema: DeactivateAcmeAccountBodySchema, - expectedAccountId: req.params.accountId + const { payload, profileId, accountId } = await validateExistingAccount({ + req, + schema: DeactivateAcmeAccountBodySchema }); return sendAcmeResponse( res, @@ -205,10 +221,8 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { - const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ - url: req.url, - profileId: req.params.profileId, - rawJwsPayload: req.body, + const { profileId, accountId, payload } = await validateExistingAccount({ + req, schema: CreateAcmeOrderBodySchema }); return sendAcmeResponse( @@ -247,10 +261,9 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { - const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ - url: req.url, - profileId: req.params.profileId, - rawJwsPayload: req.body + const { profileId, accountId } = await validateExistingAccount({ + req, + schema: FinalizeAcmeOrderBodySchema }); return sendAcmeResponse( res, @@ -288,10 +301,8 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { - const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ - url: req.url, - profileId: req.params.profileId, - rawJwsPayload: req.body, + const { profileId, accountId, payload } = await validateExistingAccount({ + req, schema: FinalizeAcmeOrderBodySchema }); return sendAcmeResponse( @@ -330,12 +341,9 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { - const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ - url: req.url, - profileId: req.params.profileId, - rawJwsPayload: req.body, - schema: ListAcmeOrdersPayloadSchema, - expectedAccountId: req.params.accountId + const { profileId, accountId } = await validateExistingAccount({ + req, + schema: ListAcmeOrdersPayloadSchema }); return sendAcmeResponse( res, @@ -372,10 +380,9 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { - const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ - url: req.url, - profileId: req.params.profileId, - rawJwsPayload: req.body + const { profileId, accountId } = await validateExistingAccount({ + req, + schema: FinalizeAcmeOrderBodySchema }); return sendAcmeResponse( res, @@ -409,10 +416,9 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { - const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ - url: req.url, - profileId: req.params.profileId, - rawJwsPayload: req.body + const { profileId, accountId, payload } = await validateExistingAccount({ + req, + schema: GetAcmeAuthorizationBodySchema }); if (payload !== "") { throw new AcmeMalformedError({ detail: "Payload should be empty" }); diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 0658886a4..3508d8c93 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -6,6 +6,7 @@ import { NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; +import { TPkiAcmeChallenges } from "@app/db/schemas"; import { EnrollmentType, TCertificateProfileWithConfigs @@ -17,6 +18,7 @@ import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal"; import { AcmeAccountDoesNotExistError, AcmeBadPublicKeyError, + AcmeError, AcmeMalformedError, AcmeServerInternalError, AcmeUnauthorizedError, @@ -49,7 +51,6 @@ import { TRawJwsPayload, TRespondToAcmeChallengeResponse } from "./pki-acme-types"; -import { TPkiAcmeChallenges } from "@app/db/schemas"; type TPkiAcmeServiceFactoryDep = { certificateProfileDAL: Pick; @@ -100,7 +101,7 @@ export const pkiAcmeServiceFactory = ({ getJWK, schema }: { - url: string; + url: URL; rawJwsPayload: TRawJwsPayload; getJWK: (protectedHeader: JWSHeaderParameters) => Promise; schema?: TSchema; @@ -115,6 +116,9 @@ export const pkiAcmeServiceFactory = ({ return await importJWK(jwk, protectedHeader.alg); }); } catch (error) { + if (error instanceof AcmeError) { + throw error; + } if (error instanceof ZodError) { throw new AcmeMalformedError({ detail: `Invalid JWS payload: ${error.message}` }); } @@ -127,7 +131,7 @@ export const pkiAcmeServiceFactory = ({ const { protectedHeader: rawProtectedHeader, payload: rawPayload } = result; try { const protectedHeader = ProtectedHeaderSchema.parse(rawProtectedHeader); - if (protectedHeader.url !== url) { + if (new URL(protectedHeader.url).href !== url.href) { throw new AcmeUnauthorizedError({ detail: "URL mismatch in the protected header" }); } // TODO: consume the nonce here @@ -139,6 +143,9 @@ export const pkiAcmeServiceFactory = ({ payload }; } catch (error) { + if (error instanceof AcmeError) { + throw error; + } if (error instanceof ZodError) { throw new AcmeMalformedError({ detail: `Invalid JWS payload: ${error.message}` }); } @@ -151,7 +158,7 @@ export const pkiAcmeServiceFactory = ({ url, rawJwsPayload }: { - url: string; + url: URL; rawJwsPayload: TRawJwsPayload; }): Promise> => { return await validateJwsPayload({ @@ -177,7 +184,7 @@ export const pkiAcmeServiceFactory = ({ schema, expectedAccountId }: { - url: string; + url: URL; profileId: string; rawJwsPayload: TRawJwsPayload; schema?: TSchema; diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index 1f962a220..082078f2a 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -58,16 +58,16 @@ export type TPkiAcmeServiceFactory = { getJWK, schema }: { - url: string; + url: URL; rawJwsPayload: TRawJwsPayload; getJWK: (protectedHeader: JWSHeaderParameters) => Promise; - schema?: z.ZodSchema; - }) => Promise>; + schema?: TSchema; + }) => Promise>; validateNewAccountJwsPayload: ({ url, rawJwsPayload }: { - url: string; + url: URL; rawJwsPayload: TRawJwsPayload; }) => Promise>; validateExistingAccountJwsPayload: < @@ -80,7 +80,7 @@ export type TPkiAcmeServiceFactory = { schema, expectedAccountId }: { - url: string; + url: URL; profileId: string; rawJwsPayload: TRawJwsPayload; schema?: TSchema; diff --git a/backend/src/server/plugins/error-handler.ts b/backend/src/server/plugins/error-handler.ts index 50251377d..8f6b63141 100644 --- a/backend/src/server/plugins/error-handler.ts +++ b/backend/src/server/plugins/error-handler.ts @@ -248,6 +248,7 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider .type("application/problem+json") .status(error.status) .send({ + reqId: req.id, status: error.status, type: `urn:ietf:params:acme:error:${error.type}`, detail: error.detail From 1677909416e2edb07396bd6a6aaef9023a4af688 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 18:16:06 -0700 Subject: [PATCH 150/294] Fix ts --- backend/src/ee/routes/v1/pki-acme-router.ts | 49 ++++++++++----------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index 37df5aa36..545794323 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -21,23 +21,33 @@ import { import { ApiDocsTags } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +const SharedParamsSchema = z.object({ + profileId: z.string().uuid() +}); + +export interface MyRequestInterface { + Params: { profileId: string; accountId?: string }; + Body: TRawJwsPayload; +} + export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { const validateExistingAccount = async < + R extends FastifyRequest, TSchema extends z.ZodSchema | undefined = undefined, T = TSchema extends z.ZodSchema ? R : string >({ req, schema }: { - req: FastifyRequest<{ Params: { profileId: string; accountId?: string }; Body: TRawJwsPayload }>; + req: R; schema?: TSchema; }): Promise> => { return await server.services.pkiAcme.validateExistingAccountJwsPayload({ url: new URL(req.url, `${req.protocol}://${req.hostname}`), - profileId: req.params.profileId, - rawJwsPayload: req.body, + profileId: (req.params as { profileId: string }).profileId, + rawJwsPayload: req.body as TRawJwsPayload, schema, - expectedAccountId: req.params.accountId + expectedAccountId: (req.params as { accountId?: string }).accountId }); }; @@ -131,9 +141,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME New Account - register a new account or find existing one", - params: z.object({ - profileId: z.string().uuid() - }), + params: SharedParamsSchema, body: RawJwsPayloadSchema, response: { 201: CreateAcmeAccountResponseSchema @@ -141,7 +149,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { }, handler: async (req, res) => { const { payload, protectedHeader } = await server.services.pkiAcme.validateNewAccountJwsPayload({ - url: req.url, + url: new URL(req.url, `${req.protocol}://${req.hostname}`), rawJwsPayload: req.body }); const { alg, jwk } = protectedHeader; @@ -170,8 +178,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME Account Deactivation", - params: z.object({ - profileId: z.string().uuid(), + params: SharedParamsSchema.extend({ accountId: z.string() }), body: RawJwsPayloadSchema, @@ -210,9 +217,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME New Order - apply for a new certificate", - params: z.object({ - profileId: z.string().uuid() - }), + params: SharedParamsSchema, body: RawJwsPayloadSchema, response: { 201: AcmeOrderResourceSchema @@ -249,8 +254,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME Get Order - return status and details of the order", - params: z.object({ - profileId: z.string().uuid(), + params: SharedParamsSchema.extend({ orderId: z.string().uuid() }), body: RawJwsPayloadSchema, @@ -289,8 +293,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME Finalize Order - finalize cert order by providing CSR", - params: z.object({ - profileId: z.string().uuid(), + params: SharedParamsSchema.extend({ orderId: z.string().uuid() }), body: RawJwsPayloadSchema, @@ -329,8 +332,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME List Orders - get existing orders from current account", - params: z.object({ - profileId: z.string().uuid(), + params: SharedParamsSchema.extend({ accountId: z.string() }), body: RawJwsPayloadSchema, @@ -368,8 +370,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME Download Certificate - download certificate when ready", - params: z.object({ - profileId: z.string().uuid(), + params: SharedParamsSchema.extend({ orderId: z.string().uuid() }), body: RawJwsPayloadSchema, @@ -404,8 +405,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME Identifier Authorization - get authorization info (challenges)", - params: z.object({ - profileId: z.string().uuid(), + params: SharedParamsSchema.extend({ authzId: z.string().uuid() }), body: RawJwsPayloadSchema, @@ -447,8 +447,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME Respond to Challenge - let ACME server know challenge is ready", - params: z.object({ - profileId: z.string().uuid(), + params: SharedParamsSchema.extend({ authzId: z.string().uuid() }), response: { From 33e140d6412ae96200e1f1f05e9da4858d36a4c7 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 18:17:23 -0700 Subject: [PATCH 151/294] Add auth feature --- backend/bdd/features/pki/acme/auth.feature | 62 +-------------------- backend/src/ee/routes/v1/pki-acme-router.ts | 5 +- 2 files changed, 3 insertions(+), 64 deletions(-) diff --git a/backend/bdd/features/pki/acme/auth.feature b/backend/bdd/features/pki/acme/auth.feature index d9cf5c268..401891b6e 100644 --- a/backend/bdd/features/pki/acme/auth.feature +++ b/backend/bdd/features/pki/acme/auth.feature @@ -1,6 +1,6 @@ Feature: Order - Scenario: Create a new order + Scenario: Get authorization Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory # # TODO: make it I have an account already instead? @@ -16,62 +16,4 @@ Feature: Order Then I create a RSA private key pair as cert_key Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order - Then the value order.uri with jq . should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+) - Then the value order.body with jq .status should be equal to "pending" - Then the value order.body with jq .identifiers should be equal to [{"type": "dns", "value": "localhost"}] - Then the value order.body with jq .finalize should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize - Then the value order.body with jq all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/")) should be equal to true - - Scenario: Create a new order with SANs - Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory -# # TODO: make it I have an account already instead? - Then I register a new ACME account with email fangpen@infisical.com and EAB key id {acme_profile.eab_kid} with secret {acme_profile.eab_secret} as acme_account - When I create certificate signing request as csr - Then I add names to certificate signing request csr - """ - { - "ORGANIZATION_NAME": "Infisical Inc", - "COMMON_NAME": "localhost" - } - """ - Then I add subject alternative name to certificate signing request csr - """ - [ - "example.com", - "infisical.com" - ] - """ - Then I create a RSA private key pair as cert_key - Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format - Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order - Then the value order.body with jq .identifiers | sort_by(.value) should be equal to json - """ - [ - {"type": "dns", "value": "example.com"}, - {"type": "dns", "value": "infisical.com"}, - {"type": "dns", "value": "localhost"} - ] - """ - - Scenario: Fetch an order - Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory -# # TODO: make it I have an account already instead? - Then I register a new ACME account with email fangpen@infisical.com and EAB key id {acme_profile.eab_kid} with secret {acme_profile.eab_secret} as acme_account - When I create certificate signing request as csr - Then I add names to certificate signing request csr - """ - { - "ORGANIZATION_NAME": "Infisical Inc", - "COMMON_NAME": "localhost" - } - """ - Then I create a RSA private key pair as cert_key - Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format - Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order - Then I send an ACME post-as-get to order.uri as fetched_order - Then the value fetched_order with jq .status should be equal to "pending" - Then the value fetched_order with jq .identifiers should be equal to [{"type": "dns", "value": "localhost"}] - Then the value fetched_order with jq .finalize should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize - Then the value fetched_order with jq all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/")) should be equal to true + Then the value order.authorizations[0].uri with jq . should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/(.+) diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index 545794323..95ef93d18 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -416,10 +416,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { - const { profileId, accountId, payload } = await validateExistingAccount({ - req, - schema: GetAcmeAuthorizationBodySchema - }); + const { profileId, accountId, payload } = await validateExistingAccount({ req }); if (payload !== "") { throw new AcmeMalformedError({ detail: "Payload should be empty" }); } From cb62ed86a59be15826f66ac7865a7555180b78e1 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 18:29:35 -0700 Subject: [PATCH 152/294] Fix auth dal --- backend/src/ee/services/pki-acme/pki-acme-auth-dal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-auth-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-auth-dal.ts index 880d54dc9..cef4d619f 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-auth-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-auth-dal.ts @@ -13,7 +13,7 @@ export const pkiAcmeAuthDALFactory = (db: TDbClient) => { const findByAccountIdAndAuthIdWithChallenges = async (accountId: string, authId: string, tx?: Knex) => { try { const rows = await (tx || db)(TableName.PkiAcmeAuth) - .join(TableName.PkiAcmeChallenge, `${TableName.PkiAcmeChallenge}.authId`, `${TableName.PkiAcmeAuth}.id`) + .leftJoin(TableName.PkiAcmeChallenge, `${TableName.PkiAcmeChallenge}.authId`, `${TableName.PkiAcmeAuth}.id`) .select( selectAllTableCols(TableName.PkiAcmeAuth), db.ref("id").withSchema(TableName.PkiAcmeChallenge).as("challengeId"), From 9bc22f8b3c13e06cc43a20f9be0d5b1ef394b744 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 19:19:23 -0700 Subject: [PATCH 153/294] Better glom path --- backend/bdd/features/pki/acme/auth.feature | 3 +- backend/bdd/features/steps/pki_acme.py | 57 +++++++++++++++++++++- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/backend/bdd/features/pki/acme/auth.feature b/backend/bdd/features/pki/acme/auth.feature index 401891b6e..6a542ea63 100644 --- a/backend/bdd/features/pki/acme/auth.feature +++ b/backend/bdd/features/pki/acme/auth.feature @@ -16,4 +16,5 @@ Feature: Order Then I create a RSA private key pair as cert_key Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order - Then the value order.authorizations[0].uri with jq . should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/(.+) + Then the value order.authorizations[0] with jq .uri should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/(.+) + Then the value order.authorizations[0] with jq . should be equal to {} diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 2df1adfa0..c5f8e068b 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -3,13 +3,13 @@ import re import jq import requests +import glom from acme import client from acme import messages from behave.runner import Context from behave import given from behave import when from behave import then -import glom from josepy.jwk import JWKRSA from josepy import JSONObjectWithFields from cryptography.hazmat.primitives import serialization @@ -41,11 +41,64 @@ def replace_vars(payload: dict | list | int | float | str, vars: dict): return payload +def parse_glom_path(path_str: str): + """ + Parse a glom path string with 'attr[index]' syntax into a Path object. + + Examples: + >>> parse_glom_path('authorizations[0].name') + Path('authorizations', 0, 'name') + + >>> parse_glom_path('items[1].user[0].id') + Path('items', 1, 'user', 0, 'id') + + >>> parse_glom_path('simple_attr') + Path('simple_attr') + + >>> parse_glom_path('nested.attr') + Path('nested', 'attr') + """ + # Pattern to match attr[index] or just attr + # Groups: (attr_name)(?:\[(index)\])? + pattern = r"([a-zA-Z_][a-zA-Z0-9_]*)(?:\[(\d+)\])?" + + # Split on dots, but preserve the parts + parts = [] + current_pos = 0 + + # Find all matches in the string + for match in re.finditer(pattern, path_str): + # Add any literal text before this match + if match.start() > current_pos: + before = path_str[current_pos : match.start()] + raise ValueError(f"Invalid path syntax: unexpected text '{before}'") + + attr_name = match.group(1) + index = match.group(2) + + # Add the attribute name + parts.append(attr_name) + + # Add index if present + if index is not None: + parts.append(int(index)) + + current_pos = match.end() + + # Check for trailing text + if current_pos < len(path_str): + after = path_str[current_pos:] + if after != ".": + raise ValueError(f"Invalid path syntax: unexpected text '{after}'") + + return glom.Path(*parts) + + def eval_var(context: Context, var_path: str, as_json: bool = True): parts = var_path.split(".", 1) value = context.vars[parts[0]] if len(parts) == 2: - value = glom.glom(value, parts[1]) + value = glom.glom(value, parse_glom_path(parts[1])) if as_json: if isinstance(value, JSONObjectWithFields): value = value.to_json() From b50d7157b94f2b18c041ac35208cf65ae75729aa Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 19:23:14 -0700 Subject: [PATCH 154/294] More asserts --- backend/bdd/features/pki/acme/auth.feature | 11 +++- backend/bdd/features/steps/pki_acme.py | 74 +++++++++++----------- 2 files changed, 45 insertions(+), 40 deletions(-) diff --git a/backend/bdd/features/pki/acme/auth.feature b/backend/bdd/features/pki/acme/auth.feature index 6a542ea63..e81d3dd61 100644 --- a/backend/bdd/features/pki/acme/auth.feature +++ b/backend/bdd/features/pki/acme/auth.feature @@ -16,5 +16,12 @@ Feature: Order Then I create a RSA private key pair as cert_key Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order - Then the value order.authorizations[0] with jq .uri should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/(.+) - Then the value order.authorizations[0] with jq . should be equal to {} + Then the value order.authorizations[0].uri with jq . should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/(.+) + Then the value order.authorizations[0].body with jq .status should be equal to "pending" + Then the value order.authorizations[0].body with jq .identifier should be equal to json + """ + { + "type": "dns", + "value": "localhost" + } + """ diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index c5f8e068b..db9e8a28a 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -41,55 +41,53 @@ def replace_vars(payload: dict | list | int | float | str, vars: dict): return payload -def parse_glom_path(path_str: str): +def parse_glom_path(path_str: str) -> glom.Path: """ Parse a glom path string with 'attr[index]' syntax into a Path object. Examples: - >>> parse_glom_path('authorizations[0].name') - Path('authorizations', 0, 'name') - - >>> parse_glom_path('items[1].user[0].id') - Path('items', 1, 'user', 0, 'id') - - >>> parse_glom_path('simple_attr') - Path('simple_attr') - - >>> parse_glom_path('nested.attr') - Path('nested', 'attr') + >>> parse_glom_path('authorizations[0]') == Path('authorizations', 0) + True + >>> parse_glom_path('data.items[1].name') == Path('data', 'items', 1, 'name') + True + >>> parse_glom_path('user.addresses[0].street') == Path('user', 'addresses', 0, 'street') + True """ - # Pattern to match attr[index] or just attr - # Groups: (attr_name)(?:\[(index)\])? - pattern = r"([a-zA-Z_][a-zA-Z0-9_]*)(?:\[(\d+)\])?" - - # Split on dots, but preserve the parts parts = [] - current_pos = 0 - # Find all matches in the string - for match in re.finditer(pattern, path_str): - # Add any literal text before this match - if match.start() > current_pos: - before = path_str[current_pos : match.start()] - raise ValueError(f"Invalid path syntax: unexpected text '{before}'") + # Split by dots, but preserve bracketed content + tokens = re.split(r"(? Date: Thu, 30 Oct 2025 19:29:49 -0700 Subject: [PATCH 155/294] Add challenge --- backend/bdd/features/pki/acme/auth.feature | 1 + backend/src/ee/routes/v1/pki-acme-router.ts | 26 +++++++------------ .../pki-acme/pki-acme-challenge-dal.ts | 13 ++++++++++ .../ee/services/pki-acme/pki-acme-schemas.ts | 10 +++++-- .../ee/services/pki-acme/pki-acme-service.ts | 17 ++++++++++-- 5 files changed, 47 insertions(+), 20 deletions(-) create mode 100644 backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts diff --git a/backend/bdd/features/pki/acme/auth.feature b/backend/bdd/features/pki/acme/auth.feature index e81d3dd61..7252d868f 100644 --- a/backend/bdd/features/pki/acme/auth.feature +++ b/backend/bdd/features/pki/acme/auth.feature @@ -18,6 +18,7 @@ Feature: Order Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order Then the value order.authorizations[0].uri with jq . should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/(.+) Then the value order.authorizations[0].body with jq .status should be equal to "pending" + Then the value order.authorizations[0].body with jq .challenge should be equal to "pending" Then the value order.authorizations[0].body with jq .identifier should be equal to json """ { diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index 95ef93d18..6935150ac 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -186,8 +186,6 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { 200: DeactivateAcmeAccountResponseSchema } }, - // TODO: replace with verify ACME signature here instead - // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { const { payload, profileId, accountId } = await validateExistingAccount({ req, @@ -223,8 +221,6 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { 201: AcmeOrderResourceSchema } }, - // TODO: replace with verify ACME signature here instead - // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { const { profileId, accountId, payload } = await validateExistingAccount({ req, @@ -262,8 +258,6 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { 200: AcmeOrderResourceSchema } }, - // TODO: replace with verify ACME signature here instead - // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { const { profileId, accountId } = await validateExistingAccount({ req, @@ -301,8 +295,6 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { 200: AcmeOrderResourceSchema } }, - // TODO: replace with verify ACME signature here instead - // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { const { profileId, accountId, payload } = await validateExistingAccount({ req, @@ -378,8 +370,6 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { 200: z.string() } }, - // TODO: replace with verify ACME signature here instead - // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { const { profileId, accountId } = await validateExistingAccount({ req, @@ -413,8 +403,6 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { 200: GetAcmeAuthorizationResponseSchema } }, - // TODO: replace with verify ACME signature here instead - // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { const { profileId, accountId, payload } = await validateExistingAccount({ req }); if (payload !== "") { @@ -451,10 +439,16 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { 200: RespondToAcmeChallengeResponseSchema } }, - // TODO: replace with verify ACME signature here instead - // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req) => { - const challenge = await server.services.pkiAcme.respondToAcmeChallenge(req.params.profileId, req.params.authzId); + handler: async (req, res) => { + const { profileId, accountId, payload } = await validateExistingAccount({ req }); + if (payload !== "") { + throw new AcmeMalformedError({ detail: "Payload should be empty" }); + } + return sendAcmeResponse( + res, + profileId, + await server.services.pkiAcme.respondToAcmeChallenge({ profileId, authzId: req.params.authzId }) + ); return challenge; } }); diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts new file mode 100644 index 000000000..77a585efd --- /dev/null +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts @@ -0,0 +1,13 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TPkiAcmeChallengeDALFactory = ReturnType; + +export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { + const pkiAcmeChallengeOrm = ormify(db, TableName.PkiAcmeChallenge); + + return { + ...pkiAcmeChallengeOrm + }; +}; diff --git a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts index ad40ea17b..e09aa81c8 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts @@ -21,6 +21,12 @@ export enum AcmeAuthStatus { Revoked = "revoked" } +export enum AcmeChallengeType { + HTTP_01 = "http-01", + DNS_01 = "dns-01", + TLS_ALPN_01 = "tls-alpn-01" +} + export const ProtectedHeaderSchema = z .object({ alg: z.string(), @@ -136,7 +142,7 @@ export const GetAcmeAuthorizationResponseSchema = z.object({ }), challenges: z.array( z.object({ - type: z.string(), + type: z.enum(Object.values(AcmeChallengeType) as [string, ...string[]]), url: z.string(), status: z.string(), token: z.string(), @@ -146,7 +152,7 @@ export const GetAcmeAuthorizationResponseSchema = z.object({ }); export const RespondToAcmeChallengeResponseSchema = z.object({ - type: z.string(), + type: z.enum(Object.values(AcmeChallengeType) as [string, ...string[]]), url: z.string(), status: z.string(), token: z.string(), diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 3508d8c93..679457814 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -15,6 +15,7 @@ import { errors, flattenedVerify, FlattenedVerifyResult, importJWK, JWSHeaderPar import { z, ZodError } from "zod"; import { TPkiAcmeAccountDALFactory } from "./pki-acme-account-dal"; import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal"; +import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal"; import { AcmeAccountDoesNotExistError, AcmeBadPublicKeyError, @@ -28,6 +29,7 @@ import { TPkiAcmeOrderAuthDALFactory } from "./pki-acme-order-auth-dal"; import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal"; import { AcmeAuthStatus, + AcmeChallengeType, AcmeIdentifierType, AcmeOrderStatus, CreateAcmeAccountBodySchema, @@ -58,6 +60,7 @@ type TPkiAcmeServiceFactoryDep = { acmeOrderDAL: Pick; acmeAuthDAL: Pick; acmeOrderAuthDAL: Pick; + acmeChallengeDAL: Pick; }; export const pkiAcmeServiceFactory = ({ @@ -65,7 +68,8 @@ export const pkiAcmeServiceFactory = ({ acmeAccountDAL, acmeOrderDAL, acmeAuthDAL, - acmeOrderAuthDAL + acmeOrderAuthDAL, + acmeChallengeDAL }: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => { const validateAcmeProfile = async (profileId: string): Promise => { const profile = await certificateProfileDAL.findById(profileId); @@ -376,7 +380,7 @@ export const pkiAcmeServiceFactory = ({ payload.identifiers.map(async (identifier) => { if (identifier.type === AcmeIdentifierType.DNS) { // TODO: reuse existing authorizations for this identifier if they exist - return await acmeAuthDAL.create( + const auth = await acmeAuthDAL.create( { accountId: account.id, status: AcmeAuthStatus.Pending, @@ -391,6 +395,15 @@ export const pkiAcmeServiceFactory = ({ }, tx ); + // TODO: support other challenge types here. Currently only HTTP-01 is supported. + await acmeChallengeDAL.create( + { + authId: auth.id, + type: AcmeChallengeType.HTTP_01 + }, + tx + ); + return auth; } else { throw new AcmeUnsupportedIdentifierError({ detail: "Only DNS identifiers are supported" }); } From 9ad75c2a2db52424ad0fce95f1b34a2bdeb395fb Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 19:35:49 -0700 Subject: [PATCH 156/294] DRY --- .../ee/services/pki-acme/pki-acme-service.ts | 48 +++++++++---------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 679457814..b199f5105 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -82,18 +82,18 @@ export const pkiAcmeServiceFactory = ({ return profile; }; - const buildUrl = (path: string): string => { + const buildUrl = (profileId: string, path: string): string => { const appCfg = getConfig(); const baseUrl = appCfg.SITE_URL ?? ""; - return `${baseUrl}${path}`; + return `${baseUrl}/api/v1/pki/acme/profiles/${profileId}${path}`; }; const extractAccountIdFromKid = (kid: string, profileId: string): string => { - const kidPrefix = buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/`); + const kidPrefix = buildUrl(profileId, "/accounts/"); if (!kid.startsWith(kidPrefix)) { throw new AcmeMalformedError({ detail: "KID must start with the profile account URL" }); } - return kid.slice(kidPrefix.length); + return z.string().uuid().parse(kid.slice(kidPrefix.length)); }; const validateJwsPayload = async < @@ -248,18 +248,18 @@ export const pkiAcmeServiceFactory = ({ value: auth.identifierValue })), authorizations: order.authorizations.map((auth: TPkiAcmeAuths) => - buildUrl(`/api/v1/pki/acme/profiles/${profileId}/authorizations/${auth.id}`) + buildUrl(profileId, `/authorizations/${auth.id}`) ), - finalize: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${order.id}/finalize`) + finalize: buildUrl(profileId, `/orders/${order.id}/finalize`) }; }; const getAcmeDirectory = async (profileId: string): Promise => { const profile = await validateAcmeProfile(profileId); return { - newNonce: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/new-nonce`), - newAccount: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/new-account`), - newOrder: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/new-order`) + newNonce: buildUrl(profile.id, "/new-nonce"), + newAccount: buildUrl(profile.id, "/new-account"), + newOrder: buildUrl(profile.id, "/new-order") }; }; @@ -297,10 +297,10 @@ export const pkiAcmeServiceFactory = ({ body: { status: "valid", contact: existingAccount.emails, - orders: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/accounts/${existingAccount.id}/orders`) + orders: buildUrl(profile.id, `/accounts/${existingAccount.id}/orders`) }, headers: { - Location: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/accounts/${existingAccount.id}`) + Location: buildUrl(profile.id, `/accounts/${existingAccount.id}`) } }; } @@ -318,10 +318,10 @@ export const pkiAcmeServiceFactory = ({ body: { status: "valid", contact: newAccount.emails, - orders: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/accounts/${newAccount.id}/orders`) + orders: buildUrl(profile.id, `/accounts/${newAccount.id}/orders`) }, headers: { - Location: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/accounts/${newAccount.id}`) + Location: buildUrl(profile.id, `/accounts/${newAccount.id}`) } }; }; @@ -343,7 +343,7 @@ export const pkiAcmeServiceFactory = ({ status: "deactivated" }, headers: { - Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${accountId}`) + Location: buildUrl(profileId, `/accounts/${accountId}`) } }; }; @@ -428,7 +428,7 @@ export const pkiAcmeServiceFactory = ({ order }), headers: { - Location: buildUrl(`/api/v1/pki/acme/profiles/${order.account.profileId}/orders/${order.id}`) + Location: buildUrl(profileId, `/orders/${order.id}`) } }; }; @@ -449,7 +449,7 @@ export const pkiAcmeServiceFactory = ({ return { status: 200, body: buildAcmeOrderResource({ profileId, order }), - headers: { Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}`) } + headers: { Location: buildUrl(profileId, `/orders/${orderId}`) } }; }; @@ -474,7 +474,7 @@ export const pkiAcmeServiceFactory = ({ status: 200, body: buildAcmeOrderResource({ profileId, order }), headers: { - Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}`) + Location: buildUrl(profileId, `/orders/${orderId}`) } }; }; @@ -498,7 +498,7 @@ export const pkiAcmeServiceFactory = ({ status: 200, body: "FIXME-certificate-pem", headers: { - Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/certificate`) + Location: buildUrl(profileId, `/orders/${orderId}/certificate`) } }; }; @@ -518,7 +518,7 @@ export const pkiAcmeServiceFactory = ({ orders: [] }, headers: { - Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${accountId}/orders`) + Location: buildUrl(profileId, `/accounts/${accountId}/orders`) } }; }; @@ -551,16 +551,14 @@ export const pkiAcmeServiceFactory = ({ challenges: auth.challenges.map((challenge: TPkiAcmeChallenges) => { return { type: challenge.type, - url: buildUrl( - `/api/v1/pki/acme/profiles/${profileId}/authorizations/${authzId}/challenges/${challenge.id}` - ), + url: buildUrl(profileId, `/authorizations/${authzId}/challenges/${challenge.id}`), status: challenge.status, token: auth.token }; }) }, headers: { - Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/authorizations/${authzId}`) + Location: buildUrl(profileId, `/authorizations/${authzId}`) } }; }; @@ -579,12 +577,12 @@ export const pkiAcmeServiceFactory = ({ status: 200, body: { type: "http-01", - url: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/authorizations/${authzId}/challenges/http-01`), + url: buildUrl(profileId, `/authorizations/${authzId}/challenges/http-01`), status: "pending", token: "FIXME-challenge-token" }, headers: { - Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/authorizations/${authzId}/challenges/http-01`) + Location: buildUrl(profileId, `/authorizations/${authzId}/challenges/http-01`) } }; }; From f2e6d598f667d48cccdcf6292e46ec7894be6a80 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 20:45:54 -0700 Subject: [PATCH 157/294] Add missing status --- backend/src/ee/services/pki-acme/pki-acme-schemas.ts | 7 +++++++ backend/src/ee/services/pki-acme/pki-acme-service.ts | 2 ++ backend/src/server/routes/index.ts | 7 +++++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts index e09aa81c8..25a3db3f4 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts @@ -21,6 +21,13 @@ export enum AcmeAuthStatus { Revoked = "revoked" } +export enum AcmeChallengeStatus { + Pending = "pending", + Processing = "processing", + Valid = "valid", + Invalid = "invalid" +} + export enum AcmeChallengeType { HTTP_01 = "http-01", DNS_01 = "dns-01", diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index b199f5105..bbff4a9c4 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -29,6 +29,7 @@ import { TPkiAcmeOrderAuthDALFactory } from "./pki-acme-order-auth-dal"; import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal"; import { AcmeAuthStatus, + AcmeChallengeStatus, AcmeChallengeType, AcmeIdentifierType, AcmeOrderStatus, @@ -399,6 +400,7 @@ export const pkiAcmeServiceFactory = ({ await acmeChallengeDAL.create( { authId: auth.id, + status: AcmeChallengeStatus.Pending, type: AcmeChallengeType.HTTP_01 }, tx diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 40ffe4f5d..d3ee4caae 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -75,8 +75,8 @@ import { permissionDALFactory } from "@app/ee/services/permission/permission-dal import { permissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { pitServiceFactory } from "@app/ee/services/pit/pit-service"; import { pkiAcmeAuthDALFactory } from "@app/ee/services/pki-acme/pki-acme-auth-dal"; -import { pkiAcmeServiceFactory } from "@app/ee/services/pki-acme/pki-acme-service"; import { pkiAcmeOrderAuthDALFactory } from "@app/ee/services/pki-acme/pki-acme-order-auth-dal"; +import { pkiAcmeServiceFactory } from "@app/ee/services/pki-acme/pki-acme-service"; import { projectTemplateDALFactory } from "@app/ee/services/project-template/project-template-dal"; import { projectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service"; import { rateLimitDALFactory } from "@app/ee/services/rate-limit/rate-limit-dal"; @@ -352,6 +352,7 @@ import { workflowIntegrationDALFactory } from "@app/services/workflow-integratio import { workflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service"; import { pkiAcmeAccountDALFactory } from "@app/ee/services/pki-acme/pki-acme-account-dal"; +import { pkiAcmeChallengeDALFactory } from "@app/ee/services/pki-acme/pki-acme-challenge-dal"; import { pkiAcmeOrderDALFactory } from "@app/ee/services/pki-acme/pki-acme-order-dal"; import { injectAuditLogInfo } from "../plugins/audit-log"; import { injectAssumePrivilege } from "../plugins/auth/inject-assume-privilege"; @@ -1072,6 +1073,7 @@ export const registerRoutes = async ( const acmeOrderDAL = pkiAcmeOrderDALFactory(db); const acmeAuthDAL = pkiAcmeAuthDALFactory(db); const acmeOrderAuthDAL = pkiAcmeOrderAuthDALFactory(db); + const acmeChallengeDAL = pkiAcmeChallengeDALFactory(db); const certificateDAL = certificateDALFactory(db); const certificateBodyDAL = certificateBodyDALFactory(db); const certificateSecretDAL = certificateSecretDALFactory(db); @@ -1177,7 +1179,8 @@ export const registerRoutes = async ( acmeAccountDAL, acmeOrderDAL, acmeAuthDAL, - acmeOrderAuthDAL + acmeOrderAuthDAL, + acmeChallengeDAL }); const pkiAlertService = pkiAlertServiceFactory({ From f6955da9caaa44cb15cdbaf74c6c16c35ecbcc22 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 20:56:01 -0700 Subject: [PATCH 158/294] More auth feature --- backend/bdd/features/pki/acme/auth.feature | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/backend/bdd/features/pki/acme/auth.feature b/backend/bdd/features/pki/acme/auth.feature index 7252d868f..b1b8ee5ae 100644 --- a/backend/bdd/features/pki/acme/auth.feature +++ b/backend/bdd/features/pki/acme/auth.feature @@ -1,4 +1,4 @@ -Feature: Order +Feature: Authorization Scenario: Get authorization Given I have an ACME cert profile as "acme_profile" @@ -18,7 +18,16 @@ Feature: Order Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order Then the value order.authorizations[0].uri with jq . should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/(.+) Then the value order.authorizations[0].body with jq .status should be equal to "pending" - Then the value order.authorizations[0].body with jq .challenge should be equal to "pending" + Then the value order.authorizations[0].body with jq .challenges | map(pick(.type, .status)) | sort_by(.type) should be equal to json + """ + [ + { + "type": "http-01", + "status": "pending" + } + ] + """ + Then the value order.authorizations[0].body with jq .challenges | map(.status) | sort should be equal to ["pending"] Then the value order.authorizations[0].body with jq .identifier should be equal to json """ { From 6cf08622ed4a1aa2c471b410582344972f6f33cb Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 21:10:46 -0700 Subject: [PATCH 159/294] Add challenge --- .../bdd/features/pki/acme/challenge.feature | 20 ++++++++++++++++++ backend/bdd/features/steps/pki_acme.py | 21 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 backend/bdd/features/pki/acme/challenge.feature diff --git a/backend/bdd/features/pki/acme/challenge.feature b/backend/bdd/features/pki/acme/challenge.feature new file mode 100644 index 000000000..72573150c --- /dev/null +++ b/backend/bdd/features/pki/acme/challenge.feature @@ -0,0 +1,20 @@ +Feature: Challenge + + Scenario: Validate challenge + Given I have an ACME cert profile as "acme_profile" + When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory +# # TODO: make it I have an account already instead? + Then I register a new ACME account with email fangpen@infisical.com and EAB key id {acme_profile.eab_kid} with secret {acme_profile.eab_secret} as acme_account + When I create certificate signing request as csr + Then I add names to certificate signing request csr + """ + { + "ORGANIZATION_NAME": "Infisical Inc", + "COMMON_NAME": "localhost" + } + """ + Then I create a RSA private key pair as cert_key + Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format + Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order + Then I select challenge with type http-01 from order as challenge + diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index db9e8a28a..294b8cd1f 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -301,3 +301,24 @@ def step_impl(context: Context, var_path: str, expected: str): def step_impl(context: Context, var_path: str): value = eval_var(context, var_path) print(json.dumps(value.json(), indent=2)) + + +@then( + "I select challenge with type {challenge_type} from {var_path} as {challenge_var}" +) +def step_impl(context: Context, challenge_type: str, var_path: str, challenge_var: str): + order = eval_var(context, var_path) + if not isinstance(order, messages.OrderResource): + raise ValueError( + f"Expected OrderResource but got {(type(order),)!r} at {var_path!r}" + ) + auths = list(filter(lambda a: a.type == challenge_type, order.authorizations)) + if not auths: + raise ValueError( + f"Authorization type {challenge_type!r} not found in {var_path!r}" + ) + if len(auths) > 1: + raise ValueError( + f"More than one authorization for type {challenge_type!r} found in {var_path!r}" + ) + context.vars[challenge_var] = auths[0] From 5d31627afd9c1cafa3aad1834ac9c10a1af77799 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 21:17:59 -0700 Subject: [PATCH 160/294] Add challenge selection --- .../bdd/features/pki/acme/challenge.feature | 2 +- backend/bdd/features/steps/pki_acme.py | 31 +++++++++++++++---- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/backend/bdd/features/pki/acme/challenge.feature b/backend/bdd/features/pki/acme/challenge.feature index 72573150c..0bf568123 100644 --- a/backend/bdd/features/pki/acme/challenge.feature +++ b/backend/bdd/features/pki/acme/challenge.feature @@ -16,5 +16,5 @@ Feature: Challenge Then I create a RSA private key pair as cert_key Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order - Then I select challenge with type http-01 from order as challenge + Then I select challenge with type http-01 for domain localhost from order at order as challenge diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 294b8cd1f..ca90c5415 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -304,20 +304,39 @@ def step_impl(context: Context, var_path: str): @then( - "I select challenge with type {challenge_type} from {var_path} as {challenge_var}" + "I select challenge with type {challenge_type} for domain {domain} from order at {var_path} as {challenge_var}" ) -def step_impl(context: Context, challenge_type: str, var_path: str, challenge_var: str): - order = eval_var(context, var_path) +def step_impl( + context: Context, + challenge_type: str, + domain: str, + var_path: str, + challenge_var: str, +): + order = eval_var(context, var_path, as_json=False) if not isinstance(order, messages.OrderResource): raise ValueError( - f"Expected OrderResource but got {(type(order),)!r} at {var_path!r}" + f"Expected OrderResource but got {type(order)!r} at {var_path!r}" ) - auths = list(filter(lambda a: a.type == challenge_type, order.authorizations)) + auths = list( + filter(lambda o: o.body.identifier.value == domain, order.authorizations) + ) if not auths: + raise ValueError( + f"Authorization for domain {domain!r} not found in {var_path!r}" + ) + if len(auths) > 1: + raise ValueError( + f"More than one order for domain {domain!r} found in {var_path!r}" + ) + auth = auths[0] + + challenges = list(filter(lambda a: a.typ == challenge_type, auth.body.challenges)) + if not challenges: raise ValueError( f"Authorization type {challenge_type!r} not found in {var_path!r}" ) - if len(auths) > 1: + if len(challenges) > 1: raise ValueError( f"More than one authorization for type {challenge_type!r} found in {var_path!r}" ) From 3a05ad8cd18a2906843d33ead8cf8e998a789125 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 21:27:22 -0700 Subject: [PATCH 161/294] Serve challenge server --- .../bdd/features/pki/acme/challenge.feature | 1 + backend/bdd/features/steps/pki_acme.py | 20 ++++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/backend/bdd/features/pki/acme/challenge.feature b/backend/bdd/features/pki/acme/challenge.feature index 0bf568123..2079b9a57 100644 --- a/backend/bdd/features/pki/acme/challenge.feature +++ b/backend/bdd/features/pki/acme/challenge.feature @@ -17,4 +17,5 @@ Feature: Challenge Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order Then I select challenge with type http-01 for domain localhost from order at order as challenge + Then I serve challenge response for challenge at localhost diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index ca90c5415..f1219b801 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -6,6 +6,7 @@ import requests import glom from acme import client from acme import messages +from acme import standalone from behave.runner import Context from behave import given from behave import when @@ -340,4 +341,21 @@ def step_impl( raise ValueError( f"More than one authorization for type {challenge_type!r} found in {var_path!r}" ) - context.vars[challenge_var] = auths[0] + context.vars[challenge_var] = challenges[0] + + +@then("I serve challenge response for {var_path} at {hostname}") +def step_impl(context: Context, var_path: str, hostname: str): + if hostname != "localhost": + raise ValueError("Currently only localhost is supported") + challenge = eval_var(context, var_path, as_json=False) + acme_challenge = challenge.chall + response, validation = acme_challenge.response_and_validation( + context.acme_client.net.key + ) + resource = standalone.HTTP01RequestHandler.HTTP01Resource( + chall=acme_challenge, response=response, validation=validation + ) + servers = standalone.HTTP01DualNetworkedServers(("", 8087), resource) + # Start client standalone web server. + servers.serve_forever() From 42706c205bb2ef7f7faa27687c1b1144625f4494 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 21:29:57 -0700 Subject: [PATCH 162/294] Run server with thread --- backend/bdd/features/steps/pki_acme.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index f1219b801..1c88b0c23 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -1,5 +1,6 @@ import json import re +import threading import jq import requests @@ -356,6 +357,10 @@ def step_impl(context: Context, var_path: str, hostname: str): resource = standalone.HTTP01RequestHandler.HTTP01Resource( chall=acme_challenge, response=response, validation=validation ) + # TODO: make port configurable servers = standalone.HTTP01DualNetworkedServers(("", 8087), resource) # Start client standalone web server. - servers.serve_forever() + web_server = threading.Thread(name="web_server", target=servers.serve_forever) + web_server.daemon = True + web_server.start() + context.web_server = web_server From b989777ed60aff385fe07e48d64e32942b408ca4 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 21:34:44 -0700 Subject: [PATCH 163/294] Tell server challenge is ready --- backend/bdd/features/pki/acme/challenge.feature | 1 + backend/bdd/features/steps/pki_acme.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/backend/bdd/features/pki/acme/challenge.feature b/backend/bdd/features/pki/acme/challenge.feature index 2079b9a57..e6b0b7885 100644 --- a/backend/bdd/features/pki/acme/challenge.feature +++ b/backend/bdd/features/pki/acme/challenge.feature @@ -18,4 +18,5 @@ Feature: Challenge Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order Then I select challenge with type http-01 for domain localhost from order at order as challenge Then I serve challenge response for challenge at localhost + Then I tell ACME server that challenge is ready to be verified diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 1c88b0c23..7562cfd2c 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -364,3 +364,12 @@ def step_impl(context: Context, var_path: str, hostname: str): web_server.daemon = True web_server.start() context.web_server = web_server + + +@then("I tell ACME server that {var_path} is ready to be verified") +def step_impl(context: Context, var_path: str): + challenge = eval_var(context, var_path, as_json=False) + acme_challenge = challenge.chall + acme_client = challenge.acme_client + response, validation = acme_challenge.response_and_validation(acme_client.net.key) + acme_client.answer_challenge(acme_challenge, response) From d14d64e11e4e2c5d55e309b4adcc779508b2a5c7 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 21:37:41 -0700 Subject: [PATCH 164/294] Fix tell ready --- backend/bdd/features/pki/acme/challenge.feature | 1 - backend/bdd/features/steps/pki_acme.py | 7 +++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/backend/bdd/features/pki/acme/challenge.feature b/backend/bdd/features/pki/acme/challenge.feature index e6b0b7885..8e7dad523 100644 --- a/backend/bdd/features/pki/acme/challenge.feature +++ b/backend/bdd/features/pki/acme/challenge.feature @@ -19,4 +19,3 @@ Feature: Challenge Then I select challenge with type http-01 for domain localhost from order at order as challenge Then I serve challenge response for challenge at localhost Then I tell ACME server that challenge is ready to be verified - diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 7562cfd2c..c5627bb8b 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -369,7 +369,6 @@ def step_impl(context: Context, var_path: str, hostname: str): @then("I tell ACME server that {var_path} is ready to be verified") def step_impl(context: Context, var_path: str): challenge = eval_var(context, var_path, as_json=False) - acme_challenge = challenge.chall - acme_client = challenge.acme_client - response, validation = acme_challenge.response_and_validation(acme_client.net.key) - acme_client.answer_challenge(acme_challenge, response) + acme_client = context.acme_client + response, validation = challenge.response_and_validation(acme_client.net.key) + acme_client.answer_challenge(challenge, response) From ea96cc87bbefc78ac6b772351a9a797ce81aac57 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 21:56:00 -0700 Subject: [PATCH 165/294] Add link headers --- backend/src/ee/routes/v1/pki-acme-router.ts | 13 ++- .../pki-acme/pki-acme-challenge-dal.ts | 32 ++++++- .../ee/services/pki-acme/pki-acme-service.ts | 93 +++++++++++-------- .../ee/services/pki-acme/pki-acme-types.ts | 8 +- 4 files changed, 101 insertions(+), 45 deletions(-) diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index 6935150ac..53bb81224 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -424,7 +424,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // Respond to Challenge (RFC 8555 Section 7.5.1) server.route({ method: "POST", - url: "/profiles/:profileId/authorizations/:authzId/challenges/http-01", + url: "/profiles/:profileId/authorizations/:authzId/challenges/:challengeId", config: { rateLimit: writeLimit }, @@ -433,7 +433,8 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { tags: [ApiDocsTags.PkiAcme], description: "ACME Respond to Challenge - let ACME server know challenge is ready", params: SharedParamsSchema.extend({ - authzId: z.string().uuid() + authzId: z.string().uuid(), + challengeId: z.string().uuid() }), response: { 200: RespondToAcmeChallengeResponseSchema @@ -447,9 +448,13 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { return sendAcmeResponse( res, profileId, - await server.services.pkiAcme.respondToAcmeChallenge({ profileId, authzId: req.params.authzId }) + await server.services.pkiAcme.respondToAcmeChallenge({ + profileId, + accountId, + authzId: req.params.authzId, + challengeId: req.params.challengeId + }) ); - return challenge; } }); }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts index 77a585efd..d23827a37 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts @@ -1,13 +1,41 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { Knex } from "knex"; export type TPkiAcmeChallengeDALFactory = ReturnType; export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { const pkiAcmeChallengeOrm = ormify(db, TableName.PkiAcmeChallenge); + const findByAccountAuthAndChallengeIdWithToken = async ( + accountId: string, + authId: string, + challengeId: string, + tx?: Knex + ) => { + try { + const challenge = await (tx || db)(TableName.PkiAcmeChallenge) + .join(TableName.PkiAcmeAuth, `${TableName.PkiAcmeChallenge}.authId`, `${TableName.PkiAcmeAuth}.id`) + .select( + selectAllTableCols(TableName.PkiAcmeChallenge), + db.ref("token").withSchema(TableName.PkiAcmeChallenge).as("token") + ) + .where(`${TableName.PkiAcmeChallenge}.id`, challengeId) + .where(`${TableName.PkiAcmeChallenge}.authId`, authId) + .where(`${TableName.PkiAcmeAuth}.accountId`, accountId) + .first(); + if (!challenge) { + return null; + } + return challenge; + } catch (error) { + throw new DatabaseError({ error, name: "Find PKI ACME challenge by account id, auth id and challenge id" }); + } + }; return { - ...pkiAcmeChallengeOrm + ...pkiAcmeChallengeOrm, + findByAccountAuthAndChallengeIdWithToken }; }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index bbff4a9c4..c55a46542 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -61,7 +61,7 @@ type TPkiAcmeServiceFactoryDep = { acmeOrderDAL: Pick; acmeAuthDAL: Pick; acmeOrderAuthDAL: Pick; - acmeChallengeDAL: Pick; + acmeChallengeDAL: Pick; }; export const pkiAcmeServiceFactory = ({ @@ -300,9 +300,10 @@ export const pkiAcmeServiceFactory = ({ contact: existingAccount.emails, orders: buildUrl(profile.id, `/accounts/${existingAccount.id}/orders`) }, - headers: { - Location: buildUrl(profile.id, `/accounts/${existingAccount.id}`) - } + headers: [ + ["Location", buildUrl(profile.id, `/accounts/${existingAccount.id}`)], + ["Link", `<${buildUrl(profile.id, "/directory")}>;rel="index"`] + ] }; } @@ -321,9 +322,10 @@ export const pkiAcmeServiceFactory = ({ contact: newAccount.emails, orders: buildUrl(profile.id, `/accounts/${newAccount.id}/orders`) }, - headers: { - Location: buildUrl(profile.id, `/accounts/${newAccount.id}`) - } + headers: [ + ["Location", buildUrl(profile.id, `/accounts/${newAccount.id}`)], + ["Link", `<${buildUrl(profile.id, "/directory")}>;rel="index"`] + ] }; }; @@ -343,9 +345,10 @@ export const pkiAcmeServiceFactory = ({ body: { status: "deactivated" }, - headers: { - Location: buildUrl(profileId, `/accounts/${accountId}`) - } + headers: [ + ["Location", buildUrl(profileId, `/accounts/${accountId}`)], + ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`] + ] }; }; @@ -429,9 +432,10 @@ export const pkiAcmeServiceFactory = ({ profileId, order }), - headers: { - Location: buildUrl(profileId, `/orders/${order.id}`) - } + headers: [ + ["Location", buildUrl(profileId, `/orders/${order.id}`)], + ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`] + ] }; }; @@ -451,7 +455,10 @@ export const pkiAcmeServiceFactory = ({ return { status: 200, body: buildAcmeOrderResource({ profileId, order }), - headers: { Location: buildUrl(profileId, `/orders/${orderId}`) } + headers: [ + ["Location", buildUrl(profileId, `/orders/${orderId}`)], + ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`] + ] }; }; @@ -475,9 +482,10 @@ export const pkiAcmeServiceFactory = ({ return { status: 200, body: buildAcmeOrderResource({ profileId, order }), - headers: { - Location: buildUrl(profileId, `/orders/${orderId}`) - } + headers: [ + ["Location", buildUrl(profileId, `/orders/${orderId}`)], + ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`] + ] }; }; @@ -499,9 +507,10 @@ export const pkiAcmeServiceFactory = ({ return { status: 200, body: "FIXME-certificate-pem", - headers: { - Location: buildUrl(profileId, `/orders/${orderId}/certificate`) - } + headers: [ + ["Location", buildUrl(profileId, `/orders/${orderId}/certificate`)], + ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`] + ] }; }; @@ -519,9 +528,10 @@ export const pkiAcmeServiceFactory = ({ body: { orders: [] }, - headers: { - Location: buildUrl(profileId, `/accounts/${accountId}/orders`) - } + headers: [ + ["Location", buildUrl(profileId, `/accounts/${accountId}/orders`)], + ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`] + ] }; }; @@ -559,33 +569,42 @@ export const pkiAcmeServiceFactory = ({ }; }) }, - headers: { - Location: buildUrl(profileId, `/authorizations/${authzId}`) - } + headers: [ + ["Location", buildUrl(profileId, `/authorizations/${authzId}`)], + ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`] + ] }; }; const respondToAcmeChallenge = async ({ profileId, - authzId + accountId, + authzId, + challengeId }: { profileId: string; + accountId: string; authzId: string; + challengeId: string; }): Promise> => { - const profile = await validateAcmeProfile(profileId); - // FIXME: Implement ACME challenge response - // Trigger verification process + const challenge = await acmeChallengeDAL.findByAccountAuthAndChallengeIdWithToken(accountId, authzId, challengeId); + if (!challenge) { + throw new NotFoundError({ message: "ACME challenge not found" }); + } + // TODO: Implement ACME challenge response return { status: 200, body: { - type: "http-01", - url: buildUrl(profileId, `/authorizations/${authzId}/challenges/http-01`), - status: "pending", - token: "FIXME-challenge-token" + type: challenge.type, + url: buildUrl(profileId, `/authorizations/${authzId}/challenges/${challengeId}`), + status: challenge.status, + token: challenge.token }, - headers: { - Location: buildUrl(profileId, `/authorizations/${authzId}/challenges/http-01`) - } + headers: [ + ["Location", buildUrl(profileId, `/authorizations/${authzId}/challenges/http-01`)], + ["Link", `<${buildUrl(profileId, `/authorizations/${authzId}`)}>;rel="up"`], + ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`] + ] }; }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index 082078f2a..dbd1ad25f 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -44,7 +44,7 @@ export type TAuthenciatedJwsPayload = TJwsPayload & { }; export type TAcmeResponse = { status: number; - headers: Record; + headers: [string, string][]; body: TPayload; }; @@ -164,9 +164,13 @@ export type TPkiAcmeServiceFactory = { }) => Promise>; respondToAcmeChallenge: ({ profileId, - authzId + accountId, + authzId, + challengeId }: { profileId: string; + accountId: string; authzId: string; + challengeId: string; }) => Promise>; }; From 3c3d89035051cd561dd843c0bf741ba0fe94ded4 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 22:00:19 -0700 Subject: [PATCH 166/294] Fix sending headers --- backend/src/ee/routes/v1/pki-acme-router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index 53bb81224..2fb52695c 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -53,7 +53,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { const sendAcmeResponse = async (res: FastifyReply, profileId: string, response: TAcmeResponse): Promise => { res.code(response.status); - for (const [key, value] of Object.entries(response.headers)) { + for (const [key, value] of response.headers) { res.header(key, value); } From 2c53e744c23d2606c09347e136c5b12bf9bf59a7 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 09:12:14 -0700 Subject: [PATCH 167/294] Fix link header overriding issue and challenge dal find method bug --- .../pki-acme/pki-acme-challenge-dal.ts | 2 +- .../ee/services/pki-acme/pki-acme-schemas.ts | 2 + .../ee/services/pki-acme/pki-acme-service.ts | 84 ++++++++++--------- .../ee/services/pki-acme/pki-acme-types.ts | 2 +- 4 files changed, 47 insertions(+), 43 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts index d23827a37..b8c909497 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts @@ -20,7 +20,7 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { .join(TableName.PkiAcmeAuth, `${TableName.PkiAcmeChallenge}.authId`, `${TableName.PkiAcmeAuth}.id`) .select( selectAllTableCols(TableName.PkiAcmeChallenge), - db.ref("token").withSchema(TableName.PkiAcmeChallenge).as("token") + db.ref("token").withSchema(TableName.PkiAcmeAuth).as("token") ) .where(`${TableName.PkiAcmeChallenge}.id`, challengeId) .where(`${TableName.PkiAcmeChallenge}.authId`, authId) diff --git a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts index 25a3db3f4..0c9015700 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts @@ -158,6 +158,8 @@ export const GetAcmeAuthorizationResponseSchema = z.object({ ) }); +export const RespondToAcmeChallengeBodySchema = z.object({}); + export const RespondToAcmeChallengeResponseSchema = z.object({ type: z.enum(Object.values(AcmeChallengeType) as [string, ...string[]]), url: z.string(), diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index c55a46542..d47d480f0 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -300,10 +300,10 @@ export const pkiAcmeServiceFactory = ({ contact: existingAccount.emails, orders: buildUrl(profile.id, `/accounts/${existingAccount.id}/orders`) }, - headers: [ - ["Location", buildUrl(profile.id, `/accounts/${existingAccount.id}`)], - ["Link", `<${buildUrl(profile.id, "/directory")}>;rel="index"`] - ] + headers: { + Location: buildUrl(profile.id, `/accounts/${existingAccount.id}`), + Link: `<${buildUrl(profile.id, "/directory")}>;rel="index"` + } }; } @@ -322,10 +322,10 @@ export const pkiAcmeServiceFactory = ({ contact: newAccount.emails, orders: buildUrl(profile.id, `/accounts/${newAccount.id}/orders`) }, - headers: [ - ["Location", buildUrl(profile.id, `/accounts/${newAccount.id}`)], - ["Link", `<${buildUrl(profile.id, "/directory")}>;rel="index"`] - ] + headers: { + Location: buildUrl(profile.id, `/accounts/${newAccount.id}`), + Link: `<${buildUrl(profile.id, "/directory")}>;rel="index"` + } }; }; @@ -345,10 +345,10 @@ export const pkiAcmeServiceFactory = ({ body: { status: "deactivated" }, - headers: [ - ["Location", buildUrl(profileId, `/accounts/${accountId}`)], - ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`] - ] + headers: { + Location: buildUrl(profileId, `/accounts/${accountId}`), + Link: `<${buildUrl(profileId, "/directory")}>;rel="index"` + } }; }; @@ -432,10 +432,10 @@ export const pkiAcmeServiceFactory = ({ profileId, order }), - headers: [ - ["Location", buildUrl(profileId, `/orders/${order.id}`)], - ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`] - ] + headers: { + Location: buildUrl(profileId, `/orders/${order.id}`), + Link: `<${buildUrl(profileId, "/directory")}>;rel="index"` + } }; }; @@ -455,10 +455,10 @@ export const pkiAcmeServiceFactory = ({ return { status: 200, body: buildAcmeOrderResource({ profileId, order }), - headers: [ - ["Location", buildUrl(profileId, `/orders/${orderId}`)], - ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`] - ] + headers: { + Location: buildUrl(profileId, `/orders/${orderId}`), + Link: `<${buildUrl(profileId, "/directory")}>;rel="index"` + } }; }; @@ -482,10 +482,10 @@ export const pkiAcmeServiceFactory = ({ return { status: 200, body: buildAcmeOrderResource({ profileId, order }), - headers: [ - ["Location", buildUrl(profileId, `/orders/${orderId}`)], - ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`] - ] + headers: { + Location: buildUrl(profileId, `/orders/${orderId}`), + Link: `<${buildUrl(profileId, "/directory")}>;rel="index"` + } }; }; @@ -507,10 +507,10 @@ export const pkiAcmeServiceFactory = ({ return { status: 200, body: "FIXME-certificate-pem", - headers: [ - ["Location", buildUrl(profileId, `/orders/${orderId}/certificate`)], - ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`] - ] + headers: { + Location: buildUrl(profileId, `/orders/${orderId}/certificate`), + Link: `<${buildUrl(profileId, "/directory")}>;rel="index"` + } }; }; @@ -528,10 +528,10 @@ export const pkiAcmeServiceFactory = ({ body: { orders: [] }, - headers: [ - ["Location", buildUrl(profileId, `/accounts/${accountId}/orders`)], - ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`] - ] + headers: { + Location: buildUrl(profileId, `/accounts/${accountId}/orders`), + Link: `<${buildUrl(profileId, "/directory")}>;rel="index"` + } }; }; @@ -569,10 +569,10 @@ export const pkiAcmeServiceFactory = ({ }; }) }, - headers: [ - ["Location", buildUrl(profileId, `/authorizations/${authzId}`)], - ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`] - ] + headers: { + Location: buildUrl(profileId, `/authorizations/${authzId}`), + Link: `<${buildUrl(profileId, "/directory")}>;rel="index"` + } }; }; @@ -600,11 +600,13 @@ export const pkiAcmeServiceFactory = ({ status: challenge.status, token: challenge.token }, - headers: [ - ["Location", buildUrl(profileId, `/authorizations/${authzId}/challenges/http-01`)], - ["Link", `<${buildUrl(profileId, `/authorizations/${authzId}`)}>;rel="up"`], - ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`] - ] + headers: { + Location: buildUrl(profileId, `/authorizations/${authzId}/challenges/${challengeId}`), + Link: [ + `<${buildUrl(profileId, `/authorizations/${authzId}`)}>;rel="up"`, + `<${buildUrl(profileId, "/directory")}>;rel="index"` + ] + } }; }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index dbd1ad25f..d2f96deb4 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -44,7 +44,7 @@ export type TAuthenciatedJwsPayload = TJwsPayload & { }; export type TAcmeResponse = { status: number; - headers: [string, string][]; + headers: Record; body: TPayload; }; From 92a1d1f41324fd16eee8267dbf1b3ceb9caa0813 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 09:13:56 -0700 Subject: [PATCH 168/294] Send headers the right way --- backend/src/ee/routes/v1/pki-acme-router.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index 2fb52695c..6344809d0 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -16,6 +16,7 @@ import { ListAcmeOrdersPayloadSchema, ListAcmeOrdersResponseSchema, RawJwsPayloadSchema, + RespondToAcmeChallengeBodySchema, RespondToAcmeChallengeResponseSchema } from "@app/ee/services/pki-acme/pki-acme-schemas"; import { ApiDocsTags } from "@app/lib/api-docs"; @@ -53,7 +54,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { const sendAcmeResponse = async (res: FastifyReply, profileId: string, response: TAcmeResponse): Promise => { res.code(response.status); - for (const [key, value] of response.headers) { + for (const [key, value] of Object.entries(response.headers)) { res.header(key, value); } @@ -441,10 +442,10 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { } }, handler: async (req, res) => { - const { profileId, accountId, payload } = await validateExistingAccount({ req }); - if (payload !== "") { - throw new AcmeMalformedError({ detail: "Payload should be empty" }); - } + const { profileId, accountId } = await validateExistingAccount({ + req, + schema: RespondToAcmeChallengeBodySchema + }); return sendAcmeResponse( res, profileId, From 1fe458e23ba8baf6f96bbe560e4452ce854cfbe8 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 10:00:24 -0700 Subject: [PATCH 169/294] Add challenge service --- .../migrations/20251029234547_add-pki-acme.ts | 8 ++- backend/src/db/schemas/pki-acme-accounts.ts | 1 + backend/src/db/schemas/pki-acme-auths.ts | 4 +- backend/src/db/schemas/pki-acme-orders.ts | 8 +-- .../pki-acme/pki-acme-challenge-dal.ts | 45 ++++++++++++++- .../pki-acme/pki-acme-challenge-service.ts | 57 +++++++++++++++++++ .../ee/services/pki-acme/pki-acme-types.ts | 2 + backend/src/lib/config/env.ts | 3 + 8 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts diff --git a/backend/src/db/migrations/20251029234547_add-pki-acme.ts b/backend/src/db/migrations/20251029234547_add-pki-acme.ts index aa334cfba..ed9353bce 100644 --- a/backend/src/db/migrations/20251029234547_add-pki-acme.ts +++ b/backend/src/db/migrations/20251029234547_add-pki-acme.ts @@ -10,7 +10,7 @@ import { dropConstraintIfExists } from "@app/db/migrations/utils/dropConstraintI const OLD_ENROLLMENT_TYPE_CHECK_CONSTRAINT = "pki_certificate_profiles_enrollmentType_check"; const NEW_ENROLLMENT_TYPE_CHECK_CONSTRAINT = "pki_certificate_profiles_enrollment_type_check"; -const PUBLIC_KEY_ALG_INDEX = "pki_acme_accounts_publicKey_alg_index"; +const PUBLIC_KEY_THUMBPRINT_ALG_INDEX = "pki_acme_accounts_publicKey_thumbprint_alg_index"; export async function up(knex: Knex): Promise { // Create PkiAcmeEnrollmentConfig table @@ -56,12 +56,14 @@ export async function up(knex: Knex): Promise { // Multi-value emails array t.specificType("emails", "text[]").notNullable(); - // TODO: make public key a string instead of jsonb to make indexing much easier // Public key (JWK format) t.jsonb("publicKey").notNullable(); + // Public key thumbprint + t.string("publicKeyThumbprint").notNullable(); // The JWS algorithm used to sign the public key when creating the account, e.g. "RS256", "ES256", "PS256", etc. t.string("alg").notNullable(); - t.index(["publicKey", "alg"], PUBLIC_KEY_ALG_INDEX); + // We may need to look up existing accounts by public key thumbprint and algorithm, so we index on both of them. + t.index(["publicKeyThumbprint", "alg"], PUBLIC_KEY_THUMBPRINT_ALG_INDEX); t.timestamps(true, true, true); }); diff --git a/backend/src/db/schemas/pki-acme-accounts.ts b/backend/src/db/schemas/pki-acme-accounts.ts index 275ff7e7e..69b1ffe49 100644 --- a/backend/src/db/schemas/pki-acme-accounts.ts +++ b/backend/src/db/schemas/pki-acme-accounts.ts @@ -12,6 +12,7 @@ export const PkiAcmeAccountsSchema = z.object({ profileId: z.string().uuid(), emails: z.string().array(), publicKey: z.unknown(), + publicKeyThumbprint: z.string(), alg: z.string(), createdAt: z.date(), updatedAt: z.date() diff --git a/backend/src/db/schemas/pki-acme-auths.ts b/backend/src/db/schemas/pki-acme-auths.ts index 15c7a7c55..6d91bb387 100644 --- a/backend/src/db/schemas/pki-acme-auths.ts +++ b/backend/src/db/schemas/pki-acme-auths.ts @@ -11,13 +11,13 @@ export const PkiAcmeAuthsSchema = z.object({ id: z.string().uuid(), accountId: z.string().uuid(), status: z.string(), + token: z.date().nullable().optional(), identifierType: z.string(), identifierValue: z.string(), expiresAt: z.date(), certificateId: z.string().uuid().nullable().optional(), createdAt: z.date(), - updatedAt: z.date(), - token: z.string().nullable().optional() + updatedAt: z.date() }); export type TPkiAcmeAuths = z.infer; diff --git a/backend/src/db/schemas/pki-acme-orders.ts b/backend/src/db/schemas/pki-acme-orders.ts index 6d5274f06..738155f46 100644 --- a/backend/src/db/schemas/pki-acme-orders.ts +++ b/backend/src/db/schemas/pki-acme-orders.ts @@ -10,12 +10,12 @@ import { TImmutableDBKeys } from "./models"; export const PkiAcmeOrdersSchema = z.object({ id: z.string().uuid(), accountId: z.string().uuid(), - status: z.string(), - createdAt: z.date(), - updatedAt: z.date(), notBefore: z.date().nullable().optional(), notAfter: z.date().nullable().optional(), - expiresAt: z.date() + expiresAt: z.date(), + status: z.string(), + createdAt: z.date(), + updatedAt: z.date() }); export type TPkiAcmeOrders = z.infer; diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts index b8c909497..ae5c1f994 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts @@ -1,7 +1,7 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { Knex } from "knex"; export type TPkiAcmeChallengeDALFactory = ReturnType; @@ -34,8 +34,49 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { throw new DatabaseError({ error, name: "Find PKI ACME challenge by account id, auth id and challenge id" }); } }; + const findByIdWithAuthForUpdate = async (id: string, tx?: Knex) => { + const rows = await (tx || db)(TableName.PkiAcmeChallenge) + .join(TableName.PkiAcmeAuth, `${TableName.PkiAcmeChallenge}.authId`, `${TableName.PkiAcmeAuth}.id`) + .select( + selectAllTableCols(TableName.PkiAcmeChallenge), + db.ref("id").withSchema(TableName.PkiAcmeAuth).as("authId"), + db.ref("token").withSchema(TableName.PkiAcmeAuth).as("authToken"), + db.ref("status").withSchema(TableName.PkiAcmeAuth).as("authStatus"), + db.ref("identifierType").withSchema(TableName.PkiAcmeAuth).as("authIdentifierType"), + db.ref("identifierValue").withSchema(TableName.PkiAcmeAuth).as("authIdentifierValue"), + db.ref("expiresAt").withSchema(TableName.PkiAcmeAuth).as("authExpiresAt") + ) + // For all challenges, acquire update lock on the auth to avoid race conditions + .forUpdate(TableName.PkiAcmeAuth) + .where(`${TableName.PkiAcmeChallenge}.id`, id); + + if (rows.length === 0) { + return null; + } + return sqlNestRelationships({ + data: rows, + key: "id", + parentMapper: (row) => row, + childrenMapper: [ + { + key: "authId", + label: "auth" as const, + mapper: ({ authId, authToken, authStatus, authIdentifierType, authIdentifierValue, authExpiresAt }) => ({ + id: authId, + token: authToken, + status: authStatus, + identifierType: authIdentifierType, + identifierValue: authIdentifierValue, + expiresAt: authExpiresAt + }) + } + ] + })?.[0]; + }; + return { ...pkiAcmeChallengeOrm, - findByAccountAuthAndChallengeIdWithToken + findByAccountAuthAndChallengeIdWithToken, + findByIdWithAuthForUpdate }; }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts new file mode 100644 index 000000000..6e077d0c4 --- /dev/null +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts @@ -0,0 +1,57 @@ +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal"; +import { AcmeAuthStatus, AcmeChallengeStatus, AcmeChallengeType } from "./pki-acme-schemas"; +import { TPkiAcmeChallengeServiceFactory } from "./pki-acme-types"; +import { getConfig } from "@app/lib/config/env"; +import { calculateJwkThumbprint } from "jose"; + +type TPkiAcmeChallengeServiceFactoryDep = { + acmeChallengeDAL: Pick; +}; + +export const pkiAcmeChallengeServiceFactory = ({ + acmeChallengeDAL +}: TPkiAcmeChallengeServiceFactoryDep): TPkiAcmeChallengeServiceFactory => { + const appCfg = getConfig(); + + const validateChallengeResponse = async (challengeId: string): Promise => { + return await acmeChallengeDAL.transaction(async (tx) => { + const challenge = await acmeChallengeDAL.findByIdWithAuthForUpdate(challengeId, tx); + if (!challenge) { + throw new NotFoundError({ message: "ACME challenge not found" }); + } + if (challenge.status !== AcmeChallengeStatus.Processing) { + throw new BadRequestError({ + message: `ACME challenge is ${challenge.status} instead of ${AcmeChallengeStatus.Processing}` + }); + } + if (challenge.auth.expiresAt < new Date()) { + throw new BadRequestError({ message: "ACME auth has expired" }); + } + if (challenge.auth.status !== AcmeAuthStatus.Pending) { + throw new BadRequestError({ + message: `ACME auth status is ${challenge.auth.status} instead of ${AcmeAuthStatus.Pending}` + }); + } + + // TODO: support other challenge types here. Currently only HTTP-01 is supported + if (challenge.type !== AcmeChallengeType.HTTP_01) { + throw new BadRequestError({ message: "Only HTTP-01 challenges are supported for now" }); + } + const baseUrl = `http://${challenge.auth.identifierValue}`; + const actualBaseUrl = appCfg.isAcmeDevelopmentMode + ? `${baseUrl}:${appCfg.ACME_DEVELOPMENT_HTTP01_CHALLENGE_PORT}` + : baseUrl; + + const challengeUrl = new URL(`/.well-known/acme-challenge/${challenge.token}`, actualBaseUrl); + const challengeResponse = await fetch(challengeUrl); + if (challengeResponse.status !== 200) { + throw new BadRequestError({ message: "ACME challenge response is not 200" }); + } + const challengeResponseBody = await challengeResponse.text(); + const expectedChallengeResponseBody = `${challenge.token}.${challenge.auth.identifierValue}`; + }); + }; + + return { validateChallengeResponse }; +}; diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index d2f96deb4..bb9a15cbb 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -174,3 +174,5 @@ export type TPkiAcmeServiceFactory = { challengeId: string; }) => Promise>; }; + +export type TPkiAcmeChallengeServiceFactory = {}; diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 9fc4cff92..7477bc032 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -106,6 +106,8 @@ const envSchema = z HTTPS_ENABLED: zodStrBool, ROTATION_DEVELOPMENT_MODE: zodStrBool.default("false").optional(), DAILY_RESOURCE_CLEAN_UP_DEVELOPMENT_MODE: zodStrBool.default("false").optional(), + ACME_DEVELOPMENT_MODE: zodStrBool.default("false").optional(), + ACME_DEVELOPMENT_HTTP01_CHALLENGE_PORT: z.coerce.number().default(8087), // smtp options SMTP_HOST: zpStr(z.string().optional()), SMTP_IGNORE_TLS: zodStrBool.default("false"), @@ -384,6 +386,7 @@ const envSchema = z (data.NODE_ENV === "development" && data.ROTATION_DEVELOPMENT_MODE) || data.NODE_ENV === "test", isDailyResourceCleanUpDevelopmentMode: data.NODE_ENV === "development" && data.DAILY_RESOURCE_CLEAN_UP_DEVELOPMENT_MODE, + isAcmeDevelopmentMode: data.NODE_ENV === "development" && data.ACME_DEVELOPMENT_MODE, isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED, isRedisSentinelMode: Boolean(data.REDIS_SENTINEL_HOSTS), REDIS_SENTINEL_HOSTS: data.REDIS_SENTINEL_HOSTS?.trim() From 27e88d186d5841741ab9482e14ec7ef3419d6c85 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 10:04:00 -0700 Subject: [PATCH 170/294] Use thumbprint instead --- .../services/pki-acme/pki-acme-account-dal.ts | 14 +++++++---- .../ee/services/pki-acme/pki-acme-service.ts | 23 +++++++++++++++---- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts index bbfe042ee..685d07f2b 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts @@ -20,19 +20,23 @@ export const pkiAcmeAccountDALFactory = (db: TDbClient) => { } }; - const findByPublicKey = async (profileId: string, alg: string, publicKey: unknown, tx?: Knex) => { + const findByProfileIdAndPublicKeyThumbprintAndAlg = async ( + profileId: string, + alg: string, + publicKeyThumbprint: string, + tx?: Knex + ) => { try { - const account = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId, alg, publicKey }).first(); - + const account = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId, alg, publicKeyThumbprint }).first(); return account || null; } catch (error) { - throw new DatabaseError({ error, name: "Find PKI ACME account by public key and alg" }); + throw new DatabaseError({ error, name: "Find PKI ACME account by profile id, public key thumbprint and alg" }); } }; return { ...pkiAcmeAccountOrm, findByProjectIdAndAccountId, - findByPublicKey + findByProfileIdAndPublicKeyThumbprintAndAlg }; }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index d47d480f0..b165df639 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -11,7 +11,14 @@ import { EnrollmentType, TCertificateProfileWithConfigs } from "@app/services/certificate-profile/certificate-profile-types"; -import { errors, flattenedVerify, FlattenedVerifyResult, importJWK, JWSHeaderParameters } from "jose"; +import { + calculateJwkThumbprint, + errors, + flattenedVerify, + FlattenedVerifyResult, + importJWK, + JWSHeaderParameters +} from "jose"; import { z, ZodError } from "zod"; import { TPkiAcmeAccountDALFactory } from "./pki-acme-account-dal"; import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal"; @@ -57,7 +64,10 @@ import { type TPkiAcmeServiceFactoryDep = { certificateProfileDAL: Pick; - acmeAccountDAL: Pick; + acmeAccountDAL: Pick< + TPkiAcmeAccountDALFactory, + "findByProjectIdAndAccountId" | "findByProfileIdAndPublicKeyThumbprintAndAlg" | "create" + >; acmeOrderDAL: Pick; acmeAuthDAL: Pick; acmeOrderAuthDAL: Pick; @@ -286,8 +296,12 @@ export const pkiAcmeServiceFactory = ({ payload: TCreateAcmeAccountPayload; }): Promise> => { const profile = await validateAcmeProfile(profileId); - // TODO: ensure unique account per public key - const existingAccount: TPkiAcmeAccounts | null = await acmeAccountDAL.findByPublicKey(profileId, alg, jwk); + const publicKeyThumbprint = await calculateJwkThumbprint(jwk, "sha256"); + const existingAccount: TPkiAcmeAccounts | null = await acmeAccountDAL.findByProfileIdAndPublicKeyThumbprintAndAlg( + profileId, + alg, + publicKeyThumbprint + ); if (onlyReturnExisting && !existingAccount) { throw new AcmeAccountDoesNotExistError({ message: "ACME account not found" }); } @@ -311,6 +325,7 @@ export const pkiAcmeServiceFactory = ({ profileId: profile.id, alg, publicKey: jwk, + publicKeyThumbprint, emails: contact ?? [] }); // TODO: create audit log here From 2cb38dae055298f93d67fcb2b1001c6ef90b0179 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 10:47:45 -0700 Subject: [PATCH 171/294] Add missing imports --- backend/src/@types/knex.d.ts | 21 ++++++++++++-- backend/src/db/schemas/models.ts | 14 +++++---- .../pki-acme/pki-acme-challenge-dal.ts | 29 +++++++++++++++---- .../pki-acme/pki-acme-challenge-service.ts | 4 +-- .../services/pki-acme/pki-acme-order-dal.ts | 14 +++++++-- 5 files changed, 62 insertions(+), 20 deletions(-) diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 1daf722a1..580da4749 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -266,6 +266,24 @@ import { TOrgRoles, TOrgRolesInsert, TOrgRolesUpdate, + TPkiAcmeAccounts, + TPkiAcmeAccountsInsert, + TPkiAcmeAccountsUpdate, + TPkiAcmeAuths, + TPkiAcmeAuthsInsert, + TPkiAcmeAuthsUpdate, + TPkiAcmeChallenges, + TPkiAcmeChallengesInsert, + TPkiAcmeChallengesUpdate, + TPkiAcmeEnrollmentConfigs, + TPkiAcmeEnrollmentConfigsInsert, + TPkiAcmeEnrollmentConfigsUpdate, + TPkiAcmeOrderAuths, + TPkiAcmeOrderAuthsInsert, + TPkiAcmeOrderAuthsUpdate, + TPkiAcmeOrders, + TPkiAcmeOrdersInsert, + TPkiAcmeOrdersUpdate, TPkiAlerts, TPkiAlertsInsert, TPkiAlertsUpdate, @@ -287,9 +305,6 @@ import { TPkiEstEnrollmentConfigs, TPkiEstEnrollmentConfigsInsert, TPkiEstEnrollmentConfigsUpdate, - TPkiAcmeEnrollmentConfigs, - TPkiAcmeEnrollmentConfigsInsert, - TPkiAcmeEnrollmentConfigsUpdate, TPkiSubscribers, TPkiSubscribersInsert, TPkiSubscribersUpdate, diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index a031c83b9..1170138a3 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -28,11 +28,6 @@ export enum TableName { PkiEstEnrollmentConfig = "pki_est_enrollment_configs", PkiApiEnrollmentConfig = "pki_api_enrollment_configs", PkiAcmeEnrollmentConfig = "pki_acme_enrollment_configs", - PkiAcmeAccount = "pki_acme_accounts", - PkiAcmeOrder = "pki_acme_orders", - PkiAcmeOrderAuth = "pki_acme_order_auths", - PkiAcmeAuth = "pki_acme_auths", - PkiAcmeChallenge = "pki_acme_challenges", PkiSubscriber = "pki_subscribers", PkiAlert = "pki_alerts", PkiCollection = "pki_collections", @@ -216,7 +211,14 @@ export enum TableName { PamAccount = "pam_accounts", PamSession = "pam_sessions", - VaultExternalMigrationConfig = "vault_external_migration_configs" + VaultExternalMigrationConfig = "vault_external_migration_configs", + + // PKI ACME + PkiAcmeAccount = "pki_acme_accounts", + PkiAcmeOrder = "pki_acme_orders", + PkiAcmeOrderAuth = "pki_acme_order_auths", + PkiAcmeAuth = "pki_acme_auths", + PkiAcmeChallenge = "pki_acme_challenges" } export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt" | "commitId"; diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts index ae5c1f994..3103fec7e 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts @@ -1,5 +1,5 @@ import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; +import { TableName, TPkiAcmeAccounts, TPkiAcmeAuths } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { Knex } from "knex"; @@ -34,9 +34,14 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { throw new DatabaseError({ error, name: "Find PKI ACME challenge by account id, auth id and challenge id" }); } }; - const findByIdWithAuthForUpdate = async (id: string, tx?: Knex) => { + const findByIdForChallengeValidation = async (id: string, tx?: Knex) => { const rows = await (tx || db)(TableName.PkiAcmeChallenge) - .join(TableName.PkiAcmeAuth, `${TableName.PkiAcmeChallenge}.authId`, `${TableName.PkiAcmeAuth}.id`) + .join(TableName.PkiAcmeAuth, `${TableName.PkiAcmeChallenge}.authId`, `${TableName.PkiAcmeAuth}.id`) + .join( + TableName.PkiAcmeAccount, + `${TableName.PkiAcmeAuth}.accountId`, + `${TableName.PkiAcmeAccount}.id` + ) .select( selectAllTableCols(TableName.PkiAcmeChallenge), db.ref("id").withSchema(TableName.PkiAcmeAuth).as("authId"), @@ -44,7 +49,9 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { db.ref("status").withSchema(TableName.PkiAcmeAuth).as("authStatus"), db.ref("identifierType").withSchema(TableName.PkiAcmeAuth).as("authIdentifierType"), db.ref("identifierValue").withSchema(TableName.PkiAcmeAuth).as("authIdentifierValue"), - db.ref("expiresAt").withSchema(TableName.PkiAcmeAuth).as("authExpiresAt") + db.ref("expiresAt").withSchema(TableName.PkiAcmeAuth).as("authExpiresAt"), + db.ref("id").withSchema(TableName.PkiAcmeAccount).as("accountId"), + db.ref("publicKeyThumbprint").withSchema(TableName.PkiAcmeAccount).as("accountPublicKeyThumbprint") ) // For all challenges, acquire update lock on the auth to avoid race conditions .forUpdate(TableName.PkiAcmeAuth) @@ -68,7 +75,17 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { identifierType: authIdentifierType, identifierValue: authIdentifierValue, expiresAt: authExpiresAt - }) + }), + childrenMapper: [ + { + key: "accountId", + label: "account" as const, + mapper: ({ accountId, accountPublicKeyThumbprint }) => ({ + id: accountId, + publicKeyThumbprint: accountPublicKeyThumbprint + }) + } + ] } ] })?.[0]; @@ -77,6 +94,6 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { return { ...pkiAcmeChallengeOrm, findByAccountAuthAndChallengeIdWithToken, - findByIdWithAuthForUpdate + findByIdForChallengeValidation }; }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts index 6e077d0c4..feaceb92f 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts @@ -6,7 +6,7 @@ import { getConfig } from "@app/lib/config/env"; import { calculateJwkThumbprint } from "jose"; type TPkiAcmeChallengeServiceFactoryDep = { - acmeChallengeDAL: Pick; + acmeChallengeDAL: Pick; }; export const pkiAcmeChallengeServiceFactory = ({ @@ -16,7 +16,7 @@ export const pkiAcmeChallengeServiceFactory = ({ const validateChallengeResponse = async (challengeId: string): Promise => { return await acmeChallengeDAL.transaction(async (tx) => { - const challenge = await acmeChallengeDAL.findByIdWithAuthForUpdate(challengeId, tx); + const challenge = await acmeChallengeDAL.findByIdForChallengeValidation(challengeId, tx); if (!challenge) { throw new NotFoundError({ message: "ACME challenge not found" }); } diff --git a/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts index d3b7466e4..cf544876c 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts @@ -1,7 +1,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; +import { TableName, TPkiAcmeAuths, TPkiAcmeOrderAuths } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; @@ -13,8 +13,16 @@ export const pkiAcmeOrderDALFactory = (db: TDbClient) => { const findByAccountAndOrderIdWithAuthorizations = async (accountId: string, orderId: string, tx?: Knex) => { try { const rows = await (tx || db)(TableName.PkiAcmeOrder) - .join(TableName.PkiAcmeOrderAuth, `${TableName.PkiAcmeOrderAuth}.orderId`, `${TableName.PkiAcmeOrder}.id`) - .join(TableName.PkiAcmeAuth, `${TableName.PkiAcmeOrderAuth}.authId`, `${TableName.PkiAcmeAuth}.id`) + .join( + TableName.PkiAcmeOrderAuth, + `${TableName.PkiAcmeOrderAuth}.orderId`, + `${TableName.PkiAcmeOrder}.id` + ) + .join( + TableName.PkiAcmeAuth, + `${TableName.PkiAcmeOrderAuth}.authId`, + `${TableName.PkiAcmeAuth}.id` + ) .select( selectAllTableCols(TableName.PkiAcmeOrder), db.ref("id").withSchema(TableName.PkiAcmeAuth).as("authId"), From bc00710df303a43bda28d80d6754850ed349beb5 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 13:11:03 -0700 Subject: [PATCH 172/294] Add queue stuff --- .../migrations/20251029234547_add-pki-acme.ts | 2 +- backend/src/db/schemas/pki-acme-auths.ts | 2 +- .../pki-acme/pki-acme-challenge-dal.ts | 83 ++++++++----------- .../pki-acme/pki-acme-challenge-queue.ts | 56 +++++++++++++ .../pki-acme/pki-acme-challenge-service.ts | 30 +++++-- .../services/pki-acme/pki-acme-order-dal.ts | 12 +-- .../ee/services/pki-acme/pki-acme-service.ts | 47 +++++++++-- .../ee/services/pki-acme/pki-acme-types.ts | 4 +- backend/src/queue/queue-service.ts | 11 ++- 9 files changed, 171 insertions(+), 76 deletions(-) create mode 100644 backend/src/ee/services/pki-acme/pki-acme-challenge-queue.ts diff --git a/backend/src/db/migrations/20251029234547_add-pki-acme.ts b/backend/src/db/migrations/20251029234547_add-pki-acme.ts index ed9353bce..c91a179a8 100644 --- a/backend/src/db/migrations/20251029234547_add-pki-acme.ts +++ b/backend/src/db/migrations/20251029234547_add-pki-acme.ts @@ -107,7 +107,7 @@ export async function up(knex: Knex): Promise { t.string("status").notNullable(); // pending, valid, invalid, deactivated, expired, revoked // Token used to validate the authorization through ACME challenge - t.timestamp("token").nullable(); + t.string("token").nullable(); // Identifier type and value t.string("identifierType").notNullable(); // dns diff --git a/backend/src/db/schemas/pki-acme-auths.ts b/backend/src/db/schemas/pki-acme-auths.ts index 6d91bb387..7f20e0f24 100644 --- a/backend/src/db/schemas/pki-acme-auths.ts +++ b/backend/src/db/schemas/pki-acme-auths.ts @@ -11,7 +11,7 @@ export const PkiAcmeAuthsSchema = z.object({ id: z.string().uuid(), accountId: z.string().uuid(), status: z.string(), - token: z.date().nullable().optional(), + token: z.string().nullable().optional(), identifierType: z.string(), identifierValue: z.string(), expiresAt: z.date(), diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts index 3103fec7e..28d1c318a 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts @@ -9,19 +9,11 @@ export type TPkiAcmeChallengeDALFactory = ReturnType { const pkiAcmeChallengeOrm = ormify(db, TableName.PkiAcmeChallenge); - const findByAccountAuthAndChallengeIdWithToken = async ( - accountId: string, - authId: string, - challengeId: string, - tx?: Knex - ) => { + const findByAccountAuthAndChallengeId = async (accountId: string, authId: string, challengeId: string, tx?: Knex) => { try { const challenge = await (tx || db)(TableName.PkiAcmeChallenge) .join(TableName.PkiAcmeAuth, `${TableName.PkiAcmeChallenge}.authId`, `${TableName.PkiAcmeAuth}.id`) - .select( - selectAllTableCols(TableName.PkiAcmeChallenge), - db.ref("token").withSchema(TableName.PkiAcmeAuth).as("token") - ) + .select(selectAllTableCols(TableName.PkiAcmeChallenge)) .where(`${TableName.PkiAcmeChallenge}.id`, challengeId) .where(`${TableName.PkiAcmeChallenge}.authId`, authId) .where(`${TableName.PkiAcmeAuth}.accountId`, accountId) @@ -34,14 +26,11 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { throw new DatabaseError({ error, name: "Find PKI ACME challenge by account id, auth id and challenge id" }); } }; + const findByIdForChallengeValidation = async (id: string, tx?: Knex) => { - const rows = await (tx || db)(TableName.PkiAcmeChallenge) - .join(TableName.PkiAcmeAuth, `${TableName.PkiAcmeChallenge}.authId`, `${TableName.PkiAcmeAuth}.id`) - .join( - TableName.PkiAcmeAccount, - `${TableName.PkiAcmeAuth}.accountId`, - `${TableName.PkiAcmeAccount}.id` - ) + const result = await (tx || db)(TableName.PkiAcmeChallenge) + .join(TableName.PkiAcmeAuth, `${TableName.PkiAcmeChallenge}.authId`, `${TableName.PkiAcmeAuth}.id`) + .join(TableName.PkiAcmeAccount, `${TableName.PkiAcmeAuth}.accountId`, `${TableName.PkiAcmeAccount}.id`) .select( selectAllTableCols(TableName.PkiAcmeChallenge), db.ref("id").withSchema(TableName.PkiAcmeAuth).as("authId"), @@ -55,45 +44,41 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { ) // For all challenges, acquire update lock on the auth to avoid race conditions .forUpdate(TableName.PkiAcmeAuth) - .where(`${TableName.PkiAcmeChallenge}.id`, id); - - if (rows.length === 0) { + .where(`${TableName.PkiAcmeChallenge}.id`, id) + .first(); + if (!result) { return null; } - return sqlNestRelationships({ - data: rows, - key: "id", - parentMapper: (row) => row, - childrenMapper: [ - { - key: "authId", - label: "auth" as const, - mapper: ({ authId, authToken, authStatus, authIdentifierType, authIdentifierValue, authExpiresAt }) => ({ - id: authId, - token: authToken, - status: authStatus, - identifierType: authIdentifierType, - identifierValue: authIdentifierValue, - expiresAt: authExpiresAt - }), - childrenMapper: [ - { - key: "accountId", - label: "account" as const, - mapper: ({ accountId, accountPublicKeyThumbprint }) => ({ - id: accountId, - publicKeyThumbprint: accountPublicKeyThumbprint - }) - } - ] + const { + authId, + authToken, + authStatus, + authIdentifierType, + authIdentifierValue, + authExpiresAt, + accountId, + accountPublicKeyThumbprint, + ...challenge + } = result; + return { + ...challenge, + auth: { + token: authToken, + status: authStatus, + identifierType: authIdentifierType, + identifierValue: authIdentifierValue, + expiresAt: authExpiresAt, + account: { + id: accountId, + publicKeyThumbprint: accountPublicKeyThumbprint } - ] - })?.[0]; + } + }; }; return { ...pkiAcmeChallengeOrm, - findByAccountAuthAndChallengeIdWithToken, + findByAccountAuthAndChallengeId, findByIdForChallengeValidation }; }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-queue.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-queue.ts new file mode 100644 index 000000000..1c42f6327 --- /dev/null +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-queue.ts @@ -0,0 +1,56 @@ +import { Knex } from "knex"; + +import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; + +import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal"; +import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal"; +import { TPkiAcmeChallengeServiceFactory } from "./pki-acme-types"; + +// Define types for job data +export type TValidateAcmeChallengeResponseDTO = { + challengeId: string; +}; + +type TChallengeQueueServiceFactoryDep = { + queueService: Pick; + acmeChallengeDAL: Pick; + acmeAuthDAL: Pick; + acmeChallengeService: TPkiAcmeChallengeServiceFactory; +}; + +export type TFolderCommitQueueServiceFactory = ReturnType; + +export const challengeQueueServiceFactory = ({ + queueService, + acmeChallengeService +}: TChallengeQueueServiceFactoryDep) => { + const scheduleChallengeValidation = async (payload: TValidateAcmeChallengeResponseDTO) => { + const { challengeId } = payload; + await queueService.queuePg(QueueJobs.ValidateAcmeChallengeResponse, payload, { + // TODO: maybe we should retry, but let's keep it simple for now + }); + }; + + const validateAcmeChallengeResponse = async (jobData: TValidateAcmeChallengeResponseDTO, tx?: Knex) => { + const { challengeId } = jobData; + await acmeChallengeService.validateChallengeResponse(challengeId); + }; + + const init = async () => { + await queueService.startPg( + QueueJobs.ValidateAcmeChallengeResponse, + async ([job]) => { + await validateAcmeChallengeResponse(job.data as TValidateAcmeChallengeResponseDTO); + }, + { + workerCount: 5, + pollingIntervalSeconds: 30 + } + ); + }; + + return { + scheduleChallengeValidation, + init + }; +}; diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts index feaceb92f..03a5c1d30 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts @@ -1,21 +1,25 @@ +import { Knex } from "knex"; + +import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal"; import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal"; import { AcmeAuthStatus, AcmeChallengeStatus, AcmeChallengeType } from "./pki-acme-schemas"; import { TPkiAcmeChallengeServiceFactory } from "./pki-acme-types"; -import { getConfig } from "@app/lib/config/env"; -import { calculateJwkThumbprint } from "jose"; type TPkiAcmeChallengeServiceFactoryDep = { - acmeChallengeDAL: Pick; + acmeAuthDAL: Pick; + acmeChallengeDAL: Pick; }; export const pkiAcmeChallengeServiceFactory = ({ + acmeAuthDAL, acmeChallengeDAL }: TPkiAcmeChallengeServiceFactoryDep): TPkiAcmeChallengeServiceFactory => { const appCfg = getConfig(); const validateChallengeResponse = async (challengeId: string): Promise => { - return await acmeChallengeDAL.transaction(async (tx) => { + return await acmeChallengeDAL.transaction(async (tx: Knex) => { const challenge = await acmeChallengeDAL.findByIdForChallengeValidation(challengeId, tx); if (!challenge) { throw new NotFoundError({ message: "ACME challenge not found" }); @@ -43,13 +47,27 @@ export const pkiAcmeChallengeServiceFactory = ({ ? `${baseUrl}:${appCfg.ACME_DEVELOPMENT_HTTP01_CHALLENGE_PORT}` : baseUrl; - const challengeUrl = new URL(`/.well-known/acme-challenge/${challenge.token}`, actualBaseUrl); + const challengeUrl = new URL(`/.well-known/acme-challenge/${challenge.auth.token}`, actualBaseUrl); + // Notice: well, we are in a transaction, ideally we should not hold transaction and perform + // a long running operation for long time. But assuming we are not performing a tons of + // challenge validation at the same time, it should be fine. + // TODO: bound it with timeout of the fetch request const challengeResponse = await fetch(challengeUrl); if (challengeResponse.status !== 200) { throw new BadRequestError({ message: "ACME challenge response is not 200" }); } const challengeResponseBody = await challengeResponse.text(); - const expectedChallengeResponseBody = `${challenge.token}.${challenge.auth.identifierValue}`; + const thumbprint = Buffer.from(challenge.auth.account.publicKeyThumbprint, "utf-8").toString("base64url"); + const expectedChallengeResponseBody = `${challenge.auth.token}.${thumbprint}`; + if (challengeResponseBody !== expectedChallengeResponseBody) { + throw new BadRequestError({ message: "ACME challenge response is not correct" }); + } + await acmeChallengeDAL.updateById( + challengeId, + { status: AcmeChallengeStatus.Valid, validatedAt: new Date() }, + tx + ); + await acmeAuthDAL.updateById(challenge.auth.account.id, { status: AcmeAuthStatus.Valid }, tx); }); }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts index cf544876c..47d72a261 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts @@ -13,16 +13,8 @@ export const pkiAcmeOrderDALFactory = (db: TDbClient) => { const findByAccountAndOrderIdWithAuthorizations = async (accountId: string, orderId: string, tx?: Knex) => { try { const rows = await (tx || db)(TableName.PkiAcmeOrder) - .join( - TableName.PkiAcmeOrderAuth, - `${TableName.PkiAcmeOrderAuth}.orderId`, - `${TableName.PkiAcmeOrder}.id` - ) - .join( - TableName.PkiAcmeAuth, - `${TableName.PkiAcmeOrderAuth}.authId`, - `${TableName.PkiAcmeAuth}.id` - ) + .join(TableName.PkiAcmeOrderAuth, `${TableName.PkiAcmeOrderAuth}.orderId`, `${TableName.PkiAcmeOrder}.id`) + .join(TableName.PkiAcmeAuth, `${TableName.PkiAcmeOrderAuth}.authId`, `${TableName.PkiAcmeAuth}.id`) .select( selectAllTableCols(TableName.PkiAcmeOrder), db.ref("id").withSchema(TableName.PkiAcmeAuth).as("authId"), diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index b165df639..335af2825 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -2,7 +2,7 @@ import { TPkiAcmeAccounts } from "@app/db/schemas/pki-acme-accounts"; import { TPkiAcmeAuths } from "@app/db/schemas/pki-acme-auths"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; -import { NotFoundError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; @@ -71,7 +71,10 @@ type TPkiAcmeServiceFactoryDep = { acmeOrderDAL: Pick; acmeAuthDAL: Pick; acmeOrderAuthDAL: Pick; - acmeChallengeDAL: Pick; + acmeChallengeDAL: Pick< + TPkiAcmeChallengeDALFactory, + "create" | "transaction" | "updateById" | "findByAccountAuthAndChallengeId" | "findByIdForChallengeValidation" + >; }; export const pkiAcmeServiceFactory = ({ @@ -575,7 +578,7 @@ export const pkiAcmeServiceFactory = ({ type: auth.identifierType, value: auth.identifierValue }, - challenges: auth.challenges.map((challenge: TPkiAcmeChallenges) => { + challenges: auth.challenges.map((challenge) => { return { type: challenge.type, url: buildUrl(profileId, `/authorizations/${authzId}/challenges/${challenge.id}`), @@ -602,10 +605,42 @@ export const pkiAcmeServiceFactory = ({ authzId: string; challengeId: string; }): Promise> => { - const challenge = await acmeChallengeDAL.findByAccountAuthAndChallengeIdWithToken(accountId, authzId, challengeId); - if (!challenge) { + const result = await acmeChallengeDAL.findByAccountAuthAndChallengeId(accountId, authzId, challengeId); + if (!result) { throw new NotFoundError({ message: "ACME challenge not found" }); } + const challenge = await acmeChallengeDAL.transaction(async (tx) => { + const challenge = await acmeChallengeDAL.findByIdForChallengeValidation(challengeId, tx); + if (!challenge) { + throw new NotFoundError({ message: "ACME challenge not found" }); + } + if (challenge.status !== AcmeChallengeStatus.Pending) { + // Ideally this should be an ACME error, but RFC 8555 doesn't say much about corner cases like this... + throw new BadRequestError({ + message: `ACME challenge is ${challenge.status} instead of ${AcmeChallengeStatus.Pending}` + }); + } + if (challenge.auth.expiresAt < new Date()) { + throw new BadRequestError({ message: "ACME auth has expired" }); + } + if (challenge.auth.status !== AcmeAuthStatus.Pending) { + throw new BadRequestError({ + message: `ACME auth status is ${challenge.auth.status} instead of ${AcmeAuthStatus.Pending}` + }); + } + if (!challenge.auth.token) { + throw new AcmeServerInternalError({ message: "ACME challenge token is required" }); + } + const updatedChallenge = await acmeChallengeDAL.updateById( + challengeId, + { status: AcmeChallengeStatus.Pending }, + tx + ); + return { + ...challenge, + ...updatedChallenge + }; + }); // TODO: Implement ACME challenge response return { status: 200, @@ -613,7 +648,7 @@ export const pkiAcmeServiceFactory = ({ type: challenge.type, url: buildUrl(profileId, `/authorizations/${authzId}/challenges/${challengeId}`), status: challenge.status, - token: challenge.token + token: challenge.auth.token! }, headers: { Location: buildUrl(profileId, `/authorizations/${authzId}/challenges/${challengeId}`), diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index bb9a15cbb..c993acf64 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -175,4 +175,6 @@ export type TPkiAcmeServiceFactory = { }) => Promise>; }; -export type TPkiAcmeChallengeServiceFactory = {}; +export type TPkiAcmeChallengeServiceFactory = { + validateChallengeResponse: (challengeId: string) => Promise; +}; diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts index 3e6b1dd19..e0bd0204c 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -42,6 +42,7 @@ import { } from "@app/services/secret-sync/secret-sync-types"; import { CacheType } from "@app/services/super-admin/super-admin-types"; import { TWebhookPayloads } from "@app/services/webhook/webhook-types"; +import { TValidateAcmeChallengeResponseDTO } from "@app/ee/services/pki-acme/pki-acme-challenge-queue"; export enum QueueName { SecretRotation = "secret-rotation", @@ -79,7 +80,8 @@ export enum QueueName { UserNotification = "user-notification", HealthAlert = "health-alert", CertificateV3AutoRenewal = "certificate-v3-auto-renewal", - PamAccountRotation = "pam-account-rotation" + PamAccountRotation = "pam-account-rotation", + PkiAcmeChallengeValidation = "pki-acme-challenge-validation" } export enum QueueJobs { @@ -130,7 +132,8 @@ export enum QueueJobs { UserNotification = "user-notification-job", HealthAlert = "health-alert", CertificateV3DailyAutoRenewal = "certificate-v3-daily-auto-renewal", - PamAccountRotation = "pam-account-rotation" + PamAccountRotation = "pam-account-rotation", + ValidateAcmeChallengeResponse = "validate-acme-challenge-response" } export type TQueueJobTypes = { @@ -369,6 +372,10 @@ export type TQueueJobTypes = { name: QueueJobs.PamAccountRotation; payload: undefined; }; + [QueueName.PkiAcmeChallengeValidation]: { + name: QueueJobs.ValidateAcmeChallengeResponse; + payload: TValidateAcmeChallengeResponseDTO; + }; }; const SECRET_SCANNING_JOBS = [ From 0613a497950a365191b398783113d3ef989645d7 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 13:23:30 -0700 Subject: [PATCH 173/294] More challenge logic --- .../pki-acme/pki-acme-challenge-service.ts | 56 +++++++++++-------- .../ee/services/pki-acme/pki-acme-service.ts | 3 + 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts index 03a5c1d30..9091b6926 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts @@ -2,6 +2,7 @@ import { Knex } from "knex"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal"; import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal"; import { AcmeAuthStatus, AcmeChallengeStatus, AcmeChallengeType } from "./pki-acme-schemas"; @@ -18,15 +19,16 @@ export const pkiAcmeChallengeServiceFactory = ({ }: TPkiAcmeChallengeServiceFactoryDep): TPkiAcmeChallengeServiceFactory => { const appCfg = getConfig(); - const validateChallengeResponse = async (challengeId: string): Promise => { + const validateChallengeResponse = async (challengeId: string, tx?: Knex): Promise => { return await acmeChallengeDAL.transaction(async (tx: Knex) => { + logger.info({ challengeId }, "Validating ACME challenge response"); const challenge = await acmeChallengeDAL.findByIdForChallengeValidation(challengeId, tx); if (!challenge) { throw new NotFoundError({ message: "ACME challenge not found" }); } - if (challenge.status !== AcmeChallengeStatus.Processing) { + if (challenge.status !== AcmeChallengeStatus.Pending) { throw new BadRequestError({ - message: `ACME challenge is ${challenge.status} instead of ${AcmeChallengeStatus.Processing}` + message: `ACME challenge is ${challenge.status} instead of ${AcmeChallengeStatus.Pending}` }); } if (challenge.auth.expiresAt < new Date()) { @@ -46,28 +48,36 @@ export const pkiAcmeChallengeServiceFactory = ({ const actualBaseUrl = appCfg.isAcmeDevelopmentMode ? `${baseUrl}:${appCfg.ACME_DEVELOPMENT_HTTP01_CHALLENGE_PORT}` : baseUrl; - const challengeUrl = new URL(`/.well-known/acme-challenge/${challenge.auth.token}`, actualBaseUrl); - // Notice: well, we are in a transaction, ideally we should not hold transaction and perform - // a long running operation for long time. But assuming we are not performing a tons of - // challenge validation at the same time, it should be fine. - // TODO: bound it with timeout of the fetch request - const challengeResponse = await fetch(challengeUrl); - if (challengeResponse.status !== 200) { - throw new BadRequestError({ message: "ACME challenge response is not 200" }); + try { + // Notice: well, we are in a transaction, ideally we should not hold transaction and perform + // a long running operation for long time. But assuming we are not performing a tons of + // challenge validation at the same time, it should be fine. + // TODO: bound it with timeout of the fetch request + const challengeResponse = await fetch(challengeUrl); + if (challengeResponse.status !== 200) { + throw new BadRequestError({ message: "ACME challenge response is not 200" }); + } + const challengeResponseBody = await challengeResponse.text(); + const thumbprint = Buffer.from(challenge.auth.account.publicKeyThumbprint, "utf-8").toString("base64url"); + const expectedChallengeResponseBody = `${challenge.auth.token}.${thumbprint}`; + if (challengeResponseBody !== expectedChallengeResponseBody) { + throw new BadRequestError({ message: "ACME challenge response is not correct" }); + } + await acmeChallengeDAL.updateById( + challengeId, + { status: AcmeChallengeStatus.Valid, validatedAt: new Date() }, + tx + ); + await acmeAuthDAL.updateById(challenge.auth.account.id, { status: AcmeAuthStatus.Valid }, tx); + await acmeAuthDAL.updateById(challenge.auth.account.id, { status: AcmeAuthStatus.Valid }, tx); + } catch (error) { + logger.error(error, "Error validating ACME challenge response"); + // TODO: we should retry the challenge validation a few times, but let's keep it simple for now + await acmeChallengeDAL.updateById(challengeId, { status: AcmeChallengeStatus.Invalid }, tx); + await acmeAuthDAL.updateById(challenge.auth.account.id, { status: AcmeAuthStatus.Invalid }, tx); + throw error; } - const challengeResponseBody = await challengeResponse.text(); - const thumbprint = Buffer.from(challenge.auth.account.publicKeyThumbprint, "utf-8").toString("base64url"); - const expectedChallengeResponseBody = `${challenge.auth.token}.${thumbprint}`; - if (challengeResponseBody !== expectedChallengeResponseBody) { - throw new BadRequestError({ message: "ACME challenge response is not correct" }); - } - await acmeChallengeDAL.updateById( - challengeId, - { status: AcmeChallengeStatus.Valid, validatedAt: new Date() }, - tx - ); - await acmeAuthDAL.updateById(challenge.auth.account.id, { status: AcmeAuthStatus.Valid }, tx); }); }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 335af2825..8cc96c1b0 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -631,6 +631,9 @@ export const pkiAcmeServiceFactory = ({ if (!challenge.auth.token) { throw new AcmeServerInternalError({ message: "ACME challenge token is required" }); } + if (challenge.type !== AcmeChallengeType.HTTP_01) { + throw new BadRequestError({ message: "Only HTTP-01 challenges are supported for now" }); + } const updatedChallenge = await acmeChallengeDAL.updateById( challengeId, { status: AcmeChallengeStatus.Pending }, From aa62e465a0a2d49c4e84897c89390c7713c2ecdd Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 13:32:29 -0700 Subject: [PATCH 174/294] Revert queue stuff --- .../pki-acme/pki-acme-challenge-queue.ts | 56 ------------------- .../pki-acme/pki-acme-challenge-service.ts | 6 +- .../ee/services/pki-acme/pki-acme-errors.ts | 24 ++++++++ .../ee/services/pki-acme/pki-acme-service.ts | 34 ++--------- backend/src/queue/queue-service.ts | 11 +--- 5 files changed, 34 insertions(+), 97 deletions(-) delete mode 100644 backend/src/ee/services/pki-acme/pki-acme-challenge-queue.ts diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-queue.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-queue.ts deleted file mode 100644 index 1c42f6327..000000000 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-queue.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Knex } from "knex"; - -import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; - -import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal"; -import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal"; -import { TPkiAcmeChallengeServiceFactory } from "./pki-acme-types"; - -// Define types for job data -export type TValidateAcmeChallengeResponseDTO = { - challengeId: string; -}; - -type TChallengeQueueServiceFactoryDep = { - queueService: Pick; - acmeChallengeDAL: Pick; - acmeAuthDAL: Pick; - acmeChallengeService: TPkiAcmeChallengeServiceFactory; -}; - -export type TFolderCommitQueueServiceFactory = ReturnType; - -export const challengeQueueServiceFactory = ({ - queueService, - acmeChallengeService -}: TChallengeQueueServiceFactoryDep) => { - const scheduleChallengeValidation = async (payload: TValidateAcmeChallengeResponseDTO) => { - const { challengeId } = payload; - await queueService.queuePg(QueueJobs.ValidateAcmeChallengeResponse, payload, { - // TODO: maybe we should retry, but let's keep it simple for now - }); - }; - - const validateAcmeChallengeResponse = async (jobData: TValidateAcmeChallengeResponseDTO, tx?: Knex) => { - const { challengeId } = jobData; - await acmeChallengeService.validateChallengeResponse(challengeId); - }; - - const init = async () => { - await queueService.startPg( - QueueJobs.ValidateAcmeChallengeResponse, - async ([job]) => { - await validateAcmeChallengeResponse(job.data as TValidateAcmeChallengeResponseDTO); - }, - { - workerCount: 5, - pollingIntervalSeconds: 30 - } - ); - }; - - return { - scheduleChallengeValidation, - init - }; -}; diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts index 9091b6926..be46de35a 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts @@ -5,6 +5,7 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal"; import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal"; +import { AcmeIncorrectResponseError } from "./pki-acme-errors"; import { AcmeAuthStatus, AcmeChallengeStatus, AcmeChallengeType } from "./pki-acme-schemas"; import { TPkiAcmeChallengeServiceFactory } from "./pki-acme-types"; @@ -62,7 +63,7 @@ export const pkiAcmeChallengeServiceFactory = ({ const thumbprint = Buffer.from(challenge.auth.account.publicKeyThumbprint, "utf-8").toString("base64url"); const expectedChallengeResponseBody = `${challenge.auth.token}.${thumbprint}`; if (challengeResponseBody !== expectedChallengeResponseBody) { - throw new BadRequestError({ message: "ACME challenge response is not correct" }); + throw new AcmeIncorrectResponseError({ message: "ACME challenge response is not correct" }); } await acmeChallengeDAL.updateById( challengeId, @@ -70,12 +71,13 @@ export const pkiAcmeChallengeServiceFactory = ({ tx ); await acmeAuthDAL.updateById(challenge.auth.account.id, { status: AcmeAuthStatus.Valid }, tx); - await acmeAuthDAL.updateById(challenge.auth.account.id, { status: AcmeAuthStatus.Valid }, tx); + // TODO: trigger a check for order status as well } catch (error) { logger.error(error, "Error validating ACME challenge response"); // TODO: we should retry the challenge validation a few times, but let's keep it simple for now await acmeChallengeDAL.updateById(challengeId, { status: AcmeChallengeStatus.Invalid }, tx); await acmeAuthDAL.updateById(challenge.auth.account.id, { status: AcmeAuthStatus.Invalid }, tx); + // TODO: trigger a check for order status as well throw error; } }); diff --git a/backend/src/ee/services/pki-acme/pki-acme-errors.ts b/backend/src/ee/services/pki-acme/pki-acme-errors.ts index 85de5cb1c..b09499346 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-errors.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-errors.ts @@ -434,3 +434,27 @@ export class AcmeUserActionRequiredError extends AcmeError { }; } } + +/** + * incorrectResponse - The response is incorrect (RFC 8555 Section 6.7.16) + */ +export class AcmeIncorrectResponseError extends AcmeError { + constructor({ + detail = "The response is incorrect", + error, + message + }: { + detail?: string; + error?: unknown; + message?: string; + } = {}) { + super({ + type: "incorrectResponse", + detail, + status: 400, + error, + message + }); + this.name = "AcmeIncorrectResponseError"; + } +} diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 8cc96c1b0..a4916c555 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -75,6 +75,7 @@ type TPkiAcmeServiceFactoryDep = { TPkiAcmeChallengeDALFactory, "create" | "transaction" | "updateById" | "findByAccountAuthAndChallengeId" | "findByIdForChallengeValidation" >; + acmeChallengeService: TPkiAcmeChallengeServiceFactory; }; export const pkiAcmeServiceFactory = ({ @@ -83,7 +84,8 @@ export const pkiAcmeServiceFactory = ({ acmeOrderDAL, acmeAuthDAL, acmeOrderAuthDAL, - acmeChallengeDAL + acmeChallengeDAL, + acmeChallengeService }: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => { const validateAcmeProfile = async (profileId: string): Promise => { const profile = await certificateProfileDAL.findById(profileId); @@ -610,35 +612,7 @@ export const pkiAcmeServiceFactory = ({ throw new NotFoundError({ message: "ACME challenge not found" }); } const challenge = await acmeChallengeDAL.transaction(async (tx) => { - const challenge = await acmeChallengeDAL.findByIdForChallengeValidation(challengeId, tx); - if (!challenge) { - throw new NotFoundError({ message: "ACME challenge not found" }); - } - if (challenge.status !== AcmeChallengeStatus.Pending) { - // Ideally this should be an ACME error, but RFC 8555 doesn't say much about corner cases like this... - throw new BadRequestError({ - message: `ACME challenge is ${challenge.status} instead of ${AcmeChallengeStatus.Pending}` - }); - } - if (challenge.auth.expiresAt < new Date()) { - throw new BadRequestError({ message: "ACME auth has expired" }); - } - if (challenge.auth.status !== AcmeAuthStatus.Pending) { - throw new BadRequestError({ - message: `ACME auth status is ${challenge.auth.status} instead of ${AcmeAuthStatus.Pending}` - }); - } - if (!challenge.auth.token) { - throw new AcmeServerInternalError({ message: "ACME challenge token is required" }); - } - if (challenge.type !== AcmeChallengeType.HTTP_01) { - throw new BadRequestError({ message: "Only HTTP-01 challenges are supported for now" }); - } - const updatedChallenge = await acmeChallengeDAL.updateById( - challengeId, - { status: AcmeChallengeStatus.Pending }, - tx - ); + await acmeChallengeService.validateChallengeResponse(challengeId, tx); return { ...challenge, ...updatedChallenge diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts index e0bd0204c..3e6b1dd19 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -42,7 +42,6 @@ import { } from "@app/services/secret-sync/secret-sync-types"; import { CacheType } from "@app/services/super-admin/super-admin-types"; import { TWebhookPayloads } from "@app/services/webhook/webhook-types"; -import { TValidateAcmeChallengeResponseDTO } from "@app/ee/services/pki-acme/pki-acme-challenge-queue"; export enum QueueName { SecretRotation = "secret-rotation", @@ -80,8 +79,7 @@ export enum QueueName { UserNotification = "user-notification", HealthAlert = "health-alert", CertificateV3AutoRenewal = "certificate-v3-auto-renewal", - PamAccountRotation = "pam-account-rotation", - PkiAcmeChallengeValidation = "pki-acme-challenge-validation" + PamAccountRotation = "pam-account-rotation" } export enum QueueJobs { @@ -132,8 +130,7 @@ export enum QueueJobs { UserNotification = "user-notification-job", HealthAlert = "health-alert", CertificateV3DailyAutoRenewal = "certificate-v3-daily-auto-renewal", - PamAccountRotation = "pam-account-rotation", - ValidateAcmeChallengeResponse = "validate-acme-challenge-response" + PamAccountRotation = "pam-account-rotation" } export type TQueueJobTypes = { @@ -372,10 +369,6 @@ export type TQueueJobTypes = { name: QueueJobs.PamAccountRotation; payload: undefined; }; - [QueueName.PkiAcmeChallengeValidation]: { - name: QueueJobs.ValidateAcmeChallengeResponse; - payload: TValidateAcmeChallengeResponseDTO; - }; }; const SECRET_SCANNING_JOBS = [ From 9e8f3843a632c2cd054857814031dfcb09321156 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 14:13:47 -0700 Subject: [PATCH 175/294] Implement mark valid cascade --- .../pki-acme/pki-acme-challenge-dal.ts | 52 ++++++++++++++++++- .../pki-acme/pki-acme-challenge-service.ts | 13 ++--- 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts index 28d1c318a..6cc465d99 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts @@ -1,14 +1,61 @@ import { TDbClient } from "@app/db"; -import { TableName, TPkiAcmeAccounts, TPkiAcmeAuths } from "@app/db/schemas"; +import { TableName, TPkiAcmeChallenges } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; import { Knex } from "knex"; +import { AcmeAuthStatus, AcmeChallengeStatus, AcmeOrderStatus } from "./pki-acme-schemas"; export type TPkiAcmeChallengeDALFactory = ReturnType; export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { const pkiAcmeChallengeOrm = ormify(db, TableName.PkiAcmeChallenge); + const markAsValidCascadeById = async (id: string, tx?: Knex): Promise => { + try { + const [challenge] = (await (tx || db)(TableName.PkiAcmeChallenge) + .where({ id }) + .update({ status: AcmeChallengeStatus.Valid, validatedAt: new Date() }) + .returning("*")) as [TPkiAcmeChallenges]; + + // Update pending auth to valid as well + const updatedAuths = await (tx || db)(TableName.PkiAcmeAuth) + .where({ id: challenge.authId, status: AcmeAuthStatus.Pending }) + .update({ status: AcmeAuthStatus.Valid }) + .returning("id"); + + if (updatedAuths.length > 0) { + // Update status for pending orders that have all auths valid + await (tx || db)(TableName.PkiAcmeOrder) + .whereIn("id", (qb) => { + qb.select("id") + .from(TableName.PkiAcmeOrder) + .join(TableName.PkiAcmeOrderAuth, `${TableName.PkiAcmeOrder}.id`, `${TableName.PkiAcmeOrderAuth}.orderId`) + .join(TableName.PkiAcmeAuth, `${TableName.PkiAcmeOrderAuth}.authId`, `${TableName.PkiAcmeAuth}.id`) + .groupBy(`${TableName.PkiAcmeOrder}.id`) + // All auths should be valid for the order to be ready + .havingRaw( + `SUM(CASE WHEN :authTable:.status = :authStatus: THEN 1 ELSE 0 END) = COUNT(DISTINCT :authTable:.id)`, + { + authTable: TableName.PkiAcmeAuth, + authStatus: AcmeAuthStatus.Valid + } + ) + // We only update orders that are pending + .where(`${TableName.PkiAcmeOrder}.status`, AcmeOrderStatus.Pending) + .whereIn( + `${TableName.PkiAcmeAuth}.id`, + updatedAuths.map((auth) => auth.id) + ); + }) + .update({ status: AcmeOrderStatus.Ready }); + } + + return challenge; + } catch (error) { + throw new DatabaseError({ error, name: "Update certificate profile" }); + } + }; + const findByAccountAuthAndChallengeId = async (accountId: string, authId: string, challengeId: string, tx?: Knex) => { try { const challenge = await (tx || db)(TableName.PkiAcmeChallenge) @@ -78,6 +125,7 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { return { ...pkiAcmeChallengeOrm, + markAsValidCascadeById, findByAccountAuthAndChallengeId, findByIdForChallengeValidation }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts index be46de35a..6cb04f2d5 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts @@ -11,7 +11,10 @@ import { TPkiAcmeChallengeServiceFactory } from "./pki-acme-types"; type TPkiAcmeChallengeServiceFactoryDep = { acmeAuthDAL: Pick; - acmeChallengeDAL: Pick; + acmeChallengeDAL: Pick< + TPkiAcmeChallengeDALFactory, + "transaction" | "findByIdForChallengeValidation" | "markAsValidCascadeById" + >; }; export const pkiAcmeChallengeServiceFactory = ({ @@ -65,13 +68,7 @@ export const pkiAcmeChallengeServiceFactory = ({ if (challengeResponseBody !== expectedChallengeResponseBody) { throw new AcmeIncorrectResponseError({ message: "ACME challenge response is not correct" }); } - await acmeChallengeDAL.updateById( - challengeId, - { status: AcmeChallengeStatus.Valid, validatedAt: new Date() }, - tx - ); - await acmeAuthDAL.updateById(challenge.auth.account.id, { status: AcmeAuthStatus.Valid }, tx); - // TODO: trigger a check for order status as well + await acmeChallengeDAL.markAsValidCascadeById(challengeId, tx); } catch (error) { logger.error(error, "Error validating ACME challenge response"); // TODO: we should retry the challenge validation a few times, but let's keep it simple for now From 737bbd3e08cab5cc620b13a48caec20e1755de1b Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 14:21:48 -0700 Subject: [PATCH 176/294] Try to make challenge works --- .../ee/services/pki-acme/pki-acme-challenge-dal.ts | 14 ++++++++++++++ .../pki-acme/pki-acme-challenge-service.ts | 9 ++++----- .../src/ee/services/pki-acme/pki-acme-service.ts | 10 ++-------- backend/src/ee/services/pki-acme/pki-acme-types.ts | 1 + 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts index 6cc465d99..48bc1dfac 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts @@ -56,6 +56,19 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { } }; + const markAsInvalidCascadeById = async (id: string, tx?: Knex): Promise => { + try { + const [challenge] = (await (tx || db)(TableName.PkiAcmeChallenge) + .where({ id }) + .update({ status: AcmeChallengeStatus.Valid, validatedAt: new Date() }) + .returning("*")) as [TPkiAcmeChallenges]; + // TODO: + return challenge; + } catch (error) { + throw new DatabaseError({ error, name: "Update certificate profile" }); + } + }; + const findByAccountAuthAndChallengeId = async (accountId: string, authId: string, challengeId: string, tx?: Knex) => { try { const challenge = await (tx || db)(TableName.PkiAcmeChallenge) @@ -126,6 +139,7 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { return { ...pkiAcmeChallengeOrm, markAsValidCascadeById, + markAsInvalidCascadeById, findByAccountAuthAndChallengeId, findByIdForChallengeValidation }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts index 6cb04f2d5..af0ea0ebe 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts @@ -8,6 +8,7 @@ import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal"; import { AcmeIncorrectResponseError } from "./pki-acme-errors"; import { AcmeAuthStatus, AcmeChallengeStatus, AcmeChallengeType } from "./pki-acme-schemas"; import { TPkiAcmeChallengeServiceFactory } from "./pki-acme-types"; +import { TPkiAcmeChallenges } from "@app/db/schemas"; type TPkiAcmeChallengeServiceFactoryDep = { acmeAuthDAL: Pick; @@ -23,8 +24,8 @@ export const pkiAcmeChallengeServiceFactory = ({ }: TPkiAcmeChallengeServiceFactoryDep): TPkiAcmeChallengeServiceFactory => { const appCfg = getConfig(); - const validateChallengeResponse = async (challengeId: string, tx?: Knex): Promise => { - return await acmeChallengeDAL.transaction(async (tx: Knex) => { + const validateChallengeResponse = async (challengeId: string): Promise => { + return await acmeChallengeDAL.transaction(async (tx) => { logger.info({ challengeId }, "Validating ACME challenge response"); const challenge = await acmeChallengeDAL.findByIdForChallengeValidation(challengeId, tx); if (!challenge) { @@ -72,9 +73,7 @@ export const pkiAcmeChallengeServiceFactory = ({ } catch (error) { logger.error(error, "Error validating ACME challenge response"); // TODO: we should retry the challenge validation a few times, but let's keep it simple for now - await acmeChallengeDAL.updateById(challengeId, { status: AcmeChallengeStatus.Invalid }, tx); - await acmeAuthDAL.updateById(challenge.auth.account.id, { status: AcmeAuthStatus.Invalid }, tx); - // TODO: trigger a check for order status as well + await acmeChallengeDAL.markAsValidCascadeById(challengeId, tx); throw error; } }); diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index a4916c555..6dad459c2 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -611,14 +611,8 @@ export const pkiAcmeServiceFactory = ({ if (!result) { throw new NotFoundError({ message: "ACME challenge not found" }); } - const challenge = await acmeChallengeDAL.transaction(async (tx) => { - await acmeChallengeService.validateChallengeResponse(challengeId, tx); - return { - ...challenge, - ...updatedChallenge - }; - }); - // TODO: Implement ACME challenge response + await acmeChallengeService.validateChallengeResponse(challengeId); + const challenge = (await acmeChallengeDAL.findByIdForChallengeValidation(challengeId))!; return { status: 200, body: { diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index c993acf64..e9154fb5d 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +import { TPkiAcmeChallenges } from "@app/db/schemas"; import { JWSHeaderParameters } from "jose"; import { AcmeOrderResourceSchema, From 8bc7c8b79667cce9e4ffb4444e427158f299088d Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 14:35:32 -0700 Subject: [PATCH 177/294] Fix mark valid logic --- .../pki-acme/pki-acme-challenge-dal.ts | 44 ++++++++++++++++--- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts index 48bc1dfac..7d77f51bd 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts @@ -24,6 +24,16 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { .returning("id"); if (updatedAuths.length > 0) { + // Find all the orders that are involved in the challenge validation + const involvedOrderIds = await (tx || db)(TableName.PkiAcmeOrder) + .distinct("id") + .join(TableName.PkiAcmeOrderAuth, `${TableName.PkiAcmeOrder}.id`, `${TableName.PkiAcmeOrderAuth}.orderId`) + .join(TableName.PkiAcmeAuth, `${TableName.PkiAcmeOrderAuth}.authId`, `${TableName.PkiAcmeAuth}.id`) + .whereIn( + `${TableName.PkiAcmeAuth}.id`, + updatedAuths.map((auth) => auth.id) + ) + .as("involvedOrderIds"); // Update status for pending orders that have all auths valid await (tx || db)(TableName.PkiAcmeOrder) .whereIn("id", (qb) => { @@ -40,12 +50,7 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { authStatus: AcmeAuthStatus.Valid } ) - // We only update orders that are pending - .where(`${TableName.PkiAcmeOrder}.status`, AcmeOrderStatus.Pending) - .whereIn( - `${TableName.PkiAcmeAuth}.id`, - updatedAuths.map((auth) => auth.id) - ); + .whereIn(`${TableName.PkiAcmeOrder}.id`, involvedOrderIds); }) .update({ status: AcmeOrderStatus.Ready }); } @@ -62,7 +67,32 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { .where({ id }) .update({ status: AcmeChallengeStatus.Valid, validatedAt: new Date() }) .returning("*")) as [TPkiAcmeChallenges]; - // TODO: + + // Update pending auth to valid as well + const updatedAuths = await (tx || db)(TableName.PkiAcmeAuth) + .where({ id: challenge.authId, status: AcmeAuthStatus.Pending }) + .update({ status: AcmeAuthStatus.Invalid }) + .returning("id"); + + if (updatedAuths.length > 0) { + // Update status for pending orders that have all auths valid + await (tx || db)(TableName.PkiAcmeOrder) + .whereIn("id", (qb) => { + qb.select("id") + .from(TableName.PkiAcmeOrder) + .join(TableName.PkiAcmeOrderAuth, `${TableName.PkiAcmeOrder}.id`, `${TableName.PkiAcmeOrderAuth}.orderId`) + .join(TableName.PkiAcmeAuth, `${TableName.PkiAcmeOrderAuth}.authId`, `${TableName.PkiAcmeAuth}.id`) + // We only update orders that are pending + .where(`${TableName.PkiAcmeOrder}.status`, AcmeOrderStatus.Pending) + .whereIn( + `${TableName.PkiAcmeAuth}.id`, + updatedAuths.map((auth) => auth.id) + ); + }) + .update({ status: AcmeOrderStatus.Invalid }); + } + + // TODO: update order status to invalid as well return challenge; } catch (error) { throw new DatabaseError({ error, name: "Update certificate profile" }); From 31801455f019074cc924021d1c1bf6e5617d10d1 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 14:36:34 -0700 Subject: [PATCH 178/294] Add cond --- backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts index 7d77f51bd..03d309c03 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts @@ -50,6 +50,8 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { authStatus: AcmeAuthStatus.Valid } ) + // Only update orders that are pending + .where(`${TableName.PkiAcmeOrder}.status`, AcmeOrderStatus.Pending) .whereIn(`${TableName.PkiAcmeOrder}.id`, involvedOrderIds); }) .update({ status: AcmeOrderStatus.Ready }); From 941e857b0b8d1fc8a66096e3d0b859226b4549ec Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 14:37:15 -0700 Subject: [PATCH 179/294] Fix syntax --- backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts index 03d309c03..08f52a790 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts @@ -44,7 +44,7 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { .groupBy(`${TableName.PkiAcmeOrder}.id`) // All auths should be valid for the order to be ready .havingRaw( - `SUM(CASE WHEN :authTable:.status = :authStatus: THEN 1 ELSE 0 END) = COUNT(DISTINCT :authTable:.id)`, + "SUM(CASE WHEN :authTable:.status = :authStatus THEN 1 ELSE 0 END) = COUNT(DISTINCT :authTable:.id)", { authTable: TableName.PkiAcmeAuth, authStatus: AcmeAuthStatus.Valid From e7a5e130852e550c656a614ef860d105bfb1cbc3 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 14:43:32 -0700 Subject: [PATCH 180/294] Add missing deps --- backend/src/ee/services/pki-acme/pki-acme-service.ts | 4 ++-- backend/src/server/routes/index.ts | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 6dad459c2..cca7c0957 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -2,11 +2,10 @@ import { TPkiAcmeAccounts } from "@app/db/schemas/pki-acme-accounts"; import { TPkiAcmeAuths } from "@app/db/schemas/pki-acme-auths"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; -import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; -import { TPkiAcmeChallenges } from "@app/db/schemas"; import { EnrollmentType, TCertificateProfileWithConfigs @@ -57,6 +56,7 @@ import { TGetAcmeDirectoryResponse, TJwsPayload, TListAcmeOrdersResponse, + TPkiAcmeChallengeServiceFactory, TPkiAcmeServiceFactory, TRawJwsPayload, TRespondToAcmeChallengeResponse diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index d3ee4caae..a3355ada3 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -75,6 +75,7 @@ import { permissionDALFactory } from "@app/ee/services/permission/permission-dal import { permissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { pitServiceFactory } from "@app/ee/services/pit/pit-service"; import { pkiAcmeAuthDALFactory } from "@app/ee/services/pki-acme/pki-acme-auth-dal"; +import { pkiAcmeChallengeServiceFactory } from "@app/ee/services/pki-acme/pki-acme-challenge-service"; import { pkiAcmeOrderAuthDALFactory } from "@app/ee/services/pki-acme/pki-acme-order-auth-dal"; import { pkiAcmeServiceFactory } from "@app/ee/services/pki-acme/pki-acme-service"; import { projectTemplateDALFactory } from "@app/ee/services/project-template/project-template-dal"; @@ -1174,13 +1175,18 @@ export const registerRoutes = async ( projectDAL }); + const acmeChallengeService = pkiAcmeChallengeServiceFactory({ + acmeAuthDAL, + acmeChallengeDAL + }); const pkiAcmeService = pkiAcmeServiceFactory({ certificateProfileDAL, acmeAccountDAL, acmeOrderDAL, acmeAuthDAL, acmeOrderAuthDAL, - acmeChallengeDAL + acmeChallengeDAL, + acmeChallengeService }); const pkiAlertService = pkiAlertServiceFactory({ From a07775664b9aeec2417f4fcaf07ea6b8f63e3d16 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 15:35:52 -0700 Subject: [PATCH 181/294] Fix the query --- .../pki-acme/pki-acme-challenge-dal.ts | 37 ++++++++----------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts index 08f52a790..05925154e 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts @@ -25,34 +25,29 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { if (updatedAuths.length > 0) { // Find all the orders that are involved in the challenge validation - const involvedOrderIds = await (tx || db)(TableName.PkiAcmeOrder) - .distinct("id") - .join(TableName.PkiAcmeOrderAuth, `${TableName.PkiAcmeOrder}.id`, `${TableName.PkiAcmeOrderAuth}.orderId`) - .join(TableName.PkiAcmeAuth, `${TableName.PkiAcmeOrderAuth}.authId`, `${TableName.PkiAcmeAuth}.id`) + const involvedOrderIds = (tx || db)({ o: TableName.PkiAcmeOrder }) + .distinct("o.id") + .join({ oa: TableName.PkiAcmeOrderAuth }, "o.id", "oa.orderId") + .join({ a: TableName.PkiAcmeAuth }, "oa.authId", `a.id`) .whereIn( - `${TableName.PkiAcmeAuth}.id`, + "a.id", updatedAuths.map((auth) => auth.id) - ) - .as("involvedOrderIds"); + ); // Update status for pending orders that have all auths valid await (tx || db)(TableName.PkiAcmeOrder) .whereIn("id", (qb) => { - qb.select("id") - .from(TableName.PkiAcmeOrder) - .join(TableName.PkiAcmeOrderAuth, `${TableName.PkiAcmeOrder}.id`, `${TableName.PkiAcmeOrderAuth}.orderId`) - .join(TableName.PkiAcmeAuth, `${TableName.PkiAcmeOrderAuth}.authId`, `${TableName.PkiAcmeAuth}.id`) - .groupBy(`${TableName.PkiAcmeOrder}.id`) + qb.select("o2.id") + .from({ o2: TableName.PkiAcmeOrder }) + .join({ oa2: TableName.PkiAcmeOrderAuth }, "o2.id", "oa2.orderId") + .join({ a2: TableName.PkiAcmeAuth }, "oa2.authId", "a2.id") + .groupBy("o2.id") // All auths should be valid for the order to be ready - .havingRaw( - "SUM(CASE WHEN :authTable:.status = :authStatus THEN 1 ELSE 0 END) = COUNT(DISTINCT :authTable:.id)", - { - authTable: TableName.PkiAcmeAuth, - authStatus: AcmeAuthStatus.Valid - } - ) + .havingRaw("SUM(CASE WHEN a2.status = ? THEN 1 ELSE 0 END) = COUNT(DISTINCT a2.id)", [ + AcmeAuthStatus.Valid + ]) // Only update orders that are pending - .where(`${TableName.PkiAcmeOrder}.status`, AcmeOrderStatus.Pending) - .whereIn(`${TableName.PkiAcmeOrder}.id`, involvedOrderIds); + .where("o2.status", AcmeOrderStatus.Pending) + .whereIn("o2.id", involvedOrderIds); }) .update({ status: AcmeOrderStatus.Ready }); } From f02adcf70cf36b2401301179436fc667e48bbc44 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 15:38:38 -0700 Subject: [PATCH 182/294] Fix invalid update --- .../src/ee/services/pki-acme/pki-acme-challenge-dal.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts index 05925154e..665eb1f9e 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts @@ -75,12 +75,12 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { // Update status for pending orders that have all auths valid await (tx || db)(TableName.PkiAcmeOrder) .whereIn("id", (qb) => { - qb.select("id") - .from(TableName.PkiAcmeOrder) - .join(TableName.PkiAcmeOrderAuth, `${TableName.PkiAcmeOrder}.id`, `${TableName.PkiAcmeOrderAuth}.orderId`) + qb.select("o.id") + .from({ o: TableName.PkiAcmeOrder }) + .join(TableName.PkiAcmeOrderAuth, "o.id", `${TableName.PkiAcmeOrderAuth}.orderId`) .join(TableName.PkiAcmeAuth, `${TableName.PkiAcmeOrderAuth}.authId`, `${TableName.PkiAcmeAuth}.id`) // We only update orders that are pending - .where(`${TableName.PkiAcmeOrder}.status`, AcmeOrderStatus.Pending) + .where("o.status", AcmeOrderStatus.Pending) .whereIn( `${TableName.PkiAcmeAuth}.id`, updatedAuths.map((auth) => auth.id) From 9314348b316760f4389d0e2587e373dfd489966b Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 16:32:15 -0700 Subject: [PATCH 183/294] More error handling --- .../pki-acme/pki-acme-challenge-service.ts | 28 +++- .../ee/services/pki-acme/pki-acme-errors.ts | 141 ++++++++++++------ .../ee/services/pki-acme/pki-acme-schemas.ts | 2 +- 3 files changed, 119 insertions(+), 52 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts index af0ea0ebe..e6464d69c 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts @@ -5,7 +5,7 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal"; import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal"; -import { AcmeIncorrectResponseError } from "./pki-acme-errors"; +import { AcmeConnectionError, AcmeDnsFailureError, AcmeIncorrectResponseError } from "./pki-acme-errors"; import { AcmeAuthStatus, AcmeChallengeStatus, AcmeChallengeType } from "./pki-acme-schemas"; import { TPkiAcmeChallengeServiceFactory } from "./pki-acme-types"; import { TPkiAcmeChallenges } from "@app/db/schemas"; @@ -25,7 +25,7 @@ export const pkiAcmeChallengeServiceFactory = ({ const appCfg = getConfig(); const validateChallengeResponse = async (challengeId: string): Promise => { - return await acmeChallengeDAL.transaction(async (tx) => { + const error = await acmeChallengeDAL.transaction(async (tx) => { logger.info({ challengeId }, "Validating ACME challenge response"); const challenge = await acmeChallengeDAL.findByIdForChallengeValidation(challengeId, tx); if (!challenge) { @@ -54,6 +54,7 @@ export const pkiAcmeChallengeServiceFactory = ({ ? `${baseUrl}:${appCfg.ACME_DEVELOPMENT_HTTP01_CHALLENGE_PORT}` : baseUrl; const challengeUrl = new URL(`/.well-known/acme-challenge/${challenge.auth.token}`, actualBaseUrl); + logger.info({ challengeUrl }, "Performing ACME HTTP-01 challenge validation"); try { // Notice: well, we are in a transaction, ideally we should not hold transaction and perform // a long running operation for long time. But assuming we are not performing a tons of @@ -66,17 +67,34 @@ export const pkiAcmeChallengeServiceFactory = ({ const challengeResponseBody = await challengeResponse.text(); const thumbprint = Buffer.from(challenge.auth.account.publicKeyThumbprint, "utf-8").toString("base64url"); const expectedChallengeResponseBody = `${challenge.auth.token}.${thumbprint}`; - if (challengeResponseBody !== expectedChallengeResponseBody) { + if (challengeResponseBody.trimEnd() !== expectedChallengeResponseBody) { throw new AcmeIncorrectResponseError({ message: "ACME challenge response is not correct" }); } await acmeChallengeDAL.markAsValidCascadeById(challengeId, tx); } catch (error) { - logger.error(error, "Error validating ACME challenge response"); // TODO: we should retry the challenge validation a few times, but let's keep it simple for now await acmeChallengeDAL.markAsValidCascadeById(challengeId, tx); - throw error; + // Properly type and inspect the error + if (error instanceof TypeError && error.message.includes("fetch failed")) { + const cause = error.cause as AggregateError; + if (cause?.errors?.[0]?.code === "ECONNREFUSED") { + logger.error(error, "Connection refused."); + return new AcmeConnectionError({ message: "Connection refused." }); + } else if (cause?.errors?.[0]?.code === "ENOTFOUND") { + logger.error(error, "Hostname could not be resolved (DNS failure)."); + return new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)." }); + } + } else if (error instanceof Error) { + logger.error(error, "Error validating ACME challenge response"); + } else { + logger.error(error, "Unknown error validating ACME challenge response"); + } + return error; } }); + if (error) { + throw error; + } }; return { validateChallengeResponse }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-errors.ts b/backend/src/ee/services/pki-acme/pki-acme-errors.ts index b09499346..8b5a19149 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-errors.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-errors.ts @@ -3,15 +3,43 @@ * https://datatracker.ietf.org/doc/html/rfc8555#section-6.2 */ +// RFC 8555 Section 6.7 - Error Types +export enum AcmeErrorType { + AccountDoesNotExist = "accountDoesNotExist", + AlreadyRevoked = "alreadyRevoked", + BadCsr = "badCSR", + BadNonce = "badNonce", + BadPublicKey = "badPublicKey", + BadRevocationReason = "badRevocationReason", + BadSignatureAlgorithm = "badSignatureAlgorithm", + CAA = "CAA", + Compound = "compound", + Connection = "connection", + DNS = "DNS", + ExternalAccountRequired = "externalAccountRequired", + IncorrectResponse = "incorrectResponse", + IncorrectContact = "incorrectContact", + Malformed = "malformed", + OrderNotReady = "orderNotReady", + RateLimited = "rateLimited", + RejectedIdentifier = "rejectedIdentifier", + ServerInternal = "serverInternal", + TLS = "tls", + Unauthorized = "unauthorized", + UnsupportedContact = "unsupportedContact", + UnsupportedIdentifier = "unsupportedIdentifier", + UserActionRequired = "userActionRequired" +} + export interface IAcmeError { - type: string; + type: AcmeErrorType; detail: string; status: number; subproblems?: Array<{ type: string; detail: string; identifier?: { type: string; value: string } }>; } export class AcmeError extends Error implements IAcmeError { - type: string; + type: AcmeErrorType; detail: string; @@ -29,7 +57,7 @@ export class AcmeError extends Error implements IAcmeError { error, message }: { - type: string; + type: AcmeErrorType; detail: string; status: number; subproblems?: Array<{ type: string; detail: string; identifier?: { type: string; value: string } }>; @@ -69,7 +97,7 @@ export class AcmeMalformedError extends AcmeError { message?: string; } = {}) { super({ - type: "malformed", + type: AcmeErrorType.Malformed, detail, status: 400, error, @@ -93,7 +121,7 @@ export class AcmeUnauthorizedError extends AcmeError { message?: string; } = {}) { super({ - type: "unauthorized", + type: AcmeErrorType.Unauthorized, detail, status: 403, error, @@ -118,7 +146,7 @@ export class AcmeAccountDoesNotExistError extends AcmeError { message?: string; } = {}) { super({ - type: "accountDoesNotExist", + type: AcmeErrorType.AccountDoesNotExist, detail, status: 400, error, @@ -142,7 +170,7 @@ export class AcmeBadNonceError extends AcmeError { message?: string; } = {}) { super({ - type: "badNonce", + type: AcmeErrorType.BadNonce, detail, status: 400, error, @@ -153,11 +181,11 @@ export class AcmeBadNonceError extends AcmeError { } /** - * badSignature - The JWS signature is invalid (RFC 8555 Section 6.7.5) + * badSignatureAlgorithm - The signature algorithm is invalid (RFC 8555 Section 6.7.5) */ -export class AcmeBadSignatureError extends AcmeError { +export class AcmeBadSignatureAlgorithmError extends AcmeError { constructor({ - detail = "The JWS signature is invalid", + detail = "The signature algorithm is invalid", error, message }: { @@ -166,13 +194,13 @@ export class AcmeBadSignatureError extends AcmeError { message?: string; } = {}) { super({ - type: "badSignature", + type: AcmeErrorType.BadSignatureAlgorithm, detail, status: 401, error, message }); - this.name = "AcmeBadSignatureError"; + this.name = "AcmeBadSignatureAlgorithmError"; } } @@ -190,7 +218,7 @@ export class AcmeBadPublicKeyError extends AcmeError { message?: string; } = {}) { super({ - type: "badPublicKey", + type: AcmeErrorType.BadPublicKey, detail, status: 400, error, @@ -214,7 +242,7 @@ export class AcmeBadCsrError extends AcmeError { message?: string; } = {}) { super({ - type: "badCSR", + type: AcmeErrorType.BadCsr, detail, status: 400, error, @@ -239,7 +267,7 @@ export class AcmeBadRevocationReasonError extends AcmeError { message?: string; } = {}) { super({ - type: "badRevocationReason", + type: AcmeErrorType.BadRevocationReason, detail, status: 400, error, @@ -263,7 +291,7 @@ export class AcmeRateLimitedError extends AcmeError { message?: string; } = {}) { super({ - type: "rateLimited", + type: AcmeErrorType.RateLimited, detail, status: 429, error, @@ -290,7 +318,7 @@ export class AcmeRejectedIdentifierError extends AcmeError { message?: string; } = {}) { super({ - type: "rejectedIdentifier", + type: AcmeErrorType.RejectedIdentifier, detail, status: 400, subproblems, @@ -315,7 +343,7 @@ export class AcmeServerInternalError extends AcmeError { message?: string; } = {}) { super({ - type: "serverInternal", + type: AcmeErrorType.ServerInternal, detail, status: 500, error, @@ -325,30 +353,6 @@ export class AcmeServerInternalError extends AcmeError { } } -/** - * serviceUnavailable - The service is unavailable (RFC 8555 Section 6.7.12) - */ -export class AcmeServiceUnavailableError extends AcmeError { - constructor({ - detail = "The service is unavailable", - error, - message - }: { - detail?: string; - error?: unknown; - message?: string; - } = {}) { - super({ - type: "serviceUnavailable", - detail, - status: 503, - error, - message - }); - this.name = "AcmeServiceUnavailableError"; - } -} - /** * unsupportedContact - A contact URL is of an unsupported type (RFC 8555 Section 6.7.13) */ @@ -363,7 +367,7 @@ export class AcmeUnsupportedContactError extends AcmeError { message?: string; } = {}) { super({ - type: "unsupportedContact", + type: AcmeErrorType.UnsupportedContact, detail, status: 400, error, @@ -388,7 +392,7 @@ export class AcmeUnsupportedIdentifierError extends AcmeError { message?: string; } = {}) { super({ - type: "unsupportedIdentifier", + type: AcmeErrorType.UnsupportedIdentifier, detail, status: 400, error, @@ -417,7 +421,7 @@ export class AcmeUserActionRequiredError extends AcmeError { message?: string; } = {}) { super({ - type: "userActionRequired", + type: AcmeErrorType.UserActionRequired, detail, status: 403, error, @@ -449,7 +453,7 @@ export class AcmeIncorrectResponseError extends AcmeError { message?: string; } = {}) { super({ - type: "incorrectResponse", + type: AcmeErrorType.IncorrectResponse, detail, status: 400, error, @@ -458,3 +462,48 @@ export class AcmeIncorrectResponseError extends AcmeError { this.name = "AcmeIncorrectResponseError"; } } + +/** + * connectionError - A connection error occurred (RFC 8555 Section 6.7.17) + */ +export class AcmeConnectionError extends AcmeError { + constructor({ + detail = "A connection error occurred", + error, + message + }: { + detail?: string; + error?: unknown; + message?: string; + } = {}) { + super({ + type: AcmeErrorType.Connection, + detail, + status: 400, + error, + message + }); + this.name = "AcmeConnectionError"; + } +} + +export class AcmeDnsFailureError extends AcmeError { + constructor({ + detail = "Hostname could not be resolved (DNS failure)", + error, + message + }: { + detail?: string; + error?: unknown; + message?: string; + } = {}) { + super({ + type: AcmeErrorType.DNS, + detail, + status: 400, + error, + message + }); + this.name = "AcmeDnsFailureError"; + } +} diff --git a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts index 0c9015700..7298dd944 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts @@ -158,7 +158,7 @@ export const GetAcmeAuthorizationResponseSchema = z.object({ ) }); -export const RespondToAcmeChallengeBodySchema = z.object({}); +export const RespondToAcmeChallengeBodySchema = z.object({}).strict(); export const RespondToAcmeChallengeResponseSchema = z.object({ type: z.enum(Object.values(AcmeChallengeType) as [string, ...string[]]), From 8aa7594f0351fca819e2ae468dd248873f47e922 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 16:36:12 -0700 Subject: [PATCH 184/294] Fix types --- backend/src/ee/routes/v1/pki-acme-router.ts | 2 +- backend/src/ee/services/pki-acme/pki-acme-service.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index 6344809d0..329a381e3 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -421,7 +421,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { } }); - // POST /api/v1/pki/acme/profiles//authorizations//challenges/http-01 + // POST /api/v1/pki/acme/profiles//authorizations//challenges/ // Respond to Challenge (RFC 8555 Section 7.5.1) server.route({ method: "POST", diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index cca7c0957..d6cf92a48 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -388,7 +388,7 @@ export const pkiAcmeServiceFactory = ({ // if we do, return the existing order const order = await acmeOrderDAL.transaction(async (tx) => { - const account = await acmeAccountDAL.findByProjectIdAndAccountId(profileId, accountId)!; + const account = (await acmeAccountDAL.findByProjectIdAndAccountId(profileId, accountId))!; const createdOrder = await acmeOrderDAL.create( { accountId: account.id, @@ -585,7 +585,7 @@ export const pkiAcmeServiceFactory = ({ type: challenge.type, url: buildUrl(profileId, `/authorizations/${authzId}/challenges/${challenge.id}`), status: challenge.status, - token: auth.token + token: auth.token! }; }) }, From 453dc578299b89c01c88711b427f5b6658ae4eaa Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 16:47:20 -0700 Subject: [PATCH 185/294] Fix status handling --- backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts | 2 +- backend/src/server/plugins/error-handler.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts index 665eb1f9e..424651aea 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts @@ -62,7 +62,7 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { try { const [challenge] = (await (tx || db)(TableName.PkiAcmeChallenge) .where({ id }) - .update({ status: AcmeChallengeStatus.Valid, validatedAt: new Date() }) + .update({ status: AcmeChallengeStatus.Invalid }) .returning("*")) as [TPkiAcmeChallenges]; // Update pending auth to valid as well diff --git a/backend/src/server/plugins/error-handler.ts b/backend/src/server/plugins/error-handler.ts index 8f6b63141..1142694b5 100644 --- a/backend/src/server/plugins/error-handler.ts +++ b/backend/src/server/plugins/error-handler.ts @@ -251,7 +251,8 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider reqId: req.id, status: error.status, type: `urn:ietf:params:acme:error:${error.type}`, - detail: error.detail + detail: error.detail, + message: error.message // TODO: add subproblems if they exist }); } else { From c7cf1cac9f5bd71955a6c33bbf412297d81d376d Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 16:52:26 -0700 Subject: [PATCH 186/294] Try to fix error handling --- backend/src/server/plugins/error-handler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/server/plugins/error-handler.ts b/backend/src/server/plugins/error-handler.ts index 1142694b5..ecaa60c99 100644 --- a/backend/src/server/plugins/error-handler.ts +++ b/backend/src/server/plugins/error-handler.ts @@ -249,7 +249,8 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider .status(error.status) .send({ reqId: req.id, - status: error.status, + error: error.name, + statusCode: error.status, type: `urn:ietf:params:acme:error:${error.type}`, detail: error.detail, message: error.message From 4d9b74c85bf0c7b2db164d49f05cca5848deda20 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 16:59:41 -0700 Subject: [PATCH 187/294] Fix error schema validation issue --- .../src/server/plugins/add-errors-to-response-schemas.ts | 9 ++++++++- backend/src/server/plugins/error-handler.ts | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/backend/src/server/plugins/add-errors-to-response-schemas.ts b/backend/src/server/plugins/add-errors-to-response-schemas.ts index 8eb358a1b..6337bae0f 100644 --- a/backend/src/server/plugins/add-errors-to-response-schemas.ts +++ b/backend/src/server/plugins/add-errors-to-response-schemas.ts @@ -6,9 +6,16 @@ import { DefaultResponseErrorsSchema } from "../routes/sanitizedSchemas"; const isScimRoutes = (pathname: string) => pathname.startsWith("/api/v1/scim/Users") || pathname.startsWith("/api/v1/scim/Groups"); +const isAcmeRoutes = (pathname: string) => pathname.startsWith("/api/v1/pki/acme/"); + export const addErrorsToResponseSchemas = fp(async (server) => { server.addHook("onRoute", (routeOptions) => { - if (routeOptions.schema && routeOptions.schema.response && !isScimRoutes(routeOptions.path)) { + if ( + routeOptions.schema && + routeOptions.schema.response && + !isScimRoutes(routeOptions.path) && + !isAcmeRoutes(routeOptions.path) + ) { routeOptions.schema.response = { ...DefaultResponseErrorsSchema, ...routeOptions.schema.response diff --git a/backend/src/server/plugins/error-handler.ts b/backend/src/server/plugins/error-handler.ts index ecaa60c99..4b29f6930 100644 --- a/backend/src/server/plugins/error-handler.ts +++ b/backend/src/server/plugins/error-handler.ts @@ -250,7 +250,7 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider .send({ reqId: req.id, error: error.name, - statusCode: error.status, + status: error.status, type: `urn:ietf:params:acme:error:${error.type}`, detail: error.detail, message: error.message From d80a5414a5712f477d5777496fec8fd0e68dfed0 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 17:09:01 -0700 Subject: [PATCH 188/294] Override host for acme challenge --- .../pki-acme/pki-acme-challenge-service.ts | 14 +++++++++----- backend/src/lib/config/env.ts | 11 ++++++++++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts index e6464d69c..26dc65aaf 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts @@ -49,11 +49,15 @@ export const pkiAcmeChallengeServiceFactory = ({ if (challenge.type !== AcmeChallengeType.HTTP_01) { throw new BadRequestError({ message: "Only HTTP-01 challenges are supported for now" }); } - const baseUrl = `http://${challenge.auth.identifierValue}`; - const actualBaseUrl = appCfg.isAcmeDevelopmentMode - ? `${baseUrl}:${appCfg.ACME_DEVELOPMENT_HTTP01_CHALLENGE_PORT}` - : baseUrl; - const challengeUrl = new URL(`/.well-known/acme-challenge/${challenge.auth.token}`, actualBaseUrl); + let host = challenge.auth.identifierValue; + if (appCfg.isAcmeDevelopmentMode && appCfg.ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES[host]) { + host = appCfg.ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES[host]; + logger.warn( + { srcHost: challenge.auth.identifierValue, dstHost: host }, + "Using ACME development HTTP-01 challenge host override" + ); + } + const challengeUrl = new URL(`/.well-known/acme-challenge/${challenge.auth.token}`, `http://${host}`); logger.info({ challengeUrl }, "Performing ACME HTTP-01 challenge validation"); try { // Notice: well, we are in a transaction, ideally we should not hold transaction and perform diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 7477bc032..6f0502184 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -107,7 +107,16 @@ const envSchema = z ROTATION_DEVELOPMENT_MODE: zodStrBool.default("false").optional(), DAILY_RESOURCE_CLEAN_UP_DEVELOPMENT_MODE: zodStrBool.default("false").optional(), ACME_DEVELOPMENT_MODE: zodStrBool.default("false").optional(), - ACME_DEVELOPMENT_HTTP01_CHALLENGE_PORT: z.coerce.number().default(8087), + ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES: zpStr( + z + .string() + .optional() + .transform((val) => { + if (!val) return {}; + return JSON.parse(val) as Record; + }) + .default("{}") + ), // smtp options SMTP_HOST: zpStr(z.string().optional()), SMTP_IGNORE_TLS: zodStrBool.default("false"), From b410d886baa58a422686ce9d924b1ed2e2c68639 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 17:49:32 -0700 Subject: [PATCH 189/294] B64url --- backend/bdd/features/steps/pki_acme.py | 7 +++---- backend/src/ee/services/pki-acme/pki-acme-service.ts | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index c5627bb8b..02074f92c 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -350,15 +350,14 @@ def step_impl(context: Context, var_path: str, hostname: str): if hostname != "localhost": raise ValueError("Currently only localhost is supported") challenge = eval_var(context, var_path, as_json=False) - acme_challenge = challenge.chall - response, validation = acme_challenge.response_and_validation( + response, validation = challenge.response_and_validation( context.acme_client.net.key ) resource = standalone.HTTP01RequestHandler.HTTP01Resource( - chall=acme_challenge, response=response, validation=validation + chall=challenge, response=response, validation=validation ) # TODO: make port configurable - servers = standalone.HTTP01DualNetworkedServers(("", 8087), resource) + servers = standalone.HTTP01DualNetworkedServers(("0.0.0.0", 8087), resource) # Start client standalone web server. web_server = threading.Thread(name="web_server", target=servers.serve_forever) web_server.daemon = True diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index d6cf92a48..f1d60b241 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -413,7 +413,7 @@ export const pkiAcmeServiceFactory = ({ // RFC 8555 suggests a token with at least 128 bits of entropy // We are using 256 bits of entropy here, should be enough for now // ref: https://datatracker.ietf.org/doc/html/rfc8555#section-11.3 - token: crypto.randomBytes(32).toString("base64"), + token: crypto.randomBytes(32).toString("base64url"), // TODO: read config from the profile to get the expiration time instead expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) }, From 7a6ba788f9897389f93ab34f2216c693abb2f432 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 17:56:00 -0700 Subject: [PATCH 190/294] Fix bdd's http-01 server --- backend/bdd/features/steps/pki_acme.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 02074f92c..39d8db3e8 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -354,10 +354,10 @@ def step_impl(context: Context, var_path: str, hostname: str): context.acme_client.net.key ) resource = standalone.HTTP01RequestHandler.HTTP01Resource( - chall=challenge, response=response, validation=validation + chall=challenge.chall, response=response, validation=validation ) # TODO: make port configurable - servers = standalone.HTTP01DualNetworkedServers(("0.0.0.0", 8087), resource) + servers = standalone.HTTP01DualNetworkedServers(("0.0.0.0", 8087), {resource}) # Start client standalone web server. web_server = threading.Thread(name="web_server", target=servers.serve_forever) web_server.daemon = True From e33a314600d5cff4e0c22e4ea9b3fb3f231cdda8 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 18:01:42 -0700 Subject: [PATCH 191/294] Well... mark as invalid --- backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts index 26dc65aaf..d2c4e52b5 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts @@ -77,7 +77,7 @@ export const pkiAcmeChallengeServiceFactory = ({ await acmeChallengeDAL.markAsValidCascadeById(challengeId, tx); } catch (error) { // TODO: we should retry the challenge validation a few times, but let's keep it simple for now - await acmeChallengeDAL.markAsValidCascadeById(challengeId, tx); + await acmeChallengeDAL.markAsInvalidCascadeById(challengeId, tx); // Properly type and inspect the error if (error instanceof TypeError && error.message.includes("fetch failed")) { const cause = error.cause as AggregateError; From 7edde542811ed6179e0c2591d173a6e0e12be62e Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 18:01:53 -0700 Subject: [PATCH 192/294] And import --- .../src/ee/services/pki-acme/pki-acme-challenge-service.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts index d2c4e52b5..2ece06427 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts @@ -1,5 +1,3 @@ -import { Knex } from "knex"; - import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; @@ -8,13 +6,12 @@ import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal"; import { AcmeConnectionError, AcmeDnsFailureError, AcmeIncorrectResponseError } from "./pki-acme-errors"; import { AcmeAuthStatus, AcmeChallengeStatus, AcmeChallengeType } from "./pki-acme-schemas"; import { TPkiAcmeChallengeServiceFactory } from "./pki-acme-types"; -import { TPkiAcmeChallenges } from "@app/db/schemas"; type TPkiAcmeChallengeServiceFactoryDep = { acmeAuthDAL: Pick; acmeChallengeDAL: Pick< TPkiAcmeChallengeDALFactory, - "transaction" | "findByIdForChallengeValidation" | "markAsValidCascadeById" + "transaction" | "findByIdForChallengeValidation" | "markAsValidCascadeById" | "markAsInvalidCascadeById" >; }; From 181faff20b79e1b40879c3302f647e507fc7d804 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 18:17:18 -0700 Subject: [PATCH 193/294] Better error handling --- .../pki-acme/pki-acme-challenge-service.ts | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts index 2ece06427..a7703f130 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts @@ -77,13 +77,21 @@ export const pkiAcmeChallengeServiceFactory = ({ await acmeChallengeDAL.markAsInvalidCascadeById(challengeId, tx); // Properly type and inspect the error if (error instanceof TypeError && error.message.includes("fetch failed")) { - const cause = error.cause as AggregateError; - if (cause?.errors?.[0]?.code === "ECONNREFUSED") { - logger.error(error, "Connection refused."); - return new AcmeConnectionError({ message: "Connection refused." }); - } else if (cause?.errors?.[0]?.code === "ENOTFOUND") { - logger.error(error, "Hostname could not be resolved (DNS failure)."); - return new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)." }); + const cause = error.cause; + if (cause instanceof Error) { + if (cause.message.includes("ECONNREFUSED")) { + return new AcmeConnectionError({ message: "Connection refused" }); + } else if (cause.message.includes("ENOTFOUND")) { + return new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)" }); + } + } else if (cause instanceof AggregateError) { + // TODO: handle multiple errors + const firstError = cause.errors?.[0]; + if (firstError?.code === "ECONNREFUSED") { + return new AcmeConnectionError({ message: "Connection refused" }); + } else if (firstError?.code === "ENOTFOUND") { + return new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)" }); + } } } else if (error instanceof Error) { logger.error(error, "Error validating ACME challenge response"); From c2a33992915fa7715ba829166aa98e33b7fe5a6a Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 18:19:28 -0700 Subject: [PATCH 194/294] improve err handing --- .../pki-acme/pki-acme-challenge-service.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts index a7703f130..f9be0a60a 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts @@ -78,18 +78,12 @@ export const pkiAcmeChallengeServiceFactory = ({ // Properly type and inspect the error if (error instanceof TypeError && error.message.includes("fetch failed")) { const cause = error.cause; - if (cause instanceof Error) { - if (cause.message.includes("ECONNREFUSED")) { + const errors = cause instanceof AggregateError ? cause.errors : cause instanceof Error ? [cause] : []; + for (const err of errors) { + // TODO: handle multiple errors, return a compound error instead of just the first error + if (err?.code === "ECONNREFUSED" || err?.message?.includes("ECONNREFUSED")) { return new AcmeConnectionError({ message: "Connection refused" }); - } else if (cause.message.includes("ENOTFOUND")) { - return new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)" }); - } - } else if (cause instanceof AggregateError) { - // TODO: handle multiple errors - const firstError = cause.errors?.[0]; - if (firstError?.code === "ECONNREFUSED") { - return new AcmeConnectionError({ message: "Connection refused" }); - } else if (firstError?.code === "ENOTFOUND") { + } else if (err?.code === "ENOTFOUND" || err?.message?.includes("ENOTFOUND")) { return new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)" }); } } From 4b10b4ec336c8ce19790bd3161002ffdf7944681 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 19:30:25 -0700 Subject: [PATCH 195/294] Fix challenge match --- backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts index f9be0a60a..fa34000cc 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts @@ -66,7 +66,7 @@ export const pkiAcmeChallengeServiceFactory = ({ throw new BadRequestError({ message: "ACME challenge response is not 200" }); } const challengeResponseBody = await challengeResponse.text(); - const thumbprint = Buffer.from(challenge.auth.account.publicKeyThumbprint, "utf-8").toString("base64url"); + const thumbprint = challenge.auth.account.publicKeyThumbprint; const expectedChallengeResponseBody = `${challenge.auth.token}.${thumbprint}`; if (challengeResponseBody.trimEnd() !== expectedChallengeResponseBody) { throw new AcmeIncorrectResponseError({ message: "ACME challenge response is not correct" }); From f3852638e57f4ee5ec8f210450942d2474531e9b Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 19:33:49 -0700 Subject: [PATCH 196/294] Poll and finalize --- backend/bdd/features/steps/pki_acme.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 39d8db3e8..3156d4a71 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -371,3 +371,10 @@ def step_impl(context: Context, var_path: str): acme_client = context.acme_client response, validation = challenge.response_and_validation(acme_client.net.key) acme_client.answer_challenge(challenge, response) + + +@then("I poll and finalize the ACME order {var_path}") +def step_impl(context: Context, var_path: str): + order = eval_var(context, var_path, as_json=False) + acme_client = context.acme_client + acme_client.poll_and_finalize(order) From b4bd05cbd0d43746f53609f15b6203f725e85bd8 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 20:16:26 -0700 Subject: [PATCH 197/294] issuing cert --- .../bdd/features/pki/acme/challenge.feature | 1 + .../migrations/20251029234547_add-pki-acme.ts | 9 ++++ .../pki-acme/pki-acme-challenge-service.ts | 3 -- .../ee/services/pki-acme/pki-acme-errors.ts | 21 +++++++++ .../services/pki-acme/pki-acme-order-dal.ts | 10 +++++ .../ee/services/pki-acme/pki-acme-service.ts | 44 +++++++++++++++++-- 6 files changed, 82 insertions(+), 6 deletions(-) diff --git a/backend/bdd/features/pki/acme/challenge.feature b/backend/bdd/features/pki/acme/challenge.feature index 8e7dad523..960842ebf 100644 --- a/backend/bdd/features/pki/acme/challenge.feature +++ b/backend/bdd/features/pki/acme/challenge.feature @@ -19,3 +19,4 @@ Feature: Challenge Then I select challenge with type http-01 for domain localhost from order at order as challenge Then I serve challenge response for challenge at localhost Then I tell ACME server that challenge is ready to be verified + Then I poll and finalize the ACME order order diff --git a/backend/src/db/migrations/20251029234547_add-pki-acme.ts b/backend/src/db/migrations/20251029234547_add-pki-acme.ts index c91a179a8..ee5ba9f36 100644 --- a/backend/src/db/migrations/20251029234547_add-pki-acme.ts +++ b/backend/src/db/migrations/20251029234547_add-pki-acme.ts @@ -85,6 +85,12 @@ export async function up(knex: Knex): Promise { t.timestamp("expiresAt").notNullable(); + t.string("csr").nullable(); + t.string("certificate").nullable(); + t.string("certificateChain").nullable(); + + t.string("error").nullable(); + // Order status t.string("status").notNullable(); // pending, ready, processing, valid, invalid @@ -160,6 +166,9 @@ export async function up(knex: Knex): Promise { // Challenge status t.string("status").notNullable(); // pending, processing, valid, invalid + // Error message when the challenge fails + t.string("error").nullable(); + // Validation timestamp t.timestamp("validatedAt").nullable(); diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts index fa34000cc..3b858e427 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts @@ -1,14 +1,12 @@ import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; -import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal"; import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal"; import { AcmeConnectionError, AcmeDnsFailureError, AcmeIncorrectResponseError } from "./pki-acme-errors"; import { AcmeAuthStatus, AcmeChallengeStatus, AcmeChallengeType } from "./pki-acme-schemas"; import { TPkiAcmeChallengeServiceFactory } from "./pki-acme-types"; type TPkiAcmeChallengeServiceFactoryDep = { - acmeAuthDAL: Pick; acmeChallengeDAL: Pick< TPkiAcmeChallengeDALFactory, "transaction" | "findByIdForChallengeValidation" | "markAsValidCascadeById" | "markAsInvalidCascadeById" @@ -16,7 +14,6 @@ type TPkiAcmeChallengeServiceFactoryDep = { }; export const pkiAcmeChallengeServiceFactory = ({ - acmeAuthDAL, acmeChallengeDAL }: TPkiAcmeChallengeServiceFactoryDep): TPkiAcmeChallengeServiceFactory => { const appCfg = getConfig(); diff --git a/backend/src/ee/services/pki-acme/pki-acme-errors.ts b/backend/src/ee/services/pki-acme/pki-acme-errors.ts index 8b5a19149..924a7b0ca 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-errors.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-errors.ts @@ -507,3 +507,24 @@ export class AcmeDnsFailureError extends AcmeError { this.name = "AcmeDnsFailureError"; } } + +export class AcmeOrderNotReadyError extends AcmeError { + constructor({ + detail = "The order is not ready", + error, + message + }: { + detail?: string; + error?: unknown; + message?: string; + } = {}) { + super({ + type: AcmeErrorType.OrderNotReady, + detail, + status: 403, + error, + message + }); + this.name = "AcmeOrderNotReadyError"; + } +} diff --git a/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts index 47d72a261..8600079df 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts @@ -10,6 +10,15 @@ export type TPkiAcmeOrderDALFactory = ReturnType; export const pkiAcmeOrderDALFactory = (db: TDbClient) => { const pkiAcmeOrderOrm = ormify(db, TableName.PkiAcmeOrder); + const findByIdForFinalization = async (id: string, tx?: Knex) => { + try { + const order = await (tx || db)(TableName.PkiAcmeOrder).forUpdate().where({ id }).first(); + return order || null; + } catch (error) { + throw new DatabaseError({ error, name: "Find PKI ACME order by id for finalization" }); + } + }; + const findByAccountAndOrderIdWithAuthorizations = async (accountId: string, orderId: string, tx?: Knex) => { try { const rows = await (tx || db)(TableName.PkiAcmeOrder) @@ -53,6 +62,7 @@ export const pkiAcmeOrderDALFactory = (db: TDbClient) => { return { ...pkiAcmeOrderOrm, + findByIdForFinalization, findByAccountAndOrderIdWithAuthorizations }; }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index f1d60b241..1e4af96e5 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -6,6 +6,7 @@ import { NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; +import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service"; import { EnrollmentType, TCertificateProfileWithConfigs @@ -27,6 +28,7 @@ import { AcmeBadPublicKeyError, AcmeError, AcmeMalformedError, + AcmeOrderNotReadyError, AcmeServerInternalError, AcmeUnauthorizedError, AcmeUnsupportedIdentifierError @@ -64,11 +66,15 @@ import { type TPkiAcmeServiceFactoryDep = { certificateProfileDAL: Pick; + internalCertificateAuthorityService: Pick; acmeAccountDAL: Pick< TPkiAcmeAccountDALFactory, "findByProjectIdAndAccountId" | "findByProfileIdAndPublicKeyThumbprintAndAlg" | "create" >; - acmeOrderDAL: Pick; + acmeOrderDAL: Pick< + TPkiAcmeOrderDALFactory, + "create" | "transaction" | "updateById" | "findByAccountAndOrderIdWithAuthorizations" | "findByIdForFinalization" + >; acmeAuthDAL: Pick; acmeOrderAuthDAL: Pick; acmeChallengeDAL: Pick< @@ -80,6 +86,7 @@ type TPkiAcmeServiceFactoryDep = { export const pkiAcmeServiceFactory = ({ certificateProfileDAL, + internalCertificateAuthorityService, acmeAccountDAL, acmeOrderDAL, acmeAuthDAL, @@ -497,8 +504,39 @@ export const pkiAcmeServiceFactory = ({ if (!order) { throw new NotFoundError({ message: "ACME order not found" }); } - const { csr } = payload; - // FIXME: Implement ACME finalize order + if (order.status === AcmeOrderStatus.Ready) { + await acmeOrderDAL.transaction(async (tx) => { + const order = (await acmeOrderDAL.findByIdForFinalization(orderId, tx))!; + const profile = (await certificateProfileDAL.findById(profileId, tx))!; + if (order.status !== AcmeOrderStatus.Ready) { + throw new AcmeOrderNotReadyError({ message: "ACME order is not ready" }); + } + if (order.expiresAt < new Date()) { + throw new AcmeOrderNotReadyError({ message: "ACME order has expired" }); + } + const { csr } = payload; + // TODO: validate the CSR and return badCSR error if it's invalid + const { certificate, certificateChain } = await internalCertificateAuthorityService.signCertFromCa({ + isInternal: true, + certificateTemplateId: profile.certificateTemplateId, + csr, + notBefore: order.notBefore?.toISOString(), + notAfter: order.notAfter?.toISOString() + }); + await acmeOrderDAL.updateById( + orderId, + { + status: AcmeOrderStatus.Valid, + csr, + certificate, + certificateChain + }, + tx + ); + }); + } else if (order.status !== AcmeOrderStatus.Valid) { + throw new AcmeOrderNotReadyError({ message: "ACME order is not ready" }); + } return { status: 200, body: buildAcmeOrderResource({ profileId, order }), From bbdb026beb328c91a7532d09986a43d2f5f5cd61 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 20:16:36 -0700 Subject: [PATCH 198/294] Add new columns --- backend/src/db/schemas/pki-acme-orders.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/src/db/schemas/pki-acme-orders.ts b/backend/src/db/schemas/pki-acme-orders.ts index 738155f46..4aa37606f 100644 --- a/backend/src/db/schemas/pki-acme-orders.ts +++ b/backend/src/db/schemas/pki-acme-orders.ts @@ -15,7 +15,10 @@ export const PkiAcmeOrdersSchema = z.object({ expiresAt: z.date(), status: z.string(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + csr: z.string().nullable().optional(), + certificate: z.string().nullable().optional(), + certificatechain: z.string().nullable().optional() }); export type TPkiAcmeOrders = z.infer; From e652c2a5a383bf9f138ffac1521a3ef9b5ad40dd Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 20:24:57 -0700 Subject: [PATCH 199/294] Issue cert --- backend/src/db/schemas/pki-acme-orders.ts | 2 +- backend/src/ee/services/pki-acme/pki-acme-service.ts | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/backend/src/db/schemas/pki-acme-orders.ts b/backend/src/db/schemas/pki-acme-orders.ts index 4aa37606f..61a15b156 100644 --- a/backend/src/db/schemas/pki-acme-orders.ts +++ b/backend/src/db/schemas/pki-acme-orders.ts @@ -18,7 +18,7 @@ export const PkiAcmeOrdersSchema = z.object({ updatedAt: z.date(), csr: z.string().nullable().optional(), certificate: z.string().nullable().optional(), - certificatechain: z.string().nullable().optional() + certificateChain: z.string().nullable().optional() }); export type TPkiAcmeOrders = z.infer; diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 1e4af96e5..458beefae 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -500,12 +500,12 @@ export const pkiAcmeServiceFactory = ({ orderId: string; payload: TFinalizeAcmeOrderPayload; }): Promise> => { - const order = await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId); + let order = await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId); if (!order) { throw new NotFoundError({ message: "ACME order not found" }); } if (order.status === AcmeOrderStatus.Ready) { - await acmeOrderDAL.transaction(async (tx) => { + order = await acmeOrderDAL.transaction(async (tx) => { const order = (await acmeOrderDAL.findByIdForFinalization(orderId, tx))!; const profile = (await certificateProfileDAL.findById(profileId, tx))!; if (order.status !== AcmeOrderStatus.Ready) { @@ -528,11 +528,12 @@ export const pkiAcmeServiceFactory = ({ { status: AcmeOrderStatus.Valid, csr, - certificate, - certificateChain + certificateChain, + certificate: certificate.toString("pem") }, tx ); + return await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId, tx); }); } else if (order.status !== AcmeOrderStatus.Valid) { throw new AcmeOrderNotReadyError({ message: "ACME order is not ready" }); From 47ee70c6a0c87011661097f87a7b96a12101096e Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 21:32:27 -0700 Subject: [PATCH 200/294] Issue cert --- .../ee/services/pki-acme/pki-acme-service.ts | 33 +++++++++++++------ backend/src/server/routes/index.ts | 28 ++++++++-------- .../server/routes/v3/certificates-router.ts | 16 +++++---- backend/src/services/auth/auth-type.ts | 1 + .../certificate-profile-dal.ts | 18 ++++++++++ .../certificate-v3-service.test.ts | 3 ++ .../certificate-v3/certificate-v3-service.ts | 12 +++++-- .../certificate-v3/certificate-v3-types.ts | 4 ++- 8 files changed, 81 insertions(+), 34 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 458beefae..a3c00a513 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -63,10 +63,12 @@ import { TRawJwsPayload, TRespondToAcmeChallengeResponse } from "./pki-acme-types"; +import { TCertificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service"; +import { ActorType, AuthMode } from "@app/services/auth/auth-type"; type TPkiAcmeServiceFactoryDep = { - certificateProfileDAL: Pick; - internalCertificateAuthorityService: Pick; + certificateProfileDAL: Pick; + certificateV3Service: Pick; acmeAccountDAL: Pick< TPkiAcmeAccountDALFactory, "findByProjectIdAndAccountId" | "findByProfileIdAndPublicKeyThumbprintAndAlg" | "create" @@ -86,7 +88,7 @@ type TPkiAcmeServiceFactoryDep = { export const pkiAcmeServiceFactory = ({ certificateProfileDAL, - internalCertificateAuthorityService, + certificateV3Service, acmeAccountDAL, acmeOrderDAL, acmeAuthDAL, @@ -507,7 +509,8 @@ export const pkiAcmeServiceFactory = ({ if (order.status === AcmeOrderStatus.Ready) { order = await acmeOrderDAL.transaction(async (tx) => { const order = (await acmeOrderDAL.findByIdForFinalization(orderId, tx))!; - const profile = (await certificateProfileDAL.findById(profileId, tx))!; + // TODO: ideally, this should be doen with onRequest: verifyAuth([AuthMode.ACME_JWS_SIGNATURE]), instead + const { ownerOrgId: actorOrgId } = (await certificateProfileDAL.findByIdWithOwnerOrgId(profileId, tx))!; if (order.status !== AcmeOrderStatus.Ready) { throw new AcmeOrderNotReadyError({ message: "ACME order is not ready" }); } @@ -516,20 +519,30 @@ export const pkiAcmeServiceFactory = ({ } const { csr } = payload; // TODO: validate the CSR and return badCSR error if it's invalid - const { certificate, certificateChain } = await internalCertificateAuthorityService.signCertFromCa({ - isInternal: true, - certificateTemplateId: profile.certificateTemplateId, + // TODO: this should be the same transaction? + const { certificate, certificateChain, certificateId } = await certificateV3Service.signCertificateFromProfile({ + actor: ActorType.ACME_ACCOUNT, + actorId: accountId, + actorAuthMethod: null, + actorOrgId, + profileId, csr, - notBefore: order.notBefore?.toISOString(), - notAfter: order.notAfter?.toISOString() + notBefore: order.notBefore ? new Date(order.notBefore) : undefined, + notAfter: order.notAfter ? new Date(order.notAfter) : undefined, + validity: { + // TODO: read config from the profile to get the expiration time instead + ttl: (24 * 60 * 60 * 1000).toString() + }, + enrollmentType: EnrollmentType.ACME }); + // TODO: associate the certificate with the order await acmeOrderDAL.updateById( orderId, { status: AcmeOrderStatus.Valid, csr, certificateChain, - certificate: certificate.toString("pem") + certificate }, tx ); diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index a3355ada3..13a5aee2c 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1175,20 +1175,6 @@ export const registerRoutes = async ( projectDAL }); - const acmeChallengeService = pkiAcmeChallengeServiceFactory({ - acmeAuthDAL, - acmeChallengeDAL - }); - const pkiAcmeService = pkiAcmeServiceFactory({ - certificateProfileDAL, - acmeAccountDAL, - acmeOrderDAL, - acmeAuthDAL, - acmeOrderAuthDAL, - acmeChallengeDAL, - acmeChallengeService - }); - const pkiAlertService = pkiAlertServiceFactory({ pkiAlertDAL, pkiCollectionDAL, @@ -2209,6 +2195,20 @@ export const registerRoutes = async ( estEnrollmentConfigDAL }); + const acmeChallengeService = pkiAcmeChallengeServiceFactory({ + acmeChallengeDAL + }); + const pkiAcmeService = pkiAcmeServiceFactory({ + certificateV3Service, + certificateProfileDAL, + acmeAccountDAL, + acmeOrderDAL, + acmeAuthDAL, + acmeOrderAuthDAL, + acmeChallengeDAL, + acmeChallengeService + }); + const pkiSubscriberService = pkiSubscriberServiceFactory({ pkiSubscriberDAL, certificateAuthorityDAL, diff --git a/backend/src/server/routes/v3/certificates-router.ts b/backend/src/server/routes/v3/certificates-router.ts index d2d696596..9aa4b198b 100644 --- a/backend/src/server/routes/v3/certificates-router.ts +++ b/backend/src/server/routes/v3/certificates-router.ts @@ -6,12 +6,6 @@ import { ms } from "@app/lib/ms"; import { writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; -import { - ACMESANType, - CertificateOrderStatus, - CertKeyAlgorithm, - CertSignatureAlgorithm -} from "@app/services/certificate/certificate-types"; import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators"; import { CertExtendedKeyUsageType, @@ -20,7 +14,14 @@ import { } from "@app/services/certificate-common/certificate-constants"; import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils"; import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils"; +import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types"; import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators"; +import { + ACMESANType, + CertificateOrderStatus, + CertKeyAlgorithm, + CertSignatureAlgorithm +} from "@app/services/certificate/certificate-types"; interface CertificateRequestForService { commonName?: string; @@ -204,7 +205,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => ttl: req.body.ttl }, notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined, - notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined + notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined, + enrollmentType: EnrollmentType.API }); await server.services.auditLog.createAuditLog({ diff --git a/backend/src/services/auth/auth-type.ts b/backend/src/services/auth/auth-type.ts index 497414a60..ef54ac0be 100644 --- a/backend/src/services/auth/auth-type.ts +++ b/backend/src/services/auth/auth-type.ts @@ -41,6 +41,7 @@ export enum ActorType { // would extend to AWS, Azure, ... IDENTITY = "identity", Machine = "machine", SCIM_CLIENT = "scimClient", + ACME_ACCOUNT = "acmeAccount", UNKNOWN_USER = "unknownUser" } diff --git a/backend/src/services/certificate-profile/certificate-profile-dal.ts b/backend/src/services/certificate-profile/certificate-profile-dal.ts index 7029d4b37..dcf6ca4e6 100644 --- a/backend/src/services/certificate-profile/certificate-profile-dal.ts +++ b/backend/src/services/certificate-profile/certificate-profile-dal.ts @@ -65,6 +65,23 @@ export const certificateProfileDALFactory = (db: TDbClient) => { } }; + const findByIdWithOwnerOrgId = async ( + id: string, + tx?: Knex + ): Promise<(TCertificateProfile & { ownerOrgId: string }) | undefined> => { + try { + const certificateProfile = (await (tx || db)(TableName.PkiCertificateProfile) + .join(TableName.Project, `${TableName.PkiCertificateProfile}.projectId`, `${TableName.Project}.id`) + .select(selectAllTableCols(TableName.PkiCertificateProfile)) + .select(db.ref("orgId").withSchema(TableName.Project).as("ownerOrgId")) + .where({ id }) + .first()) as (TCertificateProfile & { ownerOrgId: string }) | undefined; + return certificateProfile; + } catch (error) { + throw new DatabaseError({ error, name: "Find certificate profile by id with owner org id" }); + } + }; + const findByIdWithConfigs = async (id: string, tx?: Knex): Promise => { try { const query = (tx || db)(TableName.PkiCertificateProfile) @@ -444,6 +461,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => { updateById, deleteById, findById, + findByIdWithOwnerOrgId, findByIdWithConfigs, findBySlugAndProjectId, findByProjectId, diff --git a/backend/src/services/certificate-v3/certificate-v3-service.test.ts b/backend/src/services/certificate-v3/certificate-v3-service.test.ts index d11cce056..36823671f 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.test.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.test.ts @@ -697,6 +697,7 @@ describe("CertificateV3Service", () => { profileId, csr: mockCSR, validity: mockValidity, + enrollmentType: EnrollmentType.API, ...mockActor }); @@ -731,6 +732,7 @@ describe("CertificateV3Service", () => { profileId, csr: mockCSR, validity: mockValidity, + enrollmentType: EnrollmentType.API, ...mockActor }) ).rejects.toThrow(ForbiddenRequestError); @@ -740,6 +742,7 @@ describe("CertificateV3Service", () => { profileId, csr: mockCSR, validity: mockValidity, + enrollmentType: EnrollmentType.API, ...mockActor }) ).rejects.toThrow("Profile is not configured for api enrollment"); diff --git a/backend/src/services/certificate-v3/certificate-v3-service.ts b/backend/src/services/certificate-v3/certificate-v3-service.ts index 51c79f135..5ccd02698 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.ts @@ -107,6 +107,13 @@ const validateProfileAndPermissions = async ( }); } + // XXX: NOT SURE IF THIS IS SECURE TO BY PASS THE PERMISSION CHECK FOR ACME ACCOUNTS + // may need to consider this carefully + // TODO: check actor/profile ownership as well + if (actor === ActorType.ACME_ACCOUNT && requiredEnrollmentType === EnrollmentType.ACME) { + return profile; + } + const { permission } = await permissionService.getProjectPermission({ actor, actorId, @@ -484,7 +491,8 @@ export const certificateV3ServiceFactory = ({ actor, actorId, actorAuthMethod, - actorOrgId + actorOrgId, + enrollmentType }: TSignCertificateFromProfileDTO): Promise> => { const profile = await validateProfileAndPermissions( profileId, @@ -494,7 +502,7 @@ export const certificateV3ServiceFactory = ({ actorOrgId, certificateProfileDAL, permissionService, - EnrollmentType.API + enrollmentType ); const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId); diff --git a/backend/src/services/certificate-v3/certificate-v3-types.ts b/backend/src/services/certificate-v3/certificate-v3-types.ts index a62a25b73..8685d5f30 100644 --- a/backend/src/services/certificate-v3/certificate-v3-types.ts +++ b/backend/src/services/certificate-v3/certificate-v3-types.ts @@ -1,11 +1,12 @@ import { TProjectPermission } from "@app/lib/types"; -import { ACMESANType, CertificateOrderStatus } from "../certificate/certificate-types"; import { CertExtendedKeyUsageType, CertKeyUsageType, CertSubjectAlternativeNameType } from "../certificate-common/certificate-constants"; +import { EnrollmentType } from "../certificate-profile/certificate-profile-types"; +import { ACMESANType, CertificateOrderStatus } from "../certificate/certificate-types"; export type TIssueCertificateFromProfileDTO = { profileId: string; @@ -35,6 +36,7 @@ export type TSignCertificateFromProfileDTO = { }; notBefore?: Date; notAfter?: Date; + enrollmentType: EnrollmentType; } & Omit; export type TOrderCertificateFromProfileDTO = { From e5db20d06254bff577d5c16d9824474810e07edc Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 21:43:53 -0700 Subject: [PATCH 201/294] Handle errors --- backend/src/db/schemas/pki-acme-orders.ts | 3 +- .../ee/services/pki-acme/pki-acme-errors.ts | 21 ++++++ .../ee/services/pki-acme/pki-acme-service.ts | 74 ++++++++++++------- .../certificate-profile-dal.ts | 2 +- 4 files changed, 71 insertions(+), 29 deletions(-) diff --git a/backend/src/db/schemas/pki-acme-orders.ts b/backend/src/db/schemas/pki-acme-orders.ts index 61a15b156..eee8c96f0 100644 --- a/backend/src/db/schemas/pki-acme-orders.ts +++ b/backend/src/db/schemas/pki-acme-orders.ts @@ -18,7 +18,8 @@ export const PkiAcmeOrdersSchema = z.object({ updatedAt: z.date(), csr: z.string().nullable().optional(), certificate: z.string().nullable().optional(), - certificateChain: z.string().nullable().optional() + certificateChain: z.string().nullable().optional(), + error: z.string().nullable().optional() }); export type TPkiAcmeOrders = z.infer; diff --git a/backend/src/ee/services/pki-acme/pki-acme-errors.ts b/backend/src/ee/services/pki-acme/pki-acme-errors.ts index 924a7b0ca..73f17e061 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-errors.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-errors.ts @@ -528,3 +528,24 @@ export class AcmeOrderNotReadyError extends AcmeError { this.name = "AcmeOrderNotReadyError"; } } + +export class AcmeBadCSRError extends AcmeError { + constructor({ + detail = "The CSR is unacceptable", + error, + message + }: { + detail?: string; + error?: unknown; + message?: string; + } = {}) { + super({ + type: AcmeErrorType.BadCsr, + detail, + status: 400, + error, + message + }); + this.name = "AcmeBadCSRError"; + } +} diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index a3c00a513..43754d74f 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -2,7 +2,7 @@ import { TPkiAcmeAccounts } from "@app/db/schemas/pki-acme-accounts"; import { TPkiAcmeAuths } from "@app/db/schemas/pki-acme-auths"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; -import { NotFoundError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; @@ -25,6 +25,7 @@ import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal"; import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal"; import { AcmeAccountDoesNotExistError, + AcmeBadCSRError, AcmeBadPublicKeyError, AcmeError, AcmeMalformedError, @@ -520,32 +521,51 @@ export const pkiAcmeServiceFactory = ({ const { csr } = payload; // TODO: validate the CSR and return badCSR error if it's invalid // TODO: this should be the same transaction? - const { certificate, certificateChain, certificateId } = await certificateV3Service.signCertificateFromProfile({ - actor: ActorType.ACME_ACCOUNT, - actorId: accountId, - actorAuthMethod: null, - actorOrgId, - profileId, - csr, - notBefore: order.notBefore ? new Date(order.notBefore) : undefined, - notAfter: order.notAfter ? new Date(order.notAfter) : undefined, - validity: { - // TODO: read config from the profile to get the expiration time instead - ttl: (24 * 60 * 60 * 1000).toString() - }, - enrollmentType: EnrollmentType.ACME - }); - // TODO: associate the certificate with the order - await acmeOrderDAL.updateById( - orderId, - { - status: AcmeOrderStatus.Valid, - csr, - certificateChain, - certificate - }, - tx - ); + try { + const { certificate, certificateChain, certificateId } = + await certificateV3Service.signCertificateFromProfile({ + actor: ActorType.ACME_ACCOUNT, + actorId: accountId, + actorAuthMethod: null, + actorOrgId, + profileId, + csr, + notBefore: order.notBefore ? new Date(order.notBefore) : undefined, + notAfter: order.notAfter ? new Date(order.notAfter) : undefined, + validity: { + // TODO: read config from the profile to get the expiration time instead + ttl: (24 * 60 * 60 * 1000).toString() + }, + enrollmentType: EnrollmentType.ACME + }); + // TODO: associate the certificate with the order + await acmeOrderDAL.updateById( + orderId, + { + status: AcmeOrderStatus.Valid, + csr, + certificateChain, + certificate + }, + tx + ); + } catch (error) { + await acmeOrderDAL.updateById( + orderId, + { + csr, + status: AcmeOrderStatus.Invalid, + error: error instanceof Error ? error.message : "Unknown error" + }, + tx + ); + // TODO: log the error + // TODO: audit log the error + if (error instanceof BadRequestError) { + throw new AcmeBadCSRError({ detail: `Invalid CSR: ${error.message}` }); + } + throw new AcmeServerInternalError({ detail: "Failed to sign certificate" }); + } return await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId, tx); }); } else if (order.status !== AcmeOrderStatus.Valid) { diff --git a/backend/src/services/certificate-profile/certificate-profile-dal.ts b/backend/src/services/certificate-profile/certificate-profile-dal.ts index dcf6ca4e6..beea084e3 100644 --- a/backend/src/services/certificate-profile/certificate-profile-dal.ts +++ b/backend/src/services/certificate-profile/certificate-profile-dal.ts @@ -74,7 +74,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => { .join(TableName.Project, `${TableName.PkiCertificateProfile}.projectId`, `${TableName.Project}.id`) .select(selectAllTableCols(TableName.PkiCertificateProfile)) .select(db.ref("orgId").withSchema(TableName.Project).as("ownerOrgId")) - .where({ id }) + .where(`${TableName.PkiCertificateProfile}.id`, id) .first()) as (TCertificateProfile & { ownerOrgId: string }) | undefined; return certificateProfile; } catch (error) { From bba265d21ea73d560dca8692e84421ad4be58bf0 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 21:53:15 -0700 Subject: [PATCH 202/294] Keep db update --- .../migrations/20251029234547_add-pki-acme.ts | 8 ++++---- .../ee/services/pki-acme/pki-acme-service.ts | 17 +++++++++++++---- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/backend/src/db/migrations/20251029234547_add-pki-acme.ts b/backend/src/db/migrations/20251029234547_add-pki-acme.ts index ee5ba9f36..e76587159 100644 --- a/backend/src/db/migrations/20251029234547_add-pki-acme.ts +++ b/backend/src/db/migrations/20251029234547_add-pki-acme.ts @@ -85,11 +85,11 @@ export async function up(knex: Knex): Promise { t.timestamp("expiresAt").notNullable(); - t.string("csr").nullable(); - t.string("certificate").nullable(); - t.string("certificateChain").nullable(); + t.text("csr").nullable(); + t.text("certificate").nullable(); + t.text("certificateChain").nullable(); - t.string("error").nullable(); + t.text("error").nullable(); // Order status t.string("status").notNullable(); // pending, ready, processing, valid, invalid diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 43754d74f..568f3d852 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -508,7 +508,7 @@ export const pkiAcmeServiceFactory = ({ throw new NotFoundError({ message: "ACME order not found" }); } if (order.status === AcmeOrderStatus.Ready) { - order = await acmeOrderDAL.transaction(async (tx) => { + const { order: updatedOrder, error } = await acmeOrderDAL.transaction(async (tx) => { const order = (await acmeOrderDAL.findByIdForFinalization(orderId, tx))!; // TODO: ideally, this should be doen with onRequest: verifyAuth([AuthMode.ACME_JWS_SIGNATURE]), instead const { ownerOrgId: actorOrgId } = (await certificateProfileDAL.findByIdWithOwnerOrgId(profileId, tx))!; @@ -521,6 +521,7 @@ export const pkiAcmeServiceFactory = ({ const { csr } = payload; // TODO: validate the CSR and return badCSR error if it's invalid // TODO: this should be the same transaction? + let error: Error | undefined; try { const { certificate, certificateChain, certificateId } = await certificateV3Service.signCertificateFromProfile({ @@ -562,12 +563,20 @@ export const pkiAcmeServiceFactory = ({ // TODO: log the error // TODO: audit log the error if (error instanceof BadRequestError) { - throw new AcmeBadCSRError({ detail: `Invalid CSR: ${error.message}` }); + error = new AcmeBadCSRError({ detail: `Invalid CSR: ${error.message}` }); + } else { + error = new AcmeServerInternalError({ detail: "Failed to sign certificate" }); } - throw new AcmeServerInternalError({ detail: "Failed to sign certificate" }); } - return await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId, tx); + return { + order: (await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId, tx))!, + error + }; }); + if (error) { + throw error; + } + order = updatedOrder; } else if (order.status !== AcmeOrderStatus.Valid) { throw new AcmeOrderNotReadyError({ message: "ACME order is not ready" }); } From a0c0bd6a40e065aa6b560d005721c3fdea2a345d Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 22:04:03 -0700 Subject: [PATCH 203/294] Fix returning errors --- backend/src/ee/services/pki-acme/pki-acme-service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 568f3d852..84f28c7ac 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -521,7 +521,7 @@ export const pkiAcmeServiceFactory = ({ const { csr } = payload; // TODO: validate the CSR and return badCSR error if it's invalid // TODO: this should be the same transaction? - let error: Error | undefined; + let errorToReturn: Error | undefined; try { const { certificate, certificateChain, certificateId } = await certificateV3Service.signCertificateFromProfile({ @@ -563,14 +563,14 @@ export const pkiAcmeServiceFactory = ({ // TODO: log the error // TODO: audit log the error if (error instanceof BadRequestError) { - error = new AcmeBadCSRError({ detail: `Invalid CSR: ${error.message}` }); + errorToReturn = new AcmeBadCSRError({ detail: `Invalid CSR: ${error.message}` }); } else { - error = new AcmeServerInternalError({ detail: "Failed to sign certificate" }); + errorToReturn = new AcmeServerInternalError({ detail: "Failed to sign certificate with internal error" }); } } return { order: (await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId, tx))!, - error + error: errorToReturn }; }); if (error) { From 4bb19dd73061ca86fc808acc1df608d376da3f28 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 22:17:42 -0700 Subject: [PATCH 204/294] Check empty payload --- backend/src/ee/routes/v1/pki-acme-router.ts | 9 ++++++--- backend/src/ee/services/pki-acme/pki-acme-service.ts | 7 ++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index 329a381e3..8b6c7c817 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -10,6 +10,7 @@ import { CreateAcmeOrderBodySchema, DeactivateAcmeAccountBodySchema, DeactivateAcmeAccountResponseSchema, + DownloadAcmeCertificateBodySchema, FinalizeAcmeOrderBodySchema, GetAcmeAuthorizationResponseSchema, GetAcmeDirectoryResponseSchema, @@ -372,10 +373,12 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { } }, handler: async (req, res) => { - const { profileId, accountId } = await validateExistingAccount({ - req, - schema: FinalizeAcmeOrderBodySchema + const { profileId, accountId, payload } = await validateExistingAccount({ + req }); + if (payload !== "") { + throw new AcmeMalformedError({ detail: "Payload should be empty" }); + } return sendAcmeResponse( res, profileId, diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 84f28c7ac..134645fa5 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -603,11 +603,12 @@ export const pkiAcmeServiceFactory = ({ if (!order) { throw new NotFoundError({ message: "ACME order not found" }); } - // FIXME: Implement ACME certificate download - // Return the certificate in PEM format + if (order.status !== AcmeOrderStatus.Valid) { + throw new AcmeOrderNotReadyError({ message: "ACME order is not valid" }); + } return { status: 200, - body: "FIXME-certificate-pem", + body: order.certificateChain! + "\n" + order.certificate!, headers: { Location: buildUrl(profileId, `/orders/${orderId}/certificate`), Link: `<${buildUrl(profileId, "/directory")}>;rel="index"` From 5b87011e0124cd81fb20dce8eb703617d434026d Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 22:34:45 -0700 Subject: [PATCH 205/294] Add missing cert link for order --- backend/src/ee/routes/v1/pki-acme-router.ts | 8 +++++--- backend/src/ee/services/pki-acme/pki-acme-service.ts | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index 8b6c7c817..1e06a93e6 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -261,10 +261,12 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { } }, handler: async (req, res) => { - const { profileId, accountId } = await validateExistingAccount({ - req, - schema: FinalizeAcmeOrderBodySchema + const { profileId, accountId, payload } = await validateExistingAccount({ + req }); + if (payload !== "") { + throw new AcmeMalformedError({ detail: "Payload should be empty" }); + } return sendAcmeResponse( res, profileId, diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 134645fa5..a1c719912 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -276,7 +276,9 @@ export const pkiAcmeServiceFactory = ({ authorizations: order.authorizations.map((auth: TPkiAcmeAuths) => buildUrl(profileId, `/authorizations/${auth.id}`) ), - finalize: buildUrl(profileId, `/orders/${order.id}/finalize`) + finalize: buildUrl(profileId, `/orders/${order.id}/finalize`), + certificate: + order.status === AcmeOrderStatus.Valid ? buildUrl(profileId, `/orders/${order.id}/certificate`) : undefined }; }; From 07f51e767283bc80f6b52e8b452ff384987fcc58 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 22:35:56 -0700 Subject: [PATCH 206/294] Add type --- backend/src/ee/services/pki-acme/pki-acme-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index a1c719912..6e23ef2bc 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -263,7 +263,7 @@ export const pkiAcmeServiceFactory = ({ authorizations: TPkiAcmeAuths[]; }; profileId: string; - }) => { + }): TAcmeOrderResource => { return { status: order.status, expires: order.expiresAt.toISOString(), From 3a84e9a50e08aa55d0e17e57d02a1cddb7be996a Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 22:36:29 -0700 Subject: [PATCH 207/294] Org import --- backend/src/ee/services/pki-acme/pki-acme-service.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 6e23ef2bc..a478a1721 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -6,11 +6,12 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; -import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service"; +import { ActorType } from "@app/services/auth/auth-type"; import { EnrollmentType, TCertificateProfileWithConfigs } from "@app/services/certificate-profile/certificate-profile-types"; +import { TCertificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service"; import { calculateJwkThumbprint, errors, @@ -64,8 +65,6 @@ import { TRawJwsPayload, TRespondToAcmeChallengeResponse } from "./pki-acme-types"; -import { TCertificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service"; -import { ActorType, AuthMode } from "@app/services/auth/auth-type"; type TPkiAcmeServiceFactoryDep = { certificateProfileDAL: Pick; From ebb42603869f7ca9725242006fc13c1860e8939c Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 12:40:55 -0800 Subject: [PATCH 208/294] Add ACME cert profile creation feature --- backend/bdd/features/environment.py | 8 ++++++ .../features/pki/acme/cert-profile.feature | 23 +++++++++++++++++ backend/bdd/features/steps/pki_acme.py | 25 +++++++++++++++++++ backend/bdd/pyproject.toml | 1 + backend/bdd/uv.lock | 23 +++++++++++++++++ 5 files changed, 80 insertions(+) create mode 100644 backend/bdd/features/pki/acme/cert-profile.feature diff --git a/backend/bdd/features/environment.py b/backend/bdd/features/environment.py index 5b94d0415..8aa78e276 100644 --- a/backend/bdd/features/environment.py +++ b/backend/bdd/features/environment.py @@ -4,12 +4,20 @@ import httpx from behave.runner import Context BASE_URL = os.environ.get("INFISICAL_API_URL", "http://localhost:8080") +PROJECT_ID = os.environ.get("PROJECT_ID", "c051e74c-48a7-4724-832c-d5b496698546") +CERT_CA_ID = os.environ.get("CERT_CA_ID", "2f0d9820-e5a8-48bb-aac8-deed9d868a1e") +CERT_TEMPLATE_ID = os.environ.get( + "CERT_TEMPLATE_ID", "4dbf6bb0-6e86-4ee6-8550-9171428c8e82" +) AUTH_TOKEN = os.environ.get("INFISICAL_TOKEN") def before_all(context: Context): context.vars = { "BASE_URL": BASE_URL, + "PROJECT_ID": PROJECT_ID, + "CERT_CA_ID": CERT_CA_ID, + "CERT_TEMPLATE_ID": CERT_TEMPLATE_ID, } context.http_client = httpx.Client( base_url=BASE_URL, # headers={"Authorization": f"Bearer {AUTH_TOKEN}"} diff --git a/backend/bdd/features/pki/acme/cert-profile.feature b/backend/bdd/features/pki/acme/cert-profile.feature new file mode 100644 index 000000000..4af911a93 --- /dev/null +++ b/backend/bdd/features/pki/acme/cert-profile.feature @@ -0,0 +1,23 @@ +Feature: ACME Cert Profile + + Scenario: Create a cert profile + Given I make a random slug as profile_slug + When I send a POST request to "/api/v1/pki/certificate-profiles" with JSON payload + """ + { + "projectId": "{PROJECT_ID}", + "slug": "{profile_slug}", + "description": "", + "enrollmentType": "acme", + "caId": "{CA_ID}", + "certificateTemplateId": "{CERT_TEMPLATE_ID}", + "acmeConfig": {} + } + """ + Then the value response.status should be equal to 201 + Then the value response with jq .eab_kid should be present + Then the value response with jq .eab_secret should be present + Then the value response with jq .slug should be equal to {profile_slug} + Then the value response with jq .caId should be equal to {CA_ID} + Then the value response with jq .certificateTemplateId should be equal to {CERT_TEMPLATE_ID} + Then the value response with jq .enrollmentTYpe should be equal to acme diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 3156d4a71..81b4b6023 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -2,6 +2,7 @@ import json import re import threading +import faker import jq import requests import glom @@ -107,6 +108,11 @@ def eval_var(context: Context, var_path: str, as_json: bool = True): return value +@given("I make a random {faker_type} as {var_name}") +def step_impl(context: Context, faker_type: str, var_name: str): + context.vars[var_name] = getattr(faker, faker_type)() + + @given('I have an ACME cert profile as "{profile_var}"') def step_impl(context: Context, profile_var: str): # TODO: Fixed value for now, just to make test much easier, @@ -121,6 +127,13 @@ def step_impl(context: Context, method: str, url: str): context.response = context.http_client.request(method, url.format(**context.vars)) +@when('I send a {method} request to "{url}" with JSON payload') +def step_impl(context: Context, method: str, url: str): + context.response = context.http_client.request( + method, url.format(**context.vars), json_payload=context.text + ) + + @when("I have an ACME client connecting to {url}") def step_impl(context: Context, url: str): private_key = rsa.generate_private_key( @@ -260,6 +273,18 @@ def step_impl(context: Context, var_path: str, jq_query: str): ) +@then("the value {var_path} with jq {jq_query} should be present") +def step_impl(context: Context, var_path: str, jq_query: str): + value, result = apply_value_with_jq( + context=context, + var_path=var_path, + jq_query=jq_query, + ) + assert result, ( + f"{json.dumps(value)!r} with jq {jq_query!r}, the result {json.dumps(result)!r} is not present" + ) + + @then("the value {var_path} with jq {jq_query} should be equal to {expected}") def step_impl(context: Context, var_path: str, jq_query: str, expected: str): value, result = apply_value_with_jq( diff --git a/backend/bdd/pyproject.toml b/backend/bdd/pyproject.toml index 80e1979a1..2decf1967 100644 --- a/backend/bdd/pyproject.toml +++ b/backend/bdd/pyproject.toml @@ -7,6 +7,7 @@ requires-python = ">=3.12" dependencies = [ "acme>=5.1.0", "behave>=1.3.3", + "faker>=37.12.0", "glom>=24.11.0", "httpx>=0.28.1", "josepy>=2.2.0", diff --git a/backend/bdd/uv.lock b/backend/bdd/uv.lock index beceb0f26..17e9a4d28 100644 --- a/backend/bdd/uv.lock +++ b/backend/bdd/uv.lock @@ -48,6 +48,7 @@ source = { virtual = "." } dependencies = [ { name = "acme" }, { name = "behave" }, + { name = "faker" }, { name = "glom" }, { name = "httpx" }, { name = "josepy" }, @@ -58,6 +59,7 @@ dependencies = [ requires-dist = [ { name = "acme", specifier = ">=5.1.0" }, { name = "behave", specifier = ">=1.3.3" }, + { name = "faker", specifier = ">=37.12.0" }, { name = "glom", specifier = ">=24.11.0" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "josepy", specifier = ">=2.2.0" }, @@ -308,6 +310,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/47/21867c2e5fd006c8d36a560df9e32cb4f1f566b20c5dd41f5f8a2124f7de/face-24.0.0-py3-none-any.whl", hash = "sha256:0e2c17b426fa4639a4e77d1de9580f74a98f4869ba4c7c8c175b810611622cd3", size = 54742, upload-time = "2024-11-02T05:24:24.939Z" }, ] +[[package]] +name = "faker" +version = "37.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/84/e95acaa848b855e15c83331d0401ee5f84b2f60889255c2e055cb4fb6bdf/faker-37.12.0.tar.gz", hash = "sha256:7505e59a7e02fa9010f06c3e1e92f8250d4cfbb30632296140c2d6dbef09b0fa", size = 1935741, upload-time = "2025-10-24T15:19:58.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/98/2c050dec90e295a524c9b65c4cb9e7c302386a296b2938710448cbd267d5/faker-37.12.0-py3-none-any.whl", hash = "sha256:afe7ccc038da92f2fbae30d8e16d19d91e92e242f8401ce9caf44de892bab4c4", size = 1975461, upload-time = "2025-10-24T15:19:55.739Z" }, +] + [[package]] name = "glom" version = "24.11.0" @@ -503,6 +517,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + [[package]] name = "urllib3" version = "2.5.0" From 79e4231ddfcbfeaab8c6fe7c3071d2358923ae43 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 13:10:04 -0800 Subject: [PATCH 209/294] Add auth token --- backend/bdd/features/environment.py | 7 ++++ .../features/pki/acme/cert-profile.feature | 3 +- backend/bdd/features/steps/pki_acme.py | 33 ++++++++++++++++--- backend/bdd/pyproject.toml | 1 + backend/bdd/uv.lock | 22 +++++++++++++ 5 files changed, 61 insertions(+), 5 deletions(-) diff --git a/backend/bdd/features/environment.py b/backend/bdd/features/environment.py index 8aa78e276..e5c5664c1 100644 --- a/backend/bdd/features/environment.py +++ b/backend/bdd/features/environment.py @@ -2,6 +2,12 @@ import os import httpx from behave.runner import Context +from dotenv import load_dotenv +import logging + +logging.getLogger("httpx").setLevel(logging.DEBUG) + +load_dotenv() BASE_URL = os.environ.get("INFISICAL_API_URL", "http://localhost:8080") PROJECT_ID = os.environ.get("PROJECT_ID", "c051e74c-48a7-4724-832c-d5b496698546") @@ -18,6 +24,7 @@ def before_all(context: Context): "PROJECT_ID": PROJECT_ID, "CERT_CA_ID": CERT_CA_ID, "CERT_TEMPLATE_ID": CERT_TEMPLATE_ID, + "AUTH_TOKEN": AUTH_TOKEN, } context.http_client = httpx.Client( base_url=BASE_URL, # headers={"Authorization": f"Bearer {AUTH_TOKEN}"} diff --git a/backend/bdd/features/pki/acme/cert-profile.feature b/backend/bdd/features/pki/acme/cert-profile.feature index 4af911a93..7cdfe3127 100644 --- a/backend/bdd/features/pki/acme/cert-profile.feature +++ b/backend/bdd/features/pki/acme/cert-profile.feature @@ -2,6 +2,7 @@ Feature: ACME Cert Profile Scenario: Create a cert profile Given I make a random slug as profile_slug + Given I use AUTH_TOKEN for authentication When I send a POST request to "/api/v1/pki/certificate-profiles" with JSON payload """ { @@ -14,7 +15,7 @@ Feature: ACME Cert Profile "acmeConfig": {} } """ - Then the value response.status should be equal to 201 + Then the value response.status_code should be equal to 201 Then the value response with jq .eab_kid should be present Then the value response with jq .eab_secret should be present Then the value response with jq .slug should be equal to {profile_slug} diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 81b4b6023..2f397b883 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -1,11 +1,12 @@ import json +import logging import re import threading -import faker import jq import requests import glom +from faker import Faker from acme import client from acme import messages from acme import standalone @@ -23,6 +24,8 @@ from cryptography.hazmat.primitives import hashes ACC_KEY_BITS = 2048 ACC_KEY_PUBLIC_EXPONENT = 65537 +logger = logging.getLogger(__name__) +faker = Faker() class AcmeProfile: @@ -108,6 +111,16 @@ def eval_var(context: Context, var_path: str, as_json: bool = True): return value +def prepare_headers(context: Context) -> dict | None: + headers = {} + auth_token = getattr(context, "auth_token", None) + if auth_token is not None: + headers["authorization"] = "Bearer {}".format(auth_token) + if not headers: + return None + return headers + + @given("I make a random {faker_type} as {var_name}") def step_impl(context: Context, faker_type: str, var_name: str): context.vars[var_name] = getattr(faker, faker_type)() @@ -122,16 +135,28 @@ def step_impl(context: Context, profile_var: str): context.vars[profile_var] = AcmeProfile(profile_id) +@given("I use {token_var} for authentication") +def step_impl(context: Context, token_var: str): + context.auth_token = eval_var(context, token_var) + + @when('I send a {method} request to "{url}"') def step_impl(context: Context, method: str, url: str): - context.response = context.http_client.request(method, url.format(**context.vars)) + context.response = context.http_client.request( + method, url.format(**context.vars), headers=prepare_headers(context) + ) @when('I send a {method} request to "{url}" with JSON payload') def step_impl(context: Context, method: str, url: str): - context.response = context.http_client.request( - method, url.format(**context.vars), json_payload=context.text + response = context.http_client.request( + method, + url.format(**context.vars), + headers=prepare_headers(context), + json=json.loads(context.text), ) + context.response = response + context.vars["response"] = response @when("I have an ACME client connecting to {url}") diff --git a/backend/bdd/pyproject.toml b/backend/bdd/pyproject.toml index 2decf1967..4f19cb0de 100644 --- a/backend/bdd/pyproject.toml +++ b/backend/bdd/pyproject.toml @@ -7,6 +7,7 @@ requires-python = ">=3.12" dependencies = [ "acme>=5.1.0", "behave>=1.3.3", + "dotenv>=0.9.9", "faker>=37.12.0", "glom>=24.11.0", "httpx>=0.28.1", diff --git a/backend/bdd/uv.lock b/backend/bdd/uv.lock index 17e9a4d28..8ae5b6c01 100644 --- a/backend/bdd/uv.lock +++ b/backend/bdd/uv.lock @@ -48,6 +48,7 @@ source = { virtual = "." } dependencies = [ { name = "acme" }, { name = "behave" }, + { name = "dotenv" }, { name = "faker" }, { name = "glom" }, { name = "httpx" }, @@ -59,6 +60,7 @@ dependencies = [ requires-dist = [ { name = "acme", specifier = ">=5.1.0" }, { name = "behave", specifier = ">=1.3.3" }, + { name = "dotenv", specifier = ">=0.9.9" }, { name = "faker", specifier = ">=37.12.0" }, { name = "glom", specifier = ">=24.11.0" }, { name = "httpx", specifier = ">=0.28.1" }, @@ -298,6 +300,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/51/51ae3ab3b8553ec61f6558e9a0a9e8c500a9db844f9cf00a732b19c9a6ea/cucumber_tag_expressions-8.0.0-py3-none-any.whl", hash = "sha256:bfe552226f62a4462ee91c9643582f524af84ac84952643fb09057580cbb110a", size = 9726, upload-time = "2025-10-14T17:01:26.098Z" }, ] +[[package]] +name = "dotenv" +version = "0.9.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dotenv" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" }, +] + [[package]] name = "face" version = "24.0.0" @@ -475,6 +488,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/34/90/0200184d2124484f918054751ef997ed6409cb05b7e8dcbf5a22da4c4748/pyrfc3339-2.1.0-py3-none-any.whl", hash = "sha256:560f3f972e339f579513fe1396974352fd575ef27caff160a38b312252fcddf3", size = 6758, upload-time = "2025-08-23T16:40:30.49Z" }, ] +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + [[package]] name = "requests" version = "2.32.5" From 2cee9cf3cc2232a0c73ecd3f279bcd92d1d733e6 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 13:13:57 -0800 Subject: [PATCH 210/294] More tests --- backend/bdd/features/pki/acme/cert-profile.feature | 2 +- backend/bdd/features/steps/pki_acme.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/backend/bdd/features/pki/acme/cert-profile.feature b/backend/bdd/features/pki/acme/cert-profile.feature index 7cdfe3127..7e4dbdc84 100644 --- a/backend/bdd/features/pki/acme/cert-profile.feature +++ b/backend/bdd/features/pki/acme/cert-profile.feature @@ -10,7 +10,7 @@ Feature: ACME Cert Profile "slug": "{profile_slug}", "description": "", "enrollmentType": "acme", - "caId": "{CA_ID}", + "caId": "{CERT_CA_ID}", "certificateTemplateId": "{CERT_TEMPLATE_ID}", "acmeConfig": {} } diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 2f397b883..0bd55e397 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -149,14 +149,24 @@ def step_impl(context: Context, method: str, url: str): @when('I send a {method} request to "{url}" with JSON payload') def step_impl(context: Context, method: str, url: str): + json_payload = json.loads(context.text) + json_payload = replace_vars(json_payload, context.vars) + logger.debug( + "Sending %s request to %s with JSON payload: %s", + method, + url, + json.dumps(json_payload), + ) response = context.http_client.request( method, url.format(**context.vars), headers=prepare_headers(context), - json=json.loads(context.text), + json=json_payload, ) context.response = response context.vars["response"] = response + logger.debug("Response status: %r", response.status_code) + logger.debug("Response JSON payload: %r", response.json()) @when("I have an ACME client connecting to {url}") From 52ae835d0fe707714c0c48650aac80e37ba386a4 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 13:19:34 -0800 Subject: [PATCH 211/294] More tests --- backend/bdd/features/pki/acme/cert-profile.feature | 14 +++++++------- backend/bdd/features/steps/pki_acme.py | 5 ++++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/backend/bdd/features/pki/acme/cert-profile.feature b/backend/bdd/features/pki/acme/cert-profile.feature index 7e4dbdc84..1fc6f0dae 100644 --- a/backend/bdd/features/pki/acme/cert-profile.feature +++ b/backend/bdd/features/pki/acme/cert-profile.feature @@ -15,10 +15,10 @@ Feature: ACME Cert Profile "acmeConfig": {} } """ - Then the value response.status_code should be equal to 201 - Then the value response with jq .eab_kid should be present - Then the value response with jq .eab_secret should be present - Then the value response with jq .slug should be equal to {profile_slug} - Then the value response with jq .caId should be equal to {CA_ID} - Then the value response with jq .certificateTemplateId should be equal to {CERT_TEMPLATE_ID} - Then the value response with jq .enrollmentTYpe should be equal to acme + Then the value response.status_code should be equal to 200 + Then the value response with jq .certificateProfile.slug should be equal to "{profile_slug}" + Then the value response with jq .certificateProfile.caId should be equal to "{CERT_CA_ID}" + Then the value response with jq .certificateProfile.certificateTemplateId should be equal to "{CERT_TEMPLATE_ID}" + Then the value response with jq .certificateProfile.enrollmentType should be equal to "acme" + Then the value response with jq .certificateProfile.eab_kid should be present + Then the value response with jq .certificateProfile.eab_secret should be present diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 0bd55e397..f2008e867 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -3,6 +3,7 @@ import logging import re import threading +import httpx import jq import requests import glom @@ -108,6 +109,8 @@ def eval_var(context: Context, var_path: str, as_json: bool = True): value = value.to_json() elif isinstance(value, requests.Response): value = value.json() + elif isinstance(value, httpx.Response): + value = value.json() return value @@ -327,7 +330,7 @@ def step_impl(context: Context, var_path: str, jq_query: str, expected: str): var_path=var_path, jq_query=jq_query, ) - expected_value = json.loads(expected) + expected_value = replace_vars(json.loads(expected), context.vars) assert result == expected_value, ( f"{json.dumps(value)!r} with jq {jq_query!r}, the result {json.dumps(result)!r} does not match {json.dumps(expected_value)!r}" ) From 6c6c5f803d503d4f7fb95651796bc03a6eecfd4c Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 13:23:11 -0800 Subject: [PATCH 212/294] Naming style --- backend/bdd/features/pki/acme/cert-profile.feature | 4 ++-- backend/src/ee/routes/v1/pki-acme-router.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/bdd/features/pki/acme/cert-profile.feature b/backend/bdd/features/pki/acme/cert-profile.feature index 1fc6f0dae..1d6dea0c5 100644 --- a/backend/bdd/features/pki/acme/cert-profile.feature +++ b/backend/bdd/features/pki/acme/cert-profile.feature @@ -20,5 +20,5 @@ Feature: ACME Cert Profile Then the value response with jq .certificateProfile.caId should be equal to "{CERT_CA_ID}" Then the value response with jq .certificateProfile.certificateTemplateId should be equal to "{CERT_TEMPLATE_ID}" Then the value response with jq .certificateProfile.enrollmentType should be equal to "acme" - Then the value response with jq .certificateProfile.eab_kid should be present - Then the value response with jq .certificateProfile.eab_secret should be present + Then the value response with jq .certificateProfile.eabKid should be present + Then the value response with jq .certificateProfile.eabSecret should be present diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index 1e06a93e6..f628c8d36 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -10,7 +10,6 @@ import { CreateAcmeOrderBodySchema, DeactivateAcmeAccountBodySchema, DeactivateAcmeAccountResponseSchema, - DownloadAcmeCertificateBodySchema, FinalizeAcmeOrderBodySchema, GetAcmeAuthorizationResponseSchema, GetAcmeDirectoryResponseSchema, From fd6128aa65284492c38723696c2c0460bc2c7d4a Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 14:11:01 -0800 Subject: [PATCH 213/294] Implement acme secret reveal --- .../features/pki/acme/cert-profile.feature | 2 - .../ee/services/permission/default-roles.ts | 4 +- .../services/permission/project-permission.ts | 4 +- .../routes/v1/certificate-profiles-router.ts | 33 ++++++++++- .../certificate-profile-service.ts | 56 ++++++++++++++++++- .../certificate-profile-types.ts | 1 + 6 files changed, 94 insertions(+), 6 deletions(-) diff --git a/backend/bdd/features/pki/acme/cert-profile.feature b/backend/bdd/features/pki/acme/cert-profile.feature index 1d6dea0c5..f25d0eaf2 100644 --- a/backend/bdd/features/pki/acme/cert-profile.feature +++ b/backend/bdd/features/pki/acme/cert-profile.feature @@ -20,5 +20,3 @@ Feature: ACME Cert Profile Then the value response with jq .certificateProfile.caId should be equal to "{CERT_CA_ID}" Then the value response with jq .certificateProfile.certificateTemplateId should be equal to "{CERT_TEMPLATE_ID}" Then the value response with jq .certificateProfile.enrollmentType should be equal to "acme" - Then the value response with jq .certificateProfile.eabKid should be present - Then the value response with jq .certificateProfile.eabSecret should be present diff --git a/backend/src/ee/services/permission/default-roles.ts b/backend/src/ee/services/permission/default-roles.ts index 34876f739..5e7025f05 100644 --- a/backend/src/ee/services/permission/default-roles.ts +++ b/backend/src/ee/services/permission/default-roles.ts @@ -106,7 +106,9 @@ const buildAdminPermissionRules = () => { ProjectPermissionCertificateProfileActions.Edit, ProjectPermissionCertificateProfileActions.Create, ProjectPermissionCertificateProfileActions.Delete, - ProjectPermissionCertificateProfileActions.IssueCert + ProjectPermissionCertificateProfileActions.IssueCert, + ProjectPermissionCertificateProfileActions.RevealAcmeEabSecret, + ProjectPermissionCertificateProfileActions.RotateAcmeEabSecret ], ProjectPermissionSub.CertificateProfiles ); diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index bb62440c1..74e4554ed 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -116,7 +116,9 @@ export enum ProjectPermissionCertificateProfileActions { Create = "create", Edit = "edit", Delete = "delete", - IssueCert = "issue-cert" + IssueCert = "issue-cert", + RevealAcmeEabSecret = "reveal-acme-eab-secret", + RotateAcmeEabSecret = "rotate-acme-eab-secret" } export enum ProjectPermissionSecretSyncActions { diff --git a/backend/src/server/routes/v1/certificate-profiles-router.ts b/backend/src/server/routes/v1/certificate-profiles-router.ts index 7ad7aaeb1..1a670c254 100644 --- a/backend/src/server/routes/v1/certificate-profiles-router.ts +++ b/backend/src/server/routes/v1/certificate-profiles-router.ts @@ -7,8 +7,8 @@ import { ApiDocsTags } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; -import { CertStatus } from "@app/services/certificate/certificate-types"; import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types"; +import { CertStatus } from "@app/services/certificate/certificate-types"; export const registerCertificateProfilesRouter = async (server: FastifyZodProvider) => { server.route({ @@ -491,4 +491,35 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid return { certificates }; } }); + + server.route({ + method: "GET", + url: "/:id/acme/eab-secret/reveal", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateProfiles], + params: z.object({ + id: z.string().uuid() + }), + response: { + 200: z.object({ + eabSecret: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const eabSecret = await server.services.certificateProfile.revealAcmeEabSecret({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + profileId: req.params.id + }); + return { eabSecret }; + } + }); }; diff --git a/backend/src/services/certificate-profile/certificate-profile-service.ts b/backend/src/services/certificate-profile/certificate-profile-service.ts index 527ed3c96..f896bb071 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.ts @@ -782,6 +782,59 @@ export const certificateProfileServiceFactory = ({ }; }; + const revealAcmeEabSecret = async ({ + actor, + actorId, + actorAuthMethod, + actorOrgId, + profileId + }: { + actor: ActorType; + actorId: string; + actorAuthMethod: ActorAuthMethod; + actorOrgId: string; + profileId: string; + }) => { + const profile = await certificateProfileDAL.findByIdWithConfigs(profileId); + if (!profile) { + throw new NotFoundError({ message: "Certificate profile not found" }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: profile.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateProfileActions.RevealAcmeEabSecret, + ProjectPermissionSub.CertificateProfiles + ); + + if (profile.enrollmentType !== EnrollmentType.ACME) { + throw new ForbiddenRequestError({ + message: "Profile is not configured for ACME enrollment" + }); + } + if (!profile.acmeConfig) { + throw new NotFoundError({ message: "ACME configuration not found for this profile" }); + } + + const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ + projectId: profile.projectId, + projectDAL, + kmsService + }); + + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); + const eabSecret = await kmsDecryptor({ cipherTextBlob: profile.acmeConfig.encryptedEabSecret }); + return eabSecret.toString(); + }; + return { createProfile, updateProfile, @@ -791,6 +844,7 @@ export const certificateProfileServiceFactory = ({ listProfiles, deleteProfile, getProfileCertificates, - getEstConfigurationByProfile + getEstConfigurationByProfile, + revealAcmeEabSecret }; }; diff --git a/backend/src/services/certificate-profile/certificate-profile-types.ts b/backend/src/services/certificate-profile/certificate-profile-types.ts index 6e1d64fb5..8a89b4d0d 100644 --- a/backend/src/services/certificate-profile/certificate-profile-types.ts +++ b/backend/src/services/certificate-profile/certificate-profile-types.ts @@ -58,6 +58,7 @@ export type TCertificateProfileWithConfigs = TCertificateProfile & { }; acmeConfig?: { id: string; + encryptedEabSecret: Buffer; }; metrics?: TCertificateProfileMetrics; }; From 9ed4600d1d45debdf5b05f3c1c74faada29a38df Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 14:35:47 -0800 Subject: [PATCH 214/294] More test cases --- .../features/pki/acme/cert-profile.feature | 21 +++++++++++++++++ backend/bdd/features/steps/pki_acme.py | 23 +++++++++++++++++-- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/backend/bdd/features/pki/acme/cert-profile.feature b/backend/bdd/features/pki/acme/cert-profile.feature index f25d0eaf2..c14a7a3ad 100644 --- a/backend/bdd/features/pki/acme/cert-profile.feature +++ b/backend/bdd/features/pki/acme/cert-profile.feature @@ -16,7 +16,28 @@ Feature: ACME Cert Profile } """ Then the value response.status_code should be equal to 200 + Then the value response with jq .certificateProfile.id should be present Then the value response with jq .certificateProfile.slug should be equal to "{profile_slug}" Then the value response with jq .certificateProfile.caId should be equal to "{CERT_CA_ID}" Then the value response with jq .certificateProfile.certificateTemplateId should be equal to "{CERT_TEMPLATE_ID}" Then the value response with jq .certificateProfile.enrollmentType should be equal to "acme" + + Scenario: Reveal EAB secret + Given I make a random slug as profile_slug + Given I use AUTH_TOKEN for authentication + When I send a POST request to "/api/v1/pki/certificate-profiles" with JSON payload + """ + { + "projectId": "{PROJECT_ID}", + "slug": "{profile_slug}", + "description": "", + "enrollmentType": "acme", + "caId": "{CERT_CA_ID}", + "certificateTemplateId": "{CERT_TEMPLATE_ID}", + "acmeConfig": {} + } + """ + Then the value response.status_code should be equal to 200 + And I memorize response with jq ".certificateProfile.id" as profile_id + When I send a GET request to "/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal" + Then the value response.status_code should be equal to 200 diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index f2008e867..4c33b42b1 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -145,9 +145,13 @@ def step_impl(context: Context, token_var: str): @when('I send a {method} request to "{url}"') def step_impl(context: Context, method: str, url: str): - context.response = context.http_client.request( + logger.debug("Sending %s request to %s", method, url) + response = context.http_client.request( method, url.format(**context.vars), headers=prepare_headers(context) ) + context.vars["response"] = response + logger.debug("Response status: %r", response.status_code) + logger.debug("Response JSON payload: %r", response.json()) @when('I send a {method} request to "{url}" with JSON payload') @@ -166,7 +170,6 @@ def step_impl(context: Context, method: str, url: str): headers=prepare_headers(context), json=json_payload, ) - context.response = response context.vars["response"] = response logger.debug("Response status: %r", response.status_code) logger.debug("Response JSON payload: %r", response.json()) @@ -362,6 +365,22 @@ def step_impl(context: Context, var_path: str, expected: str): assert value == expected_value, f"{value!r} does not match {expected_value!r}" +@then('I memorize {var_path} with jq "{jq_query}" as {var_name}') +def step_impl(context: Context, var_path: str, jq_query, var_name: str): + _, value = apply_value_with_jq( + context=context, + var_path=var_path, + jq_query=jq_query, + ) + context.vars[var_name] = value + + +@then("I memorize {var_path} as {var_name}") +def step_impl(context: Context, var_path: str, var_name: str): + value = eval_var(context, var_path) + context.vars[var_name] = value + + @then("I print the value {var_path}") def step_impl(context: Context, var_path: str): value = eval_var(context, var_path) From 49ad8ab630667d50ad351d1d353d928d91e4566b Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 14:45:24 -0800 Subject: [PATCH 215/294] Return eab kid as well --- .../src/server/routes/v1/certificate-profiles-router.ts | 5 +++-- .../certificate-profile/certificate-profile-dal.ts | 8 ++++++-- .../certificate-profile/certificate-profile-service.ts | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/backend/src/server/routes/v1/certificate-profiles-router.ts b/backend/src/server/routes/v1/certificate-profiles-router.ts index 1a670c254..f85f8a677 100644 --- a/backend/src/server/routes/v1/certificate-profiles-router.ts +++ b/backend/src/server/routes/v1/certificate-profiles-router.ts @@ -506,20 +506,21 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid }), response: { 200: z.object({ + eabKid: z.string(), eabSecret: z.string() }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const eabSecret = await server.services.certificateProfile.revealAcmeEabSecret({ + const { eabKid, eabSecret } = await server.services.certificateProfile.revealAcmeEabSecret({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, profileId: req.params.id }); - return { eabSecret }; + return { eabKid, eabSecret }; } }); }; diff --git a/backend/src/services/certificate-profile/certificate-profile-dal.ts b/backend/src/services/certificate-profile/certificate-profile-dal.ts index beea084e3..f8f511ab4 100644 --- a/backend/src/services/certificate-profile/certificate-profile-dal.ts +++ b/backend/src/services/certificate-profile/certificate-profile-dal.ts @@ -130,7 +130,8 @@ export const certificateProfileDALFactory = (db: TDbClient) => { db.ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigId"), db.ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigAutoRenew"), db.ref("renewBeforeDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigRenewBeforeDays"), - db.ref("id").withSchema(TableName.PkiAcmeEnrollmentConfig).as("acmeConfigId") + db.ref("id").withSchema(TableName.PkiAcmeEnrollmentConfig).as("acmeConfigId"), + db.ref("encryptedEabSecret").withSchema(TableName.PkiAcmeEnrollmentConfig).as("acmeConfigEncryptedEabSecret") ) .where(`${TableName.PkiCertificateProfile}.id`, id) .first(); @@ -158,7 +159,10 @@ export const certificateProfileDALFactory = (db: TDbClient) => { : undefined; const acmeConfig = result.acmeConfigId - ? ({ id: result.acmeConfigId } as TCertificateProfileWithConfigs["acmeConfig"]) + ? ({ + id: result.acmeConfigId, + encryptedEabSecret: result.acmeConfigEncryptedEabSecret + } as TCertificateProfileWithConfigs["acmeConfig"]) : undefined; const certificateAuthority = diff --git a/backend/src/services/certificate-profile/certificate-profile-service.ts b/backend/src/services/certificate-profile/certificate-profile-service.ts index f896bb071..2e92c4b02 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.ts @@ -832,7 +832,7 @@ export const certificateProfileServiceFactory = ({ kmsId: certificateManagerKmsId }); const eabSecret = await kmsDecryptor({ cipherTextBlob: profile.acmeConfig.encryptedEabSecret }); - return eabSecret.toString(); + return { eabKid: profile.id, eabSecret: eabSecret.toString() }; }; return { From 4f5e1760d47e6d0b01e1351f9bdbc455ab3397dc Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 14:48:35 -0800 Subject: [PATCH 216/294] Assert eab --- backend/bdd/features/pki/acme/cert-profile.feature | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/bdd/features/pki/acme/cert-profile.feature b/backend/bdd/features/pki/acme/cert-profile.feature index c14a7a3ad..042f86e39 100644 --- a/backend/bdd/features/pki/acme/cert-profile.feature +++ b/backend/bdd/features/pki/acme/cert-profile.feature @@ -41,3 +41,5 @@ Feature: ACME Cert Profile And I memorize response with jq ".certificateProfile.id" as profile_id When I send a GET request to "/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal" Then the value response.status_code should be equal to 200 + Then the value response with jq .eabKid should be equal to "{profile_id}" + Then the value response with jq .eabSecret should be present From 5b2c0583e080850c84765f71d128ce1549a12498 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 14:56:30 -0800 Subject: [PATCH 217/294] Add quote for jq --- backend/bdd/features/pki/acme/auth.feature | 8 ++++---- backend/bdd/features/pki/acme/cert-profile.feature | 14 +++++++------- backend/bdd/features/pki/acme/order.feature | 10 +++++----- backend/bdd/features/steps/pki_acme.py | 6 +++--- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/backend/bdd/features/pki/acme/auth.feature b/backend/bdd/features/pki/acme/auth.feature index b1b8ee5ae..dd824d18f 100644 --- a/backend/bdd/features/pki/acme/auth.feature +++ b/backend/bdd/features/pki/acme/auth.feature @@ -17,8 +17,8 @@ Feature: Authorization Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order Then the value order.authorizations[0].uri with jq . should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/(.+) - Then the value order.authorizations[0].body with jq .status should be equal to "pending" - Then the value order.authorizations[0].body with jq .challenges | map(pick(.type, .status)) | sort_by(.type) should be equal to json + Then the value order.authorizations[0].body with jq ".status" should be equal to "pending" + Then the value order.authorizations[0].body with jq ".challenges | map(pick(.type, .status)) | sort_by(.type)" should be equal to json """ [ { @@ -27,8 +27,8 @@ Feature: Authorization } ] """ - Then the value order.authorizations[0].body with jq .challenges | map(.status) | sort should be equal to ["pending"] - Then the value order.authorizations[0].body with jq .identifier should be equal to json + Then the value order.authorizations[0].body with jq ".challenges | map(.status) | sort" should be equal to ["pending"] + Then the value order.authorizations[0].body with jq ".identifier" should be equal to json """ { "type": "dns", diff --git a/backend/bdd/features/pki/acme/cert-profile.feature b/backend/bdd/features/pki/acme/cert-profile.feature index 042f86e39..564b06013 100644 --- a/backend/bdd/features/pki/acme/cert-profile.feature +++ b/backend/bdd/features/pki/acme/cert-profile.feature @@ -16,11 +16,11 @@ Feature: ACME Cert Profile } """ Then the value response.status_code should be equal to 200 - Then the value response with jq .certificateProfile.id should be present - Then the value response with jq .certificateProfile.slug should be equal to "{profile_slug}" - Then the value response with jq .certificateProfile.caId should be equal to "{CERT_CA_ID}" - Then the value response with jq .certificateProfile.certificateTemplateId should be equal to "{CERT_TEMPLATE_ID}" - Then the value response with jq .certificateProfile.enrollmentType should be equal to "acme" + Then the value response with jq ".certificateProfile.id" should be present + Then the value response with jq ".certificateProfile.slug" should be equal to "{profile_slug}" + Then the value response with jq ".certificateProfile.caId" should be equal to "{CERT_CA_ID}" + Then the value response with jq ".certificateProfile.certificateTemplateId" should be equal to "{CERT_TEMPLATE_ID}" + Then the value response with jq ".certificateProfile.enrollmentType" should be equal to "acme" Scenario: Reveal EAB secret Given I make a random slug as profile_slug @@ -41,5 +41,5 @@ Feature: ACME Cert Profile And I memorize response with jq ".certificateProfile.id" as profile_id When I send a GET request to "/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal" Then the value response.status_code should be equal to 200 - Then the value response with jq .eabKid should be equal to "{profile_id}" - Then the value response with jq .eabSecret should be present + Then the value response with jq ".eabKid" should be equal to "{profile_id}" + Then the value response with jq ".eabSecret" should be present diff --git a/backend/bdd/features/pki/acme/order.feature b/backend/bdd/features/pki/acme/order.feature index d9cf5c268..1ba2c3570 100644 --- a/backend/bdd/features/pki/acme/order.feature +++ b/backend/bdd/features/pki/acme/order.feature @@ -45,7 +45,7 @@ Feature: Order Then I create a RSA private key pair as cert_key Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order - Then the value order.body with jq .identifiers | sort_by(.value) should be equal to json + Then the value order.body with jq ".identifiers | sort_by(.value)" should be equal to json """ [ {"type": "dns", "value": "example.com"}, @@ -71,7 +71,7 @@ Feature: Order Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order Then I send an ACME post-as-get to order.uri as fetched_order - Then the value fetched_order with jq .status should be equal to "pending" - Then the value fetched_order with jq .identifiers should be equal to [{"type": "dns", "value": "localhost"}] - Then the value fetched_order with jq .finalize should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize - Then the value fetched_order with jq all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/")) should be equal to true + Then the value fetched_order with jq ".status" should be equal to "pending" + Then the value fetched_order with jq ".identifiers" should be equal to [{"type": "dns", "value": "localhost"}] + Then the value fetched_order with jq ".finalize" should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize + Then the value fetched_order with jq "all(.authorizations[]; startswith('{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/'))" should be equal to true diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 4c33b42b1..855195ac5 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -314,7 +314,7 @@ def step_impl(context: Context, var_path: str, jq_query: str): ) -@then("the value {var_path} with jq {jq_query} should be present") +@then('the value {var_path} with jq "{jq_query}" should be present') def step_impl(context: Context, var_path: str, jq_query: str): value, result = apply_value_with_jq( context=context, @@ -326,7 +326,7 @@ def step_impl(context: Context, var_path: str, jq_query: str): ) -@then("the value {var_path} with jq {jq_query} should be equal to {expected}") +@then('the value {var_path} with jq "{jq_query}" should be equal to {expected}') def step_impl(context: Context, var_path: str, jq_query: str, expected: str): value, result = apply_value_with_jq( context=context, @@ -339,7 +339,7 @@ def step_impl(context: Context, var_path: str, jq_query: str, expected: str): ) -@then("the value {var_path} with jq {jq_query} should match pattern {regex}") +@then('the value {var_path} with jq "{jq_query}" should match pattern {regex}') def step_impl(context: Context, var_path: str, jq_query: str, regex: str): value, result = apply_value_with_jq( context=context, From e595e947e28df0f5bdf6f2b46242992d42929ada Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 14:58:26 -0800 Subject: [PATCH 218/294] Use eab to sign up --- backend/bdd/features/pki/acme/cert-profile.feature | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/bdd/features/pki/acme/cert-profile.feature b/backend/bdd/features/pki/acme/cert-profile.feature index 564b06013..e090d0581 100644 --- a/backend/bdd/features/pki/acme/cert-profile.feature +++ b/backend/bdd/features/pki/acme/cert-profile.feature @@ -43,3 +43,7 @@ Feature: ACME Cert Profile Then the value response.status_code should be equal to 200 Then the value response with jq ".eabKid" should be equal to "{profile_id}" Then the value response with jq ".eabSecret" should be present + And I memorize response with jq ".eabKid" as eab_kid + And I memorize response with jq ".eabSecret" as eab_secret + When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + Then I register a new ACME account with email fangpen@infisical.com and EAB key id {eab_kid} with secret {eab_secret} as acme_account From a5363cb7ea40773f6908d5478d454cb09dbb22c4 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 15:26:31 -0800 Subject: [PATCH 219/294] Implement EAB verification --- backend/bdd/features/steps/pki_acme.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 855195ac5..c78c45ede 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -220,9 +220,20 @@ def step_impl(context: Context): "I register a new ACME account with email {email} and EAB key id {kid} with secret {secret} as {account_var}" ) def step_impl(context: Context, email: str, kid: str, secret: str, account_var: str): - # TODO: add EAB info here - registration = messages.NewRegistration.from_data(email=email) - context.vars[account_var] = context.acme_client.new_account(registration) + acme_client = context.acme_client + account_public_key = acme_client.net.key.public_key() + eab = messages.ExternalAccountBinding.from_data( + account_public_key=account_public_key, + kid=kid, + hmac_key=secret, + directory=acme_client.directory, + hmac_alg="HS256", + ) + registration = messages.NewRegistration.from_data( + email=email, + external_account_binding=eab, + ) + context.vars[account_var] = acme_client.new_account(registration) @then( From fe55483eeb877928f8b9998a50f35208041955d5 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 15:29:38 -0800 Subject: [PATCH 220/294] Implement eab verification --- .../ee/services/pki-acme/pki-acme-errors.ts | 21 +++++++ .../ee/services/pki-acme/pki-acme-schemas.ts | 8 +-- .../ee/services/pki-acme/pki-acme-service.ts | 58 +++++++++++++++++-- 3 files changed, 74 insertions(+), 13 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-errors.ts b/backend/src/ee/services/pki-acme/pki-acme-errors.ts index 73f17e061..329cb1989 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-errors.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-errors.ts @@ -549,3 +549,24 @@ export class AcmeBadCSRError extends AcmeError { this.name = "AcmeBadCSRError"; } } + +export class AcmeExternalAccountRequiredError extends AcmeError { + constructor({ + detail = "External account binding is required", + error, + message + }: { + detail?: string; + error?: unknown; + message?: string; + } = {}) { + super({ + type: AcmeErrorType.ExternalAccountRequired, + detail, + status: 400, + error, + message + }); + this.name = "AcmeExternalAccountRequiredError"; + } +} diff --git a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts index 7298dd944..1d2f95fdf 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts @@ -66,13 +66,7 @@ export const CreateAcmeAccountBodySchema = z.object({ contact: z.array(z.string()).optional(), termsOfServiceAgreed: z.boolean().optional(), onlyReturnExisting: z.boolean().optional(), - externalAccountBinding: z - .object({ - protected: z.string(), - payload: z.string(), - signature: z.string() - }) - .optional() + externalAccountBinding: RawJwsPayloadSchema.optional() }); // New Account endpoint diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index a478a1721..16d917fae 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -2,7 +2,7 @@ import { TPkiAcmeAccounts } from "@app/db/schemas/pki-acme-accounts"; import { TPkiAcmeAuths } from "@app/db/schemas/pki-acme-auths"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; -import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; @@ -12,6 +12,9 @@ import { TCertificateProfileWithConfigs } from "@app/services/certificate-profile/certificate-profile-types"; import { TCertificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { calculateJwkThumbprint, errors, @@ -29,6 +32,7 @@ import { AcmeBadCSRError, AcmeBadPublicKeyError, AcmeError, + AcmeExternalAccountRequiredError, AcmeMalformedError, AcmeOrderNotReadyError, AcmeServerInternalError, @@ -67,8 +71,8 @@ import { } from "./pki-acme-types"; type TPkiAcmeServiceFactoryDep = { - certificateProfileDAL: Pick; - certificateV3Service: Pick; + projectDAL: Pick; + certificateProfileDAL: Pick; acmeAccountDAL: Pick< TPkiAcmeAccountDALFactory, "findByProjectIdAndAccountId" | "findByProfileIdAndPublicKeyThumbprintAndAlg" | "create" @@ -83,21 +87,25 @@ type TPkiAcmeServiceFactoryDep = { TPkiAcmeChallengeDALFactory, "create" | "transaction" | "updateById" | "findByAccountAuthAndChallengeId" | "findByIdForChallengeValidation" >; + kmsService: Pick; + certificateV3Service: Pick; acmeChallengeService: TPkiAcmeChallengeServiceFactory; }; export const pkiAcmeServiceFactory = ({ + projectDAL, certificateProfileDAL, - certificateV3Service, acmeAccountDAL, acmeOrderDAL, acmeAuthDAL, acmeOrderAuthDAL, acmeChallengeDAL, + kmsService, + certificateV3Service, acmeChallengeService }: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => { const validateAcmeProfile = async (profileId: string): Promise => { - const profile = await certificateProfileDAL.findById(profileId); + const profile = await certificateProfileDAL.findByIdWithConfigs(profileId); if (!profile) { throw new NotFoundError({ message: "Certificate profile not found" }); } @@ -304,7 +312,7 @@ export const pkiAcmeServiceFactory = ({ profileId, alg, jwk, - payload: { onlyReturnExisting, contact } + payload: { onlyReturnExisting, contact, externalAccountBinding } }: { profileId: string; alg: string; @@ -312,6 +320,44 @@ export const pkiAcmeServiceFactory = ({ payload: TCreateAcmeAccountPayload; }): Promise> => { const profile = await validateAcmeProfile(profileId); + if (!externalAccountBinding) { + throw new AcmeExternalAccountRequiredError({ detail: "External account binding is required" }); + } + + const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ + projectId: profile.projectId, + projectDAL, + kmsService + }); + + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); + const eabSecret = await kmsDecryptor({ cipherTextBlob: profile.acmeConfig!.encryptedEabSecret }); + const encodedSecret = new TextEncoder().encode(eabSecret.toString()); + try { + const { payload: eabPayload, protectedHeader: eabProtectedHeader } = await flattenedVerify( + externalAccountBinding, + encodedSecret + ); + const alg = eabProtectedHeader!.alg!; + if (!["HS256", "HS384", "HS512"].includes(alg)) { + throw new AcmeMalformedError({ detail: "Invalid algorithm for external account binding JWS payload" }); + } + if ((eabPayload as unknown as { kid: string }).kid !== profile.id) { + throw new UnauthorizedError({ message: "External account binding KID mismatch" }); + } + } catch (error) { + if (error instanceof errors.JWSInvalid) { + throw new AcmeMalformedError({ detail: "Invalid external account binding JWS payload" }); + } + if (error instanceof AcmeError) { + throw error; + } + logger.error(error, "Unexpected error while verifying EAB JWS payload"); + throw new AcmeServerInternalError({ detail: "Failed to verify EAB JWS payload" }); + } + const publicKeyThumbprint = await calculateJwkThumbprint(jwk, "sha256"); const existingAccount: TPkiAcmeAccounts | null = await acmeAccountDAL.findByProfileIdAndPublicKeyThumbprintAndAlg( profileId, From a8cd3619bd8beb81cb169969e2248adf1b4017aa Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 15:34:09 -0800 Subject: [PATCH 221/294] Add quote --- backend/bdd/features/pki/acme/account.feature | 2 +- backend/bdd/features/pki/acme/auth.feature | 4 ++-- .../bdd/features/pki/acme/cert-profile.feature | 4 ++-- backend/bdd/features/pki/acme/challenge.feature | 2 +- backend/bdd/features/pki/acme/order.feature | 16 ++++++++-------- backend/bdd/features/steps/pki_acme.py | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/backend/bdd/features/pki/acme/account.feature b/backend/bdd/features/pki/acme/account.feature index 1a291bfdb..463e6b04f 100644 --- a/backend/bdd/features/pki/acme/account.feature +++ b/backend/bdd/features/pki/acme/account.feature @@ -2,4 +2,4 @@ Feature: Account Scenario: Create a new account Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory - Then I register a new ACME account with email fangpen@infisical.com and EAB key id {acme_profile.eab_kid} with secret {acme_profile.eab_secret} as acme_account + Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account diff --git a/backend/bdd/features/pki/acme/auth.feature b/backend/bdd/features/pki/acme/auth.feature index dd824d18f..b70599b66 100644 --- a/backend/bdd/features/pki/acme/auth.feature +++ b/backend/bdd/features/pki/acme/auth.feature @@ -4,7 +4,7 @@ Feature: Authorization Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory # # TODO: make it I have an account already instead? - Then I register a new ACME account with email fangpen@infisical.com and EAB key id {acme_profile.eab_kid} with secret {acme_profile.eab_secret} as acme_account + Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account When I create certificate signing request as csr Then I add names to certificate signing request csr """ @@ -16,7 +16,7 @@ Feature: Authorization Then I create a RSA private key pair as cert_key Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order - Then the value order.authorizations[0].uri with jq . should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/(.+) + Then the value order.authorizations[0].uri with jq "." should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/(.+) Then the value order.authorizations[0].body with jq ".status" should be equal to "pending" Then the value order.authorizations[0].body with jq ".challenges | map(pick(.type, .status)) | sort_by(.type)" should be equal to json """ diff --git a/backend/bdd/features/pki/acme/cert-profile.feature b/backend/bdd/features/pki/acme/cert-profile.feature index e090d0581..7ce1ecc8c 100644 --- a/backend/bdd/features/pki/acme/cert-profile.feature +++ b/backend/bdd/features/pki/acme/cert-profile.feature @@ -45,5 +45,5 @@ Feature: ACME Cert Profile Then the value response with jq ".eabSecret" should be present And I memorize response with jq ".eabKid" as eab_kid And I memorize response with jq ".eabSecret" as eab_secret - When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory - Then I register a new ACME account with email fangpen@infisical.com and EAB key id {eab_kid} with secret {eab_secret} as acme_account + When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{profile_id}/directory + Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{eab_kid}" with secret "{eab_secret}" as acme_account diff --git a/backend/bdd/features/pki/acme/challenge.feature b/backend/bdd/features/pki/acme/challenge.feature index 960842ebf..a5d94aff5 100644 --- a/backend/bdd/features/pki/acme/challenge.feature +++ b/backend/bdd/features/pki/acme/challenge.feature @@ -4,7 +4,7 @@ Feature: Challenge Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory # # TODO: make it I have an account already instead? - Then I register a new ACME account with email fangpen@infisical.com and EAB key id {acme_profile.eab_kid} with secret {acme_profile.eab_secret} as acme_account + Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account When I create certificate signing request as csr Then I add names to certificate signing request csr """ diff --git a/backend/bdd/features/pki/acme/order.feature b/backend/bdd/features/pki/acme/order.feature index 1ba2c3570..49f72e485 100644 --- a/backend/bdd/features/pki/acme/order.feature +++ b/backend/bdd/features/pki/acme/order.feature @@ -4,7 +4,7 @@ Feature: Order Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory # # TODO: make it I have an account already instead? - Then I register a new ACME account with email fangpen@infisical.com and EAB key id {acme_profile.eab_kid} with secret {acme_profile.eab_secret} as acme_account + Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account When I create certificate signing request as csr Then I add names to certificate signing request csr """ @@ -16,17 +16,17 @@ Feature: Order Then I create a RSA private key pair as cert_key Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order - Then the value order.uri with jq . should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+) - Then the value order.body with jq .status should be equal to "pending" - Then the value order.body with jq .identifiers should be equal to [{"type": "dns", "value": "localhost"}] - Then the value order.body with jq .finalize should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize - Then the value order.body with jq all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/")) should be equal to true + Then the value order.uri with jq "." should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+) + Then the value order.body with jq ".status" should be equal to "pending" + Then the value order.body with jq ".identifiers" should be equal to [{"type": "dns", "value": "localhost"}] + Then the value order.body with jq ".finalize" should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize + Then the value order.body with jq "all(.authorizations[]; startswith('{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/'))" should be equal to true Scenario: Create a new order with SANs Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory # # TODO: make it I have an account already instead? - Then I register a new ACME account with email fangpen@infisical.com and EAB key id {acme_profile.eab_kid} with secret {acme_profile.eab_secret} as acme_account + Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account When I create certificate signing request as csr Then I add names to certificate signing request csr """ @@ -58,7 +58,7 @@ Feature: Order Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory # # TODO: make it I have an account already instead? - Then I register a new ACME account with email fangpen@infisical.com and EAB key id {acme_profile.eab_kid} with secret {acme_profile.eab_secret} as acme_account + Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account When I create certificate signing request as csr Then I add names to certificate signing request csr """ diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index c78c45ede..167eab20d 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -217,7 +217,7 @@ def step_impl(context: Context): @then( - "I register a new ACME account with email {email} and EAB key id {kid} with secret {secret} as {account_var}" + 'I register a new ACME account with email {email} and EAB key id "{kid}" with secret "{secret}" as {account_var}' ) def step_impl(context: Context, email: str, kid: str, secret: str, account_var: str): acme_client = context.acme_client From ce3d70c30a459ee3395216ad02a98ea36ed10940 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 15:36:04 -0800 Subject: [PATCH 222/294] Replace secret and kid --- backend/bdd/features/steps/pki_acme.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 167eab20d..e005803da 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -224,8 +224,8 @@ def step_impl(context: Context, email: str, kid: str, secret: str, account_var: account_public_key = acme_client.net.key.public_key() eab = messages.ExternalAccountBinding.from_data( account_public_key=account_public_key, - kid=kid, - hmac_key=secret, + kid=replace_vars(kid, context.vars), + hmac_key=replace_vars(secret, context.vars), directory=acme_client.directory, hmac_alg="HS256", ) From e7f098e63ebc61ae44cf2802e5f76737e839c5f7 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 16:07:19 -0800 Subject: [PATCH 223/294] Implement more checks --- .../ee/services/pki-acme/pki-acme-service.ts | 30 +++++++++++++++---- .../certificate-profile-service.ts | 2 +- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 16d917fae..930e22872 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -324,26 +324,45 @@ export const pkiAcmeServiceFactory = ({ throw new AcmeExternalAccountRequiredError({ detail: "External account binding is required" }); } + const publicKeyThumbprint = await calculateJwkThumbprint(jwk, "sha256"); const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ projectId: profile.projectId, projectDAL, kmsService }); - const kmsDecryptor = await kmsService.decryptWithKmsKey({ kmsId: certificateManagerKmsId }); const eabSecret = await kmsDecryptor({ cipherTextBlob: profile.acmeConfig!.encryptedEabSecret }); - const encodedSecret = new TextEncoder().encode(eabSecret.toString()); try { const { payload: eabPayload, protectedHeader: eabProtectedHeader } = await flattenedVerify( externalAccountBinding, - encodedSecret + eabSecret ); - const alg = eabProtectedHeader!.alg!; - if (!["HS256", "HS384", "HS512"].includes(alg)) { + const eabAlg = eabProtectedHeader!.alg!; + if (!["HS256", "HS384", "HS512"].includes(eabAlg)) { throw new AcmeMalformedError({ detail: "Invalid algorithm for external account binding JWS payload" }); } + // Make sure the URL matches the expected URL + const url = eabProtectedHeader!.url!; + if (url !== buildUrl(profile.id, "/new-account")) { + throw new UnauthorizedError({ message: "External account binding URL mismatch" }); + } + + // Make sure the JWK in the EAB payload matches the one provided in the outer JWS payload + const decoder = new TextDecoder(); + const decodedEabPayload = decoder.decode(eabPayload); + const eabPayloadJson = JSON.parse(decodedEabPayload); + const eabPayloadJwkThumbprint = await calculateJwkThumbprint( + eabPayloadJson.jwk as JsonWebKey, + alg as "sha256" | "sha384" | "sha512" + ); + if (eabPayloadJwkThumbprint !== publicKeyThumbprint || eabAlg !== alg) { + throw new AcmeBadPublicKeyError({ + message: "External account binding public key thumbprint or algorithm mismatch" + }); + } + // Make sure the KID in the EAB payload matches the profile ID if ((eabPayload as unknown as { kid: string }).kid !== profile.id) { throw new UnauthorizedError({ message: "External account binding KID mismatch" }); } @@ -358,7 +377,6 @@ export const pkiAcmeServiceFactory = ({ throw new AcmeServerInternalError({ detail: "Failed to verify EAB JWS payload" }); } - const publicKeyThumbprint = await calculateJwkThumbprint(jwk, "sha256"); const existingAccount: TPkiAcmeAccounts | null = await acmeAccountDAL.findByProfileIdAndPublicKeyThumbprintAndAlg( profileId, alg, diff --git a/backend/src/services/certificate-profile/certificate-profile-service.ts b/backend/src/services/certificate-profile/certificate-profile-service.ts index 2e92c4b02..be7a0a253 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.ts @@ -832,7 +832,7 @@ export const certificateProfileServiceFactory = ({ kmsId: certificateManagerKmsId }); const eabSecret = await kmsDecryptor({ cipherTextBlob: profile.acmeConfig.encryptedEabSecret }); - return { eabKid: profile.id, eabSecret: eabSecret.toString() }; + return { eabKid: profile.id, eabSecret: eabSecret.toString("base64url") }; }; return { From 72717cdc584aa4224a025ddc66b420db0858c5c1 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 16:10:04 -0800 Subject: [PATCH 224/294] Fix EAB cal --- backend/src/ee/services/pki-acme/pki-acme-service.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 930e22872..2dee5e7f1 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -352,11 +352,8 @@ export const pkiAcmeServiceFactory = ({ // Make sure the JWK in the EAB payload matches the one provided in the outer JWS payload const decoder = new TextDecoder(); const decodedEabPayload = decoder.decode(eabPayload); - const eabPayloadJson = JSON.parse(decodedEabPayload); - const eabPayloadJwkThumbprint = await calculateJwkThumbprint( - eabPayloadJson.jwk as JsonWebKey, - alg as "sha256" | "sha384" | "sha512" - ); + const eabJWK = JSON.parse(decodedEabPayload); + const eabPayloadJwkThumbprint = await calculateJwkThumbprint(eabJWK, "sha256"); if (eabPayloadJwkThumbprint !== publicKeyThumbprint || eabAlg !== alg) { throw new AcmeBadPublicKeyError({ message: "External account binding public key thumbprint or algorithm mismatch" From 4d9833acd6a0162b5ee983be1b5c8faa7b294a4f Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 16:18:49 -0800 Subject: [PATCH 225/294] Not same type of alg --- backend/src/ee/services/pki-acme/pki-acme-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 2dee5e7f1..498537c82 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -354,7 +354,7 @@ export const pkiAcmeServiceFactory = ({ const decodedEabPayload = decoder.decode(eabPayload); const eabJWK = JSON.parse(decodedEabPayload); const eabPayloadJwkThumbprint = await calculateJwkThumbprint(eabJWK, "sha256"); - if (eabPayloadJwkThumbprint !== publicKeyThumbprint || eabAlg !== alg) { + if (eabPayloadJwkThumbprint !== publicKeyThumbprint) { throw new AcmeBadPublicKeyError({ message: "External account binding public key thumbprint or algorithm mismatch" }); From 5256b11402cd74dc74b101c3d1a8d67200dbd5a9 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 16:21:21 -0800 Subject: [PATCH 226/294] Fix KID match --- .../src/ee/services/pki-acme/pki-acme-service.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 498537c82..cc3df7ce7 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -339,10 +339,15 @@ export const pkiAcmeServiceFactory = ({ externalAccountBinding, eabSecret ); - const eabAlg = eabProtectedHeader!.alg!; - if (!["HS256", "HS384", "HS512"].includes(eabAlg)) { + const { alg: eabAlg, kid: eabKid } = eabProtectedHeader!; + if (!["HS256", "HS384", "HS512"].includes(eabAlg!)) { throw new AcmeMalformedError({ detail: "Invalid algorithm for external account binding JWS payload" }); } + // Make sure the KID in the EAB payload matches the profile ID + if (eabKid !== profile.id) { + throw new UnauthorizedError({ message: "External account binding KID mismatch" }); + } + // Make sure the URL matches the expected URL const url = eabProtectedHeader!.url!; if (url !== buildUrl(profile.id, "/new-account")) { @@ -359,10 +364,6 @@ export const pkiAcmeServiceFactory = ({ message: "External account binding public key thumbprint or algorithm mismatch" }); } - // Make sure the KID in the EAB payload matches the profile ID - if ((eabPayload as unknown as { kid: string }).kid !== profile.id) { - throw new UnauthorizedError({ message: "External account binding KID mismatch" }); - } } catch (error) { if (error instanceof errors.JWSInvalid) { throw new AcmeMalformedError({ detail: "Invalid external account binding JWS payload" }); From 393fa0157094380726815a01ee3cd8642c4865f8 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 16:23:12 -0800 Subject: [PATCH 227/294] Add deps --- backend/src/server/routes/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 13a5aee2c..9af799ba6 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -2199,13 +2199,15 @@ export const registerRoutes = async ( acmeChallengeDAL }); const pkiAcmeService = pkiAcmeServiceFactory({ - certificateV3Service, + projectDAL, certificateProfileDAL, acmeAccountDAL, acmeOrderDAL, acmeAuthDAL, acmeOrderAuthDAL, acmeChallengeDAL, + kmsService, + certificateV3Service, acmeChallengeService }); From 59ce408ec13c81e7e286a7cd324a1ea4b6bbeaea Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 16:30:56 -0800 Subject: [PATCH 228/294] Reveal --- .../context/ProjectPermissionContext/types.ts | 3 ++- .../CertificateProfilesTab.tsx | 13 ++++++++- .../CertificateProfilesTab/ProfileList.tsx | 8 +++++- .../CertificateProfilesTab/ProfileRow.tsx | 27 +++++++++++++++++-- 4 files changed, 46 insertions(+), 5 deletions(-) diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 0236113eb..b6fd85f44 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -129,7 +129,8 @@ export enum ProjectPermissionCertificateProfileActions { Create = "create", Edit = "edit", Delete = "delete", - IssueCert = "issue-cert" + IssueCert = "issue-cert", + RevealAcmeEabSecret = "reveal-acme-eab-secret" } export enum ProjectPermissionSecretRotationActions { diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx index 7938d04f5..974b49279 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx @@ -23,6 +23,8 @@ export const CertificateProfilesTab = () => { const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isRevealProfileAcmeEabSecretModalOpen, setIsRevealProfileAcmeEabSecretModalOpen] = + useState(false); const [selectedProfile, setSelectedProfile] = useState( null ); @@ -43,6 +45,11 @@ export const CertificateProfilesTab = () => { setIsEditModalOpen(true); }; + const handleRevealProfileAcmeEabSecret = (profile: TCertificateProfileWithDetails) => { + setSelectedProfile(profile); + setIsRevealProfileAcmeEabSecretModalOpen(true); + }; + const handleDeleteProfile = (profile: TCertificateProfileWithDetails) => { setSelectedProfile(profile); setIsDeleteModalOpen(true); @@ -85,7 +92,11 @@ export const CertificateProfilesTab = () => { )}
- + setIsCreateModalOpen(false)} /> diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileList.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileList.tsx index b3fc0e22f..7b1ac187a 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileList.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileList.tsx @@ -20,9 +20,14 @@ import { ProfileRow } from "./ProfileRow"; interface Props { onEditProfile: (profile: TCertificateProfileWithDetails) => void; onDeleteProfile: (profile: TCertificateProfileWithDetails) => void; + onRevealProfileAcmeEabSecret: (profile: TCertificateProfileWithDetails) => void; } -export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => { +export const ProfileList = ({ + onEditProfile, + onRevealProfileAcmeEabSecret, + onDeleteProfile +}: Props) => { const { currentProject } = useProject(); const { data, isLoading } = useListCertificateProfiles({ @@ -88,6 +93,7 @@ export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => { key={profile.id} profile={profile} onEditProfile={onEditProfile} + onRevealProfileAcmeEabSecret={onRevealProfileAcmeEabSecret} onDeleteProfile={onDeleteProfile} /> ))} diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileRow.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileRow.tsx index 19638483b..222800364 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileRow.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileRow.tsx @@ -1,14 +1,15 @@ -import { useCallback } from "react"; import { faCheck, faCircleInfo, faCopy, faEdit, faEllipsis, + faEye, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useCallback } from "react"; import { createNotification } from "@app/components/notifications"; import { @@ -36,10 +37,16 @@ import { CertificateIssuanceModal } from "@app/pages/cert-manager/CertificatesPa interface Props { profile: TCertificateProfile; onEditProfile: (profile: TCertificateProfile) => void; + onRevealProfileAcmeEabSecret: (profile: TCertificateProfile) => void; onDeleteProfile: (profile: TCertificateProfile) => void; } -export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) => { +export const ProfileRow = ({ + profile, + onEditProfile, + onRevealProfileAcmeEabSecret, + onDeleteProfile +}: Props) => { const { permission } = useProjectPermission(); const { data: caData } = useGetCaById(profile.caId); @@ -69,6 +76,11 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) = ProjectPermissionSub.CertificateAuthorities ); + const canRevealProfileAcmeEabSecret = permission.can( + ProjectPermissionCertificateProfileActions.RevealAcmeEabSecret, + ProjectPermissionSub.CertificateProfiles + ); + const canIssueCertificate = permission.can( ProjectPermissionCertificateProfileActions.IssueCert, ProjectPermissionSub.CertificateProfiles @@ -144,6 +156,17 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) = Edit Profile )} + {canRevealProfileAcmeEabSecret && profile.enrollmentType === "acme" && ( + { + e.stopPropagation(); + onRevealProfileAcmeEabSecret(profile); + }} + icon={} + > + Reveal ACME EAB Secret + + )} {canIssueCertificate && profile.enrollmentType === "api" && ( { From ab47ef9e6e6db35e7734da4948542c2ac4cdca75 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 16:37:44 -0800 Subject: [PATCH 229/294] Reveal --- .../CertificateProfilesTab.tsx | 12 ++++++++- .../RevealAcmeEabSecretModal.tsx | 25 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/RevealAcmeEabSecretModal.tsx diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx index 974b49279..9ee238743 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx @@ -1,6 +1,6 @@ -import { useState } from "react"; import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useState } from "react"; import { createNotification } from "@app/components/notifications"; import { Button, DeleteActionModal } from "@app/components/v2"; @@ -16,6 +16,7 @@ import { import { CreateProfileModal } from "./CreateProfileModal"; import { ProfileList } from "./ProfileList"; +import { RevealAcmeEabSecretModal } from "./RevealAcmeEabSecretModal"; export const CertificateProfilesTab = () => { const { permission } = useProjectPermission(); @@ -112,6 +113,15 @@ export const CertificateProfilesTab = () => { mode="edit" /> + { + setIsRevealProfileAcmeEabSecretModalOpen(false); + setSelectedProfile(null); + }} + profile={selectedProfile} + /> + void; + profile: TCertificateProfileWithDetails; +}; + +export const RevealAcmeEabSecretModal = ({ isOpen, onClose, profile }: Props) => { + return ( + { + if (!open) { + onClose(); + } + }} + > + +
Reveal ACME EAB Secret
+
+
+ ); +}; From f2fa429458b717f4449a519e95af3cf94d71eda2 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 17:01:08 -0800 Subject: [PATCH 230/294] Improve UI --- .../CertificateProfilesTab/ProfileRow.tsx | 2 +- .../RevealAcmeEabSecretModal.tsx | 78 ++++++++++++++++++- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileRow.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileRow.tsx index 222800364..2a06063c4 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileRow.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileRow.tsx @@ -164,7 +164,7 @@ export const ProfileRow = ({ }} icon={} > - Reveal ACME EAB Secret + Reveal EAB Secret
)} {canIssueCertificate && profile.enrollmentType === "api" && ( diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/RevealAcmeEabSecretModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/RevealAcmeEabSecretModal.tsx index 9596dd37c..076f36fb3 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/RevealAcmeEabSecretModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/RevealAcmeEabSecretModal.tsx @@ -1,5 +1,8 @@ -import { Modal, ModalContent } from "@app/components/v2"; +import { FormLabel, IconButton, Input, Modal, ModalContent } from "@app/components/v2"; +import { useToggle } from "@app/hooks"; import { TCertificateProfileWithDetails } from "@app/hooks/api/certificateProfiles"; +import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; type Props = { isOpen: boolean; @@ -8,6 +11,13 @@ type Props = { }; export const RevealAcmeEabSecretModal = ({ isOpen, onClose, profile }: Props) => { + const [isAcmeDirectoryUrlCopied, setIsAcmeDirectoryUrlCopied] = useToggle(false); + const [isEabKidCopied, setIsEabKidCopied] = useToggle(false); + const [isEabSecretCopied, setIsEabSecretCopied] = useToggle(false); + + const acmeDirectoryUrl = "http://FIXME.com/directory"; + const eabKid = profile.id; + const eabSecret = "FIXME"; return ( } }} > - -
Reveal ACME EAB Secret
+ + + +
+ { + navigator.clipboard.writeText(acmeDirectoryUrl); + setIsAcmeDirectoryUrlCopied.on(); + }} + className="w-10" + > + + +
+ + +
+ + { + navigator.clipboard.writeText(eabKid); + setIsEabKidCopied.on(); + }} + className="w-10" + > + + +
+ + +
+ + { + navigator.clipboard.writeText(eabSecret); + setIsEabSecretCopied.on(); + }} + className="w-10" + > + + +
); From 01a2871a6ebb39a4c42380fa5ae95fd9a1eab3d9 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 17:45:09 -0800 Subject: [PATCH 231/294] UI adjustment --- .../CertificateProfilesTab/RevealAcmeEabSecretModal.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/RevealAcmeEabSecretModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/RevealAcmeEabSecretModal.tsx index 076f36fb3..31ccb1d9b 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/RevealAcmeEabSecretModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/RevealAcmeEabSecretModal.tsx @@ -29,7 +29,7 @@ export const RevealAcmeEabSecretModal = ({ isOpen, onClose, profile }: Props) => >
@@ -74,7 +74,7 @@ export const RevealAcmeEabSecretModal = ({ isOpen, onClose, profile }: Props) =>
From c928b6abf715333b35538038a078991ec591d104 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 17:47:57 -0800 Subject: [PATCH 232/294] ui --- .../CertificateProfilesTab/RevealAcmeEabSecretModal.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/RevealAcmeEabSecretModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/RevealAcmeEabSecretModal.tsx index 31ccb1d9b..729b3e896 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/RevealAcmeEabSecretModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/RevealAcmeEabSecretModal.tsx @@ -33,10 +33,10 @@ export const RevealAcmeEabSecretModal = ({ isOpen, onClose, profile }: Props) => > -
+
From e931d1936fce9f5040122a54c4e446e442e4bac1 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 18:07:04 -0800 Subject: [PATCH 233/294] Return directory url --- .../certificate-profile-service.ts | 15 +++++++++++++-- .../certificate-profile-types.ts | 4 ++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/backend/src/services/certificate-profile/certificate-profile-service.ts b/backend/src/services/certificate-profile/certificate-profile-service.ts index be7a0a253..f1cee1761 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.ts @@ -31,6 +31,7 @@ import { TCertificateProfileWithConfigs } from "./certificate-profile-types"; import { TAcmeEnrollmentConfigDALFactory } from "../enrollment-config/acme-enrollment-config-dal"; +import { buildUrl } from "@app/ee/services/pki-acme/pki-acme-fns"; const generateAndEncryptAcmeEabSecret = async ( projectId: string, @@ -484,6 +485,12 @@ export const certificateProfileServiceFactory = ({ profile.estConfig.caChain = ""; } } + if (profile.enrollmentType === EnrollmentType.ACME && profile.acmeConfig) { + profile.acmeConfig.directoryUrl = buildUrl(profile.id, "/directory"); + if (profile.acmeConfig.encryptedEabSecret) { + profile.acmeConfig.encryptedEabSecret = undefined; + } + } return { ...profile, @@ -619,7 +626,11 @@ export const certificateProfileServiceFactory = ({ const result: TCertificateProfileWithConfigs = { ...converted, estConfig: decryptedEstConfig, - apiConfig: profileWithConfigs.apiConfig + apiConfig: profileWithConfigs.apiConfig, + acmeConfig: + profile.enrollmentType === EnrollmentType.ACME + ? { id: profile.id, directoryUrl: buildUrl(profile.id, "/directory") } + : undefined }; return result; @@ -831,7 +842,7 @@ export const certificateProfileServiceFactory = ({ const kmsDecryptor = await kmsService.decryptWithKmsKey({ kmsId: certificateManagerKmsId }); - const eabSecret = await kmsDecryptor({ cipherTextBlob: profile.acmeConfig.encryptedEabSecret }); + const eabSecret = await kmsDecryptor({ cipherTextBlob: profile.acmeConfig.encryptedEabSecret! }); return { eabKid: profile.id, eabSecret: eabSecret.toString("base64url") }; }; diff --git a/backend/src/services/certificate-profile/certificate-profile-types.ts b/backend/src/services/certificate-profile/certificate-profile-types.ts index 8a89b4d0d..82ba0f378 100644 --- a/backend/src/services/certificate-profile/certificate-profile-types.ts +++ b/backend/src/services/certificate-profile/certificate-profile-types.ts @@ -58,9 +58,9 @@ export type TCertificateProfileWithConfigs = TCertificateProfile & { }; acmeConfig?: { id: string; - encryptedEabSecret: Buffer; + directoryUrl: string; + encryptedEabSecret?: Buffer; }; - metrics?: TCertificateProfileMetrics; }; export interface TCertificateProfileCertificate { From b5a555fee55e2b4b3b78e0dff5cb5b4974346241 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 18:30:01 -0800 Subject: [PATCH 234/294] Expose directory url --- .../routes/v1/certificate-profiles-router.ts | 6 + .../certificate-profile-dal.ts | 17 +- .../certificate-profile-service.ts | 7 +- .../enrollment-config-types.ts | 4 +- .../hooks/api/certificateProfiles/queries.tsx | 22 ++- .../hooks/api/certificateProfiles/types.ts | 8 + .../RevealAcmeEabSecretModal.tsx | 152 ++++++++++-------- 7 files changed, 143 insertions(+), 73 deletions(-) diff --git a/backend/src/server/routes/v1/certificate-profiles-router.ts b/backend/src/server/routes/v1/certificate-profiles-router.ts index f85f8a677..7d4e04773 100644 --- a/backend/src/server/routes/v1/certificate-profiles-router.ts +++ b/backend/src/server/routes/v1/certificate-profiles-router.ts @@ -168,6 +168,12 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid autoRenew: z.boolean(), renewBeforeDays: z.number().optional() }) + .optional(), + acmeConfig: z + .object({ + id: z.string(), + directoryUrl: z.string() + }) .optional() }).array(), totalCount: z.number() diff --git a/backend/src/services/certificate-profile/certificate-profile-dal.ts b/backend/src/services/certificate-profile/certificate-profile-dal.ts index f8f511ab4..5296cb172 100644 --- a/backend/src/services/certificate-profile/certificate-profile-dal.ts +++ b/backend/src/services/certificate-profile/certificate-profile-dal.ts @@ -274,6 +274,11 @@ export const certificateProfileDALFactory = (db: TDbClient) => { `${TableName.PkiCertificateProfile}.apiConfigId`, `${TableName.PkiApiEnrollmentConfig}.id` ) + .leftJoin( + TableName.PkiAcmeEnrollmentConfig, + `${TableName.PkiCertificateProfile}.acmeConfigId`, + `${TableName.PkiAcmeEnrollmentConfig}.id` + ) .select(selectAllTableCols(TableName.PkiCertificateProfile)) .select( db.ref("id").withSchema(TableName.PkiEstEnrollmentConfig).as("estId"), @@ -285,7 +290,8 @@ export const certificateProfileDALFactory = (db: TDbClient) => { db.ref("encryptedCaChain").withSchema(TableName.PkiEstEnrollmentConfig).as("estEncryptedCaChain"), db.ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiId"), db.ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiAutoRenew"), - db.ref("renewBeforeDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiRenewBeforeDays") + db.ref("renewBeforeDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiRenewBeforeDays"), + db.ref("id").withSchema(TableName.PkiAcmeEnrollmentConfig).as("acmeId") ); const results = (await query @@ -312,6 +318,12 @@ export const certificateProfileDALFactory = (db: TDbClient) => { } : undefined; + const acmeConfig = result.acmeId + ? { + id: result.acmeId as string + } + : undefined; + const baseProfile = { id: result.id, projectId: result.projectId, @@ -325,7 +337,8 @@ export const certificateProfileDALFactory = (db: TDbClient) => { createdAt: result.createdAt, updatedAt: result.updatedAt, estConfig, - apiConfig + apiConfig, + acmeConfig }; return baseProfile as TCertificateProfileWithConfigs; diff --git a/backend/src/services/certificate-profile/certificate-profile-service.ts b/backend/src/services/certificate-profile/certificate-profile-service.ts index f1cee1761..8adbf2c45 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.ts @@ -627,10 +627,9 @@ export const certificateProfileServiceFactory = ({ ...converted, estConfig: decryptedEstConfig, apiConfig: profileWithConfigs.apiConfig, - acmeConfig: - profile.enrollmentType === EnrollmentType.ACME - ? { id: profile.id, directoryUrl: buildUrl(profile.id, "/directory") } - : undefined + acmeConfig: profileWithConfigs.acmeConfig + ? { ...profileWithConfigs.acmeConfig, directoryUrl: buildUrl(profile.id, "/directory") } + : undefined }; return result; diff --git a/backend/src/services/enrollment-config/enrollment-config-types.ts b/backend/src/services/enrollment-config/enrollment-config-types.ts index ff017e409..ed2f921c4 100644 --- a/backend/src/services/enrollment-config/enrollment-config-types.ts +++ b/backend/src/services/enrollment-config/enrollment-config-types.ts @@ -37,6 +37,4 @@ export interface TApiConfigData { renewBeforeDays?: number; } -export interface TAcmeConfigData { - eabSecret: string; -} +export interface TAcmeConfigData {} diff --git a/frontend/src/hooks/api/certificateProfiles/queries.tsx b/frontend/src/hooks/api/certificateProfiles/queries.tsx index 19859b02f..1e0fe3b9b 100644 --- a/frontend/src/hooks/api/certificateProfiles/queries.tsx +++ b/frontend/src/hooks/api/certificateProfiles/queries.tsx @@ -10,7 +10,8 @@ import { TGetProfileCertificatesDTO, TGetProfileMetricsDTO, TListCertificateProfilesDTO, - TProfileCertificate + TProfileCertificate, + TRevealAcmeEabSecretDTO } from "./types"; export const certificateProfileKeys = { @@ -41,6 +42,11 @@ export const certificateProfileKeys = { "metrics", profileId, params + ], + revealAcmeEabSecret: (profileId: string) => [ + "certificate-profiles", + "reveal-acme-eab-secret", + profileId ] }; @@ -112,6 +118,20 @@ export const useGetCertificateProfileBySlug = ({ }); }; +export const useRevealAcmeEabSecret = ({ profileId }: TRevealAcmeEabSecretDTO) => { + return useQuery({ + queryKey: certificateProfileKeys.revealAcmeEabSecret(profileId), + queryFn: async () => { + const { data } = await apiRequest.get<{ + eabKid: string; + eabSecret: string; + }>(`/api/v1/pki/certificate-profiles/${profileId}/acme/eab-secret/reveal`); + return data; + }, + enabled: Boolean(profileId) + }); +}; + export const useGetProfileCertificates = ({ profileId, offset = 0, diff --git a/frontend/src/hooks/api/certificateProfiles/types.ts b/frontend/src/hooks/api/certificateProfiles/types.ts index 94d8d0c6d..7acb6fef9 100644 --- a/frontend/src/hooks/api/certificateProfiles/types.ts +++ b/frontend/src/hooks/api/certificateProfiles/types.ts @@ -36,6 +36,10 @@ export type TCertificateProfileWithDetails = TCertificateProfile & { autoRenew: boolean; renewBeforeDays?: number; }; + acmeConfig?: { + id: string; + directoryUrl: string; + }; }; export type TCreateCertificateProfileDTO = { @@ -95,6 +99,10 @@ export type TGetCertificateProfileBySlugDTO = { slug: string; }; +export type TRevealAcmeEabSecretDTO = { + profileId: string; +}; + export type TProfileCertificate = { id: string; serialNumber: string; diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/RevealAcmeEabSecretModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/RevealAcmeEabSecretModal.tsx index 729b3e896..7156fc7e4 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/RevealAcmeEabSecretModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/RevealAcmeEabSecretModal.tsx @@ -1,6 +1,16 @@ -import { FormLabel, IconButton, Input, Modal, ModalContent } from "@app/components/v2"; +import { + Alert, + AlertDescription, + FormLabel, + IconButton, + Input, + Modal, + ModalContent, + Spinner +} from "@app/components/v2"; import { useToggle } from "@app/hooks"; import { TCertificateProfileWithDetails } from "@app/hooks/api/certificateProfiles"; +import { useRevealAcmeEabSecret } from "@app/hooks/api/certificateProfiles/queries"; import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -14,10 +24,12 @@ export const RevealAcmeEabSecretModal = ({ isOpen, onClose, profile }: Props) => const [isAcmeDirectoryUrlCopied, setIsAcmeDirectoryUrlCopied] = useToggle(false); const [isEabKidCopied, setIsEabKidCopied] = useToggle(false); const [isEabSecretCopied, setIsEabSecretCopied] = useToggle(false); + const revealAcmeEabSecret = useRevealAcmeEabSecret({ profileId: profile.id }); + const { data, isLoading, isError, error } = revealAcmeEabSecret; + + const { directoryUrl } = profile.acmeConfig!; + const { eabKid, eabSecret } = data ?? { eabKid: "", eabSecret: "" }; - const acmeDirectoryUrl = "http://FIXME.com/directory"; - const eabKid = profile.id; - const eabSecret = "FIXME"; return ( title="Reveal EAB Secret" subTitle="To issue certificates automatically, your ACME client needs the following details." > - -
- - { - navigator.clipboard.writeText(acmeDirectoryUrl); - setIsAcmeDirectoryUrlCopied.on(); - }} - className="w-10" - > - - -
+ {isLoading && ( +
+ +
+ )} + {isError && ( + + Failed to reveal EAB secret: {error.message} + + )} + {data && ( + <> + +
+ + { + navigator.clipboard.writeText(directoryUrl); + setIsAcmeDirectoryUrlCopied.on(); + }} + className="w-10" + > + + +
- -
- - { - navigator.clipboard.writeText(eabKid); - setIsEabKidCopied.on(); - }} - className="w-10" - > - - -
+ +
+ + { + navigator.clipboard.writeText(eabKid); + setIsEabKidCopied.on(); + }} + className="w-10" + > + + +
- -
- - { - navigator.clipboard.writeText(eabSecret); - setIsEabSecretCopied.on(); - }} - className="w-10" - > - - -
+ +
+ + { + navigator.clipboard.writeText(eabSecret); + setIsEabSecretCopied.on(); + }} + className="w-10" + > + + +
+ + )}
); From 431fc222a129cbba268b6b2a415aa743688dce5c Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 18:33:02 -0800 Subject: [PATCH 235/294] Extract fns --- .../src/ee/services/pki-acme/pki-acme-fns.ts | 17 +++++++++++++++++ .../ee/services/pki-acme/pki-acme-service.ts | 16 +--------------- 2 files changed, 18 insertions(+), 15 deletions(-) create mode 100644 backend/src/ee/services/pki-acme/pki-acme-fns.ts diff --git a/backend/src/ee/services/pki-acme/pki-acme-fns.ts b/backend/src/ee/services/pki-acme/pki-acme-fns.ts new file mode 100644 index 000000000..0763a8ed0 --- /dev/null +++ b/backend/src/ee/services/pki-acme/pki-acme-fns.ts @@ -0,0 +1,17 @@ +import { getConfig } from "@app/lib/config/env"; +import { z } from "zod"; +import { AcmeMalformedError } from "./pki-acme-errors"; + +export const buildUrl = (profileId: string, path: string): string => { + const appCfg = getConfig(); + const baseUrl = appCfg.SITE_URL ?? ""; + return `${baseUrl}/api/v1/pki/acme/profiles/${profileId}${path}`; +}; + +export const extractAccountIdFromKid = (kid: string, profileId: string): string => { + const kidPrefix = buildUrl(profileId, "/accounts/"); + if (!kid.startsWith(kidPrefix)) { + throw new AcmeMalformedError({ detail: "KID must start with the profile account URL" }); + } + return z.string().uuid().parse(kid.slice(kidPrefix.length)); +}; diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index cc3df7ce7..43c77b3d1 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -1,6 +1,5 @@ import { TPkiAcmeAccounts } from "@app/db/schemas/pki-acme-accounts"; import { TPkiAcmeAuths } from "@app/db/schemas/pki-acme-auths"; -import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; @@ -39,6 +38,7 @@ import { AcmeUnauthorizedError, AcmeUnsupportedIdentifierError } from "./pki-acme-errors"; +import { buildUrl, extractAccountIdFromKid } from "./pki-acme-fns"; import { TPkiAcmeOrderAuthDALFactory } from "./pki-acme-order-auth-dal"; import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal"; import { @@ -115,20 +115,6 @@ export const pkiAcmeServiceFactory = ({ return profile; }; - const buildUrl = (profileId: string, path: string): string => { - const appCfg = getConfig(); - const baseUrl = appCfg.SITE_URL ?? ""; - return `${baseUrl}/api/v1/pki/acme/profiles/${profileId}${path}`; - }; - - const extractAccountIdFromKid = (kid: string, profileId: string): string => { - const kidPrefix = buildUrl(profileId, "/accounts/"); - if (!kid.startsWith(kidPrefix)) { - throw new AcmeMalformedError({ detail: "KID must start with the profile account URL" }); - } - return z.string().uuid().parse(kid.slice(kidPrefix.length)); - }; - const validateJwsPayload = async < TSchema extends z.ZodSchema | undefined = undefined, T = TSchema extends z.ZodSchema ? R : string From 3c887522ec94fcf73394bb234edeedfb4581e497 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 18:39:09 -0800 Subject: [PATCH 236/294] Add cert foreign key --- backend/src/db/migrations/20251029234547_add-pki-acme.ts | 4 ++++ backend/src/ee/services/pki-acme/pki-acme-service.ts | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/src/db/migrations/20251029234547_add-pki-acme.ts b/backend/src/db/migrations/20251029234547_add-pki-acme.ts index e76587159..f9db5c2ac 100644 --- a/backend/src/db/migrations/20251029234547_add-pki-acme.ts +++ b/backend/src/db/migrations/20251029234547_add-pki-acme.ts @@ -80,6 +80,10 @@ export async function up(knex: Knex): Promise { t.uuid("accountId").notNullable(); t.foreign("accountId").references("id").inTable(TableName.PkiAcmeAccount).onDelete("CASCADE"); + // Foreign key to certificate + t.uuid("certificateId").nullable(); + t.foreign("certificateId").references("id").inTable(TableName.Certificate).onDelete("CASCADE"); + t.timestamp("notBefore").nullable(); t.timestamp("notAfter").nullable(); diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 43c77b3d1..86c5607d3 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -319,7 +319,7 @@ export const pkiAcmeServiceFactory = ({ const kmsDecryptor = await kmsService.decryptWithKmsKey({ kmsId: certificateManagerKmsId }); - const eabSecret = await kmsDecryptor({ cipherTextBlob: profile.acmeConfig!.encryptedEabSecret }); + const eabSecret = await kmsDecryptor({ cipherTextBlob: profile.acmeConfig!.encryptedEabSecret! }); try { const { payload: eabPayload, protectedHeader: eabProtectedHeader } = await flattenedVerify( externalAccountBinding, @@ -393,7 +393,6 @@ export const pkiAcmeServiceFactory = ({ emails: contact ?? [] }); // TODO: create audit log here - // TODO: check EAB authentication here return { status: 201, body: { From 68300ed018292d13b9ff4557fef4a087156506f8 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 18:43:49 -0800 Subject: [PATCH 237/294] Assoicate cert id with order --- backend/src/db/schemas/pki-acme-orders.ts | 3 ++- backend/src/ee/services/pki-acme/pki-acme-service.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/src/db/schemas/pki-acme-orders.ts b/backend/src/db/schemas/pki-acme-orders.ts index eee8c96f0..da4c640a8 100644 --- a/backend/src/db/schemas/pki-acme-orders.ts +++ b/backend/src/db/schemas/pki-acme-orders.ts @@ -19,7 +19,8 @@ export const PkiAcmeOrdersSchema = z.object({ csr: z.string().nullable().optional(), certificate: z.string().nullable().optional(), certificateChain: z.string().nullable().optional(), - error: z.string().nullable().optional() + error: z.string().nullable().optional(), + certificateid: z.string().uuid().nullable().optional() }); export type TPkiAcmeOrders = z.infer; diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 86c5607d3..7fae7ea92 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -594,7 +594,8 @@ export const pkiAcmeServiceFactory = ({ status: AcmeOrderStatus.Valid, csr, certificateChain, - certificate + certificate, + certificateId }, tx ); From 6a721e6624e3babdae3aa4e3758d4d5bd9019604 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 18:45:37 -0800 Subject: [PATCH 238/294] Fix db schema --- backend/src/db/schemas/pki-acme-orders.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/db/schemas/pki-acme-orders.ts b/backend/src/db/schemas/pki-acme-orders.ts index da4c640a8..67396f2d9 100644 --- a/backend/src/db/schemas/pki-acme-orders.ts +++ b/backend/src/db/schemas/pki-acme-orders.ts @@ -20,7 +20,7 @@ export const PkiAcmeOrdersSchema = z.object({ certificate: z.string().nullable().optional(), certificateChain: z.string().nullable().optional(), error: z.string().nullable().optional(), - certificateid: z.string().uuid().nullable().optional() + certificateId: z.string().uuid().nullable().optional() }); export type TPkiAcmeOrders = z.infer; From ef98b1b3bdd2efcdc21c3371cf17a7a76a64009d Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 18:50:06 -0800 Subject: [PATCH 239/294] Add eab secrets and kid for bdd --- backend/bdd/features/steps/pki_acme.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index e005803da..ca265699e 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -1,5 +1,6 @@ import json import logging +import os import re import threading @@ -30,8 +31,10 @@ faker = Faker() class AcmeProfile: - def __init__(self, id: str): + def __init__(self, id: str, eab_kid: str, eab_secret: str): self.id = id + self.eab_kid = eab_kid + self.eab_secret = eab_secret def replace_vars(payload: dict | list | int | float | str, vars: dict): @@ -134,8 +137,14 @@ def step_impl(context: Context, profile_var: str): # TODO: Fixed value for now, just to make test much easier, # we should call infisical API to create such profile instead # in the future - profile_id = "322be4ee-fe20-41c0-ba7c-bdbdfeee2ba8" - context.vars[profile_var] = AcmeProfile(profile_id) + profile_id = "9fda66ee-03f0-4b7c-95ad-542feff77177" + kid = profile_id + secret = os.getenv("EAB_SECRET") + context.vars[profile_var] = AcmeProfile( + profile_id, + eab_kid=kid, + eab_secret=secret, + ) @given("I use {token_var} for authentication") From 0d883af30b125e84b0e70f967e33ec5b37cf30e3 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 18:59:31 -0800 Subject: [PATCH 240/294] Implement nonce gen and check --- .../ee/services/pki-acme/pki-acme-service.ts | 27 ++++++++++++++++--- backend/src/keystore/keystore.ts | 4 ++- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 7fae7ea92..49142d5c1 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -5,6 +5,7 @@ import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/erro import { logger } from "@app/lib/logger"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; +import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { ActorType } from "@app/services/auth/auth-type"; import { EnrollmentType, @@ -29,6 +30,7 @@ import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal"; import { AcmeAccountDoesNotExistError, AcmeBadCSRError, + AcmeBadNonceError, AcmeBadPublicKeyError, AcmeError, AcmeExternalAccountRequiredError, @@ -87,6 +89,7 @@ type TPkiAcmeServiceFactoryDep = { TPkiAcmeChallengeDALFactory, "create" | "transaction" | "updateById" | "findByAccountAuthAndChallengeId" | "findByIdForChallengeValidation" >; + keyStore: Pick; kmsService: Pick; certificateV3Service: Pick; acmeChallengeService: TPkiAcmeChallengeServiceFactory; @@ -100,6 +103,7 @@ export const pkiAcmeServiceFactory = ({ acmeAuthDAL, acmeOrderAuthDAL, acmeChallengeDAL, + keyStore, kmsService, certificateV3Service, acmeChallengeService @@ -157,6 +161,14 @@ export const pkiAcmeServiceFactory = ({ if (new URL(protectedHeader.url).href !== url.href) { throw new AcmeUnauthorizedError({ detail: "URL mismatch in the protected header" }); } + if (!protectedHeader.nonce) { + throw new AcmeMalformedError({ detail: "Nonce is required in the protected header" }); + } + const deleted = await keyStore.deleteItem(KeyStorePrefixes.PkiAcmeNonce(protectedHeader.nonce)); + if (deleted !== 1) { + throw new AcmeBadNonceError({ detail: "Invalid nonce" }); + } + // TODO: consume the nonce here const decoder = new TextDecoder(); const textPayload = decoder.decode(rawPayload); @@ -285,10 +297,17 @@ export const pkiAcmeServiceFactory = ({ }; const getAcmeNewNonce = async (profileId: string): Promise => { - const profile = await validateAcmeProfile(profileId); - // FIXME: Implement ACME new nonce generation - // Generate a new nonce, store it, and return it - return "FIXME-generate-nonce"; + await validateAcmeProfile(profileId); + const nonce = crypto.randomBytes(32).toString("base64url"); + const nonceKey = KeyStorePrefixes.PkiAcmeNonce(nonce); + await keyStore.setItemWithExpiry( + nonceKey, + // Expire in 5 minutes. + // TODO: read config from the profile to get the expiration time instead + 60 * 5, + nonce + ); + return nonce; }; /** -------------------------------------------------------------- diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index 3155fe05c..204952a92 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -77,7 +77,9 @@ export const KeyStorePrefixes = { UserProjectPermissionPattern: (userId: string) => `project-permission:*:*:USER:${userId}:*` as const, IdentityProjectPermissionPattern: (identityId: string) => `project-permission:*:*:IDENTITY:${identityId}:*` as const, GroupMemberProjectPermissionPattern: (projectId: string, groupId: string) => - `group-member-project-permission:${projectId}:${groupId}:*` as const + `group-member-project-permission:${projectId}:${groupId}:*` as const, + + PkiAcmeNonce: (nonce: string) => `pki-acme-nonce:${nonce}` as const }; export const KeyStoreTtls = { From 4ab3e2cb97c4210aa84542f917b8f64d80b656b8 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 19:00:54 -0800 Subject: [PATCH 241/294] comments --- backend/src/ee/services/pki-acme/pki-acme-service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 49142d5c1..237eb2061 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -158,9 +158,11 @@ export const pkiAcmeServiceFactory = ({ const { protectedHeader: rawProtectedHeader, payload: rawPayload } = result; try { const protectedHeader = ProtectedHeaderSchema.parse(rawProtectedHeader); + // Validate the URL if (new URL(protectedHeader.url).href !== url.href) { throw new AcmeUnauthorizedError({ detail: "URL mismatch in the protected header" }); } + // Consume the nonce if (!protectedHeader.nonce) { throw new AcmeMalformedError({ detail: "Nonce is required in the protected header" }); } @@ -169,7 +171,7 @@ export const pkiAcmeServiceFactory = ({ throw new AcmeBadNonceError({ detail: "Invalid nonce" }); } - // TODO: consume the nonce here + // Parse the payload const decoder = new TextDecoder(); const textPayload = decoder.decode(rawPayload); const payload = schema ? schema.parse(JSON.parse(textPayload)) : textPayload; From ec855e8952dd5af2d78f86ae89e536ccd52cd131 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 19:02:33 -0800 Subject: [PATCH 242/294] Add missing ks --- backend/src/server/routes/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 9af799ba6..bc19e6146 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -2206,6 +2206,7 @@ export const registerRoutes = async ( acmeAuthDAL, acmeOrderAuthDAL, acmeChallengeDAL, + keyStore, kmsService, certificateV3Service, acmeChallengeService From 25a08d28d09110d44199474b201aafd453f7de11 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 19:15:34 -0800 Subject: [PATCH 243/294] Fix type errors --- .../src/ee/services/pki-acme/pki-acme-service.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 237eb2061..90c5a4ddc 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -267,7 +267,12 @@ export const pkiAcmeServiceFactory = ({ expiresAt: Date; notBefore?: Date | null; notAfter?: Date | null; - authorizations: TPkiAcmeAuths[]; + authorizations: { + id: string; + identifierType: string; + identifierValue: string; + expiresAt: Date; + }[]; }; profileId: string; }): TAcmeOrderResource => { @@ -276,13 +281,11 @@ export const pkiAcmeServiceFactory = ({ expires: order.expiresAt.toISOString(), notBefore: order.notBefore?.toISOString(), notAfter: order.notAfter?.toISOString(), - identifiers: order.authorizations.map((auth: TPkiAcmeAuths) => ({ + identifiers: order.authorizations.map((auth) => ({ type: auth.identifierType, value: auth.identifierValue })), - authorizations: order.authorizations.map((auth: TPkiAcmeAuths) => - buildUrl(profileId, `/authorizations/${auth.id}`) - ), + authorizations: order.authorizations.map((auth) => buildUrl(profileId, `/authorizations/${auth.id}`)), finalize: buildUrl(profileId, `/orders/${order.id}/finalize`), certificate: order.status === AcmeOrderStatus.Valid ? buildUrl(profileId, `/orders/${order.id}/certificate`) : undefined @@ -630,7 +633,7 @@ export const pkiAcmeServiceFactory = ({ }, tx ); - // TODO: log the error + logger.error(error, "Failed to sign certificate"); // TODO: audit log the error if (error instanceof BadRequestError) { errorToReturn = new AcmeBadCSRError({ detail: `Invalid CSR: ${error.message}` }); From 675b4f5a26e12c37600236495e86dd1e5bd26e9c Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 19:49:03 -0800 Subject: [PATCH 244/294] Add TODO --- backend/src/ee/services/pki-acme/pki-acme-service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 90c5a4ddc..757e60e86 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -617,6 +617,8 @@ export const pkiAcmeServiceFactory = ({ { status: AcmeOrderStatus.Valid, csr, + // TODO: we actually don't need to store the certificate and certificate chain here + // It appears that the certificate and certificate chain are stored in the certificate_body table already certificateChain, certificate, certificateId From 50bc8ad891141232ebe34fafa8be922a5d82c330 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 19:57:27 -0800 Subject: [PATCH 245/294] Fix wrong cert order --- backend/src/ee/services/pki-acme/pki-acme-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 757e60e86..e4cdc74c9 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -683,7 +683,7 @@ export const pkiAcmeServiceFactory = ({ } return { status: 200, - body: order.certificateChain! + "\n" + order.certificate!, + body: order.certificate! + "\n" + order.certificateChain!, headers: { Location: buildUrl(profileId, `/orders/${orderId}/certificate`), Link: `<${buildUrl(profileId, "/directory")}>;rel="index"` From 79c7320140cf0695ad15e8f8b09b25f2fd9a9d8f Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 3 Nov 2025 20:32:40 -0800 Subject: [PATCH 246/294] Fix cert format --- backend/bdd/features/pki/acme/challenge.feature | 3 ++- backend/bdd/features/steps/pki_acme.py | 7 ++++--- backend/src/ee/routes/v1/pki-acme-router.ts | 1 + backend/src/ee/services/pki-acme/pki-acme-service.ts | 8 +++++++- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/backend/bdd/features/pki/acme/challenge.feature b/backend/bdd/features/pki/acme/challenge.feature index a5d94aff5..155cc64f2 100644 --- a/backend/bdd/features/pki/acme/challenge.feature +++ b/backend/bdd/features/pki/acme/challenge.feature @@ -19,4 +19,5 @@ Feature: Challenge Then I select challenge with type http-01 for domain localhost from order at order as challenge Then I serve challenge response for challenge at localhost Then I tell ACME server that challenge is ready to be verified - Then I poll and finalize the ACME order order + Then I poll and finalize the ACME order order as finalized_order + # TODO: check the content of the order diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index ca265699e..0ccf71ff8 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -475,8 +475,9 @@ def step_impl(context: Context, var_path: str): acme_client.answer_challenge(challenge, response) -@then("I poll and finalize the ACME order {var_path}") -def step_impl(context: Context, var_path: str): +@then("I poll and finalize the ACME order {var_path} as {finalized_var}") +def step_impl(context: Context, var_path: str, finalized_var: str): order = eval_var(context, var_path, as_json=False) acme_client = context.acme_client - acme_client.poll_and_finalize(order) + finalized_order = acme_client.poll_and_finalize(order) + context.vars[finalized_var] = finalized_order diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index f628c8d36..4a516dae2 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -380,6 +380,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { if (payload !== "") { throw new AcmeMalformedError({ detail: "Payload should be empty" }); } + res.type("application/pem-certificate-chain"); return sendAcmeResponse( res, profileId, diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index e4cdc74c9..f1ce7019d 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -683,7 +683,13 @@ export const pkiAcmeServiceFactory = ({ } return { status: 200, - body: order.certificate! + "\n" + order.certificateChain!, + body: + order.certificate!.trim().replace("\n", "\r\n") + + "\r\n" + + order.certificateChain!.trim().replace("\n", "\r\n") + + // The final line is needed, otherwise some clients will not parse the certificate chain correctly + // ref: https://github.com/certbot/certbot/blob/4d5d5f7ae8164884c841969e46caed8db1ad34af/certbot/src/certbot/crypto_util.py#L506-L514 + "\r\n", headers: { Location: buildUrl(profileId, `/orders/${orderId}/certificate`), Link: `<${buildUrl(profileId, "/directory")}>;rel="index"` From f555c4ea7dd04bc2b7699d32a2a0be599c67e21d Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 09:21:04 -0800 Subject: [PATCH 247/294] Format --- backend/bdd/features/pki/acme/account.feature | 5 +++-- backend/bdd/features/pki/acme/dicrectory.feature | 7 ++++--- backend/bdd/features/pki/acme/nonce.feature | 7 ++++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/backend/bdd/features/pki/acme/account.feature b/backend/bdd/features/pki/acme/account.feature index 463e6b04f..7e1d67a93 100644 --- a/backend/bdd/features/pki/acme/account.feature +++ b/backend/bdd/features/pki/acme/account.feature @@ -1,5 +1,6 @@ Feature: Account + Scenario: Create a new account Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory - Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account + When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account diff --git a/backend/bdd/features/pki/acme/dicrectory.feature b/backend/bdd/features/pki/acme/dicrectory.feature index 04eade31e..481a3337a 100644 --- a/backend/bdd/features/pki/acme/dicrectory.feature +++ b/backend/bdd/features/pki/acme/dicrectory.feature @@ -1,9 +1,10 @@ Feature: Directory + Scenario: Get the directory of ACME service urls Given I have an ACME cert profile as "acme_profile" - When I send a GET request to "/api/v1/pki/acme/profiles/{acme_profile.id}/directory" - Then the response status code should be "200" - Then the response body should match JSON value + When I send a GET request to "/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + Then the response status code should be "200" + Then the response body should match JSON value """ { "newNonce": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-nonce", diff --git a/backend/bdd/features/pki/acme/nonce.feature b/backend/bdd/features/pki/acme/nonce.feature index 0d62f7b46..7bbeb3b9d 100644 --- a/backend/bdd/features/pki/acme/nonce.feature +++ b/backend/bdd/features/pki/acme/nonce.feature @@ -1,6 +1,7 @@ Feature: Nonce + Scenario: Generate a new nonce Given I have an ACME cert profile as "acme_profile" - When I send a HEAD request to "/api/v1/pki/acme/profiles/{acme_profile.id}/new-nonce" - Then the response status code should be "200" - Then the response header "Replay-Nonce" should contains non-empty value + When I send a HEAD request to "/api/v1/pki/acme/profiles/{acme_profile.id}/new-nonce" + Then the response status code should be "200" + Then the response header "Replay-Nonce" should contains non-empty value From ba16793de5c7a74ee08d518683b063158ba7f342 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 09:22:06 -0800 Subject: [PATCH 248/294] Update todo --- backend/bdd/features/pki/acme/challenge.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/bdd/features/pki/acme/challenge.feature b/backend/bdd/features/pki/acme/challenge.feature index 155cc64f2..8a0237631 100644 --- a/backend/bdd/features/pki/acme/challenge.feature +++ b/backend/bdd/features/pki/acme/challenge.feature @@ -20,4 +20,4 @@ Feature: Challenge Then I serve challenge response for challenge at localhost Then I tell ACME server that challenge is ready to be verified Then I poll and finalize the ACME order order as finalized_order - # TODO: check the content of the order + # TODO: check the fullchain pem content of the order From 088059b3050e0b29a608202cb43fddae45598b49 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 09:24:21 -0800 Subject: [PATCH 249/294] Fix BDD test --- backend/bdd/features/steps/pki_acme.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 0ccf71ff8..30482c5b3 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -321,7 +321,7 @@ def apply_value_with_jq(context: Context, var_path: str, jq_query: str): ).first() -@then("the value {var_path} with jq {jq_query} should be equal to json") +@then('the value {var_path} with jq "{jq_query}" should be equal to json') def step_impl(context: Context, var_path: str, jq_query: str): value, result = apply_value_with_jq( context=context, From c9f6cebd7bb72ea8936da5fc2975e87551640366 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 09:27:11 -0800 Subject: [PATCH 250/294] Remove org name in csr for now --- backend/bdd/features/pki/acme/auth.feature | 1 - backend/bdd/features/pki/acme/challenge.feature | 2 +- backend/bdd/features/pki/acme/order.feature | 3 --- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/backend/bdd/features/pki/acme/auth.feature b/backend/bdd/features/pki/acme/auth.feature index b70599b66..4605e2eef 100644 --- a/backend/bdd/features/pki/acme/auth.feature +++ b/backend/bdd/features/pki/acme/auth.feature @@ -9,7 +9,6 @@ Feature: Authorization Then I add names to certificate signing request csr """ { - "ORGANIZATION_NAME": "Infisical Inc", "COMMON_NAME": "localhost" } """ diff --git a/backend/bdd/features/pki/acme/challenge.feature b/backend/bdd/features/pki/acme/challenge.feature index 8a0237631..3f3690838 100644 --- a/backend/bdd/features/pki/acme/challenge.feature +++ b/backend/bdd/features/pki/acme/challenge.feature @@ -9,7 +9,6 @@ Feature: Challenge Then I add names to certificate signing request csr """ { - "ORGANIZATION_NAME": "Infisical Inc", "COMMON_NAME": "localhost" } """ @@ -20,4 +19,5 @@ Feature: Challenge Then I serve challenge response for challenge at localhost Then I tell ACME server that challenge is ready to be verified Then I poll and finalize the ACME order order as finalized_order + Then the value finalized_order.body.status should be equal to valid # TODO: check the fullchain pem content of the order diff --git a/backend/bdd/features/pki/acme/order.feature b/backend/bdd/features/pki/acme/order.feature index 49f72e485..2a85dc84c 100644 --- a/backend/bdd/features/pki/acme/order.feature +++ b/backend/bdd/features/pki/acme/order.feature @@ -9,7 +9,6 @@ Feature: Order Then I add names to certificate signing request csr """ { - "ORGANIZATION_NAME": "Infisical Inc", "COMMON_NAME": "localhost" } """ @@ -31,7 +30,6 @@ Feature: Order Then I add names to certificate signing request csr """ { - "ORGANIZATION_NAME": "Infisical Inc", "COMMON_NAME": "localhost" } """ @@ -63,7 +61,6 @@ Feature: Order Then I add names to certificate signing request csr """ { - "ORGANIZATION_NAME": "Infisical Inc", "COMMON_NAME": "localhost" } """ From efe923d2a07ad3e6bcb4278678e1fdc8ed4de5f0 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 09:28:48 -0800 Subject: [PATCH 251/294] Fix test --- backend/bdd/features/pki/acme/challenge.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/bdd/features/pki/acme/challenge.feature b/backend/bdd/features/pki/acme/challenge.feature index 3f3690838..ba9970e43 100644 --- a/backend/bdd/features/pki/acme/challenge.feature +++ b/backend/bdd/features/pki/acme/challenge.feature @@ -19,5 +19,5 @@ Feature: Challenge Then I serve challenge response for challenge at localhost Then I tell ACME server that challenge is ready to be verified Then I poll and finalize the ACME order order as finalized_order - Then the value finalized_order.body.status should be equal to valid + Then the value finalized_order.body with jq ".status" should be equal to "valid" # TODO: check the fullchain pem content of the order From 65b61514ae9e4f3950d7c39964a876a160722f12 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 09:32:59 -0800 Subject: [PATCH 252/294] Fix all broken tests --- backend/bdd/features/pki/acme/order.feature | 4 ++-- backend/bdd/features/steps/pki_acme.py | 13 ++++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/backend/bdd/features/pki/acme/order.feature b/backend/bdd/features/pki/acme/order.feature index 2a85dc84c..046dcda55 100644 --- a/backend/bdd/features/pki/acme/order.feature +++ b/backend/bdd/features/pki/acme/order.feature @@ -19,7 +19,7 @@ Feature: Order Then the value order.body with jq ".status" should be equal to "pending" Then the value order.body with jq ".identifiers" should be equal to [{"type": "dns", "value": "localhost"}] Then the value order.body with jq ".finalize" should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize - Then the value order.body with jq "all(.authorizations[]; startswith('{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/'))" should be equal to true + Then the value order.body with jq "all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/"))" should be equal to true Scenario: Create a new order with SANs Given I have an ACME cert profile as "acme_profile" @@ -71,4 +71,4 @@ Feature: Order Then the value fetched_order with jq ".status" should be equal to "pending" Then the value fetched_order with jq ".identifiers" should be equal to [{"type": "dns", "value": "localhost"}] Then the value fetched_order with jq ".finalize" should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize - Then the value fetched_order with jq "all(.authorizations[]; startswith('{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/'))" should be equal to true + Then the value fetched_order with jq "all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/"))" should be equal to true diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 30482c5b3..3441b851a 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -160,7 +160,10 @@ def step_impl(context: Context, method: str, url: str): ) context.vars["response"] = response logger.debug("Response status: %r", response.status_code) - logger.debug("Response JSON payload: %r", response.json()) + try: + logger.debug("Response JSON payload: %r", response.json()) + except json.decoder.JSONDecodeError: + pass @when('I send a {method} request to "{url}" with JSON payload') @@ -203,14 +206,14 @@ def step_impl(context: Context, url: str): @then('the response status code should be "{expected_status_code:d}"') def step_impl(context: Context, expected_status_code: int): - assert context.response.status_code == expected_status_code, ( - f"{context.response.status_code} != {expected_status_code}" + assert context.vars["response"].status_code == expected_status_code, ( + f"{context.vars['response'].status_code} != {expected_status_code}" ) @then('the response header "{header}" should contains non-empty value') def step_impl(context: Context, header: str): - header_value = context.response.headers.get(header) + header_value = context.vars["response"].headers.get(header) assert header_value is not None, f"Header {header} not found in response" assert header_value, ( f"Header {header} found in response, but value {header_value:!r} is empty" @@ -219,7 +222,7 @@ def step_impl(context: Context, header: str): @then("the response body should match JSON value") def step_impl(context: Context): - payload = context.response.json() + payload = context.vars["response"].json() expected = json.loads(context.text) replaced = replace_vars(expected, context.vars) assert payload == replaced, f"{payload} != {replaced}" From 7f69674e2c34c62542a9a1b9e4c0ee3e43a0009a Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 10:50:40 -0800 Subject: [PATCH 253/294] Use existing cert obj --- .../ee/services/pki-acme/pki-acme-service.ts | 32 +++++++++++++++++-- backend/src/server/routes/index.ts | 1 + 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index f1ce7019d..8ec786ec9 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -4,6 +4,7 @@ import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; +import * as x509 from "@peculiar/x509"; import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { ActorType } from "@app/services/auth/auth-type"; @@ -12,6 +13,7 @@ import { TCertificateProfileWithConfigs } from "@app/services/certificate-profile/certificate-profile-types"; import { TCertificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service"; +import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; @@ -75,6 +77,7 @@ import { type TPkiAcmeServiceFactoryDep = { projectDAL: Pick; certificateProfileDAL: Pick; + certificateBodyDAL: Pick; acmeAccountDAL: Pick< TPkiAcmeAccountDALFactory, "findByProjectIdAndAccountId" | "findByProfileIdAndPublicKeyThumbprintAndAlg" | "create" @@ -98,6 +101,7 @@ type TPkiAcmeServiceFactoryDep = { export const pkiAcmeServiceFactory = ({ projectDAL, certificateProfileDAL, + certificateBodyDAL, acmeAccountDAL, acmeOrderDAL, acmeAuthDAL, @@ -674,6 +678,7 @@ export const pkiAcmeServiceFactory = ({ accountId: string; orderId: string; }): Promise> => { + const profile = await validateAcmeProfile(profileId); const order = await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId); if (!order) { throw new NotFoundError({ message: "ACME order not found" }); @@ -681,12 +686,35 @@ export const pkiAcmeServiceFactory = ({ if (order.status !== AcmeOrderStatus.Valid) { throw new AcmeOrderNotReadyError({ message: "ACME order is not valid" }); } + if (!order.certificateId) { + throw new NotFoundError({ message: "The underlying certificate no longer exists" }); + } + + const certBody = await certificateBodyDAL.findOne({ certId: order.certificateId }); + const certificateManagerKeyId = await getProjectKmsCertificateKeyId({ + projectId: profile.projectId, + projectDAL, + kmsService + }); + + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: certificateManagerKeyId + }); + const decryptedCert = await kmsDecryptor({ + cipherTextBlob: certBody.encryptedCertificate + }); + const certObj = new x509.X509Certificate(decryptedCert); + const decryptedCertChain = await kmsDecryptor({ + cipherTextBlob: certBody.encryptedCertificateChain! + }); + const certificateChain = decryptedCertChain.toString(); + return { status: 200, body: - order.certificate!.trim().replace("\n", "\r\n") + + certObj.toString("pem").trim().replace("\n", "\r\n") + "\r\n" + - order.certificateChain!.trim().replace("\n", "\r\n") + + certificateChain.trim().replace("\n", "\r\n") + // The final line is needed, otherwise some clients will not parse the certificate chain correctly // ref: https://github.com/certbot/certbot/blob/4d5d5f7ae8164884c841969e46caed8db1ad34af/certbot/src/certbot/crypto_util.py#L506-L514 "\r\n", diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index bc19e6146..cd8fac508 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -2201,6 +2201,7 @@ export const registerRoutes = async ( const pkiAcmeService = pkiAcmeServiceFactory({ projectDAL, certificateProfileDAL, + certificateBodyDAL, acmeAccountDAL, acmeOrderDAL, acmeAuthDAL, From f85d913017480fddd034316286c898e93903c717 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 10:54:46 -0800 Subject: [PATCH 254/294] Remove not needed columns --- backend/src/db/migrations/20251029234547_add-pki-acme.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/src/db/migrations/20251029234547_add-pki-acme.ts b/backend/src/db/migrations/20251029234547_add-pki-acme.ts index f9db5c2ac..c6b95009b 100644 --- a/backend/src/db/migrations/20251029234547_add-pki-acme.ts +++ b/backend/src/db/migrations/20251029234547_add-pki-acme.ts @@ -90,11 +90,7 @@ export async function up(knex: Knex): Promise { t.timestamp("expiresAt").notNullable(); t.text("csr").nullable(); - t.text("certificate").nullable(); - t.text("certificateChain").nullable(); - t.text("error").nullable(); - // Order status t.string("status").notNullable(); // pending, ready, processing, valid, invalid From 089157b237dba613f481fc5b1f10861b3ae8c0c7 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 11:09:01 -0800 Subject: [PATCH 255/294] Update db schema, load cert from cert body instead --- ...acme.ts => 20251104234547_add-pki-acme.ts} | 0 backend/src/db/schemas/pki-acme-challenges.ts | 1 + backend/src/db/schemas/pki-acme-orders.ts | 10 +++--- .../ee/services/pki-acme/pki-acme-service.ts | 35 ++++++++----------- 4 files changed, 20 insertions(+), 26 deletions(-) rename backend/src/db/migrations/{20251029234547_add-pki-acme.ts => 20251104234547_add-pki-acme.ts} (100%) diff --git a/backend/src/db/migrations/20251029234547_add-pki-acme.ts b/backend/src/db/migrations/20251104234547_add-pki-acme.ts similarity index 100% rename from backend/src/db/migrations/20251029234547_add-pki-acme.ts rename to backend/src/db/migrations/20251104234547_add-pki-acme.ts diff --git a/backend/src/db/schemas/pki-acme-challenges.ts b/backend/src/db/schemas/pki-acme-challenges.ts index 17282da06..18245bb76 100644 --- a/backend/src/db/schemas/pki-acme-challenges.ts +++ b/backend/src/db/schemas/pki-acme-challenges.ts @@ -12,6 +12,7 @@ export const PkiAcmeChallengesSchema = z.object({ authId: z.string().uuid(), type: z.string(), status: z.string(), + error: z.string().nullable().optional(), validatedAt: z.date().nullable().optional(), createdAt: z.date(), updatedAt: z.date() diff --git a/backend/src/db/schemas/pki-acme-orders.ts b/backend/src/db/schemas/pki-acme-orders.ts index 67396f2d9..928753d8c 100644 --- a/backend/src/db/schemas/pki-acme-orders.ts +++ b/backend/src/db/schemas/pki-acme-orders.ts @@ -10,17 +10,15 @@ import { TImmutableDBKeys } from "./models"; export const PkiAcmeOrdersSchema = z.object({ id: z.string().uuid(), accountId: z.string().uuid(), + certificateId: z.string().uuid().nullable().optional(), notBefore: z.date().nullable().optional(), notAfter: z.date().nullable().optional(), expiresAt: z.date(), + csr: z.string().nullable().optional(), + error: z.string().nullable().optional(), status: z.string(), createdAt: z.date(), - updatedAt: z.date(), - csr: z.string().nullable().optional(), - certificate: z.string().nullable().optional(), - certificateChain: z.string().nullable().optional(), - error: z.string().nullable().optional(), - certificateId: z.string().uuid().nullable().optional() + updatedAt: z.date() }); export type TPkiAcmeOrders = z.infer; diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 8ec786ec9..546bd6757 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -599,32 +599,27 @@ export const pkiAcmeServiceFactory = ({ // TODO: this should be the same transaction? let errorToReturn: Error | undefined; try { - const { certificate, certificateChain, certificateId } = - await certificateV3Service.signCertificateFromProfile({ - actor: ActorType.ACME_ACCOUNT, - actorId: accountId, - actorAuthMethod: null, - actorOrgId, - profileId, - csr, - notBefore: order.notBefore ? new Date(order.notBefore) : undefined, - notAfter: order.notAfter ? new Date(order.notAfter) : undefined, - validity: { - // TODO: read config from the profile to get the expiration time instead - ttl: (24 * 60 * 60 * 1000).toString() - }, - enrollmentType: EnrollmentType.ACME - }); + const { certificateId } = await certificateV3Service.signCertificateFromProfile({ + actor: ActorType.ACME_ACCOUNT, + actorId: accountId, + actorAuthMethod: null, + actorOrgId, + profileId, + csr, + notBefore: order.notBefore ? new Date(order.notBefore) : undefined, + notAfter: order.notAfter ? new Date(order.notAfter) : undefined, + validity: { + // TODO: read config from the profile to get the expiration time instead + ttl: (24 * 60 * 60 * 1000).toString() + }, + enrollmentType: EnrollmentType.ACME + }); // TODO: associate the certificate with the order await acmeOrderDAL.updateById( orderId, { status: AcmeOrderStatus.Valid, csr, - // TODO: we actually don't need to store the certificate and certificate chain here - // It appears that the certificate and certificate chain are stored in the certificate_body table already - certificateChain, - certificate, certificateId }, tx From 82d03a1129e5a5ad260545440813d65900098069 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 11:09:25 -0800 Subject: [PATCH 256/294] Update text --- backend/src/ee/services/pki-acme/pki-acme-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 546bd6757..0f537731c 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -682,7 +682,7 @@ export const pkiAcmeServiceFactory = ({ throw new AcmeOrderNotReadyError({ message: "ACME order is not valid" }); } if (!order.certificateId) { - throw new NotFoundError({ message: "The underlying certificate no longer exists" }); + throw new NotFoundError({ message: "The certificate for this ACME order no longer exists" }); } const certBody = await certificateBodyDAL.findOne({ certId: order.certificateId }); From bf39f5eb173438bcf1b054cf8cfc42598b083b3a Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 11:19:06 -0800 Subject: [PATCH 257/294] Implement list order --- .../ee/services/pki-acme/pki-acme-order-dal.ts | 12 +++++++++++- .../src/ee/services/pki-acme/pki-acme-service.ts | 15 ++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts index 8600079df..ca02b09bb 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts @@ -60,9 +60,19 @@ export const pkiAcmeOrderDALFactory = (db: TDbClient) => { } }; + const listByAccountId = async (accountId: string, tx?: Knex) => { + try { + const orders = await (tx || db)(TableName.PkiAcmeOrder).where({ accountId }).orderBy("createdAt", "desc"); + return orders; + } catch (error) { + throw new DatabaseError({ error, name: "List PKI ACME orders by account id" }); + } + }; + return { ...pkiAcmeOrderOrm, findByIdForFinalization, - findByAccountAndOrderIdWithAuthorizations + findByAccountAndOrderIdWithAuthorizations, + listByAccountId }; }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 0f537731c..5bfbf2120 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -84,7 +84,12 @@ type TPkiAcmeServiceFactoryDep = { >; acmeOrderDAL: Pick< TPkiAcmeOrderDALFactory, - "create" | "transaction" | "updateById" | "findByAccountAndOrderIdWithAuthorizations" | "findByIdForFinalization" + | "create" + | "transaction" + | "updateById" + | "findByAccountAndOrderIdWithAuthorizations" + | "findByIdForFinalization" + | "listByAccountId" >; acmeAuthDAL: Pick; acmeOrderAuthDAL: Pick; @@ -727,15 +732,11 @@ export const pkiAcmeServiceFactory = ({ profileId: string; accountId: string; }): Promise> => { - const profile = await validateAcmeProfile(profileId); - // FIXME: Implement ACME list orders + const orders = await acmeOrderDAL.listByAccountId(accountId); return { status: 200, - body: { - orders: [] - }, + body: { orders: orders.map((order) => buildUrl(profileId, `/orders/${order.id}`)) }, headers: { - Location: buildUrl(profileId, `/accounts/${accountId}/orders`), Link: `<${buildUrl(profileId, "/directory")}>;rel="index"` } }; From 10054b3e4bb18e0ba3655d5dd4e1bd03eede5907 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 11:30:43 -0800 Subject: [PATCH 258/294] Refine eab payload checking logic --- .../ee/services/pki-acme/pki-acme-service.ts | 62 +++++++++---------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 5bfbf2120..caf6c53cd 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -353,46 +353,44 @@ export const pkiAcmeServiceFactory = ({ kmsId: certificateManagerKmsId }); const eabSecret = await kmsDecryptor({ cipherTextBlob: profile.acmeConfig!.encryptedEabSecret! }); + let eabPayload: Uint8Array | undefined; + let eabProtectedHeader: JWSHeaderParameters | undefined; try { - const { payload: eabPayload, protectedHeader: eabProtectedHeader } = await flattenedVerify( - externalAccountBinding, - eabSecret - ); - const { alg: eabAlg, kid: eabKid } = eabProtectedHeader!; - if (!["HS256", "HS384", "HS512"].includes(eabAlg!)) { - throw new AcmeMalformedError({ detail: "Invalid algorithm for external account binding JWS payload" }); - } - // Make sure the KID in the EAB payload matches the profile ID - if (eabKid !== profile.id) { - throw new UnauthorizedError({ message: "External account binding KID mismatch" }); - } - - // Make sure the URL matches the expected URL - const url = eabProtectedHeader!.url!; - if (url !== buildUrl(profile.id, "/new-account")) { - throw new UnauthorizedError({ message: "External account binding URL mismatch" }); - } - - // Make sure the JWK in the EAB payload matches the one provided in the outer JWS payload - const decoder = new TextDecoder(); - const decodedEabPayload = decoder.decode(eabPayload); - const eabJWK = JSON.parse(decodedEabPayload); - const eabPayloadJwkThumbprint = await calculateJwkThumbprint(eabJWK, "sha256"); - if (eabPayloadJwkThumbprint !== publicKeyThumbprint) { - throw new AcmeBadPublicKeyError({ - message: "External account binding public key thumbprint or algorithm mismatch" - }); - } + const result = await flattenedVerify(externalAccountBinding, eabSecret); + eabPayload = result.payload; + eabProtectedHeader = result.protectedHeader; } catch (error) { if (error instanceof errors.JWSInvalid) { throw new AcmeMalformedError({ detail: "Invalid external account binding JWS payload" }); } - if (error instanceof AcmeError) { - throw error; - } logger.error(error, "Unexpected error while verifying EAB JWS payload"); throw new AcmeServerInternalError({ detail: "Failed to verify EAB JWS payload" }); } + const { alg: eabAlg, kid: eabKid } = eabProtectedHeader!; + if (!["HS256", "HS384", "HS512"].includes(eabAlg!)) { + throw new AcmeMalformedError({ detail: "Invalid algorithm for external account binding JWS payload" }); + } + // Make sure the KID in the EAB payload matches the profile ID + if (eabKid !== profile.id) { + throw new UnauthorizedError({ message: "External account binding KID mismatch" }); + } + + // Make sure the URL matches the expected URL + const url = eabProtectedHeader!.url!; + if (url !== buildUrl(profile.id, "/new-account")) { + throw new UnauthorizedError({ message: "External account binding URL mismatch" }); + } + + // Make sure the JWK in the EAB payload matches the one provided in the outer JWS payload + const decoder = new TextDecoder(); + const decodedEabPayload = decoder.decode(eabPayload); + const eabJWK = JSON.parse(decodedEabPayload); + const eabPayloadJwkThumbprint = await calculateJwkThumbprint(eabJWK, "sha256"); + if (eabPayloadJwkThumbprint !== publicKeyThumbprint) { + throw new AcmeBadPublicKeyError({ + message: "External account binding public key thumbprint or algorithm mismatch" + }); + } const existingAccount: TPkiAcmeAccounts | null = await acmeAccountDAL.findByProfileIdAndPublicKeyThumbprintAndAlg( profileId, From 708b8f9f65f7451699a985571905034743ecab3b Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 11:32:23 -0800 Subject: [PATCH 259/294] Refine --- .../ee/services/pki-acme/pki-acme-service.ts | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index caf6c53cd..373063925 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -353,19 +353,22 @@ export const pkiAcmeServiceFactory = ({ kmsId: certificateManagerKmsId }); const eabSecret = await kmsDecryptor({ cipherTextBlob: profile.acmeConfig!.encryptedEabSecret! }); - let eabPayload: Uint8Array | undefined; - let eabProtectedHeader: JWSHeaderParameters | undefined; - try { - const result = await flattenedVerify(externalAccountBinding, eabSecret); - eabPayload = result.payload; - eabProtectedHeader = result.protectedHeader; - } catch (error) { - if (error instanceof errors.JWSInvalid) { - throw new AcmeMalformedError({ detail: "Invalid external account binding JWS payload" }); + const { eabPayload, eabProtectedHeader } = await (async () => { + try { + const { payload: eabPayload, protectedHeader: eabProtectedHeader } = await flattenedVerify( + externalAccountBinding, + eabSecret + ); + return { eabPayload, eabProtectedHeader }; + } catch (error) { + if (error instanceof errors.JWSInvalid) { + throw new AcmeMalformedError({ detail: "Invalid external account binding JWS payload" }); + } + logger.error(error, "Unexpected error while verifying EAB JWS payload"); + throw new AcmeServerInternalError({ detail: "Failed to verify EAB JWS payload" }); } - logger.error(error, "Unexpected error while verifying EAB JWS payload"); - throw new AcmeServerInternalError({ detail: "Failed to verify EAB JWS payload" }); - } + })(); + const { alg: eabAlg, kid: eabKid } = eabProtectedHeader!; if (!["HS256", "HS384", "HS512"].includes(eabAlg!)) { throw new AcmeMalformedError({ detail: "Invalid algorithm for external account binding JWS payload" }); From 1adcd721fe79d90c8927b2db532e300d9698233e Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 11:35:11 -0800 Subject: [PATCH 260/294] Use the right error type --- backend/src/ee/services/pki-acme/pki-acme-service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 373063925..f78aa93fc 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -158,7 +158,7 @@ export const pkiAcmeServiceFactory = ({ if (error instanceof ZodError) { throw new AcmeMalformedError({ detail: `Invalid JWS payload: ${error.message}` }); } - if (error instanceof errors.JWSInvalid) { + if (error instanceof errors.JWSSignatureVerificationFailed) { throw new AcmeBadPublicKeyError({ detail: "Invalid JWS payload" }); } logger.error(error, "Unexpected error while verifying JWS payload"); @@ -361,7 +361,7 @@ export const pkiAcmeServiceFactory = ({ ); return { eabPayload, eabProtectedHeader }; } catch (error) { - if (error instanceof errors.JWSInvalid) { + if (error instanceof errors.JWSSignatureVerificationFailed) { throw new AcmeMalformedError({ detail: "Invalid external account binding JWS payload" }); } logger.error(error, "Unexpected error while verifying EAB JWS payload"); From 52870c4bdcccbeeb1b4bd7a5d802784aad59ce39 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 11:37:40 -0800 Subject: [PATCH 261/294] wording --- backend/src/ee/services/pki-acme/pki-acme-service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index f78aa93fc..985c87063 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -362,10 +362,10 @@ export const pkiAcmeServiceFactory = ({ return { eabPayload, eabProtectedHeader }; } catch (error) { if (error instanceof errors.JWSSignatureVerificationFailed) { - throw new AcmeMalformedError({ detail: "Invalid external account binding JWS payload" }); + throw new AcmeMalformedError({ detail: "Invalid external account binding JWS signature" }); } - logger.error(error, "Unexpected error while verifying EAB JWS payload"); - throw new AcmeServerInternalError({ detail: "Failed to verify EAB JWS payload" }); + logger.error(error, "Unexpected error while verifying EAB JWS signature"); + throw new AcmeServerInternalError({ detail: "Failed to verify EAB JWS signature" }); } })(); From 582f0bc067f20a84f8c64205bc65eb4b3a2b21d7 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 11:42:24 -0800 Subject: [PATCH 262/294] Add env for bdd --- backend/bdd/.env.example | 14 ++++++++++++++ backend/bdd/features/environment.py | 8 +++----- backend/bdd/features/steps/pki_acme.py | 2 +- 3 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 backend/bdd/.env.example diff --git a/backend/bdd/.env.example b/backend/bdd/.env.example new file mode 100644 index 000000000..8e59c033d --- /dev/null +++ b/backend/bdd/.env.example @@ -0,0 +1,14 @@ +# API URL to the Infisical server +INFISICAL_API_URL="http://localhost:8080" +# JWT token with admin permission for the cert projects +INFISICAL_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoTWV0aG9kIjoiZW1haWwiLCJhdXRoVG9rZW5UeXBlIjoiYWNjZXNzVG9rZW4iLCJ1c2VySWQiOiJkOWZlMzMwZi00OTQwLTQ3ZmYtYmE4Yy0zZGUxYTVlYjYzNGEiLCJ0b2tlblZlcnNpb25JZCI6ImM4MWZhODY1LTFjYTAtNGNmZS1iNjM5LThlMDI3M2E2N2JjYyIsImFjY2Vzc1ZlcnNpb24iOjEsIm9yZ2FuaXphdGlvbklkIjoiM2I5OTRkNTktMjE5Ny00MjcwLWE3MGMtOTczMzdmZjlmYTRkIiwiaWF0IjoxNzYyMjAzMjQwLCJleHAiOjE3NjMwNjcyNDB9.KFYeMYAv3Ceis0hp-pTa8fsLLWbT-JcqhuWyIY0DWU0" +# PKI project id +PROJECT_ID="c051e74c-48a7-4724-832c-d5b496698546" +# Certificate CA id +CERT_CA_ID="2f0d9820-e5a8-48bb-aac8-deed9d868a1e" +# Certificate template id +CERT_TEMPLATE_ID="4dbf6bb0-6e86-4ee6-8550-9171428c8e82" +# ACME profile ID +PROFILE_ID="108c6303-ab8c-4986-ab88-eefe11bb5553" +# ACME profile EAB secret +EAB_SECRET="JHYxJDEwJFJldE9tb3dkUU9XVnJLZWFia3IxVC94L1pIbHRoQnJsNVRKZWFoV1hpNTczVHpwMFNGZzU4OGtuU3NVK1crVGM" diff --git a/backend/bdd/features/environment.py b/backend/bdd/features/environment.py index e5c5664c1..366790e63 100644 --- a/backend/bdd/features/environment.py +++ b/backend/bdd/features/environment.py @@ -10,11 +10,9 @@ logging.getLogger("httpx").setLevel(logging.DEBUG) load_dotenv() BASE_URL = os.environ.get("INFISICAL_API_URL", "http://localhost:8080") -PROJECT_ID = os.environ.get("PROJECT_ID", "c051e74c-48a7-4724-832c-d5b496698546") -CERT_CA_ID = os.environ.get("CERT_CA_ID", "2f0d9820-e5a8-48bb-aac8-deed9d868a1e") -CERT_TEMPLATE_ID = os.environ.get( - "CERT_TEMPLATE_ID", "4dbf6bb0-6e86-4ee6-8550-9171428c8e82" -) +PROJECT_ID = os.environ.get("PROJECT_ID") +CERT_CA_ID = os.environ.get("CERT_CA_ID") +CERT_TEMPLATE_ID = os.environ.get("CERT_TEMPLATE_ID") AUTH_TOKEN = os.environ.get("INFISICAL_TOKEN") diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 3441b851a..d0fa627cd 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -137,7 +137,7 @@ def step_impl(context: Context, profile_var: str): # TODO: Fixed value for now, just to make test much easier, # we should call infisical API to create such profile instead # in the future - profile_id = "9fda66ee-03f0-4b7c-95ad-542feff77177" + profile_id = os.getenv("PROFILE_ID") kid = profile_id secret = os.getenv("EAB_SECRET") context.vars[profile_var] = AcmeProfile( From 5218195ed4faded10fdf06274118f2fb3e9ee7f2 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 12:14:52 -0800 Subject: [PATCH 263/294] Update for review --- backend/src/ee/routes/v1/pki-acme-router.ts | 10 ++-------- backend/src/ee/services/pki-acme/pki-acme-errors.ts | 4 ++-- backend/src/ee/services/pki-acme/pki-acme-service.ts | 4 +++- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index 4a516dae2..a66967bbc 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -71,7 +71,6 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { done(null, undefined); } const json: unknown = JSON.parse(strBody as string); - // TODO: deal with JWS payload here done(null, json); } catch (err) { const error = err as Error; @@ -97,10 +96,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { 200: GetAcmeDirectoryResponseSchema } }, - handler: async (req) => { - const directory = await server.services.pkiAcme.getAcmeDirectory(req.params.profileId); - return directory; - } + handler: async (req) => server.services.pkiAcme.getAcmeDirectory(req.params.profileId) }); // HEAD /api/v1/pki/acme/profiles//new-nonce @@ -109,7 +105,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { method: "HEAD", url: "/profiles/:profileId/new-nonce", config: { - // TODO: probably a different rate limit for nonce creation + // TODO: probably a different rate limit for nonce creation? rateLimit: readLimit }, schema: { @@ -335,8 +331,6 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { 200: ListAcmeOrdersResponseSchema } }, - // TODO: replace with verify ACME signature here instead - // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { const { profileId, accountId } = await validateExistingAccount({ req, diff --git a/backend/src/ee/services/pki-acme/pki-acme-errors.ts b/backend/src/ee/services/pki-acme/pki-acme-errors.ts index 329cb1989..ed075d63b 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-errors.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-errors.ts @@ -1,6 +1,6 @@ /** - * ACME Error Classes based on RFC 8555 Section 6.2 - * https://datatracker.ietf.org/doc/html/rfc8555#section-6.2 + * ACME Error Classes based on RFC 8555 Section 6.7 + * https://datatracker.ietf.org/doc/html/rfc8555#section-6.7 */ // RFC 8555 Section 6.7 - Error Types diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 985c87063..b9a983ac8 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -478,6 +478,8 @@ export const pkiAcmeServiceFactory = ({ }): Promise> => { // TODO: check and see if we have existing orders for this account that meet the criteria // if we do, return the existing order + // TODO: check the identifiers and see if are they even allowed for this profile. + // if not, we may be able to reject it early with an unsupportedIdentifier error. const order = await acmeOrderDAL.transaction(async (tx) => { const account = (await acmeAccountDAL.findByProjectIdAndAccountId(profileId, accountId))!; @@ -592,7 +594,7 @@ export const pkiAcmeServiceFactory = ({ if (order.status === AcmeOrderStatus.Ready) { const { order: updatedOrder, error } = await acmeOrderDAL.transaction(async (tx) => { const order = (await acmeOrderDAL.findByIdForFinalization(orderId, tx))!; - // TODO: ideally, this should be doen with onRequest: verifyAuth([AuthMode.ACME_JWS_SIGNATURE]), instead + // TODO: ideally, this should be doen with onRequest: verifyAuth([AuthMode.ACME_JWS_SIGNATURE]), instead? const { ownerOrgId: actorOrgId } = (await certificateProfileDAL.findByIdWithOwnerOrgId(profileId, tx))!; if (order.status !== AcmeOrderStatus.Ready) { throw new AcmeOrderNotReadyError({ message: "ACME order is not ready" }); From 8a8f683c4d5c8e32270eebd17b2632ffcb5a7f0e Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 12:28:26 -0800 Subject: [PATCH 264/294] Hide fixme from user --- .../components/CertificateProfilesTab/CreateProfileModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx index eefc76a1d..b15d5daa6 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx @@ -566,7 +566,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" } name="acmeConfig" render={({ field, fieldState: { error } }) => ( -
FIXME: ACME configuration
+
{/* FIXME: ACME configuration */}
)} /> From cc749d5513c4b9b20cfa2d04ad8e701a3707bdb9 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 12:33:05 -0800 Subject: [PATCH 265/294] Add feature flag to gate the feature --- backend/src/ee/routes/v1/index.ts | 8 ++++++-- backend/src/lib/config/env.ts | 4 ++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index a22cd4583..2228c43bd 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -1,11 +1,11 @@ import { registerProjectTemplateRouter } from "@app/ee/routes/v1/project-template-router"; +import { getConfig } from "@app/lib/config/env"; import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router"; import { registerAccessApprovalRequestRouter } from "./access-approval-request-router"; import { registerAssumePrivilegeRouter } from "./assume-privilege-router"; import { AUDIT_LOG_STREAM_REGISTER_ROUTER_MAP, registerAuditLogStreamRouter } from "./audit-log-stream-routers"; import { registerCaCrlRouter } from "./certificate-authority-crl-router"; -import { registerPkiAcmeRouter } from "./pki-acme-router"; import { registerDeprecatedProjectRoleRouter } from "./deprecated-project-role-router"; import { registerDeprecatedProjectRouter } from "./deprecated-project-router"; import { registerDeprecatedSecretApprovalPolicyRouter } from "./deprecated-secret-approval-policy-router"; @@ -31,6 +31,7 @@ import { PAM_RESOURCE_REGISTER_ROUTER_MAP } from "./pam-resource-routers"; import { registerPamResourceRouter } from "./pam-resource-routers/pam-resource-router"; import { registerPamSessionRouter } from "./pam-session-router"; import { registerPITRouter } from "./pit-router"; +import { registerPkiAcmeRouter } from "./pki-acme-router"; import { registerProjectRoleRouter } from "./project-role-router"; import { registerProjectRouter } from "./project-router"; import { registerRateLimitRouter } from "./rate-limit-router"; @@ -108,7 +109,10 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { await server.register( async (pkiRouter) => { await pkiRouter.register(registerCaCrlRouter, { prefix: "/crl" }); - await pkiRouter.register(registerPkiAcmeRouter, { prefix: "/acme" }); + // Notice: current this feature is still in development and is not yet ready for production. + if (getConfig().ACME_FEATURE_ENABLED === true) { + await pkiRouter.register(registerPkiAcmeRouter, { prefix: "/acme" }); + } }, { prefix: "/pki" } ); diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 6f0502184..94ce79e94 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -106,6 +106,10 @@ const envSchema = z HTTPS_ENABLED: zodStrBool, ROTATION_DEVELOPMENT_MODE: zodStrBool.default("false").optional(), DAILY_RESOURCE_CLEAN_UP_DEVELOPMENT_MODE: zodStrBool.default("false").optional(), + // Note: The ACME feature is still in development and is not yet ready for production. + // This is the feature flag to enable/disable the ACME feature. + // It's not intended to be used by users outside of the development team yet. + ACME_FEATURE_ENABLED: zodStrBool.default("false").optional(), ACME_DEVELOPMENT_MODE: zodStrBool.default("false").optional(), ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES: zpStr( z From f6db76a23c400a70b300e7091acc3f63a398436f Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 12:44:19 -0800 Subject: [PATCH 266/294] Expose feature flag to frontend as well --- backend/src/ee/routes/v1/index.ts | 2 +- backend/src/lib/config/env.ts | 1 + backend/src/server/plugins/serve-ui.ts | 5 ++++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 2228c43bd..e104a8355 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -110,7 +110,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { async (pkiRouter) => { await pkiRouter.register(registerCaCrlRouter, { prefix: "/crl" }); // Notice: current this feature is still in development and is not yet ready for production. - if (getConfig().ACME_FEATURE_ENABLED === true) { + if (getConfig().isAcmeFeatureEnabled === true) { await pkiRouter.register(registerPkiAcmeRouter, { prefix: "/acme" }); } }, diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 94ce79e94..b60971b1f 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -399,6 +399,7 @@ const envSchema = z (data.NODE_ENV === "development" && data.ROTATION_DEVELOPMENT_MODE) || data.NODE_ENV === "test", isDailyResourceCleanUpDevelopmentMode: data.NODE_ENV === "development" && data.DAILY_RESOURCE_CLEAN_UP_DEVELOPMENT_MODE, + isAcmeFeatureEnabled: data.NODE_ENV === "development" && data.ACME_FEATURE_ENABLED === true, isAcmeDevelopmentMode: data.NODE_ENV === "development" && data.ACME_DEVELOPMENT_MODE, isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED, isRedisSentinelMode: Boolean(data.REDIS_SENTINEL_HOSTS), diff --git a/backend/src/server/plugins/serve-ui.ts b/backend/src/server/plugins/serve-ui.ts index b71451b6e..4330f9397 100644 --- a/backend/src/server/plugins/serve-ui.ts +++ b/backend/src/server/plugins/serve-ui.ts @@ -31,7 +31,10 @@ export const registerServeUI = async ( CAPTCHA_SITE_KEY: appCfg.CAPTCHA_SITE_KEY, POSTHOG_API_KEY: appCfg.POSTHOG_PROJECT_API_KEY, INTERCOM_ID: appCfg.INTERCOM_ID, - TELEMETRY_CAPTURING_ENABLED: appCfg.TELEMETRY_ENABLED + TELEMETRY_CAPTURING_ENABLED: appCfg.TELEMETRY_ENABLED, + // The feature flag to enable/disable the ACME feature. + // Will be removed once the feature is ready for production. + ACME_FEATURE_ENABLED: appCfg.isAcmeFeatureEnabled }; const js = `window.__INFISICAL_RUNTIME_ENV__ = Object.freeze(${JSON.stringify(config)});`; return res.send(js); From e8eb238744b6dcc32311a4dbbfba1b13fd04966a Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 13:04:09 -0800 Subject: [PATCH 267/294] Put acme behind the flag --- .../CertificateProfilesTab/CreateProfileModal.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx index b15d5daa6..bc4aa19be 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx @@ -1,8 +1,8 @@ -import { useEffect } from "react"; -import { Controller, useForm } from "react-hook-form"; import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; @@ -18,6 +18,7 @@ import { TextArea, Tooltip } from "@app/components/v2"; +import { envConfig } from "@app/config/env"; import { useProject } from "@app/context"; import { useListCasByProjectId } from "@app/hooks/api/ca/queries"; import { @@ -109,7 +110,7 @@ const editSchema = z .trim() .max(1000, "Description must be less than 1000 characters") .optional(), - enrollmentType: z.enum(["api", "est"]), + enrollmentType: z.enum(["api", "est", "acme"]), certificateAuthorityId: z.string().optional(), certificateTemplateId: z.string().optional(), estConfig: z @@ -124,7 +125,8 @@ const editSchema = z autoRenew: z.boolean().optional(), renewBeforeDays: z.number().min(1).max(365).optional() }) - .optional() + .optional(), + acmeConfig: z.object({}).optional() }) .refine( (data) => { @@ -134,6 +136,9 @@ const editSchema = z if (data.enrollmentType === "api" && !data.apiConfig) { return false; } + if (data.enrollmentType === "acme" && !data.acmeConfig) { + return false; + } return true; }, { @@ -448,7 +453,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" } > API EST - ACME + {envConfig.isAcmeFeatureEnabled && ACME} )} From 18c13317aef9761148fe7eb130af32c60caa3a10 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 13:08:01 -0800 Subject: [PATCH 268/294] Add feature flag for frontend --- frontend/src/config/env.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/config/env.ts b/frontend/src/config/env.ts index 63446c95f..c8100bd16 100644 --- a/frontend/src/config/env.ts +++ b/frontend/src/config/env.ts @@ -26,6 +26,9 @@ export const envConfig = { import.meta.env.VITE_TELEMETRY_CAPTURING_ENABLED === true ); }, + get ACME_FEATURE_ENABLED() { + return window?.__INFISICAL_RUNTIME_ENV__?.ACME_FEATURE_ENABLED ?? false; + }, get PLATFORM_VERSION() { return import.meta.env.VITE_INFISICAL_PLATFORM_VERSION; From c38985a533a196aefad0109a33f4f6a2142a2f52 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 13:14:03 -0800 Subject: [PATCH 269/294] Wrong feature flag --- .../components/CertificateProfilesTab/CreateProfileModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx index bc4aa19be..4395dbb9d 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx @@ -453,7 +453,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" } > API EST - {envConfig.isAcmeFeatureEnabled && ACME} + {envConfig.ACME_FEATURE_ENABLED && ACME} )} From dd30028c5dc284936bf04a1950bf6fc935bb8541 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 13:16:31 -0800 Subject: [PATCH 270/294] Add missing acme stuff --- .../CertificateProfilesTab/CreateProfileModal.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx index 4395dbb9d..2e1705f17 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx @@ -438,12 +438,16 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" } disableBootstrapCaValidation: false, passphrase: "" }); - } else { + } else if (value === "api") { setValue("estConfig", undefined); setValue("apiConfig", { autoRenew: false, renewBeforeDays: 30 }); + } else if (value === "acme") { + setValue("apiConfig", undefined); + setValue("estConfig", undefined); + setValue("acmeConfig", {}); } onChange(value); }} From 4b0f7a80d7d5c967f48ca80280c983e369e2e929 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 13:17:49 -0800 Subject: [PATCH 271/294] Add more missing stuff --- .../CertificateProfilesTab/CreateProfileModal.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx index 2e1705f17..3ad2290b2 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx @@ -390,17 +390,23 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" } {...field} onValueChange={(value) => { if (watchedEnrollmentType === "est") { + setValue("apiConfig", undefined); setValue("estConfig", { disableBootstrapCaValidation: false, passphrase: "" }); - setValue("apiConfig", undefined); - } else { + setValue("acmeConfig", undefined); + } else if (watchedEnrollmentType === "api") { setValue("apiConfig", { autoRenew: false, renewBeforeDays: 30 }); setValue("estConfig", undefined); + setValue("acmeConfig", undefined); + } else if (watchedEnrollmentType === "acme") { + setValue("estConfig", undefined); + setValue("apiConfig", undefined); + setValue("acmeConfig", {}); } onChange(value); }} @@ -438,12 +444,14 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" } disableBootstrapCaValidation: false, passphrase: "" }); + setValue("acmeConfig", undefined); } else if (value === "api") { - setValue("estConfig", undefined); setValue("apiConfig", { autoRenew: false, renewBeforeDays: 30 }); + setValue("estConfig", undefined); + setValue("acmeConfig", undefined); } else if (value === "acme") { setValue("apiConfig", undefined); setValue("estConfig", undefined); From 3ac3f44f1a9eb5a177a22dde09020585bbf338be Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 13:26:10 -0800 Subject: [PATCH 272/294] Fix UI --- frontend/src/global.d.ts | 1 + .../CertificateProfilesTab.tsx | 18 ++++++++++-------- .../RevealAcmeEabSecretModal.tsx | 1 - 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/frontend/src/global.d.ts b/frontend/src/global.d.ts index 30b80cb70..a6de7ac8c 100644 --- a/frontend/src/global.d.ts +++ b/frontend/src/global.d.ts @@ -7,6 +7,7 @@ declare global { POSTHOG_API_KEY?: string; INTERCOM_ID?: string; TELEMETRY_CAPTURING_ENABLED: string; + ACME_FEATURE_ENABLED?: boolean; }; } } diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx index 9ee238743..78517b49f 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx @@ -113,14 +113,16 @@ export const CertificateProfilesTab = () => { mode="edit" /> - { - setIsRevealProfileAcmeEabSecretModalOpen(false); - setSelectedProfile(null); - }} - profile={selectedProfile} - /> + {selectedProfile.enrollmentType === "acme" && ( + { + setIsRevealProfileAcmeEabSecretModalOpen(false); + setSelectedProfile(null); + }} + profile={selectedProfile} + /> + )} const [isEabSecretCopied, setIsEabSecretCopied] = useToggle(false); const revealAcmeEabSecret = useRevealAcmeEabSecret({ profileId: profile.id }); const { data, isLoading, isError, error } = revealAcmeEabSecret; - const { directoryUrl } = profile.acmeConfig!; const { eabKid, eabSecret } = data ?? { eabKid: "", eabSecret: "" }; From 69381fa6ddedb3202912c20e47bbf52bd30b7ef1 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 13:39:43 -0800 Subject: [PATCH 273/294] Add missing stuff for ui --- .../certificate-profile-schemas.ts | 20 ++++++++++++++++++- .../CreateProfileModal.tsx | 2 ++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/backend/src/services/certificate-profile/certificate-profile-schemas.ts b/backend/src/services/certificate-profile/certificate-profile-schemas.ts index 8ac494fe6..bf88593bd 100644 --- a/backend/src/services/certificate-profile/certificate-profile-schemas.ts +++ b/backend/src/services/certificate-profile/certificate-profile-schemas.ts @@ -27,7 +27,8 @@ export const createCertificateProfileSchema = z autoRenew: z.boolean().default(false), renewBeforeDays: z.number().min(1).max(30).optional() }) - .optional() + .optional(), + acmeConfig: z.object({}).optional() }) .refine( (data) => { @@ -38,6 +39,9 @@ export const createCertificateProfileSchema = z if (data.apiConfig) { return false; } + if (data.acmeConfig) { + return false; + } } if (data.enrollmentType === EnrollmentType.API) { if (!data.apiConfig) { @@ -46,6 +50,20 @@ export const createCertificateProfileSchema = z if (data.estConfig) { return false; } + if (data.acmeConfig) { + return false; + } + } + if (data.enrollmentType === EnrollmentType.ACME) { + if (!data.acmeConfig) { + return false; + } + if (data.estConfig) { + return false; + } + if (data.apiConfig) { + return false; + } } return true; }, diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx index 3ad2290b2..c3ed8478a 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx @@ -288,6 +288,8 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" } }; } else if (data.enrollmentType === "api" && data.apiConfig) { createData.apiConfig = data.apiConfig; + } else if (data.enrollmentType === "acme" && data.acmeConfig) { + createData.acmeConfig = data.acmeConfig; } await createProfile.mutateAsync(createData); From 7faac62c8ad55405fb72d4f82ad13e32c6fcdeb8 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 14:22:10 -0800 Subject: [PATCH 274/294] Fix linter errors --- backend/bdd/features/environment.py | 3 --- .../certificate-profile-service.test.ts | 13 +++++++++++++ .../CertificateProfilesTab/CreateProfileModal.tsx | 2 +- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/backend/bdd/features/environment.py b/backend/bdd/features/environment.py index 366790e63..b355f98ae 100644 --- a/backend/bdd/features/environment.py +++ b/backend/bdd/features/environment.py @@ -3,9 +3,6 @@ import os import httpx from behave.runner import Context from dotenv import load_dotenv -import logging - -logging.getLogger("httpx").setLevel(logging.DEBUG) load_dotenv() diff --git a/backend/src/services/certificate-profile/certificate-profile-service.test.ts b/backend/src/services/certificate-profile/certificate-profile-service.test.ts index bb30b8d5c..bb869d16f 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.test.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.test.ts @@ -10,6 +10,7 @@ import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { ActorType, AuthMethod } from "../auth/auth-type"; import type { TCertificateTemplateV2DALFactory } from "../certificate-template-v2/certificate-template-v2-dal"; +import { TAcmeEnrollmentConfigDALFactory } from "../enrollment-config/acme-enrollment-config-dal"; import type { TApiEnrollmentConfigDALFactory } from "../enrollment-config/api-enrollment-config-dal"; import type { TEstEnrollmentConfigDALFactory } from "../enrollment-config/est-enrollment-config-dal"; import type { TKmsServiceFactory } from "../kms/kms-service"; @@ -142,6 +143,17 @@ describe("CertificateProfileService", () => { delete: vi.fn() } as unknown as TEstEnrollmentConfigDALFactory; + const mockAcmeEnrollmentConfigDAL = { + create: vi.fn().mockResolvedValue({ id: "acme-config-123" }), + findById: vi.fn(), + updateById: vi.fn(), + transaction: vi.fn(), + find: vi.fn(), + findOne: vi.fn(), + update: vi.fn(), + delete: vi.fn() + } as unknown as TAcmeEnrollmentConfigDALFactory; + const mockPermissionService = { getProjectPermission: vi.fn().mockResolvedValue({ permission: { @@ -182,6 +194,7 @@ describe("CertificateProfileService", () => { certificateTemplateV2DAL: mockCertificateTemplateV2DAL, apiEnrollmentConfigDAL: mockApiEnrollmentConfigDAL, estEnrollmentConfigDAL: mockEstEnrollmentConfigDAL, + acmeEnrollmentConfigDAL: mockAcmeEnrollmentConfigDAL, permissionService: mockPermissionService, kmsService: mockKmsService, projectDAL: mockProjectDAL diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx index c3ed8478a..40bc1f671 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx @@ -583,7 +583,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" } ( + render={({ fieldState: { error } }) => (
{/* FIXME: ACME configuration */}
From 4791b16b00a256fabd22c4e85d0c18e40a8a6798 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 16:05:54 -0800 Subject: [PATCH 275/294] Fix lint --- .../pki-acme/pki-acme-challenge-service.ts | 30 ++++++++++++------- .../ee/services/pki-acme/pki-acme-service.ts | 18 +++++------ 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts index 3b858e427..52a277c31 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts @@ -6,6 +6,10 @@ import { AcmeConnectionError, AcmeDnsFailureError, AcmeIncorrectResponseError } import { AcmeAuthStatus, AcmeChallengeStatus, AcmeChallengeType } from "./pki-acme-schemas"; import { TPkiAcmeChallengeServiceFactory } from "./pki-acme-types"; +type FetchError = Error & { + code?: string; +}; + type TPkiAcmeChallengeServiceFactoryDep = { acmeChallengeDAL: Pick< TPkiAcmeChallengeDALFactory, @@ -69,27 +73,33 @@ export const pkiAcmeChallengeServiceFactory = ({ throw new AcmeIncorrectResponseError({ message: "ACME challenge response is not correct" }); } await acmeChallengeDAL.markAsValidCascadeById(challengeId, tx); - } catch (error) { + } catch (exp) { // TODO: we should retry the challenge validation a few times, but let's keep it simple for now await acmeChallengeDAL.markAsInvalidCascadeById(challengeId, tx); // Properly type and inspect the error - if (error instanceof TypeError && error.message.includes("fetch failed")) { - const cause = error.cause; - const errors = cause instanceof AggregateError ? cause.errors : cause instanceof Error ? [cause] : []; + if (exp instanceof TypeError && exp.message.includes("fetch failed")) { + const { cause } = exp; + let errors: Error[] = []; + if (cause instanceof AggregateError) { + errors = cause.errors; + } else if (cause instanceof Error) { + errors = [cause]; + } for (const err of errors) { // TODO: handle multiple errors, return a compound error instead of just the first error - if (err?.code === "ECONNREFUSED" || err?.message?.includes("ECONNREFUSED")) { + const fetchError = err as FetchError; + if (fetchError.code === "ECONNREFUSED" || fetchError.message.includes("ECONNREFUSED")) { return new AcmeConnectionError({ message: "Connection refused" }); - } else if (err?.code === "ENOTFOUND" || err?.message?.includes("ENOTFOUND")) { + } else if (fetchError.code === "ENOTFOUND" || fetchError.message.includes("ENOTFOUND")) { return new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)" }); } } - } else if (error instanceof Error) { - logger.error(error, "Error validating ACME challenge response"); + } else if (exp instanceof Error) { + logger.error(exp, "Error validating ACME challenge response"); } else { - logger.error(error, "Unknown error validating ACME challenge response"); + logger.error(exp, "Unknown error validating ACME challenge response"); } - return error; + return exp; } }); if (error) { diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index b9a983ac8..9f848748e 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -129,7 +129,7 @@ export const pkiAcmeServiceFactory = ({ }; const validateJwsPayload = async < - TSchema extends z.ZodSchema | undefined = undefined, + TSchema extends z.ZodSchema | undefined = undefined, T = TSchema extends z.ZodSchema ? R : string >({ url, @@ -149,7 +149,8 @@ export const pkiAcmeServiceFactory = ({ throw new AcmeMalformedError({ detail: "Protected header is required" }); } const jwk = await getJWK(protectedHeader); - return await importJWK(jwk, protectedHeader.alg); + const key = await importJWK(jwk, protectedHeader.alg); + return key; }); } catch (error) { if (error instanceof AcmeError) { @@ -186,7 +187,7 @@ export const pkiAcmeServiceFactory = ({ const payload = schema ? schema.parse(JSON.parse(textPayload)) : textPayload; return { protectedHeader, - payload + payload: payload as T }; } catch (error) { if (error instanceof AcmeError) { @@ -200,14 +201,14 @@ export const pkiAcmeServiceFactory = ({ } }; - const validateNewAccountJwsPayload = async ({ + const validateNewAccountJwsPayload = ({ url, rawJwsPayload }: { url: URL; rawJwsPayload: TRawJwsPayload; }): Promise> => { - return await validateJwsPayload({ + return validateJwsPayload({ url, rawJwsPayload, getJWK: async (protectedHeader) => { @@ -355,11 +356,8 @@ export const pkiAcmeServiceFactory = ({ const eabSecret = await kmsDecryptor({ cipherTextBlob: profile.acmeConfig!.encryptedEabSecret! }); const { eabPayload, eabProtectedHeader } = await (async () => { try { - const { payload: eabPayload, protectedHeader: eabProtectedHeader } = await flattenedVerify( - externalAccountBinding, - eabSecret - ); - return { eabPayload, eabProtectedHeader }; + const result = await flattenedVerify(externalAccountBinding, eabSecret); + return { eabPayload: result.payload, eabProtectedHeader: result.protectedHeader }; } catch (error) { if (error instanceof errors.JWSSignatureVerificationFailed) { throw new AcmeMalformedError({ detail: "Invalid external account binding JWS signature" }); From e02afc5eefed6b60c53518824c172c78d9de9978 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 16:15:00 -0800 Subject: [PATCH 276/294] Linter --- backend/src/ee/services/pki-acme/pki-acme-errors.ts | 2 ++ backend/src/ee/services/pki-acme/pki-acme-order-dal.ts | 2 +- backend/src/ee/services/pki-acme/pki-acme-types.ts | 5 ++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-errors.ts b/backend/src/ee/services/pki-acme/pki-acme-errors.ts index ed075d63b..5872addae 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-errors.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-errors.ts @@ -3,6 +3,8 @@ * https://datatracker.ietf.org/doc/html/rfc8555#section-6.7 */ +/* eslint-disable max-classes-per-file */ + // RFC 8555 Section 6.7 - Error Types export enum AcmeErrorType { AccountDoesNotExist = "accountDoesNotExist", diff --git a/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts index ca02b09bb..bb3671daa 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts @@ -1,7 +1,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TPkiAcmeAuths, TPkiAcmeOrderAuths } from "@app/db/schemas"; +import { TableName } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index e9154fb5d..3132c9812 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -1,6 +1,5 @@ import { z } from "zod"; -import { TPkiAcmeChallenges } from "@app/db/schemas"; import { JWSHeaderParameters } from "jose"; import { AcmeOrderResourceSchema, @@ -51,7 +50,7 @@ export type TAcmeResponse = { export type TPkiAcmeServiceFactory = { validateJwsPayload: < - TSchema extends z.ZodSchema | undefined = undefined, + TSchema extends z.ZodSchema | undefined = undefined, T = TSchema extends z.ZodSchema ? R : string >({ url, @@ -72,7 +71,7 @@ export type TPkiAcmeServiceFactory = { rawJwsPayload: TRawJwsPayload; }) => Promise>; validateExistingAccountJwsPayload: < - TSchema extends z.ZodSchema | undefined = undefined, + TSchema extends z.ZodSchema | undefined = undefined, T = TSchema extends z.ZodSchema ? R : string >({ url, From a7b6c97dbc26994c6e2fb533a3fc184572108da6 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 16:17:29 -0800 Subject: [PATCH 277/294] Try to fix tests --- .../certificate-profile/certificate-profile-service.test.ts | 1 + .../services/certificate-profile/certificate-profile-types.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/services/certificate-profile/certificate-profile-service.test.ts b/backend/src/services/certificate-profile/certificate-profile-service.test.ts index bb869d16f..e79c0cf3e 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.test.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.test.ts @@ -247,6 +247,7 @@ describe("CertificateProfileService", () => { certificateTemplateId: "template-123", apiConfigId: "api-config-123", estConfigId: null, + acmeConfig: null, projectId: "project-123" }, undefined diff --git a/backend/src/services/certificate-profile/certificate-profile-types.ts b/backend/src/services/certificate-profile/certificate-profile-types.ts index 82ba0f378..4a3339857 100644 --- a/backend/src/services/certificate-profile/certificate-profile-types.ts +++ b/backend/src/services/certificate-profile/certificate-profile-types.ts @@ -29,7 +29,7 @@ export type TCertificateProfileUpdate = Omit Date: Tue, 4 Nov 2025 16:29:42 -0800 Subject: [PATCH 278/294] Lint --- backend/src/ee/routes/v1/pki-acme-router.ts | 5 +- .../pki-acme/pki-acme-challenge-service.ts | 10 ++- .../services/pki-acme/pki-acme-order-dal.ts | 4 +- .../ee/services/pki-acme/pki-acme-service.ts | 87 +++++++++---------- 4 files changed, 55 insertions(+), 51 deletions(-) diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index a66967bbc..8e300f8f6 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -33,8 +33,9 @@ export interface MyRequestInterface { export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { const validateExistingAccount = async < + // eslint-disable-next-line @typescript-eslint/no-explicit-any R extends FastifyRequest, - TSchema extends z.ZodSchema | undefined = undefined, + TSchema extends z.ZodSchema | undefined = undefined, T = TSchema extends z.ZodSchema ? R : string >({ req, @@ -70,7 +71,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { if (!strBody) { done(null, undefined); } - const json: unknown = JSON.parse(strBody as string); + const json = JSON.parse(strBody as string); done(null, json); } catch (err) { const error = err as Error; diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts index 52a277c31..9567072a1 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts @@ -2,7 +2,12 @@ import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal"; -import { AcmeConnectionError, AcmeDnsFailureError, AcmeIncorrectResponseError } from "./pki-acme-errors"; +import { + AcmeConnectionError, + AcmeDnsFailureError, + AcmeIncorrectResponseError, + AcmeServerInternalError +} from "./pki-acme-errors"; import { AcmeAuthStatus, AcmeChallengeStatus, AcmeChallengeType } from "./pki-acme-schemas"; import { TPkiAcmeChallengeServiceFactory } from "./pki-acme-types"; @@ -23,7 +28,7 @@ export const pkiAcmeChallengeServiceFactory = ({ const appCfg = getConfig(); const validateChallengeResponse = async (challengeId: string): Promise => { - const error = await acmeChallengeDAL.transaction(async (tx) => { + const error: Error | undefined = await acmeChallengeDAL.transaction(async (tx) => { logger.info({ challengeId }, "Validating ACME challenge response"); const challenge = await acmeChallengeDAL.findByIdForChallengeValidation(challengeId, tx); if (!challenge) { @@ -98,6 +103,7 @@ export const pkiAcmeChallengeServiceFactory = ({ logger.error(exp, "Error validating ACME challenge response"); } else { logger.error(exp, "Unknown error validating ACME challenge response"); + return new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" }); } return exp; } diff --git a/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts index bb3671daa..5aab0be63 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-order-dal.ts @@ -48,8 +48,8 @@ export const pkiAcmeOrderDALFactory = (db: TDbClient) => { label: "authorizations" as const, mapper: ({ authId, identifierType, identifierValue, authExpiresAt }) => ({ id: authId, - identifierType: identifierType, - identifierValue: identifierValue, + identifierType, + identifierValue, expiresAt: authExpiresAt }) } diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 9f848748e..91e444e8d 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -441,14 +441,13 @@ export const pkiAcmeServiceFactory = ({ const deactivateAcmeAccount = async ({ profileId, - accountId, - payload: { status } = { status: "deactivated" } + accountId }: { profileId: string; accountId: string; payload?: TDeactivateAcmeAccountPayload; }): Promise> => { - const profile = await validateAcmeProfile(profileId); + await validateAcmeProfile(profileId); // FIXME: Implement ACME account deactivation return { status: 200, @@ -494,36 +493,35 @@ export const pkiAcmeServiceFactory = ({ ); const authorizations: TPkiAcmeAuths[] = await Promise.all( payload.identifiers.map(async (identifier) => { - if (identifier.type === AcmeIdentifierType.DNS) { - // TODO: reuse existing authorizations for this identifier if they exist - const auth = await acmeAuthDAL.create( - { - accountId: account.id, - status: AcmeAuthStatus.Pending, - identifierType: identifier.type, - identifierValue: identifier.value, - // RFC 8555 suggests a token with at least 128 bits of entropy - // We are using 256 bits of entropy here, should be enough for now - // ref: https://datatracker.ietf.org/doc/html/rfc8555#section-11.3 - token: crypto.randomBytes(32).toString("base64url"), - // TODO: read config from the profile to get the expiration time instead - expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) - }, - tx - ); - // TODO: support other challenge types here. Currently only HTTP-01 is supported. - await acmeChallengeDAL.create( - { - authId: auth.id, - status: AcmeChallengeStatus.Pending, - type: AcmeChallengeType.HTTP_01 - }, - tx - ); - return auth; - } else { + if (identifier.type !== AcmeIdentifierType.DNS) { throw new AcmeUnsupportedIdentifierError({ detail: "Only DNS identifiers are supported" }); } + // TODO: reuse existing authorizations for this identifier if they exist + const auth = await acmeAuthDAL.create( + { + accountId: account.id, + status: AcmeAuthStatus.Pending, + identifierType: identifier.type, + identifierValue: identifier.value, + // RFC 8555 suggests a token with at least 128 bits of entropy + // We are using 256 bits of entropy here, should be enough for now + // ref: https://datatracker.ietf.org/doc/html/rfc8555#section-11.3 + token: crypto.randomBytes(32).toString("base64url"), + // TODO: read config from the profile to get the expiration time instead + expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) + }, + tx + ); + // TODO: support other challenge types here. Currently only HTTP-01 is supported. + await acmeChallengeDAL.create( + { + authId: auth.id, + status: AcmeChallengeStatus.Pending, + type: AcmeChallengeType.HTTP_01 + }, + tx + ); + return auth; }) ); @@ -591,13 +589,13 @@ export const pkiAcmeServiceFactory = ({ } if (order.status === AcmeOrderStatus.Ready) { const { order: updatedOrder, error } = await acmeOrderDAL.transaction(async (tx) => { - const order = (await acmeOrderDAL.findByIdForFinalization(orderId, tx))!; + const finalizingOrder = (await acmeOrderDAL.findByIdForFinalization(orderId, tx))!; // TODO: ideally, this should be doen with onRequest: verifyAuth([AuthMode.ACME_JWS_SIGNATURE]), instead? const { ownerOrgId: actorOrgId } = (await certificateProfileDAL.findByIdWithOwnerOrgId(profileId, tx))!; - if (order.status !== AcmeOrderStatus.Ready) { + if (finalizingOrder.status !== AcmeOrderStatus.Ready) { throw new AcmeOrderNotReadyError({ message: "ACME order is not ready" }); } - if (order.expiresAt < new Date()) { + if (finalizingOrder.expiresAt < new Date()) { throw new AcmeOrderNotReadyError({ message: "ACME order has expired" }); } const { csr } = payload; @@ -612,8 +610,8 @@ export const pkiAcmeServiceFactory = ({ actorOrgId, profileId, csr, - notBefore: order.notBefore ? new Date(order.notBefore) : undefined, - notAfter: order.notAfter ? new Date(order.notAfter) : undefined, + notBefore: finalizingOrder.notBefore ? new Date(finalizingOrder.notBefore) : undefined, + notAfter: finalizingOrder.notAfter ? new Date(finalizingOrder.notAfter) : undefined, validity: { // TODO: read config from the profile to get the expiration time instead ttl: (24 * 60 * 60 * 1000).toString() @@ -630,20 +628,20 @@ export const pkiAcmeServiceFactory = ({ }, tx ); - } catch (error) { + } catch (exp) { await acmeOrderDAL.updateById( orderId, { csr, status: AcmeOrderStatus.Invalid, - error: error instanceof Error ? error.message : "Unknown error" + error: exp instanceof Error ? exp.message : "Unknown error" }, tx ); - logger.error(error, "Failed to sign certificate"); + logger.error(exp, "Failed to sign certificate"); // TODO: audit log the error - if (error instanceof BadRequestError) { - errorToReturn = new AcmeBadCSRError({ detail: `Invalid CSR: ${error.message}` }); + if (exp instanceof BadRequestError) { + errorToReturn = new AcmeBadCSRError({ detail: `Invalid CSR: ${exp.message}` }); } else { errorToReturn = new AcmeServerInternalError({ detail: "Failed to sign certificate with internal error" }); } @@ -710,15 +708,14 @@ export const pkiAcmeServiceFactory = ({ }); const certificateChain = decryptedCertChain.toString(); + const certLeaf = certObj.toString("pem").trim().replace("\n", "\r\n"); + const certChain = certificateChain.trim().replace("\n", "\r\n"); return { status: 200, body: - certObj.toString("pem").trim().replace("\n", "\r\n") + - "\r\n" + - certificateChain.trim().replace("\n", "\r\n") + // The final line is needed, otherwise some clients will not parse the certificate chain correctly // ref: https://github.com/certbot/certbot/blob/4d5d5f7ae8164884c841969e46caed8db1ad34af/certbot/src/certbot/crypto_util.py#L506-L514 - "\r\n", + `${certLeaf}\r\n${certChain}\r\n`, headers: { Location: buildUrl(profileId, `/orders/${orderId}/certificate`), Link: `<${buildUrl(profileId, "/directory")}>;rel="index"` From 694a13fce2ff242e1ab9a2a4c8f79532dc1de954 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 16:35:33 -0800 Subject: [PATCH 279/294] More linter --- .../src/ee/services/pki-acme/pki-acme-challenge-service.ts | 4 +++- backend/src/ee/services/pki-acme/pki-acme-service.ts | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts index 9567072a1..f266aef98 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts @@ -86,7 +86,7 @@ export const pkiAcmeChallengeServiceFactory = ({ const { cause } = exp; let errors: Error[] = []; if (cause instanceof AggregateError) { - errors = cause.errors; + errors = cause.errors as Error[]; } else if (cause instanceof Error) { errors = [cause]; } @@ -97,6 +97,8 @@ export const pkiAcmeChallengeServiceFactory = ({ return new AcmeConnectionError({ message: "Connection refused" }); } else if (fetchError.code === "ENOTFOUND" || fetchError.message.includes("ENOTFOUND")) { return new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)" }); + } else { + return new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" }); } } } else if (exp instanceof Error) { diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 91e444e8d..dfde68891 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -222,6 +222,7 @@ export const pkiAcmeServiceFactory = ({ }; const validateExistingAccountJwsPayload = async < + // eslint-disable-next-line @typescript-eslint/no-explicit-any TSchema extends z.ZodSchema | undefined = undefined, T = TSchema extends z.ZodSchema ? R : string >({ @@ -385,7 +386,7 @@ export const pkiAcmeServiceFactory = ({ // Make sure the JWK in the EAB payload matches the one provided in the outer JWS payload const decoder = new TextDecoder(); const decodedEabPayload = decoder.decode(eabPayload); - const eabJWK = JSON.parse(decodedEabPayload); + const eabJWK = JSON.parse(decodedEabPayload) as JsonWebKey; const eabPayloadJwkThumbprint = await calculateJwkThumbprint(eabJWK, "sha256"); if (eabPayloadJwkThumbprint !== publicKeyThumbprint) { throw new AcmeBadPublicKeyError({ From cb2810b263bdfe9cd457b31d12fabaf1d740bcbc Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 16:39:50 -0800 Subject: [PATCH 280/294] More linter --- backend/src/ee/routes/v1/pki-acme-router.ts | 4 ++-- .../src/ee/services/pki-acme/pki-acme-challenge-service.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index 8e300f8f6..aa8f23766 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -44,7 +44,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { req: R; schema?: TSchema; }): Promise> => { - return await server.services.pkiAcme.validateExistingAccountJwsPayload({ + return server.services.pkiAcme.validateExistingAccountJwsPayload({ url: new URL(req.url, `${req.protocol}://${req.hostname}`), profileId: (req.params as { profileId: string }).profileId, rawJwsPayload: req.body as TRawJwsPayload, @@ -71,7 +71,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { if (!strBody) { done(null, undefined); } - const json = JSON.parse(strBody as string); + const json = JSON.parse(strBody); done(null, json); } catch (err) { const error = err as Error; diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts index f266aef98..cbc6abbab 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts @@ -90,6 +90,7 @@ export const pkiAcmeChallengeServiceFactory = ({ } else if (cause instanceof Error) { errors = [cause]; } + // eslint-disable-next-line no-unreachable-loop for (const err of errors) { // TODO: handle multiple errors, return a compound error instead of just the first error const fetchError = err as FetchError; From 62dbd3248a5c3e0895feccffd5b671e8af4e70c2 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 16:57:07 -0800 Subject: [PATCH 281/294] More lint --- backend/src/ee/routes/v1/pki-acme-router.ts | 4 ++-- .../ee/services/pki-acme/pki-acme-challenge-dal.ts | 14 ++++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index aa8f23766..b1419c086 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -36,7 +36,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any R extends FastifyRequest, TSchema extends z.ZodSchema | undefined = undefined, - T = TSchema extends z.ZodSchema ? R : string + T = TSchema extends z.ZodSchema ? U : string >({ req, schema @@ -71,7 +71,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { if (!strBody) { done(null, undefined); } - const json = JSON.parse(strBody); + const json: unknown = JSON.parse(strBody); done(null, json); } catch (err) { const error = err as Error; diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts index 424651aea..a07dc401d 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts @@ -35,8 +35,9 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { ); // Update status for pending orders that have all auths valid await (tx || db)(TableName.PkiAcmeOrder) - .whereIn("id", (qb) => { - qb.select("o2.id") + .whereIn("id", async (qb) => { + void (await qb + .select("o2.id") .from({ o2: TableName.PkiAcmeOrder }) .join({ oa2: TableName.PkiAcmeOrderAuth }, "o2.id", "oa2.orderId") .join({ a2: TableName.PkiAcmeAuth }, "oa2.authId", "a2.id") @@ -47,7 +48,7 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { ]) // Only update orders that are pending .where("o2.status", AcmeOrderStatus.Pending) - .whereIn("o2.id", involvedOrderIds); + .whereIn("o2.id", involvedOrderIds)); }) .update({ status: AcmeOrderStatus.Ready }); } @@ -74,8 +75,9 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { if (updatedAuths.length > 0) { // Update status for pending orders that have all auths valid await (tx || db)(TableName.PkiAcmeOrder) - .whereIn("id", (qb) => { - qb.select("o.id") + .whereIn("id", async (qb) => { + void (await qb + .select("o.id") .from({ o: TableName.PkiAcmeOrder }) .join(TableName.PkiAcmeOrderAuth, "o.id", `${TableName.PkiAcmeOrderAuth}.orderId`) .join(TableName.PkiAcmeAuth, `${TableName.PkiAcmeOrderAuth}.authId`, `${TableName.PkiAcmeAuth}.id`) @@ -84,7 +86,7 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { .whereIn( `${TableName.PkiAcmeAuth}.id`, updatedAuths.map((auth) => auth.id) - ); + )); }) .update({ status: AcmeOrderStatus.Invalid }); } From 9ad9d5cc32de213985996578511040ed538ff839 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 16:58:14 -0800 Subject: [PATCH 282/294] Fix test --- .../certificate-profile/certificate-profile-service.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/services/certificate-profile/certificate-profile-service.test.ts b/backend/src/services/certificate-profile/certificate-profile-service.test.ts index e79c0cf3e..ffca3c1cb 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.test.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.test.ts @@ -247,7 +247,7 @@ describe("CertificateProfileService", () => { certificateTemplateId: "template-123", apiConfigId: "api-config-123", estConfigId: null, - acmeConfig: null, + acmeConfigId: null, projectId: "project-123" }, undefined From 98ecd18e0d4222a634dcce9d1e323b9898b33b33 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 17:21:44 -0800 Subject: [PATCH 283/294] Fix linter --- .../ee/services/pki-acme/pki-acme-challenge-dal.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts index a07dc401d..948f4af6e 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts @@ -35,8 +35,8 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { ); // Update status for pending orders that have all auths valid await (tx || db)(TableName.PkiAcmeOrder) - .whereIn("id", async (qb) => { - void (await qb + .whereIn("id", (qb) => { + void qb .select("o2.id") .from({ o2: TableName.PkiAcmeOrder }) .join({ oa2: TableName.PkiAcmeOrderAuth }, "o2.id", "oa2.orderId") @@ -48,7 +48,7 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { ]) // Only update orders that are pending .where("o2.status", AcmeOrderStatus.Pending) - .whereIn("o2.id", involvedOrderIds)); + .whereIn("o2.id", involvedOrderIds); }) .update({ status: AcmeOrderStatus.Ready }); } @@ -75,8 +75,8 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { if (updatedAuths.length > 0) { // Update status for pending orders that have all auths valid await (tx || db)(TableName.PkiAcmeOrder) - .whereIn("id", async (qb) => { - void (await qb + .whereIn("id", (qb) => { + void qb .select("o.id") .from({ o: TableName.PkiAcmeOrder }) .join(TableName.PkiAcmeOrderAuth, "o.id", `${TableName.PkiAcmeOrderAuth}.orderId`) @@ -86,7 +86,7 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { .whereIn( `${TableName.PkiAcmeAuth}.id`, updatedAuths.map((auth) => auth.id) - )); + ); }) .update({ status: AcmeOrderStatus.Invalid }); } From df175b66de7d01a09b709b7cb86347790a0698ca Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 17:23:19 -0800 Subject: [PATCH 284/294] Fix linter --- .../ee/services/pki-acme/pki-acme-challenge-service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts index cbc6abbab..6b820032b 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts @@ -96,11 +96,11 @@ export const pkiAcmeChallengeServiceFactory = ({ const fetchError = err as FetchError; if (fetchError.code === "ECONNREFUSED" || fetchError.message.includes("ECONNREFUSED")) { return new AcmeConnectionError({ message: "Connection refused" }); - } else if (fetchError.code === "ENOTFOUND" || fetchError.message.includes("ENOTFOUND")) { - return new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)" }); - } else { - return new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" }); } + if (fetchError.code === "ENOTFOUND" || fetchError.message.includes("ENOTFOUND")) { + return new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)" }); + } + return new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" }); } } else if (exp instanceof Error) { logger.error(exp, "Error validating ACME challenge response"); From eebf62f30c0c6d0f63b4fa51a2bf021abee22570 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 17:40:13 -0800 Subject: [PATCH 285/294] Fix linter --- frontend/src/hooks/api/certificateProfiles/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/hooks/api/certificateProfiles/types.ts b/frontend/src/hooks/api/certificateProfiles/types.ts index 7acb6fef9..c2b38e8e8 100644 --- a/frontend/src/hooks/api/certificateProfiles/types.ts +++ b/frontend/src/hooks/api/certificateProfiles/types.ts @@ -58,7 +58,7 @@ export type TCreateCertificateProfileDTO = { autoRenew?: boolean; renewBeforeDays?: number; }; - acmeConfig?: {}; + acmeConfig?: unknown; }; export type TUpdateCertificateProfileDTO = { @@ -74,7 +74,7 @@ export type TUpdateCertificateProfileDTO = { autoRenew?: boolean; renewBeforeDays?: number; }; - acmeConfig?: {}; + acmeConfig?: unknown; }; export type TDeleteCertificateProfileDTO = { From d4c9c4464b74b35aefc8176e59495b7fdeb21506 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 19:48:52 -0800 Subject: [PATCH 286/294] Fix review feedbacks --- .../pki-acme/pki-acme-challenge-service.ts | 5 +++++ .../ee/services/pki-acme/pki-acme-service.ts | 7 ++++--- backend/src/server/routes/index.ts | 1 + .../certificate-v3-service.test.ts | 6 ++++++ .../certificate-v3/certificate-v3-service.ts | 17 ++++++++++++++--- 5 files changed, 30 insertions(+), 6 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts index 6b820032b..f4100c6aa 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts @@ -1,5 +1,6 @@ import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { isPrivateIp } from "@app/lib/ip/ipRange"; import { logger } from "@app/lib/logger"; import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal"; import { @@ -53,6 +54,10 @@ export const pkiAcmeChallengeServiceFactory = ({ throw new BadRequestError({ message: "Only HTTP-01 challenges are supported for now" }); } let host = challenge.auth.identifierValue; + // check if host is a private ip address + if (isPrivateIp(host)) { + throw new BadRequestError({ message: "Private IP addresses are not allowed" }); + } if (appCfg.isAcmeDevelopmentMode && appCfg.ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES[host]) { host = appCfg.ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES[host]; logger.warn( diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index dfde68891..4f4eb93ad 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -7,6 +7,7 @@ import { TCertificateProfileDALFactory } from "@app/services/certificate-profile import * as x509 from "@peculiar/x509"; import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; +import { isPrivateIp } from "@app/lib/ip/ipRange"; import { ActorType } from "@app/services/auth/auth-type"; import { EnrollmentType, @@ -497,7 +498,9 @@ export const pkiAcmeServiceFactory = ({ if (identifier.type !== AcmeIdentifierType.DNS) { throw new AcmeUnsupportedIdentifierError({ detail: "Only DNS identifiers are supported" }); } - // TODO: reuse existing authorizations for this identifier if they exist + if (isPrivateIp(identifier.value)) { + throw new AcmeUnsupportedIdentifierError({ detail: "Private IP addresses are not allowed" }); + } const auth = await acmeAuthDAL.create( { accountId: account.id, @@ -600,8 +603,6 @@ export const pkiAcmeServiceFactory = ({ throw new AcmeOrderNotReadyError({ message: "ACME order has expired" }); } const { csr } = payload; - // TODO: validate the CSR and return badCSR error if it's invalid - // TODO: this should be the same transaction? let errorToReturn: Error | undefined; try { const { certificateId } = await certificateV3Service.signCertificateFromProfile({ diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index cd8fac508..961c0a940 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -2169,6 +2169,7 @@ export const registerRoutes = async ( certificateAuthorityDAL, certificateProfileDAL, certificateTemplateV2Service, + acmeAccountDAL, internalCaService: internalCertificateAuthorityService, permissionService, certificateSyncDAL, diff --git a/backend/src/services/certificate-v3/certificate-v3-service.test.ts b/backend/src/services/certificate-v3/certificate-v3-service.test.ts index 36823671f..67c3b268b 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.test.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.test.ts @@ -30,6 +30,7 @@ import { extractCertificateRequestFromCSR } from "../certificate-common/certificate-csr-utils"; import { certificateV3ServiceFactory, TCertificateV3ServiceFactory } from "./certificate-v3-service"; +import { TPkiAcmeAccountDALFactory } from "@app/ee/services/pki-acme/pki-acme-account-dal"; vi.mock("../certificate-common/certificate-csr-utils", () => ({ extractCertificateRequestFromCSR: vi.fn(), @@ -69,6 +70,10 @@ describe("CertificateV3Service", () => { getTemplateV2ById: vi.fn() }; + const mockAcmeAccountDAL: Pick = { + findById: vi.fn() + }; + const mockInternalCaService: Pick = { signCertFromCa: vi.fn(), @@ -132,6 +137,7 @@ describe("CertificateV3Service", () => { certificateAuthorityDAL: mockCertificateAuthorityDAL, certificateProfileDAL: mockCertificateProfileDAL, certificateTemplateV2Service: mockCertificateTemplateV2Service, + acmeAccountDAL: mockAcmeAccountDAL, internalCaService: mockInternalCaService, permissionService: mockPermissionService, certificateSyncDAL: { diff --git a/backend/src/services/certificate-v3/certificate-v3-service.ts b/backend/src/services/certificate-v3/certificate-v3-service.ts index 5ccd02698..72346c55c 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.ts @@ -64,12 +64,14 @@ import { TSignCertificateFromProfileDTO, TUpdateRenewalConfigDTO } from "./certificate-v3-types"; +import { TPkiAcmeAccountDALFactory } from "@app/ee/services/pki-acme/pki-acme-account-dal"; type TCertificateV3ServiceFactoryDep = { certificateDAL: Pick; certificateSecretDAL: Pick; certificateAuthorityDAL: Pick; certificateProfileDAL: Pick; + acmeAccountDAL: Pick; certificateTemplateV2Service: Pick< TCertificateTemplateV2ServiceFactory, "validateCertificateRequest" | "getTemplateV2ById" @@ -93,6 +95,7 @@ const validateProfileAndPermissions = async ( actorAuthMethod: ActorAuthMethod, actorOrgId: string, certificateProfileDAL: Pick, + acmeAccountDAL: Pick, permissionService: Pick, requiredEnrollmentType: EnrollmentType ) => { @@ -107,10 +110,16 @@ const validateProfileAndPermissions = async ( }); } - // XXX: NOT SURE IF THIS IS SECURE TO BY PASS THE PERMISSION CHECK FOR ACME ACCOUNTS - // may need to consider this carefully - // TODO: check actor/profile ownership as well if (actor === ActorType.ACME_ACCOUNT && requiredEnrollmentType === EnrollmentType.ACME) { + const account = await acmeAccountDAL.findById(actorId); + if (!account) { + throw new NotFoundError({ message: "ACME account not found" }); + } + if (account.profileId !== profile.id) { + throw new ForbiddenRequestError({ + message: "ACME account is not associated with this profile" + }); + } return profile; } @@ -343,6 +352,7 @@ export const certificateV3ServiceFactory = ({ certificateSecretDAL, certificateAuthorityDAL, certificateProfileDAL, + acmeAccountDAL, certificateTemplateV2Service, internalCaService, permissionService, @@ -365,6 +375,7 @@ export const certificateV3ServiceFactory = ({ actorAuthMethod, actorOrgId, certificateProfileDAL, + acmeAccountDAL, permissionService, EnrollmentType.API ); From 00b654e07660c40b1e4b9a20726af1b2ab348d42 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 20:00:28 -0800 Subject: [PATCH 287/294] Add timeout for fetch --- .../services/pki-acme/pki-acme-challenge-service.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts index f4100c6aa..b64c43393 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts @@ -68,11 +68,12 @@ export const pkiAcmeChallengeServiceFactory = ({ const challengeUrl = new URL(`/.well-known/acme-challenge/${challenge.auth.token}`, `http://${host}`); logger.info({ challengeUrl }, "Performing ACME HTTP-01 challenge validation"); try { + // TODO: read config from the profile to get the timeout instead + const timeoutMs = 10000; // 10 seconds // Notice: well, we are in a transaction, ideally we should not hold transaction and perform // a long running operation for long time. But assuming we are not performing a tons of // challenge validation at the same time, it should be fine. - // TODO: bound it with timeout of the fetch request - const challengeResponse = await fetch(challengeUrl); + const challengeResponse = await fetch(challengeUrl, { signal: AbortSignal.timeout(timeoutMs) }); if (challengeResponse.status !== 200) { throw new BadRequestError({ message: "ACME challenge response is not 200" }); } @@ -107,6 +108,13 @@ export const pkiAcmeChallengeServiceFactory = ({ } return new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" }); } + } else if (exp instanceof DOMException) { + if (exp.name === "TimeoutError") { + logger.error(exp, "Connection timed out while validating ACME challenge response"); + return new AcmeConnectionError({ message: "Connection timed out" }); + } + logger.error(exp, "Unknown error validating ACME challenge response"); + return new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" }); } else if (exp instanceof Error) { logger.error(exp, "Error validating ACME challenge response"); } else { From dec7273652a6c76bb180d20c519a6a8377e90764 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 20:03:55 -0800 Subject: [PATCH 288/294] Code style --- backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts index b64c43393..a058241c4 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts @@ -69,7 +69,7 @@ export const pkiAcmeChallengeServiceFactory = ({ logger.info({ challengeUrl }, "Performing ACME HTTP-01 challenge validation"); try { // TODO: read config from the profile to get the timeout instead - const timeoutMs = 10000; // 10 seconds + const timeoutMs = 10 * 1000; // 10 seconds // Notice: well, we are in a transaction, ideally we should not hold transaction and perform // a long running operation for long time. But assuming we are not performing a tons of // challenge validation at the same time, it should be fine. From 136d755577dadd527c6ec35ee6e6801e336cdf39 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 4 Nov 2025 20:27:50 -0800 Subject: [PATCH 289/294] Fix broken stuff --- backend/src/services/certificate-v3/certificate-v3-service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/services/certificate-v3/certificate-v3-service.ts b/backend/src/services/certificate-v3/certificate-v3-service.ts index 72346c55c..7f4c6c637 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.ts @@ -512,6 +512,7 @@ export const certificateV3ServiceFactory = ({ actorAuthMethod, actorOrgId, certificateProfileDAL, + acmeAccountDAL, permissionService, enrollmentType ); @@ -614,6 +615,7 @@ export const certificateV3ServiceFactory = ({ actorAuthMethod, actorOrgId, certificateProfileDAL, + acmeAccountDAL, permissionService, EnrollmentType.API ); From d2c8904e8fbcd8dd741c402c59a494d49ee9d2e3 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 09:16:27 -0800 Subject: [PATCH 290/294] Address review feedbacks --- backend/src/ee/services/pki-acme/pki-acme-errors.ts | 6 +++--- backend/src/ee/services/pki-acme/pki-acme-service.ts | 11 +++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-errors.ts b/backend/src/ee/services/pki-acme/pki-acme-errors.ts index 5872addae..febce5e81 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-errors.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-errors.ts @@ -14,13 +14,13 @@ export enum AcmeErrorType { BadPublicKey = "badPublicKey", BadRevocationReason = "badRevocationReason", BadSignatureAlgorithm = "badSignatureAlgorithm", - CAA = "CAA", + CAA = "caa", Compound = "compound", Connection = "connection", - DNS = "DNS", + DNS = "dns", ExternalAccountRequired = "externalAccountRequired", IncorrectResponse = "incorrectResponse", - IncorrectContact = "incorrectContact", + InvalidContact = "invalidContact", Malformed = "malformed", OrderNotReady = "orderNotReady", RateLimited = "rateLimited", diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 4f4eb93ad..e3a82000d 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -614,10 +614,13 @@ export const pkiAcmeServiceFactory = ({ csr, notBefore: finalizingOrder.notBefore ? new Date(finalizingOrder.notBefore) : undefined, notAfter: finalizingOrder.notAfter ? new Date(finalizingOrder.notAfter) : undefined, - validity: { - // TODO: read config from the profile to get the expiration time instead - ttl: (24 * 60 * 60 * 1000).toString() - }, + validity: !finalizingOrder.notAfter + ? { + // TODO: read config from the profile to get the expiration time instead + ttl: (24 * 60 * 60 * 1000).toString() + } + : // ttl is not used if notAfter is provided + ({ ttl: "0" } as const), enrollmentType: EnrollmentType.ACME }); // TODO: associate the certificate with the order From b0bdc01b1eb8015422131fef8e810dbf2176431d Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 09:30:52 -0800 Subject: [PATCH 291/294] Fix DNS validation. Using re2 instead --- backend/src/ee/services/pki-acme/pki-acme-schemas.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts index 1d2f95fdf..4c7d6c3c1 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts @@ -1,3 +1,4 @@ +import RE2 from "re2"; import { z } from "zod"; export enum AcmeIdentifierType { @@ -88,9 +89,12 @@ export const CreateAcmeOrderBodySchema = z.object({ identifiers: z.array( z.object({ type: z.enum(Object.values(AcmeIdentifierType) as [string, ...string[]]), - value: z - .string() - .regex(/^(?!-)[A-Za-z0-9-]{1,63}(? { + // DNS label pattern: 1-63 chars, alphanumeric or hyphen, but not starting or ending with hyphen + const labelPattern = new RE2(/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/); + const labels = val.split("."); + return labels.every((label) => label.length >= 1 && label.length <= 63 && labelPattern.test(label)); + }, "Invalid DNS identifier") }) ), notBefore: z.string().optional(), From d7274e7e6374f48bd9909bf06f6d49a0a27009f3 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 09:44:43 -0800 Subject: [PATCH 292/294] Show copy button again --- .../RevealAcmeEabSecretModal.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/RevealAcmeEabSecretModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/RevealAcmeEabSecretModal.tsx index 84163633f..55442e7ba 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/RevealAcmeEabSecretModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/RevealAcmeEabSecretModal.tsx @@ -14,6 +14,8 @@ import { useRevealAcmeEabSecret } from "@app/hooks/api/certificateProfiles/queri import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +const RESET_COPIED_DELAY = 1 * 1000; + type Props = { isOpen: boolean; onClose: () => void; @@ -67,6 +69,9 @@ export const RevealAcmeEabSecretModal = ({ isOpen, onClose, profile }: Props) => onClick={() => { navigator.clipboard.writeText(directoryUrl); setIsAcmeDirectoryUrlCopied.on(); + setTimeout(() => { + setIsAcmeDirectoryUrlCopied.off(); + }, RESET_COPIED_DELAY); }} className="w-10" > @@ -88,6 +93,9 @@ export const RevealAcmeEabSecretModal = ({ isOpen, onClose, profile }: Props) => onClick={() => { navigator.clipboard.writeText(eabKid); setIsEabKidCopied.on(); + setTimeout(() => { + setIsEabKidCopied.off(); + }, RESET_COPIED_DELAY); }} className="w-10" > @@ -109,6 +117,9 @@ export const RevealAcmeEabSecretModal = ({ isOpen, onClose, profile }: Props) => onClick={() => { navigator.clipboard.writeText(eabSecret); setIsEabSecretCopied.on(); + setTimeout(() => { + setIsEabSecretCopied.off(); + }, RESET_COPIED_DELAY); }} className="w-10" > From 198f9558c83beac275bf23e8112ebaad41c0a524 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 10:07:12 -0800 Subject: [PATCH 293/294] Rename UI name --- .../components/CertificateProfilesTab/ProfileRow.tsx | 2 +- .../CertificateProfilesTab/RevealAcmeEabSecretModal.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileRow.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileRow.tsx index 2a06063c4..16286d5e4 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileRow.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileRow.tsx @@ -164,7 +164,7 @@ export const ProfileRow = ({ }} icon={} > - Reveal EAB Secret + Reveal ACME EAB )} {canIssueCertificate && profile.enrollmentType === "api" && ( diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/RevealAcmeEabSecretModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/RevealAcmeEabSecretModal.tsx index 55442e7ba..628e4106b 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/RevealAcmeEabSecretModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/RevealAcmeEabSecretModal.tsx @@ -41,7 +41,7 @@ export const RevealAcmeEabSecretModal = ({ isOpen, onClose, profile }: Props) => }} > {isLoading && ( From 91c85b33d51c89359dd99d33a9d782487f2f82b1 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 10:41:25 -0800 Subject: [PATCH 294/294] Fix icon mis-aligment --- .../components/CertificateProfilesTab/ProfileRow.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileRow.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileRow.tsx index 16286d5e4..1e6f259e1 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileRow.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileRow.tsx @@ -140,7 +140,7 @@ export const ProfileRow = ({ } + icon={} onClick={() => handleCopyId()} > Copy Profile ID @@ -151,7 +151,7 @@ export const ProfileRow = ({ e.stopPropagation(); onEditProfile(profile); }} - icon={} + icon={} > Edit Profile @@ -162,7 +162,7 @@ export const ProfileRow = ({ e.stopPropagation(); onRevealProfileAcmeEabSecret(profile); }} - icon={} + icon={} > Reveal ACME EAB @@ -173,7 +173,7 @@ export const ProfileRow = ({ e.stopPropagation(); handlePopUpToggle("issueCertificate"); }} - icon={} + icon={} > Issue Certificate @@ -184,7 +184,7 @@ export const ProfileRow = ({ e.stopPropagation(); onDeleteProfile(profile); }} - icon={} + icon={} > Delete Profile