1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum ActionCategory {
36 Dml,
38 Ddl,
40 Schema,
42 Function,
44 Mgmt,
46 Policy,
48 Admin,
50 Config,
52 Vault,
54 Wildcard,
56 Ai,
58 Other,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub enum LifecycleState {
66 Active,
68 Deprecated {
70 replacement: Option<&'static str>,
72 since_version: &'static str,
74 },
75 Removed,
79}
80
81#[derive(Debug, Clone)]
83pub struct ActionEntry {
84 pub name: &'static str,
85 pub category: ActionCategory,
86 pub lifecycle_state: LifecycleState,
87 pub gates_description: &'static str,
88}
89
90pub const ACTIONS: &[ActionEntry] = &[
99 ActionEntry {
101 name: "select",
102 category: ActionCategory::Dml,
103 lifecycle_state: LifecycleState::Active,
104 gates_description: "read rows from a collection",
105 },
106 ActionEntry {
107 name: "write",
108 category: ActionCategory::Dml,
109 lifecycle_state: LifecycleState::Active,
110 gates_description: "any mutating DML (insert/update/delete)",
111 },
112 ActionEntry {
113 name: "insert",
114 category: ActionCategory::Dml,
115 lifecycle_state: LifecycleState::Active,
116 gates_description: "insert rows into a collection",
117 },
118 ActionEntry {
119 name: "update",
120 category: ActionCategory::Dml,
121 lifecycle_state: LifecycleState::Active,
122 gates_description: "update rows in a collection",
123 },
124 ActionEntry {
125 name: "delete",
126 category: ActionCategory::Dml,
127 lifecycle_state: LifecycleState::Active,
128 gates_description: "delete rows from a collection",
129 },
130 ActionEntry {
131 name: "truncate",
132 category: ActionCategory::Dml,
133 lifecycle_state: LifecycleState::Active,
134 gates_description: "truncate a collection",
135 },
136 ActionEntry {
137 name: "references",
138 category: ActionCategory::Schema,
139 lifecycle_state: LifecycleState::Active,
140 gates_description: "declare a foreign key referencing a table",
141 },
142 ActionEntry {
143 name: "execute",
144 category: ActionCategory::Function,
145 lifecycle_state: LifecycleState::Active,
146 gates_description: "execute a stored function",
147 },
148 ActionEntry {
149 name: "usage",
150 category: ActionCategory::Schema,
151 lifecycle_state: LifecycleState::Active,
152 gates_description: "use a schema namespace",
153 },
154 ActionEntry {
155 name: "grant",
156 category: ActionCategory::Mgmt,
157 lifecycle_state: LifecycleState::Active,
158 gates_description: "grant privileges to another principal",
159 },
160 ActionEntry {
161 name: "revoke",
162 category: ActionCategory::Mgmt,
163 lifecycle_state: LifecycleState::Active,
164 gates_description: "revoke privileges from another principal",
165 },
166 ActionEntry {
167 name: "create",
168 category: ActionCategory::Ddl,
169 lifecycle_state: LifecycleState::Active,
170 gates_description: "create a database object",
171 },
172 ActionEntry {
173 name: "drop",
174 category: ActionCategory::Ddl,
175 lifecycle_state: LifecycleState::Active,
176 gates_description: "drop a database object",
177 },
178 ActionEntry {
179 name: "alter",
180 category: ActionCategory::Ddl,
181 lifecycle_state: LifecycleState::Active,
182 gates_description: "alter a database object",
183 },
184 ActionEntry {
186 name: "policy:put",
187 category: ActionCategory::Policy,
188 lifecycle_state: LifecycleState::Active,
189 gates_description: "create or update a managed policy document",
190 },
191 ActionEntry {
192 name: "policy:drop",
193 category: ActionCategory::Policy,
194 lifecycle_state: LifecycleState::Active,
195 gates_description: "delete a managed policy document",
196 },
197 ActionEntry {
198 name: "policy:attach",
199 category: ActionCategory::Policy,
200 lifecycle_state: LifecycleState::Active,
201 gates_description: "attach a policy to a principal",
202 },
203 ActionEntry {
204 name: "policy:detach",
205 category: ActionCategory::Policy,
206 lifecycle_state: LifecycleState::Active,
207 gates_description: "detach a policy from a principal",
208 },
209 ActionEntry {
210 name: "policy:simulate",
211 category: ActionCategory::Policy,
212 lifecycle_state: LifecycleState::Active,
213 gates_description: "run the policy simulator",
214 },
215 ActionEntry {
217 name: "kv:invalidate",
218 category: ActionCategory::Other,
219 lifecycle_state: LifecycleState::Active,
220 gates_description: "invalidate cached KV entries",
221 },
222 ActionEntry {
224 name: "admin:bootstrap",
225 category: ActionCategory::Admin,
226 lifecycle_state: LifecycleState::Active,
227 gates_description: "execute the bootstrap workflow",
228 },
229 ActionEntry {
230 name: "admin:audit-read",
231 category: ActionCategory::Admin,
232 lifecycle_state: LifecycleState::Active,
233 gates_description: "read the platform audit log",
234 },
235 ActionEntry {
236 name: "admin:reload",
237 category: ActionCategory::Admin,
238 lifecycle_state: LifecycleState::Active,
239 gates_description: "reload runtime configuration",
240 },
241 ActionEntry {
242 name: "admin:lease-promote",
243 category: ActionCategory::Admin,
244 lifecycle_state: LifecycleState::Active,
245 gates_description: "promote a standby instance via lease handoff",
246 },
247 ActionEntry {
249 name: "config:read",
250 category: ActionCategory::Config,
251 lifecycle_state: LifecycleState::Active,
252 gates_description: "read runtime configuration values",
253 },
254 ActionEntry {
255 name: "config:write",
256 category: ActionCategory::Config,
257 lifecycle_state: LifecycleState::Active,
258 gates_description: "mutate runtime configuration values",
259 },
260 ActionEntry {
261 name: "config:*",
262 category: ActionCategory::Wildcard,
263 lifecycle_state: LifecycleState::Active,
264 gates_description: "any runtime configuration verb",
265 },
266 ActionEntry {
268 name: "vault:read_metadata",
269 category: ActionCategory::Vault,
270 lifecycle_state: LifecycleState::Active,
271 gates_description: "read vault entry metadata (no plaintext)",
272 },
273 ActionEntry {
274 name: "vault:read",
275 category: ActionCategory::Vault,
276 lifecycle_state: LifecycleState::Active,
277 gates_description: "reveal vault entry plaintext",
278 },
279 ActionEntry {
280 name: "vault:write",
281 category: ActionCategory::Vault,
282 lifecycle_state: LifecycleState::Active,
283 gates_description: "write or rotate vault entries",
284 },
285 ActionEntry {
286 name: "vault:unseal",
287 category: ActionCategory::Vault,
288 lifecycle_state: LifecycleState::Active,
289 gates_description: "unseal the vault master key for this session",
290 },
291 ActionEntry {
297 name: "vault:unseal_history",
298 category: ActionCategory::Vault,
299 lifecycle_state: LifecycleState::Deprecated {
300 replacement: Some("vault:read_metadata"),
301 since_version: "0.5.0",
302 },
303 gates_description: "read the vault unseal-event audit trail",
304 },
305 ActionEntry {
306 name: "vault:purge",
307 category: ActionCategory::Vault,
308 lifecycle_state: LifecycleState::Active,
309 gates_description: "purge (destructively remove) vault entries",
310 },
311 ActionEntry {
313 name: "evidence:export",
314 category: ActionCategory::Other,
315 lifecycle_state: LifecycleState::Active,
316 gates_description: "export evidence bundles",
317 },
318 ActionEntry {
319 name: "evidence:*",
320 category: ActionCategory::Wildcard,
321 lifecycle_state: LifecycleState::Active,
322 gates_description: "any evidence-pipeline verb",
323 },
324 ActionEntry {
326 name: "red.registry:register",
327 category: ActionCategory::Other,
328 lifecycle_state: LifecycleState::Active,
329 gates_description: "register a new managed-config schema",
330 },
331 ActionEntry {
332 name: "red.registry:supersede",
333 category: ActionCategory::Other,
334 lifecycle_state: LifecycleState::Active,
335 gates_description: "supersede an existing managed-config schema",
336 },
337 ActionEntry {
338 name: "red.registry:*",
339 category: ActionCategory::Wildcard,
340 lifecycle_state: LifecycleState::Active,
341 gates_description: "any registry verb",
342 },
343 ActionEntry {
345 name: "*",
346 category: ActionCategory::Wildcard,
347 lifecycle_state: LifecycleState::Active,
348 gates_description: "any action (escape hatch — audit usage carefully)",
349 },
350 ActionEntry {
351 name: "admin:*",
352 category: ActionCategory::Wildcard,
353 lifecycle_state: LifecycleState::Active,
354 gates_description: "any admin verb",
355 },
356 ActionEntry {
357 name: "vault:*",
358 category: ActionCategory::Wildcard,
359 lifecycle_state: LifecycleState::Active,
360 gates_description: "any vault verb",
361 },
362 ActionEntry {
363 name: "kv:*",
364 category: ActionCategory::Wildcard,
365 lifecycle_state: LifecycleState::Active,
366 gates_description: "any KV verb",
367 },
368 ActionEntry {
369 name: "policy:*",
370 category: ActionCategory::Wildcard,
371 lifecycle_state: LifecycleState::Active,
372 gates_description: "any policy lifecycle verb",
373 },
374];
375
376pub fn is_valid_action(name: &str) -> bool {
380 ACTIONS
381 .iter()
382 .any(|e| e.name == name && !matches!(e.lifecycle_state, LifecycleState::Removed))
383}
384
385pub fn lookup(name: &str) -> Option<&'static ActionEntry> {
387 ACTIONS.iter().find(|e| e.name == name)
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393 use std::collections::HashSet;
394
395 const HISTORICAL_ALLOWLIST: &[&str] = &[
400 "select",
401 "write",
402 "insert",
403 "update",
404 "delete",
405 "truncate",
406 "references",
407 "execute",
408 "usage",
409 "grant",
410 "revoke",
411 "create",
412 "drop",
413 "alter",
414 "policy:put",
415 "policy:drop",
416 "policy:attach",
417 "policy:detach",
418 "policy:simulate",
419 "kv:invalidate",
420 "admin:bootstrap",
421 "admin:audit-read",
422 "admin:reload",
423 "admin:lease-promote",
424 "config:read",
425 "config:write",
426 "config:*",
427 "vault:read_metadata",
428 "vault:read",
429 "vault:write",
430 "vault:unseal",
431 "vault:unseal_history",
432 "vault:purge",
433 "evidence:export",
434 "evidence:*",
435 "red.registry:register",
436 "red.registry:supersede",
437 "red.registry:*",
438 "*",
439 "admin:*",
440 "vault:*",
441 "kv:*",
442 "policy:*",
443 ];
444
445 #[test]
446 fn no_duplicate_names() {
447 let mut seen = HashSet::new();
448 for entry in ACTIONS {
449 assert!(
450 seen.insert(entry.name),
451 "duplicate action name in catalog: {}",
452 entry.name
453 );
454 }
455 }
456
457 #[test]
458 fn covers_historical_allowlist() {
459 let names: HashSet<&'static str> = ACTIONS.iter().map(|e| e.name).collect();
460 for action in HISTORICAL_ALLOWLIST {
461 assert!(
462 names.contains(action),
463 "catalog missing historically-accepted action: {action}",
464 );
465 }
466 }
467
468 #[test]
469 fn historical_allowlist_still_validates() {
470 for action in HISTORICAL_ALLOWLIST {
471 assert!(
472 is_valid_action(action),
473 "action {action} was accepted before the catalog and must still validate",
474 );
475 }
476 }
477
478 #[test]
479 fn has_at_least_one_deprecated_entry() {
480 let count = ACTIONS
481 .iter()
482 .filter(|e| matches!(e.lifecycle_state, LifecycleState::Deprecated { .. }))
483 .count();
484 assert!(
485 count >= 1,
486 "catalog must demonstrate the Deprecated lifecycle state with at least one entry",
487 );
488 }
489
490 #[test]
491 fn removed_entries_are_rejected() {
492 for entry in ACTIONS {
495 if matches!(entry.lifecycle_state, LifecycleState::Removed) {
496 assert!(
497 !is_valid_action(entry.name),
498 "Removed entry {} must not validate",
499 entry.name,
500 );
501 }
502 }
503 }
504
505 #[test]
506 fn lookup_finds_known_entries() {
507 assert!(lookup("policy:put").is_some());
508 assert!(lookup("definitely-not-an-action").is_none());
509 }
510}