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