ograf_core/access.rs
1//! Dependency Inversion seam: Core defines what access control it needs,
2//! never how it's decided. Consumers can provide their own implementation;
3//! [`AllowAllAccessControl`] is a trivial one for anyone who wants no
4//! restriction at all.
5
6use async_trait::async_trait;
7
8use crate::models::{Graphic, RendererInfo};
9
10#[async_trait]
11pub trait AccessControl: Send + Sync {
12 /// Authorizes a renderer's WebSocket connection *before* it's upgraded.
13 /// `query` is the connect URL's raw query string, unparsed and
14 /// unvalidated by Core — an implementation decides what it means (a
15 /// zone name + token for `ZoneAccessControl`, ignored entirely here).
16 async fn authorize_connect(&self, query: &str) -> bool;
17
18 /// Authorizes the renderer name from `hello`, before the session is registered.
19 /// Called after `authorize_connect` and after name validation, but before
20 /// attempting to register the session. Returning `false` closes the connection
21 /// and never registers it.
22 ///
23 /// Default implementation allows all names (backward compatible). Implementations
24 /// can enforce policies like "name must match query string" or "name must be
25 /// authorized for this zone".
26 ///
27 /// Added in 0.4.0 to prevent name squatting (connecting with valid query but
28 /// claiming another zone's renderer name in `hello`).
29 async fn authorize_name(&self, _name: &str, _query: &str) -> bool {
30 true
31 }
32
33 /// Called once a renderer's `hello` names it, after `authorize_connect`
34 /// already approved the connection — a chance to record durable identity
35 /// for tracking or failover purposes. Best-effort: Core doesn't drop the
36 /// connection if this does nothing.
37 async fn on_renderer_connected(&self, name: &str, query: &str);
38
39 /// Filters `renderers` down to what `api_key` may see — `GET /renderers`.
40 async fn filter_visible(
41 &self,
42 api_key: &str,
43 renderers: Vec<RendererInfo>,
44 ) -> Vec<RendererInfo>;
45
46 /// Filters `graphics` down to what `api_key` may see — `GET /graphics`.
47 /// Default implementation returns all graphics (no filtering), maintaining
48 /// backward compatibility and the original "unscoped by design" behavior.
49 async fn filter_graphics(&self, _api_key: &str, graphics: Vec<Graphic>) -> Vec<Graphic> {
50 graphics
51 }
52
53 /// Whether `api_key` may target the renderer named `renderer_name` —
54 /// checked before every renderer-scoped call (get/target/load/play/
55 /// stop/update/customAction/clear). Keyed on the stable *name*, not the
56 /// per-session `id`.
57 async fn can_target(&self, api_key: &str, renderer_name: &str) -> bool;
58
59 /// Whether `api_key` may load `graphic_id` onto `renderer_name` — checked
60 /// before load() sends a LoadMessage. Default implementation allows all
61 /// loads (maintaining backward compatibility), but implementations can
62 /// enforce zone/renderer-specific graphic restrictions.
63 async fn can_load_graphic(
64 &self,
65 _api_key: &str,
66 _renderer_name: &str,
67 _graphic_id: &str,
68 ) -> bool {
69 true
70 }
71
72 /// Whether `api_key` may delete `graphic_id` — `DELETE /graphics/{id}`.
73 /// Defaults to allowing it, like every other call without access control:
74 /// a deployment that runs Core on its own already lets anyone on its
75 /// network clear what's on air. A delete without `force` only unlists the
76 /// graphic, so it's recoverable until the retention period ends.
77 async fn can_delete_graphic(&self, _api_key: &str, _graphic_id: &str) -> bool {
78 true
79 }
80}
81
82/// No restriction at all — every renderer visible, every key can target
83/// anything, every connection accepted. The default for a consumer that
84/// doesn't need access control (or hasn't wired anything up yet).
85pub struct AllowAllAccessControl;
86
87#[async_trait]
88impl AccessControl for AllowAllAccessControl {
89 async fn authorize_connect(&self, _query: &str) -> bool {
90 true
91 }
92
93 async fn on_renderer_connected(&self, _name: &str, _query: &str) {}
94
95 async fn filter_visible(
96 &self,
97 _api_key: &str,
98 renderers: Vec<RendererInfo>,
99 ) -> Vec<RendererInfo> {
100 renderers
101 }
102
103 async fn can_target(&self, _api_key: &str, _renderer_name: &str) -> bool {
104 true
105 }
106}