1use crate::sql::Row;
12use serde_json::Value;
13use tracing::{debug, info};
14use uuid::Uuid;
15
16use crate::db::Database;
17use crate::nonce::now_secs;
18use crate::status::{self, AuthzStatus, ChallengeStatus};
19use acme_proxy_core::datetime::rfc3339;
20use acme_proxy_core::identifier::Identifier;
21use acme_proxy_core::random::random_token;
22
23#[derive(Debug)]
47pub struct Authorization {
48 pub id: Uuid,
49 pub order_id: Uuid,
50 pub identifier: Identifier,
51 pub status: AuthzStatus,
52 pub expires: i64,
53 pub created_at: i64,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum ValidationClaim {
59 Claimed,
61 Decided,
64 Limited,
68}
69
70#[derive(Debug)]
82pub struct Challenge {
83 pub id: Uuid,
84 pub authz_id: Uuid,
85 pub typ: String,
86 pub token: String,
87 pub status: ChallengeStatus,
88 pub validated: Option<i64>,
89 pub error: Option<Value>,
92 pub created_at: i64,
93}
94
95macro_rules! authz_columns {
101 () => {
102 "id, order_id, identifier, status, expires, created_at"
103 };
104}
105
106impl Authorization {
107 fn from_row(row: Row) -> Result<Self, sqlx::Error> {
108 let identifier_json: String = row.try_get("identifier")?;
109 let identifier: Identifier =
110 serde_json::from_str(&identifier_json).map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
111
112 Ok(Authorization {
113 id: row.try_get("id")?,
114 order_id: row.try_get("order_id")?,
115 identifier,
116 status: status::from_column(row.try_get::<String>("status")?.as_str())?,
117 expires: row.try_get("expires")?,
118 created_at: row.try_get("created_at")?,
119 })
120 }
121
122 pub fn new(order_id: Uuid, identifier: Identifier, expires: i64) -> Authorization {
125 Authorization {
126 id: crate::id::mint(),
127 order_id,
128 identifier,
129 status: AuthzStatus::Pending,
130 expires,
131 created_at: now_secs(),
132 }
133 }
134
135 pub async fn insert<'e>(
138 &self,
139 executor: impl Into<crate::sql::Exec<'e>>,
140 ) -> Result<(), sqlx::Error> {
141 let identifier_json = serde_json::to_string(&self.identifier)
143 .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
144
145 debug!(event = "db_authz_create_started", outcome = "progress", authz_id = ?self.id, order_id = ?self.order_id);
146 crate::sql::query(
147 "INSERT INTO authorizations (id, order_id, identifier, status, expires, created_at) \
148 VALUES (?, ?, ?, ?, ?, ?);",
149 )
150 .bind(self.id)
151 .bind(self.order_id)
152 .bind(identifier_json)
153 .bind(self.status.as_str())
154 .bind(self.expires)
155 .bind(self.created_at)
156 .execute(executor)
157 .await?;
158
159 info!(event = "db_authz_created", outcome = "success", authz_id = ?self.id, order_id = ?self.order_id);
160 Ok(())
161 }
162
163 pub async fn create(
165 order_id: Uuid,
166 identifier: Identifier,
167 expires: i64,
168 database: &Database,
169 ) -> Result<Authorization, sqlx::Error> {
170 let authz = Authorization::new(order_id, identifier, expires);
171 authz.insert(database).await?;
172 Ok(authz)
173 }
174
175 pub async fn find_by_id(
178 id: &str,
179 database: &Database,
180 ) -> Result<Option<Authorization>, sqlx::Error> {
181 debug!(event = "db_authz_find_by_id_started", outcome = "progress", authz_id = ?id);
182 let Some(id) = crate::id::parse(id) else {
183 return Ok(None);
184 };
185 let row = crate::sql::query(concat!(
186 "SELECT ",
187 authz_columns!(),
188 " FROM authorizations WHERE id = ?;"
189 ))
190 .bind(id)
191 .fetch_optional(database)
192 .await?;
193
194 row.map(Authorization::from_row).transpose()
195 }
196
197 pub async fn find_by_order(
200 order_id: Uuid,
201 database: &Database,
202 ) -> Result<Vec<Authorization>, sqlx::Error> {
203 Self::find_by_order_with(order_id, database).await
204 }
205
206 pub async fn count_by_order(order_id: Uuid, database: &Database) -> Result<i64, sqlx::Error> {
209 let row = crate::sql::query("SELECT COUNT(*) FROM authorizations WHERE order_id = ?;")
210 .bind(order_id)
211 .fetch_one(database)
212 .await?;
213 row.try_get::<i64>(0)
214 }
215
216 pub async fn find_ids_by_orders(
226 order_ids: &[Uuid],
227 database: &Database,
228 ) -> Result<std::collections::HashMap<Uuid, Vec<Uuid>>, sqlx::Error> {
229 let mut grouped: std::collections::HashMap<Uuid, Vec<Uuid>> =
230 std::collections::HashMap::new();
231 if order_ids.is_empty() {
232 return Ok(grouped);
233 }
234
235 let mut builder = crate::sql::Builder::new(
239 database.dialect(),
240 "SELECT id, order_id FROM authorizations WHERE order_id IN (",
241 );
242 let mut separated = builder.separated(", ");
243 for id in order_ids {
244 separated.push_bind(*id);
245 }
246 builder.push(") ORDER BY created_at ASC;");
247
248 debug!(
249 event = "db_authz_find_ids_by_orders",
250 outcome = "success",
251 orders = order_ids.len()
252 );
253 for row in builder.build().fetch_all(database).await? {
254 let order_id: Uuid = row.try_get("order_id")?;
255 let id: Uuid = row.try_get("id")?;
256 grouped.entry(order_id).or_default().push(id);
257 }
258 Ok(grouped)
259 }
260
261 pub async fn find_by_order_with<'e>(
270 order_id: Uuid,
271 executor: impl Into<crate::sql::Exec<'e>>,
272 ) -> Result<Vec<Authorization>, sqlx::Error> {
273 debug!(event = "db_authz_find_by_order_started", outcome = "progress", order_id = ?order_id);
274 let rows = crate::sql::query(concat!(
275 "SELECT ",
276 authz_columns!(),
277 " FROM authorizations WHERE order_id = ? ORDER BY created_at ASC;"
278 ))
279 .bind(order_id)
280 .fetch_all(executor)
281 .await?;
282
283 rows.into_iter().map(Authorization::from_row).collect()
284 }
285
286 pub async fn set_valid<'e>(
301 id: Uuid,
302 executor: impl Into<crate::sql::Exec<'e>>,
303 ) -> Result<bool, sqlx::Error> {
304 let written = crate::sql::query(
305 "UPDATE authorizations SET status = 'valid' WHERE id = ? AND status = 'pending';",
306 )
307 .bind(id)
308 .execute(executor)
309 .await?
310 .rows_affected();
311 Ok(written == 1)
312 }
313
314 pub async fn set_invalid<'e>(
317 id: Uuid,
318 executor: impl Into<crate::sql::Exec<'e>>,
319 ) -> Result<bool, sqlx::Error> {
320 let written = crate::sql::query(
321 "UPDATE authorizations SET status = 'invalid' WHERE id = ? AND status = 'pending';",
322 )
323 .bind(id)
324 .execute(executor)
325 .await?
326 .rows_affected();
327 Ok(written == 1)
328 }
329
330 pub async fn set_deactivated<'e>(
344 id: Uuid,
345 executor: impl Into<crate::sql::Exec<'e>>,
346 ) -> Result<bool, sqlx::Error> {
347 let written = crate::sql::query(
348 "UPDATE authorizations SET status = 'deactivated' \
349 WHERE id = ? AND status IN ('pending', 'valid');",
350 )
351 .bind(id)
352 .execute(executor)
353 .await?
354 .rows_affected();
355 Ok(written == 1)
356 }
357
358 pub async fn mark_valid(&mut self, database: &Database) -> Result<bool, sqlx::Error> {
362 debug!(event = "db_authz_mark_valid_started", outcome = "progress", authz_id = ?self.id);
363 if !Self::set_valid(self.id, database).await? {
364 return Ok(false);
365 }
366
367 self.status = AuthzStatus::Valid;
368 info!(event = "db_authz_marked_valid", outcome = "success", authz_id = ?self.id);
369 Ok(true)
370 }
371
372 pub async fn mark_invalid(&mut self, database: &Database) -> Result<bool, sqlx::Error> {
379 debug!(event = "db_authz_mark_invalid_started", outcome = "progress", authz_id = ?self.id);
380 if !Self::set_invalid(self.id, database).await? {
381 return Ok(false);
382 }
383
384 self.status = AuthzStatus::Invalid;
385 info!(event = "db_authz_marked_invalid", outcome = "failure", authz_id = ?self.id);
386 Ok(true)
387 }
388
389 #[must_use]
394 pub fn is_wildcard(&self) -> bool {
395 self.identifier.value.starts_with("*.")
396 }
397
398 #[must_use]
404 pub fn base_identifier(&self) -> &str {
405 self.identifier
406 .value
407 .strip_prefix("*.")
408 .unwrap_or(&self.identifier.value)
409 }
410
411 #[must_use]
415 pub fn to_json(&self, base_url: &str, challenges: &[Challenge]) -> Value {
416 let mut object = serde_json::Map::new();
417 object.insert(
421 "identifier".to_string(),
422 serde_json::to_value(Identifier::new(
423 self.identifier.typ.clone(),
424 self.base_identifier().to_string(),
425 ))
426 .expect("Identifier is always serializable"),
427 );
428 object.insert(
429 "status".to_string(),
430 Value::String(self.status.as_str().to_string()),
431 );
432 object.insert("expires".to_string(), Value::String(rfc3339(self.expires)));
433 let challenges: Vec<Value> = challenges.iter().map(|c| c.to_json(base_url)).collect();
434 object.insert("challenges".to_string(), Value::Array(challenges));
435 if self.is_wildcard() {
436 object.insert("wildcard".to_string(), Value::Bool(true));
437 }
438 Value::Object(object)
439 }
440}
441
442macro_rules! challenge_columns {
448 () => {
449 "id, authz_id, type, token, status, validated, error, created_at"
450 };
451}
452
453impl Challenge {
454 fn from_row(row: Row) -> Result<Self, sqlx::Error> {
455 let error_json: Option<String> = row.try_get("error")?;
456 let error = error_json
457 .map(|json| serde_json::from_str(&json))
458 .transpose()
459 .map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
460
461 Ok(Challenge {
462 id: row.try_get("id")?,
463 authz_id: row.try_get("authz_id")?,
464 typ: row.try_get("type")?,
465 token: row.try_get("token")?,
466 status: status::from_column(row.try_get::<String>("status")?.as_str())?,
467 validated: row.try_get("validated")?,
468 error,
469 created_at: row.try_get("created_at")?,
470 })
471 }
472
473 pub fn new(authz_id: Uuid, typ: &str) -> Challenge {
480 Challenge {
481 id: crate::id::mint(),
482 authz_id,
483 typ: typ.to_string(),
484 token: random_token(),
485 status: ChallengeStatus::Pending,
486 validated: None,
487 error: None,
488 created_at: now_secs(),
489 }
490 }
491
492 pub async fn insert<'e>(
495 &self,
496 executor: impl Into<crate::sql::Exec<'e>>,
497 ) -> Result<(), sqlx::Error> {
498 debug!(event = "db_challenge_create_started", outcome = "progress", challenge_id = ?self.id, authz_id = ?self.authz_id);
499 crate::sql::query(
500 "INSERT INTO challenges (id, authz_id, type, token, status, validated, created_at) \
501 VALUES (?, ?, ?, ?, ?, NULL, ?);",
502 )
503 .bind(self.id)
504 .bind(self.authz_id)
505 .bind(&self.typ)
506 .bind(&self.token)
507 .bind(self.status.as_str())
508 .bind(self.created_at)
509 .execute(executor)
510 .await?;
511
512 info!(event = "db_challenge_created", outcome = "success", challenge_id = ?self.id, authz_id = ?self.authz_id);
513 Ok(())
514 }
515
516 pub async fn create(
519 authz_id: Uuid,
520 typ: &str,
521 database: &Database,
522 ) -> Result<Challenge, sqlx::Error> {
523 let challenge = Challenge::new(authz_id, typ);
524 challenge.insert(database).await?;
525 Ok(challenge)
526 }
527
528 pub async fn find_by_id(
531 id: &str,
532 database: &Database,
533 ) -> Result<Option<Challenge>, sqlx::Error> {
534 debug!(event = "db_challenge_find_by_id_started", outcome = "progress", challenge_id = ?id);
535 let Some(id) = crate::id::parse(id) else {
536 return Ok(None);
537 };
538 let row = crate::sql::query(concat!(
539 "SELECT ",
540 challenge_columns!(),
541 " FROM challenges WHERE id = ?;"
542 ))
543 .bind(id)
544 .fetch_optional(database)
545 .await?;
546
547 row.map(Challenge::from_row).transpose()
548 }
549
550 pub async fn find_by_authz(
553 authz_id: Uuid,
554 database: &Database,
555 ) -> Result<Vec<Challenge>, sqlx::Error> {
556 debug!(event = "db_challenge_find_by_authz_started", outcome = "progress", authz_id = ?authz_id);
557 let rows = crate::sql::query(concat!(
558 "SELECT ",
559 challenge_columns!(),
560 " FROM challenges WHERE authz_id = ? ORDER BY created_at ASC;"
561 ))
562 .bind(authz_id)
563 .fetch_all(database)
564 .await?;
565
566 rows.into_iter().map(Challenge::from_row).collect()
567 }
568
569 pub async fn claim_for_validation(
605 &mut self,
606 max_in_flight: u32,
607 database: &Database,
608 ) -> Result<ValidationClaim, sqlx::Error> {
609 debug!(event = "db_challenge_claim_started", outcome = "progress", challenge_id = ?self.id);
610 let claimed = crate::sql::query(
611 "UPDATE challenges SET status = 'processing' \
612 WHERE id = ? AND status = 'pending' AND EXISTS ( \
613 SELECT 1 FROM authorizations a JOIN orders o ON o.id = a.order_id \
614 WHERE a.id = challenges.authz_id \
615 AND a.status = 'pending' AND o.status = 'pending') \
616 AND (? = 0 OR ( \
617 SELECT COUNT(*) FROM challenges c \
618 JOIN authorizations a ON a.id = c.authz_id \
619 JOIN orders o ON o.id = a.order_id \
620 WHERE c.status = 'processing' AND o.account_id = ( \
621 SELECT o2.account_id FROM authorizations a2 \
622 JOIN orders o2 ON o2.id = a2.order_id \
623 WHERE a2.id = challenges.authz_id)) < ?);",
624 )
625 .bind(self.id)
626 .bind(max_in_flight)
627 .bind(max_in_flight)
628 .execute(database)
629 .await?
630 .rows_affected()
631 == 1;
632
633 if !claimed {
634 if max_in_flight > 0 && self.in_flight_for_account(database).await? >= max_in_flight {
637 debug!(event = "db_challenge_claim_limited", outcome = "advisory", challenge_id = ?self.id, limit = max_in_flight);
638 return Ok(ValidationClaim::Limited);
639 }
640 debug!(event = "db_challenge_claim_refused", outcome = "advisory", challenge_id = ?self.id);
641 return Ok(ValidationClaim::Decided);
642 }
643
644 self.status = ChallengeStatus::Processing;
645 debug!(event = "db_challenge_claimed", outcome = "success", challenge_id = ?self.id);
646 Ok(ValidationClaim::Claimed)
647 }
648
649 async fn in_flight_for_account(&self, database: &Database) -> Result<u32, sqlx::Error> {
652 let row = crate::sql::query(
653 "SELECT COUNT(*) FROM challenges c \
654 JOIN authorizations a ON a.id = c.authz_id \
655 JOIN orders o ON o.id = a.order_id \
656 WHERE c.status = 'processing' AND o.account_id = ( \
657 SELECT o2.account_id FROM authorizations a2 \
658 JOIN orders o2 ON o2.id = a2.order_id WHERE a2.id = ?);",
659 )
660 .bind(self.authz_id)
661 .fetch_one(database)
662 .await?;
663 Ok(u32::try_from(row.try_get::<i64>(0)?).unwrap_or(u32::MAX))
664 }
665
666 pub async fn release_validation_claim(
677 &mut self,
678 database: &Database,
679 ) -> Result<bool, sqlx::Error> {
680 let released = crate::sql::query(
681 "UPDATE challenges SET status = 'pending' WHERE id = ? AND status = 'processing';",
682 )
683 .bind(self.id)
684 .execute(database)
685 .await?
686 .rows_affected()
687 == 1;
688
689 if released {
690 self.status = ChallengeStatus::Pending;
691 debug!(event = "db_challenge_claim_released", outcome = "success", challenge_id = ?self.id);
692 }
693 Ok(released)
694 }
695
696 pub async fn find_processing(
706 now: i64,
707 database: &Database,
708 ) -> Result<Vec<(Uuid, i64)>, sqlx::Error> {
709 debug!(
710 event = "db_challenge_find_processing_started",
711 outcome = "progress"
712 );
713 let rows = crate::sql::query(
714 "SELECT c.id, a.expires FROM challenges c \
715 JOIN authorizations a ON a.id = c.authz_id \
716 WHERE c.status = 'processing' AND a.expires > ?;",
717 )
718 .bind(now)
719 .fetch_all(database)
720 .await?;
721
722 rows.into_iter()
723 .map(|row| Ok((row.try_get("id")?, row.try_get("expires")?)))
724 .collect()
725 }
726
727 pub async fn set_valid<'e>(
738 id: Uuid,
739 validated: i64,
740 executor: impl Into<crate::sql::Exec<'e>>,
741 ) -> Result<bool, sqlx::Error> {
742 let written = crate::sql::query(
743 "UPDATE challenges SET status = 'valid', validated = ? \
744 WHERE id = ? AND status IN ('pending', 'processing');",
745 )
746 .bind(validated)
747 .bind(id)
748 .execute(executor)
749 .await?
750 .rows_affected();
751 Ok(written == 1)
752 }
753
754 pub async fn set_invalid<'e>(
757 id: Uuid,
758 error: &Value,
759 executor: impl Into<crate::sql::Exec<'e>>,
760 ) -> Result<bool, sqlx::Error> {
761 let error_json =
762 serde_json::to_string(error).map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
763 let written = crate::sql::query(
764 "UPDATE challenges SET status = 'invalid', error = ? \
765 WHERE id = ? AND status IN ('pending', 'processing');",
766 )
767 .bind(error_json)
768 .bind(id)
769 .execute(executor)
770 .await?
771 .rows_affected();
772 Ok(written == 1)
773 }
774
775 pub async fn mark_valid(&mut self, database: &Database) -> Result<bool, sqlx::Error> {
778 let validated = now_secs();
779 debug!(event = "db_challenge_mark_valid_started", outcome = "progress", challenge_id = ?self.id);
780 if !Self::set_valid(self.id, validated, database).await? {
781 return Ok(false);
782 }
783
784 self.status = ChallengeStatus::Valid;
785 self.validated = Some(validated);
786 info!(event = "db_challenge_marked_valid", outcome = "success", challenge_id = ?self.id);
787 Ok(true)
788 }
789
790 pub async fn mark_invalid(
797 &mut self,
798 error: Value,
799 database: &Database,
800 ) -> Result<bool, sqlx::Error> {
801 debug!(event = "db_challenge_mark_invalid_started", outcome = "progress", challenge_id = ?self.id);
802 if !Self::set_invalid(self.id, &error, database).await? {
803 return Ok(false);
804 }
805
806 self.status = ChallengeStatus::Invalid;
807 self.error = Some(error);
808 info!(event = "db_challenge_marked_invalid", outcome = "failure", challenge_id = ?self.id);
809 Ok(true)
810 }
811
812 #[must_use]
815 pub fn to_json(&self, base_url: &str) -> Value {
816 let mut object = serde_json::Map::new();
817 object.insert("type".to_string(), Value::String(self.typ.clone()));
818 object.insert(
819 "url".to_string(),
820 Value::String(format!("{base_url}/chall/{}", self.id)),
821 );
822 object.insert(
823 "status".to_string(),
824 Value::String(self.status.as_str().to_string()),
825 );
826 object.insert("token".to_string(), Value::String(self.token.clone()));
827 if let Some(validated) = self.validated {
828 object.insert("validated".to_string(), Value::String(rfc3339(validated)));
829 }
830 if let Some(error) = &self.error {
831 object.insert("error".to_string(), error.clone());
832 }
833 Value::Object(object)
834 }
835}
836
837#[cfg(test)]
838mod tests {
839
840 use super::*;
841 use crate::account::Account;
842 use crate::order::Order;
843 use crate::status::OrderStatus;
844 use crate::testutil::account_id;
845 use acme_proxy_core::audit::ClientContext;
846 use std::sync::Arc;
847
848 #[tokio::test]
850 async fn ids_for_several_orders_come_back_grouped_in_one_query() {
851 let db = Arc::new(Database::connect_for_test().await.unwrap());
852 let account = account_id(&db).await;
853
854 let mut expected = Vec::new();
855 for name in ["a.example.com", "b.example.com"] {
856 let order = Order::create(
857 "default",
858 account,
859 vec![Identifier::dns(name)],
860 now_secs() + 3600,
861 None,
862 None,
863 &db,
864 )
865 .await
866 .unwrap();
867 let first =
868 Authorization::create(order.id, Identifier::dns(name), now_secs() + 3600, &db)
869 .await
870 .unwrap();
871 expected.push((order.id, first.id));
872 }
873
874 let ids: Vec<Uuid> = expected.iter().map(|(o, _)| *o).collect();
875 let grouped = Authorization::find_ids_by_orders(&ids, &db).await.unwrap();
876 assert_eq!(grouped.len(), 2);
877 for (order_id, authz_id) in &expected {
878 assert_eq!(grouped[order_id], vec![*authz_id]);
879 }
880
881 let grouped = Authorization::find_ids_by_orders(&[crate::id::mint()], &db)
884 .await
885 .unwrap();
886 assert!(grouped.is_empty());
887
888 assert!(
890 Authorization::find_ids_by_orders(&[], &db)
891 .await
892 .unwrap()
893 .is_empty()
894 );
895 }
896
897 async fn order_id(db: &Arc<Database>) -> String {
900 let (account, _) = Account::find_or_create(
901 "default",
902 &[1u8, 2, 3],
903 vec![],
904 &ClientContext::default(),
905 db,
906 )
907 .await
908 .unwrap();
909 let order = Order::create(
910 "default",
911 account.id,
912 vec![Identifier::dns("example.com")],
913 now_secs() + 3600,
914 None,
915 None,
916 db,
917 )
918 .await
919 .unwrap();
920 order.id.to_string()
921 }
922
923 #[tokio::test]
932 async fn the_validation_transitions_roll_back_together() {
933 let db = Arc::new(Database::connect_for_test().await.unwrap());
934 let oid = order_id(&db).await;
935 let authz = Authorization::create(
936 oid.parse().unwrap(),
937 Identifier::dns("example.com"),
938 now_secs() + 3600,
939 &db,
940 )
941 .await
942 .unwrap();
943 let challenge = Challenge::create(authz.id, "http-01", &db).await.unwrap();
944
945 let mut tx = db.transaction().await.unwrap();
948 Challenge::set_valid(challenge.id, now_secs(), tx.conn())
949 .await
950 .unwrap();
951 Authorization::set_valid(authz.id, tx.conn()).await.unwrap();
952 Order::set_ready(oid.parse().unwrap(), tx.conn())
953 .await
954 .unwrap();
955 drop(tx); let reloaded_authz = Authorization::find_by_id(authz.id.to_string().as_str(), &db)
958 .await
959 .unwrap()
960 .unwrap();
961 let reloaded_challenge = Challenge::find_by_id(challenge.id.to_string().as_str(), &db)
962 .await
963 .unwrap()
964 .unwrap();
965 let reloaded_order = Order::find_by_id(&oid, &db).await.unwrap().unwrap();
966
967 assert_eq!(reloaded_challenge.status, ChallengeStatus::Pending);
968 assert_eq!(reloaded_authz.status, AuthzStatus::Pending);
969 assert_eq!(reloaded_order.status, OrderStatus::Pending);
970 }
971
972 #[tokio::test]
975 async fn the_validation_transitions_commit_together() {
976 let db = Arc::new(Database::connect_for_test().await.unwrap());
977 let oid = order_id(&db).await;
978 let authz = Authorization::create(
979 oid.parse().unwrap(),
980 Identifier::dns("example.com"),
981 now_secs() + 3600,
982 &db,
983 )
984 .await
985 .unwrap();
986 let challenge = Challenge::create(authz.id, "http-01", &db).await.unwrap();
987
988 let mut tx = db.transaction().await.unwrap();
989 Challenge::set_valid(challenge.id, now_secs(), tx.conn())
990 .await
991 .unwrap();
992 Authorization::set_valid(authz.id, tx.conn()).await.unwrap();
993 Order::set_ready(oid.parse().unwrap(), tx.conn())
994 .await
995 .unwrap();
996 tx.commit().await.unwrap();
997
998 assert_eq!(
999 Challenge::find_by_id(challenge.id.to_string().as_str(), &db)
1000 .await
1001 .unwrap()
1002 .unwrap()
1003 .status,
1004 ChallengeStatus::Valid
1005 );
1006 assert_eq!(
1007 Authorization::find_by_id(authz.id.to_string().as_str(), &db)
1008 .await
1009 .unwrap()
1010 .unwrap()
1011 .status,
1012 AuthzStatus::Valid
1013 );
1014 assert_eq!(
1015 Order::find_by_id(&oid, &db).await.unwrap().unwrap().status,
1016 OrderStatus::Ready
1017 );
1018 }
1019
1020 #[tokio::test]
1021 async fn authz_create_find_round_trip() {
1022 let db = Arc::new(Database::connect_for_test().await.unwrap());
1023 let oid = order_id(&db).await;
1024
1025 let authz = Authorization::create(
1026 oid.parse().unwrap(),
1027 Identifier::dns("example.com"),
1028 now_secs() + 3600,
1029 &db,
1030 )
1031 .await
1032 .unwrap();
1033 assert_eq!(authz.status, AuthzStatus::Pending);
1034
1035 let by_id = Authorization::find_by_id(authz.id.to_string().as_str(), &db)
1036 .await
1037 .unwrap()
1038 .unwrap();
1039 assert_eq!(by_id.identifier, Identifier::dns("example.com"));
1040 assert_eq!(by_id.order_id.to_string(), oid);
1041
1042 let by_order = Authorization::find_by_order(oid.parse().unwrap(), &db)
1043 .await
1044 .unwrap();
1045 assert_eq!(by_order.len(), 1);
1046 }
1047
1048 #[tokio::test]
1049 async fn authz_mark_valid_persists_and_syncs() {
1050 let db = Arc::new(Database::connect_for_test().await.unwrap());
1051 let oid = order_id(&db).await;
1052
1053 let mut authz = Authorization::create(
1054 oid.parse().unwrap(),
1055 Identifier::dns("example.com"),
1056 now_secs() + 3600,
1057 &db,
1058 )
1059 .await
1060 .unwrap();
1061 authz.mark_valid(&db).await.unwrap();
1062
1063 assert_eq!(authz.status, AuthzStatus::Valid);
1064 let reloaded = Authorization::find_by_id(authz.id.to_string().as_str(), &db)
1065 .await
1066 .unwrap()
1067 .unwrap();
1068 assert_eq!(reloaded.status, AuthzStatus::Valid);
1069 }
1070
1071 #[tokio::test]
1072 async fn authz_to_json_shape() {
1073 let db = Arc::new(Database::connect_for_test().await.unwrap());
1074 let oid = order_id(&db).await;
1075
1076 let authz = Authorization::create(
1077 oid.parse().unwrap(),
1078 Identifier::dns("example.com"),
1079 now_secs() + 3600,
1080 &db,
1081 )
1082 .await
1083 .unwrap();
1084 let challenge = Challenge::create(authz.id, "http-01", &db).await.unwrap();
1085
1086 let json = authz.to_json("http://localhost:3000", std::slice::from_ref(&challenge));
1087 assert_eq!(json["status"], "pending");
1088 assert_eq!(
1089 json["identifier"],
1090 serde_json::json!({"type":"dns","value":"example.com"})
1091 );
1092 assert!(json["expires"].as_str().unwrap().ends_with('Z'));
1093 assert_eq!(json["challenges"].as_array().unwrap().len(), 1);
1094 assert_eq!(json["challenges"][0]["type"], "http-01");
1095 }
1096
1097 #[tokio::test]
1098 async fn challenge_create_find_round_trip() {
1099 let db = Arc::new(Database::connect_for_test().await.unwrap());
1100 let oid = order_id(&db).await;
1101 let authz = Authorization::create(
1102 oid.parse().unwrap(),
1103 Identifier::dns("example.com"),
1104 now_secs() + 3600,
1105 &db,
1106 )
1107 .await
1108 .unwrap();
1109
1110 let challenge = Challenge::create(authz.id, "http-01", &db).await.unwrap();
1111 assert_eq!(challenge.typ, "http-01");
1112 assert_eq!(challenge.status, ChallengeStatus::Pending);
1113 assert!(!challenge.token.is_empty());
1114 assert!(challenge.validated.is_none());
1115
1116 let by_id = Challenge::find_by_id(challenge.id.to_string().as_str(), &db)
1117 .await
1118 .unwrap()
1119 .unwrap();
1120 assert_eq!(by_id.token, challenge.token);
1121
1122 let by_authz = Challenge::find_by_authz(authz.id, &db).await.unwrap();
1123 assert_eq!(by_authz.len(), 1);
1124 }
1125
1126 #[tokio::test]
1127 async fn challenge_mark_valid_persists_and_syncs() {
1128 let db = Arc::new(Database::connect_for_test().await.unwrap());
1129 let oid = order_id(&db).await;
1130 let authz = Authorization::create(
1131 oid.parse().unwrap(),
1132 Identifier::dns("example.com"),
1133 now_secs() + 3600,
1134 &db,
1135 )
1136 .await
1137 .unwrap();
1138
1139 let mut challenge = Challenge::create(authz.id, "http-01", &db).await.unwrap();
1140 challenge.mark_valid(&db).await.unwrap();
1141
1142 assert_eq!(challenge.status, ChallengeStatus::Valid);
1143 assert!(challenge.validated.is_some());
1144
1145 let reloaded = Challenge::find_by_id(challenge.id.to_string().as_str(), &db)
1146 .await
1147 .unwrap()
1148 .unwrap();
1149 assert_eq!(reloaded.status, ChallengeStatus::Valid);
1150 let json = reloaded.to_json("http://localhost:3000");
1151 assert_eq!(json["status"], "valid");
1152 assert_eq!(
1153 json["url"],
1154 format!("http://localhost:3000/chall/{}", challenge.id)
1155 );
1156 assert!(json["validated"].as_str().unwrap().ends_with('Z'));
1157 }
1158
1159 #[tokio::test]
1160 async fn challenge_mark_invalid_persists_the_problem_document() {
1161 let db = Arc::new(Database::connect_for_test().await.unwrap());
1162 let oid = order_id(&db).await;
1163 let authz = Authorization::create(
1164 oid.parse().unwrap(),
1165 Identifier::dns("example.com"),
1166 now_secs() + 3600,
1167 &db,
1168 )
1169 .await
1170 .unwrap();
1171
1172 let mut challenge = Challenge::create(authz.id, "http-01", &db).await.unwrap();
1173 let problem = serde_json::json!({
1174 "type": "urn:ietf:params:acme:error:incorrectResponse",
1175 "detail": "response body does not match the key authorization",
1176 "status": 403,
1177 });
1178 challenge.mark_invalid(problem.clone(), &db).await.unwrap();
1179
1180 assert_eq!(challenge.status, ChallengeStatus::Invalid);
1181 assert_eq!(challenge.error.as_ref(), Some(&problem));
1182 assert!(challenge.validated.is_none());
1184
1185 let reloaded = Challenge::find_by_id(challenge.id.to_string().as_str(), &db)
1186 .await
1187 .unwrap()
1188 .unwrap();
1189 assert_eq!(reloaded.status, ChallengeStatus::Invalid);
1190 assert_eq!(reloaded.error.as_ref(), Some(&problem));
1191 let json = reloaded.to_json("http://localhost:3000");
1192 assert_eq!(json["error"], problem);
1193 assert!(json.get("validated").is_none());
1194 }
1195
1196 #[tokio::test]
1197 async fn authz_mark_invalid_persists_and_syncs() {
1198 let db = Arc::new(Database::connect_for_test().await.unwrap());
1199 let oid = order_id(&db).await;
1200
1201 let mut authz = Authorization::create(
1202 oid.parse().unwrap(),
1203 Identifier::dns("example.com"),
1204 now_secs() + 3600,
1205 &db,
1206 )
1207 .await
1208 .unwrap();
1209 authz.mark_invalid(&db).await.unwrap();
1210
1211 assert_eq!(authz.status, AuthzStatus::Invalid);
1212 let reloaded = Authorization::find_by_id(authz.id.to_string().as_str(), &db)
1213 .await
1214 .unwrap()
1215 .unwrap();
1216 assert_eq!(reloaded.status, AuthzStatus::Invalid);
1217 }
1218
1219 #[tokio::test]
1222 async fn an_authorization_holds_one_challenge_per_type() {
1223 let db = Arc::new(Database::connect_for_test().await.unwrap());
1224 let oid = order_id(&db).await;
1225 let authz = Authorization::create(
1226 oid.parse().unwrap(),
1227 Identifier::dns("example.com"),
1228 now_secs() + 3600,
1229 &db,
1230 )
1231 .await
1232 .unwrap();
1233
1234 let http = Challenge::create(authz.id, "http-01", &db).await.unwrap();
1235 let dns_01 = Challenge::create(authz.id, "dns-01", &db).await.unwrap();
1236 Challenge::create(authz.id, "tls-alpn-01", &db)
1237 .await
1238 .unwrap();
1239
1240 assert_ne!(http.token, dns_01.token);
1242
1243 let challenges = Challenge::find_by_authz(authz.id, &db).await.unwrap();
1244 assert_eq!(challenges.len(), 3);
1245
1246 assert!(Challenge::create(authz.id, "http-01", &db).await.is_err());
1248 }
1249
1250 #[tokio::test]
1254 async fn a_wildcard_authorization_stores_the_prefix_and_renders_the_base_name() {
1255 let db = Arc::new(Database::connect_for_test().await.unwrap());
1256 let oid = order_id(&db).await;
1257
1258 let authz = Authorization::create(
1259 oid.parse().unwrap(),
1260 Identifier::dns("*.example.com"),
1261 now_secs() + 3600,
1262 &db,
1263 )
1264 .await
1265 .unwrap();
1266 let challenge = Challenge::create(authz.id, "dns-01", &db).await.unwrap();
1267
1268 let reloaded = Authorization::find_by_id(authz.id.to_string().as_str(), &db)
1271 .await
1272 .unwrap()
1273 .unwrap();
1274 assert_eq!(reloaded.identifier.value, "*.example.com");
1275 assert!(reloaded.is_wildcard());
1276 assert_eq!(reloaded.base_identifier(), "example.com");
1277
1278 let json = reloaded.to_json("http://localhost:3000", std::slice::from_ref(&challenge));
1279 assert_eq!(
1280 json["identifier"],
1281 serde_json::json!({"type":"dns","value":"example.com"})
1282 );
1283 assert_eq!(json["wildcard"], true);
1284 assert_eq!(json["challenges"][0]["type"], "dns-01");
1285 }
1286
1287 #[tokio::test]
1290 async fn a_plain_authorization_has_no_wildcard_member() {
1291 let db = Arc::new(Database::connect_for_test().await.unwrap());
1292 let oid = order_id(&db).await;
1293
1294 let authz = Authorization::create(
1295 oid.parse().unwrap(),
1296 Identifier::dns("example.com"),
1297 now_secs() + 3600,
1298 &db,
1299 )
1300 .await
1301 .unwrap();
1302 assert!(!authz.is_wildcard());
1303 assert_eq!(authz.base_identifier(), "example.com");
1304 assert!(
1305 authz
1306 .to_json("http://localhost:3000", &[])
1307 .get("wildcard")
1308 .is_none()
1309 );
1310 }
1311
1312 #[tokio::test]
1313 async fn absent_lookups_return_none() {
1314 let db = Arc::new(Database::connect_for_test().await.unwrap());
1315 assert!(
1316 Authorization::find_by_id("nope", &db)
1317 .await
1318 .unwrap()
1319 .is_none()
1320 );
1321 assert!(Challenge::find_by_id("nope", &db).await.unwrap().is_none());
1322 }
1323}