use crate::access_control::{Action, ResourceType};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Permission {
pub id: String,
pub name: String,
pub action: Action,
pub resource_type: ResourceType,
pub resource_pattern: Option<String>,
pub description: Option<String>,
}
impl Permission {
pub fn new(id: String, name: String, action: Action, resource_type: ResourceType) -> Self {
Self {
id,
name,
action,
resource_type,
resource_pattern: None,
description: None,
}
}
pub fn with_pattern(mut self, pattern: String) -> Self {
self.resource_pattern = Some(pattern);
self
}
pub fn with_description(mut self, description: String) -> Self {
self.description = Some(description);
self
}
pub fn matches_resource(&self, resource_id: &str) -> bool {
if let Some(ref pattern) = self.resource_pattern {
glob_match(pattern, resource_id)
} else {
true }
}
}
fn glob_match(pattern: &str, text: &str) -> bool {
let pattern_parts: Vec<&str> = pattern.split('*').collect();
if pattern_parts.len() == 1 {
return pattern == text;
}
let mut text_idx = 0;
for (i, part) in pattern_parts.iter().enumerate() {
if part.is_empty() {
continue;
}
if i == 0 {
if !text.starts_with(part) {
return false;
}
text_idx = part.len();
} else if i == pattern_parts.len() - 1 {
if !text.ends_with(part) {
return false;
}
} else {
if let Some(pos) = text[text_idx..].find(part) {
text_idx += pos + part.len();
} else {
return false;
}
}
}
true
}
pub struct PermissionSet {
permissions: Vec<Permission>,
}
impl PermissionSet {
pub fn new() -> Self {
Self {
permissions: Vec::new(),
}
}
#[allow(clippy::should_implement_trait)]
pub fn add(mut self, permission: Permission) -> Self {
self.permissions.push(permission);
self
}
pub fn build(self) -> Vec<Permission> {
self.permissions
}
pub fn read_only() -> Self {
let mut set = Self::new();
for resource_type in &[
ResourceType::Dataset,
ResourceType::Layer,
ResourceType::Feature,
ResourceType::Raster,
ResourceType::File,
ResourceType::Directory,
] {
set = set.add(Permission::new(
format!("read-{:?}", resource_type).to_lowercase(),
format!("Read {:?}", resource_type),
Action::Read,
*resource_type,
));
}
set
}
pub fn read_write() -> Self {
let mut set = Self::read_only();
for resource_type in &[
ResourceType::Dataset,
ResourceType::Layer,
ResourceType::Feature,
ResourceType::Raster,
ResourceType::File,
ResourceType::Directory,
] {
for action in &[Action::Write, Action::Create, Action::Update] {
set = set.add(Permission::new(
format!("{:?}-{:?}", action, resource_type).to_lowercase(),
format!("{:?} {:?}", action, resource_type),
*action,
*resource_type,
));
}
}
set
}
pub fn admin() -> Self {
let mut set = Self::read_write();
for resource_type in &[
ResourceType::Dataset,
ResourceType::Layer,
ResourceType::Feature,
ResourceType::Raster,
ResourceType::File,
ResourceType::Directory,
ResourceType::Service,
ResourceType::Tenant,
] {
for action in &[Action::Delete, Action::Admin, Action::Execute, Action::List] {
set = set.add(Permission::new(
format!("{:?}-{:?}", action, resource_type).to_lowercase(),
format!("{:?} {:?}", action, resource_type),
*action,
*resource_type,
));
}
}
set
}
}
impl Default for PermissionSet {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_permission_creation() {
let permission = Permission::new(
"read-dataset".to_string(),
"Read Dataset".to_string(),
Action::Read,
ResourceType::Dataset,
);
assert_eq!(permission.id, "read-dataset");
assert_eq!(permission.action, Action::Read);
assert_eq!(permission.resource_type, ResourceType::Dataset);
}
#[test]
fn test_permission_with_pattern() {
let permission = Permission::new(
"read-dataset".to_string(),
"Read Dataset".to_string(),
Action::Read,
ResourceType::Dataset,
)
.with_pattern("dataset-*".to_string());
assert_eq!(permission.resource_pattern, Some("dataset-*".to_string()));
assert!(permission.matches_resource("dataset-123"));
assert!(permission.matches_resource("dataset-abc"));
assert!(!permission.matches_resource("other-123"));
}
#[test]
fn test_glob_matching() {
assert!(glob_match("*", "anything"));
assert!(glob_match("test-*", "test-123"));
assert!(glob_match("*-test", "123-test"));
assert!(glob_match("*test*", "123test456"));
assert!(glob_match("exact", "exact"));
assert!(!glob_match("test-*", "other-123"));
assert!(!glob_match("exact", "not-exact"));
}
#[test]
fn test_permission_set_read_only() {
let permissions = PermissionSet::read_only().build();
assert!(!permissions.is_empty());
assert!(permissions.iter().all(|p| p.action == Action::Read));
}
#[test]
fn test_permission_set_read_write() {
let permissions = PermissionSet::read_write().build();
assert!(!permissions.is_empty());
let has_read = permissions.iter().any(|p| p.action == Action::Read);
let has_write = permissions.iter().any(|p| p.action == Action::Write);
assert!(has_read);
assert!(has_write);
}
#[test]
fn test_permission_set_admin() {
let permissions = PermissionSet::admin().build();
assert!(!permissions.is_empty());
let has_admin = permissions.iter().any(|p| p.action == Action::Admin);
let has_delete = permissions.iter().any(|p| p.action == Action::Delete);
assert!(has_admin);
assert!(has_delete);
}
}