1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
//! The `/admin/v1` router: a separate surface with its own state, its own
//! authentication layer, and its own error envelope.
//!
//! Separate from [`crate::routes`] in the way that matters: the inference router
//! is built over [`AppState`](crate::state::AppState) and layers
//! `authenticate_middleware`, which resolves an inference principal from the
//! configured gateway keys and minted-token verifiers. This router is built over
//! [`AdminApi`] and layers [`admin_authenticate`], which resolves an
//! [`AdminIdentity`] from an [`AdminAuthenticator`]. Neither layer can see the
//! other's credentials, and nothing merges the two route tables — an inference
//! key cannot administer because there is no code path that would let it, not
//! because a capability table says no.
//!
//! Two properties the table enforces rather than documents:
//!
//! - **Every administrative route is authenticated.** Registration goes through
//! [`AdminRouteSpec`], which has no unauthenticated posture to declare. There is
//! no `/admin/v1` analogue of `/healthz`: liveness is answered by the
//! unauthenticated probes on the inference surface, which never consult a
//! backend.
//! - **Every mutating route parses its preconditions before its handler runs.**
//! A spec declaring [`AdminAction::mutates`] gets
//! [`MutationPreconditions`] parsed in the layer and inserted as an extension,
//! so a handler cannot publish without an idempotency key and an expected
//! revision — it would have nothing to build a candidate from.
//!
//! # What the unauthenticated fallbacks disclose
//!
//! The 404/405 split answers before authentication, so an anonymous caller can
//! tell a registered administrative path from an unregistered one and thereby
//! enumerate the route table. That is deliberate and acceptable under
//! [ADR 0027][adr]: the administrative route table is published API surface,
//! identical in every deployment and documented, so hiding it protects nothing —
//! while the alternative, answering `404` to a wrong method, would make a
//! client's own protocol mistake indistinguishable from a typo, on the surface
//! an operator reaches during an incident.
//!
//! What the fallbacks must never disclose is anything a credential would have
//! decided: they never state whether a credential was presented, whether one
//! would have been accepted, or what the deployment contains. They read no
//! request state, consult neither authority, and touch no backend, so the answer
//! to a wrong method is the same for an anonymous caller and an administrator.
//! ADR 0027's disjointness rule is unaffected — enumerating paths grants no
//! authority over any of them.
//!
//! [adr]: https://github.com/Litvue/axond/blob/main/docs/adr/0027-stateless-and-stateful-operating-modes.md
//!
//! # Contract only
//!
//! [`admin_route_specs`] is empty in this slice, and `serve` does not mount this
//! router. Resource-specific handlers — providers, credentials, catalogues,
//! prices, aliases, policy, tenants — are #143's, and each one is a spec plus an
//! edit over the complete state, with the protocol properties above already held.
//! What ships here is the boundary those handlers register into, its middleware,
//! and the tests that hold it, mounted on a synthetic spec so the layer's
//! behaviour is asserted rather than asserted-about.
//!
//! [`AdminIdentity`]: super::auth::AdminIdentity
//! [`MutationPreconditions`]: super::protocol::MutationPreconditions
use Error as _;
use Arc;
use Router;
use ;
use HeaderMap;
use ;
use Response;
use MethodRouter;
use warn;
use ;
use AdminError;
use ;
use AdminService;
use crateResourceScope;
/// Everything an administrative handler needs: the service, and the two
/// authorities that decide who may call it.
/// A route's complete administrative registration.
///
/// The action is declared here rather than derived from the path, because it is
/// what the authorizer decides on and what the middleware reads to know whether
/// the route mutates.
///
/// One action, and therefore one method per spec: precondition parsing is keyed
/// to this action, so a spec whose router combined `get(read).post(publish)`
/// would either demand an idempotency key of its reader or let its writer
/// publish without one. A path that answers both registers two specs on the same
/// path — mounting merges them and each keeps its own layer, which is what makes
/// "every mutating route parses its preconditions" hold per method rather than
/// per path.
/// The administrative route table.
///
/// Empty until #143 lands resource handlers. Deliberately a function returning a
/// table rather than an absent module: the boundary, its middleware, and its
/// tests exist now, so a handler is a row here plus an edit.
/// Mount a table under [`ADMIN_PREFIX`].
///
/// `pub(crate)` so the contract tests can mount a synthetic spec and assert the
/// layer's behaviour directly, rather than waiting for a real handler to exist.
pub
/// The administrative surface.
/// An unknown `/admin/v1` path answers in the administrative envelope, so a
/// client parses one error shape from this surface rather than axum's empty
/// body.
async
/// A known `/admin/v1` path reached with a method it does not serve.
async
/// Authenticate once per administrative request, and parse the preconditions a
/// mutating route requires.
///
/// Authorization is *not* done here: it needs the scope the request names, which
/// only the handler can extract. What the layer guarantees is that a handler runs
/// with an established identity, and — on a mutating route — with preconditions
/// that parsed.
async : ,
headers: HeaderMap,
mut request: Request,
next: Next,
)