information
This commit is contained in:
2
src/user_privilege/mod.rs
Normal file
2
src/user_privilege/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod privilege;
|
||||
pub mod user;
|
314
src/user_privilege/privilege.rs
Normal file
314
src/user_privilege/privilege.rs
Normal file
@@ -0,0 +1,314 @@
|
||||
// Copyright (C) 2024 Bellande Architecture Mechanism Research Innovation Center, Ronaldson Bellande
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU 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 General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::hash::Hash;
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::Result;
|
||||
use log::error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::audit::audit::log_audit_event;
|
||||
use crate::config::config::Config;
|
||||
use crate::user_privilege::user::User;
|
||||
use chrono::Timelike;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone, Copy)]
|
||||
pub enum PrivilegeLevel {
|
||||
User, // Basic user privileges
|
||||
Group, // Group-based privileges
|
||||
Administrator, // Administrative privileges
|
||||
Root, // Root-level access
|
||||
Bell, // Highest level - system owner
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum PrivilegeLevelError {
|
||||
#[error("Invalid privilege level: {0}")]
|
||||
InvalidPrivilegeLevel(String),
|
||||
#[error("Insufficient privileges")]
|
||||
InsufficientPrivileges,
|
||||
#[error("Expired privileges")]
|
||||
ExpiredPrivileges,
|
||||
#[error("Group not found: {0}")]
|
||||
GroupNotFound(String),
|
||||
#[error("Permission not found: {0}")]
|
||||
PermissionNotFound(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PrivilegeConfig {
|
||||
pub elevation_timeout: Duration,
|
||||
pub require_mfa: bool,
|
||||
pub allowed_elevation_hours: Vec<u8>,
|
||||
pub max_concurrent_elevations: usize,
|
||||
pub restricted_commands: HashMap<PrivilegeLevel, Vec<String>>,
|
||||
}
|
||||
|
||||
impl Default for PrivilegeConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
elevation_timeout: Duration::from_secs(3600),
|
||||
require_mfa: true,
|
||||
allowed_elevation_hours: (0..24).collect(),
|
||||
max_concurrent_elevations: 3,
|
||||
restricted_commands: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PrivilegeManager {
|
||||
config: PrivilegeConfig,
|
||||
active_elevations: HashMap<String, Vec<PrivilegeElevation>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PrivilegeElevation {
|
||||
level: PrivilegeLevel,
|
||||
granted_at: SystemTime,
|
||||
expires_at: SystemTime,
|
||||
reason: String,
|
||||
}
|
||||
|
||||
impl FromStr for PrivilegeLevel {
|
||||
type Err = PrivilegeLevelError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"user" => Ok(PrivilegeLevel::User),
|
||||
"group" => Ok(PrivilegeLevel::Group),
|
||||
"admin" | "administrator" => Ok(PrivilegeLevel::Administrator),
|
||||
"root" => Ok(PrivilegeLevel::Root),
|
||||
"bell" => Ok(PrivilegeLevel::Bell),
|
||||
_ => Err(PrivilegeLevelError::InvalidPrivilegeLevel(s.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PrivilegeLevel {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
PrivilegeLevel::User => write!(f, "user"),
|
||||
PrivilegeLevel::Group => write!(f, "group"),
|
||||
PrivilegeLevel::Administrator => write!(f, "administrator"),
|
||||
PrivilegeLevel::Root => write!(f, "root"),
|
||||
PrivilegeLevel::Bell => write!(f, "bell"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PrivilegeManager {
|
||||
pub fn new(config: PrivilegeConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
active_elevations: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn check_permission(
|
||||
&self,
|
||||
user: &User,
|
||||
required_privilege: PrivilegeLevel,
|
||||
config: &Config,
|
||||
) -> Result<bool> {
|
||||
// Direct privilege level check
|
||||
if user.privilege >= required_privilege {
|
||||
log_audit_event(
|
||||
"PRIVILEGE_CHECK",
|
||||
&user.username,
|
||||
&format!("Direct privilege granted: {:?}", required_privilege),
|
||||
)
|
||||
.await?;
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Check active elevations
|
||||
if let Some(elevations) = self.active_elevations.get(&user.username) {
|
||||
for elevation in elevations {
|
||||
if elevation.level >= required_privilege && SystemTime::now() < elevation.expires_at
|
||||
{
|
||||
log_audit_event(
|
||||
"PRIVILEGE_CHECK",
|
||||
&user.username,
|
||||
&format!("Elevation privilege granted: {:?}", required_privilege),
|
||||
)
|
||||
.await?;
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check group permissions
|
||||
for group_name in &user.groups {
|
||||
if let Some(group) = config.groups.iter().find(|g| g.name == *group_name) {
|
||||
if group.permissions.contains(&required_privilege.to_string()) {
|
||||
log_audit_event(
|
||||
"PRIVILEGE_CHECK",
|
||||
&user.username,
|
||||
&format!(
|
||||
"Group privilege granted: {:?} from {}",
|
||||
required_privilege, group_name
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log_audit_event(
|
||||
"PRIVILEGE_CHECK",
|
||||
&user.username,
|
||||
&format!("Permission denied for: {:?}", required_privilege),
|
||||
)
|
||||
.await?;
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub async fn elevate_privilege(
|
||||
&mut self,
|
||||
user: &User,
|
||||
requested_level: PrivilegeLevel,
|
||||
reason: &str,
|
||||
mfa_token: Option<&str>,
|
||||
) -> Result<()> {
|
||||
// Check if elevation is allowed at current hour
|
||||
let current_hour = chrono::Local::now().hour() as u8;
|
||||
if !self.config.allowed_elevation_hours.contains(¤t_hour) {
|
||||
return Err(PrivilegeLevelError::InsufficientPrivileges.into());
|
||||
}
|
||||
|
||||
// Check MFA requirement
|
||||
if self.config.require_mfa && mfa_token.is_none() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"MFA token required for privilege elevation"
|
||||
));
|
||||
}
|
||||
|
||||
// Check concurrent elevations
|
||||
let user_elevations = self
|
||||
.active_elevations
|
||||
.entry(user.username.clone())
|
||||
.or_default();
|
||||
if user_elevations.len() >= self.config.max_concurrent_elevations {
|
||||
return Err(anyhow::anyhow!("Maximum concurrent elevations reached"));
|
||||
}
|
||||
|
||||
// Create new elevation
|
||||
let elevation = PrivilegeElevation {
|
||||
level: requested_level,
|
||||
granted_at: SystemTime::now(),
|
||||
expires_at: SystemTime::now() + self.config.elevation_timeout,
|
||||
reason: reason.to_string(),
|
||||
};
|
||||
|
||||
user_elevations.push(elevation);
|
||||
|
||||
log_audit_event(
|
||||
"PRIVILEGE_ELEVATION",
|
||||
&user.username,
|
||||
&format!("Elevated to {:?} for reason: {}", requested_level, reason),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn revoke_elevation(&mut self, user: &str, level: PrivilegeLevel) -> Result<()> {
|
||||
if let Some(elevations) = self.active_elevations.get_mut(user) {
|
||||
elevations.retain(|e| e.level != level);
|
||||
log_audit_event(
|
||||
"PRIVILEGE_REVOCATION",
|
||||
user,
|
||||
&format!("Revoked elevation: {:?}", level),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cleanup_expired_elevations(&mut self) {
|
||||
let now = SystemTime::now();
|
||||
for elevations in self.active_elevations.values_mut() {
|
||||
elevations.retain(|e| e.expires_at > now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OS-specific privilege checks
|
||||
pub async fn check_os_specific_privileges(
|
||||
user: &User,
|
||||
required_privilege: PrivilegeLevel,
|
||||
) -> Result<bool> {
|
||||
match std::env::consts::OS {
|
||||
"macos" => check_macos_privileges(user, required_privilege).await,
|
||||
"linux" => check_linux_privileges(user, required_privilege).await,
|
||||
"bellandeos" => check_bellande_privileges(user, required_privilege).await,
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_macos_privileges(user: &User, required_privilege: PrivilegeLevel) -> Result<bool> {
|
||||
// Check admin group membership
|
||||
if required_privilege >= PrivilegeLevel::Administrator {
|
||||
let output = std::process::Command::new("dseditgroup")
|
||||
.args(&["-o", "checkmember", "-m", &user.username, "admin"])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn check_linux_privileges(user: &User, required_privilege: PrivilegeLevel) -> Result<bool> {
|
||||
// Check sudo group membership
|
||||
if required_privilege >= PrivilegeLevel::Administrator {
|
||||
let output = std::process::Command::new("groups")
|
||||
.arg(&user.username)
|
||||
.output()?;
|
||||
|
||||
let groups = String::from_utf8_lossy(&output.stdout);
|
||||
if !groups.contains("sudo") && !groups.contains("wheel") {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn check_bellande_privileges(
|
||||
user: &User,
|
||||
required_privilege: PrivilegeLevel,
|
||||
) -> Result<bool> {
|
||||
// Check BellandeOS specific privileges
|
||||
let output = std::process::Command::new("bellctl")
|
||||
.args(&[
|
||||
"user",
|
||||
"check-privilege",
|
||||
&user.username,
|
||||
&required_privilege.to_string(),
|
||||
])
|
||||
.output()?;
|
||||
|
||||
Ok(output.status.success())
|
||||
}
|
675
src/user_privilege/user.rs
Normal file
675
src/user_privilege/user.rs
Normal file
@@ -0,0 +1,675 @@
|
||||
// Copyright (C) 2024 Bellande Architecture Mechanism Research Innovation Center, Ronaldson Bellande
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU 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 General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use argon2::{
|
||||
password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
|
||||
Argon2,
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use log::error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::Write;
|
||||
use thiserror::Error;
|
||||
use totp_rs::Secret;
|
||||
|
||||
use crate::audit::audit::log_audit_event;
|
||||
use crate::config::config::Config;
|
||||
use crate::user_privilege::privilege::PrivilegeLevel;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct User {
|
||||
pub username: String,
|
||||
pub password_hash: String,
|
||||
pub privilege: PrivilegeLevel,
|
||||
pub totp_secret: String,
|
||||
pub groups: Vec<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub last_login: Option<DateTime<Utc>>,
|
||||
pub password_changed_at: DateTime<Utc>,
|
||||
pub failed_login_attempts: u32,
|
||||
pub locked_until: Option<DateTime<Utc>>,
|
||||
pub settings: UserSettings,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct UserSettings {
|
||||
pub require_mfa: bool,
|
||||
pub password_expiry_days: u32,
|
||||
pub max_failed_attempts: u32,
|
||||
pub lockout_duration: Duration,
|
||||
pub allowed_ip_ranges: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum UserError {
|
||||
#[error("User not found: {0}")]
|
||||
UserNotFound(String),
|
||||
#[error("User already exists: {0}")]
|
||||
UserExists(String),
|
||||
#[error("Invalid password: {0}")]
|
||||
InvalidPassword(String),
|
||||
#[error("Account locked: {0}")]
|
||||
AccountLocked(String),
|
||||
#[error("Password expired")]
|
||||
PasswordExpired,
|
||||
#[error("Invalid group: {0}")]
|
||||
InvalidGroup(String),
|
||||
}
|
||||
|
||||
impl Default for UserSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
require_mfa: true,
|
||||
password_expiry_days: 90,
|
||||
max_failed_attempts: 5,
|
||||
lockout_duration: Duration::from_secs(1800), // 30 minutes
|
||||
allowed_ip_ranges: vec!["127.0.0.1/8".to_string()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn new(username: &str, password: &str, privilege: PrivilegeLevel) -> Result<Self> {
|
||||
let password_hash = hash_password(password)?;
|
||||
let totp_secret = generate_totp_secret();
|
||||
let now = Utc::now();
|
||||
|
||||
Ok(Self {
|
||||
username: username.to_string(),
|
||||
password_hash,
|
||||
privilege,
|
||||
totp_secret,
|
||||
groups: Vec::new(),
|
||||
created_at: now,
|
||||
last_login: None,
|
||||
password_changed_at: now,
|
||||
failed_login_attempts: 0,
|
||||
locked_until: None,
|
||||
settings: UserSettings::default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_locked(&self) -> bool {
|
||||
if let Some(locked_until) = self.locked_until {
|
||||
Utc::now() < locked_until
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn password_expired(&self) -> bool {
|
||||
let expiry = chrono::Duration::days(self.settings.password_expiry_days as i64);
|
||||
Utc::now() - self.password_changed_at > expiry
|
||||
}
|
||||
|
||||
pub fn record_login_attempt(&mut self, success: bool) {
|
||||
if success {
|
||||
self.last_login = Some(Utc::now());
|
||||
self.failed_login_attempts = 0;
|
||||
self.locked_until = None;
|
||||
} else {
|
||||
self.failed_login_attempts += 1;
|
||||
if self.failed_login_attempts >= self.settings.max_failed_attempts {
|
||||
self.locked_until = Some(
|
||||
Utc::now()
|
||||
+ chrono::Duration::from_std(self.settings.lockout_duration).unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn add_user(
|
||||
config: &mut Config,
|
||||
username: &str,
|
||||
password: &str,
|
||||
privilege: PrivilegeLevel,
|
||||
) -> Result<()> {
|
||||
// Check if user already exists
|
||||
if config.users.iter().any(|u| u.username == username) {
|
||||
return Err(UserError::UserExists(username.to_string()).into());
|
||||
}
|
||||
|
||||
// Create new user
|
||||
let new_user = User::new(username, password, privilege)?;
|
||||
|
||||
// Create OS-specific user account
|
||||
create_os_user(username, privilege).await?;
|
||||
|
||||
config.users.push(new_user.clone());
|
||||
config.save()?;
|
||||
|
||||
log_audit_event(
|
||||
"USER_ADDED",
|
||||
"SYSTEM",
|
||||
&format!("Added user: {} with privilege: {:?}", username, privilege),
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!(
|
||||
"User added successfully. TOTP secret: {}",
|
||||
new_user.totp_secret
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_user(config: &mut Config, username: &str) -> Result<()> {
|
||||
// Check if user exists
|
||||
if !config.users.iter().any(|u| u.username == username) {
|
||||
return Err(UserError::UserNotFound(username.to_string()).into());
|
||||
}
|
||||
|
||||
// Remove OS-specific user account
|
||||
remove_os_user(username).await?;
|
||||
|
||||
config.users.retain(|u| u.username != username);
|
||||
config.save()?;
|
||||
|
||||
log_audit_event(
|
||||
"USER_REMOVED",
|
||||
"SYSTEM",
|
||||
&format!("Removed user: {}", username),
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("User removed successfully.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn change_password(
|
||||
config: &mut Config,
|
||||
username: &str,
|
||||
new_password: &str,
|
||||
) -> Result<()> {
|
||||
// Validate password complexity first
|
||||
validate_password_complexity(new_password)?;
|
||||
|
||||
// Find user index
|
||||
let user_index = config
|
||||
.users
|
||||
.iter()
|
||||
.position(|u| u.username == username)
|
||||
.ok_or_else(|| UserError::UserNotFound(username.to_string()))?;
|
||||
|
||||
// Update password
|
||||
let new_hash = hash_password(new_password)?;
|
||||
|
||||
// Update the user's password
|
||||
{
|
||||
let user = &mut config.users[user_index];
|
||||
user.password_hash = new_hash;
|
||||
user.password_changed_at = Utc::now();
|
||||
}
|
||||
|
||||
// Update OS-specific password
|
||||
update_os_password(username, new_password).await?;
|
||||
|
||||
config.save()?;
|
||||
|
||||
log_audit_event(
|
||||
"PASSWORD_CHANGED",
|
||||
username,
|
||||
"Password changed successfully",
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("Password changed successfully.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn change_privilege(
|
||||
config: &mut Config,
|
||||
username: &str,
|
||||
new_privilege: PrivilegeLevel,
|
||||
) -> Result<()> {
|
||||
// Find user index first
|
||||
let user_index = config
|
||||
.users
|
||||
.iter()
|
||||
.position(|u| u.username == username)
|
||||
.ok_or_else(|| UserError::UserNotFound(username.to_string()))?;
|
||||
|
||||
// Get the values we need before modifying the user
|
||||
let old_privilege = config.users[user_index].privilege;
|
||||
let username_clone = config.users[user_index].username.clone();
|
||||
|
||||
// Update the privilege
|
||||
config.users[user_index].privilege = new_privilege;
|
||||
|
||||
// Update OS-specific privileges
|
||||
update_os_privileges(username, new_privilege).await?;
|
||||
|
||||
// Save the configuration
|
||||
config.save()?;
|
||||
|
||||
// Log the audit event
|
||||
log_audit_event(
|
||||
"PRIVILEGE_CHANGED",
|
||||
&username_clone,
|
||||
&format!(
|
||||
"Privilege changed from {:?} to {:?}",
|
||||
old_privilege, new_privilege
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("Privilege level changed successfully.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_user_to_group(
|
||||
config: &mut Config,
|
||||
username: &str,
|
||||
group_name: &str,
|
||||
) -> Result<()> {
|
||||
// Check if group exists first
|
||||
if !config.groups.iter().any(|g| g.name == group_name) {
|
||||
return Err(UserError::InvalidGroup(group_name.to_string()).into());
|
||||
}
|
||||
|
||||
// Find user index
|
||||
let user_index = config
|
||||
.users
|
||||
.iter()
|
||||
.position(|u| u.username == username)
|
||||
.ok_or_else(|| UserError::UserNotFound(username.to_string()))?;
|
||||
|
||||
// Check if user is already in group
|
||||
let already_in_group = config.users[user_index]
|
||||
.groups
|
||||
.contains(&group_name.to_string());
|
||||
|
||||
if !already_in_group {
|
||||
// Get username for audit log before modification
|
||||
let username_clone = config.users[user_index].username.clone();
|
||||
|
||||
// Add user to group
|
||||
config.users[user_index].groups.push(group_name.to_string());
|
||||
|
||||
// Update OS-specific group membership
|
||||
add_os_user_to_group(username, group_name).await?;
|
||||
|
||||
// Save configuration
|
||||
config.save()?;
|
||||
|
||||
// Log audit event
|
||||
log_audit_event(
|
||||
"USER_ADDED_TO_GROUP",
|
||||
&username_clone,
|
||||
&format!("Added to group: {}", group_name),
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("User added to group successfully.");
|
||||
} else {
|
||||
println!("User is already in this group.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_user_from_group(
|
||||
config: &mut Config,
|
||||
username: &str,
|
||||
group_name: &str,
|
||||
) -> Result<()> {
|
||||
// Find user index
|
||||
let user_index = config
|
||||
.users
|
||||
.iter()
|
||||
.position(|u| u.username == username)
|
||||
.ok_or_else(|| UserError::UserNotFound(username.to_string()))?;
|
||||
|
||||
// Get username for audit log before modification
|
||||
let username_clone = config.users[user_index].username.clone();
|
||||
|
||||
// Remove the group
|
||||
config.users[user_index].groups.retain(|g| g != group_name);
|
||||
|
||||
// Update OS-specific group membership
|
||||
remove_os_user_from_group(username, group_name).await?;
|
||||
|
||||
// Save configuration
|
||||
config.save()?;
|
||||
|
||||
// Log audit event
|
||||
log_audit_event(
|
||||
"USER_REMOVED_FROM_GROUP",
|
||||
&username_clone,
|
||||
&format!("Removed from group: {}", group_name),
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("User removed from group successfully.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
fn hash_password(password: &str) -> Result<String> {
|
||||
// Generate a random salt
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
|
||||
// Create default Argon2 instance
|
||||
let argon2 = Argon2::default();
|
||||
|
||||
// Hash the password
|
||||
Ok(argon2
|
||||
.hash_password(password.as_bytes(), &salt)?
|
||||
.to_string())
|
||||
}
|
||||
|
||||
// And here's a corresponding verify function you'll need
|
||||
fn verify_password(hash: &str, password: &str) -> Result<bool> {
|
||||
use argon2::password_hash::PasswordHash;
|
||||
use argon2::PasswordVerifier;
|
||||
|
||||
// Parse the hash string into a PasswordHash instance
|
||||
let parsed_hash = PasswordHash::new(hash)?;
|
||||
|
||||
// Verify the password against the hash
|
||||
Ok(Argon2::default()
|
||||
.verify_password(password.as_bytes(), &parsed_hash)
|
||||
.is_ok())
|
||||
}
|
||||
|
||||
fn generate_totp_secret() -> String {
|
||||
Secret::generate_secret().to_string()
|
||||
}
|
||||
|
||||
fn validate_password_complexity(password: &str) -> Result<()> {
|
||||
if password.len() < 12 {
|
||||
return Err(UserError::InvalidPassword("Password too short".to_string()).into());
|
||||
}
|
||||
|
||||
let has_uppercase = password.chars().any(|c| c.is_uppercase());
|
||||
let has_lowercase = password.chars().any(|c| c.is_lowercase());
|
||||
let has_digit = password.chars().any(|c| c.is_digit(10));
|
||||
let has_special = password.chars().any(|c| !c.is_alphanumeric());
|
||||
|
||||
if !(has_uppercase && has_lowercase && has_digit && has_special) {
|
||||
return Err(UserError::InvalidPassword(
|
||||
"Password does not meet complexity requirements".to_string(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// OS-specific functions
|
||||
async fn create_os_user(username: &str, privilege: PrivilegeLevel) -> Result<()> {
|
||||
match std::env::consts::OS {
|
||||
"macos" => create_macos_user(username, privilege).await,
|
||||
"linux" => create_linux_user(username, privilege).await,
|
||||
"bellandeos" => create_bellande_user(username, privilege).await,
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn remove_os_user(username: &str) -> Result<()> {
|
||||
match std::env::consts::OS {
|
||||
"macos" => remove_macos_user(username).await,
|
||||
"linux" => remove_linux_user(username).await,
|
||||
"bellandeos" => remove_bellande_user(username).await,
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_os_password(username: &str, password: &str) -> Result<()> {
|
||||
match std::env::consts::OS {
|
||||
"macos" => update_macos_password(username, password).await,
|
||||
"linux" => update_linux_password(username, password).await,
|
||||
"bellandeos" => update_bellande_password(username, password).await,
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_os_privileges(username: &str, privilege: PrivilegeLevel) -> Result<()> {
|
||||
match std::env::consts::OS {
|
||||
"macos" => update_macos_privileges(username, privilege).await,
|
||||
"linux" => update_linux_privileges(username, privilege).await,
|
||||
"bellandeos" => update_bellande_privileges(username, privilege).await,
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
// OS-specific implementations for macOS, Linux, and BellandeOS...
|
||||
async fn create_macos_user(username: &str, privilege: PrivilegeLevel) -> Result<()> {
|
||||
let mut cmd = std::process::Command::new("sysadminctl");
|
||||
cmd.args(&["-addUser", username]);
|
||||
|
||||
match privilege {
|
||||
PrivilegeLevel::Administrator => {
|
||||
cmd.arg("-admin");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
cmd.output().context("Failed to create macOS user")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_linux_user(username: &str, privilege: PrivilegeLevel) -> Result<()> {
|
||||
let mut cmd = std::process::Command::new("useradd");
|
||||
cmd.arg(username);
|
||||
|
||||
match privilege {
|
||||
PrivilegeLevel::Administrator => {
|
||||
cmd.args(&["-G", "sudo"]);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
cmd.output().context("Failed to create Linux user")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_bellande_user(username: &str, privilege: PrivilegeLevel) -> Result<()> {
|
||||
let mut cmd = std::process::Command::new("bellctl");
|
||||
cmd.args(&["user", "create", username]);
|
||||
|
||||
match privilege {
|
||||
PrivilegeLevel::Administrator => {
|
||||
cmd.arg("--admin");
|
||||
}
|
||||
PrivilegeLevel::Root => {
|
||||
cmd.arg("--root");
|
||||
}
|
||||
PrivilegeLevel::Bell => {
|
||||
cmd.arg("--bell");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
cmd.output().context("Failed to create BellandeOS user")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_macos_user(username: &str) -> Result<()> {
|
||||
std::process::Command::new("sysadminctl")
|
||||
.args(&["-deleteUser", username])
|
||||
.output()
|
||||
.context("Failed to remove macOS user")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_linux_user(username: &str) -> Result<()> {
|
||||
std::process::Command::new("userdel")
|
||||
.args(&["-r", username]) // -r flag removes home directory and mail spool
|
||||
.output()
|
||||
.context("Failed to remove Linux user")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_bellande_user(username: &str) -> Result<()> {
|
||||
std::process::Command::new("bellctl")
|
||||
.args(&["user", "remove", username])
|
||||
.output()
|
||||
.context("Failed to remove BellandeOS user")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_macos_password(username: &str, password: &str) -> Result<()> {
|
||||
std::process::Command::new("dscl")
|
||||
.args(&[".", "-passwd", &format!("/Users/{}", username), password])
|
||||
.output()
|
||||
.context("Failed to update macOS password")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_linux_password(username: &str, password: &str) -> Result<()> {
|
||||
let passwd_input = format!("{}:{}", username, password);
|
||||
let mut child = std::process::Command::new("chpasswd")
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.context("Failed to spawn chpasswd")?;
|
||||
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
stdin
|
||||
.write_all(passwd_input.as_bytes())
|
||||
.context("Failed to write to chpasswd stdin")?;
|
||||
}
|
||||
|
||||
child.wait().context("Failed to wait for chpasswd")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_bellande_password(username: &str, password: &str) -> Result<()> {
|
||||
let mut child = std::process::Command::new("bellctl")
|
||||
.args(&["user", "set-password", username])
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.context("Failed to spawn bellctl")?;
|
||||
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
stdin
|
||||
.write_all(password.as_bytes())
|
||||
.context("Failed to set BellandeOS password")?;
|
||||
}
|
||||
|
||||
child.wait().context("Failed to wait for bellctl")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_macos_privileges(username: &str, privilege: PrivilegeLevel) -> Result<()> {
|
||||
match privilege {
|
||||
PrivilegeLevel::Administrator | PrivilegeLevel::Root | PrivilegeLevel::Bell => {
|
||||
std::process::Command::new("dseditgroup")
|
||||
.args(&["-o", "edit", "-a", username, "-t", "user", "admin"])
|
||||
.output()
|
||||
.context("Failed to update macOS privileges")?;
|
||||
}
|
||||
_ => {
|
||||
std::process::Command::new("dseditgroup")
|
||||
.args(&["-o", "edit", "-d", username, "-t", "user", "admin"])
|
||||
.output()
|
||||
.context("Failed to update macOS privileges")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_linux_privileges(username: &str, privilege: PrivilegeLevel) -> Result<()> {
|
||||
match privilege {
|
||||
PrivilegeLevel::Administrator | PrivilegeLevel::Root => {
|
||||
std::process::Command::new("usermod")
|
||||
.args(&["-aG", "sudo", username])
|
||||
.output()
|
||||
.context("Failed to update Linux privileges")?;
|
||||
}
|
||||
PrivilegeLevel::Bell => {
|
||||
std::process::Command::new("usermod")
|
||||
.args(&["-aG", "sudo,adm,root", username])
|
||||
.output()
|
||||
.context("Failed to update Linux privileges")?;
|
||||
}
|
||||
_ => {
|
||||
std::process::Command::new("deluser")
|
||||
.args(&[username, "sudo"])
|
||||
.output()
|
||||
.context("Failed to update Linux privileges")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_bellande_privileges(username: &str, privilege: PrivilegeLevel) -> Result<()> {
|
||||
let privilege_str = match privilege {
|
||||
PrivilegeLevel::User => "user",
|
||||
PrivilegeLevel::Group => "group",
|
||||
PrivilegeLevel::Administrator => "admin",
|
||||
PrivilegeLevel::Root => "root",
|
||||
PrivilegeLevel::Bell => "bell",
|
||||
};
|
||||
|
||||
std::process::Command::new("bellctl")
|
||||
.args(&["user", "set-privilege", username, privilege_str])
|
||||
.output()
|
||||
.context("Failed to update BellandeOS privileges")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_os_user_to_group(username: &str, group: &str) -> Result<()> {
|
||||
match std::env::consts::OS {
|
||||
"macos" => {
|
||||
std::process::Command::new("dseditgroup")
|
||||
.args(&["-o", "edit", "-a", username, "-t", "user", group])
|
||||
.output()
|
||||
.context("Failed to add macOS user to group")?;
|
||||
}
|
||||
"linux" => {
|
||||
std::process::Command::new("usermod")
|
||||
.args(&["-aG", group, username])
|
||||
.output()
|
||||
.context("Failed to add Linux user to group")?;
|
||||
}
|
||||
"bellandeos" => {
|
||||
std::process::Command::new("bellctl")
|
||||
.args(&["user", "add-to-group", username, group])
|
||||
.output()
|
||||
.context("Failed to add BellandeOS user to group")?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_os_user_from_group(username: &str, group: &str) -> Result<()> {
|
||||
match std::env::consts::OS {
|
||||
"macos" => {
|
||||
std::process::Command::new("dseditgroup")
|
||||
.args(&["-o", "edit", "-d", username, "-t", "user", group])
|
||||
.output()
|
||||
.context("Failed to remove macOS user from group")?;
|
||||
}
|
||||
"linux" => {
|
||||
std::process::Command::new("deluser")
|
||||
.args(&[username, group])
|
||||
.output()
|
||||
.context("Failed to remove Linux user from group")?;
|
||||
}
|
||||
"bellandeos" => {
|
||||
std::process::Command::new("bellctl")
|
||||
.args(&["user", "remove-from-group", username, group])
|
||||
.output()
|
||||
.context("Failed to remove BellandeOS user from group")?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
Reference in New Issue
Block a user