Skip to main content

securitydept_utils/
observability.rs

1use serde::Serialize;
2use serde_json::{Map, Value};
3
4pub struct AuthFlowOperation;
5
6impl AuthFlowOperation {
7    pub const PROJECTION_CONFIG_FETCH: &'static str = "projection.config_fetch";
8    pub const OIDC_AUTHORIZE: &'static str = "oidc.authorize";
9    pub const OIDC_CALLBACK: &'static str = "oidc.callback";
10    pub const OIDC_METADATA_REDEEM: &'static str = "oidc.metadata_redeem";
11    pub const OIDC_TOKEN_REFRESH: &'static str = "oidc.token_refresh";
12    pub const OIDC_USER_INFO: &'static str = "oidc.user_info";
13    pub const FORWARD_AUTH_CHECK: &'static str = "forward_auth.check";
14    pub const PROPAGATION_FORWARD: &'static str = "propagation.forward";
15    pub const BASIC_AUTH_LOGIN: &'static str = "basic_auth.login";
16    pub const BASIC_AUTH_LOGOUT: &'static str = "basic_auth.logout";
17    pub const BASIC_AUTH_AUTHORIZE: &'static str = "basic_auth.authorize";
18    pub const SESSION_LOGIN: &'static str = "session.login";
19    pub const SESSION_LOGOUT: &'static str = "session.logout";
20    pub const SESSION_USER_INFO: &'static str = "session.user_info";
21    pub const DASHBOARD_AUTH_CHECK: &'static str = "dashboard_auth.check";
22    pub const CREDS_MANAGE_GROUP_LIST: &'static str = "creds_manage.group.list";
23    pub const CREDS_MANAGE_GROUP_GET: &'static str = "creds_manage.group.get";
24    pub const CREDS_MANAGE_GROUP_CREATE: &'static str = "creds_manage.group.create";
25    pub const CREDS_MANAGE_GROUP_UPDATE: &'static str = "creds_manage.group.update";
26    pub const CREDS_MANAGE_GROUP_DELETE: &'static str = "creds_manage.group.delete";
27    pub const CREDS_MANAGE_ENTRY_LIST: &'static str = "creds_manage.entry.list";
28    pub const CREDS_MANAGE_ENTRY_GET: &'static str = "creds_manage.entry.get";
29    pub const CREDS_MANAGE_ENTRY_CREATE_BASIC: &'static str = "creds_manage.entry.create_basic";
30    pub const CREDS_MANAGE_ENTRY_CREATE_TOKEN: &'static str = "creds_manage.entry.create_token";
31    pub const CREDS_MANAGE_ENTRY_UPDATE: &'static str = "creds_manage.entry.update";
32    pub const CREDS_MANAGE_ENTRY_DELETE: &'static str = "creds_manage.entry.delete";
33}
34
35pub struct AuthFlowDiagnosisField;
36
37impl AuthFlowDiagnosisField {
38    pub const ADAPTER: &'static str = "adapter";
39    pub const ACCESS_TOKEN_PRESENT: &'static str = "access_token_present";
40    pub const AUTH_FAMILY: &'static str = "auth_family";
41    pub const AUTH_SCHEME: &'static str = "auth_scheme";
42    pub const CALLBACK_PATH: &'static str = "callback_path";
43    pub const CREDENTIAL_SOURCE: &'static str = "credential_source";
44    pub const DIRECTIVE_HEADER: &'static str = "directive_header";
45    pub const ENTRY_IDS_COUNT: &'static str = "entry_ids_count";
46    pub const ENTRY_NAME: &'static str = "entry_name";
47    pub const ENTITY_KIND: &'static str = "entity_kind";
48    pub const EXTERNAL_BASE_URL: &'static str = "external_base_url";
49    pub const FAILURE_STAGE: &'static str = "failure_stage";
50    pub const GROUP: &'static str = "group";
51    pub const GROUP_ID: &'static str = "group_id";
52    pub const GROUP_IDS_COUNT: &'static str = "group_ids_count";
53    pub const HAS_AUTHORIZATION_HEADER: &'static str = "has_authorization_header";
54    pub const HAS_CODE: &'static str = "has_code";
55    pub const HAS_COOKIE_HEADER: &'static str = "has_cookie_header";
56    pub const HAS_ID_TOKEN: &'static str = "has_id_token";
57    pub const HAS_METADATA: &'static str = "has_metadata";
58    pub const HAS_POST_AUTH_REDIRECT_URI: &'static str = "has_post_auth_redirect_uri";
59    pub const HAS_PROPAGATION_DIRECTIVE: &'static str = "has_propagation_directive";
60    pub const HAS_REQUESTED_POST_AUTH_REDIRECT_URI: &'static str =
61        "has_requested_post_auth_redirect_uri";
62    pub const HAS_STATE: &'static str = "has_state";
63    pub const HAS_TARGET_ID: &'static str = "has_target_id";
64    pub const HTTP_STATUS: &'static str = "http_status";
65    pub const METADATA_ID_PRESENT: &'static str = "metadata_id_present";
66    pub const METADATA_REDEEMED: &'static str = "metadata_redeemed";
67    pub const METHOD: &'static str = "method";
68    pub const MODE: &'static str = "mode";
69    pub const OPERATION_KIND: &'static str = "operation_kind";
70    pub const POST_AUTH_REDIRECT_PRESENT: &'static str = "post_auth_redirect_present";
71    pub const PROPAGATION_ENABLED: &'static str = "propagation_enabled";
72    pub const REASON: &'static str = "reason";
73    pub const REQUEST_PATH: &'static str = "request_path";
74    pub const RESPONSE_TRANSPORT: &'static str = "response_transport";
75    pub const RESOLVED_CLIENT_IP_PRESENT: &'static str = "resolved_client_ip_present";
76    pub const RESULT_COUNT: &'static str = "result_count";
77    pub const ROUTE: &'static str = "route";
78    pub const STATUS: &'static str = "status";
79    pub const SUBJECT: &'static str = "subject";
80    pub const TARGET_ID: &'static str = "target_id";
81    pub const TARGET_PATH: &'static str = "target_path";
82    pub const TOKEN_CREATED: &'static str = "token_created";
83    pub const TRANSPORT: &'static str = "transport";
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
87#[serde(rename_all = "snake_case")]
88pub enum AuthFlowDiagnosisOutcome {
89    Started,
90    Succeeded,
91    Failed,
92    Rejected,
93}
94
95impl AuthFlowDiagnosisOutcome {
96    pub const fn as_str(self) -> &'static str {
97        match self {
98            Self::Started => "started",
99            Self::Succeeded => "succeeded",
100            Self::Failed => "failed",
101            Self::Rejected => "rejected",
102        }
103    }
104}
105
106#[derive(Debug, Clone, PartialEq, Serialize)]
107pub struct AuthFlowDiagnosis {
108    pub operation: String,
109    pub outcome: AuthFlowDiagnosisOutcome,
110    #[serde(default, skip_serializing_if = "Map::is_empty")]
111    pub fields: Map<String, Value>,
112}
113
114impl AuthFlowDiagnosis {
115    pub fn new(operation: impl Into<String>, outcome: AuthFlowDiagnosisOutcome) -> Self {
116        Self {
117            operation: operation.into(),
118            outcome,
119            fields: Map::new(),
120        }
121    }
122
123    pub fn started(operation: impl Into<String>) -> Self {
124        Self::new(operation, AuthFlowDiagnosisOutcome::Started)
125    }
126
127    pub fn succeeded(operation: impl Into<String>) -> Self {
128        Self::new(operation, AuthFlowDiagnosisOutcome::Succeeded)
129    }
130
131    pub fn failed(operation: impl Into<String>) -> Self {
132        Self::new(operation, AuthFlowDiagnosisOutcome::Failed)
133    }
134
135    pub fn rejected(operation: impl Into<String>) -> Self {
136        Self::new(operation, AuthFlowDiagnosisOutcome::Rejected)
137    }
138
139    pub fn with_outcome(mut self, outcome: AuthFlowDiagnosisOutcome) -> Self {
140        self.outcome = outcome;
141        self
142    }
143
144    pub fn field<V>(mut self, key: impl Into<String>, value: V) -> Self
145    where
146        V: Serialize,
147    {
148        if let Ok(value) = serde_json::to_value(value) {
149            self.fields.insert(key.into(), value);
150        }
151        self
152    }
153
154    pub fn to_json_value(&self) -> Value {
155        serde_json::to_value(self).unwrap_or_else(|_| Value::Null)
156    }
157}
158
159#[derive(Debug)]
160pub struct DiagnosedResult<T, E> {
161    diagnosis: AuthFlowDiagnosis,
162    result: Result<T, E>,
163}
164
165impl<T, E> DiagnosedResult<T, E> {
166    pub fn success(diagnosis: AuthFlowDiagnosis, value: T) -> Self {
167        Self {
168            diagnosis,
169            result: Ok(value),
170        }
171    }
172
173    pub fn failure(diagnosis: AuthFlowDiagnosis, error: E) -> Self {
174        Self {
175            diagnosis,
176            result: Err(error),
177        }
178    }
179
180    pub fn diagnosis(&self) -> &AuthFlowDiagnosis {
181        &self.diagnosis
182    }
183
184    pub fn result(&self) -> &Result<T, E> {
185        &self.result
186    }
187
188    pub fn into_result(self) -> Result<T, E> {
189        self.result
190    }
191
192    pub fn into_parts(self) -> (AuthFlowDiagnosis, Result<T, E>) {
193        (self.diagnosis, self.result)
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn auth_flow_operation_constants_remain_stable() {
203        assert_eq!(
204            AuthFlowOperation::PROJECTION_CONFIG_FETCH,
205            "projection.config_fetch"
206        );
207        assert_eq!(AuthFlowOperation::OIDC_AUTHORIZE, "oidc.authorize");
208        assert_eq!(AuthFlowOperation::OIDC_CALLBACK, "oidc.callback");
209        assert_eq!(
210            AuthFlowOperation::OIDC_METADATA_REDEEM,
211            "oidc.metadata_redeem"
212        );
213        assert_eq!(AuthFlowOperation::OIDC_TOKEN_REFRESH, "oidc.token_refresh");
214        assert_eq!(AuthFlowOperation::OIDC_USER_INFO, "oidc.user_info");
215        assert_eq!(AuthFlowOperation::FORWARD_AUTH_CHECK, "forward_auth.check");
216        assert_eq!(
217            AuthFlowOperation::PROPAGATION_FORWARD,
218            "propagation.forward"
219        );
220        assert_eq!(AuthFlowOperation::BASIC_AUTH_LOGIN, "basic_auth.login");
221        assert_eq!(AuthFlowOperation::BASIC_AUTH_LOGOUT, "basic_auth.logout");
222        assert_eq!(
223            AuthFlowOperation::BASIC_AUTH_AUTHORIZE,
224            "basic_auth.authorize"
225        );
226        assert_eq!(AuthFlowOperation::SESSION_LOGIN, "session.login");
227        assert_eq!(AuthFlowOperation::SESSION_LOGOUT, "session.logout");
228        assert_eq!(AuthFlowOperation::SESSION_USER_INFO, "session.user_info");
229        assert_eq!(
230            AuthFlowOperation::DASHBOARD_AUTH_CHECK,
231            "dashboard_auth.check"
232        );
233        assert_eq!(
234            AuthFlowOperation::CREDS_MANAGE_GROUP_LIST,
235            "creds_manage.group.list"
236        );
237        assert_eq!(
238            AuthFlowOperation::CREDS_MANAGE_GROUP_GET,
239            "creds_manage.group.get"
240        );
241        assert_eq!(
242            AuthFlowOperation::CREDS_MANAGE_GROUP_CREATE,
243            "creds_manage.group.create"
244        );
245        assert_eq!(
246            AuthFlowOperation::CREDS_MANAGE_GROUP_UPDATE,
247            "creds_manage.group.update"
248        );
249        assert_eq!(
250            AuthFlowOperation::CREDS_MANAGE_GROUP_DELETE,
251            "creds_manage.group.delete"
252        );
253        assert_eq!(
254            AuthFlowOperation::CREDS_MANAGE_ENTRY_LIST,
255            "creds_manage.entry.list"
256        );
257        assert_eq!(
258            AuthFlowOperation::CREDS_MANAGE_ENTRY_GET,
259            "creds_manage.entry.get"
260        );
261        assert_eq!(
262            AuthFlowOperation::CREDS_MANAGE_ENTRY_CREATE_BASIC,
263            "creds_manage.entry.create_basic"
264        );
265        assert_eq!(
266            AuthFlowOperation::CREDS_MANAGE_ENTRY_CREATE_TOKEN,
267            "creds_manage.entry.create_token"
268        );
269        assert_eq!(
270            AuthFlowOperation::CREDS_MANAGE_ENTRY_UPDATE,
271            "creds_manage.entry.update"
272        );
273        assert_eq!(
274            AuthFlowOperation::CREDS_MANAGE_ENTRY_DELETE,
275            "creds_manage.entry.delete"
276        );
277    }
278
279    #[test]
280    fn auth_flow_diagnosis_field_constants_work_with_field_insertion() {
281        let diagnosis = AuthFlowDiagnosis::started(AuthFlowOperation::DASHBOARD_AUTH_CHECK)
282            .field(AuthFlowDiagnosisField::AUTH_FAMILY, "dashboard")
283            .field(AuthFlowDiagnosisField::CREDENTIAL_SOURCE, "bearer")
284            .field(AuthFlowDiagnosisField::HAS_COOKIE_HEADER, true)
285            .field(AuthFlowDiagnosisField::PROPAGATION_ENABLED, false)
286            .field(AuthFlowDiagnosisField::REASON, "propagation_disabled");
287
288        let value = diagnosis.to_json_value();
289        assert_eq!(
290            value["fields"][AuthFlowDiagnosisField::AUTH_FAMILY],
291            "dashboard"
292        );
293        assert_eq!(
294            value["fields"][AuthFlowDiagnosisField::CREDENTIAL_SOURCE],
295            "bearer"
296        );
297        assert_eq!(
298            value["fields"][AuthFlowDiagnosisField::HAS_COOKIE_HEADER],
299            true
300        );
301        assert_eq!(
302            value["fields"][AuthFlowDiagnosisField::PROPAGATION_ENABLED],
303            false
304        );
305        assert_eq!(
306            value["fields"][AuthFlowDiagnosisField::REASON],
307            "propagation_disabled"
308        );
309    }
310
311    #[test]
312    fn diagnosis_serializes_operation_outcome_and_fields() {
313        let diagnosis = AuthFlowDiagnosis::started(AuthFlowOperation::PROJECTION_CONFIG_FETCH)
314            .field(AuthFlowDiagnosisField::MODE, "frontend_oidc")
315            .field("pkce_enabled", true);
316
317        let value = diagnosis.to_json_value();
318        assert_eq!(
319            value["operation"],
320            AuthFlowOperation::PROJECTION_CONFIG_FETCH
321        );
322        assert_eq!(value["outcome"], "started");
323        assert_eq!(
324            value["fields"][AuthFlowDiagnosisField::MODE],
325            "frontend_oidc"
326        );
327        assert_eq!(value["fields"]["pkce_enabled"], true);
328    }
329
330    #[test]
331    fn auth_flow_diagnosis_extended_field_constants_remain_stable() {
332        assert_eq!(
333            AuthFlowDiagnosisField::ACCESS_TOKEN_PRESENT,
334            "access_token_present"
335        );
336        assert_eq!(AuthFlowDiagnosisField::AUTH_SCHEME, "auth_scheme");
337        assert_eq!(AuthFlowDiagnosisField::CALLBACK_PATH, "callback_path");
338        assert_eq!(AuthFlowDiagnosisField::ENTRY_IDS_COUNT, "entry_ids_count");
339        assert_eq!(AuthFlowDiagnosisField::ENTRY_NAME, "entry_name");
340        assert_eq!(
341            AuthFlowDiagnosisField::EXTERNAL_BASE_URL,
342            "external_base_url"
343        );
344        assert_eq!(AuthFlowDiagnosisField::HAS_CODE, "has_code");
345        assert_eq!(AuthFlowDiagnosisField::HAS_ID_TOKEN, "has_id_token");
346        assert_eq!(AuthFlowDiagnosisField::HAS_METADATA, "has_metadata");
347        assert_eq!(
348            AuthFlowDiagnosisField::HAS_POST_AUTH_REDIRECT_URI,
349            "has_post_auth_redirect_uri"
350        );
351        assert_eq!(
352            AuthFlowDiagnosisField::HAS_REQUESTED_POST_AUTH_REDIRECT_URI,
353            "has_requested_post_auth_redirect_uri"
354        );
355        assert_eq!(AuthFlowDiagnosisField::HAS_STATE, "has_state");
356        assert_eq!(
357            AuthFlowDiagnosisField::METADATA_ID_PRESENT,
358            "metadata_id_present"
359        );
360        assert_eq!(
361            AuthFlowDiagnosisField::METADATA_REDEEMED,
362            "metadata_redeemed"
363        );
364        assert_eq!(
365            AuthFlowDiagnosisField::POST_AUTH_REDIRECT_PRESENT,
366            "post_auth_redirect_present"
367        );
368        assert_eq!(
369            AuthFlowDiagnosisField::RESPONSE_TRANSPORT,
370            "response_transport"
371        );
372        assert_eq!(AuthFlowDiagnosisField::RESULT_COUNT, "result_count");
373        assert_eq!(AuthFlowDiagnosisField::SUBJECT, "subject");
374    }
375
376    #[test]
377    fn diagnosed_result_preserves_diagnosis_on_failure() {
378        let diagnosed = DiagnosedResult::<(), &str>::failure(
379            AuthFlowDiagnosis::failed(AuthFlowOperation::PROPAGATION_FORWARD)
380                .field(AuthFlowDiagnosisField::REASON, "missing_header"),
381            "boom",
382        );
383
384        assert!(diagnosed.result().is_err());
385        assert_eq!(
386            diagnosed.diagnosis().operation,
387            AuthFlowOperation::PROPAGATION_FORWARD
388        );
389        assert_eq!(
390            diagnosed.diagnosis().fields[AuthFlowDiagnosisField::REASON],
391            "missing_header"
392        );
393    }
394}