grants.rs 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. //! Grant chains and the per-grant token index.
  2. //!
  3. //! Links rotated families so reuse anywhere in the chain triggers the
  4. //! cascade; no `KEYS` scans in request paths (per-grant sets + Lua).
  5. use std::{collections::HashMap, time::Duration};
  6. use serde::{Deserialize, Serialize};
  7. use super::{SCRIPT_GRANT_REVOKE, Store, StoreError, hset_record, retry_op};
  8. use crate::state::RedisConn;
  9. /// A grant-chain record: the durable link between rotated token families.
  10. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  11. pub struct GrantRecord {
  12. /// Grant identifier (also the token-set suffix).
  13. pub grant_id: String,
  14. /// Client the grant was issued to.
  15. pub client_id: String,
  16. /// Resource-owner subject, empty for client-credentials grants.
  17. pub subject: String,
  18. /// Space-joined scope.
  19. pub scope: String,
  20. }
  21. impl GrantRecord {
  22. fn to_pairs(&self) -> Vec<(String, String)> {
  23. vec![
  24. ("grant_id".to_owned(), self.grant_id.clone()),
  25. ("client_id".to_owned(), self.client_id.clone()),
  26. ("subject".to_owned(), self.subject.clone()),
  27. ("scope".to_owned(), self.scope.clone()),
  28. ]
  29. }
  30. fn from_map(grant_id: &str, map: &HashMap<String, String>) -> Result<Self, StoreError> {
  31. let get = |field: &str| map.get(field).cloned().ok_or(StoreError::InvalidData);
  32. Ok(Self {
  33. grant_id: grant_id.to_owned(),
  34. client_id: get("client_id")?,
  35. subject: get("subject")?,
  36. scope: get("scope")?,
  37. })
  38. }
  39. }
  40. /// Create a grant-chain record (idempotent rewrite).
  41. pub async fn create(
  42. conn: &mut RedisConn,
  43. store: &Store,
  44. record: &GrantRecord,
  45. ttl: Duration,
  46. ) -> Result<(), StoreError> {
  47. let key = store.grant_key(&record.grant_id);
  48. let pairs = record.to_pairs();
  49. retry_op!(conn, store.retry_budget(), async {
  50. hset_record(conn, &key, &pairs, ttl)
  51. .await
  52. .map_err(grants_redis)
  53. })
  54. .map_err(StoreError::Redis)
  55. }
  56. /// Read a grant-chain record; `Ok(None)` when absent.
  57. pub async fn get(
  58. conn: &mut RedisConn,
  59. store: &Store,
  60. grant_id: &str,
  61. ) -> Result<Option<GrantRecord>, StoreError> {
  62. let key = store.grant_key(grant_id);
  63. let budget = store.retry_budget();
  64. let map = retry_op!(conn, budget, async {
  65. super::hget_record(conn, &key).await.map_err(grants_redis)
  66. })
  67. .map_err(StoreError::Redis)?;
  68. map
  69. .map(|fields| GrantRecord::from_map(grant_id, &fields))
  70. .transpose()
  71. }
  72. /// List the token hashes indexed under a grant.
  73. pub async fn members(
  74. conn: &mut RedisConn,
  75. store: &Store,
  76. grant_id: &str,
  77. ) -> Result<Vec<String>, StoreError> {
  78. let set_key = store.grant_tokens_key(grant_id);
  79. retry_op!(conn, store.retry_budget(), async {
  80. redis::cmd("SMEMBERS")
  81. .arg(&set_key)
  82. .query_async::<Vec<String>>(conn)
  83. .await
  84. })
  85. .map_err(StoreError::Redis)
  86. }
  87. /// Cascade-revoke a grant: delete every member token key plus the grant set
  88. /// and record (single-slot Lua, no `KEYS` scan).
  89. ///
  90. /// One script pass is atomic but blind to a mint racing the cascade: a member
  91. /// `SADD`ed after the pass's `SMEMBERS` snapshot would survive with live
  92. /// records. The pass therefore repeats until the set reads empty (bounded),
  93. /// so a token minted mid-cascade is deleted by a later pass instead of
  94. /// surviving the revoke.
  95. ///
  96. /// The bound is fail-closed: when the set is still non-empty after the pass
  97. /// budget (a mint landing after every snapshot), revocation reports
  98. /// [`super::StoreError::InvalidData`] instead of a silent success count, and
  99. /// a post-barrier re-read rejects a member `SADD`ed after the final pass the
  100. /// same way — a late `SADD` can never leave a live TTL-less member behind
  101. /// quietly.
  102. ///
  103. /// Returns the number of token keys deleted. Unknown grants revoke to zero.
  104. pub async fn revoke(
  105. conn: &mut RedisConn,
  106. store: &Store,
  107. grant_id: &str,
  108. ) -> Result<u64, StoreError> {
  109. revoke_with_passes(conn, store, grant_id, REVOKE_DRAIN_PASSES).await
  110. }
  111. /// Drain budget: enough passes to absorb a racing mint, small enough to fail
  112. /// closed fast under continuous churn.
  113. const REVOKE_DRAIN_PASSES: u32 = 8;
  114. /// Cascade-revoke with an explicit drain budget (the production path uses
  115. /// [`REVOKE_DRAIN_PASSES`]); a zero budget models an already-exhausted drain.
  116. pub(crate) async fn revoke_with_passes(
  117. conn: &mut RedisConn,
  118. store: &Store,
  119. grant_id: &str,
  120. max_passes: u32,
  121. ) -> Result<u64, StoreError> {
  122. let set_key = store.grant_tokens_key(grant_id);
  123. let grant_key = store.grant_key(grant_id);
  124. let refresh_prefix = store.refresh_key_prefix();
  125. let access_prefix = store.access_key_prefix();
  126. let mut total = 0_u64;
  127. let mut drained = false;
  128. for _ in 0..max_passes {
  129. let mut invocation = SCRIPT_GRANT_REVOKE.prepare_invoke();
  130. invocation.key(set_key.as_str()).key(grant_key.as_str());
  131. invocation
  132. .arg(refresh_prefix.as_str())
  133. .arg(access_prefix.as_str());
  134. let deleted: u64 = invocation
  135. .invoke_async(conn)
  136. .await
  137. .map_err(StoreError::Redis)?;
  138. total = total.saturating_add(deleted);
  139. let remaining: Vec<String> = redis::cmd("SMEMBERS")
  140. .arg(&set_key)
  141. .query_async(conn)
  142. .await
  143. .map_err(StoreError::Redis)?;
  144. if remaining.is_empty() {
  145. drained = true;
  146. break;
  147. }
  148. }
  149. if !drained {
  150. return Err(StoreError::InvalidData);
  151. }
  152. // Revocation is a reuse-driven security response: barrier it like any
  153. // tombstone-class write before the caller reports the outcome.
  154. super::durability_barrier(conn, store).await?;
  155. // A member `SADD`ed after the final pass would otherwise survive with live
  156. // records behind a success count: re-read after the barrier and fail closed.
  157. let late: Vec<String> = redis::cmd("SMEMBERS")
  158. .arg(&set_key)
  159. .query_async(conn)
  160. .await
  161. .map_err(StoreError::Redis)?;
  162. if late.is_empty() {
  163. Ok(total)
  164. } else {
  165. Err(StoreError::InvalidData)
  166. }
  167. }
  168. /// Cascade-revoke the grant family containing `token_hash`: resolve the grant
  169. /// through the token record, then [`revoke`].
  170. pub async fn revoke_by_token_hash(
  171. conn: &mut RedisConn,
  172. store: &Store,
  173. token_hash: &str,
  174. ) -> Result<u64, StoreError> {
  175. let grant_id = grant_of_token(conn, store, token_hash).await?;
  176. match grant_id {
  177. Some(id) => revoke(conn, store, &id).await,
  178. None => Ok(0),
  179. }
  180. }
  181. /// Resolve the grant id holding `token_hash` (refresh record first, then
  182. /// access record); `Ok(None)` when the token is unknown.
  183. async fn grant_of_token(
  184. conn: &mut RedisConn,
  185. store: &Store,
  186. token_hash: &str,
  187. ) -> Result<Option<String>, StoreError> {
  188. for key in [store.refresh_key(token_hash), store.access_key(token_hash)] {
  189. let map = super::hget_record(conn, &key).await?;
  190. if let Some(fields) = map {
  191. return fields
  192. .get("grant_id")
  193. .cloned()
  194. .filter(|id| !id.is_empty())
  195. .map_or(Err(StoreError::InvalidData), |id| Ok(Some(id)));
  196. }
  197. }
  198. Ok(None)
  199. }
  200. fn grants_redis(error: StoreError) -> redis::RedisError {
  201. match error {
  202. StoreError::Redis(inner) => inner,
  203. _ => redis::RedisError::from((redis::ErrorKind::Client, "store grant write failed")),
  204. }
  205. }