Files
openfang/crates/openfang-kernel/src/auth.rs
T
jaberjaber23 5692c96494 Initial commit — OpenFang Agent Operating System
Open-source Agent OS built in Rust.

- 14 crates, 1,767+ tests, zero clippy warnings
- 7 autonomous Hands (Clip, Lead, Collector, Predictor, Researcher, Twitter, Browser)
- 16 security systems (WASM sandbox, Merkle audit trail, taint tracking, Ed25519 signing, SSRF protection, secret zeroization, HMAC-SHA256 mutual auth, and more)
- 30 pre-built agents across 4 performance tiers
- 40 channel adapters (Telegram, Discord, Slack, WhatsApp, Teams, and 35 more)
- 38 built-in tools + MCP client/server + A2A protocol
- 26 LLM providers with intelligent routing and cost tracking
- 60+ bundled skills with FangHub marketplace
- Tauri 2.0 native desktop app
- 140+ REST/WS/SSE API endpoints with Alpine.js dashboard
- OpenAI-compatible /v1/chat/completions endpoint
- One-command install, production-ready
2026-02-26 01:00:27 +03:00

317 lines
10 KiB
Rust

//! RBAC authentication and authorization for multi-user access control.
//!
//! The AuthManager maps platform user identities (Telegram ID, Discord ID, etc.)
//! to OpenFang users with roles, then enforces permission checks on actions.
use dashmap::DashMap;
use openfang_types::agent::UserId;
use openfang_types::config::UserConfig;
use openfang_types::error::{OpenFangError, OpenFangResult};
use std::fmt;
use tracing::info;
/// User roles with hierarchical permissions.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum UserRole {
/// Read-only access — can view agent output but cannot interact.
Viewer = 0,
/// Standard user — can chat with agents.
User = 1,
/// Admin — can spawn/kill agents, install skills, view usage.
Admin = 2,
/// Owner — full access including user management and config changes.
Owner = 3,
}
impl fmt::Display for UserRole {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
UserRole::Viewer => write!(f, "viewer"),
UserRole::User => write!(f, "user"),
UserRole::Admin => write!(f, "admin"),
UserRole::Owner => write!(f, "owner"),
}
}
}
impl UserRole {
/// Parse a role from a string.
pub fn from_str_role(s: &str) -> Self {
match s.to_lowercase().as_str() {
"owner" => UserRole::Owner,
"admin" => UserRole::Admin,
"viewer" => UserRole::Viewer,
_ => UserRole::User,
}
}
}
/// Actions that can be authorized.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Action {
/// Chat with an agent.
ChatWithAgent,
/// Spawn a new agent.
SpawnAgent,
/// Kill a running agent.
KillAgent,
/// Install a skill.
InstallSkill,
/// View kernel configuration.
ViewConfig,
/// Modify kernel configuration.
ModifyConfig,
/// View usage/billing data.
ViewUsage,
/// Manage users (create, delete, change roles).
ManageUsers,
}
impl Action {
/// Minimum role required for this action.
fn required_role(&self) -> UserRole {
match self {
Action::ChatWithAgent => UserRole::User,
Action::ViewConfig => UserRole::User,
Action::ViewUsage => UserRole::Admin,
Action::SpawnAgent => UserRole::Admin,
Action::KillAgent => UserRole::Admin,
Action::InstallSkill => UserRole::Admin,
Action::ModifyConfig => UserRole::Owner,
Action::ManageUsers => UserRole::Owner,
}
}
}
/// A resolved user identity.
#[derive(Debug, Clone)]
pub struct UserIdentity {
/// OpenFang user ID.
pub id: UserId,
/// Display name.
pub name: String,
/// Role.
pub role: UserRole,
}
/// RBAC authentication and authorization manager.
pub struct AuthManager {
/// Known users by their OpenFang user ID.
users: DashMap<UserId, UserIdentity>,
/// Channel binding index: "channel_type:platform_id" → UserId.
channel_index: DashMap<String, UserId>,
}
impl AuthManager {
/// Create a new AuthManager from kernel user configuration.
pub fn new(user_configs: &[UserConfig]) -> Self {
let manager = Self {
users: DashMap::new(),
channel_index: DashMap::new(),
};
for config in user_configs {
let user_id = UserId::new();
let role = UserRole::from_str_role(&config.role);
let identity = UserIdentity {
id: user_id,
name: config.name.clone(),
role,
};
manager.users.insert(user_id, identity);
// Index channel bindings
for (channel_type, platform_id) in &config.channel_bindings {
let key = format!("{channel_type}:{platform_id}");
manager.channel_index.insert(key, user_id);
}
info!(
user = %config.name,
role = %role,
bindings = config.channel_bindings.len(),
"Registered user"
);
}
manager
}
/// Identify a user from a channel identity.
///
/// Returns the OpenFang UserId if a matching channel binding exists,
/// or None for unrecognized users.
pub fn identify(&self, channel_type: &str, platform_id: &str) -> Option<UserId> {
let key = format!("{channel_type}:{platform_id}");
self.channel_index.get(&key).map(|r| *r.value())
}
/// Get a user's identity by their UserId.
pub fn get_user(&self, user_id: UserId) -> Option<UserIdentity> {
self.users.get(&user_id).map(|r| r.value().clone())
}
/// Authorize a user for an action.
///
/// Returns Ok(()) if the user has sufficient permissions, or AuthDenied error.
pub fn authorize(&self, user_id: UserId, action: &Action) -> OpenFangResult<()> {
let identity = self
.users
.get(&user_id)
.ok_or_else(|| OpenFangError::AuthDenied("Unknown user".to_string()))?;
let required = action.required_role();
if identity.role >= required {
Ok(())
} else {
Err(OpenFangError::AuthDenied(format!(
"User '{}' (role: {}) lacks permission for {:?} (requires: {})",
identity.name, identity.role, action, required
)))
}
}
/// Check if RBAC is configured (any users registered).
pub fn is_enabled(&self) -> bool {
!self.users.is_empty()
}
/// Get the count of registered users.
pub fn user_count(&self) -> usize {
self.users.len()
}
/// List all registered users.
pub fn list_users(&self) -> Vec<UserIdentity> {
self.users.iter().map(|r| r.value().clone()).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn test_configs() -> Vec<UserConfig> {
vec![
UserConfig {
name: "Alice".to_string(),
role: "owner".to_string(),
channel_bindings: {
let mut m = HashMap::new();
m.insert("telegram".to_string(), "123456".to_string());
m.insert("discord".to_string(), "987654".to_string());
m
},
api_key_hash: None,
},
UserConfig {
name: "Guest".to_string(),
role: "user".to_string(),
channel_bindings: {
let mut m = HashMap::new();
m.insert("telegram".to_string(), "999999".to_string());
m
},
api_key_hash: None,
},
UserConfig {
name: "ReadOnly".to_string(),
role: "viewer".to_string(),
channel_bindings: HashMap::new(),
api_key_hash: None,
},
]
}
#[test]
fn test_user_registration() {
let manager = AuthManager::new(&test_configs());
assert!(manager.is_enabled());
assert_eq!(manager.user_count(), 3);
}
#[test]
fn test_identify_from_channel() {
let manager = AuthManager::new(&test_configs());
// Alice on Telegram
let owner_tg = manager.identify("telegram", "123456");
assert!(owner_tg.is_some());
// Alice on Discord
let owner_dc = manager.identify("discord", "987654");
assert!(owner_dc.is_some());
// Same user across channels
assert_eq!(owner_tg.unwrap(), owner_dc.unwrap());
// Unknown user
assert!(manager.identify("telegram", "unknown").is_none());
}
#[test]
fn test_owner_can_do_everything() {
let manager = AuthManager::new(&test_configs());
let owner_id = manager.identify("telegram", "123456").unwrap();
assert!(manager.authorize(owner_id, &Action::ChatWithAgent).is_ok());
assert!(manager.authorize(owner_id, &Action::SpawnAgent).is_ok());
assert!(manager.authorize(owner_id, &Action::KillAgent).is_ok());
assert!(manager.authorize(owner_id, &Action::ManageUsers).is_ok());
assert!(manager.authorize(owner_id, &Action::ModifyConfig).is_ok());
}
#[test]
fn test_user_limited_access() {
let manager = AuthManager::new(&test_configs());
let guest_id = manager.identify("telegram", "999999").unwrap();
// User can chat and view config
assert!(manager.authorize(guest_id, &Action::ChatWithAgent).is_ok());
assert!(manager.authorize(guest_id, &Action::ViewConfig).is_ok());
// User cannot spawn/kill/manage
assert!(manager.authorize(guest_id, &Action::SpawnAgent).is_err());
assert!(manager.authorize(guest_id, &Action::KillAgent).is_err());
assert!(manager.authorize(guest_id, &Action::ManageUsers).is_err());
}
#[test]
fn test_viewer_read_only() {
let manager = AuthManager::new(&test_configs());
let users = manager.list_users();
let viewer = users.iter().find(|u| u.name == "ReadOnly").unwrap();
// Viewer cannot even chat
assert!(manager
.authorize(viewer.id, &Action::ChatWithAgent)
.is_err());
}
#[test]
fn test_unknown_user_denied() {
let manager = AuthManager::new(&test_configs());
let fake_id = UserId::new();
assert!(manager.authorize(fake_id, &Action::ChatWithAgent).is_err());
}
#[test]
fn test_no_users_means_disabled() {
let manager = AuthManager::new(&[]);
assert!(!manager.is_enabled());
assert_eq!(manager.user_count(), 0);
}
#[test]
fn test_role_parsing() {
assert_eq!(UserRole::from_str_role("owner"), UserRole::Owner);
assert_eq!(UserRole::from_str_role("admin"), UserRole::Admin);
assert_eq!(UserRole::from_str_role("viewer"), UserRole::Viewer);
assert_eq!(UserRole::from_str_role("user"), UserRole::User);
assert_eq!(UserRole::from_str_role("OWNER"), UserRole::Owner);
assert_eq!(UserRole::from_str_role("unknown"), UserRole::User);
}
}