diff --git a/src/Appwrite/Event/Certificate.php b/src/Appwrite/Event/Certificate.php index 5f8fe0408c..326ce9518a 100644 --- a/src/Appwrite/Event/Certificate.php +++ b/src/Appwrite/Event/Certificate.php @@ -11,6 +11,7 @@ class Certificate extends Event public const string ACTION_DOMAIN_VERIFICATION = 'verification'; public const string ACTION_GENERATION = 'generation'; protected bool $skipRenewCheck = false; + protected bool $skipRule = false; protected string $action = self::ACTION_GENERATION; protected ?Document $domain = null; protected ?string $validationDomain = null; @@ -94,6 +95,29 @@ class Certificate extends Event return $this->skipRenewCheck; } + /** + * Set if rule lookup and updates should be skipped. + * + * @param bool $skipRule + * @return self + */ + public function setSkipRule(bool $skipRule): self + { + $this->skipRule = $skipRule; + + return $this; + } + + /** + * Return if rule lookup and updates should be skipped. + * + * @return bool + */ + public function getSkipRule(): bool + { + return $this->skipRule; + } + /** * Set action for this certificate event. * @@ -128,6 +152,7 @@ class Certificate extends Event 'project' => $this->project, 'domain' => $this->domain, 'skipRenewCheck' => $this->skipRenewCheck, + 'skipRule' => $this->skipRule, 'validationDomain' => $this->validationDomain, 'action' => $this->action ]; diff --git a/src/Appwrite/Platform/Tasks/SSL.php b/src/Appwrite/Platform/Tasks/SSL.php index 651cb4de11..e35cd88785 100644 --- a/src/Appwrite/Platform/Tasks/SSL.php +++ b/src/Appwrite/Platform/Tasks/SSL.php @@ -23,13 +23,15 @@ class SSL extends Action ->desc('Validate server certificates') ->param('domain', System::getEnv('_APP_DOMAIN', ''), new Hostname(), 'Domain to generate certificate for. If empty, main domain will be used.', true) ->param('skip-check', true, new Boolean(true), 'If DNS and renew check should be skipped. Defaults to true, and when true, all jobs will result in certificate generation attempt.', true) + ->param('skip-rule', false, new Boolean(true), 'If rule lookup and updates should be skipped. Defaults to false. Set to true for domains without a rule in DB (e.g., _APP_DOMAIN).', true) ->inject('queueForCertificates') ->callback($this->action(...)); } - public function action(string $domain, bool|string $skipCheck, Certificate $queueForCertificates): void + public function action(string $domain, bool|string $skipCheck, bool|string $skipRule, Certificate $queueForCertificates): void { $skipCheck = \strval($skipCheck) === 'true'; + $skipRule = \strval($skipRule) === 'true'; Console::success('Scheduling a job to issue a TLS certificate for domain: ' . $domain); @@ -38,6 +40,7 @@ class SSL extends Action 'domain' => $domain ])) ->setSkipRenewCheck($skipCheck) + ->setSkipRule($skipRule) ->trigger(); } } diff --git a/src/Appwrite/Platform/Workers/Certificates.php b/src/Appwrite/Platform/Workers/Certificates.php index bfa6bf87c7..479113a89e 100644 --- a/src/Appwrite/Platform/Workers/Certificates.php +++ b/src/Appwrite/Platform/Workers/Certificates.php @@ -104,6 +104,7 @@ class Certificates extends Action $domain = new Domain($document->getAttribute('domain', '')); $domainType = $document->getAttribute('domainType'); $skipRenewCheck = $payload['skipRenewCheck'] ?? false; + $skipRule = $payload['skipRule'] ?? false; $validationDomain = $payload['validationDomain'] ?? null; $action = $payload['action'] ?? Certificate::ACTION_GENERATION; @@ -115,7 +116,7 @@ class Certificates extends Action break; case Certificate::ACTION_GENERATION: - $this->handleCertificateGenerationAction($domain, $domainType, $dbForPlatform, $queueForMails, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $log, $certificates, $authorization, $skipRenewCheck, $plan, $validationDomain); + $this->handleCertificateGenerationAction($domain, $domainType, $dbForPlatform, $queueForMails, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $log, $certificates, $authorization, $skipRenewCheck, $skipRule, $plan, $validationDomain); break; default: @@ -210,6 +211,7 @@ class Certificates extends Action * @param CertificatesAdapter $certificates * @param ValidatorAuthorization $authorization * @param bool $skipRenewCheck + * @param bool $skipRule Skip rule lookup and updates (e.g., for _APP_DOMAIN) * @param array $plan * @param string|null $validationDomain * @return void @@ -234,6 +236,7 @@ class Certificates extends Action CertificatesAdapter $certificates, ValidatorAuthorization $authorization, bool $skipRenewCheck = false, + bool $skipRule = false, array $plan = [], ?string $validationDomain = null ): void { @@ -266,26 +269,37 @@ class Certificates extends Action * Note: Renewals are checked and scheduled from maintenance worker */ - // Get rule document for domain - // TODO: (@Meldiron) Remove after 1.7.x migration - $rule = System::getEnv('_APP_RULES_FORMAT') === 'md5' - ? $authorization->skip(fn () => $dbForPlatform->getDocument('rules', md5($domain->get()))) - : $authorization->skip(fn () => $dbForPlatform->findOne('rules', [ - Query::equal('domain', [$domain->get()]), - Query::limit(1), - ])); + $rule = new Document(); - // Rule not found (or) not in the expected state - if ($rule->isEmpty() || !\in_array($rule->getAttribute('status'), [RULE_STATUS_CERTIFICATE_GENERATING, RULE_STATUS_VERIFIED])) { - Console::warning('Certificate generation for ' . $domain->get() . ' is skipped as the associated rule is either empty or not in the expected state.'); - return; + if (!$skipRule) { + // Get rule document for domain + // TODO: (@Meldiron) Remove after 1.7.x migration + $rule = System::getEnv('_APP_RULES_FORMAT') === 'md5' + ? $authorization->skip(fn () => $dbForPlatform->getDocument('rules', md5($domain->get()))) + : $authorization->skip(fn () => $dbForPlatform->findOne('rules', [ + Query::equal('domain', [$domain->get()]), + Query::limit(1), + ])); + + // Rule not found (or) not in the expected state + if ($rule->isEmpty() || !\in_array($rule->getAttribute('status'), [RULE_STATUS_CERTIFICATE_GENERATING, RULE_STATUS_VERIFIED])) { + Console::warning('Certificate generation for ' . $domain->get() . ' is skipped as the associated rule is either empty or not in the expected state.'); + return; + } } - // Get associated certificate for the rule - $certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId') ?? ''); + // Get associated certificate for the rule (or by domain if skipRule is true) + if ($skipRule) { + $certificate = $dbForPlatform->findOne('certificates', [ + Query::equal('domain', [$domain->get()]), + Query::limit(1), + ]); + } else { + $certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId') ?? ''); + } - // If we don't have certificate for the rule yet, let's create one. - if ($certificate->isEmpty()) { + // If we don't have certificate yet, let's create one. + if ($certificate === false || $certificate->isEmpty()) { $certificate = new Document(); $certificate->setAttribute('domain', $domain->get()); } @@ -295,9 +309,11 @@ class Certificates extends Action $certificate->setAttribute('logs', "\033[90m[{$date}] \033[97mCertificate generation started. \033[0m\n"); // Persist ASAP so that logs are reset in retry flow and user can see the latest logs on Console. - $certificate = $this->upsertCertificate($rule, $certificate, $dbForPlatform); + $certificate = $this->upsertCertificate($rule, $certificate, $dbForPlatform, $skipRule, $domain->get()); // Ensure certificate is associated with the rule - $rule->setAttribute('certificateId', $certificate->getId()); + if (!$skipRule) { + $rule->setAttribute('certificateId', $certificate->getId()); + } // Validate domain and DNS records. Skip if job is forced if (!$skipRenewCheck) { @@ -316,7 +332,9 @@ class Certificates extends Action // If certificate is generated instantly, we can mark the rule as 'verified'. if ($certificates->isInstantGeneration($domain->get(), $domainType)) { - $rule->setAttribute('status', RULE_STATUS_VERIFIED); + if (!$skipRule) { + $rule->setAttribute('status', RULE_STATUS_VERIFIED); + } $certificate->setAttribute('logs', 'Certificate successfully generated.'); } @@ -341,7 +359,9 @@ class Certificates extends Action ]); // Mark rule as 'unverified' - $rule->setAttribute('status', RULE_STATUS_CERTIFICATE_GENERATION_FAILED); + if (!$skipRule) { + $rule->setAttribute('status', RULE_STATUS_CERTIFICATE_GENERATION_FAILED); + } // Send email to security email $this->notifyError($domain->get(), $e->getMessage(), $attempts, $queueForMails, $plan); @@ -351,7 +371,12 @@ class Certificates extends Action // All actions result in new 'updated' date $certificate->setAttribute('updated', DateTime::now()); // Save certificate document to database - $this->upsertCertificate($rule, $certificate, $dbForPlatform); + $this->upsertCertificate($rule, $certificate, $dbForPlatform, $skipRule, $domain->get()); + + // Skip rule update if skipRule is true (e.g., for _APP_DOMAIN) + if ($skipRule) { + return; + } // Ensure certificate is associated with the rule $rule->setAttribute('certificateId', $certificate->getId()); @@ -366,6 +391,8 @@ class Certificates extends Action * @param Document $rule Rule associated with the domain * @param Document $certificate Certificate document that we need to save * @param Database $dbForPlatform Database connection for console + * @param bool $skipRule Whether to skip rule and look up certificate by domain + * @param string $domain Domain name for certificate lookup when skipRule is true * @return Document * @throws \Utopia\Database\Exception * @throws Authorization @@ -376,11 +403,21 @@ class Certificates extends Action Document $rule, Document $certificate, Database $dbForPlatform, + bool $skipRule = false, + string $domain = '', ): Document { // Decide whether update (or) insert is needed - $existingCertificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId') ?? ''); + // When skipRule is true, look up certificate by domain name + if ($skipRule) { + $existingCertificate = $dbForPlatform->findOne('certificates', [ + Query::equal('domain', [$domain]), + Query::limit(1), + ]); + } else { + $existingCertificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId') ?? ''); + } - if ($existingCertificate->isEmpty()) { + if ($existingCertificate === false || $existingCertificate->isEmpty()) { $certificate->removeAttribute('$sequence'); $certificate = $dbForPlatform->createDocument('certificates', $certificate); } else {