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}