ferrijs_std/
permissions.rs1use 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#[allow(unsafe_code)]
29unsafe impl JsLifetime<'_> for ContainerUd {
30 type Changed<'to> = ContainerUd;
31}
32
33pub fn install(ctx: &Ctx<'_>, container: Arc<Container>) {
35 let _ = ctx.store_userdata(ContainerUd(container));
36}
37
38#[must_use]
40pub fn container(ctx: &Ctx<'_>) -> Option<Arc<Container>> {
41 ctx.userdata::<ContainerUd>().map(|ud| Arc::clone(&ud.0))
42}
43
44#[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
71pub 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
82pub 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
93pub 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
104pub 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
115pub 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
126pub 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
145pub 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}