Skip to main content

auth/
session_policy.rs

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