1use std::collections::HashMap;
14use std::sync::{Arc, Mutex};
15
16use async_trait::async_trait;
17use cheers_core::{PrincipalId, StoreError};
18
19use crate::bundles::ScopeOrBundle;
20
21#[async_trait]
27pub trait GrantStore: Send + Sync {
28 async fn list_for(
33 &self,
34 principal: &PrincipalId,
35 aud: &str,
36 ) -> Result<Vec<ScopeOrBundle>, StoreError>;
37}
38
39#[derive(Default, Clone)]
42pub struct MemoryGrantStore {
43 inner: Arc<Mutex<HashMap<(PrincipalId, String), Vec<ScopeOrBundle>>>>,
44}
45
46impl MemoryGrantStore {
47 pub fn new() -> Self {
48 Self::default()
49 }
50
51 pub fn put(
53 &self,
54 principal: PrincipalId,
55 aud: impl Into<String>,
56 entries: Vec<ScopeOrBundle>,
57 ) {
58 self.inner
59 .lock()
60 .expect("grant store mutex poisoned")
61 .insert((principal, aud.into()), entries);
62 }
63}
64
65#[async_trait]
66impl GrantStore for MemoryGrantStore {
67 async fn list_for(
68 &self,
69 principal: &PrincipalId,
70 aud: &str,
71 ) -> Result<Vec<ScopeOrBundle>, StoreError> {
72 Ok(self
73 .inner
74 .lock()
75 .expect("grant store mutex poisoned")
76 .get(&(principal.clone(), aud.to_owned()))
77 .cloned()
78 .unwrap_or_default())
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use cheers_core::Scope;
86 use pollster::block_on;
87
88 #[test]
89 fn memory_store_missing_key_yields_empty_list() {
90 let store = MemoryGrantStore::new();
91 let entries = block_on(store.list_for(&PrincipalId::user("u1"), "https://aud")).unwrap();
92 assert!(entries.is_empty());
93 }
94
95 #[test]
96 fn memory_store_put_then_list_roundtrip() {
97 let store = MemoryGrantStore::new();
98 let user = PrincipalId::user("alice");
99 store.put(
100 user.clone(),
101 "https://aud",
102 vec![
103 ScopeOrBundle::Scope(Scope::CloudRead),
104 ScopeOrBundle::Scope(Scope::CloudDeploy),
105 ],
106 );
107 let entries = block_on(store.list_for(&user, "https://aud")).unwrap();
108 assert_eq!(
109 entries,
110 vec![
111 ScopeOrBundle::Scope(Scope::CloudRead),
112 ScopeOrBundle::Scope(Scope::CloudDeploy),
113 ]
114 );
115 }
116
117 #[test]
118 fn memory_store_is_keyed_per_aud() {
119 let store = MemoryGrantStore::new();
120 let user = PrincipalId::user("alice");
121 store.put(
122 user.clone(),
123 "https://aud-a",
124 vec![ScopeOrBundle::Scope(Scope::CloudRead)],
125 );
126 let other = block_on(store.list_for(&user, "https://aud-b")).unwrap();
127 assert!(other.is_empty(), "other aud must not leak: {other:?}");
128 }
129}