Skip to main content

auth/
session_policy.rs

1use crate::models::AuthUserId;
2use chrono::{DateTime, Utc};
3use platform_core::{AppContext, AppResult};
4use std::sync::Arc;
5
6#[derive(Debug, Clone, Default, PartialEq, Eq)]
7pub struct SessionCreateOptions {
8    pub device_id: Option<String>,
9}
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct SessionCreateInput {
13    pub user_id: AuthUserId,
14    pub session_id: String,
15    pub proposed_device_id: Option<String>,
16    pub created_at: DateTime<Utc>,
17    pub expires_at: DateTime<Utc>,
18}
19
20#[derive(Debug, Clone, Default, PartialEq, Eq)]
21pub struct SessionCreateDecision {
22    pub device_id: Option<String>,
23}
24
25#[async_trait::async_trait]
26pub trait AuthSessionPolicy: std::fmt::Debug + Send + Sync {
27    async fn before_session_create(
28        &self,
29        input: &SessionCreateInput,
30    ) -> AppResult<SessionCreateDecision>;
31}
32
33pub type AuthSessionPolicyFactory = fn(&AppContext) -> Arc<dyn AuthSessionPolicy>;
34
35#[derive(Debug, Clone, Copy)]
36pub struct AuthHostExtension {
37    session_policy: Option<AuthSessionPolicyFactory>,
38}
39
40impl AuthHostExtension {
41    #[must_use]
42    pub const fn session_policy(factory: AuthSessionPolicyFactory) -> Self {
43        Self {
44            session_policy: Some(factory),
45        }
46    }
47
48    #[must_use]
49    pub const fn session_policy_factory(self) -> Option<AuthSessionPolicyFactory> {
50        self.session_policy
51    }
52}
53
54#[derive(Debug, Clone)]
55pub struct AuthSessionPolicyHandle {
56    policy: Arc<dyn AuthSessionPolicy>,
57}
58
59impl AuthSessionPolicyHandle {
60    #[must_use]
61    pub fn new(policy: Arc<dyn AuthSessionPolicy>) -> Self {
62        Self { policy }
63    }
64
65    #[must_use]
66    pub fn allow() -> Self {
67        Self::new(Arc::new(AllowSessionPolicy))
68    }
69
70    #[must_use]
71    pub fn policy(&self) -> &dyn AuthSessionPolicy {
72        self.policy.as_ref()
73    }
74
75    #[must_use]
76    pub fn into_policy(self) -> Arc<dyn AuthSessionPolicy> {
77        self.policy
78    }
79}
80
81impl Default for AuthSessionPolicyHandle {
82    fn default() -> Self {
83        Self::allow()
84    }
85}
86
87#[derive(Debug, Clone)]
88pub struct AuthSessionPolicyChain {
89    policies: Vec<Arc<dyn AuthSessionPolicy>>,
90}
91
92impl AuthSessionPolicyChain {
93    #[must_use]
94    pub fn new(policies: Vec<Arc<dyn AuthSessionPolicy>>) -> Self {
95        Self { policies }
96    }
97
98    #[must_use]
99    pub fn handle(policies: Vec<Arc<dyn AuthSessionPolicy>>) -> AuthSessionPolicyHandle {
100        if policies.is_empty() {
101            AuthSessionPolicyHandle::allow()
102        } else {
103            AuthSessionPolicyHandle::new(Arc::new(Self::new(policies)))
104        }
105    }
106}
107
108#[async_trait::async_trait]
109impl AuthSessionPolicy for AuthSessionPolicyChain {
110    async fn before_session_create(
111        &self,
112        input: &SessionCreateInput,
113    ) -> AppResult<SessionCreateDecision> {
114        let mut next_input = input.clone();
115        let mut decision = AllowSessionPolicy
116            .before_session_create(&next_input)
117            .await?;
118
119        for policy in &self.policies {
120            next_input.proposed_device_id = decision.device_id;
121            decision = policy.before_session_create(&next_input).await?;
122        }
123
124        Ok(decision)
125    }
126}
127
128#[derive(Debug, Default)]
129pub struct AllowSessionPolicy;
130
131#[async_trait::async_trait]
132impl AuthSessionPolicy for AllowSessionPolicy {
133    async fn before_session_create(
134        &self,
135        input: &SessionCreateInput,
136    ) -> AppResult<SessionCreateDecision> {
137        Ok(SessionCreateDecision {
138            device_id: input.proposed_device_id.clone(),
139        })
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use chrono::Utc;
147    use std::sync::Arc;
148
149    #[tokio::test]
150    async fn policy_chain_applies_session_policies_in_order() {
151        let chain = AuthSessionPolicyChain::new(vec![
152            Arc::new(SuffixPolicy("-trusted")),
153            Arc::new(SuffixPolicy("-primary")),
154        ]);
155        let now = Utc::now();
156
157        let decision = chain
158            .before_session_create(&SessionCreateInput {
159                user_id: AuthUserId("usr_policy".to_owned()),
160                session_id: "sess_policy".to_owned(),
161                proposed_device_id: Some("device".to_owned()),
162                created_at: now,
163                expires_at: now,
164            })
165            .await
166            .expect("policy chain should allow session");
167
168        assert_eq!(
169            decision.device_id.as_deref(),
170            Some("device-trusted-primary")
171        );
172    }
173
174    #[derive(Debug)]
175    struct SuffixPolicy(&'static str);
176
177    #[async_trait::async_trait]
178    impl AuthSessionPolicy for SuffixPolicy {
179        async fn before_session_create(
180            &self,
181            input: &SessionCreateInput,
182        ) -> AppResult<SessionCreateDecision> {
183            Ok(SessionCreateDecision {
184                device_id: input
185                    .proposed_device_id
186                    .as_ref()
187                    .map(|device_id| format!("{device_id}{}", self.0)),
188            })
189        }
190    }
191}