orinium_browser/engine/permissions.rs
1//! Capability set inherited down the frame tree.
2//!
3//! A browsing context never decides its own privileges from its URL alone.
4//! Instead its effective [`Permissions`] are the intersection of the base
5//! capabilities of its own origin and the effective permissions of its parent
6//! frame ([`Permissions::inherit`]). By induction a descendant can never hold a
7//! capability its top-level ancestor lacks, so a nested or embedded context
8//! cannot escalate beyond the root document.
9//!
10//! Only browser-created chrome origins hold [`Permissions::chrome`] (which
11//! includes arbitrary local-file access); ordinary documents, including bundled
12//! `resource:///` pages, only ever get [`Permissions::content_for`].
13
14use url::Url;
15
16/// The capabilities a document is allowed to exercise.
17///
18/// Scheme capabilities (`network`/`resource`/`file`) govern *what can be
19/// loaded*; they never grant the right to read a cross-origin response. That
20/// latter authority is separate: only `privileged` (a browser-created chrome
21/// context) bypasses CORS. Keeping the two independent means a capability can
22/// never be mistaken for a CORS grant, nor vice versa.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
24pub struct Permissions {
25 /// May load network-backed schemes (`http`, `https`) and `data:` URLs.
26 pub network: bool,
27 /// May load `resource://` content. The resource loader confines such loads
28 /// to the bundle directory.
29 pub resource: bool,
30 /// May load arbitrary local files via `file://`. Reserved for chrome.
31 pub file: bool,
32 /// Marks a browser-created chrome context (shell, DevTools). This is the
33 /// only authority that bypasses ordinary CORS read checks; it is tracked
34 /// separately from the scheme capabilities so the two mechanisms stay
35 /// independent.
36 pub privileged: bool,
37}
38
39impl Permissions {
40 /// No capabilities at all.
41 pub const fn none() -> Self {
42 Self {
43 network: false,
44 resource: false,
45 file: false,
46 privileged: false,
47 }
48 }
49
50 /// Base capabilities for a document loaded from `url`.
51 ///
52 /// Every document may use the network; `resource://` documents additionally
53 /// reach bundled resources (confined to the bundle root). Local files are
54 /// never reachable from content, and ordinary documents are never
55 /// privileged.
56 pub fn content_for(url: &Url) -> Self {
57 Self {
58 network: true,
59 resource: url.scheme() == "resource",
60 file: false,
61 privileged: false,
62 }
63 }
64
65 /// Base capabilities for a document loaded from the string form of a URL.
66 ///
67 /// Unparsable URLs are treated as network-capable opaque content, matching a
68 /// document that cannot be resolved to a bundled or local origin.
69 pub fn content_for_str(url: &str) -> Self {
70 match Url::parse(url) {
71 Ok(url) => Self::content_for(&url),
72 Err(_) => Self {
73 network: true,
74 resource: false,
75 file: false,
76 privileged: false,
77 },
78 }
79 }
80
81 /// Full capabilities for browser-created origins (shell, DevTools).
82 pub const fn chrome() -> Self {
83 Self {
84 network: true,
85 resource: true,
86 file: true,
87 privileged: true,
88 }
89 }
90
91 /// Derives the permissions a child frame may hold, given its parent's
92 /// effective permissions. The result is the intersection of the two, so a
93 /// child can never exceed any ancestor.
94 pub const fn inherit(self, parent: Self) -> Self {
95 Self {
96 network: self.network && parent.network,
97 resource: self.resource && parent.resource,
98 file: self.file && parent.file,
99 privileged: self.privileged && parent.privileged,
100 }
101 }
102
103 /// Effective capabilities for a child frame loaded from `child_url` under
104 /// this (parent) set: the base capabilities of the child's own origin,
105 /// intersected with the parent's. A child can never gain a capability, and
106 /// privilege is never inherited.
107 pub fn for_child_frame(self, child_url: &str) -> Self {
108 Self::content_for_str(child_url).inherit(self)
109 }
110
111 /// Whether this set is a browser-created chrome context: the only authority
112 /// that bypasses ordinary CORS read checks.
113 pub const fn is_chrome(self) -> bool {
114 self.privileged
115 }
116
117 /// Whether a resource addressed by `url` may be loaded under these
118 /// permissions.
119 pub fn allows(self, url: &Url) -> bool {
120 match url.scheme() {
121 "http" | "https" | "data" => self.network,
122 "resource" => self.resource,
123 "file" => self.file,
124 _ => false,
125 }
126 }
127
128 /// Whether a resource addressed by the string form of a URL may be loaded.
129 ///
130 /// Unparsable URLs are treated as opaque content and only allowed when the
131 /// network capability is held.
132 pub fn allows_str(self, url: &str) -> bool {
133 match Url::parse(url) {
134 Ok(url) => self.allows(&url),
135 Err(_) => self.network,
136 }
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use crate::test_support::parse_url as url;
144
145 #[test]
146 fn content_resource_page_gets_no_file_access() {
147 let perms = Permissions::content_for(&url("resource:///test/index.html"));
148 assert!(perms.network);
149 assert!(perms.resource);
150 assert!(!perms.file);
151 assert!(!perms.allows(&url("file:///etc/passwd")));
152 assert!(perms.allows(&url("resource:///test/style.css")));
153 }
154
155 #[test]
156 fn web_page_reaches_network_only() {
157 let perms = Permissions::content_for(&url("https://example.test/"));
158 assert!(perms.allows(&url("https://example.test/x.js")));
159 assert!(perms.allows(&url("data:text/plain,hi")));
160 assert!(!perms.allows(&url("resource:///test/index.html")));
161 assert!(!perms.allows(&url("file:///etc/passwd")));
162 }
163
164 #[test]
165 fn chrome_reaches_files() {
166 let chrome = Permissions::chrome();
167 assert!(chrome.is_chrome());
168 assert!(chrome.privileged);
169 assert!(chrome.allows(&url("file:///etc/passwd")));
170 assert!(chrome.allows(&url("resource:///test/index.html")));
171 }
172
173 #[test]
174 fn privilege_is_separate_from_scheme_capabilities() {
175 // Every scheme capability can be present without any CORS-bypass
176 // authority: privilege is not implied by reaching files or resources.
177 let permissive_content = Permissions {
178 network: true,
179 resource: true,
180 file: true,
181 privileged: false,
182 };
183 assert!(!permissive_content.is_chrome());
184 assert!(permissive_content.allows(&url("file:///etc/passwd")));
185 }
186
187 #[test]
188 fn inherit_never_grants_more_than_the_parent() {
189 // A chrome frame embedding a web frame cannot hand down file access or
190 // privilege.
191 let child =
192 Permissions::content_for(&url("https://example.test/")).inherit(Permissions::chrome());
193 assert!(!child.file);
194 assert!(!child.resource);
195 assert!(!child.privileged);
196 assert!(child.network);
197
198 // A web frame embedding a chrome frame also yields web-only.
199 let child =
200 Permissions::chrome().inherit(Permissions::content_for(&url("https://example.test/")));
201 assert!(!child.file);
202 assert!(!child.privileged);
203 }
204
205 #[test]
206 fn privilege_never_reappears_down_the_tree() {
207 // chrome -> web -> chrome: once privilege is lost it cannot return,
208 // even if the deepest frame is itself a chrome URL.
209 let web = Permissions::chrome().for_child_frame("https://example.test/");
210 let regressed = web.for_child_frame("resource:///shell.html");
211 assert!(!regressed.privileged);
212 assert!(!regressed.is_chrome());
213 }
214
215 #[test]
216 fn for_child_frame_uses_the_child_origin_not_the_parent() {
217 // A resource child of a chrome parent keeps bundle access but is not
218 // privileged; a web child of a chrome parent loses resource access too.
219 let chrome = Permissions::chrome();
220 let resource_child = chrome.for_child_frame("resource:///test/index.html");
221 assert!(resource_child.resource);
222 assert!(!resource_child.file);
223 assert!(!resource_child.privileged);
224
225 let web_child = chrome.for_child_frame("https://example.test/");
226 assert!(!web_child.resource);
227 assert!(!web_child.file);
228 assert!(!web_child.privileged);
229
230 // An unparsable child URL is treated as opaque network content.
231 let opaque_child = chrome.for_child_frame("not a url");
232 assert!(opaque_child.network);
233 assert!(!opaque_child.resource);
234 assert!(!opaque_child.file);
235 assert!(!opaque_child.privileged);
236 }
237
238 #[test]
239 fn inherit_of_chrome_and_chrome_stays_chrome() {
240 assert_eq!(
241 Permissions::chrome().inherit(Permissions::chrome()),
242 Permissions::chrome()
243 );
244 }
245
246 #[test]
247 fn inherit_is_idempotent() {
248 let perms = Permissions::content_for(&url("resource:///a/b.html"));
249 assert_eq!(perms.inherit(perms), perms);
250 }
251}