use std::path::Path;
use std::sync::Arc;
use ferrijs_permissions::{Container, Denied, SysInfo};
use rquickjs::{Ctx, JsLifetime, Object, Value};
struct ContainerUd(Arc<Container>);
#[allow(unsafe_code)]
unsafe impl JsLifetime<'_> for ContainerUd {
type Changed<'to> = ContainerUd;
}
pub fn install(ctx: &Ctx<'_>, container: Arc<Container>) {
let _ = ctx.store_userdata(ContainerUd(container));
}
#[must_use]
pub fn container(ctx: &Ctx<'_>) -> Option<Arc<Container>> {
ctx.userdata::<ContainerUd>().map(|ud| Arc::clone(&ud.0))
}
#[must_use]
pub fn throw_denied(ctx: &Ctx<'_>, denied: &Denied) -> rquickjs::Error {
let built: rquickjs::Result<Value<'_>> = (|| {
let ctor: rquickjs::function::Constructor<'_> = ctx.globals().get("Error")?;
let err: Object<'_> = ctor.construct((denied.to_string(),))?;
err.set("name", Denied::NAME)?;
err.set("code", Denied::CODE)?;
err.set("permission", denied.kind.as_str())?;
err.set("resource", denied.resource.as_str())?;
Ok(err.into_value())
})();
match built {
Ok(v) => ctx.throw(v),
Err(_) => rquickjs::Exception::throw_message(ctx, &denied.to_string()),
}
}
fn checked(ctx: &Ctx<'_>, result: Result<(), Denied>) -> rquickjs::Result<()> {
result.map_err(|denied| throw_denied(ctx, &denied))
}
pub fn check_read(ctx: &Ctx<'_>, path: &Path) -> rquickjs::Result<()> {
match container(ctx) {
Some(c) => checked(ctx, c.check_read(path)),
None => Ok(()),
}
}
pub fn check_write(ctx: &Ctx<'_>, path: &Path) -> rquickjs::Result<()> {
match container(ctx) {
Some(c) => checked(ctx, c.check_write(path)),
None => Ok(()),
}
}
pub fn check_net(ctx: &Ctx<'_>, host: &str, port: Option<u16>) -> rquickjs::Result<()> {
match container(ctx) {
Some(c) => checked(ctx, c.check_net(host, port)),
None => Ok(()),
}
}
pub fn check_env(ctx: &Ctx<'_>, name: &str) -> rquickjs::Result<()> {
match container(ctx) {
Some(c) => checked(ctx, c.check_env(name)),
None => Ok(()),
}
}
pub fn check_sys(ctx: &Ctx<'_>, item: SysInfo) -> rquickjs::Result<()> {
match container(ctx) {
Some(c) => checked(ctx, c.check_sys(item)),
None => Ok(()),
}
}
pub fn has(ctx: &Ctx<'_>, kind: &str, resource: Option<&str>) -> rquickjs::Result<bool> {
let kind: ferrijs_permissions::Kind = kind
.parse()
.map_err(|m: String| rquickjs::Exception::throw_type(ctx, &m))?;
match container(ctx) {
Some(c) => c
.has(kind, resource)
.map_err(|m| rquickjs::Exception::throw_type(ctx, &m)),
None => Ok(true),
}
}
pub fn drop(ctx: &Ctx<'_>, kind: &str, resource: Option<&str>) -> rquickjs::Result<()> {
let kind: ferrijs_permissions::Kind = kind
.parse()
.map_err(|m: String| rquickjs::Exception::throw_type(ctx, &m))?;
let Some(c) = container(ctx) else {
return Ok(());
};
match resource {
Some(r) => c.deny(kind, r).map_err(|m| rquickjs::Exception::throw_type(ctx, &m)),
None => {
let mut remaining = (*c.permissions()).clone();
match kind {
ferrijs_permissions::Kind::Read => remaining.read = ferrijs_permissions::Allow::None,
ferrijs_permissions::Kind::Write => remaining.write = ferrijs_permissions::Allow::None,
ferrijs_permissions::Kind::Net => remaining.net = ferrijs_permissions::Allow::None,
ferrijs_permissions::Kind::Env => remaining.env = ferrijs_permissions::Allow::None,
ferrijs_permissions::Kind::Sys => remaining.sys = ferrijs_permissions::Allow::None,
}
c.revoke(&remaining);
Ok(())
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use ferrijs_permissions::Permissions;
#[test]
fn a_refusal_is_a_node_shaped_error() {
let rt = rquickjs::Runtime::new().unwrap();
let cx = rquickjs::Context::full(&rt).unwrap();
cx.with(|ctx| {
install(&ctx, Arc::new(Container::new(Permissions::none())));
let err = check_read(&ctx, Path::new("/etc/passwd")).unwrap_err();
assert!(matches!(err, rquickjs::Error::Exception));
let ex = ctx.catch();
let obj = ex.as_object().unwrap();
assert_eq!(obj.get::<_, String>("name").unwrap(), "PermissionDeniedError");
assert_eq!(obj.get::<_, String>("code").unwrap(), "ERR_ACCESS_DENIED");
assert_eq!(obj.get::<_, String>("permission").unwrap(), "read");
assert_eq!(obj.get::<_, String>("resource").unwrap(), "/etc/passwd");
});
}
#[test]
fn no_container_means_no_restriction() {
let rt = rquickjs::Runtime::new().unwrap();
let cx = rquickjs::Context::full(&rt).unwrap();
cx.with(|ctx| {
assert!(check_write(&ctx, Path::new("/anything")).is_ok());
assert!(check_net(&ctx, "example.com", Some(443)).is_ok());
});
}
}