Skip to main content

dinio_sdk/api/
certificates.rs

1//! `/api/v1/certificates` (`../core/src/api/certificates.ts`), zone certificates only.
2//! Platform certificates (no zone, `is_default`) are Admin-only and not covered.
3
4use reqwest::Method;
5use serde::Serialize;
6use uuid::Uuid;
7
8use crate::api::PageRequest;
9use crate::client::Client;
10use crate::envelope::Page;
11use crate::error::Result;
12use crate::models::{Certificate, CertificateRenewal, NewCertificate};
13
14/// Largest `per_page` Core accepts on this route (`int_query(..., 200)`).
15const MAX_CERTIFICATES_PER_PAGE: u32 = 200;
16
17#[derive(Serialize)]
18struct ListQuery<'a> {
19    #[serde(skip_serializing_if = "Option::is_none")]
20    zone_id: Option<Uuid>,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    name: Option<&'a str>,
23    #[serde(flatten)]
24    page: PageRequest,
25}
26
27/// TLS certificates of zones, from [`Client::certificates`].
28///
29/// Access is the zone's: its key, a member of the zone, or an Admin. Every name in a zone
30/// certificate must be served by that zone. Certificates Core issues for redirects
31/// (`source` = `Acme`) can be read here but not replaced or deleted.
32pub struct Certificates<'a> {
33    client: &'a Client,
34}
35
36impl<'a> Certificates<'a> {
37    pub(crate) fn new(client: &'a Client) -> Self {
38        Self { client }
39    }
40
41    /// One page of the certificates of `zone_id`, or of every zone the caller can access
42    /// when `None` (an Admin also sees platform certificates).
43    ///
44    /// `GET /api/v1/certificates[?zone_id=...]`
45    ///
46    /// # Errors
47    ///
48    /// [`ErrorCode::Forbidden`](crate::ErrorCode::Forbidden) if the caller has no access to
49    /// `zone_id`.
50    pub async fn list(
51        &self,
52        zone_id: Option<Uuid>,
53        page: PageRequest,
54    ) -> Result<Page<Certificate>> {
55        let query = ListQuery {
56            zone_id,
57            name: None,
58            page,
59        };
60        let request = self
61            .client
62            .request(Method::GET, &["certificates"])?
63            .query(&query);
64        let (items, meta) = self.client.send(request).await?;
65        Ok(Page { items, meta })
66    }
67
68    /// All certificates [`list`](Self::list) would return, following pagination.
69    ///
70    /// `GET /api/v1/certificates`, as many times as there are pages.
71    ///
72    /// # Errors
73    ///
74    /// As [`list`](Self::list).
75    pub async fn list_all(&self, zone_id: Option<Uuid>) -> Result<Vec<Certificate>> {
76        let mut all = Vec::new();
77        let mut page = 1;
78        loop {
79            let request = PageRequest {
80                page: Some(page),
81                per_page: Some(MAX_CERTIFICATES_PER_PAGE),
82            };
83            let Page { items, meta } = self.list(zone_id, request).await?;
84            all.extend(items);
85            match meta {
86                Some(meta) if page < meta.total_pages => page += 1,
87                _ => return Ok(all),
88            }
89        }
90    }
91
92    /// The visible certificates that cover a host name the way Relay picks them (exact
93    /// name, or a wildcard one label up). Core allows one certificate per name, so this is
94    /// normally zero or one.
95    ///
96    /// `GET /api/v1/certificates?name=...`
97    pub async fn covering(&self, name: &str) -> Result<Vec<Certificate>> {
98        let query = ListQuery {
99            zone_id: None,
100            name: Some(name),
101            page: PageRequest::default(),
102        };
103        let request = self
104            .client
105            .request(Method::GET, &["certificates"])?
106            .query(&query);
107        Ok(self.client.send(request).await?.0)
108    }
109
110    /// One certificate by uuid.
111    ///
112    /// `GET /api/v1/certificates/:uuid`
113    ///
114    /// # Errors
115    ///
116    /// [`ErrorCode::NotFound`](crate::ErrorCode::NotFound) if there is no such certificate.
117    pub async fn get(&self, uuid: Uuid) -> Result<Certificate> {
118        let request = self
119            .client
120            .request(Method::GET, &["certificates", &uuid.to_string()])?;
121        Ok(self.client.send(request).await?.0)
122    }
123
124    /// Uploads a certificate for names of a zone; every Relay gets it. Zone members and the
125    /// zone key may do this.
126    ///
127    /// `POST /api/v1/certificates`
128    ///
129    /// # Errors
130    ///
131    /// * [`ErrorCode::Validation`](crate::ErrorCode::Validation) if the PEM cannot be
132    ///   parsed, is expired, the key does not match, the chain is broken, or there are no
133    ///   SAN names.
134    /// * [`ErrorCode::Conflict`](crate::ErrorCode::Conflict) if a name is not served by the
135    ///   zone, has a redirect with `tls` = `Auto` (switch it to `Manual` first), or is
136    ///   already covered by another certificate.
137    ///
138    /// # Examples
139    ///
140    /// ```no_run
141    /// use dinio_sdk::models::NewCertificate;
142    ///
143    /// # async fn example(client: &dinio_sdk::Client) -> dinio_sdk::Result<()> {
144    /// let zone = client.zones().get("example.com").await?;
145    /// let cert = client
146    ///     .certificates()
147    ///     .create(&NewCertificate {
148    ///         chain_pem: std::fs::read_to_string("fullchain.pem").unwrap(),
149    ///         key_pem: std::fs::read_to_string("privkey.pem").unwrap(),
150    ///         zone_id: zone.uuid,
151    ///     })
152    ///     .await?;
153    /// println!("{} valid until {}", cert.names.join(", "), cert.valid_to);
154    /// # Ok(())
155    /// # }
156    /// ```
157    pub async fn create(&self, certificate: &NewCertificate) -> Result<Certificate> {
158        let request = self
159            .client
160            .request(Method::POST, &["certificates"])?
161            .json(certificate);
162        Ok(self.client.send(request).await?.0)
163    }
164
165    /// Replaces chain and key under the same id (renewal). The same name rules as
166    /// [`create`](Self::create) apply to the new chain.
167    ///
168    /// `PUT /api/v1/certificates/:uuid`
169    ///
170    /// # Errors
171    ///
172    /// * [`ErrorCode::ManagedCertificate`](crate::ErrorCode::ManagedCertificate) for a
173    ///   certificate Core issued for a redirect.
174    /// * As [`create`](Self::create).
175    pub async fn replace(&self, uuid: Uuid, renewal: &CertificateRenewal) -> Result<Certificate> {
176        let request = self
177            .client
178            .request(Method::PUT, &["certificates", &uuid.to_string()])?
179            .json(renewal);
180        Ok(self.client.send(request).await?.0)
181    }
182
183    /// Deletes a certificate. Relays fall back to the next match or their default.
184    ///
185    /// `DELETE /api/v1/certificates/:uuid`
186    ///
187    /// # Errors
188    ///
189    /// * [`ErrorCode::NotFound`](crate::ErrorCode::NotFound) if there is no such
190    ///   certificate.
191    /// * [`ErrorCode::ManagedCertificate`](crate::ErrorCode::ManagedCertificate) for a
192    ///   certificate Core issued for a redirect.
193    pub async fn delete(&self, uuid: Uuid) -> Result<()> {
194        let request = self
195            .client
196            .request(Method::DELETE, &["certificates", &uuid.to_string()])?;
197        self.client.send_empty(request).await
198    }
199}