use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use cheers_core::{PrincipalId, StoreError};
use crate::bundles::ScopeOrBundle;
#[async_trait]
pub trait GrantStore: Send + Sync {
async fn list_for(
&self,
principal: &PrincipalId,
aud: &str,
) -> Result<Vec<ScopeOrBundle>, StoreError>;
}
#[derive(Default, Clone)]
pub struct MemoryGrantStore {
inner: Arc<Mutex<HashMap<(PrincipalId, String), Vec<ScopeOrBundle>>>>,
}
impl MemoryGrantStore {
pub fn new() -> Self {
Self::default()
}
pub fn put(
&self,
principal: PrincipalId,
aud: impl Into<String>,
entries: Vec<ScopeOrBundle>,
) {
self.inner
.lock()
.expect("grant store mutex poisoned")
.insert((principal, aud.into()), entries);
}
}
#[async_trait]
impl GrantStore for MemoryGrantStore {
async fn list_for(
&self,
principal: &PrincipalId,
aud: &str,
) -> Result<Vec<ScopeOrBundle>, StoreError> {
Ok(self
.inner
.lock()
.expect("grant store mutex poisoned")
.get(&(principal.clone(), aud.to_owned()))
.cloned()
.unwrap_or_default())
}
}
#[cfg(test)]
mod tests {
use super::*;
use cheers_core::Scope;
use pollster::block_on;
#[test]
fn memory_store_missing_key_yields_empty_list() {
let store = MemoryGrantStore::new();
let entries = block_on(store.list_for(&PrincipalId::user("u1"), "https://aud")).unwrap();
assert!(entries.is_empty());
}
#[test]
fn memory_store_put_then_list_roundtrip() {
let store = MemoryGrantStore::new();
let user = PrincipalId::user("alice");
store.put(
user.clone(),
"https://aud",
vec![
ScopeOrBundle::Scope(Scope::CloudRead),
ScopeOrBundle::Scope(Scope::CloudDeploy),
],
);
let entries = block_on(store.list_for(&user, "https://aud")).unwrap();
assert_eq!(
entries,
vec![
ScopeOrBundle::Scope(Scope::CloudRead),
ScopeOrBundle::Scope(Scope::CloudDeploy),
]
);
}
#[test]
fn memory_store_is_keyed_per_aud() {
let store = MemoryGrantStore::new();
let user = PrincipalId::user("alice");
store.put(
user.clone(),
"https://aud-a",
vec![ScopeOrBundle::Scope(Scope::CloudRead)],
);
let other = block_on(store.list_for(&user, "https://aud-b")).unwrap();
assert!(other.is_empty(), "other aud must not leak: {other:?}");
}
}