Skip to main content

arcly_http_identity/
consent.rs

1//! Consent capture & withdrawal (GDPR / privacy). A [`ConsentStore`] records
2//! which purposes a subject has agreed to, with a timestamp and version, so the
3//! app can prove lawful basis and honour withdrawal.
4
5use std::collections::HashMap;
6use std::sync::RwLock;
7use std::time::{SystemTime, UNIX_EPOCH};
8
9use async_trait::async_trait;
10use serde::{Deserialize, Serialize};
11
12use crate::error::Result;
13
14/// A single consent grant for one purpose.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ConsentGrant {
17    pub purpose: String,
18    /// Version of the policy/notice the subject agreed to.
19    pub version: String,
20    pub granted: bool,
21    pub timestamp: u64,
22}
23
24/// Storage for consent records. Back with an append-only table so withdrawals
25/// are recorded rather than overwriting history (audit requirement).
26#[async_trait]
27pub trait ConsentStore: Send + Sync + 'static {
28    async fn record(&self, user_id: &str, grant: ConsentGrant) -> Result<()>;
29    /// Latest state per purpose for the subject.
30    async fn current(&self, user_id: &str) -> Result<Vec<ConsentGrant>>;
31    /// Whether the subject currently consents to `purpose`.
32    async fn has_consent(&self, user_id: &str, purpose: &str) -> Result<bool> {
33        Ok(self
34            .current(user_id)
35            .await?
36            .into_iter()
37            .find(|g| g.purpose == purpose)
38            .map(|g| g.granted)
39            .unwrap_or(false))
40    }
41}
42
43pub fn now() -> u64 {
44    SystemTime::now()
45        .duration_since(UNIX_EPOCH)
46        .map(|d| d.as_secs())
47        .unwrap_or(0)
48}
49
50/// In-memory [`ConsentStore`] keeping full history — tests / dev only.
51#[derive(Default)]
52pub struct MemoryConsentStore {
53    inner: RwLock<HashMap<String, Vec<ConsentGrant>>>,
54}
55impl MemoryConsentStore {
56    pub fn new() -> Self {
57        Self::default()
58    }
59}
60
61#[async_trait]
62impl ConsentStore for MemoryConsentStore {
63    async fn record(&self, user_id: &str, grant: ConsentGrant) -> Result<()> {
64        self.inner
65            .write()
66            .unwrap()
67            .entry(user_id.to_owned())
68            .or_default()
69            .push(grant);
70        Ok(())
71    }
72
73    async fn current(&self, user_id: &str) -> Result<Vec<ConsentGrant>> {
74        let guard = self.inner.read().unwrap();
75        let Some(history) = guard.get(user_id) else {
76            return Ok(Vec::new());
77        };
78        // Collapse to the latest grant per purpose.
79        let mut latest: HashMap<&str, &ConsentGrant> = HashMap::new();
80        for g in history {
81            latest
82                .entry(g.purpose.as_str())
83                .and_modify(|cur| {
84                    if g.timestamp >= cur.timestamp {
85                        *cur = g;
86                    }
87                })
88                .or_insert(g);
89        }
90        Ok(latest.into_values().cloned().collect())
91    }
92}