use crate::context_expr::ContextExpr;
use crate::error::{PolicyError, Result};
use crate::path::PathPattern;
use crate::{MAX_POLICY_NAME_LENGTH, MAX_RULES_PER_POLICY};
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Action {
Read,
Write,
Execute,
Delete,
All,
Custom(String),
}
impl Action {
#[must_use]
pub fn matches(&self, other: &Self) -> bool {
match (self, other) {
(Self::All, _) | (_, Self::All) => true,
(a, b) => a == b,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Resource {
File(String),
Usb(String),
Tunnel(String),
All,
Custom {
resource_type: String,
path: String,
},
}
impl Resource {
#[must_use]
pub fn matches(&self, other: &Self) -> bool {
match (self, other) {
(Self::All, _) | (_, Self::All) => true,
(Self::File(pattern), Self::File(path)) => {
PathPattern::new_unchecked(pattern).matches(path)
}
(Self::Usb(pattern), Self::Usb(device)) => {
PathPattern::new_unchecked(pattern).matches(device)
}
(Self::Tunnel(pattern), Self::Tunnel(name)) => {
PathPattern::new_unchecked(pattern).matches(name)
}
(
Self::Custom {
resource_type: t1,
path: p1,
},
Self::Custom {
resource_type: t2,
path: p2,
},
) => t1 == t2 && PathPattern::new_unchecked(p1).matches(p2),
_ => false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyRule {
pub peer_id: String,
pub action: Action,
pub resource: Resource,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at: Option<u64>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub attributes: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context_expr: Option<ContextExpr>,
}
impl PolicyRule {
#[must_use]
pub fn new(peer_id: String, action: Action, resource: Resource) -> Self {
Self {
peer_id,
action,
resource,
expires_at: None,
attributes: BTreeMap::new(),
context_expr: None,
}
}
#[must_use]
pub fn with_expiration(
peer_id: String,
action: Action,
resource: Resource,
expires_at: u64,
) -> Self {
Self {
peer_id,
action,
resource,
expires_at: Some(expires_at),
attributes: BTreeMap::new(),
context_expr: None,
}
}
#[must_use]
pub const fn with_attributes(
peer_id: String,
action: Action,
resource: Resource,
attributes: BTreeMap<String, String>,
) -> Self {
Self {
peer_id,
action,
resource,
expires_at: None,
attributes,
context_expr: None,
}
}
#[must_use]
pub const fn expires_at(mut self, timestamp: u64) -> Self {
self.expires_at = Some(timestamp);
self
}
#[must_use]
pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.attributes.insert(key.into(), value.into());
self
}
#[must_use]
pub fn with_context_expr(mut self, expr: ContextExpr) -> Self {
self.context_expr = Some(expr);
self
}
#[must_use]
pub fn is_expired(&self, current_time: u64) -> bool {
self.expires_at.is_some_and(|exp| current_time >= exp)
}
#[must_use]
pub fn matches_context(&self, context: &BTreeMap<String, String>) -> bool {
let attributes_match = if self.attributes.is_empty() {
true } else {
self.attributes
.iter()
.all(|(key, value)| context.get(key) == Some(value))
};
let expr_match = match &self.context_expr {
None => true, Some(expr) => {
expr.evaluate(context, 0).unwrap_or(false)
}
};
attributes_match && expr_match
}
#[must_use]
pub fn allows(&self, peer_id: &str, action: &Action, resource: &Resource) -> bool {
self.peer_id == peer_id && self.action.matches(action) && self.resource.matches(resource)
}
#[must_use]
pub fn allows_with_context(
&self,
peer_id: &str,
action: &Action,
resource: &Resource,
current_time: u64,
context: &BTreeMap<String, String>,
) -> bool {
if !self.allows(peer_id, action, resource) {
return false;
}
if self.is_expired(current_time) {
return false;
}
if !self.matches_context(context) {
return false;
}
true
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(try_from = "PolicyRaw")]
pub struct Policy {
name: String,
version: u64,
issued_at: u64,
valid_until: u64,
rules: Vec<PolicyRule>,
metadata: BTreeMap<String, String>,
}
fn default_version() -> u64 {
1
}
#[derive(Debug, Clone, Deserialize)]
struct PolicyRaw {
name: String,
#[serde(default = "default_version")]
version: u64,
#[serde(default)]
issued_at: u64,
#[serde(default)]
valid_until: u64,
rules: Vec<PolicyRule>,
#[serde(default)]
metadata: BTreeMap<String, String>,
}
impl TryFrom<PolicyRaw> for Policy {
type Error = PolicyError;
fn try_from(raw: PolicyRaw) -> Result<Self> {
if raw.name.len() > MAX_POLICY_NAME_LENGTH {
return Err(PolicyError::NameTooLong {
max: MAX_POLICY_NAME_LENGTH,
length: raw.name.len(),
});
}
if raw.rules.len() > MAX_RULES_PER_POLICY {
return Err(PolicyError::TooManyRules {
max: MAX_RULES_PER_POLICY,
attempted: raw.rules.len(),
});
}
let policy = Policy {
name: raw.name,
version: raw.version,
issued_at: raw.issued_at,
valid_until: raw.valid_until,
rules: raw.rules,
metadata: raw.metadata,
};
policy.validate()?;
Ok(policy)
}
}
impl Policy {
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub const fn version(&self) -> u64 {
self.version
}
#[must_use]
pub const fn issued_at(&self) -> u64 {
self.issued_at
}
#[must_use]
pub const fn valid_until(&self) -> u64 {
self.valid_until
}
#[must_use]
pub fn rules(&self) -> &[PolicyRule] {
&self.rules
}
#[must_use]
pub fn metadata(&self) -> &BTreeMap<String, String> {
&self.metadata
}
pub fn new(
name: impl Into<String>,
valid_duration_secs: u64,
current_time: u64,
) -> Result<Self> {
let name = name.into();
if name.len() > MAX_POLICY_NAME_LENGTH {
return Err(PolicyError::NameTooLong {
max: MAX_POLICY_NAME_LENGTH,
length: name.len(),
});
}
Ok(Self {
name,
version: 1,
issued_at: current_time,
valid_until: current_time + valid_duration_secs,
rules: Vec::new(),
metadata: BTreeMap::new(),
})
}
pub fn new_unversioned(name: impl Into<String>) -> Result<Self> {
let name = name.into();
if name.len() > MAX_POLICY_NAME_LENGTH {
return Err(PolicyError::NameTooLong {
max: MAX_POLICY_NAME_LENGTH,
length: name.len(),
});
}
Ok(Self {
name,
version: 1,
issued_at: 0,
valid_until: 2_000_000_000, rules: Vec::new(),
metadata: BTreeMap::new(),
})
}
pub fn add_rule(mut self, rule: PolicyRule) -> Result<Self> {
if self.rules.len() >= MAX_RULES_PER_POLICY {
return Err(PolicyError::TooManyRules {
max: MAX_RULES_PER_POLICY,
attempted: self.rules.len() + 1,
});
}
self.rules.push(rule);
Ok(self)
}
#[must_use]
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
#[must_use]
pub fn is_allowed(&self, peer_id: &str, action: &Action, resource: &Resource) -> bool {
crate::authorizer::PolicyAuthorizer::new(&self.rules).is_allowed(peer_id, action, resource)
}
pub fn validate(&self) -> Result<()> {
if self.name.is_empty() {
return Err(PolicyError::InvalidRule(
"Policy name cannot be empty".to_string(),
));
}
if self.rules.is_empty() {
return Err(PolicyError::InvalidRule(
"Policy must have at least one rule".to_string(),
));
}
for rule in &self.rules {
if rule.peer_id.is_empty() {
return Err(PolicyError::InvalidRule(
"Peer ID cannot be empty".to_string(),
));
}
}
Ok(())
}
#[cfg(feature = "toml")]
pub fn from_toml(toml_str: &str) -> Result<Self> {
let policy: Self = toml::from_str(toml_str)?;
policy.validate()?;
Ok(policy)
}
#[cfg(feature = "toml")]
pub fn to_toml(&self) -> Result<String> {
toml::to_string(self).map_err(|e| PolicyError::SerializationError(e.to_string()))
}
}