1use serde::{Deserialize, Deserializer, Serialize, Serializer};
4use std::collections::BTreeSet;
5use std::iter::FromIterator;
6use uuid::Uuid;
7
8use crate::InterfaceOperation;
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
13#[serde(transparent)]
14pub struct PrincipalId(Uuid);
15
16impl PrincipalId {
17 pub fn from_uuid(value: Uuid) -> Self {
18 Self(value)
19 }
20
21 pub fn as_uuid(&self) -> &Uuid {
22 &self.0
23 }
24}
25
26#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
28#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
29#[serde(rename_all = "kebab-case")]
30pub enum Capability {
31 #[serde(rename = "session:read")]
32 SessionRead,
33 #[serde(rename = "session:write")]
34 SessionWrite,
35 #[serde(rename = "page:read")]
36 PageRead,
37 #[serde(rename = "page:write")]
38 PageWrite,
39 #[serde(rename = "browser:mutate")]
40 BrowserMutate,
41 #[serde(rename = "network:egress")]
42 NetworkEgress,
43 #[serde(rename = "file:upload")]
44 FileUpload,
45 #[serde(rename = "file:download")]
46 FileDownload,
47 #[serde(rename = "javascript:evaluate")]
48 JavascriptEvaluate,
49 #[serde(rename = "intent:execute")]
50 IntentExecute,
51 #[serde(rename = "vision:assist")]
52 VisionAssist,
53 #[serde(rename = "artifact:read")]
54 ArtifactRead,
55 #[serde(rename = "context:read")]
56 ContextRead,
57 #[serde(rename = "artifact:capture")]
58 ArtifactCapture,
59 #[serde(rename = "recovery:read")]
60 RecoveryRead,
61 #[serde(rename = "recovery:write")]
62 RecoveryWrite,
63 #[serde(rename = "job:submit")]
64 JobSubmit,
65 #[serde(rename = "job:read")]
66 JobRead,
67 #[serde(rename = "job:cancel")]
68 JobCancel,
69 #[serde(rename = "authority:admin")]
70 AuthorityAdmin,
71 #[serde(rename = "browser:fingerprint")]
72 BrowserFingerprint,
73 #[serde(rename = "browser:humanize")]
74 BrowserHumanize,
75}
76
77impl Capability {
78 pub const ALL: [Self; 22] = [
83 Self::SessionRead,
84 Self::SessionWrite,
85 Self::PageRead,
86 Self::PageWrite,
87 Self::BrowserMutate,
88 Self::NetworkEgress,
89 Self::FileUpload,
90 Self::FileDownload,
91 Self::JavascriptEvaluate,
92 Self::IntentExecute,
93 Self::VisionAssist,
94 Self::ArtifactRead,
95 Self::ContextRead,
96 Self::ArtifactCapture,
97 Self::RecoveryRead,
98 Self::RecoveryWrite,
99 Self::JobSubmit,
100 Self::JobRead,
101 Self::JobCancel,
102 Self::AuthorityAdmin,
103 Self::BrowserFingerprint,
104 Self::BrowserHumanize,
105 ];
106
107 pub const fn as_str(self) -> &'static str {
108 match self {
109 Self::SessionRead => "session:read",
110 Self::SessionWrite => "session:write",
111 Self::PageRead => "page:read",
112 Self::PageWrite => "page:write",
113 Self::BrowserMutate => "browser:mutate",
114 Self::NetworkEgress => "network:egress",
115 Self::FileUpload => "file:upload",
116 Self::FileDownload => "file:download",
117 Self::JavascriptEvaluate => "javascript:evaluate",
118 Self::IntentExecute => "intent:execute",
119 Self::VisionAssist => "vision:assist",
120 Self::ArtifactRead => "artifact:read",
121 Self::ContextRead => "context:read",
122 Self::ArtifactCapture => "artifact:capture",
123 Self::RecoveryRead => "recovery:read",
124 Self::RecoveryWrite => "recovery:write",
125 Self::JobSubmit => "job:submit",
126 Self::JobRead => "job:read",
127 Self::JobCancel => "job:cancel",
128 Self::AuthorityAdmin => "authority:admin",
129 Self::BrowserFingerprint => "browser:fingerprint",
130 Self::BrowserHumanize => "browser:humanize",
131 }
132 }
133}
134
135#[derive(Debug, Clone, Default, PartialEq, Eq)]
137#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
138pub struct CapabilitySet(BTreeSet<Capability>);
139
140impl std::str::FromStr for Capability {
141 type Err = UnknownCapability;
142
143 fn from_str(value: &str) -> Result<Self, Self::Err> {
147 Ok(match value {
148 "session:read" => Self::SessionRead,
149 "session:write" => Self::SessionWrite,
150 "page:read" => Self::PageRead,
151 "page:write" => Self::PageWrite,
152 "browser:mutate" => Self::BrowserMutate,
153 "network:egress" => Self::NetworkEgress,
154 "file:upload" => Self::FileUpload,
155 "file:download" => Self::FileDownload,
156 "javascript:evaluate" => Self::JavascriptEvaluate,
157 "intent:execute" => Self::IntentExecute,
158 "vision:assist" => Self::VisionAssist,
159 "artifact:read" => Self::ArtifactRead,
160 "context:read" => Self::ContextRead,
161 "artifact:capture" => Self::ArtifactCapture,
162 "recovery:read" => Self::RecoveryRead,
163 "recovery:write" => Self::RecoveryWrite,
164 "job:submit" => Self::JobSubmit,
165 "job:read" => Self::JobRead,
166 "job:cancel" => Self::JobCancel,
167 "authority:admin" => Self::AuthorityAdmin,
168 "browser:fingerprint" => Self::BrowserFingerprint,
169 "browser:humanize" => Self::BrowserHumanize,
170 _ => return Err(UnknownCapability(value.to_owned())),
171 })
172 }
173}
174
175#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
177#[error("unknown capability: {0}")]
178pub struct UnknownCapability(pub String);
179
180impl CapabilitySet {
181 pub fn new(capabilities: impl IntoIterator<Item = Capability>) -> Self {
182 capabilities.into_iter().collect()
183 }
184
185 pub fn contains(&self, capability: Capability) -> bool {
186 self.0.contains(&capability)
187 }
188
189 pub fn allows(&self, operation: InterfaceOperation) -> bool {
190 operation
191 .required()
192 .iter()
193 .all(|capability| self.contains(*capability))
194 }
195}
196
197impl FromIterator<Capability> for CapabilitySet {
198 fn from_iter<T: IntoIterator<Item = Capability>>(iter: T) -> Self {
199 Self(iter.into_iter().collect())
200 }
201}
202
203impl Serialize for CapabilitySet {
204 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
205 where
206 S: Serializer,
207 {
208 let mut capabilities: Vec<_> = self.0.iter().copied().collect();
209 capabilities.sort_by_key(|capability| capability.as_str());
210 capabilities.serialize(serializer)
211 }
212}
213
214impl<'de> Deserialize<'de> for CapabilitySet {
215 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
216 where
217 D: Deserializer<'de>,
218 {
219 let capabilities = Vec::<Capability>::deserialize(deserializer)?;
220 let mut verified = BTreeSet::new();
221 for capability in capabilities {
222 if !verified.insert(capability) {
223 return Err(serde::de::Error::custom("duplicate capability"));
224 }
225 }
226 Ok(Self(verified))
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
235 fn authority_admin_serde_round_trip() {
236 let json = serde_json::to_string(&Capability::AuthorityAdmin).unwrap();
237 assert_eq!(json, "\"authority:admin\"");
238 let parsed: Capability = serde_json::from_str(&json).unwrap();
239 assert_eq!(parsed, Capability::AuthorityAdmin);
240 }
241
242 #[test]
243 fn all_is_exhaustive_and_unique() {
244 fn listed(capability: Capability) {
248 match capability {
249 Capability::SessionRead
250 | Capability::SessionWrite
251 | Capability::PageRead
252 | Capability::PageWrite
253 | Capability::BrowserMutate
254 | Capability::NetworkEgress
255 | Capability::FileUpload
256 | Capability::FileDownload
257 | Capability::JavascriptEvaluate
258 | Capability::IntentExecute
259 | Capability::VisionAssist
260 | Capability::ArtifactRead
261 | Capability::ContextRead
262 | Capability::ArtifactCapture
263 | Capability::RecoveryRead
264 | Capability::RecoveryWrite
265 | Capability::JobSubmit
266 | Capability::JobRead
267 | Capability::JobCancel
268 | Capability::AuthorityAdmin
269 | Capability::BrowserFingerprint
270 | Capability::BrowserHumanize => {}
271 }
272 assert!(
273 Capability::ALL.contains(&capability),
274 "{capability:?} is missing from Capability::ALL"
275 );
276 }
277
278 for capability in Capability::ALL {
279 listed(capability);
280 assert_eq!(
281 capability.as_str().parse::<Capability>().unwrap(),
282 capability
283 );
284 }
285
286 let unique = Capability::ALL.into_iter().collect::<BTreeSet<_>>();
287 assert_eq!(unique.len(), Capability::ALL.len(), "ALL has duplicates");
288 }
289}