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}