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}