yeti_types/schema/access.rs
1//! Authorization declarations for tables (`@access` directive).
2//!
3//! Introduces `@access` as the dedicated authorization axis.
4//! It owns the `public: [...]` allow-list and adds a per-op
5//! `roles: { op: [role, ...] }` RBAC matrix.
6//!
7//! Extends the matrix to support both flat-list and
8//! per-protocol shapes. See [`OpPolicy`] for the per-op shape.
9
10use std::collections::HashMap;
11
12/// Transport that delivered a request — the request-origin role of the
13/// canonical [`Transport`](crate::transport::Transport). Used as a key in
14/// the `@access(roles:)` per-protocol shape and as a field on `Context`
15/// so the dispatch layer can match requests against per-protocol gates.
16///
17/// This is the SAME type as `@export`'s transport set; parse a key with
18/// `Protocol::from_name`. Kept under the `Protocol` name for its
19/// request-origin reading at call sites.
20pub use crate::transport::Transport as Protocol;
21
22/// Per-op policy in `@access(roles:)`.
23///
24/// `AnyProtocol(roles)` — the historical flat list shape; the role
25/// list applies regardless of which transport delivered the request.
26/// Parsed from `roles: { update: [admin, editor] }`.
27///
28/// `PerProtocol(map)` — gates by `(role, protocol)`; only the
29/// roles listed for the originating transport are allowed. Parsed
30/// from `roles: { update: { rest: [admin], mqtt: [client] } }`.
31#[derive(Debug, Clone)]
32pub enum OpPolicy {
33 /// Flat list: the role list applies regardless of transport.
34 /// Parsed from `roles: { update: [admin, editor] }`.
35 AnyProtocol(Vec<String>),
36 /// Per-protocol matrix: only roles listed for the originating
37 /// transport are allowed. Parsed from
38 /// `roles: { update: { rest: [admin], mqtt: [client] } }`.
39 PerProtocol(HashMap<Protocol, Vec<String>>),
40}
41
42impl OpPolicy {
43 /// Allowed roles for a request arriving on `protocol`. Returns
44 /// an empty slice when the op-policy denies all roles on that
45 /// protocol (per-protocol shape, protocol not listed).
46 #[must_use]
47 pub fn allowed_for(&self, protocol: Protocol) -> &[String] {
48 match self {
49 Self::AnyProtocol(roles) => roles.as_slice(),
50 Self::PerProtocol(map) => map.get(&protocol).map_or(&[], Vec::as_slice),
51 }
52 }
53}
54
55/// Operations that can be declared publicly accessible via
56/// `@access(public: [read, create, ...])`.
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
58pub enum PublicAccess {
59 /// GET requests (`allow_read`)
60 Read,
61 /// POST requests (`allow_create`)
62 Create,
63 /// PUT/PATCH requests (`allow_update`)
64 Update,
65 /// DELETE requests (`allow_delete`)
66 Delete,
67 /// SSE subscriptions (`allow_subscribe`)
68 Subscribe,
69 /// WebSocket connections (`allow_connect`)
70 Connect,
71 /// Publish operations (`allow_publish`)
72 Publish,
73}
74
75impl PublicAccess {
76 /// Parse a public access operation from a string (case-insensitive).
77 #[must_use]
78 pub fn parse(s: &str) -> Option<Self> {
79 match s.to_lowercase().as_str() {
80 "read" => Some(Self::Read),
81 "create" => Some(Self::Create),
82 "update" => Some(Self::Update),
83 "delete" => Some(Self::Delete),
84 "subscribe" => Some(Self::Subscribe),
85 "connect" => Some(Self::Connect),
86 "publish" => Some(Self::Publish),
87 _ => None,
88 }
89 }
90}
91
92/// `@access` directive — authorization axis.
93///
94/// Bare `@access` (no args) inherits the app's auth pipeline (current
95/// default behavior). Set `public` to allow unauthenticated callers
96/// to perform specific operations; set `roles` to require specific
97/// roles per operation.
98#[derive(Debug, Clone, Default)]
99pub struct AccessConfig {
100 /// Operations anyone can perform without authentication. Same
101 /// semantics as the older `@export(public:)` allow-list.
102 pub public: std::collections::HashSet<PublicAccess>,
103 /// Per-op required roles + optional per-protocol gating.
104 /// A request to operation `Op` arriving on
105 /// transport `P` is allowed when the authenticated user holds at
106 /// least one of `roles[&Op].allowed_for(P)`. Missing-from-the-map
107 /// = no per-op role requirement; the app's auth pipeline decides.
108 pub roles: HashMap<PublicAccess, OpPolicy>,
109}