use std::fmt;
use std::path::{Component, Path, PathBuf};
use std::sync::Arc;
use crate::graph::ElementKind;
use crate::graph::definition::DefinitionKind;
pub trait Authorizer: Send + Sync {
fn authorize(&self, request: &AccessRequest<'_>) -> Result<(), Denied>;
fn resolve_file(&self, path: &Path) -> Result<PathBuf, Denied> {
Ok(path.to_path_buf())
}
}
impl<T: Authorizer + ?Sized> Authorizer for Arc<T> {
fn authorize(&self, request: &AccessRequest<'_>) -> Result<(), Denied> {
(**self).authorize(request)
}
fn resolve_file(&self, path: &Path) -> Result<PathBuf, Denied> {
(**self).resolve_file(path)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct AllowAll;
impl Authorizer for AllowAll {
fn authorize(&self, _request: &AccessRequest<'_>) -> Result<(), Denied> {
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Denied {
reason: String,
}
impl Denied {
pub fn new(reason: impl Into<String>) -> Self {
Self {
reason: reason.into(),
}
}
pub fn reason(&self) -> &str {
&self.reason
}
}
impl fmt::Display for Denied {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.reason)
}
}
impl std::error::Error for Denied {}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AccessRequest<'a> {
pub action: Action<'a>,
pub resource: Resource<'a>,
}
impl<'a> AccessRequest<'a> {
pub fn new(action: Action<'a>, resource: Resource<'a>) -> Self {
Self { action, resource }
}
pub fn writes_graph(&self) -> bool {
matches!(
self.action,
Action::Create | Action::Update | Action::Delete
) && matches!(
self.resource,
Resource::Data { .. } | Resource::Schema | Resource::Definition { .. }
)
}
pub fn to_owned_request(&self) -> AccessRequestBuf {
AccessRequestBuf::from(self)
}
}
impl fmt::Display for AccessRequest<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {}", self.action, self.resource)
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Action<'a> {
Read,
Create,
Update,
Delete,
Execute,
Custom(&'a str),
}
impl Action<'_> {
pub fn kind(&self) -> ActionKind {
match self {
Action::Read => ActionKind::Read,
Action::Create => ActionKind::Create,
Action::Update => ActionKind::Update,
Action::Delete => ActionKind::Delete,
Action::Execute => ActionKind::Execute,
Action::Custom(_) => ActionKind::Custom,
}
}
}
impl fmt::Display for Action<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Action::Custom(name) => f.write_str(name),
other => f.write_str(other.kind().name()),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Resource<'a> {
Data {
element: Option<ElementKind>,
label: Option<&'a str>,
},
Schema,
Definition {
kind: Option<DefinitionKind>,
name: Option<&'a str>,
},
File {
path: &'a Path,
},
Option {
key: &'a str,
},
Custom {
kind: &'a str,
name: Option<&'a str>,
},
}
impl<'a> Resource<'a> {
pub fn data(element: Option<ElementKind>, label: Option<&'a str>) -> Self {
Resource::Data { element, label }
}
pub fn definition(kind: Option<DefinitionKind>, name: Option<&'a str>) -> Self {
Resource::Definition { kind, name }
}
pub fn kind(&self) -> ResourceKind {
match self {
Resource::Data { .. } => ResourceKind::Data,
Resource::Schema => ResourceKind::Schema,
Resource::Definition { .. } => ResourceKind::Definition,
Resource::File { .. } => ResourceKind::File,
Resource::Option { .. } => ResourceKind::Option,
Resource::Custom { .. } => ResourceKind::Custom,
}
}
}
impl fmt::Display for Resource<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use crate::value::literal::rhai_string;
match self {
Resource::Data { element, label } => {
f.write_str("Data")?;
match (element, label) {
(None, None) => Ok(()),
(Some(element), None) => write!(f, "({element})"),
(None, Some(label)) => write!(f, "({})", rhai_string(label)),
(Some(element), Some(label)) => {
write!(f, "({element} {})", rhai_string(label))
}
}
}
Resource::Schema => f.write_str("Schema"),
Resource::Definition { kind, name } => {
f.write_str("Definition")?;
match (kind, name) {
(None, None) => Ok(()),
(Some(kind), None) => write!(f, "({kind})"),
(None, Some(name)) => write!(f, "({})", rhai_string(name)),
(Some(kind), Some(name)) => write!(f, "({kind} {})", rhai_string(name)),
}
}
Resource::File { path } => {
write!(f, "File({})", rhai_string(&path.to_string_lossy()))
}
Resource::Option { key } => write!(f, "Option({})", rhai_string(key)),
Resource::Custom { kind, name } => match name {
Some(name) => write!(f, "{kind}({})", rhai_string(name)),
None => f.write_str(kind),
},
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ActionKind {
Read,
Create,
Update,
Delete,
Execute,
Custom,
}
impl ActionKind {
pub const ALL: [ActionKind; 6] = [
ActionKind::Read,
ActionKind::Create,
ActionKind::Update,
ActionKind::Delete,
ActionKind::Execute,
ActionKind::Custom,
];
pub fn name(self) -> &'static str {
match self {
ActionKind::Read => "Read",
ActionKind::Create => "Create",
ActionKind::Update => "Update",
ActionKind::Delete => "Delete",
ActionKind::Execute => "Execute",
ActionKind::Custom => "Custom",
}
}
fn index(self) -> u32 {
match self {
ActionKind::Read => 0,
ActionKind::Create => 1,
ActionKind::Update => 2,
ActionKind::Delete => 3,
ActionKind::Execute => 4,
ActionKind::Custom => 5,
}
}
}
impl fmt::Display for ActionKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ResourceKind {
Data,
Schema,
File,
Option,
Definition,
Custom,
}
impl ResourceKind {
pub const ALL: [ResourceKind; 6] = [
ResourceKind::Data,
ResourceKind::Schema,
ResourceKind::File,
ResourceKind::Option,
ResourceKind::Definition,
ResourceKind::Custom,
];
pub fn name(self) -> &'static str {
match self {
ResourceKind::Data => "Data",
ResourceKind::Schema => "Schema",
ResourceKind::File => "File",
ResourceKind::Option => "Option",
ResourceKind::Definition => "Definition",
ResourceKind::Custom => "Custom",
}
}
fn index(self) -> u32 {
match self {
ResourceKind::Data => 0,
ResourceKind::Schema => 1,
ResourceKind::File => 2,
ResourceKind::Option => 3,
ResourceKind::Definition => 5,
ResourceKind::Custom => 4,
}
}
}
impl fmt::Display for ResourceKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct AccessRequestBuf {
action: ActionKind,
custom_action: String,
resource: ResourceBuf,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum ResourceBuf {
Data {
element: Option<ElementKind>,
label: Option<String>,
},
Schema,
Definition {
kind: Option<DefinitionKind>,
name: Option<String>,
},
File(PathBuf),
Option(String),
Custom {
kind: String,
name: Option<String>,
},
}
impl AccessRequestBuf {
pub fn as_request(&self) -> AccessRequest<'_> {
let action = match self.action {
ActionKind::Read => Action::Read,
ActionKind::Create => Action::Create,
ActionKind::Update => Action::Update,
ActionKind::Delete => Action::Delete,
ActionKind::Execute => Action::Execute,
ActionKind::Custom => Action::Custom(&self.custom_action),
};
let resource = match &self.resource {
ResourceBuf::Data { element, label } => Resource::Data {
element: *element,
label: label.as_deref(),
},
ResourceBuf::Schema => Resource::Schema,
ResourceBuf::Definition { kind, name } => Resource::Definition {
kind: *kind,
name: name.as_deref(),
},
ResourceBuf::File(path) => Resource::File { path },
ResourceBuf::Option(key) => Resource::Option { key },
ResourceBuf::Custom { kind, name } => Resource::Custom {
kind,
name: name.as_deref(),
},
};
AccessRequest { action, resource }
}
}
impl From<&AccessRequest<'_>> for AccessRequestBuf {
fn from(request: &AccessRequest<'_>) -> Self {
let custom_action = match request.action {
Action::Custom(name) => name.to_string(),
_ => String::new(),
};
let resource = match request.resource {
Resource::Data { element, label } => ResourceBuf::Data {
element,
label: label.map(str::to_string),
},
Resource::Schema => ResourceBuf::Schema,
Resource::Definition { kind, name } => ResourceBuf::Definition {
kind,
name: name.map(str::to_string),
},
Resource::File { path } => ResourceBuf::File(path.to_path_buf()),
Resource::Option { key } => ResourceBuf::Option(key.to_string()),
Resource::Custom { kind, name } => ResourceBuf::Custom {
kind: kind.to_string(),
name: name.map(str::to_string),
},
};
Self {
action: request.action.kind(),
custom_action,
resource,
}
}
}
impl fmt::Display for AccessRequestBuf {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.as_request().fmt(f)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccessPolicy {
allowed: u64,
file_root: Option<Arc<Path>>,
}
impl Default for AccessPolicy {
fn default() -> Self {
Self::allow_all()
}
}
const RESOURCES: u32 = ResourceKind::ALL.len() as u32;
fn bit(action: ActionKind, resource: ResourceKind) -> u64 {
1u64 << (action.index() * RESOURCES + resource.index())
}
const _: () = assert!(ActionKind::ALL.len() * ResourceKind::ALL.len() <= u64::BITS as usize);
impl AccessPolicy {
pub fn allow_all() -> Self {
let allowed = ActionKind::ALL.iter().fold(0, |bits, action| {
ResourceKind::ALL
.iter()
.fold(bits, |bits, resource| bits | bit(*action, *resource))
});
Self {
allowed,
file_root: None,
}
}
pub fn deny_all() -> Self {
Self {
allowed: 0,
file_root: None,
}
}
pub fn read_only() -> Self {
Self::deny_all()
.allow(ActionKind::Read, ResourceKind::Data)
.allow(ActionKind::Read, ResourceKind::Schema)
.allow(ActionKind::Read, ResourceKind::Definition)
.allow(ActionKind::Execute, ResourceKind::Definition)
}
pub fn allow(mut self, action: ActionKind, resource: ResourceKind) -> Self {
self.allowed |= bit(action, resource);
self
}
pub fn deny(mut self, action: ActionKind, resource: ResourceKind) -> Self {
self.allowed &= !bit(action, resource);
self
}
pub fn allow_resource(self, resource: ResourceKind) -> Self {
ActionKind::ALL
.iter()
.fold(self, |policy, action| policy.allow(*action, resource))
}
pub fn deny_resource(self, resource: ResourceKind) -> Self {
ActionKind::ALL
.iter()
.fold(self, |policy, action| policy.deny(*action, resource))
}
pub fn allows(&self, action: ActionKind, resource: ResourceKind) -> bool {
self.allowed & bit(action, resource) != 0
}
pub fn with_file_root(mut self, root: impl Into<PathBuf>) -> Self {
self.file_root = Some(Arc::from(root.into().into_boxed_path()));
self
}
pub fn file_root(&self) -> Option<&Path> {
self.file_root.as_deref()
}
}
impl Authorizer for AccessPolicy {
fn authorize(&self, request: &AccessRequest<'_>) -> Result<(), Denied> {
let (action, resource) = (request.action.kind(), request.resource.kind());
if self.allows(action, resource) {
Ok(())
} else {
Err(Denied::new(format!(
"the host's access policy does not allow {action} on {resource}"
)))
}
}
fn resolve_file(&self, path: &Path) -> Result<PathBuf, Denied> {
let Some(root) = self.file_root.as_deref() else {
return Ok(path.to_path_buf());
};
let root = canonical(root).map_err(|err| {
Denied::new(format!(
"the file root '{}' cannot be resolved: {err}",
root.display()
))
})?;
let candidate = normalize(&root.join(path));
let resolved = canonicalize_existing_prefix(&candidate);
if resolved.starts_with(&root) {
Ok(resolved)
} else {
Err(Denied::new(format!(
"'{}' is outside the file root '{}'",
path.display(),
root.display()
)))
}
}
}
fn canonical(path: &Path) -> std::io::Result<PathBuf> {
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
{
let _ = path;
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"no file system on this target",
))
}
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
{
std::fs::canonicalize(path)
}
}
fn normalize(path: &Path) -> PathBuf {
let mut normalized = PathBuf::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
normalized.pop();
}
other => normalized.push(other.as_os_str()),
}
}
normalized
}
fn canonicalize_existing_prefix(path: &Path) -> PathBuf {
let mut existing = path.to_path_buf();
let mut rest = Vec::new();
loop {
if let Ok(canonical) = canonical(&existing) {
let mut resolved = canonical;
for part in rest.iter().rev() {
resolved.push(part);
}
return resolved;
}
match (
existing.file_name().map(|n| n.to_os_string()),
existing.parent(),
) {
(Some(name), Some(parent)) => {
rest.push(name);
existing = parent.to_path_buf();
}
_ => return path.to_path_buf(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deny_all_allows_nothing_and_allow_all_everything() {
for action in ActionKind::ALL {
for resource in ResourceKind::ALL {
assert!(!AccessPolicy::deny_all().allows(action, resource));
assert!(AccessPolicy::allow_all().allows(action, resource));
}
}
assert_eq!(AccessPolicy::default(), AccessPolicy::allow_all());
}
#[test]
fn allow_and_deny_toggle_one_pair() {
let policy = AccessPolicy::read_only().allow(ActionKind::Create, ResourceKind::Data);
assert!(policy.allows(ActionKind::Create, ResourceKind::Data));
assert!(!policy.allows(ActionKind::Update, ResourceKind::Option));
let policy = policy.deny(ActionKind::Read, ResourceKind::Data);
assert!(!policy.allows(ActionKind::Read, ResourceKind::Data));
assert!(policy.allows(ActionKind::Read, ResourceKind::Schema));
let policy = AccessPolicy::allow_all().deny_resource(ResourceKind::File);
assert!(!policy.allows(ActionKind::Execute, ResourceKind::File));
assert!(policy.allows(ActionKind::Execute, ResourceKind::Custom));
}
#[test]
fn normalize_applies_parent_components_lexically() {
assert_eq!(
normalize(Path::new("/a/b/../c/./d")),
PathBuf::from("/a/c/d")
);
assert_eq!(normalize(Path::new("/a/../../b")), PathBuf::from("/b"));
}
#[test]
fn without_root_a_path_passes_unchanged() {
let path = AccessPolicy::allow_all()
.resolve_file(Path::new("../x.xml"))
.unwrap();
assert_eq!(path, PathBuf::from("../x.xml"));
}
#[test]
fn an_owned_request_round_trips_and_renders() {
let path = Path::new("a.xml");
for request in [
AccessRequest::new(
Action::Create,
Resource::data(Some(ElementKind::Vertex), Some("p")),
),
AccessRequest::new(Action::Read, Resource::data(None, None)),
AccessRequest::new(Action::Update, Resource::Schema),
AccessRequest::new(Action::Execute, Resource::File { path }),
AccessRequest::new(
Action::Update,
Resource::Option {
key: "evaluationTimeout",
},
),
AccessRequest::new(
Action::Custom("open"),
Resource::Custom {
kind: "database",
name: Some("sales"),
},
),
] {
assert_eq!(request.to_owned_request().as_request(), request);
}
assert_eq!(
AccessRequest::new(
Action::Create,
Resource::data(Some(ElementKind::Vertex), Some("p"))
)
.to_string(),
"Create Data(vertex \"p\")"
);
assert_eq!(
AccessRequest::new(
Action::Custom("open"),
Resource::Custom {
kind: "database",
name: Some("sales")
}
)
.to_string(),
"open database(\"sales\")"
);
}
}