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

411 lines
12 KiB
Rust

use std::sync::Arc;
use tracing::info;
use uuid::Uuid;
use validator::Validate;
use crate::error::{auth, http, ControlCenterError, Result};
use crate::models::{CreateRoleRequest, Role, RoleResponse, UpdateRoleRequest};
use crate::services::DatabaseService;
/// Role service for managing role operations
#[derive(Clone)]
pub struct RoleService {
db: Arc<DatabaseService>,
}
impl RoleService {
/// Create a new role service
pub fn new(db: Arc<DatabaseService>) -> Self {
Self { db }
}
/// Create a new role
pub async fn create_role(&self, request: CreateRoleRequest) -> Result<RoleResponse> {
// Validate request
request
.validate()
.map_err(|e| ControlCenterError::Http(http::HttpError::Validation(e.to_string())))?;
// Check if role already exists
if self.find_by_name(&request.name).await?.is_some() {
let msg = format!("Role with name '{}' already exists", request.name);
return Err(ControlCenterError::Http(http::conflict(&msg)));
}
// Create role
let role = Role::new(
request.name,
request.description,
request.permissions,
false, // User-created roles are not system roles
);
// Save to database
let created_role: Role =
self.db
.db
.create("roles")
.content(role)
.await?
.ok_or_else(|| {
ControlCenterError::from(crate::error::database::DatabaseError::Database(
"Failed to create role".to_string(),
))
})?;
info!("Created role: {}", created_role.name);
Ok(RoleResponse::from(created_role))
}
/// Get role by ID
pub async fn get_by_id(&self, role_id: Uuid) -> Result<Role> {
let query = "SELECT * FROM roles WHERE role_id = $role_id";
let mut result = self.db.db.query(query).bind(("role_id", role_id)).await?;
let roles: Vec<Role> = result.take(0)?;
roles.into_iter().next().ok_or_else(|| {
ControlCenterError::Auth(auth::AuthError::RoleNotFound(role_id.to_string()))
})
}
/// Get role response by ID (without sensitive data)
pub async fn get_role_response_by_id(&self, role_id: Uuid) -> Result<RoleResponse> {
let role = self.get_by_id(role_id).await?;
Ok(RoleResponse::from(role))
}
/// Find role by name
pub async fn find_by_name(&self, name: &str) -> Result<Option<Role>> {
let query = "SELECT * FROM roles WHERE name = $name";
let mut result = self
.db
.db
.query(query)
.bind(("name", name.to_string()))
.await?;
let roles: Vec<Role> = result.take(0)?;
Ok(roles.into_iter().next())
}
/// Update role
pub async fn update_role(
&self,
role_id: Uuid,
request: UpdateRoleRequest,
) -> Result<RoleResponse> {
// Validate request
request
.validate()
.map_err(|e| ControlCenterError::Http(http::HttpError::Validation(e.to_string())))?;
// Get existing role
let mut role = self.get_by_id(role_id).await?;
// Check if it's a system role
if role.is_system {
return Err(ControlCenterError::Http(http::HttpError::BadRequest(
"Cannot modify system roles".to_string(),
)));
}
// Check for conflicts if name is being changed
if let Some(ref new_name) = request.name {
if new_name != &role.name && self.find_by_name(new_name).await?.is_some() {
let msg = format!("Role with name '{}' already exists", new_name);
return Err(ControlCenterError::Http(http::conflict(&msg)));
}
}
// Update role
role.update(request);
// Save to database
let query = "UPDATE roles SET
name = $name,
description = $description,
permissions = $permissions,
updated_at = $updated_at
WHERE role_id = $role_id";
// Clone data before using in database operations to avoid borrow issues
let name = role.name.clone();
let description = role.description.clone();
let permissions = role.permissions.clone();
let updated_at = role.updated_at;
self.db
.db
.query(query)
.bind(("name", name))
.bind(("description", description))
.bind(("permissions", permissions))
.bind(("updated_at", updated_at))
.bind(("role_id", role_id))
.await?;
info!("Updated role: {}", role.name);
Ok(RoleResponse::from(role))
}
/// Delete role
pub async fn delete_role(&self, role_id: Uuid) -> Result<()> {
// Get existing role
let role = self.get_by_id(role_id).await?;
// Check if it's a system role
if role.is_system {
return Err(ControlCenterError::Http(http::HttpError::BadRequest(
"Cannot delete system roles".to_string(),
)));
}
// Check if role is in use by any users
let user_count = self.get_users_with_role_count(&role.name).await?;
if user_count > 0 {
let msg = format!(
"Cannot delete role '{}' - it is assigned to {} users",
role.name, user_count
);
return Err(ControlCenterError::Http(http::conflict(&msg)));
}
// Delete role
let query = "DELETE roles WHERE role_id = $role_id";
self.db.db.query(query).bind(("role_id", role_id)).await?;
info!("Deleted role: {}", role.name);
Ok(())
}
/// List all roles
pub async fn list_roles(
&self,
include_system: Option<bool>,
limit: Option<usize>,
offset: Option<usize>,
) -> Result<Vec<RoleResponse>> {
let mut query = "SELECT * FROM roles".to_string();
if let Some(include_system) = include_system {
if !include_system {
query.push_str(" WHERE is_system = false");
}
}
query.push_str(" ORDER BY created_at DESC");
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 roles: Vec<Role> = result.take(0)?;
Ok(roles.into_iter().map(RoleResponse::from).collect())
}
/// Add permission to role
pub async fn add_permission(&self, role_id: Uuid, permission: String) -> Result<RoleResponse> {
let mut role = self.get_by_id(role_id).await?;
// Check if it's a system role
if role.is_system {
return Err(ControlCenterError::Http(http::HttpError::BadRequest(
"Cannot modify system roles".to_string(),
)));
}
// Add permission if not already present
role.add_permission(permission.clone());
// Save to database
let query = "UPDATE roles SET permissions = $permissions, updated_at = $updated_at WHERE \
role_id = $role_id";
let permissions = role.permissions.clone();
let updated_at = role.updated_at;
self.db
.db
.query(query)
.bind(("permissions", permissions))
.bind(("updated_at", updated_at))
.bind(("role_id", role_id))
.await?;
info!("Added permission '{}' to role: {}", permission, role.name);
Ok(RoleResponse::from(role))
}
/// Remove permission from role
pub async fn remove_permission(
&self,
role_id: Uuid,
permission: String,
) -> Result<RoleResponse> {
let mut role = self.get_by_id(role_id).await?;
// Check if it's a system role
if role.is_system {
return Err(ControlCenterError::Http(http::HttpError::BadRequest(
"Cannot modify system roles".to_string(),
)));
}
// Remove permission
role.remove_permission(&permission);
// Save to database
let query = "UPDATE roles SET permissions = $permissions, updated_at = $updated_at WHERE \
role_id = $role_id";
let permissions = role.permissions.clone();
let updated_at = role.updated_at;
self.db
.db
.query(query)
.bind(("permissions", permissions))
.bind(("updated_at", updated_at))
.bind(("role_id", role_id))
.await?;
info!(
"Removed permission '{}' from role: {}",
permission, role.name
);
Ok(RoleResponse::from(role))
}
/// Get count of users with a specific role
async fn get_users_with_role_count(&self, role_name: &str) -> Result<i64> {
let query = "SELECT count() FROM users WHERE $role_name IN roles";
let mut result = self
.db
.db
.query(query)
.bind(("role_name", role_name.to_string()))
.await?;
let counts: Vec<i64> = result.take(0)?;
Ok(counts.into_iter().next().unwrap_or(0))
}
/// Get role count
pub async fn get_role_count(&self, include_system: Option<bool>) -> Result<i64> {
let mut query = "SELECT count() FROM roles".to_string();
if let Some(include_system) = include_system {
if !include_system {
query.push_str(" WHERE is_system = false");
}
}
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 roles by names
pub async fn get_roles_by_names(&self, role_names: &[String]) -> Result<Vec<Role>> {
if role_names.is_empty() {
return Ok(Vec::new());
}
let query = "SELECT * FROM roles WHERE name IN $role_names";
let mut result = self
.db
.db
.query(query)
.bind(("role_names", role_names.to_vec()))
.await?;
let roles: Vec<Role> = result.take(0)?;
Ok(roles)
}
/// Check if role has permission
pub async fn has_permission(&self, role_name: &str, permission: &str) -> Result<bool> {
if let Some(role) = self.find_by_name(role_name).await? {
Ok(role.has_permission(permission))
} else {
Ok(false)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_role_request_validation() {
// Valid request
let valid_request = CreateRoleRequest {
name: "test_role".to_string(),
description: Some("Test role description".to_string()),
permissions: Some(vec!["read".to_string(), "write".to_string()]),
};
assert!(valid_request.validate().is_ok());
// Short name
let short_name = CreateRoleRequest {
name: "a".to_string(),
description: None,
permissions: None,
};
assert!(short_name.validate().is_err());
// Long name
let long_name = CreateRoleRequest {
name: "a".repeat(51),
description: None,
permissions: None,
};
assert!(long_name.validate().is_err());
// Long description
let long_description = CreateRoleRequest {
name: "test_role".to_string(),
description: Some("a".repeat(201)),
permissions: None,
};
assert!(long_description.validate().is_err());
}
#[test]
fn test_update_role_request_validation() {
// Valid request
let valid_request = UpdateRoleRequest {
name: Some("updated_role".to_string()),
description: Some("Updated description".to_string()),
permissions: Some(vec!["read".to_string()]),
};
assert!(valid_request.validate().is_ok());
// Short name
let short_name = UpdateRoleRequest {
name: Some("a".to_string()),
description: None,
permissions: None,
};
assert!(short_name.validate().is_err());
// Valid empty update
let empty_update = UpdateRoleRequest {
name: None,
description: None,
permissions: None,
};
assert!(empty_update.validate().is_ok());
}
}