vtx_sdk/modules/auth/
user.rs1use crate::bindings::vtx::api::vtx_auth_types::UserContext;
4use crate::error::{VtxError, VtxResult};
5
6pub struct AuthRequest<'a> {
11 headers: &'a [(String, String)],
12}
13
14impl<'a> AuthRequest<'a> {
15 pub fn new(headers: &'a [(String, String)]) -> Self {
17 Self { headers }
18 }
19
20 pub fn header(&self, key: &str) -> Option<&str> {
22 let search_key = key.to_lowercase();
23 for (k, v) in self.headers {
24 if k.to_lowercase() == search_key {
25 return Some(v.as_str());
26 }
27 }
28 None
29 }
30
31 pub fn require_header(&self, key: &str) -> VtxResult<&str> {
36 self.header(key).ok_or({
37 VtxError::AuthDenied(401)
40 })
41 }
42
43 pub fn bearer_token(&self) -> Option<&str> {
47 let val = self.header("Authorization")?;
48 if val.starts_with("Bearer ") || val.starts_with("bearer ") {
49 Some(&val[7..])
50 } else {
51 None
52 }
53 }
54
55 pub fn require_bearer_token(&self) -> VtxResult<&str> {
60 self.bearer_token().ok_or(VtxError::AuthDenied(401))
61 }
62
63 pub fn basic_auth(&self) -> Option<&str> {
65 let val = self.header("Authorization")?;
66 if val.starts_with("Basic ") || val.starts_with("basic ") {
67 Some(&val[6..])
68 } else {
69 None
70 }
71 }
72}
73
74pub struct UserBuilder {
79 user_id: String,
80 username: String,
81 groups: Vec<String>,
82 metadata: serde_json::Map<String, serde_json::Value>,
83}
84
85impl UserBuilder {
86 pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
90 Self {
91 user_id: id.into(),
92 username: name.into(),
93 groups: Vec::new(),
94 metadata: serde_json::Map::new(),
95 }
96 }
97
98 pub fn group(mut self, group: impl Into<String>) -> Self {
100 self.groups.push(group.into());
101 self
102 }
103
104 pub fn meta<V: serde::Serialize>(mut self, key: &str, value: V) -> Self {
108 if let Ok(val) = serde_json::to_value(value) {
109 self.metadata.insert(key.to_string(), val);
110 }
111 self
112 }
113
114 pub fn build(self) -> UserContext {
118 UserContext {
119 user_id: self.user_id,
120 username: self.username,
121 groups: self.groups,
122 metadata: serde_json::to_string(&self.metadata).unwrap_or_else(|_| "{}".to_string()),
123 }
124 }
125}
126
127pub trait IntoAuthResult {
133 fn into_auth_result(self) -> Result<UserContext, u16>;
134}
135
136impl IntoAuthResult for VtxResult<UserContext> {
137 fn into_auth_result(self) -> Result<UserContext, u16> {
138 match self {
139 Ok(ctx) => Ok(ctx),
140 Err(e) => {
141 let status_code = match e {
143 VtxError::AuthDenied(code) => code,
144 VtxError::PermissionDenied(_) => 403,
145 VtxError::NotFound(_) => 404,
146 VtxError::DatabaseError(_)
148 | VtxError::SerializationError(_)
149 | VtxError::Internal(_) => 500,
150 };
151 Err(status_code)
152 }
153 }
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::{AuthRequest, IntoAuthResult, UserBuilder};
160 use crate::error::VtxError;
161 use serde::Serialize;
162
163 #[test]
164 fn header_lookup_is_case_insensitive() {
165 let headers = vec![
166 ("authorization".to_string(), "Bearer token".to_string()),
167 ("X-Trace-Id".to_string(), "abc".to_string()),
168 ];
169 let req = AuthRequest::new(&headers);
170 assert_eq!(req.header("Authorization"), Some("Bearer token"));
171 assert_eq!(req.header("x-trace-id"), Some("abc"));
172 assert_eq!(req.header("missing"), None);
173 }
174
175 #[test]
176 fn bearer_and_basic_auth_parsing() {
177 let headers = vec![("Authorization".to_string(), "Bearer abc".to_string())];
178 let req = AuthRequest::new(&headers);
179 assert_eq!(req.bearer_token(), Some("abc"));
180 assert!(req.basic_auth().is_none());
181
182 let headers = vec![(
183 "Authorization".to_string(),
184 "Basic Zm9vOmJhcg==".to_string(),
185 )];
186 let req = AuthRequest::new(&headers);
187 assert_eq!(req.basic_auth(), Some("Zm9vOmJhcg=="));
188 assert!(req.bearer_token().is_none());
189 }
190
191 #[test]
192 fn require_bearer_token_rejects_missing_or_invalid() {
193 let headers = vec![("Authorization".to_string(), "Token abc".to_string())];
194 let req = AuthRequest::new(&headers);
195 let err = req.require_bearer_token().unwrap_err();
196 assert!(matches!(err, VtxError::AuthDenied(401)));
197 }
198
199 #[test]
200 fn user_builder_ignores_unserializable_meta() {
201 struct BadSerialize;
202 impl Serialize for BadSerialize {
203 fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
204 where
205 S: serde::Serializer,
206 {
207 Err(serde::ser::Error::custom("nope"))
208 }
209 }
210
211 let ctx = UserBuilder::new("u1", "tester")
212 .group("admin")
213 .meta("good", 123)
214 .meta("bad", BadSerialize)
215 .build();
216
217 let meta: serde_json::Value =
218 serde_json::from_str(&ctx.metadata).expect("valid metadata json");
219 assert_eq!(meta["good"], 123);
220 assert!(meta.get("bad").is_none());
221 }
222
223 #[test]
224 fn into_auth_result_maps_errors_to_status_codes() {
225 let err = Err(VtxError::PermissionDenied("nope".to_string())).into_auth_result();
226 assert!(matches!(err, Err(403)));
227
228 let err = Err(VtxError::NotFound("missing".to_string())).into_auth_result();
229 assert!(matches!(err, Err(404)));
230
231 let err = Err(VtxError::Internal("boom".to_string())).into_auth_result();
232 assert!(matches!(err, Err(500)));
233 }
234}