diff --git a/Cargo.lock b/Cargo.lock
index 6c7b3b2..b995791 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -424,7 +424,7 @@ dependencies = [
[[package]]
name = "bichon"
-version = "0.1.4"
+version = "0.2.0"
dependencies = [
"ahash",
"async-imap",
diff --git a/Cargo.toml b/Cargo.toml
index ca347e7..31d5c39 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "bichon"
-version = "0.1.4"
+version = "0.2.0"
edition = "2021"
[[bin]]
diff --git a/src/main.rs b/src/main.rs
index 7a6da72..ef9765a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -16,7 +16,6 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-
use mimalloc::MiMalloc;
use modules::{
common::rustls::RustMailerTls,
@@ -25,11 +24,12 @@ use modules::{
logger,
rest::start_http_server,
tasks::PeriodicTasks,
- token::root::ensure_root_token,
};
use tracing::info;
-use crate::modules::{common::signal::SignalManager, settings::dir::DataDirManager};
+use crate::modules::{
+ common::signal::SignalManager, settings::dir::DataDirManager, users::manager::UserManager,
+};
mod modules;
@@ -68,7 +68,7 @@ async fn initialize() -> BichonResult<()> {
// SETTINGS.validate()?;
SignalManager::initialize().await?;
DataDirManager::initialize().await?;
- ensure_root_token().await?;
+ UserManager::initialize().await?;
RustMailerTls::initialize().await?;
EmailClientExecutors::initialize().await?;
PeriodicTasks::start_background_tasks();
diff --git a/src/modules/account/grant.rs b/src/modules/account/grant.rs
new file mode 100644
index 0000000..f7e2a47
--- /dev/null
+++ b/src/modules/account/grant.rs
@@ -0,0 +1,160 @@
+//
+// 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 .
+
+use poem_openapi::Object;
+use serde::{Deserialize, Serialize};
+
+use crate::{
+ modules::{
+ account::migration::AccountModel,
+ common::auth::ClientContext,
+ database::{manager::DB_MANAGER, with_transaction},
+ error::{code::ErrorCode, BichonResult},
+ users::{
+ permissions::Permission,
+ role::{RoleType, UserRole},
+ BichonUser,
+ },
+ },
+ raise_error, utc_now,
+};
+
+#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)]
+pub struct BatchAccountRoleRequest {
+ pub account_ids: Vec,
+ pub user_ids: Vec,
+ pub role_id: u64,
+}
+
+impl BatchAccountRoleRequest {
+ pub async fn validate_existence(&self) -> BichonResult<()> {
+ let role = UserRole::find(self.role_id).await?.ok_or_else(|| {
+ raise_error!(
+ format!("Role ID {} not found", self.role_id),
+ ErrorCode::ResourceNotFound
+ )
+ })?;
+
+ if !matches!(role.role_type, RoleType::Account) {
+ return Err(raise_error!(
+ "Only Account roles can be assigned to individual account".into(),
+ ErrorCode::InvalidParameter
+ ));
+ }
+
+ for id in &self.account_ids {
+ let exists = AccountModel::find(*id).await?; // Assuming an exists helper
+ if exists.is_none() {
+ return Err(raise_error!(
+ format!("Account ID {} not found", id),
+ ErrorCode::ResourceNotFound
+ ));
+ }
+ }
+
+ for id in &self.user_ids {
+ let exists = BichonUser::find(*id).await?; // Assuming an exists helper
+ if exists.is_none() {
+ return Err(raise_error!(
+ format!("User ID {} not found", id),
+ ErrorCode::ResourceNotFound
+ ));
+ }
+ }
+
+ Ok(())
+ }
+
+ async fn grant_batch_account_access(
+ account_ids: Vec,
+ user_ids: Vec,
+ role_id: u64,
+ ) -> BichonResult<()> {
+ with_transaction(DB_MANAGER.meta_db(), move |rw| {
+ for &uid in &user_ids {
+ // Fetch the current user record from the database
+ let user = rw
+ .get()
+ .primary::(uid)
+ .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?
+ .ok_or_else(|| {
+ raise_error!(
+ format!("User with id={} not found.", uid),
+ ErrorCode::ResourceNotFound
+ )
+ })?;
+
+ let mut updated_user = user.clone();
+
+ // Apply the role to each specified account_id
+ for &aid in &account_ids {
+ updated_user.account_access_map.insert(aid, role_id);
+ }
+
+ updated_user.updated_at = utc_now!();
+
+ // Save the updated user back to the database within the transaction
+ rw.update(user, updated_user)
+ .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
+ }
+ Ok(())
+ })
+ .await
+ }
+
+ pub async fn do_assign(self, context: &ClientContext) -> BichonResult<()> {
+ for account_id in &self.account_ids {
+ // Get the user's specific access for this account
+ let assigned_role_id =
+ context
+ .user
+ .account_access_map
+ .get(account_id)
+ .ok_or_else(|| {
+ raise_error!(
+ format!("No access to account {}", account_id),
+ ErrorCode::Forbidden
+ )
+ })?;
+
+ // Fetch the role definition from the database
+ let user_scoped_role = UserRole::find(*assigned_role_id).await?.ok_or_else(|| {
+ raise_error!(
+ "Assigned account role no longer exists".into(),
+ ErrorCode::InternalError
+ )
+ })?;
+
+ // Critical Check: Does this role grant management/sharing rights?
+ if !user_scoped_role
+ .permissions
+ .contains(Permission::ACCOUNT_MANAGE)
+ {
+ return Err(raise_error!(
+ format!("Your role on account {} does not allow sharing", account_id),
+ ErrorCode::Forbidden
+ ));
+ }
+
+ // Optional: Ensure manager isn't giving away perms they don't have
+ // This is where you'd compare target_role.permissions vs manager's perms
+ }
+
+ Self::grant_batch_account_access(self.account_ids, self.user_ids, self.role_id).await
+ }
+}
diff --git a/src/modules/account/migration.rs b/src/modules/account/migration.rs
index 64b4d37..cce3fa4 100644
--- a/src/modules/account/migration.rs
+++ b/src/modules/account/migration.rs
@@ -29,9 +29,10 @@ use crate::{
modules::{
account::{entity::ImapConfig, since::DateSince, state::AccountRunningState},
cache::imap::mailbox::MailBox,
- database::{insert_impl, list_all_impl},
+ database::{list_all_impl, with_transaction},
error::BichonResult,
indexer::manager::{EML_INDEX_MANAGER, ENVELOPE_INDEX_MANAGER},
+ users::{role::DEFAULT_ACCOUNT_MANAGER_ROLE_ID, BichonUser, DEFAULT_ADMIN_USER_ID},
},
utc_now,
};
@@ -52,10 +53,9 @@ use crate::modules::database::{
use crate::modules::error::code::ErrorCode;
use crate::modules::oauth2::token::OAuth2AccessToken;
use crate::modules::rest::response::DataPage;
-use crate::modules::token::AccessToken;
use crate::raise_error;
-pub type AccountModel = AccountV2;
+pub type AccountModel = AccountV3;
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Enum)]
pub enum AccountType {
@@ -121,8 +121,40 @@ impl AccountV2 {
fn pk(&self) -> String {
format!("{}_{}", self.created_at, self.id)
}
+}
- pub fn new(request: AccountCreateRequest) -> BichonResult {
+#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)]
+#[native_model(id = 4, version = 3, from = AccountV2)]
+#[native_db(primary_key(pk -> String))]
+pub struct AccountV3 {
+ #[secondary_key(unique)]
+ pub id: u64,
+ pub imap: Option,
+ pub enabled: bool,
+ #[oai(validator(custom = "crate::modules::common::validator::EmailValidator"))]
+ pub email: String,
+ pub name: Option,
+ pub capabilities: Option>,
+ pub date_since: Option,
+ pub folder_limit: Option,
+ pub sync_folders: Option>,
+ pub account_type: AccountType,
+ pub sync_interval_min: Option,
+ pub known_folders: Option>,
+ pub created_at: i64,
+ pub updated_at: i64,
+ pub created_by: u64, //user id
+ pub use_proxy: Option,
+ pub use_dangerous: bool,
+ pub pgp_key: Option,
+}
+
+impl AccountV3 {
+ fn pk(&self) -> String {
+ format!("{}_{}", self.created_at, self.id)
+ }
+
+ pub fn new(user_id: u64, request: AccountCreateRequest) -> BichonResult {
Ok(Self {
id: id!(64),
email: request.email,
@@ -141,12 +173,13 @@ impl AccountV2 {
folder_limit: request.folder_limit,
use_dangerous: request.use_dangerous,
pgp_key: request.pgp_key,
+ created_by: user_id,
})
}
pub async fn check_account_exists(account_id: u64) -> BichonResult {
let account =
- secondary_find_impl::(DB_MANAGER.meta_db(), AccountV2Key::id, account_id)
+ secondary_find_impl::(DB_MANAGER.meta_db(), AccountV3Key::id, account_id)
.await?
.ok_or_else(|| {
raise_error!(
@@ -176,24 +209,53 @@ impl AccountV2 {
}
pub async fn find(account_id: u64) -> BichonResult