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}