1use axum::Router;
37use axum::extract::{FromRef, Path, Query, State};
38use axum::http::{HeaderMap, StatusCode};
39use axum::response::{IntoResponse, Json, Response};
40use axum::routing::{delete, get, post};
41use serde::{Deserialize, Serialize};
42use serde_json::json;
43
44use crate::ctx::AuthCtx;
45use crate::state::AdminApiKeys;
46use crate::store::User;
47
48pub fn router<S>() -> Router<S>
52where
53 S: Clone + Send + Sync + 'static,
54 AuthCtx: FromRef<S>,
55 AdminApiKeys: FromRef<S>,
56{
57 Router::new()
58 .route("/admin/users", get(list_users).post(create_user_handler))
59 .route(
60 "/admin/users/{id}",
61 get(get_user_detail)
62 .put(update_user_handler)
63 .delete(delete_user_handler),
64 )
65 .route(
66 "/admin/users/{id}/password-reset",
67 post(password_reset_handler),
68 )
69 .route("/admin/sessions", get(list_sessions))
70 .route("/admin/sessions/{id}", delete(revoke_session))
71 .route(
72 "/admin/sessions/by-user/{user_id}",
73 delete(revoke_sessions_for_user),
74 )
75 .route("/admin/biscuit", get(biscuit_info))
76 .route("/admin/jwks", get(jwks_proxy))
77 .route(
78 "/admin/zanzibar/namespaces",
79 get(zanzibar_list_namespaces).post(zanzibar_define_namespace),
80 )
81 .route(
82 "/admin/zanzibar/namespaces/{name}",
83 get(zanzibar_get_namespace),
84 )
85 .route(
86 "/admin/zanzibar/tuples",
87 post(zanzibar_write_tuple).delete(zanzibar_delete_tuple),
88 )
89 .route("/admin/zanzibar/check", post(zanzibar_check_handler))
90 .route("/admin/zanzibar/expand", post(zanzibar_expand_handler))
91 .route("/admin/audit", get(audit_list))
92}
93
94async fn require_admin(
105 headers: &HeaderMap,
106 ctx: &AuthCtx,
107 keys: &AdminApiKeys,
108) -> Result<crate::gate::Caller, Box<Response>> {
109 crate::gate::require_role_for(headers, ctx, keys, "auth", "system", "admin").await
110}
111
112#[derive(Clone, Debug, Default, Deserialize)]
117pub struct ListUsersQuery {
118 #[serde(default)]
119 pub limit: Option<i64>,
120 #[serde(default)]
121 pub offset: Option<i64>,
122 #[serde(default)]
123 pub search: Option<String>,
124}
125
126#[derive(Clone, Debug, Serialize)]
127pub struct ListUsersResponse {
128 pub items: Vec<User>,
129 pub total: i64,
130 pub limit: i64,
131 pub offset: i64,
132}
133
134async fn list_users(
135 State(ctx): State<AuthCtx>,
136 State(keys): State<AdminApiKeys>,
137 headers: HeaderMap,
138 Query(q): Query<ListUsersQuery>,
139) -> Response {
140 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
141 return *r;
142 }
143 let limit = q.limit.unwrap_or(50).clamp(1, 500);
144 let offset = q.offset.unwrap_or(0).max(0);
145 let search = q.search.as_deref();
146 let items = match ctx.users.list_users(limit, offset, search).await {
147 Ok(v) => v,
148 Err(e) => return server_error(&format!("list users: {e}")),
149 };
150 let total = match ctx.users.count_users(search).await {
151 Ok(n) => n,
152 Err(e) => return server_error(&format!("count users: {e}")),
153 };
154 (
155 StatusCode::OK,
156 Json(ListUsersResponse {
157 items,
158 total,
159 limit,
160 offset,
161 }),
162 )
163 .into_response()
164}
165
166#[derive(Clone, Debug, Deserialize)]
167pub struct CreateUserBody {
168 pub email: Option<String>,
169 pub display_name: Option<String>,
170 #[serde(default)]
171 pub email_verified: bool,
172 pub password: Option<String>,
176}
177
178async fn create_user_handler(
179 State(ctx): State<AuthCtx>,
180 State(keys): State<AdminApiKeys>,
181 headers: HeaderMap,
182 Json(body): Json<CreateUserBody>,
183) -> Response {
184 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
185 return *r;
186 }
187 let id = format!(
188 "usr_{}",
189 data_encoding::BASE64URL_NOPAD.encode(&random_bytes::<12>())
190 );
191 let user = User {
192 id: id.clone(),
193 email: body.email,
194 email_verified: body.email_verified,
195 display_name: body.display_name,
196 created_at: now_secs(),
197 };
198 if let Err(e) = ctx.users.create_user(&user).await {
199 return server_error(&format!("create user: {e}"));
200 }
201 if let Some(pw) = body.password.as_ref() {
202 #[cfg(feature = "auth-password")]
203 {
204 match crate::password::PasswordHasher::default().hash(pw) {
205 Ok(hash) => {
206 if let Err(e) = ctx.users.set_password_hash(&id, &hash).await {
207 return server_error(&format!("set password hash: {e}"));
208 }
209 }
210 Err(e) => return server_error(&format!("hash password: {e}")),
211 }
212 }
213 #[cfg(not(feature = "auth-password"))]
214 {
215 let _ = pw;
216 return svc_unavailable("auth-password feature not compiled in");
217 }
218 }
219 (StatusCode::CREATED, Json(user)).into_response()
220}
221
222#[derive(Clone, Debug, Serialize)]
223pub struct UserDetailResponse {
224 pub user: User,
225 pub passkeys: Vec<PasskeySummary>,
226 pub sessions: Vec<crate::store::Session>,
227 pub upstream: Vec<UpstreamLink>,
228}
229
230#[derive(Clone, Debug, Serialize)]
231pub struct PasskeySummary {
232 pub credential_id: String,
233 pub sign_count: u32,
234 pub transports: Vec<String>,
235 pub created_at: f64,
236}
237
238#[derive(Clone, Debug, Serialize)]
239pub struct UpstreamLink {
240 pub provider: String,
241 pub subject: String,
242}
243
244async fn get_user_detail(
245 State(ctx): State<AuthCtx>,
246 State(keys): State<AdminApiKeys>,
247 headers: HeaderMap,
248 Path(id): Path<String>,
249) -> Response {
250 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
251 return *r;
252 }
253 let user = match ctx.users.get_user_by_id(&id).await {
254 Ok(Some(u)) => u,
255 Ok(None) => {
256 return (
257 StatusCode::NOT_FOUND,
258 Json(json!({"error": "unknown user_id"})),
259 )
260 .into_response();
261 }
262 Err(e) => return server_error(&format!("get user: {e}")),
263 };
264 let passkeys = match ctx.users.list_passkeys(&id).await {
265 Ok(v) => v
266 .into_iter()
267 .map(|p| PasskeySummary {
268 credential_id: data_encoding::BASE64URL_NOPAD.encode(&p.credential_id),
269 sign_count: p.sign_count,
270 transports: p.transports,
271 created_at: p.created_at,
272 })
273 .collect(),
274 Err(e) => return server_error(&format!("list passkeys: {e}")),
275 };
276 let sessions = match ctx.sessions.list_for_user(&id).await {
277 Ok(v) => v,
278 Err(e) => return server_error(&format!("list sessions: {e}")),
279 };
280 let upstream = match ctx.users.list_upstream_for_user(&id).await {
281 Ok(v) => v
282 .into_iter()
283 .map(|(provider, subject)| UpstreamLink { provider, subject })
284 .collect(),
285 Err(e) => return server_error(&format!("list upstream: {e}")),
286 };
287 (
288 StatusCode::OK,
289 Json(UserDetailResponse {
290 user,
291 passkeys,
292 sessions,
293 upstream,
294 }),
295 )
296 .into_response()
297}
298
299#[derive(Clone, Debug, Deserialize)]
300pub struct UpdateUserBody {
301 pub email: Option<String>,
302 pub display_name: Option<String>,
303 pub email_verified: Option<bool>,
304}
305
306async fn update_user_handler(
307 State(ctx): State<AuthCtx>,
308 State(keys): State<AdminApiKeys>,
309 headers: HeaderMap,
310 Path(id): Path<String>,
311 Json(body): Json<UpdateUserBody>,
312) -> Response {
313 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
314 return *r;
315 }
316 let mut user = match ctx.users.get_user_by_id(&id).await {
317 Ok(Some(u)) => u,
318 Ok(None) => {
319 return (
320 StatusCode::NOT_FOUND,
321 Json(json!({"error": "unknown user_id"})),
322 )
323 .into_response();
324 }
325 Err(e) => return server_error(&format!("get user: {e}")),
326 };
327 if let Some(email) = body.email {
328 user.email = Some(email);
329 }
330 if let Some(name) = body.display_name {
331 user.display_name = Some(name);
332 }
333 if let Some(v) = body.email_verified {
334 user.email_verified = v;
335 }
336 if let Err(e) = ctx.users.update_user(&user).await {
337 return server_error(&format!("update user: {e}"));
338 }
339 (StatusCode::OK, Json(user)).into_response()
340}
341
342async fn delete_user_handler(
343 State(ctx): State<AuthCtx>,
344 State(keys): State<AdminApiKeys>,
345 headers: HeaderMap,
346 Path(id): Path<String>,
347) -> Response {
348 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
349 return *r;
350 }
351 match ctx.users.delete_user(&id).await {
352 Ok(true) => StatusCode::NO_CONTENT.into_response(),
353 Ok(false) => (
354 StatusCode::NOT_FOUND,
355 Json(json!({"error": "unknown user_id"})),
356 )
357 .into_response(),
358 Err(e) => server_error(&format!("delete user: {e}")),
359 }
360}
361
362#[derive(Clone, Debug, Deserialize)]
363pub struct PasswordResetBody {
364 pub password: String,
368}
369
370async fn password_reset_handler(
371 State(ctx): State<AuthCtx>,
372 State(keys): State<AdminApiKeys>,
373 headers: HeaderMap,
374 Path(id): Path<String>,
375 Json(body): Json<PasswordResetBody>,
376) -> Response {
377 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
378 return *r;
379 }
380 if ctx
381 .users
382 .get_user_by_id(&id)
383 .await
384 .unwrap_or(None)
385 .is_none()
386 {
387 return (
388 StatusCode::NOT_FOUND,
389 Json(json!({"error": "unknown user_id"})),
390 )
391 .into_response();
392 }
393 #[cfg(feature = "auth-password")]
394 {
395 let hash = match crate::password::PasswordHasher::default().hash(&body.password) {
396 Ok(h) => h,
397 Err(e) => return server_error(&format!("hash password: {e}")),
398 };
399 if let Err(e) = ctx.users.set_password_hash(&id, &hash).await {
400 return server_error(&format!("set password hash: {e}"));
401 }
402 StatusCode::NO_CONTENT.into_response()
403 }
404 #[cfg(not(feature = "auth-password"))]
405 {
406 let _ = body.password;
407 let _ = ctx;
408 svc_unavailable("auth-password feature not compiled in")
409 }
410}
411
412#[derive(Clone, Debug, Default, Deserialize)]
417pub struct ListSessionsQuery {
418 #[serde(default)]
419 pub limit: Option<i64>,
420 #[serde(default)]
421 pub offset: Option<i64>,
422 #[serde(default)]
423 pub user_id: Option<String>,
424}
425
426#[derive(Clone, Debug, Serialize)]
427pub struct ListSessionsResponse {
428 pub items: Vec<crate::store::Session>,
429 pub total: i64,
430 pub limit: i64,
431 pub offset: i64,
432}
433
434async fn list_sessions(
435 State(ctx): State<AuthCtx>,
436 State(keys): State<AdminApiKeys>,
437 headers: HeaderMap,
438 Query(q): Query<ListSessionsQuery>,
439) -> Response {
440 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
441 return *r;
442 }
443 let limit = q.limit.unwrap_or(50).clamp(1, 500);
444 let offset = q.offset.unwrap_or(0).max(0);
445 let user_filter = q.user_id.as_deref();
446 let items = match ctx.sessions.list_all(limit, offset, user_filter).await {
447 Ok(v) => v,
448 Err(e) => return server_error(&format!("list sessions: {e}")),
449 };
450 let total = match ctx.sessions.count_all(user_filter).await {
451 Ok(n) => n,
452 Err(e) => return server_error(&format!("count sessions: {e}")),
453 };
454 (
455 StatusCode::OK,
456 Json(ListSessionsResponse {
457 items,
458 total,
459 limit,
460 offset,
461 }),
462 )
463 .into_response()
464}
465
466async fn revoke_session(
467 State(ctx): State<AuthCtx>,
468 State(keys): State<AdminApiKeys>,
469 headers: HeaderMap,
470 Path(id): Path<String>,
471) -> Response {
472 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
473 return *r;
474 }
475 match ctx.sessions.delete(&id).await {
476 Ok(true) => StatusCode::NO_CONTENT.into_response(),
477 Ok(false) => (
478 StatusCode::NOT_FOUND,
479 Json(json!({"error": "unknown session_id"})),
480 )
481 .into_response(),
482 Err(e) => server_error(&format!("revoke session: {e}")),
483 }
484}
485
486#[derive(Clone, Debug, Serialize)]
487pub struct RevokeAllResponse {
488 pub revoked: u64,
489}
490
491async fn revoke_sessions_for_user(
492 State(ctx): State<AuthCtx>,
493 State(keys): State<AdminApiKeys>,
494 headers: HeaderMap,
495 Path(user_id): Path<String>,
496) -> Response {
497 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
498 return *r;
499 }
500 match ctx.sessions.delete_for_user(&user_id).await {
501 Ok(n) => (StatusCode::OK, Json(RevokeAllResponse { revoked: n })).into_response(),
502 Err(e) => server_error(&format!("revoke for user: {e}")),
503 }
504}
505
506#[derive(Clone, Debug, Serialize)]
511pub struct BiscuitInfo {
512 pub kid: String,
513 pub public_pem: String,
514}
515
516async fn biscuit_info(
517 State(ctx): State<AuthCtx>,
518 State(keys): State<AdminApiKeys>,
519 headers: HeaderMap,
520) -> Response {
521 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
522 return *r;
523 }
524 let kid = ctx.biscuit.active_kid();
525 let public_pem = match ctx.biscuit.public_pem() {
526 Ok(s) => s,
527 Err(e) => return server_error(&format!("public pem: {e}")),
528 };
529 (StatusCode::OK, Json(BiscuitInfo { kid, public_pem })).into_response()
530}
531
532async fn jwks_proxy(
533 State(ctx): State<AuthCtx>,
534 State(keys): State<AdminApiKeys>,
535 headers: HeaderMap,
536) -> Response {
537 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
538 return *r;
539 }
540 #[cfg(feature = "auth-oidc-provider")]
545 {
546 if let Some(provider) = ctx.oidc_provider.as_ref() {
547 let payload = match &provider.jwks_source {
548 #[cfg(feature = "backend-postgres")]
549 crate::oidc_provider::JwksSource::Postgres(_) => json!({"keys": []}),
550 #[cfg(feature = "backend-sqlite")]
551 crate::oidc_provider::JwksSource::Sqlite(_) => json!({"keys": []}),
552 crate::oidc_provider::JwksSource::Memory(v) => json!({"keys": v}),
553 };
554 return (StatusCode::OK, Json(payload)).into_response();
555 }
556 }
557 let _ = ctx;
558 (StatusCode::OK, Json(json!({"keys": []}))).into_response()
559}
560
561async fn zanzibar_list_namespaces(
566 State(ctx): State<AuthCtx>,
567 State(keys): State<AdminApiKeys>,
568 headers: HeaderMap,
569) -> Response {
570 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
571 return *r;
572 }
573 #[cfg(feature = "auth-zanzibar")]
574 {
575 let Some(store) = ctx.zanzibar.as_ref() else {
576 return svc_unavailable("zanzibar not enabled");
577 };
578 return match store.list_namespaces().await {
579 Ok(v) => (StatusCode::OK, Json(v)).into_response(),
580 Err(e) => server_error(&format!("list namespaces: {e}")),
581 };
582 }
583 #[cfg(not(feature = "auth-zanzibar"))]
584 {
585 let _ = ctx;
586 svc_unavailable("zanzibar not compiled in")
587 }
588}
589
590async fn zanzibar_get_namespace(
591 State(ctx): State<AuthCtx>,
592 State(keys): State<AdminApiKeys>,
593 headers: HeaderMap,
594 Path(name): Path<String>,
595) -> Response {
596 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
597 return *r;
598 }
599 #[cfg(feature = "auth-zanzibar")]
600 {
601 let Some(store) = ctx.zanzibar.as_ref() else {
602 return svc_unavailable("zanzibar not enabled");
603 };
604 return match store.get_namespace(&name).await {
605 Ok(Some(ns)) => (StatusCode::OK, Json(ns)).into_response(),
606 Ok(None) => (
607 StatusCode::NOT_FOUND,
608 Json(json!({"error": "unknown namespace"})),
609 )
610 .into_response(),
611 Err(e) => server_error(&format!("get namespace: {e}")),
612 };
613 }
614 #[cfg(not(feature = "auth-zanzibar"))]
615 {
616 let _ = (ctx, name);
617 svc_unavailable("zanzibar not compiled in")
618 }
619}
620
621async fn zanzibar_define_namespace(
622 State(ctx): State<AuthCtx>,
623 State(keys): State<AdminApiKeys>,
624 headers: HeaderMap,
625 Json(schema): Json<crate::zanzibar::NamespaceSchema>,
626) -> Response {
627 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
628 return *r;
629 }
630 #[cfg(feature = "auth-zanzibar")]
631 {
632 let Some(store) = ctx.zanzibar.as_ref() else {
633 return svc_unavailable("zanzibar not enabled");
634 };
635 return match store.define_namespace(&schema).await {
636 Ok(()) => (
637 StatusCode::CREATED,
638 Json(json!({"ok": true, "name": schema.name})),
639 )
640 .into_response(),
641 Err(e) => server_error(&format!("define namespace: {e}")),
642 };
643 }
644 #[cfg(not(feature = "auth-zanzibar"))]
645 {
646 let _ = (ctx, schema);
647 svc_unavailable("zanzibar not compiled in")
648 }
649}
650
651#[derive(Clone, Debug, Deserialize)]
652pub struct TupleBody {
653 pub object_type: String,
654 pub object_id: String,
655 pub relation: String,
656 pub subject_type: String,
657 pub subject_id: String,
658 #[serde(default)]
661 pub subject_rel: String,
662}
663
664async fn zanzibar_write_tuple(
665 State(ctx): State<AuthCtx>,
666 State(keys): State<AdminApiKeys>,
667 headers: HeaderMap,
668 Json(body): Json<TupleBody>,
669) -> Response {
670 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
671 return *r;
672 }
673 #[cfg(feature = "auth-zanzibar")]
674 {
675 let Some(store) = ctx.zanzibar.as_ref() else {
676 return svc_unavailable("zanzibar not enabled");
677 };
678 let tuple = body_to_tuple(body);
679 return match store.write_tuple(&tuple).await {
680 Ok(()) => (StatusCode::CREATED, Json(json!({"ok": true}))).into_response(),
681 Err(e) => server_error(&format!("write tuple: {e}")),
682 };
683 }
684 #[cfg(not(feature = "auth-zanzibar"))]
685 {
686 let _ = (ctx, body);
687 svc_unavailable("zanzibar not compiled in")
688 }
689}
690
691async fn zanzibar_delete_tuple(
692 State(ctx): State<AuthCtx>,
693 State(keys): State<AdminApiKeys>,
694 headers: HeaderMap,
695 Json(body): Json<TupleBody>,
696) -> Response {
697 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
698 return *r;
699 }
700 #[cfg(feature = "auth-zanzibar")]
701 {
702 let Some(store) = ctx.zanzibar.as_ref() else {
703 return svc_unavailable("zanzibar not enabled");
704 };
705 let tuple = body_to_tuple(body);
706 return match store.delete_tuple(&tuple).await {
707 Ok(true) => StatusCode::NO_CONTENT.into_response(),
708 Ok(false) => (
709 StatusCode::NOT_FOUND,
710 Json(json!({"error": "tuple not found"})),
711 )
712 .into_response(),
713 Err(e) => server_error(&format!("delete tuple: {e}")),
714 };
715 }
716 #[cfg(not(feature = "auth-zanzibar"))]
717 {
718 let _ = (ctx, body);
719 svc_unavailable("zanzibar not compiled in")
720 }
721}
722
723#[derive(Clone, Debug, Deserialize)]
724pub struct CheckBody {
725 pub resource_type: String,
726 pub resource_id: String,
727 pub permission: String,
728 pub subject_type: String,
729 pub subject_id: String,
730 #[serde(default)]
733 pub subject_rel: String,
734}
735
736#[derive(Clone, Debug, Serialize)]
737pub struct CheckResponse {
738 pub result: String,
739 pub allowed: bool,
740}
741
742async fn zanzibar_check_handler(
743 State(ctx): State<AuthCtx>,
744 State(keys): State<AdminApiKeys>,
745 headers: HeaderMap,
746 Json(body): Json<CheckBody>,
747) -> Response {
748 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
749 return *r;
750 }
751 #[cfg(feature = "auth-zanzibar")]
752 {
753 use crate::zanzibar::{CheckResult, Consistency, ObjectRef, SubjectRef};
754 let Some(store) = ctx.zanzibar.as_ref() else {
755 return svc_unavailable("zanzibar not enabled");
756 };
757 let resource = ObjectRef {
758 object_type: body.resource_type,
759 object_id: body.resource_id,
760 };
761 let subject = SubjectRef {
762 subject_type: body.subject_type,
763 subject_id: body.subject_id,
764 subject_rel: body.subject_rel,
765 };
766 return match store
767 .check(&resource, &body.permission, &subject, Consistency::Minimum)
768 .await
769 {
770 Ok(r) => {
771 let (label, allowed) = match &r {
772 CheckResult::Allowed { .. } => ("Allowed", true),
773 CheckResult::Denied => ("Denied", false),
774 CheckResult::DepthExceeded => ("DepthExceeded", false),
775 CheckResult::CycleDetected => ("CycleDetected", false),
776 };
777 (
778 StatusCode::OK,
779 Json(CheckResponse {
780 result: label.to_string(),
781 allowed,
782 }),
783 )
784 .into_response()
785 }
786 Err(e) => server_error(&format!("check: {e}")),
787 };
788 }
789 #[cfg(not(feature = "auth-zanzibar"))]
790 {
791 let _ = (ctx, body);
792 svc_unavailable("zanzibar not compiled in")
793 }
794}
795
796#[derive(Clone, Debug, Deserialize)]
797pub struct ExpandBody {
798 pub resource_type: String,
799 pub resource_id: String,
800 pub relation: String,
801 #[serde(default)]
802 pub depth_limit: Option<u32>,
803}
804
805async fn zanzibar_expand_handler(
806 State(ctx): State<AuthCtx>,
807 State(keys): State<AdminApiKeys>,
808 headers: HeaderMap,
809 Json(body): Json<ExpandBody>,
810) -> Response {
811 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
812 return *r;
813 }
814 #[cfg(feature = "auth-zanzibar")]
815 {
816 use crate::zanzibar::{MAX_DEPTH, ObjectRef};
817 let Some(store) = ctx.zanzibar.as_ref() else {
818 return svc_unavailable("zanzibar not enabled");
819 };
820 let resource = ObjectRef {
821 object_type: body.resource_type,
822 object_id: body.resource_id,
823 };
824 let depth = body.depth_limit.unwrap_or(MAX_DEPTH);
825 return match store.expand(&resource, &body.relation, depth).await {
826 Ok(tree) => (StatusCode::OK, Json(tree)).into_response(),
827 Err(e) => server_error(&format!("expand: {e}")),
828 };
829 }
830 #[cfg(not(feature = "auth-zanzibar"))]
831 {
832 let _ = (ctx, body);
833 svc_unavailable("zanzibar not compiled in")
834 }
835}
836
837#[derive(Clone, Debug, Default, Deserialize)]
842pub struct ListAuditQuery {
843 #[serde(default)]
844 pub limit: Option<i64>,
845 #[serde(default)]
846 pub offset: Option<i64>,
847 #[serde(default)]
848 pub actor: Option<String>,
849 #[serde(default)]
850 pub action: Option<String>,
851 #[serde(default)]
852 pub since: Option<f64>,
853 #[serde(default)]
854 pub until: Option<f64>,
855}
856
857#[derive(Clone, Debug, Serialize)]
858pub struct AuditResponse {
859 pub items: Vec<serde_json::Value>,
860 pub total: i64,
861 pub limit: i64,
862 pub offset: i64,
863 pub enabled: bool,
868}
869
870async fn audit_list(
871 State(ctx): State<AuthCtx>,
872 State(keys): State<AdminApiKeys>,
873 headers: HeaderMap,
874 Query(q): Query<ListAuditQuery>,
875) -> Response {
876 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
877 return *r;
878 }
879 let limit = q.limit.unwrap_or(50).clamp(1, 500);
880 let offset = q.offset.unwrap_or(0).max(0);
881 (
882 StatusCode::OK,
883 Json(AuditResponse {
884 items: Vec::new(),
885 total: 0,
886 limit,
887 offset,
888 enabled: false,
889 }),
890 )
891 .into_response()
892}
893
894#[cfg(feature = "auth-zanzibar")]
899fn body_to_tuple(body: TupleBody) -> crate::zanzibar::Tuple {
900 crate::zanzibar::Tuple {
901 object_type: body.object_type,
902 object_id: body.object_id,
903 relation: body.relation,
904 subject_type: body.subject_type,
905 subject_id: body.subject_id,
906 subject_rel: body.subject_rel,
907 }
908}
909
910fn server_error(msg: &str) -> Response {
911 (
912 StatusCode::INTERNAL_SERVER_ERROR,
913 Json(json!({"error": "server_error", "error_description": msg})),
914 )
915 .into_response()
916}
917
918fn svc_unavailable(msg: &str) -> Response {
919 (
920 StatusCode::SERVICE_UNAVAILABLE,
921 Json(json!({"error": "service_unavailable", "error_description": msg})),
922 )
923 .into_response()
924}
925
926fn now_secs() -> f64 {
927 use std::time::{SystemTime, UNIX_EPOCH};
928 SystemTime::now()
929 .duration_since(UNIX_EPOCH)
930 .unwrap_or_default()
931 .as_secs_f64()
932}
933
934fn random_bytes<const N: usize>() -> [u8; N] {
935 use rand::RngCore;
936 let mut buf = [0u8; N];
937 rand::rng().fill_bytes(&mut buf);
938 buf
939}
940
941