prvng_platform/crates/control-center/src/services/permission.rs
Jesús Pérez 09a97ac8f5
chore: update platform submodule to monorepo crates structure
Platform restructured into crates/, added AI service and detector,
       migrated control-center-ui to Leptos 0.8
2026-01-08 21:32:59 +00:00

332 lines
10 KiB
Rust

use std::sync::Arc;
use tracing::info;
use uuid::Uuid;
use validator::Validate;
use crate::error::{http, ControlCenterError, Result};
use crate::models::{Permission, PermissionCheckRequest, PermissionResponse};
use crate::services::DatabaseService;
/// 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";
let mut result = self
.db
.db
.query(query)
.bind(("permission_id", permission_id))
.await?;
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";
let mut result = self
.db
.db
.query(query)
.bind(("name", name.to_string()))
.await?;
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
request
.validate()
.map_err(|e| ControlCenterError::Http(http::HttpError::Validation(e.to_string())))?;
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() {
let msg = format!("Permission '{}' already exists", name);
return Err(ControlCenterError::Http(http::conflict(&msg)));
}
// 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?
.ok_or_else(|| {
ControlCenterError::from(crate::error::database::DatabaseError::Database(
"Failed to create permission".to_string(),
))
})?;
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() {
let msg = format!(
"Cannot delete permission '{}' - it is used by {} roles: {}",
permission.name,
roles_using_permission.len(),
roles_using_permission.join(", ")
);
return Err(ControlCenterError::Http(http::conflict(&msg)));
}
// 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);
}
}