2025-10-07 10:59:52 +01:00
|
|
|
use std::sync::Arc;
|
2026-01-08 21:32:59 +00:00
|
|
|
|
2025-10-07 10:59:52 +01:00
|
|
|
use tracing::info;
|
|
|
|
|
use uuid::Uuid;
|
|
|
|
|
use validator::Validate;
|
|
|
|
|
|
2026-01-08 21:32:59 +00:00
|
|
|
use crate::error::{http, ControlCenterError, Result};
|
|
|
|
|
use crate::models::{Permission, PermissionCheckRequest, PermissionResponse};
|
|
|
|
|
use crate::services::DatabaseService;
|
|
|
|
|
|
2025-10-07 10:59:52 +01:00
|
|
|
/// Permission service for managing permission operations
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub struct PermissionService {
|
|
|
|
|
db: Arc<DatabaseService>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl PermissionService {
|
|
|
|
|
/// Create a new permission service
|
|
|
|
|
pub fn new(db: Arc<DatabaseService>) -> Self {
|
|
|
|
|
Self { db }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get permission by ID
|
|
|
|
|
pub async fn get_by_id(&self, permission_id: Uuid) -> Result<Permission> {
|
|
|
|
|
let query = "SELECT * FROM permissions WHERE permission_id = $permission_id";
|
2026-01-08 21:32:59 +00:00
|
|
|
let mut result = self
|
|
|
|
|
.db
|
|
|
|
|
.db
|
|
|
|
|
.query(query)
|
|
|
|
|
.bind(("permission_id", permission_id))
|
|
|
|
|
.await?;
|
2025-10-07 10:59:52 +01:00
|
|
|
|
|
|
|
|
let permissions: Vec<Permission> = result.take(0)?;
|
|
|
|
|
permissions
|
|
|
|
|
.into_iter()
|
|
|
|
|
.next()
|
|
|
|
|
.ok_or_else(|| ControlCenterError::PermissionNotFound(permission_id.to_string()))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get permission response by ID
|
|
|
|
|
pub async fn get_permission_response_by_id(
|
|
|
|
|
&self,
|
|
|
|
|
permission_id: Uuid,
|
|
|
|
|
) -> Result<PermissionResponse> {
|
|
|
|
|
let permission = self.get_by_id(permission_id).await?;
|
|
|
|
|
Ok(PermissionResponse::from(permission))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Find permission by name
|
|
|
|
|
pub async fn find_by_name(&self, name: &str) -> Result<Option<Permission>> {
|
|
|
|
|
let query = "SELECT * FROM permissions WHERE name = $name";
|
2026-01-08 21:32:59 +00:00
|
|
|
let mut result = self
|
|
|
|
|
.db
|
|
|
|
|
.db
|
|
|
|
|
.query(query)
|
|
|
|
|
.bind(("name", name.to_string()))
|
|
|
|
|
.await?;
|
2025-10-07 10:59:52 +01:00
|
|
|
|
|
|
|
|
let permissions: Vec<Permission> = result.take(0)?;
|
|
|
|
|
Ok(permissions.into_iter().next())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// List all permissions
|
|
|
|
|
pub async fn list_permissions(
|
|
|
|
|
&self,
|
|
|
|
|
resource: Option<String>,
|
|
|
|
|
action: Option<String>,
|
|
|
|
|
limit: Option<usize>,
|
|
|
|
|
offset: Option<usize>,
|
|
|
|
|
) -> Result<Vec<PermissionResponse>> {
|
|
|
|
|
let mut query = "SELECT * FROM permissions".to_string();
|
|
|
|
|
let mut conditions = Vec::new();
|
|
|
|
|
|
|
|
|
|
if let Some(resource) = resource {
|
|
|
|
|
conditions.push(format!("resource = '{}'", resource));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(action) = action {
|
|
|
|
|
conditions.push(format!("action = '{}'", action));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !conditions.is_empty() {
|
|
|
|
|
query.push_str(&format!(" WHERE {}", conditions.join(" AND ")));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query.push_str(" ORDER BY resource, action");
|
|
|
|
|
|
|
|
|
|
if let Some(limit) = limit {
|
|
|
|
|
query.push_str(&format!(" LIMIT {}", limit));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(offset) = offset {
|
|
|
|
|
query.push_str(&format!(" START {}", offset));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut result = self.db.db.query(&query).await?;
|
|
|
|
|
let permissions: Vec<Permission> = result.take(0)?;
|
|
|
|
|
|
|
|
|
|
Ok(permissions
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(PermissionResponse::from)
|
|
|
|
|
.collect())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Check if a permission exists
|
|
|
|
|
pub async fn permission_exists(&self, name: &str) -> Result<bool> {
|
|
|
|
|
let permission = self.find_by_name(name).await?;
|
|
|
|
|
Ok(permission.is_some())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Check permission by resource and action
|
|
|
|
|
pub async fn check_permission(&self, request: PermissionCheckRequest) -> Result<bool> {
|
|
|
|
|
// Validate request
|
2026-01-08 21:32:59 +00:00
|
|
|
request
|
|
|
|
|
.validate()
|
|
|
|
|
.map_err(|e| ControlCenterError::Http(http::HttpError::Validation(e.to_string())))?;
|
2025-10-07 10:59:52 +01:00
|
|
|
|
|
|
|
|
let permission_name = format!("{}:{}", request.resource, request.action);
|
|
|
|
|
self.permission_exists(&permission_name).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get permissions by resource
|
|
|
|
|
pub async fn get_permissions_by_resource(
|
|
|
|
|
&self,
|
|
|
|
|
resource: &str,
|
|
|
|
|
) -> Result<Vec<PermissionResponse>> {
|
|
|
|
|
self.list_permissions(Some(resource.to_string()), None, None, None)
|
|
|
|
|
.await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get permissions by action
|
|
|
|
|
pub async fn get_permissions_by_action(&self, action: &str) -> Result<Vec<PermissionResponse>> {
|
|
|
|
|
self.list_permissions(None, Some(action.to_string()), None, None)
|
|
|
|
|
.await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get all unique resources
|
|
|
|
|
pub async fn get_resources(&self) -> Result<Vec<String>> {
|
|
|
|
|
let query = "SELECT DISTINCT resource FROM permissions ORDER BY resource";
|
|
|
|
|
let mut result = self.db.db.query(query).await?;
|
|
|
|
|
|
|
|
|
|
let resources: Vec<String> = result.take(0)?;
|
|
|
|
|
Ok(resources)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get all unique actions
|
|
|
|
|
pub async fn get_actions(&self) -> Result<Vec<String>> {
|
|
|
|
|
let query = "SELECT DISTINCT action FROM permissions ORDER BY action";
|
|
|
|
|
let mut result = self.db.db.query(query).await?;
|
|
|
|
|
|
|
|
|
|
let actions: Vec<String> = result.take(0)?;
|
|
|
|
|
Ok(actions)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get permission count
|
|
|
|
|
pub async fn get_permission_count(&self, resource: Option<String>) -> Result<i64> {
|
|
|
|
|
let mut query = "SELECT count() FROM permissions".to_string();
|
|
|
|
|
|
|
|
|
|
if let Some(resource) = resource {
|
|
|
|
|
query.push_str(&format!(" WHERE resource = '{}'", resource));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut result = self.db.db.query(&query).await?;
|
|
|
|
|
let counts: Vec<i64> = result.take(0)?;
|
|
|
|
|
|
|
|
|
|
Ok(counts.into_iter().next().unwrap_or(0))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get permissions by names
|
|
|
|
|
pub async fn get_permissions_by_names(
|
|
|
|
|
&self,
|
|
|
|
|
permission_names: &[String],
|
|
|
|
|
) -> Result<Vec<Permission>> {
|
|
|
|
|
if permission_names.is_empty() {
|
|
|
|
|
return Ok(Vec::new());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let query = "SELECT * FROM permissions WHERE name IN $permission_names";
|
|
|
|
|
let mut result = self
|
|
|
|
|
.db
|
|
|
|
|
.db
|
|
|
|
|
.query(query)
|
|
|
|
|
.bind(("permission_names", permission_names.to_vec()))
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
let permissions: Vec<Permission> = result.take(0)?;
|
|
|
|
|
Ok(permissions)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Validate permissions exist
|
|
|
|
|
pub async fn validate_permissions_exist(
|
|
|
|
|
&self,
|
|
|
|
|
permission_names: &[String],
|
|
|
|
|
) -> Result<Vec<String>> {
|
|
|
|
|
let existing_permissions = self.get_permissions_by_names(permission_names).await?;
|
|
|
|
|
let existing_names: std::collections::HashSet<_> = existing_permissions
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|p| p.name.clone())
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
let missing_permissions: Vec<String> = permission_names
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|name| !existing_names.contains(*name))
|
|
|
|
|
.cloned()
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
Ok(missing_permissions)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create a custom permission (for advanced use cases)
|
|
|
|
|
pub async fn create_permission(
|
|
|
|
|
&self,
|
|
|
|
|
name: String,
|
|
|
|
|
resource: String,
|
|
|
|
|
action: String,
|
|
|
|
|
description: Option<String>,
|
|
|
|
|
) -> Result<PermissionResponse> {
|
|
|
|
|
// Check if permission already exists
|
|
|
|
|
if self.find_by_name(&name).await?.is_some() {
|
2026-01-08 21:32:59 +00:00
|
|
|
let msg = format!("Permission '{}' already exists", name);
|
|
|
|
|
return Err(ControlCenterError::Http(http::conflict(&msg)));
|
2025-10-07 10:59:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create permission
|
|
|
|
|
let permission = Permission::new(name, resource, action, description);
|
|
|
|
|
|
|
|
|
|
// Save to database
|
|
|
|
|
let created_permission: Permission = self
|
|
|
|
|
.db
|
|
|
|
|
.db
|
|
|
|
|
.create("permissions")
|
|
|
|
|
.content(permission)
|
|
|
|
|
.await?
|
2026-01-08 21:32:59 +00:00
|
|
|
.ok_or_else(|| {
|
|
|
|
|
ControlCenterError::from(crate::error::database::DatabaseError::Database(
|
|
|
|
|
"Failed to create permission".to_string(),
|
|
|
|
|
))
|
|
|
|
|
})?;
|
2025-10-07 10:59:52 +01:00
|
|
|
|
|
|
|
|
info!("Created permission: {}", created_permission.name);
|
|
|
|
|
|
|
|
|
|
Ok(PermissionResponse::from(created_permission))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Delete a custom permission (system permissions cannot be deleted)
|
|
|
|
|
pub async fn delete_permission(&self, permission_id: Uuid) -> Result<()> {
|
|
|
|
|
let permission = self.get_by_id(permission_id).await?;
|
|
|
|
|
|
|
|
|
|
// Check if permission is in use by any roles
|
|
|
|
|
let roles_using_permission = self.get_roles_using_permission(&permission.name).await?;
|
|
|
|
|
if !roles_using_permission.is_empty() {
|
2026-01-08 21:32:59 +00:00
|
|
|
let msg = format!(
|
2025-10-07 10:59:52 +01:00
|
|
|
"Cannot delete permission '{}' - it is used by {} roles: {}",
|
|
|
|
|
permission.name,
|
|
|
|
|
roles_using_permission.len(),
|
|
|
|
|
roles_using_permission.join(", ")
|
2026-01-08 21:32:59 +00:00
|
|
|
);
|
|
|
|
|
return Err(ControlCenterError::Http(http::conflict(&msg)));
|
2025-10-07 10:59:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Delete permission
|
|
|
|
|
let query = "DELETE permissions WHERE permission_id = $permission_id";
|
|
|
|
|
self.db
|
|
|
|
|
.db
|
|
|
|
|
.query(query)
|
|
|
|
|
.bind(("permission_id", permission_id))
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
info!("Deleted permission: {}", permission.name);
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get roles that use a specific permission
|
|
|
|
|
async fn get_roles_using_permission(&self, permission_name: &str) -> Result<Vec<String>> {
|
|
|
|
|
let query = "SELECT name FROM roles WHERE $permission_name IN permissions";
|
|
|
|
|
let mut result = self
|
|
|
|
|
.db
|
|
|
|
|
.db
|
|
|
|
|
.query(query)
|
|
|
|
|
.bind(("permission_name", permission_name.to_string()))
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
let role_names: Vec<String> = result.take(0)?;
|
|
|
|
|
Ok(role_names)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_permission_check_request_validation() {
|
|
|
|
|
// Valid request
|
|
|
|
|
let valid_request = PermissionCheckRequest {
|
|
|
|
|
resource: "users".to_string(),
|
|
|
|
|
action: "read".to_string(),
|
|
|
|
|
};
|
|
|
|
|
assert!(valid_request.validate().is_ok());
|
|
|
|
|
|
|
|
|
|
// Empty resource
|
|
|
|
|
let empty_resource = PermissionCheckRequest {
|
|
|
|
|
resource: "".to_string(),
|
|
|
|
|
action: "read".to_string(),
|
|
|
|
|
};
|
|
|
|
|
assert!(empty_resource.validate().is_err());
|
|
|
|
|
|
|
|
|
|
// Empty action
|
|
|
|
|
let empty_action = PermissionCheckRequest {
|
|
|
|
|
resource: "users".to_string(),
|
|
|
|
|
action: "".to_string(),
|
|
|
|
|
};
|
|
|
|
|
assert!(empty_action.validate().is_err());
|
|
|
|
|
|
|
|
|
|
// Both empty
|
|
|
|
|
let both_empty = PermissionCheckRequest {
|
|
|
|
|
resource: "".to_string(),
|
|
|
|
|
action: "".to_string(),
|
|
|
|
|
};
|
|
|
|
|
assert!(both_empty.validate().is_err());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_permission_name_format() {
|
|
|
|
|
let resource = "users";
|
|
|
|
|
let action = "read";
|
|
|
|
|
let expected_name = "users:read";
|
|
|
|
|
let actual_name = format!("{}:{}", resource, action);
|
|
|
|
|
assert_eq!(actual_name, expected_name);
|
|
|
|
|
}
|
2026-01-08 21:32:59 +00:00
|
|
|
}
|