Skip to main content

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}