Skip to main content

ferrijs_std/
permissions.rs

1//! The realm's permission container, and the checks every module makes.
2//!
3//! The policy itself is `ferrijs_permissions`; this module is where it
4//! meets `QuickJS`: the [`Container`] is stored as context userdata, each
5//! `fs` / `os` / network entry point calls one of the `check_*` helpers,
6//! and a refusal is thrown into JS as a `PermissionDeniedError` carrying
7//! Node's `ERR_ACCESS_DENIED` code plus the `permission` and `resource`
8//! Node attaches.
9//!
10//! A realm with no container installed is unrestricted. The `ferrijs`
11//! runtime always installs one (deny-all unless the host grants more);
12//! a host embedding this crate without the runtime calls [`install`]
13//! itself, or gets a standard library with no sandbox.
14//!
15//! The container is the realm's for its whole life and only narrows.
16//! Nothing here carries a policy across a callback: a timer fires under
17//! the same container it was armed under, because there is only one.
18
19use std::path::Path;
20use std::sync::Arc;
21
22use ferrijs_permissions::{Container, Denied, SysInfo};
23use rquickjs::{Ctx, JsLifetime, Object, Value};
24
25struct ContainerUd(Arc<Container>);
26
27// SAFETY: holds an owned `Arc` to `'static` data; no borrowed JS values.
28#[allow(unsafe_code)]
29unsafe impl JsLifetime<'_> for ContainerUd {
30  type Changed<'to> = ContainerUd;
31}
32
33/// Install the realm's container. A second call replaces the first.
34pub fn install(ctx: &Ctx<'_>, container: Arc<Container>) {
35  let _ = ctx.store_userdata(ContainerUd(container));
36}
37
38/// The realm's container, if a host installed one.
39#[must_use]
40pub fn container(ctx: &Ctx<'_>) -> Option<Arc<Container>> {
41  ctx.userdata::<ContainerUd>().map(|ud| Arc::clone(&ud.0))
42}
43
44/// Throw `denied` into JS as a `PermissionDeniedError`.
45///
46/// Shape: `name` is `PermissionDeniedError`, `code` is
47/// `ERR_ACCESS_DENIED`, `permission` is the kind (`read`, `net`, ...)
48/// and `resource` is what was asked for, so a script can catch and
49/// report it the way it would Node's.
50#[must_use]
51pub fn throw_denied(ctx: &Ctx<'_>, denied: &Denied) -> rquickjs::Error {
52  let built: rquickjs::Result<Value<'_>> = (|| {
53    let ctor: rquickjs::function::Constructor<'_> = ctx.globals().get("Error")?;
54    let err: Object<'_> = ctor.construct((denied.to_string(),))?;
55    err.set("name", Denied::NAME)?;
56    err.set("code", Denied::CODE)?;
57    err.set("permission", denied.kind.as_str())?;
58    err.set("resource", denied.resource.as_str())?;
59    Ok(err.into_value())
60  })();
61  match built {
62    Ok(v) => ctx.throw(v),
63    Err(_) => rquickjs::Exception::throw_message(ctx, &denied.to_string()),
64  }
65}
66
67fn checked(ctx: &Ctx<'_>, result: Result<(), Denied>) -> rquickjs::Result<()> {
68  result.map_err(|denied| throw_denied(ctx, &denied))
69}
70
71/// # Errors
72///
73/// A `PermissionDeniedError` when the realm's `read` grant does not
74/// cover `path`.
75pub fn check_read(ctx: &Ctx<'_>, path: &Path) -> rquickjs::Result<()> {
76  match container(ctx) {
77    Some(c) => checked(ctx, c.check_read(path)),
78    None => Ok(()),
79  }
80}
81
82/// # Errors
83///
84/// A `PermissionDeniedError` when the realm's `write` grant does not
85/// cover `path`.
86pub fn check_write(ctx: &Ctx<'_>, path: &Path) -> rquickjs::Result<()> {
87  match container(ctx) {
88    Some(c) => checked(ctx, c.check_write(path)),
89    None => Ok(()),
90  }
91}
92
93/// # Errors
94///
95/// A `PermissionDeniedError` when the realm's `net` grant does not
96/// cover `host:port`.
97pub fn check_net(ctx: &Ctx<'_>, host: &str, port: Option<u16>) -> rquickjs::Result<()> {
98  match container(ctx) {
99    Some(c) => checked(ctx, c.check_net(host, port)),
100    None => Ok(()),
101  }
102}
103
104/// # Errors
105///
106/// A `PermissionDeniedError` when the realm's `env` grant does not
107/// cover `name`.
108pub fn check_env(ctx: &Ctx<'_>, name: &str) -> rquickjs::Result<()> {
109  match container(ctx) {
110    Some(c) => checked(ctx, c.check_env(name)),
111    None => Ok(()),
112  }
113}
114
115/// # Errors
116///
117/// A `PermissionDeniedError` when the realm's `sys` grant does not
118/// cover `item`.
119pub fn check_sys(ctx: &Ctx<'_>, item: SysInfo) -> rquickjs::Result<()> {
120  match container(ctx) {
121    Some(c) => checked(ctx, c.check_sys(item)),
122    None => Ok(()),
123  }
124}
125
126/// Whether `kind` covers `resource` right now, for a `has()`-style
127/// query. Unrestricted when no container is installed.
128///
129/// # Errors
130///
131/// A `net` rule or `sys` name that does not parse, thrown as a
132/// `TypeError`.
133pub fn has(ctx: &Ctx<'_>, kind: &str, resource: Option<&str>) -> rquickjs::Result<bool> {
134  let kind: ferrijs_permissions::Kind = kind
135    .parse()
136    .map_err(|m: String| rquickjs::Exception::throw_type(ctx, &m))?;
137  match container(ctx) {
138    Some(c) => c
139      .has(kind, resource)
140      .map_err(|m| rquickjs::Exception::throw_type(ctx, &m)),
141    None => Ok(true),
142  }
143}
144
145/// Give up `resource` under `kind` (or the whole kind) for good: Node's
146/// `process.permission.drop`. Nothing when no container is installed.
147///
148/// # Errors
149///
150/// A kind, rule or name that does not parse, thrown as a `TypeError`.
151pub fn drop(ctx: &Ctx<'_>, kind: &str, resource: Option<&str>) -> rquickjs::Result<()> {
152  let kind: ferrijs_permissions::Kind = kind
153    .parse()
154    .map_err(|m: String| rquickjs::Exception::throw_type(ctx, &m))?;
155  let Some(c) = container(ctx) else {
156    return Ok(());
157  };
158  match resource {
159    Some(r) => c.deny(kind, r).map_err(|m| rquickjs::Exception::throw_type(ctx, &m)),
160    None => {
161      let mut remaining = (*c.permissions()).clone();
162      match kind {
163        ferrijs_permissions::Kind::Read => remaining.read = ferrijs_permissions::Allow::None,
164        ferrijs_permissions::Kind::Write => remaining.write = ferrijs_permissions::Allow::None,
165        ferrijs_permissions::Kind::Net => remaining.net = ferrijs_permissions::Allow::None,
166        ferrijs_permissions::Kind::Env => remaining.env = ferrijs_permissions::Allow::None,
167        ferrijs_permissions::Kind::Sys => remaining.sys = ferrijs_permissions::Allow::None,
168      }
169      c.revoke(&remaining);
170      Ok(())
171    },
172  }
173}
174
175#[cfg(test)]
176mod tests {
177  use super::*;
178  use ferrijs_permissions::Permissions;
179
180  #[test]
181  fn a_refusal_is_a_node_shaped_error() {
182    let rt = rquickjs::Runtime::new().unwrap();
183    let cx = rquickjs::Context::full(&rt).unwrap();
184    cx.with(|ctx| {
185      install(&ctx, Arc::new(Container::new(Permissions::none())));
186      let err = check_read(&ctx, Path::new("/etc/passwd")).unwrap_err();
187      assert!(matches!(err, rquickjs::Error::Exception));
188      let ex = ctx.catch();
189      let obj = ex.as_object().unwrap();
190      assert_eq!(obj.get::<_, String>("name").unwrap(), "PermissionDeniedError");
191      assert_eq!(obj.get::<_, String>("code").unwrap(), "ERR_ACCESS_DENIED");
192      assert_eq!(obj.get::<_, String>("permission").unwrap(), "read");
193      assert_eq!(obj.get::<_, String>("resource").unwrap(), "/etc/passwd");
194    });
195  }
196
197  #[test]
198  fn no_container_means_no_restriction() {
199    let rt = rquickjs::Runtime::new().unwrap();
200    let cx = rquickjs::Context::full(&rt).unwrap();
201    cx.with(|ctx| {
202      assert!(check_write(&ctx, Path::new("/anything")).is_ok());
203      assert!(check_net(&ctx, "example.com", Some(443)).is_ok());
204    });
205  }
206}