1use super::{CertIdentity, OAuthIdentity, Role, UserId};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum AuthSource {
15 Password,
17 ClientCert,
19 Oauth,
21}
22
23#[derive(Debug, Clone)]
25pub enum AuthResult {
26 Authenticated {
28 principal: UserId,
29 role: Role,
30 source: AuthSource,
34 },
35 Anonymous,
37 Denied(String),
39}
40
41impl AuthResult {
42 pub fn password(username: impl Into<String>, role: Role) -> Self {
45 Self::Authenticated {
46 principal: UserId::platform(username),
47 role,
48 source: AuthSource::Password,
49 }
50 }
51
52 pub fn password_principal(principal: UserId, role: Role) -> Self {
53 Self::Authenticated {
54 principal,
55 role,
56 source: AuthSource::Password,
57 }
58 }
59
60 pub fn from_cert(id: CertIdentity) -> Self {
62 Self::Authenticated {
63 principal: UserId::platform(id.username),
64 role: id.role,
65 source: AuthSource::ClientCert,
66 }
67 }
68
69 pub fn from_oauth(id: OAuthIdentity) -> Self {
71 Self::Authenticated {
72 principal: UserId::from_parts(id.tenant.as_deref(), &id.username),
73 role: id.role,
74 source: AuthSource::Oauth,
75 }
76 }
77
78 pub fn summary(&self) -> String {
80 match self {
81 Self::Authenticated {
82 principal,
83 role,
84 source,
85 } => {
86 let src = match source {
87 AuthSource::Password => "pwd",
88 AuthSource::ClientCert => "cert",
89 AuthSource::Oauth => "oauth",
90 };
91 format!("user={principal} role={role} via={src}")
92 }
93 Self::Anonymous => "anonymous".to_string(),
94 Self::Denied(reason) => format!("denied: {reason}"),
95 }
96 }
97
98 pub fn is_authenticated(&self) -> bool {
100 matches!(self, Self::Authenticated { .. })
101 }
102}
103
104pub fn check_permission(
114 auth: &AuthResult,
115 requires_write: bool,
116 requires_admin: bool,
117) -> Result<(), String> {
118 match auth {
119 AuthResult::Authenticated { role, .. } => {
120 if requires_admin && !role.can_admin() {
121 return Err("admin role required".into());
122 }
123 if requires_write && !role.can_write() {
124 return Err("write permission required".into());
125 }
126 Ok(())
127 }
128 AuthResult::Anonymous => {
129 if requires_admin {
130 return Err("admin authentication required".into());
131 }
132 Ok(())
134 }
135 AuthResult::Denied(reason) => Err(reason.clone()),
136 }
137}
138
139#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn test_admin_can_do_everything() {
149 let auth = AuthResult::Authenticated {
150 principal: UserId::platform("root"),
151 role: Role::Admin,
152 source: AuthSource::Password,
153 };
154 assert!(check_permission(&auth, false, false).is_ok());
155 assert!(check_permission(&auth, true, false).is_ok());
156 assert!(check_permission(&auth, false, true).is_ok());
157 assert!(check_permission(&auth, true, true).is_ok());
158 }
159
160 #[test]
161 fn test_write_role_cannot_admin() {
162 let auth = AuthResult::Authenticated {
163 principal: UserId::platform("writer"),
164 role: Role::Write,
165 source: AuthSource::Password,
166 };
167 assert!(check_permission(&auth, false, false).is_ok());
168 assert!(check_permission(&auth, true, false).is_ok());
169 assert!(check_permission(&auth, false, true).is_err());
170 }
171
172 #[test]
173 fn test_read_role_cannot_write() {
174 let auth = AuthResult::Authenticated {
175 principal: UserId::platform("reader"),
176 role: Role::Read,
177 source: AuthSource::Password,
178 };
179 assert!(check_permission(&auth, false, false).is_ok());
180 assert!(check_permission(&auth, true, false).is_err());
181 assert!(check_permission(&auth, false, true).is_err());
182 }
183
184 #[test]
185 fn test_anonymous_access() {
186 let auth = AuthResult::Anonymous;
187 assert!(check_permission(&auth, false, false).is_ok());
189 assert!(check_permission(&auth, true, false).is_ok());
190 assert!(check_permission(&auth, false, true).is_err());
191 }
192
193 #[test]
194 fn test_denied_always_fails() {
195 let auth = AuthResult::Denied("bad token".into());
196 assert!(check_permission(&auth, false, false).is_err());
197 assert!(check_permission(&auth, true, true).is_err());
198 }
199
200 #[test]
201 fn test_auth_result_summary() {
202 let auth = AuthResult::Authenticated {
203 principal: UserId::platform("alice"),
204 role: Role::Admin,
205 source: AuthSource::Password,
206 };
207 assert!(auth.summary().contains("alice"));
208 assert!(auth.is_authenticated());
209
210 let anon = AuthResult::Anonymous;
211 assert_eq!(anon.summary(), "anonymous");
212 assert!(!anon.is_authenticated());
213 }
214
215 #[test]
216 fn oauth_result_preserves_tenant_principal() {
217 let auth = AuthResult::from_oauth(OAuthIdentity {
218 username: "alice".to_string(),
219 tenant: Some("acme".to_string()),
220 role: Role::Read,
221 issuer: "https://issuer.example".to_string(),
222 subject: None,
223 expires_at_unix_secs: None,
224 });
225 let AuthResult::Authenticated { principal, .. } = auth else {
226 panic!("expected authenticated result");
227 };
228 assert_eq!(principal, UserId::scoped("acme", "alice"));
229 }
230}