use crate::error::{Error, Result};
use std::collections::HashMap;
use std::sync::Arc;
#[cfg_attr(feature = "persistence", derive(serde::Serialize, serde::Deserialize))]
pub struct Permission {
action: String,
resource_type: String,
instance: Option<String>,
#[cfg_attr(feature = "persistence", serde(skip))]
condition: Option<PermissionCondition>,
}
pub type PermissionCondition = Arc<dyn Fn(&HashMap<String, String>) -> bool + Send + Sync>;
impl Permission {
pub fn new(action: impl Into<String>, resource_type: impl Into<String>) -> Self {
let action = action.into();
let resource_type = resource_type.into();
Self::validate_permission_field(&action, "action").expect("Invalid action in permission");
Self::validate_permission_field(&resource_type, "resource_type")
.expect("Invalid resource_type in permission");
Self {
action,
resource_type,
instance: None,
condition: None,
}
}
pub fn try_new(action: impl Into<String>, resource_type: impl Into<String>) -> Result<Self> {
let action = action.into();
let resource_type = resource_type.into();
Self::validate_permission_field(&action, "action")?;
Self::validate_permission_field(&resource_type, "resource_type")?;
Ok(Self {
action,
resource_type,
instance: None,
condition: None,
})
}
pub fn with_instance(
action: impl Into<String>,
resource_type: impl Into<String>,
instance: impl Into<String>,
) -> Self {
let mut permission = Self::new(action, resource_type);
let instance = instance.into();
Self::validate_permission_field(&instance, "instance")
.expect("Invalid instance in permission");
permission.instance = Some(instance.to_owned());
permission
}
pub fn with_condition<F>(
action: impl Into<String>,
resource_type: impl Into<String>,
condition: F,
) -> Self
where
F: Fn(&HashMap<String, String>) -> bool + Send + Sync + 'static,
{
let mut permission = Self::new(action, resource_type);
permission.condition = Some(Arc::new(condition));
permission
}
pub fn with_instance_and_condition<F>(
action: impl Into<String>,
resource_type: impl Into<String>,
instance: impl Into<String>,
condition: F,
) -> Self
where
F: Fn(&HashMap<String, String>) -> bool + Send + Sync + 'static,
{
let mut permission = Self::with_instance(action, resource_type, instance);
permission.condition = Some(Arc::new(condition));
permission
}
pub fn wildcard(resource_type: impl Into<String>) -> Self {
Self::new("*", resource_type)
}
pub fn super_admin() -> Self {
Self::new("*", "*")
}
pub fn with_context(
resource_type: impl Into<String>,
action: impl Into<String>,
context: Option<impl Into<String>>,
) -> Self {
let mut permission = Self::new(action, resource_type);
if let Some(ctx) = context {
permission.instance = Some(ctx.into());
}
permission
}
pub fn with_scope(
resource_type: impl Into<String>,
action: impl Into<String>,
scopes: Vec<impl Into<String>>,
) -> Vec<Self> {
let resource_type = resource_type.into();
let action = action.into();
scopes
.into_iter()
.map(|scope| Self::with_instance(action.clone(), resource_type.clone(), scope.into()))
.collect()
}
pub fn conditional(
resource_type: impl Into<String>,
action: impl Into<String>,
) -> ConditionalPermissionBuilder {
ConditionalPermissionBuilder::new(resource_type, action)
}
pub fn action(&self) -> &str {
&self.action
}
pub fn resource_type(&self) -> &str {
&self.resource_type
}
pub fn instance(&self) -> Option<&str> {
self.instance.as_deref()
}
pub fn matches(&self, action: &str, resource_type: &str) -> bool {
let action_match = self.action == "*" || self.action == action;
let resource_match = self.resource_type == "*" || self.resource_type == resource_type;
action_match && resource_match
}
pub fn matches_with_instance(
&self,
action: &str,
resource_type: &str,
instance: Option<&str>,
) -> bool {
let action_match = self.action == "*" || self.action == action;
let resource_match = self.resource_type == "*" || self.resource_type == resource_type;
let instance_match = match (&self.instance, instance) {
(None, _) => true, (Some(perm_inst), Some(req_inst)) => perm_inst == "*" || perm_inst == req_inst,
(Some(_), None) => false, };
action_match && resource_match && instance_match
}
pub fn implies(&self, other: &Permission) -> bool {
let action_implies = self.action == "*" || self.action == other.action;
let resource_implies =
self.resource_type == "*" || self.resource_type == other.resource_type;
let instance_implies = match (&self.instance, &other.instance) {
(None, _) => true, (Some(_), None) => false, (Some(self_inst), Some(other_inst)) => self_inst == "*" || self_inst == other_inst,
};
action_implies && resource_implies && instance_implies
}
pub fn is_granted(
&self,
action: &str,
resource_type: &str,
context: &HashMap<String, String>,
) -> bool {
if !self.matches(action, resource_type) {
return false;
}
if let Some(condition) = &self.condition {
condition(context)
} else {
true
}
}
pub fn parse(permission_str: &str) -> Result<Self> {
let parts: Vec<&str> = permission_str.split(':').collect();
match parts.len() {
2 => {
let action = parts[0].trim();
let resource_type = parts[1].trim();
Self::validate_permission_field(action, "action")?;
Self::validate_permission_field(resource_type, "resource_type")?;
Ok(Self::new(action, resource_type))
}
3 => {
let action = parts[0].trim();
let resource_type = parts[1].trim();
let instance = parts[2].trim();
Self::validate_permission_field(action, "action")?;
Self::validate_permission_field(resource_type, "resource_type")?;
Self::validate_permission_field(instance, "instance")?;
Ok(Self::with_instance(action, resource_type, instance))
}
_ => Err(Error::InvalidPermission(format!(
"Permission must be in format 'action:resource_type' or 'action:resource_type:instance', got: '{permission_str}'"
))),
}
}
fn validate_permission_field(value: &str, field_name: &str) -> Result<()> {
if value.trim().is_empty() {
return Err(Error::ValidationError {
field: field_name.to_string(),
reason: "cannot be empty".to_string(),
invalid_value: Some(value.to_string()),
});
}
if value.len() > 255 {
return Err(Error::ValidationError {
field: field_name.to_string(),
reason: "exceeds maximum length of 255 characters".to_string(),
invalid_value: Some(format!("{}...", &value[..50])),
});
}
if value
.chars()
.any(|c| c.is_control() || "'\";{}[]\\<>".contains(c))
{
return Err(Error::ValidationError {
field: field_name.to_string(),
reason: "contains invalid characters".to_string(),
invalid_value: Some(value.to_string()),
});
}
if value.contains("..") || value.contains('\0') {
return Err(Error::ValidationError {
field: field_name.to_string(),
reason: "contains path traversal sequences".to_string(),
invalid_value: Some(value.to_string()),
});
}
Ok(())
}
}
pub struct ConditionalPermissionBuilder {
resource_type: String,
action: String,
conditions: Vec<PermissionCondition>,
}
impl ConditionalPermissionBuilder {
pub fn new(resource_type: impl Into<String>, action: impl Into<String>) -> Self {
Self {
resource_type: resource_type.into(),
action: action.into(),
conditions: Vec::new(),
}
}
pub fn when<F>(mut self, condition: F) -> Self
where
F: Fn(&HashMap<String, String>) -> bool + Send + Sync + 'static,
{
self.conditions.push(Arc::new(condition));
self
}
pub fn or_when<F>(mut self, condition: F) -> Self
where
F: Fn(&HashMap<String, String>) -> bool + Send + Sync + 'static,
{
self.conditions.push(Arc::new(condition));
self
}
pub fn build(self) -> Permission {
let combined_condition = if self.conditions.is_empty() {
None
} else {
Some(Arc::new(move |context: &HashMap<String, String>| {
self.conditions.iter().any(|condition| condition(context))
}) as PermissionCondition)
};
Permission {
action: self.action,
resource_type: self.resource_type,
instance: None,
condition: combined_condition,
}
}
}
impl std::fmt::Debug for Permission {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Permission")
.field("action", &self.action)
.field("resource_type", &self.resource_type)
.field("instance", &self.instance)
.field("has_condition", &self.condition.is_some())
.finish()
}
}
impl Clone for Permission {
fn clone(&self) -> Self {
Self {
action: self.action.clone(),
resource_type: self.resource_type.clone(),
instance: self.instance.clone(),
condition: self.condition.clone(), }
}
}
impl PartialEq for Permission {
fn eq(&self, other: &Self) -> bool {
self.action == other.action
&& self.resource_type == other.resource_type
&& self.instance == other.instance
}
}
impl Eq for Permission {}
impl std::hash::Hash for Permission {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.action.hash(state);
self.resource_type.hash(state);
self.instance.hash(state);
}
}
impl std::fmt::Display for Permission {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.instance {
Some(instance) => write!(f, "{}:{}:{}", self.action, self.resource_type, instance),
None => write!(f, "{}:{}", self.action, self.resource_type),
}
}
}
impl std::str::FromStr for Permission {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Self::parse(s)
}
}
#[cfg_attr(feature = "persistence", derive(serde::Serialize, serde::Deserialize))]
pub struct PermissionSet {
permissions: Vec<Permission>,
}
impl std::fmt::Debug for PermissionSet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PermissionSet")
.field("permissions", &self.permissions)
.field("count", &self.permissions.len())
.finish()
}
}
impl Clone for PermissionSet {
fn clone(&self) -> Self {
Self {
permissions: self.permissions.clone(),
}
}
}
impl Default for PermissionSet {
fn default() -> Self {
Self::new()
}
}
impl PermissionSet {
pub fn new() -> Self {
Self {
permissions: Vec::new(),
}
}
pub fn add(&mut self, permission: Permission) {
self.permissions.push(permission);
}
pub fn remove(&mut self, permission: &Permission) {
self.permissions.retain(|p| p != permission);
}
pub fn contains(&self, permission: &Permission) -> bool {
self.permissions.contains(permission)
}
pub fn grants(
&self,
action: &str,
resource_type: &str,
context: &HashMap<String, String>,
) -> bool {
self.permissions
.iter()
.any(|p| p.is_granted(action, resource_type, context))
}
pub fn grants_with_instance(
&self,
action: &str,
resource_type: &str,
instance: Option<&str>,
context: &HashMap<String, String>,
) -> bool {
self.permissions.iter().any(|p| {
p.matches_with_instance(action, resource_type, instance)
&& (p.condition.is_none() || p.condition.as_ref().unwrap()(context))
})
}
pub fn implies(&self, permission: &Permission) -> bool {
self.permissions.iter().any(|p| p.implies(permission))
}
pub fn permissions(&self) -> &[Permission] {
&self.permissions
}
pub fn len(&self) -> usize {
self.permissions.len()
}
pub fn is_empty(&self) -> bool {
self.permissions.is_empty()
}
pub fn merge(&mut self, other: PermissionSet) {
for permission in other.permissions {
if !self.contains(&permission) {
self.add(permission);
}
}
}
}
impl From<Vec<Permission>> for PermissionSet {
fn from(permissions: Vec<Permission>) -> Self {
Self { permissions }
}
}
impl From<Permission> for PermissionSet {
fn from(permission: Permission) -> Self {
Self {
permissions: vec![permission],
}
}
}
impl IntoIterator for PermissionSet {
type Item = Permission;
type IntoIter = std::vec::IntoIter<Permission>;
fn into_iter(self) -> Self::IntoIter {
self.permissions.into_iter()
}
}
impl<'a> IntoIterator for &'a PermissionSet {
type Item = &'a Permission;
type IntoIter = std::slice::Iter<'a, Permission>;
fn into_iter(self) -> Self::IntoIter {
self.permissions.iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_permission_creation() {
let permission = Permission::new("read", "documents");
assert_eq!(permission.action(), "read");
assert_eq!(permission.resource_type(), "documents");
assert_eq!(permission.instance(), None);
}
#[test]
fn test_permission_with_instance() {
let permission = Permission::with_instance("read", "documents", "doc123");
assert_eq!(permission.action(), "read");
assert_eq!(permission.resource_type(), "documents");
assert_eq!(permission.instance(), Some("doc123"));
}
#[test]
fn test_permission_matching() {
let permission = Permission::new("read", "documents");
assert!(permission.matches("read", "documents"));
assert!(!permission.matches("write", "documents"));
assert!(!permission.matches("read", "users"));
}
#[test]
fn test_permission_matching_with_instance() {
let permission = Permission::with_instance("read", "documents", "doc123");
assert!(permission.matches_with_instance("read", "documents", Some("doc123")));
assert!(!permission.matches_with_instance("read", "documents", Some("doc456")));
assert!(!permission.matches_with_instance("read", "documents", None));
let general_permission = Permission::new("read", "documents");
assert!(general_permission.matches_with_instance("read", "documents", Some("doc123")));
assert!(general_permission.matches_with_instance("read", "documents", None));
}
#[test]
fn test_permission_implication() {
let general = Permission::new("read", "documents");
let specific = Permission::with_instance("read", "documents", "doc123");
assert!(general.implies(&specific));
assert!(!specific.implies(&general));
assert!(general.implies(&general));
assert!(specific.implies(&specific));
let wildcard_action = Permission::new("*", "documents");
let wildcard_resource = Permission::new("read", "*");
let super_admin = Permission::super_admin();
assert!(wildcard_action.implies(&general));
assert!(wildcard_resource.implies(&general));
assert!(super_admin.implies(&general));
assert!(super_admin.implies(&specific));
}
#[test]
fn test_wildcard_permission() {
let permission = Permission::wildcard("documents");
assert!(permission.matches("read", "documents"));
assert!(permission.matches("write", "documents"));
assert!(!permission.matches("read", "users"));
}
#[test]
fn test_super_admin_permission() {
let permission = Permission::super_admin();
assert!(permission.matches("read", "documents"));
assert!(permission.matches("write", "users"));
assert!(permission.matches("delete", "anything"));
}
#[test]
fn test_permission_parsing() {
let permission = Permission::parse("read:documents").unwrap();
assert_eq!(permission.action(), "read");
assert_eq!(permission.resource_type(), "documents");
assert_eq!(permission.instance(), None);
let permission = Permission::parse("read:documents:doc123").unwrap();
assert_eq!(permission.action(), "read");
assert_eq!(permission.resource_type(), "documents");
assert_eq!(permission.instance(), Some("doc123"));
assert!(Permission::parse("invalid").is_err());
assert!(Permission::parse("read:").is_err());
assert!(Permission::parse(":documents").is_err());
assert!(Permission::parse("read:documents:").is_err());
assert!(Permission::parse("read:documents:instance:extra").is_err());
}
#[test]
fn test_permission_display() {
let permission = Permission::new("read", "documents");
assert_eq!(permission.to_string(), "read:documents");
let permission_with_instance = Permission::with_instance("read", "documents", "doc123");
assert_eq!(
permission_with_instance.to_string(),
"read:documents:doc123"
);
}
#[test]
fn test_permission_set() {
let mut set = PermissionSet::new();
let perm1 = Permission::new("read", "documents");
let perm2 = Permission::new("write", "documents");
set.add(perm1.clone());
set.add(perm2.clone());
assert_eq!(set.len(), 2);
assert!(set.contains(&perm1));
assert!(set.contains(&perm2));
let context = HashMap::new();
assert!(set.grants("read", "documents", &context));
assert!(set.grants("write", "documents", &context));
assert!(!set.grants("delete", "documents", &context));
}
#[test]
fn test_permission_set_with_instances() {
let mut set = PermissionSet::new();
let general_perm = Permission::new("read", "documents");
let specific_perm = Permission::with_instance("write", "documents", "doc123");
set.add(general_perm);
set.add(specific_perm);
let context = HashMap::new();
assert!(set.grants_with_instance("read", "documents", Some("doc123"), &context));
assert!(set.grants_with_instance("read", "documents", Some("doc456"), &context));
assert!(set.grants_with_instance("read", "documents", None, &context));
assert!(set.grants_with_instance("write", "documents", Some("doc123"), &context));
assert!(!set.grants_with_instance("write", "documents", Some("doc456"), &context));
assert!(!set.grants_with_instance("write", "documents", None, &context));
}
#[test]
fn test_permission_set_implication() {
let mut set = PermissionSet::new();
let general_perm = Permission::new("read", "documents");
let admin_perm = Permission::new("admin", "*");
set.add(general_perm);
set.add(admin_perm);
let specific_perm = Permission::with_instance("read", "documents", "doc123");
let admin_users_perm = Permission::new("admin", "users");
assert!(set.implies(&specific_perm));
assert!(set.implies(&admin_users_perm));
}
}