From 9e8f3843a632c2cd054857814031dfcb09321156 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 31 Oct 2025 14:13:47 -0700 Subject: [PATCH] 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