Skip to main content

coil_wasm/
grants.rs

1use std::collections::BTreeSet;
2use std::fmt;
3use std::time::Duration;
4
5use crate::error::WasmModelError;
6use crate::ids::ExtensionPointKind;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
9pub enum StorageClassGrant {
10    PublicUpload,
11    PrivateShared,
12    LocalOnlySensitive,
13    PublicAsset,
14}
15
16impl fmt::Display for StorageClassGrant {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        match self {
19            Self::PublicUpload => f.write_str("public_upload"),
20            Self::PrivateShared => f.write_str("private_shared"),
21            Self::LocalOnlySensitive => f.write_str("local_only_sensitive"),
22            Self::PublicAsset => f.write_str("public_asset"),
23        }
24    }
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub enum MetadataGrant {
29    JsonLd,
30    SitemapEntry,
31    Translation,
32    SeoHead,
33}
34
35impl fmt::Display for MetadataGrant {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        match self {
38            Self::JsonLd => f.write_str("json_ld"),
39            Self::SitemapEntry => f.write_str("sitemap_entry"),
40            Self::Translation => f.write_str("translation"),
41            Self::SeoHead => f.write_str("seo_head"),
42        }
43    }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
47pub enum HostCapabilityGrant {
48    DataRead { resource: String },
49    DataWrite { resource: String },
50    AuthCheck,
51    AuthList,
52    AuthLookup,
53    AuthTupleWrite,
54    StorageRead { class: StorageClassGrant },
55    StorageWrite { class: StorageClassGrant },
56    RenderFragment { slot: String },
57    MetadataWrite { kind: MetadataGrant },
58    CacheHintWrite,
59    OutboundHttp { integration: String },
60    SecretRead { secret: String },
61    EnqueueJob { queue: String },
62}
63
64impl fmt::Display for HostCapabilityGrant {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        match self {
67            Self::DataRead { resource } => write!(f, "data.read:{resource}"),
68            Self::DataWrite { resource } => write!(f, "data.write:{resource}"),
69            Self::AuthCheck => f.write_str("auth.check"),
70            Self::AuthList => f.write_str("auth.list"),
71            Self::AuthLookup => f.write_str("auth.lookup"),
72            Self::AuthTupleWrite => f.write_str("auth.tuple_write"),
73            Self::StorageRead { class } => write!(f, "storage.read:{class}"),
74            Self::StorageWrite { class } => write!(f, "storage.write:{class}"),
75            Self::RenderFragment { slot } => write!(f, "render.fragment:{slot}"),
76            Self::MetadataWrite { kind } => write!(f, "metadata.write:{kind}"),
77            Self::CacheHintWrite => f.write_str("cache.hint.write"),
78            Self::OutboundHttp { integration } => write!(f, "http.outbound:{integration}"),
79            Self::SecretRead { secret } => write!(f, "secret.read:{secret}"),
80            Self::EnqueueJob { queue } => write!(f, "job.enqueue:{queue}"),
81        }
82    }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Default)]
86pub struct HostGrantSet {
87    grants: BTreeSet<HostCapabilityGrant>,
88}
89
90impl HostGrantSet {
91    pub fn new() -> Self {
92        Self::default()
93    }
94
95    pub fn from_grants(grants: impl IntoIterator<Item = HostCapabilityGrant>) -> Self {
96        let mut set = Self::new();
97        for grant in grants {
98            set.insert(grant);
99        }
100        set
101    }
102
103    pub fn insert(&mut self, grant: HostCapabilityGrant) {
104        self.grants.insert(grant);
105    }
106
107    pub fn contains(&self, grant: &HostCapabilityGrant) -> bool {
108        self.grants.contains(grant)
109    }
110
111    pub fn is_subset_of(&self, other: &Self) -> bool {
112        self.grants.iter().all(|grant| other.contains(grant))
113    }
114
115    pub fn len(&self) -> usize {
116        self.grants.len()
117    }
118
119    pub fn iter(&self) -> impl Iterator<Item = &HostCapabilityGrant> {
120        self.grants.iter()
121    }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub struct ResourceLimits {
126    pub max_runtime: Duration,
127    pub max_memory_bytes: u64,
128    pub max_outbound_requests: u32,
129    pub max_outbound_response_bytes: u64,
130    pub max_storage_writes: u32,
131    pub max_storage_bytes: u64,
132    pub max_concurrency: u16,
133}
134
135impl ResourceLimits {
136    pub const fn new(
137        max_runtime: Duration,
138        max_memory_bytes: u64,
139        max_outbound_requests: u32,
140        max_outbound_response_bytes: u64,
141        max_storage_writes: u32,
142        max_storage_bytes: u64,
143        max_concurrency: u16,
144    ) -> Self {
145        Self {
146            max_runtime,
147            max_memory_bytes,
148            max_outbound_requests,
149            max_outbound_response_bytes,
150            max_storage_writes,
151            max_storage_bytes,
152            max_concurrency,
153        }
154    }
155
156    pub fn baseline_for(point: ExtensionPointKind) -> Self {
157        match point {
158            ExtensionPointKind::Page
159            | ExtensionPointKind::Api
160            | ExtensionPointKind::AdminWidget
161            | ExtensionPointKind::RenderHook => Self::new(
162                Duration::from_secs(2),
163                64 * 1024 * 1024,
164                4,
165                4 * 1024 * 1024,
166                2,
167                8 * 1024 * 1024,
168                32,
169            ),
170            ExtensionPointKind::Webhook => Self::new(
171                Duration::from_secs(5),
172                64 * 1024 * 1024,
173                6,
174                8 * 1024 * 1024,
175                2,
176                8 * 1024 * 1024,
177                16,
178            ),
179            ExtensionPointKind::Job | ExtensionPointKind::ScheduledJob => Self::new(
180                Duration::from_secs(30),
181                128 * 1024 * 1024,
182                20,
183                16 * 1024 * 1024,
184                16,
185                64 * 1024 * 1024,
186                4,
187            ),
188        }
189    }
190
191    pub fn validate(&self) -> Result<(), WasmModelError> {
192        if self.max_runtime.is_zero() {
193            return Err(WasmModelError::ZeroLimit {
194                field: "max_runtime",
195            });
196        }
197        if self.max_memory_bytes == 0 {
198            return Err(WasmModelError::ZeroLimit {
199                field: "max_memory_bytes",
200            });
201        }
202        if self.max_outbound_requests == 0 {
203            return Err(WasmModelError::ZeroLimit {
204                field: "max_outbound_requests",
205            });
206        }
207        if self.max_outbound_response_bytes == 0 {
208            return Err(WasmModelError::ZeroLimit {
209                field: "max_outbound_response_bytes",
210            });
211        }
212        if self.max_storage_writes == 0 {
213            return Err(WasmModelError::ZeroLimit {
214                field: "max_storage_writes",
215            });
216        }
217        if self.max_storage_bytes == 0 {
218            return Err(WasmModelError::ZeroLimit {
219                field: "max_storage_bytes",
220            });
221        }
222        if self.max_concurrency == 0 {
223            return Err(WasmModelError::ZeroLimit {
224                field: "max_concurrency",
225            });
226        }
227
228        Ok(())
229    }
230
231    pub(crate) fn ensure_no_looser_than(
232        &self,
233        declared: &Self,
234        handler_id: &crate::ids::HandlerId,
235    ) -> Result<(), WasmModelError> {
236        let checks = [
237            (self.max_runtime <= declared.max_runtime, "max_runtime"),
238            (
239                self.max_memory_bytes <= declared.max_memory_bytes,
240                "max_memory_bytes",
241            ),
242            (
243                self.max_outbound_requests <= declared.max_outbound_requests,
244                "max_outbound_requests",
245            ),
246            (
247                self.max_outbound_response_bytes <= declared.max_outbound_response_bytes,
248                "max_outbound_response_bytes",
249            ),
250            (
251                self.max_storage_writes <= declared.max_storage_writes,
252                "max_storage_writes",
253            ),
254            (
255                self.max_storage_bytes <= declared.max_storage_bytes,
256                "max_storage_bytes",
257            ),
258            (
259                self.max_concurrency <= declared.max_concurrency,
260                "max_concurrency",
261            ),
262        ];
263
264        for (passes, field) in checks {
265            if !passes {
266                return Err(WasmModelError::LimitOverrideExceedsDeclared {
267                    handler_id: handler_id.to_string(),
268                    field,
269                });
270            }
271        }
272
273        Ok(())
274    }
275}