vtx_sdk/modules/auth/
user.rs

1//! Host-side auth helpers.
2
3use crate::bindings::vtx::api::vtx_auth_types::UserContext;
4use crate::error::{VtxError, VtxResult};
5
6/// Authentication request helper class.
7///
8/// Responsibilities:
9/// Encapsulates the raw Header list and provides convenient methods for Token extraction and validation.
10pub struct AuthRequest<'a> {
11    headers: &'a [(String, String)],
12}
13
14impl<'a> AuthRequest<'a> {
15    /// Instantiates an AuthRequest.
16    pub fn new(headers: &'a [(String, String)]) -> Self {
17        Self { headers }
18    }
19
20    /// Retrieves a Header value (Case-insensitive).
21    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    /// Retrieves a required Header value.
32    ///
33    /// Behavior:
34    /// Returns an `AuthDenied(401)` error if the Header does not exist.
35    pub fn require_header(&self, key: &str) -> VtxResult<&str> {
36        self.header(key).ok_or({
37            // Note: Information about which specific header is missing will be lost when converted to u16,
38            // but it may be useful during debugging or future log extensions.
39            VtxError::AuthDenied(401)
40        })
41    }
42
43    /// Extracts the Bearer Token.
44    ///
45    /// Format support: `Authorization: Bearer <token>` (case-insensitive for 'Bearer').
46    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    /// Retrieves the required Bearer Token.
56    ///
57    /// Behavior:
58    /// Returns `AuthDenied(401)` if the Authorization header is missing or incorrectly formatted.
59    pub fn require_bearer_token(&self) -> VtxResult<&str> {
60        self.bearer_token().ok_or(VtxError::AuthDenied(401))
61    }
62
63    /// Extracts Basic Auth credentials.
64    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
74/// User context builder (Builder Pattern).
75///
76/// Responsibilities:
77/// Constructs `UserContext` objects with support for chained calls.
78pub 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    /// Initializes the builder.
87    ///
88    /// Required parameters: User ID and Username.
89    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    /// Adds a group/role the user belongs to.
99    pub fn group(mut self, group: impl Into<String>) -> Self {
100        self.groups.push(group.into());
101        self
102    }
103
104    /// Adds a metadata key-value pair.
105    ///
106    /// If `value` fails to serialize, the field will be silently ignored.
107    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    /// Builds the UserContext.
115    ///
116    /// The result contains the serialized metadata JSON string.
117    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
127/// Authentication result conversion extension trait.
128///
129/// Responsibilities:
130/// Converts the standard SDK `VtxResult<UserContext>` into the `Result<UserContext, u16>` required by the WIT interface.
131/// This allows developers to handle DB or logic errors uniformly using the `?` operator in the `authenticate` implementation.
132pub 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                // Error degradation strategy: Maps rich error types to HTTP status codes.
142                let status_code = match e {
143                    VtxError::AuthDenied(code) => code,
144                    VtxError::PermissionDenied(_) => 403,
145                    VtxError::NotFound(_) => 404,
146                    // Database errors, serialization errors, or internal errors are treated uniformly as 500.
147                    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}