use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fmt;
use std::hash::{Hash, Hasher};
pub const WILDCARD: &str = "*";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Permission {
resource: String,
action: String,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
}
impl Permission {
pub fn new(resource: impl Into<String>, action: impl Into<String>) -> Self {
Self {
resource: resource.into(),
action: action.into(),
description: None,
}
}
pub fn with_description(
resource: impl Into<String>,
action: impl Into<String>,
description: impl Into<String>,
) -> Self {
Self {
resource: resource.into(),
action: action.into(),
description: Some(description.into()),
}
}
pub fn wildcard() -> Self {
Self {
resource: WILDCARD.to_string(),
action: WILDCARD.to_string(),
description: Some("Full access to all resources".to_string()),
}
}
pub fn resource_wildcard(resource: impl Into<String>) -> Self {
Self {
resource: resource.into(),
action: WILDCARD.to_string(),
description: None,
}
}
pub fn action_wildcard(action: impl Into<String>) -> Self {
Self {
resource: WILDCARD.to_string(),
action: action.into(),
description: None,
}
}
pub fn parse(s: &str) -> Option<Self> {
let parts: Vec<&str> = s.splitn(2, ':').collect();
if parts.len() == 2 {
Some(Self::new(parts[0], parts[1]))
} else {
None
}
}
pub fn resource(&self) -> &str {
&self.resource
}
pub fn action(&self) -> &str {
&self.action
}
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
pub fn set_description(&mut self, description: impl Into<String>) {
self.description = Some(description.into());
}
pub fn is_wildcard(&self) -> bool {
self.resource == WILDCARD && self.action == WILDCARD
}
pub fn is_resource_wildcard(&self) -> bool {
self.action == WILDCARD && self.resource != WILDCARD
}
pub fn is_action_wildcard(&self) -> bool {
self.resource == WILDCARD && self.action != WILDCARD
}
pub fn has_wildcard(&self) -> bool {
self.resource == WILDCARD || self.action == WILDCARD
}
pub fn matches(&self, other: &Permission) -> bool {
let resource_matches = self.resource == WILDCARD || self.resource == other.resource;
let action_matches = self.action == WILDCARD || self.action == other.action;
resource_matches && action_matches
}
pub fn to_string_format(&self) -> String {
format!("{}:{}", self.resource, self.action)
}
}
impl PartialEq for Permission {
fn eq(&self, other: &Self) -> bool {
self.resource == other.resource && self.action == other.action
}
}
impl Eq for Permission {}
impl Hash for Permission {
fn hash<H: Hasher>(&self, state: &mut H) {
self.resource.hash(state);
self.action.hash(state);
}
}
impl fmt::Display for Permission {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.resource, self.action)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Resource {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
pub attributes: std::collections::HashMap<String, String>,
}
impl Resource {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
id: None,
attributes: std::collections::HashMap::new(),
}
}
pub fn with_id(name: impl Into<String>, id: impl Into<String>) -> Self {
Self {
name: name.into(),
id: Some(id.into()),
attributes: std::collections::HashMap::new(),
}
}
pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.attributes.insert(key.into(), value.into());
self
}
pub fn name(&self) -> &str {
&self.name
}
pub fn id(&self) -> Option<&str> {
self.id.as_deref()
}
pub fn get_attribute(&self, key: &str) -> Option<&str> {
self.attributes.get(key).map(|s| s.as_str())
}
pub fn matches_name(&self, name: &str) -> bool {
self.name == name || name == WILDCARD
}
}
impl fmt::Display for Resource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.id {
Some(id) => write!(f, "{}:{}", self.name, id),
None => write!(f, "{}", self.name),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Action {
pub name: String,
}
impl Action {
pub fn new(name: impl Into<String>) -> Self {
Self { name: name.into() }
}
pub fn name(&self) -> &str {
&self.name
}
pub fn matches(&self, name: &str) -> bool {
self.name == name || name == WILDCARD || self.name == WILDCARD
}
pub fn read() -> Self {
Self::new("read")
}
pub fn write() -> Self {
Self::new("write")
}
pub fn update() -> Self {
Self::new("update")
}
pub fn delete() -> Self {
Self::new("delete")
}
pub fn list() -> Self {
Self::new("list")
}
pub fn manage() -> Self {
Self::new("manage")
}
}
impl fmt::Display for Action {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PermissionSet {
permissions: HashSet<Permission>,
}
impl PermissionSet {
pub fn new() -> Self {
Self {
permissions: HashSet::new(),
}
}
pub fn from_permissions(permissions: impl IntoIterator<Item = Permission>) -> Self {
Self {
permissions: permissions.into_iter().collect(),
}
}
pub fn add(&mut self, permission: Permission) -> bool {
self.permissions.insert(permission)
}
pub fn remove(&mut self, permission: &Permission) -> bool {
self.permissions.remove(permission)
}
pub fn contains(&self, permission: &Permission) -> bool {
if self.permissions.contains(permission) {
return true;
}
for p in &self.permissions {
if p.matches(permission) {
return true;
}
}
false
}
pub fn contains_all(&self, permissions: &[Permission]) -> bool {
permissions.iter().all(|p| self.contains(p))
}
pub fn contains_any(&self, permissions: &[Permission]) -> bool {
permissions.iter().any(|p| self.contains(p))
}
pub fn len(&self) -> usize {
self.permissions.len()
}
pub fn is_empty(&self) -> bool {
self.permissions.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &Permission> {
self.permissions.iter()
}
pub fn merge(&mut self, other: &PermissionSet) {
for p in &other.permissions {
self.permissions.insert(p.clone());
}
}
pub fn to_string_list(&self) -> Vec<String> {
self.permissions.iter().map(|p| p.to_string()).collect()
}
pub fn clear(&mut self) {
self.permissions.clear();
}
}
impl IntoIterator for PermissionSet {
type Item = Permission;
type IntoIter = std::collections::hash_set::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::collections::hash_set::Iter<'a, Permission>;
fn into_iter(self) -> Self::IntoIter {
self.permissions.iter()
}
}
impl FromIterator<Permission> for PermissionSet {
fn from_iter<T: IntoIterator<Item = Permission>>(iter: T) -> Self {
Self {
permissions: iter.into_iter().collect(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_permission_new() {
let perm = Permission::new("posts", "read");
assert_eq!(perm.resource(), "posts");
assert_eq!(perm.action(), "read");
assert!(!perm.has_wildcard());
}
#[test]
fn test_permission_parse() {
let perm = Permission::parse("users:delete").unwrap();
assert_eq!(perm.resource(), "users");
assert_eq!(perm.action(), "delete");
assert!(Permission::parse("invalid").is_none());
}
#[test]
fn test_permission_wildcard() {
let wildcard = Permission::wildcard();
assert!(wildcard.is_wildcard());
assert!(wildcard.has_wildcard());
let resource_wild = Permission::resource_wildcard("posts");
assert!(resource_wild.is_resource_wildcard());
assert!(!resource_wild.is_wildcard());
let action_wild = Permission::action_wildcard("read");
assert!(action_wild.is_action_wildcard());
assert!(!action_wild.is_wildcard());
}
#[test]
fn test_permission_matches() {
let all = Permission::wildcard();
let posts_all = Permission::resource_wildcard("posts");
let read_posts = Permission::new("posts", "read");
let delete_posts = Permission::new("posts", "delete");
let read_users = Permission::new("users", "read");
assert!(all.matches(&read_posts));
assert!(all.matches(&delete_posts));
assert!(all.matches(&read_users));
assert!(posts_all.matches(&read_posts));
assert!(posts_all.matches(&delete_posts));
assert!(!posts_all.matches(&read_users));
assert!(read_posts.matches(&read_posts));
assert!(!read_posts.matches(&delete_posts));
}
#[test]
fn test_permission_equality() {
let p1 = Permission::new("posts", "read");
let p2 = Permission::new("posts", "read");
let p3 = Permission::new("posts", "write");
assert_eq!(p1, p2);
assert_ne!(p1, p3);
}
#[test]
fn test_permission_display() {
let perm = Permission::new("posts", "read");
assert_eq!(format!("{}", perm), "posts:read");
}
#[test]
fn test_resource() {
let resource = Resource::new("posts");
assert_eq!(resource.name(), "posts");
assert!(resource.id().is_none());
let resource_with_id = Resource::with_id("posts", "123");
assert_eq!(resource_with_id.name(), "posts");
assert_eq!(resource_with_id.id(), Some("123"));
let resource_with_attr = Resource::new("posts").with_attribute("owner", "user1");
assert_eq!(resource_with_attr.get_attribute("owner"), Some("user1"));
}
#[test]
fn test_action() {
let action = Action::new("read");
assert_eq!(action.name(), "read");
assert!(action.matches("read"));
assert!(action.matches("*"));
assert!(!action.matches("write"));
assert_eq!(Action::read().name(), "read");
assert_eq!(Action::write().name(), "write");
assert_eq!(Action::delete().name(), "delete");
}
#[test]
fn test_permission_set() {
let mut set = PermissionSet::new();
let read_posts = Permission::new("posts", "read");
let write_posts = Permission::new("posts", "write");
let delete_posts = Permission::new("posts", "delete");
set.add(read_posts.clone());
set.add(write_posts.clone());
assert!(set.contains(&read_posts));
assert!(set.contains(&write_posts));
assert!(!set.contains(&delete_posts));
assert_eq!(set.len(), 2);
}
#[test]
fn test_permission_set_wildcard() {
let mut set = PermissionSet::new();
set.add(Permission::resource_wildcard("posts"));
assert!(set.contains(&Permission::new("posts", "read")));
assert!(set.contains(&Permission::new("posts", "write")));
assert!(set.contains(&Permission::new("posts", "delete")));
assert!(!set.contains(&Permission::new("users", "read")));
}
#[test]
fn test_permission_set_contains_all_any() {
let mut set = PermissionSet::new();
set.add(Permission::new("posts", "read"));
set.add(Permission::new("posts", "write"));
let check = vec![
Permission::new("posts", "read"),
Permission::new("posts", "write"),
];
assert!(set.contains_all(&check));
let check_fail = vec![
Permission::new("posts", "read"),
Permission::new("posts", "delete"),
];
assert!(!set.contains_all(&check_fail));
assert!(set.contains_any(&check_fail));
}
#[test]
fn test_permission_set_merge() {
let mut set1 = PermissionSet::new();
set1.add(Permission::new("posts", "read"));
let mut set2 = PermissionSet::new();
set2.add(Permission::new("posts", "write"));
set2.add(Permission::new("users", "read"));
set1.merge(&set2);
assert_eq!(set1.len(), 3);
assert!(set1.contains(&Permission::new("posts", "read")));
assert!(set1.contains(&Permission::new("posts", "write")));
assert!(set1.contains(&Permission::new("users", "read")));
}
}