Skip to main content

postrust_proxy/saas/
manager.rs

1//! Domain management service.
2//!
3//! High-level service for managing custom domains, routes, and upstreams.
4
5use crate::error::{ProxyError, ProxyResult};
6use crate::saas::db;
7use crate::saas::options::{DomainPolicy, UploadTrust};
8use crate::saas::types::*;
9use crate::saas::validation;
10use crate::saas::verification::DomainVerificationService;
11use rand::Rng;
12use sqlx::PgPool;
13use std::sync::Arc;
14use uuid::Uuid;
15
16/// Domain management service.
17pub struct DomainManager {
18    pool: PgPool,
19    verification_service: Arc<DomainVerificationService>,
20    policy: DomainPolicy,
21}
22
23impl DomainManager {
24    /// Create a new domain manager with the default [`DomainPolicy`].
25    pub fn new(pool: PgPool, verification_service: Arc<DomainVerificationService>) -> Self {
26        Self::with_policy(pool, verification_service, DomainPolicy::default())
27    }
28
29    /// Create a domain manager with an explicit policy.
30    pub fn with_policy(
31        pool: PgPool,
32        verification_service: Arc<DomainVerificationService>,
33        policy: DomainPolicy,
34    ) -> Self {
35        Self {
36            pool,
37            verification_service,
38            policy,
39        }
40    }
41
42    // =========================================================================
43    // Domain Management
44    // =========================================================================
45
46    /// Create a new domain for a tenant.
47    ///
48    /// The name is normalised first and stored in that form, so that the
49    /// uniqueness check, the Host-header match and the certificate all see one
50    /// spelling. A reserved name is refused outright.
51    pub async fn create_domain(
52        &self,
53        tenant_id: Uuid,
54        mut req: CreateDomainRequest,
55    ) -> ProxyResult<DomainResponse> {
56        req.domain = validation::normalize_domain(&req.domain)?;
57        if self.policy.reserved.is_reserved(&req.domain) {
58            return Err(ProxyError::Validation(format!(
59                "{} is reserved and cannot be registered",
60                req.domain
61            )));
62        }
63
64        let verification_token = generate_verification_token();
65        let expected_value = format!("postrust-verify={}", verification_token);
66
67        let domain = db::register_domain(
68            &self.pool,
69            tenant_id,
70            &req,
71            &verification_token,
72            &expected_value,
73            self.policy.pending_ttl,
74        )
75        .await?;
76
77        // Generate verification instructions
78        let instructions = match domain.verification_method {
79            VerificationMethod::Dns => {
80                VerificationInstructions::dns(&domain.domain, &verification_token)
81            }
82            VerificationMethod::Http => {
83                VerificationInstructions::http(&domain.domain, &verification_token)
84            }
85        };
86
87        Ok(DomainResponse {
88            domain,
89            verification_instructions: instructions,
90        })
91    }
92
93    /// Get a domain by ID for a tenant.
94    pub async fn get_domain(
95        &self,
96        id: Uuid,
97        tenant_id: Uuid,
98    ) -> ProxyResult<Option<DomainResponse>> {
99        let domain = db::get_domain_for_tenant(&self.pool, id, tenant_id).await?;
100
101        Ok(domain.map(|d| {
102            let instructions = match d.verification_method {
103                VerificationMethod::Dns => {
104                    VerificationInstructions::dns(&d.domain, &d.verification_token)
105                }
106                VerificationMethod::Http => {
107                    VerificationInstructions::http(&d.domain, &d.verification_token)
108                }
109            };
110            DomainResponse {
111                domain: d,
112                verification_instructions: instructions,
113            }
114        }))
115    }
116
117    /// List all domains for a tenant.
118    pub async fn list_domains(&self, tenant_id: Uuid) -> ProxyResult<Vec<Domain>> {
119        db::list_domains(&self.pool, tenant_id).await
120    }
121
122    /// Delete a domain, and every certificate linked to it.
123    ///
124    /// The certificate rows go with the domain row; `cert_store` is told as
125    /// well, so its files and its memory cache do not go on holding a private
126    /// key for a domain that no longer exists. A certificate is only removed
127    /// if it is *linked* to this domain: a row for the same name with no
128    /// domain is the operator's, and is left alone.
129    pub async fn delete_domain(
130        &self,
131        id: Uuid,
132        tenant_id: Uuid,
133        cert_store: &crate::tls::CertificateStore,
134    ) -> ProxyResult<bool> {
135        let Some(certificates) = db::delete_domain(&self.pool, id, tenant_id).await? else {
136            return Ok(false);
137        };
138        for name in certificates {
139            if let Err(error) = cert_store.remove(&name).await {
140                // The row is already gone with the domain; what is left is a
141                // cached file. Worth a warning, not worth failing a delete
142                // that has happened.
143                tracing::warn!(domain = %name, %error, "could not clear a cached certificate");
144            }
145        }
146        Ok(true)
147    }
148
149    /// Verify a domain.
150    ///
151    /// Throttled per domain: one attempt per
152    /// [`verification_min_interval`](DomainPolicy::verification_min_interval),
153    /// and at most
154    /// [`verification_max_attempts`](DomainPolicy::verification_max_attempts)
155    /// in all. Refused once the challenge or the registration has expired. An
156    /// already-verified domain answers `verified` without checking again.
157    pub async fn verify_domain(
158        &self,
159        id: Uuid,
160        tenant_id: Uuid,
161    ) -> ProxyResult<VerificationResult> {
162        let domain = match db::start_verification_attempt(
163            &self.pool,
164            id,
165            tenant_id,
166            self.policy.verification_min_interval,
167            self.policy.verification_max_attempts,
168            self.policy.pending_ttl,
169        )
170        .await?
171        {
172            db::AttemptStart::Started(domain) => domain,
173            db::AttemptStart::AlreadyVerified => return Ok(VerificationResult::Verified),
174            db::AttemptStart::NotFound => {
175                return Err(ProxyError::NotFound("Domain not found".into()))
176            }
177            db::AttemptStart::TooSoon => return Err(ProxyError::RateLimitExceeded),
178            db::AttemptStart::Exhausted => {
179                return Ok(VerificationResult::Failed {
180                    reason: "This domain has used all its verification attempts; \
181                             delete it and add it again"
182                        .into(),
183                })
184            }
185            db::AttemptStart::Expired => {
186                return Ok(VerificationResult::Failed {
187                    reason: "The verification challenge has expired; \
188                             delete the domain and add it again"
189                        .into(),
190                })
191            }
192        };
193
194        // Perform verification based on method
195        let result = match domain.verification_method {
196            VerificationMethod::Dns => {
197                self.verification_service
198                    .verify_dns(&domain.domain, &domain.verification_token)
199                    .await
200            }
201            VerificationMethod::Http => {
202                self.verification_service
203                    .verify_http(&domain.domain, &domain.verification_token)
204                    .await
205            }
206        };
207
208        match &result {
209            VerificationResult::Verified => {
210                // A domain asking for ACME is queued, not marked
211                // `provisioning`.
212                //
213                // `pending` is the worker's inbox: `saas::ssl::run` claims
214                // these rows, flips them to `provisioning` itself, and records
215                // `active` or `failed` with a reason. Setting `provisioning`
216                // here would claim work was under way before anything had
217                // started, and would take the row out of the worker's own
218                // claim query -- so it would sit mid-issuance for ever.
219                db::mark_verified(&self.pool, id).await?;
220                tracing::info!(domain = %domain.domain, "Domain verified successfully");
221            }
222            VerificationResult::Failed { reason } => {
223                tracing::warn!(domain = %domain.domain, reason = %reason, "Domain verification failed");
224            }
225            VerificationResult::Pending => {}
226        }
227
228        Ok(result)
229    }
230
231    /// Look up a live HTTP verification challenge by its token.
232    ///
233    /// Exposed so the well-known endpoint can answer challenges without
234    /// reaching past this service for the pool. The caller must still check
235    /// that the request arrived for the returned domain.
236    pub async fn find_live_http_challenge(
237        &self,
238        token: &str,
239    ) -> ProxyResult<Option<db::HttpChallenge>> {
240        db::find_live_http_challenge(&self.pool, token).await
241    }
242
243    /// Apply a partial update to a domain.
244    ///
245    /// The domain name cannot be changed -- see [`UpdateDomainRequest`] for
246    /// why -- so this only moves the verification method and the SSL provider.
247    pub async fn update_domain(
248        &self,
249        id: Uuid,
250        tenant_id: Uuid,
251        req: UpdateDomainRequest,
252    ) -> ProxyResult<Domain> {
253        db::update_domain(
254            &self.pool,
255            id,
256            tenant_id,
257            req.verification_method.as_ref(),
258            req.ssl_provider.as_ref(),
259        )
260        .await?
261        .ok_or_else(|| ProxyError::NotFound("Domain not found".into()))
262    }
263
264    /// Store a certificate an operator supplied, after checking it.
265    ///
266    /// The checks are the point. An unchecked upload gives a listener that
267    /// accepts the certificate now and fails every handshake later: the key may
268    /// not match the chain, the certificate may be expired, or it may be for
269    /// another domain entirely. All three are refused here, with a message
270    /// saying which.
271    ///
272    /// And the domain must be verified. What is stored here is served by the
273    /// TLS listener for that name, to everyone; before this check, a tenant
274    /// could register any name -- the platform's own included -- and have its
275    /// certificate served for it without proving anything. The chain must also
276    /// be trusted according to [`DomainPolicy::upload_trust`].
277    pub async fn upload_certificate(
278        &self,
279        id: Uuid,
280        tenant_id: Uuid,
281        cert_store: &crate::tls::CertificateStore,
282        req: UploadCertificateRequest,
283    ) -> ProxyResult<Domain> {
284        let domain = db::get_domain_for_tenant(&self.pool, id, tenant_id)
285            .await?
286            .ok_or_else(|| ProxyError::NotFound("Domain not found".into()))?;
287
288        if domain.verification_status != VerificationStatus::Verified {
289            return Err(ProxyError::Validation(
290                "Domain must be verified before a certificate can be uploaded".into(),
291            ));
292        }
293
294        let facts = crate::tls::validate_for_domain(
295            &domain.domain,
296            req.cert_pem.as_bytes(),
297            req.key_pem.as_bytes(),
298        )?;
299        verify_chain(
300            &self.policy.upload_trust,
301            &domain.domain,
302            req.cert_pem.as_bytes(),
303        )?;
304
305        cert_store
306            .save(crate::tls::Certificate {
307                domain: domain.domain.clone(),
308                cert_pem: req.cert_pem.into_bytes(),
309                key_pem: req.key_pem.into_bytes(),
310                expires_at: Some(facts.expires_at),
311            })
312            .await?;
313
314        // The verified condition is in this statement too: the domain may have
315        // been deleted since it was read. If it has, the certificate just
316        // saved belongs to nobody, and is unlinked -- nothing would ever
317        // remove it, and the TLS listener would serve it. Take it back out.
318        let Some(updated) =
319            db::record_uploaded_certificate(&self.pool, id, tenant_id, facts.expires_at).await?
320        else {
321            if let Err(error) = cert_store.remove(&domain.domain).await {
322                tracing::error!(
323                    domain = %domain.domain,
324                    %error,
325                    "could not remove a certificate uploaded for a domain deleted mid-upload"
326                );
327            }
328            return Err(ProxyError::NotFound("Domain not found".into()));
329        };
330
331        tracing::info!(
332            domain = %updated.domain,
333            expires_at = %facts.expires_at,
334            "stored an uploaded certificate"
335        );
336        Ok(updated)
337    }
338
339    /// Queue a verified ACME domain for certificate issuance.
340    ///
341    /// Sets state and returns; the work is done by the issuance worker. That is
342    /// the whole endpoint, and deliberately so: an ACME order is several round
343    /// trips to the CA plus a challenge the CA has to fetch back from us, and
344    /// retrying under a rate limit is the normal failure mode. Doing it inline
345    /// would hold the request open and let a client timeout orphan an order the
346    /// CA had already started.
347    ///
348    /// Idempotent, and also the way to retry after a failure -- it clears the
349    /// attempt count and the backoff.
350    pub async fn queue_ssl_issuance(&self, id: Uuid, tenant_id: Uuid) -> ProxyResult<Domain> {
351        db::queue_for_issuance(&self.pool, id, tenant_id)
352            .await?
353            .ok_or_else(|| {
354                ProxyError::NotFound(
355                    "No verified domain with ssl_provider = acme for this tenant".into(),
356                )
357            })
358    }
359
360    /// Enable a verified domain.
361    pub async fn enable_domain(&self, id: Uuid, tenant_id: Uuid) -> ProxyResult<bool> {
362        // First check if domain is verified
363        let domain = db::get_domain_for_tenant(&self.pool, id, tenant_id)
364            .await?
365            .ok_or_else(|| ProxyError::NotFound("Domain not found".into()))?;
366
367        if domain.verification_status != VerificationStatus::Verified {
368            return Err(ProxyError::Validation(
369                "Domain must be verified before enabling".into(),
370            ));
371        }
372
373        db::enable_domain(&self.pool, id, tenant_id).await
374    }
375
376    /// Disable a domain.
377    pub async fn disable_domain(&self, id: Uuid, tenant_id: Uuid) -> ProxyResult<bool> {
378        db::disable_domain(&self.pool, id, tenant_id).await
379    }
380
381    // =========================================================================
382    // Route Management
383    // =========================================================================
384
385    /// Create a route for a verified domain, within the per-domain quota.
386    pub async fn create_route(
387        &self,
388        domain_id: Uuid,
389        tenant_id: Uuid,
390        req: CreateDomainRouteRequest,
391    ) -> ProxyResult<DomainRoute> {
392        validation::validate_resource_name("Route", &req.name)?;
393        validation::validate_route_match(
394            &req.path_pattern,
395            &req.path_type,
396            req.methods.as_deref(),
397        )?;
398        validation::validate_add_headers(&req.add_headers)?;
399        validation::validate_remove_headers(&req.remove_headers)?;
400
401        // Ownership of the domain and the upstream, verification and the quota
402        // are all checked inside the insert's transaction.
403        db::create_route(&self.pool, domain_id, tenant_id, req).await
404    }
405
406    /// Get a route by ID.
407    pub async fn get_route(&self, id: Uuid, tenant_id: Uuid) -> ProxyResult<Option<DomainRoute>> {
408        db::get_route_for_tenant(&self.pool, id, tenant_id).await
409    }
410
411    /// List routes for a domain.
412    pub async fn list_routes_for_domain(
413        &self,
414        domain_id: Uuid,
415        tenant_id: Uuid,
416    ) -> ProxyResult<Vec<DomainRoute>> {
417        // Verify domain belongs to tenant
418        db::get_domain_for_tenant(&self.pool, domain_id, tenant_id)
419            .await?
420            .ok_or_else(|| ProxyError::NotFound("Domain not found".into()))?;
421
422        db::list_routes_for_domain(&self.pool, domain_id, tenant_id).await
423    }
424
425    /// Update a route.
426    pub async fn update_route(
427        &self,
428        id: Uuid,
429        tenant_id: Uuid,
430        req: UpdateDomainRouteRequest,
431    ) -> ProxyResult<Option<DomainRoute>> {
432        if let Some(name) = &req.name {
433            validation::validate_resource_name("Route", name)?;
434        }
435        if req.path_pattern.is_some() || req.path_type.is_some() || req.methods.is_some() {
436            // The pattern is checked against the type it will have after the
437            // update, which may be the stored one.
438            let current = db::get_route_for_tenant(&self.pool, id, tenant_id)
439                .await?
440                .ok_or_else(|| ProxyError::NotFound("Route not found".into()))?;
441            validation::validate_route_match(
442                req.path_pattern.as_deref().unwrap_or(&current.path_pattern),
443                req.path_type.as_ref().unwrap_or(&current.path_type),
444                req.methods.as_deref(),
445            )?;
446        }
447        if let Some(headers) = &req.add_headers {
448            validation::validate_add_headers(headers)?;
449        }
450        if let Some(headers) = &req.remove_headers {
451            validation::validate_remove_headers(headers)?;
452        }
453
454        // If upstream_id is being updated, verify it belongs to tenant
455        if let Some(upstream_id) = req.upstream_id {
456            db::get_upstream_for_tenant(&self.pool, upstream_id, tenant_id)
457                .await?
458                .ok_or_else(|| ProxyError::NotFound("Upstream not found".into()))?;
459        }
460
461        db::update_route(&self.pool, id, tenant_id, req).await
462    }
463
464    /// Delete a route.
465    pub async fn delete_route(&self, id: Uuid, tenant_id: Uuid) -> ProxyResult<bool> {
466        db::delete_route(&self.pool, id, tenant_id).await
467    }
468
469    // =========================================================================
470    // Upstream Management
471    // =========================================================================
472
473    /// Create an upstream for a tenant, with any backends given inline.
474    ///
475    /// All or nothing: either the upstream and every backend are stored, or
476    /// none of them is.
477    pub async fn create_upstream(
478        &self,
479        tenant_id: Uuid,
480        mut req: CreateUpstreamRequest,
481    ) -> ProxyResult<DomainUpstream> {
482        validation::validate_resource_name("Upstream", &req.name)?;
483        validation::validate_health_check_path(&req.health_check_path)?;
484        for backend in &mut req.backends {
485            check_backend(backend)?;
486        }
487        db::create_upstream(&self.pool, tenant_id, req).await
488    }
489
490    /// Get an upstream by ID.
491    pub async fn get_upstream(
492        &self,
493        id: Uuid,
494        tenant_id: Uuid,
495    ) -> ProxyResult<Option<DomainUpstream>> {
496        db::get_upstream_for_tenant(&self.pool, id, tenant_id).await
497    }
498
499    /// List upstreams for a tenant.
500    pub async fn list_upstreams(&self, tenant_id: Uuid) -> ProxyResult<Vec<DomainUpstream>> {
501        db::list_upstreams(&self.pool, tenant_id).await
502    }
503
504    /// Update an upstream.
505    pub async fn update_upstream(
506        &self,
507        id: Uuid,
508        tenant_id: Uuid,
509        req: UpdateUpstreamRequest,
510    ) -> ProxyResult<Option<DomainUpstream>> {
511        if let Some(name) = &req.name {
512            validation::validate_resource_name("Upstream", name)?;
513        }
514        if let Some(path) = &req.health_check_path {
515            validation::validate_health_check_path(path)?;
516        }
517        db::update_upstream(&self.pool, id, tenant_id, req).await
518    }
519
520    /// Delete an upstream.
521    pub async fn delete_upstream(&self, id: Uuid, tenant_id: Uuid) -> ProxyResult<bool> {
522        db::delete_upstream(&self.pool, id, tenant_id).await
523    }
524
525    /// Add a backend to an upstream, within the per-upstream quota.
526    pub async fn add_backend(
527        &self,
528        upstream_id: Uuid,
529        tenant_id: Uuid,
530        mut req: CreateBackendRequest,
531    ) -> ProxyResult<DomainBackend> {
532        check_backend(&mut req)?;
533        db::create_backend(&self.pool, upstream_id, tenant_id, req).await
534    }
535
536    /// Remove a backend from an upstream.
537    pub async fn remove_backend(
538        &self,
539        backend_id: Uuid,
540        upstream_id: Uuid,
541        tenant_id: Uuid,
542    ) -> ProxyResult<bool> {
543        db::delete_backend(&self.pool, backend_id, upstream_id, tenant_id).await
544    }
545
546    // =========================================================================
547    // Tenant Management
548    // =========================================================================
549
550    /// Get tenant usage statistics.
551    pub async fn get_tenant_usage(&self, tenant_id: Uuid) -> ProxyResult<TenantUsage> {
552        db::get_tenant_usage(&self.pool, tenant_id).await
553    }
554}
555
556/// Check a backend and put its address in canonical form.
557///
558/// Nothing reads `proxy_domain_backends` yet, but something will, and an
559/// address stored now is one connected to then. See
560/// [`validate_backend_address`](validation::validate_backend_address) for what
561/// is refused, and for the check the eventual consumer still has to make.
562fn check_backend(backend: &mut CreateBackendRequest) -> ProxyResult<()> {
563    validation::validate_scheme(&backend.scheme)?;
564    backend.address = validation::validate_backend_address(&backend.address)?;
565    if !(1..=10_000).contains(&backend.weight) {
566        return Err(ProxyError::Validation(
567            "backend weight must be from 1 to 10000".into(),
568        ));
569    }
570    Ok(())
571}
572
573/// Check that an uploaded chain leads to a trusted root, for `domain`.
574///
575/// The leaf's name, expiry and key have already been checked by
576/// `tls::validate_for_domain`; this adds the one thing it does not -- whether
577/// anyone but the uploader vouches for the certificate. The verifier is
578/// rustls's own, the one a client would run, so a chain passes here exactly
579/// when a client with these roots would accept it.
580fn verify_chain(trust: &UploadTrust, domain: &str, cert_pem: &[u8]) -> ProxyResult<()> {
581    use rustls::client::danger::ServerCertVerifier;
582    use rustls::pki_types::{ServerName, UnixTime};
583
584    let roots = match trust {
585        UploadTrust::Unchecked => return Ok(()),
586        UploadTrust::PublicRoots => Arc::new(rustls::RootCertStore {
587            roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
588        }),
589        UploadTrust::Roots(roots) => roots.clone(),
590    };
591
592    let chain: Vec<_> = rustls_pemfile::certs(&mut &cert_pem[..])
593        .collect::<Result<_, _>>()
594        .map_err(|_| ProxyError::Validation("could not parse the certificate chain".into()))?;
595    let (leaf, intermediates) = chain
596        .split_first()
597        .ok_or_else(|| ProxyError::Validation("the certificate chain is empty".into()))?;
598    let name = ServerName::try_from(domain.to_string())
599        .map_err(|_| ProxyError::Validation("the domain is not a valid server name".into()))?;
600
601    let verifier = rustls::client::WebPkiServerVerifier::builder_with_provider(
602        roots,
603        Arc::new(rustls::crypto::aws_lc_rs::default_provider()),
604    )
605    .build()
606    .map_err(|e| ProxyError::Tls(format!("could not build a certificate verifier: {e}")))?;
607
608    verifier
609        .verify_server_cert(leaf, intermediates, &name, &[], UnixTime::now())
610        .map_err(|error| {
611            tracing::info!(%domain, %error, "refused an uploaded certificate chain");
612            ProxyError::Validation(
613                "the certificate chain is not trusted: it must be issued by a trusted \
614                 certificate authority and include any intermediate certificates"
615                    .into(),
616            )
617        })?;
618    Ok(())
619}
620
621/// Generate a secure verification token.
622fn generate_verification_token() -> String {
623    const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789";
624    let mut rng = rand::rng();
625
626    (0..32)
627        .map(|_| {
628            let idx = rng.random_range(0..CHARSET.len());
629            CHARSET[idx] as char
630        })
631        .collect()
632}
633
634#[cfg(test)]
635mod tests {
636    use super::*;
637
638    // Domain-name rules are tested in `validation`, and the manager's use of
639    // them through the router in tests/control_plane.rs.
640
641    #[test]
642    fn test_generate_verification_token() {
643        let token = generate_verification_token();
644        assert_eq!(token.len(), 32);
645        assert!(token.chars().all(|c| c.is_alphanumeric()));
646    }
647}