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