mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-31 01:52:30 +00:00
407 lines
14 KiB
Rust
407 lines
14 KiB
Rust
//
|
|
// Copyright (c) 2025 rustmailer.com (https://rustmailer.com)
|
|
//
|
|
// This file is part of the Bichon Email Archiving Project
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
use crate::{
|
|
modules::{
|
|
account::migration::AccountModel,
|
|
error::{code::ErrorCode, BichonResult},
|
|
users::{
|
|
acl::AccessControl,
|
|
permissions::{Permission, VALID_PERMISSION_SET},
|
|
role::{RoleType, UserRole},
|
|
},
|
|
utils::decode_avatar_bytes,
|
|
},
|
|
raise_error,
|
|
};
|
|
use poem_openapi::Object;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
|
|
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)]
|
|
pub struct RoleCreateRequest {
|
|
pub name: String,
|
|
pub role_type: RoleType,
|
|
pub description: Option<String>,
|
|
pub permissions: BTreeSet<String>,
|
|
}
|
|
|
|
impl RoleCreateRequest {
|
|
pub async fn validate(&self) -> BichonResult<()> {
|
|
let trimmed_name = self.name.trim();
|
|
if trimmed_name.is_empty() {
|
|
return Err(raise_error!(
|
|
"Role name cannot be empty or consist only of whitespace.".into(),
|
|
ErrorCode::InvalidParameter
|
|
));
|
|
}
|
|
|
|
let name_lower = trimmed_name.to_lowercase();
|
|
if name_lower == "admin" || name_lower == "manager" || name_lower == "viewer" {
|
|
return Err(raise_error!(
|
|
format!(
|
|
"The name '{}' is reserved for system builtin roles.",
|
|
trimmed_name
|
|
),
|
|
ErrorCode::InvalidParameter
|
|
));
|
|
}
|
|
|
|
if self.permissions.is_empty() {
|
|
return Err(raise_error!(
|
|
"Role must be assigned at least one permission.".into(),
|
|
ErrorCode::InvalidParameter
|
|
));
|
|
}
|
|
|
|
for permission in &self.permissions {
|
|
if !VALID_PERMISSION_SET.contains(permission.as_str()) {
|
|
return Err(raise_error!(
|
|
format!(
|
|
"Invalid permission '{}' specified in the request.",
|
|
permission
|
|
),
|
|
ErrorCode::InvalidParameter
|
|
));
|
|
}
|
|
}
|
|
|
|
Permission::validate_role_permissions(&self.role_type, &self.permissions)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)]
|
|
pub struct RoleUpdateRequest {
|
|
pub name: Option<String>,
|
|
pub description: Option<String>,
|
|
pub permissions: Option<BTreeSet<String>>,
|
|
}
|
|
|
|
impl RoleUpdateRequest {
|
|
pub async fn validate(&self) -> BichonResult<()> {
|
|
// 1. Ensure at least one field is provided for the update
|
|
if self.name.is_none() && self.description.is_none() && self.permissions.is_none() {
|
|
return Err(raise_error!(
|
|
"Update request must contain at least one field to modify (name, description, or permissions).".into(),
|
|
ErrorCode::InvalidParameter
|
|
));
|
|
}
|
|
|
|
// 2. Validate Name if present
|
|
if let Some(name) = &self.name {
|
|
let trimmed_name = name.trim();
|
|
if trimmed_name.is_empty() {
|
|
return Err(raise_error!(
|
|
"Role name cannot be set to an empty string or consist only of whitespace."
|
|
.into(),
|
|
ErrorCode::InvalidParameter
|
|
));
|
|
}
|
|
|
|
// Prevent renaming to reserved system names
|
|
let name_lower = trimmed_name.to_lowercase();
|
|
if name_lower == "admin" || name_lower == "manager" || name_lower == "viewer" {
|
|
return Err(raise_error!(
|
|
format!(
|
|
"The name '{}' is reserved for system builtin roles.",
|
|
trimmed_name
|
|
),
|
|
ErrorCode::InvalidParameter
|
|
));
|
|
}
|
|
}
|
|
|
|
// 3. Validate Permissions if present
|
|
if let Some(permissions) = &self.permissions {
|
|
// Ensure the role doesn't end up with zero permissions
|
|
if permissions.is_empty() {
|
|
return Err(raise_error!(
|
|
"Permissions list cannot be empty. A role must have at least one permission."
|
|
.into(),
|
|
ErrorCode::InvalidParameter
|
|
));
|
|
}
|
|
|
|
// Check for invalid permission strings using a functional approach
|
|
if let Some(invalid_permission) = permissions
|
|
.iter()
|
|
.find(|p| !VALID_PERMISSION_SET.contains(p.as_str()))
|
|
{
|
|
return Err(raise_error!(
|
|
format!(
|
|
"Invalid permission '{}' specified in the update request.",
|
|
invalid_permission
|
|
),
|
|
ErrorCode::InvalidParameter
|
|
));
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)]
|
|
pub struct UserCreateRequest {
|
|
pub username: String,
|
|
|
|
#[oai(validator(custom = "crate::modules::common::validator::EmailValidator"))]
|
|
pub email: String,
|
|
|
|
pub password: String,
|
|
|
|
/// Global Roles: System-wide permissions (e.g., Admin, User Manager).
|
|
pub global_roles: Vec<u64>,
|
|
|
|
/// Scoped Access: List of accounts paired with specific roles.
|
|
/// This allows different permissions per account.
|
|
pub account_access_map: BTreeMap<u64, u64>,
|
|
|
|
pub acl: Option<AccessControl>,
|
|
pub avatar_base64: Option<String>,
|
|
pub description: Option<String>,
|
|
}
|
|
|
|
impl UserCreateRequest {
|
|
pub async fn validate(&self) -> BichonResult<()> {
|
|
let username_len = self.username.len();
|
|
|
|
// 1. Username constraints
|
|
if username_len < 5 {
|
|
return Err(raise_error!(
|
|
"Username must be at least 5 characters long.".into(),
|
|
ErrorCode::InvalidParameter
|
|
));
|
|
}
|
|
if username_len > 32 {
|
|
return Err(raise_error!(
|
|
"Username cannot exceed 32 characters.".into(),
|
|
ErrorCode::InvalidParameter
|
|
));
|
|
}
|
|
|
|
// 2. Password constraints
|
|
let password_len = self.password.len();
|
|
if password_len < 8 {
|
|
return Err(raise_error!(
|
|
"Password must be at least 8 characters long.".into(),
|
|
ErrorCode::InvalidParameter
|
|
));
|
|
}
|
|
if password_len > 32 {
|
|
return Err(raise_error!(
|
|
"Password cannot exceed 32 characters.".into(),
|
|
ErrorCode::InvalidParameter
|
|
));
|
|
}
|
|
|
|
// 3. Global Roles validation
|
|
if self.global_roles.is_empty() {
|
|
return Err(raise_error!(
|
|
"Global roles list cannot be empty. At least one role must be selected.".into(),
|
|
ErrorCode::InvalidParameter
|
|
));
|
|
}
|
|
|
|
let all_roles = UserRole::list_all().await?;
|
|
let role_type_map: HashMap<u64, RoleType> =
|
|
all_roles.into_iter().map(|r| (r.id, r.role_type)).collect();
|
|
|
|
for rid in &self.global_roles {
|
|
match role_type_map.get(rid) {
|
|
Some(RoleType::Global) => {}
|
|
Some(_) => {
|
|
return Err(raise_error!(
|
|
format!("Role {} is not a System role", rid),
|
|
ErrorCode::InvalidParameter
|
|
))
|
|
}
|
|
None => {
|
|
return Err(raise_error!(
|
|
format!("System Role {} not found", rid),
|
|
ErrorCode::InvalidParameter
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
for (aid, rid) in &self.account_access_map {
|
|
if AccountModel::find(*aid).await?.is_none() {
|
|
return Err(raise_error!(
|
|
format!("Account {} not found", aid),
|
|
ErrorCode::InvalidParameter
|
|
));
|
|
}
|
|
match role_type_map.get(rid) {
|
|
Some(RoleType::Account) => {}
|
|
Some(_) => {
|
|
return Err(raise_error!(
|
|
format!(
|
|
"Role {} assigned to account {} must be an Account role",
|
|
rid, aid
|
|
),
|
|
ErrorCode::InvalidParameter
|
|
))
|
|
}
|
|
None => {
|
|
return Err(raise_error!(
|
|
format!("Role {} for account {} not found", rid, aid),
|
|
ErrorCode::InvalidParameter
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(acl) = &self.acl {
|
|
acl.validate()?;
|
|
}
|
|
|
|
if let Some(desc) = &self.description {
|
|
if desc.len() > 256 {
|
|
return Err(raise_error!(
|
|
"Description cannot exceed 256 characters.".into(),
|
|
ErrorCode::InvalidParameter
|
|
));
|
|
}
|
|
}
|
|
|
|
if let Some(avatar_base64) = &self.avatar_base64 {
|
|
decode_avatar_bytes(&avatar_base64)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)]
|
|
pub struct UserUpdateRequest {
|
|
pub username: Option<String>,
|
|
#[oai(validator(custom = "crate::modules::common::validator::EmailValidator"))]
|
|
pub email: Option<String>,
|
|
pub password: Option<String>,
|
|
pub avatar_base64: Option<String>,
|
|
pub global_roles: Option<Vec<u64>>,
|
|
/// Scoped Access
|
|
pub account_access_map: Option<BTreeMap<u64, u64>>,
|
|
pub acl: Option<AccessControl>,
|
|
pub description: Option<String>,
|
|
}
|
|
|
|
impl UserUpdateRequest {
|
|
pub async fn validate(&self) -> BichonResult<()> {
|
|
if let Some(username) = &self.username {
|
|
let len = username.len();
|
|
if len < 5 || len > 32 {
|
|
return Err(raise_error!(
|
|
"Username must be 5-32 characters.".into(),
|
|
ErrorCode::InvalidParameter
|
|
));
|
|
}
|
|
}
|
|
|
|
if let Some(password) = &self.password {
|
|
let len = password.len();
|
|
if len < 8 || len > 32 {
|
|
return Err(raise_error!(
|
|
"Password must be 8-32 characters.".into(),
|
|
ErrorCode::InvalidParameter
|
|
));
|
|
}
|
|
}
|
|
|
|
let all_roles = UserRole::list_all().await?;
|
|
let role_type_map: HashMap<u64, RoleType> =
|
|
all_roles.into_iter().map(|r| (r.id, r.role_type)).collect();
|
|
|
|
if let Some(roles) = &self.global_roles {
|
|
if roles.is_empty() {
|
|
return Err(raise_error!(
|
|
"Roles list cannot be empty.".into(),
|
|
ErrorCode::InvalidParameter
|
|
));
|
|
}
|
|
for role_id in roles {
|
|
match role_type_map.get(role_id) {
|
|
Some(RoleType::Global) => {}
|
|
Some(_) => {
|
|
return Err(raise_error!(
|
|
format!("Role {} is not a System role", role_id),
|
|
ErrorCode::InvalidParameter
|
|
))
|
|
}
|
|
None => {
|
|
return Err(raise_error!(
|
|
format!("System Role {} not found", role_id),
|
|
ErrorCode::InvalidParameter
|
|
))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(account_access_map) = &self.account_access_map {
|
|
for (aid, rid) in account_access_map {
|
|
if AccountModel::find(*aid).await?.is_none() {
|
|
return Err(raise_error!(
|
|
format!("Account {} not found", aid),
|
|
ErrorCode::InvalidParameter
|
|
));
|
|
}
|
|
match role_type_map.get(rid) {
|
|
Some(RoleType::Account) => {}
|
|
Some(_) => {
|
|
return Err(raise_error!(
|
|
format!(
|
|
"Role {} assigned to account {} must be an Account role",
|
|
rid, aid
|
|
),
|
|
ErrorCode::InvalidParameter
|
|
))
|
|
}
|
|
None => {
|
|
return Err(raise_error!(
|
|
format!("Role {} for account {} not found", rid, aid),
|
|
ErrorCode::InvalidParameter
|
|
))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(desc) = &self.description {
|
|
if desc.len() > 256 {
|
|
return Err(raise_error!(
|
|
"Description too long.".into(),
|
|
ErrorCode::InvalidParameter
|
|
));
|
|
}
|
|
}
|
|
|
|
if let Some(acl) = &self.acl {
|
|
acl.validate()?;
|
|
}
|
|
|
|
if let Some(avatar) = &self.avatar_base64 {
|
|
decode_avatar_bytes(avatar)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|