Skip to main content

cairn_mod/server/admin/
mod.rs

1//! `tools.cairn.admin.*` handlers (§F12).
2//!
3//! Each endpoint lives in its own file; [`admin_router`] wires them
4//! together under one `Extension<AdminState>` so sub-modules don't
5//! need to duplicate state plumbing. Per-request auth + role + CORS
6//! gating is centralized in the shared `common::verify_and_authorize`
7//! helper inside this module.
8
9use std::sync::Arc;
10
11use axum::Extension;
12use axum::Router;
13use axum::routing::{get, post};
14use sqlx::{Pool, Sqlite};
15
16use crate::auth::AuthContext;
17use crate::writer::WriterHandle;
18
19mod apply_label;
20mod audit_view;
21mod common;
22mod flag_reporter;
23mod get_audit_log;
24mod get_report;
25mod get_subject_history;
26mod get_subject_strikes;
27mod get_trust_chain;
28mod list_audit_log;
29mod list_labels;
30mod list_reports;
31mod negate_label;
32mod record_action;
33mod report_view;
34mod resolve_report;
35mod retention_sweep;
36mod revoke_action;
37mod subject_action_view;
38
39/// Operator configuration for admin endpoints. Kept separate from the
40/// subscribe/query configs so operators can tune label-value policy
41/// without touching read-side knobs.
42#[derive(Debug, Clone, Default)]
43pub struct AdminConfig {
44    /// Operational allowlist for `applyLabel` (§F12 `InvalidLabelValue`).
45    /// When `Some`, `applyLabel` rejects values not in this set.
46    /// When `None`, any val ≤128 bytes is accepted — the §F11
47    /// anti-leak principle applies: the error message on reject
48    /// does NOT enumerate the allowed values.
49    ///
50    /// Distinct from [`Self::declared_label_values`] (the trust-chain
51    /// surface for the labeler's *declared* taxonomy). They typically
52    /// match in production but conceptually differ — the allowlist
53    /// gates incoming writes; the declared list documents what the
54    /// labeler publishes.
55    ///
56    /// Future: a #9 service-record update may derive this from the
57    /// published `app.bsky.labeler.service` record so the lexicon set
58    /// and the runtime policy stay in lockstep.
59    pub label_values: Option<Vec<String>>,
60
61    /// Service DID surfaced in `tools.cairn.admin.getTrustChain` (#36).
62    /// Mirrors `Config::service_did`; populated at admin_router
63    /// construction in `serve::run`. Default empty for tests that
64    /// don't exercise the trust-chain endpoint.
65    pub service_did: String,
66
67    /// Service endpoint URL surfaced in
68    /// `tools.cairn.admin.getTrustChain` (#36). Mirrors
69    /// `Config::service_endpoint`. Default empty for tests that
70    /// don't exercise the trust-chain endpoint.
71    pub service_endpoint: String,
72
73    /// Labeler-declared label values from the `[labeler]` config
74    /// block — surfaced by `tools.cairn.admin.getTrustChain` as the
75    /// trust-chain "taxonomy" snapshot. `None` when the deployment
76    /// runs without `[labeler]` (§F19 labeler-absent path); the
77    /// trust-chain endpoint then reports `serviceRecord: null`.
78    /// Distinct from [`Self::label_values`] above — see that field's
79    /// doc comment.
80    pub declared_label_values: Option<Vec<String>>,
81}
82
83/// Build a Router exposing the tools.cairn.admin.* endpoints
84/// registered so far. Compose with subscribe/query/createReport
85/// routers via `Router::merge`.
86///
87/// `strike_policy` is the resolved v1.4 `[strike_policy]` (#48); the
88/// strikes read endpoint consults the threshold + decay window when
89/// projecting the wire envelope. Pass the same instance the writer
90/// task holds — `serve::run` resolves once at startup and clones
91/// here.
92pub fn admin_router(
93    pool: Pool<Sqlite>,
94    writer: WriterHandle,
95    auth: Arc<AuthContext>,
96    config: AdminConfig,
97    strike_policy: crate::moderation::policy::StrikePolicy,
98) -> Router {
99    let state = common::AdminState {
100        pool,
101        writer,
102        auth,
103        config: Arc::new(config),
104        strike_policy: Arc::new(strike_policy),
105    };
106    Router::new()
107        .route(
108            "/xrpc/tools.cairn.admin.applyLabel",
109            post(apply_label::handler),
110        )
111        .route(
112            "/xrpc/tools.cairn.admin.negateLabel",
113            post(negate_label::handler),
114        )
115        .route(
116            "/xrpc/tools.cairn.admin.listLabels",
117            get(list_labels::handler),
118        )
119        .route(
120            "/xrpc/tools.cairn.admin.listReports",
121            get(list_reports::handler),
122        )
123        .route(
124            "/xrpc/tools.cairn.admin.getReport",
125            get(get_report::handler),
126        )
127        .route(
128            "/xrpc/tools.cairn.admin.resolveReport",
129            post(resolve_report::handler),
130        )
131        .route(
132            "/xrpc/tools.cairn.admin.flagReporter",
133            post(flag_reporter::handler),
134        )
135        .route(
136            "/xrpc/tools.cairn.admin.listAuditLog",
137            get(list_audit_log::handler),
138        )
139        .route(
140            "/xrpc/tools.cairn.admin.getAuditLog",
141            get(get_audit_log::handler),
142        )
143        .route(
144            "/xrpc/tools.cairn.admin.retentionSweep",
145            post(retention_sweep::handler),
146        )
147        .route(
148            "/xrpc/tools.cairn.admin.getTrustChain",
149            get(get_trust_chain::handler),
150        )
151        .route(
152            "/xrpc/tools.cairn.admin.recordAction",
153            post(record_action::handler),
154        )
155        .route(
156            "/xrpc/tools.cairn.admin.revokeAction",
157            post(revoke_action::handler),
158        )
159        .route(
160            "/xrpc/tools.cairn.admin.getSubjectHistory",
161            get(get_subject_history::handler),
162        )
163        .route(
164            "/xrpc/tools.cairn.admin.getSubjectStrikes",
165            get(get_subject_strikes::handler),
166        )
167        .layer(Extension(state))
168}