botserver/src/security/auth.rs

1317 lines
39 KiB
Rust
Raw Normal View History

2025-12-28 19:29:18 -03:00
use axum::{
body::Body,
extract::{Path, State},
http::{header, Request, StatusCode},
middleware::Next,
response::{IntoResponse, Response},
Json,
};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::collections::{HashMap, HashSet};
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Permission {
Read,
Write,
Delete,
Admin,
ManageUsers,
ManageBots,
ViewAnalytics,
ManageSettings,
ExecuteTasks,
ViewLogs,
ManageSecrets,
AccessApi,
ManageFiles,
SendMessages,
ViewConversations,
ManageWebhooks,
ManageIntegrations,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Role {
Anonymous,
User,
Moderator,
Admin,
SuperAdmin,
Service,
Bot,
BotOwner,
BotOperator,
BotViewer,
}
impl Role {
pub fn permissions(&self) -> HashSet<Permission> {
match self {
Self::Anonymous => HashSet::new(),
Self::User => {
let mut perms = HashSet::new();
perms.insert(Permission::Read);
perms.insert(Permission::AccessApi);
perms
}
Self::Moderator => {
let mut perms = Self::User.permissions();
perms.insert(Permission::Write);
perms.insert(Permission::ViewLogs);
perms.insert(Permission::ViewAnalytics);
perms.insert(Permission::ViewConversations);
perms
}
Self::Admin => {
let mut perms = Self::Moderator.permissions();
perms.insert(Permission::Delete);
perms.insert(Permission::ManageUsers);
perms.insert(Permission::ManageBots);
perms.insert(Permission::ManageSettings);
perms.insert(Permission::ExecuteTasks);
perms.insert(Permission::ManageFiles);
perms.insert(Permission::ManageWebhooks);
perms
}
Self::SuperAdmin => {
let mut perms = Self::Admin.permissions();
perms.insert(Permission::Admin);
perms.insert(Permission::ManageSecrets);
perms.insert(Permission::ManageIntegrations);
perms
}
Self::Service => {
let mut perms = HashSet::new();
perms.insert(Permission::Read);
perms.insert(Permission::Write);
perms.insert(Permission::AccessApi);
perms.insert(Permission::ExecuteTasks);
perms.insert(Permission::SendMessages);
perms
}
Self::Bot => {
let mut perms = HashSet::new();
perms.insert(Permission::Read);
perms.insert(Permission::Write);
perms.insert(Permission::AccessApi);
perms.insert(Permission::SendMessages);
perms
}
Self::BotOwner => {
let mut perms = HashSet::new();
perms.insert(Permission::Read);
perms.insert(Permission::Write);
perms.insert(Permission::Delete);
perms.insert(Permission::AccessApi);
perms.insert(Permission::ManageBots);
perms.insert(Permission::ManageSettings);
perms.insert(Permission::ViewAnalytics);
perms.insert(Permission::ViewLogs);
perms.insert(Permission::ManageFiles);
perms.insert(Permission::SendMessages);
perms.insert(Permission::ViewConversations);
perms.insert(Permission::ManageWebhooks);
perms
}
Self::BotOperator => {
let mut perms = HashSet::new();
perms.insert(Permission::Read);
perms.insert(Permission::Write);
perms.insert(Permission::AccessApi);
perms.insert(Permission::ViewAnalytics);
perms.insert(Permission::ViewLogs);
perms.insert(Permission::SendMessages);
perms.insert(Permission::ViewConversations);
perms
}
Self::BotViewer => {
let mut perms = HashSet::new();
perms.insert(Permission::Read);
perms.insert(Permission::AccessApi);
perms.insert(Permission::ViewAnalytics);
perms.insert(Permission::ViewConversations);
perms
}
}
}
pub fn has_permission(&self, permission: &Permission) -> bool {
self.permissions().contains(permission)
}
pub fn from_str(s: &str) -> Self {
match s.to_lowercase().as_str() {
"anonymous" => Self::Anonymous,
"user" => Self::User,
"moderator" | "mod" => Self::Moderator,
"admin" => Self::Admin,
"superadmin" | "super_admin" | "super" => Self::SuperAdmin,
"service" | "svc" => Self::Service,
"bot" => Self::Bot,
"bot_owner" | "botowner" | "owner" => Self::BotOwner,
"bot_operator" | "botoperator" | "operator" => Self::BotOperator,
"bot_viewer" | "botviewer" | "viewer" => Self::BotViewer,
_ => Self::Anonymous,
}
}
pub fn hierarchy_level(&self) -> u8 {
match self {
Self::Anonymous => 0,
Self::User => 1,
Self::BotViewer => 2,
Self::BotOperator => 3,
Self::BotOwner => 4,
Self::Bot => 4,
Self::Moderator => 5,
Self::Service => 6,
Self::Admin => 7,
Self::SuperAdmin => 8,
}
}
pub fn is_at_least(&self, other: &Role) -> bool {
self.hierarchy_level() >= other.hierarchy_level()
}
}
impl Default for Role {
fn default() -> Self {
Self::Anonymous
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BotAccess {
pub bot_id: Uuid,
pub role: Role,
pub granted_at: Option<i64>,
pub granted_by: Option<Uuid>,
pub expires_at: Option<i64>,
}
impl BotAccess {
pub fn new(bot_id: Uuid, role: Role) -> Self {
Self {
bot_id,
role,
granted_at: Some(chrono::Utc::now().timestamp()),
granted_by: None,
expires_at: None,
}
}
pub fn owner(bot_id: Uuid) -> Self {
Self::new(bot_id, Role::BotOwner)
}
pub fn operator(bot_id: Uuid) -> Self {
Self::new(bot_id, Role::BotOperator)
}
pub fn viewer(bot_id: Uuid) -> Self {
Self::new(bot_id, Role::BotViewer)
}
pub fn with_expiry(mut self, expires_at: i64) -> Self {
self.expires_at = Some(expires_at);
self
}
pub fn with_grantor(mut self, granted_by: Uuid) -> Self {
self.granted_by = Some(granted_by);
self
}
pub fn is_expired(&self) -> bool {
if let Some(expires) = self.expires_at {
chrono::Utc::now().timestamp() > expires
} else {
false
}
}
pub fn is_valid(&self) -> bool {
!self.is_expired()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthenticatedUser {
pub user_id: Uuid,
pub username: String,
pub email: Option<String>,
pub roles: Vec<Role>,
pub bot_access: HashMap<Uuid, BotAccess>,
pub current_bot_id: Option<Uuid>,
pub session_id: Option<String>,
pub organization_id: Option<Uuid>,
pub metadata: HashMap<String, String>,
}
impl Default for AuthenticatedUser {
fn default() -> Self {
Self::anonymous()
}
}
impl AuthenticatedUser {
pub fn new(user_id: Uuid, username: String) -> Self {
Self {
user_id,
username,
email: None,
roles: vec![Role::User],
bot_access: HashMap::new(),
current_bot_id: None,
session_id: None,
organization_id: None,
metadata: HashMap::new(),
}
}
pub fn anonymous() -> Self {
Self {
user_id: Uuid::nil(),
username: "anonymous".to_string(),
email: None,
roles: vec![Role::Anonymous],
bot_access: HashMap::new(),
current_bot_id: None,
session_id: None,
organization_id: None,
metadata: HashMap::new(),
}
}
pub fn service(name: &str) -> Self {
Self {
user_id: Uuid::nil(),
username: format!("service:{}", name),
email: None,
roles: vec![Role::Service],
bot_access: HashMap::new(),
current_bot_id: None,
session_id: None,
organization_id: None,
metadata: HashMap::new(),
}
}
pub fn bot_user(bot_id: Uuid, bot_name: &str) -> Self {
Self {
user_id: bot_id,
username: format!("bot:{}", bot_name),
email: None,
roles: vec![Role::Bot],
bot_access: HashMap::new(),
current_bot_id: Some(bot_id),
session_id: None,
organization_id: None,
metadata: HashMap::new(),
}
}
pub fn with_email(mut self, email: impl Into<String>) -> Self {
self.email = Some(email.into());
self
}
pub fn with_role(mut self, role: Role) -> Self {
if !self.roles.contains(&role) {
self.roles.push(role);
}
self
}
pub fn with_roles(mut self, roles: Vec<Role>) -> Self {
self.roles = roles;
self
}
pub fn with_bot_access(mut self, access: BotAccess) -> Self {
self.bot_access.insert(access.bot_id, access);
self
}
pub fn with_current_bot(mut self, bot_id: Uuid) -> Self {
self.current_bot_id = Some(bot_id);
self
}
pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
self.session_id = Some(session_id.into());
self
}
pub fn with_organization(mut self, org_id: Uuid) -> Self {
self.organization_id = Some(org_id);
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn has_permission(&self, permission: &Permission) -> bool {
self.roles.iter().any(|r| r.has_permission(permission))
}
pub fn has_any_permission(&self, permissions: &[Permission]) -> bool {
permissions.iter().any(|p| self.has_permission(p))
}
pub fn has_all_permissions(&self, permissions: &[Permission]) -> bool {
permissions.iter().all(|p| self.has_permission(p))
}
pub fn has_role(&self, role: &Role) -> bool {
self.roles.contains(role)
}
pub fn has_any_role(&self, roles: &[Role]) -> bool {
roles.iter().any(|r| self.roles.contains(r))
}
pub fn highest_role(&self) -> &Role {
self.roles
.iter()
.max_by_key(|r| r.hierarchy_level())
.unwrap_or(&Role::Anonymous)
}
pub fn is_admin(&self) -> bool {
self.has_role(&Role::Admin) || self.has_role(&Role::SuperAdmin)
}
pub fn is_super_admin(&self) -> bool {
self.has_role(&Role::SuperAdmin)
}
pub fn is_authenticated(&self) -> bool {
!self.has_role(&Role::Anonymous) && self.user_id != Uuid::nil()
}
pub fn is_service(&self) -> bool {
self.has_role(&Role::Service)
}
pub fn is_bot(&self) -> bool {
self.has_role(&Role::Bot)
}
pub fn get_bot_access(&self, bot_id: &Uuid) -> Option<&BotAccess> {
self.bot_access.get(bot_id).filter(|a| a.is_valid())
}
pub fn get_bot_role(&self, bot_id: &Uuid) -> Option<&Role> {
self.get_bot_access(bot_id).map(|a| &a.role)
}
pub fn has_bot_permission(&self, bot_id: &Uuid, permission: &Permission) -> bool {
if self.is_admin() {
return true;
}
if let Some(access) = self.get_bot_access(bot_id) {
access.role.has_permission(permission)
} else {
false
}
}
pub fn can_access_bot(&self, bot_id: &Uuid) -> bool {
if self.is_admin() || self.is_service() {
return true;
}
if self.current_bot_id.as_ref() == Some(bot_id) && self.is_bot() {
return true;
}
self.get_bot_access(bot_id).is_some()
}
pub fn can_manage_bot(&self, bot_id: &Uuid) -> bool {
if self.is_admin() {
return true;
}
if let Some(access) = self.get_bot_access(bot_id) {
access.role == Role::BotOwner
} else {
false
}
}
pub fn can_operate_bot(&self, bot_id: &Uuid) -> bool {
if self.is_admin() {
return true;
}
if let Some(access) = self.get_bot_access(bot_id) {
access.role.is_at_least(&Role::BotOperator)
} else {
false
}
}
pub fn can_view_bot(&self, bot_id: &Uuid) -> bool {
if self.is_admin() || self.is_service() {
return true;
}
if let Some(access) = self.get_bot_access(bot_id) {
access.role.is_at_least(&Role::BotViewer)
} else {
false
}
}
pub fn can_access_organization(&self, org_id: &Uuid) -> bool {
if self.is_admin() {
return true;
}
self.organization_id
.as_ref()
.map(|id| id == org_id)
.unwrap_or(false)
}
pub fn accessible_bot_ids(&self) -> Vec<Uuid> {
self.bot_access
.iter()
.filter(|(_, access)| access.is_valid())
.map(|(id, _)| *id)
.collect()
}
pub fn owned_bot_ids(&self) -> Vec<Uuid> {
self.bot_access
.iter()
.filter(|(_, access)| access.is_valid() && access.role == Role::BotOwner)
.map(|(id, _)| *id)
.collect()
}
}
#[derive(Debug, Clone)]
pub struct AuthConfig {
pub require_auth: bool,
pub jwt_secret: Option<String>,
pub api_key_header: String,
pub bearer_prefix: String,
pub session_cookie_name: String,
pub allow_anonymous_paths: Vec<String>,
pub public_paths: Vec<String>,
pub bot_id_header: String,
pub org_id_header: String,
}
impl Default for AuthConfig {
fn default() -> Self {
Self {
require_auth: true,
jwt_secret: None,
api_key_header: "X-API-Key".to_string(),
bearer_prefix: "Bearer ".to_string(),
session_cookie_name: "session_id".to_string(),
allow_anonymous_paths: vec![
"/health".to_string(),
"/healthz".to_string(),
"/api/health".to_string(),
"/api/v1/health".to_string(),
"/.well-known".to_string(),
"/metrics".to_string(),
],
public_paths: vec![
"/".to_string(),
"/static".to_string(),
"/favicon.ico".to_string(),
"/robots.txt".to_string(),
],
bot_id_header: "X-Bot-ID".to_string(),
org_id_header: "X-Organization-ID".to_string(),
}
}
}
impl AuthConfig {
pub fn new() -> Self {
Self::default()
}
pub fn from_env() -> Self {
let mut config = Self::default();
if let Ok(secret) = std::env::var("JWT_SECRET") {
config.jwt_secret = Some(secret);
}
if let Ok(require) = std::env::var("REQUIRE_AUTH") {
config.require_auth = require == "true" || require == "1";
}
if let Ok(paths) = std::env::var("ANONYMOUS_PATHS") {
config.allow_anonymous_paths = paths
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
}
config
}
pub fn with_jwt_secret(mut self, secret: impl Into<String>) -> Self {
self.jwt_secret = Some(secret.into());
self
}
pub fn with_require_auth(mut self, require: bool) -> Self {
self.require_auth = require;
self
}
pub fn add_anonymous_path(mut self, path: impl Into<String>) -> Self {
self.allow_anonymous_paths.push(path.into());
self
}
pub fn add_public_path(mut self, path: impl Into<String>) -> Self {
self.public_paths.push(path.into());
self
}
pub fn is_public_path(&self, path: &str) -> bool {
for public_path in &self.public_paths {
if path == public_path || path.starts_with(&format!("{}/", public_path)) {
return true;
}
}
false
}
pub fn is_anonymous_allowed(&self, path: &str) -> bool {
for allowed_path in &self.allow_anonymous_paths {
if path == allowed_path || path.starts_with(&format!("{}/", allowed_path)) {
return true;
}
}
false
}
}
#[derive(Debug)]
pub enum AuthError {
MissingToken,
InvalidToken,
ExpiredToken,
InsufficientPermissions,
InvalidApiKey,
SessionExpired,
UserNotFound,
AccountDisabled,
RateLimited,
BotAccessDenied,
BotNotFound,
OrganizationAccessDenied,
InternalError(String),
}
impl AuthError {
pub fn status_code(&self) -> StatusCode {
match self {
Self::MissingToken => StatusCode::UNAUTHORIZED,
Self::InvalidToken => StatusCode::UNAUTHORIZED,
Self::ExpiredToken => StatusCode::UNAUTHORIZED,
Self::InsufficientPermissions => StatusCode::FORBIDDEN,
Self::InvalidApiKey => StatusCode::UNAUTHORIZED,
Self::SessionExpired => StatusCode::UNAUTHORIZED,
Self::UserNotFound => StatusCode::UNAUTHORIZED,
Self::AccountDisabled => StatusCode::FORBIDDEN,
Self::RateLimited => StatusCode::TOO_MANY_REQUESTS,
Self::BotAccessDenied => StatusCode::FORBIDDEN,
Self::BotNotFound => StatusCode::NOT_FOUND,
Self::OrganizationAccessDenied => StatusCode::FORBIDDEN,
Self::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
pub fn error_code(&self) -> &'static str {
match self {
Self::MissingToken => "missing_token",
Self::InvalidToken => "invalid_token",
Self::ExpiredToken => "expired_token",
Self::InsufficientPermissions => "insufficient_permissions",
Self::InvalidApiKey => "invalid_api_key",
Self::SessionExpired => "session_expired",
Self::UserNotFound => "user_not_found",
Self::AccountDisabled => "account_disabled",
Self::RateLimited => "rate_limited",
Self::BotAccessDenied => "bot_access_denied",
Self::BotNotFound => "bot_not_found",
Self::OrganizationAccessDenied => "organization_access_denied",
Self::InternalError(_) => "internal_error",
}
}
pub fn message(&self) -> String {
match self {
Self::MissingToken => "Authentication token is required".to_string(),
Self::InvalidToken => "Invalid authentication token".to_string(),
Self::ExpiredToken => "Authentication token has expired".to_string(),
Self::InsufficientPermissions => {
"You don't have permission to access this resource".to_string()
}
Self::InvalidApiKey => "Invalid API key".to_string(),
Self::SessionExpired => "Your session has expired".to_string(),
Self::UserNotFound => "User not found".to_string(),
Self::AccountDisabled => "Your account has been disabled".to_string(),
Self::RateLimited => "Too many requests, please try again later".to_string(),
Self::BotAccessDenied => "You don't have access to this bot".to_string(),
Self::BotNotFound => "Bot not found".to_string(),
Self::OrganizationAccessDenied => {
"You don't have access to this organization".to_string()
}
Self::InternalError(_) => "An internal error occurred".to_string(),
}
}
}
impl IntoResponse for AuthError {
fn into_response(self) -> Response {
let status = self.status_code();
let body = Json(json!({
"error": self.error_code(),
"message": self.message()
}));
(status, body).into_response()
}
}
pub fn extract_user_from_request(
request: &Request<Body>,
config: &AuthConfig,
) -> Result<AuthenticatedUser, AuthError> {
if let Some(api_key) = request
.headers()
.get(&config.api_key_header)
.and_then(|v| v.to_str().ok())
{
let mut user = validate_api_key_sync(api_key)?;
if let Some(bot_id) = extract_bot_id_from_request(request, config) {
user = user.with_current_bot(bot_id);
}
return Ok(user);
}
if let Some(auth_header) = request
.headers()
.get(header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
{
if let Some(token) = auth_header.strip_prefix(&config.bearer_prefix) {
let mut user = validate_bearer_token_sync(token)?;
if let Some(bot_id) = extract_bot_id_from_request(request, config) {
user = user.with_current_bot(bot_id);
}
return Ok(user);
}
}
if let Some(session_id) =
extract_session_from_cookies(request, &config.session_cookie_name)
{
let mut user = validate_session_sync(&session_id)?;
if let Some(bot_id) = extract_bot_id_from_request(request, config) {
user = user.with_current_bot(bot_id);
}
return Ok(user);
}
if let Some(user_id) = request
.headers()
.get("X-User-ID")
.and_then(|v| v.to_str().ok())
.and_then(|s| Uuid::parse_str(s).ok())
{
let mut user = AuthenticatedUser::new(user_id, "header-user".to_string());
if let Some(bot_id) = extract_bot_id_from_request(request, config) {
user = user.with_current_bot(bot_id);
}
return Ok(user);
}
Err(AuthError::MissingToken)
}
fn extract_bot_id_from_request(request: &Request<Body>, config: &AuthConfig) -> Option<Uuid> {
request
.headers()
.get(&config.bot_id_header)
.and_then(|v| v.to_str().ok())
.and_then(|s| Uuid::parse_str(s).ok())
}
fn extract_session_from_cookies(request: &Request<Body>, cookie_name: &str) -> Option<String> {
request
.headers()
.get(header::COOKIE)
.and_then(|v| v.to_str().ok())
.and_then(|cookies| {
cookies.split(';').find_map(|cookie| {
let mut parts = cookie.trim().splitn(2, '=');
let name = parts.next()?;
let value = parts.next()?;
if name == cookie_name {
Some(value.to_string())
} else {
None
}
})
})
}
fn validate_api_key_sync(api_key: &str) -> Result<AuthenticatedUser, AuthError> {
if api_key.is_empty() {
return Err(AuthError::InvalidApiKey);
}
if api_key.len() < 16 {
return Err(AuthError::InvalidApiKey);
}
Ok(AuthenticatedUser::service("api-client").with_metadata("api_key_prefix", &api_key[..8]))
}
fn validate_bearer_token_sync(token: &str) -> Result<AuthenticatedUser, AuthError> {
if token.is_empty() {
return Err(AuthError::InvalidToken);
}
let parts: Vec<&str> = token.split('.').collect();
if parts.len() != 3 {
return Err(AuthError::InvalidToken);
}
Ok(AuthenticatedUser::new(
Uuid::new_v4(),
"jwt-user".to_string(),
))
}
fn validate_session_sync(session_id: &str) -> Result<AuthenticatedUser, AuthError> {
if session_id.is_empty() {
return Err(AuthError::SessionExpired);
}
if Uuid::parse_str(session_id).is_err() && session_id.len() < 32 {
return Err(AuthError::InvalidToken);
}
Ok(
AuthenticatedUser::new(Uuid::new_v4(), "session-user".to_string())
.with_session(session_id),
)
}
pub async fn auth_middleware(
State(config): State<std::sync::Arc<AuthConfig>>,
mut request: Request<Body>,
next: Next,
) -> Result<Response, AuthError> {
let path = request.uri().path().to_string();
if config.is_public_path(&path) || config.is_anonymous_allowed(&path) {
request
.extensions_mut()
.insert(AuthenticatedUser::anonymous());
return Ok(next.run(request).await);
}
match extract_user_from_request(&request, &config) {
Ok(user) => {
request.extensions_mut().insert(user);
Ok(next.run(request).await)
}
Err(e) => {
if !config.require_auth {
request
.extensions_mut()
.insert(AuthenticatedUser::anonymous());
return Ok(next.run(request).await);
}
Err(e)
}
}
}
pub async fn require_auth_middleware(
mut request: Request<Body>,
next: Next,
) -> Result<Response, AuthError> {
let user = request
.extensions()
.get::<AuthenticatedUser>()
.cloned()
.unwrap_or_else(AuthenticatedUser::anonymous);
if !user.is_authenticated() {
return Err(AuthError::MissingToken);
}
request.extensions_mut().insert(user);
Ok(next.run(request).await)
}
pub fn require_permission(
permission: Permission,
) -> impl Fn(Request<Body>) -> Result<Request<Body>, AuthError> + Clone {
move |request: Request<Body>| {
let user = request
.extensions()
.get::<AuthenticatedUser>()
.cloned()
.unwrap_or_else(AuthenticatedUser::anonymous);
if !user.has_permission(&permission) {
return Err(AuthError::InsufficientPermissions);
}
Ok(request)
}
}
pub fn require_role(
role: Role,
) -> impl Fn(Request<Body>) -> Result<Request<Body>, AuthError> + Clone {
move |request: Request<Body>| {
let user = request
.extensions()
.get::<AuthenticatedUser>()
.cloned()
.unwrap_or_else(AuthenticatedUser::anonymous);
if !user.has_role(&role) {
return Err(AuthError::InsufficientPermissions);
}
Ok(request)
}
}
pub fn require_admin() -> impl Fn(Request<Body>) -> Result<Request<Body>, AuthError> + Clone {
move |request: Request<Body>| {
let user = request
.extensions()
.get::<AuthenticatedUser>()
.cloned()
.unwrap_or_else(AuthenticatedUser::anonymous);
if !user.is_admin() {
return Err(AuthError::InsufficientPermissions);
}
Ok(request)
}
}
pub fn require_bot_access(
bot_id: Uuid,
) -> impl Fn(Request<Body>) -> Result<Request<Body>, AuthError> + Clone {
move |request: Request<Body>| {
let user = request
.extensions()
.get::<AuthenticatedUser>()
.cloned()
.unwrap_or_else(AuthenticatedUser::anonymous);
if !user.can_access_bot(&bot_id) {
return Err(AuthError::BotAccessDenied);
}
Ok(request)
}
}
pub fn require_bot_permission(
bot_id: Uuid,
permission: Permission,
) -> impl Fn(Request<Body>) -> Result<Request<Body>, AuthError> + Clone {
move |request: Request<Body>| {
let user = request
.extensions()
.get::<AuthenticatedUser>()
.cloned()
.unwrap_or_else(AuthenticatedUser::anonymous);
if !user.has_bot_permission(&bot_id, &permission) {
return Err(AuthError::InsufficientPermissions);
}
Ok(request)
}
}
pub async fn require_permission_middleware(
permission: Permission,
request: Request<Body>,
next: Next,
) -> Result<Response, AuthError> {
let user = request
.extensions()
.get::<AuthenticatedUser>()
.cloned()
.unwrap_or_else(AuthenticatedUser::anonymous);
if !user.has_permission(&permission) {
return Err(AuthError::InsufficientPermissions);
}
Ok(next.run(request).await)
}
pub async fn require_role_middleware(
role: Role,
request: Request<Body>,
next: Next,
) -> Result<Response, AuthError> {
let user = request
.extensions()
.get::<AuthenticatedUser>()
.cloned()
.unwrap_or_else(AuthenticatedUser::anonymous);
if !user.has_role(&role) {
return Err(AuthError::InsufficientPermissions);
}
Ok(next.run(request).await)
}
pub async fn admin_only_middleware(
request: Request<Body>,
next: Next,
) -> Result<Response, AuthError> {
let user = request
.extensions()
.get::<AuthenticatedUser>()
.cloned()
.unwrap_or_else(AuthenticatedUser::anonymous);
if !user.is_admin() {
return Err(AuthError::InsufficientPermissions);
}
Ok(next.run(request).await)
}
pub async fn bot_scope_middleware(
Path(bot_id): Path<Uuid>,
mut request: Request<Body>,
next: Next,
) -> Result<Response, AuthError> {
let user = request
.extensions()
.get::<AuthenticatedUser>()
.cloned()
.unwrap_or_else(AuthenticatedUser::anonymous);
if !user.can_access_bot(&bot_id) {
return Err(AuthError::BotAccessDenied);
}
let user = user.with_current_bot(bot_id);
request.extensions_mut().insert(user);
Ok(next.run(request).await)
}
pub async fn bot_owner_middleware(
Path(bot_id): Path<Uuid>,
request: Request<Body>,
next: Next,
) -> Result<Response, AuthError> {
let user = request
.extensions()
.get::<AuthenticatedUser>()
.cloned()
.unwrap_or_else(AuthenticatedUser::anonymous);
if !user.can_manage_bot(&bot_id) {
return Err(AuthError::InsufficientPermissions);
}
Ok(next.run(request).await)
}
pub async fn bot_operator_middleware(
Path(bot_id): Path<Uuid>,
request: Request<Body>,
next: Next,
) -> Result<Response, AuthError> {
let user = request
.extensions()
.get::<AuthenticatedUser>()
.cloned()
.unwrap_or_else(AuthenticatedUser::anonymous);
if !user.can_operate_bot(&bot_id) {
return Err(AuthError::InsufficientPermissions);
}
Ok(next.run(request).await)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_role_permissions() {
assert!(!Role::Anonymous.has_permission(&Permission::Read));
assert!(Role::User.has_permission(&Permission::Read));
assert!(Role::User.has_permission(&Permission::AccessApi));
assert!(!Role::User.has_permission(&Permission::Write));
assert!(Role::Admin.has_permission(&Permission::ManageUsers));
assert!(Role::Admin.has_permission(&Permission::Delete));
assert!(Role::SuperAdmin.has_permission(&Permission::ManageSecrets));
}
#[test]
fn test_role_from_str() {
assert_eq!(Role::from_str("admin"), Role::Admin);
assert_eq!(Role::from_str("ADMIN"), Role::Admin);
assert_eq!(Role::from_str("user"), Role::User);
assert_eq!(Role::from_str("superadmin"), Role::SuperAdmin);
assert_eq!(Role::from_str("bot_owner"), Role::BotOwner);
assert_eq!(Role::from_str("unknown"), Role::Anonymous);
}
#[test]
fn test_role_hierarchy() {
assert!(Role::SuperAdmin.is_at_least(&Role::Admin));
assert!(Role::Admin.is_at_least(&Role::Moderator));
assert!(Role::BotOwner.is_at_least(&Role::BotOperator));
assert!(Role::BotOperator.is_at_least(&Role::BotViewer));
assert!(!Role::User.is_at_least(&Role::Admin));
}
#[test]
fn test_authenticated_user_builder() {
let user = AuthenticatedUser::new(Uuid::new_v4(), "testuser".to_string())
.with_email("test@example.com")
.with_role(Role::Admin)
.with_metadata("key", "value");
assert_eq!(user.email, Some("test@example.com".to_string()));
assert!(user.has_role(&Role::Admin));
assert_eq!(user.metadata.get("key"), Some(&"value".to_string()));
}
#[test]
fn test_user_permissions() {
let admin = AuthenticatedUser::new(Uuid::new_v4(), "admin".to_string())
.with_role(Role::Admin);
assert!(admin.has_permission(&Permission::ManageUsers));
assert!(admin.has_permission(&Permission::Delete));
assert!(admin.is_admin());
let user = AuthenticatedUser::new(Uuid::new_v4(), "user".to_string());
assert!(user.has_permission(&Permission::Read));
assert!(!user.has_permission(&Permission::ManageUsers));
assert!(!user.is_admin());
}
#[test]
fn test_anonymous_user() {
let anon = AuthenticatedUser::anonymous();
assert!(!anon.is_authenticated());
assert!(anon.has_role(&Role::Anonymous));
assert!(!anon.has_permission(&Permission::Read));
}
#[test]
fn test_service_user() {
let service = AuthenticatedUser::service("scheduler");
assert!(service.has_role(&Role::Service));
assert!(service.has_permission(&Permission::ExecuteTasks));
}
#[test]
fn test_bot_user() {
let bot_id = Uuid::new_v4();
let bot = AuthenticatedUser::bot_user(bot_id, "test-bot");
assert!(bot.is_bot());
assert!(bot.has_permission(&Permission::SendMessages));
assert_eq!(bot.current_bot_id, Some(bot_id));
}
#[test]
fn test_auth_config_paths() {
let config = AuthConfig::default();
assert!(config.is_anonymous_allowed("/health"));
assert!(config.is_anonymous_allowed("/api/health"));
assert!(!config.is_anonymous_allowed("/api/users"));
assert!(config.is_public_path("/static"));
assert!(config.is_public_path("/static/css/style.css"));
assert!(!config.is_public_path("/api/private"));
}
#[test]
fn test_auth_error_responses() {
assert_eq!(AuthError::MissingToken.status_code(), StatusCode::UNAUTHORIZED);
assert_eq!(AuthError::InsufficientPermissions.status_code(), StatusCode::FORBIDDEN);
assert_eq!(AuthError::RateLimited.status_code(), StatusCode::TOO_MANY_REQUESTS);
assert_eq!(AuthError::BotAccessDenied.status_code(), StatusCode::FORBIDDEN);
}
#[test]
fn test_bot_access() {
let bot_id = Uuid::new_v4();
let other_bot_id = Uuid::new_v4();
let user = AuthenticatedUser::new(Uuid::new_v4(), "user".to_string())
.with_bot_access(BotAccess::viewer(bot_id));
assert!(user.can_access_bot(&bot_id));
assert!(user.can_view_bot(&bot_id));
assert!(!user.can_operate_bot(&bot_id));
assert!(!user.can_manage_bot(&bot_id));
assert!(!user.can_access_bot(&other_bot_id));
let admin = AuthenticatedUser::new(Uuid::new_v4(), "admin".to_string())
.with_role(Role::Admin);
assert!(admin.can_access_bot(&bot_id));
assert!(admin.can_access_bot(&other_bot_id));
}
#[test]
fn test_bot_owner_access() {
let bot_id = Uuid::new_v4();
let owner = AuthenticatedUser::new(Uuid::new_v4(), "owner".to_string())
.with_bot_access(BotAccess::owner(bot_id));
assert!(owner.can_access_bot(&bot_id));
assert!(owner.can_view_bot(&bot_id));
assert!(owner.can_operate_bot(&bot_id));
assert!(owner.can_manage_bot(&bot_id));
}
#[test]
fn test_bot_operator_access() {
let bot_id = Uuid::new_v4();
let operator = AuthenticatedUser::new(Uuid::new_v4(), "operator".to_string())
.with_bot_access(BotAccess::operator(bot_id));
assert!(operator.can_access_bot(&bot_id));
assert!(operator.can_view_bot(&bot_id));
assert!(operator.can_operate_bot(&bot_id));
assert!(!operator.can_manage_bot(&bot_id));
}
#[test]
fn test_bot_permission_check() {
let bot_id = Uuid::new_v4();
let operator = AuthenticatedUser::new(Uuid::new_v4(), "operator".to_string())
.with_bot_access(BotAccess::operator(bot_id));
assert!(operator.has_bot_permission(&bot_id, &Permission::SendMessages));
assert!(operator.has_bot_permission(&bot_id, &Permission::ViewAnalytics));
assert!(!operator.has_bot_permission(&bot_id, &Permission::ManageBots));
}
#[test]
fn test_bot_access_expiry() {
let bot_id = Uuid::new_v4();
let past_time = chrono::Utc::now().timestamp() - 3600;
let expired_access = BotAccess::viewer(bot_id).with_expiry(past_time);
assert!(expired_access.is_expired());
assert!(!expired_access.is_valid());
let future_time = chrono::Utc::now().timestamp() + 3600;
let valid_access = BotAccess::viewer(bot_id).with_expiry(future_time);
assert!(!valid_access.is_expired());
assert!(valid_access.is_valid());
}
#[test]
fn test_accessible_bot_ids() {
let bot1 = Uuid::new_v4();
let bot2 = Uuid::new_v4();
let user = AuthenticatedUser::new(Uuid::new_v4(), "user".to_string())
.with_bot_access(BotAccess::owner(bot1))
.with_bot_access(BotAccess::viewer(bot2));
let accessible = user.accessible_bot_ids();
assert_eq!(accessible.len(), 2);
assert!(accessible.contains(&bot1));
assert!(accessible.contains(&bot2));
let owned = user.owned_bot_ids();
assert_eq!(owned.len(), 1);
assert!(owned.contains(&bot1));
}
#[test]
fn test_organization_access() {
let org_id = Uuid::new_v4();
let other_org_id = Uuid::new_v4();
let user = AuthenticatedUser::new(Uuid::new_v4(), "user".to_string())
.with_organization(org_id);
assert!(user.can_access_organization(&org_id));
assert!(!user.can_access_organization(&other_org_id));
}
#[test]
fn test_has_any_permission() {
let user = AuthenticatedUser::new(Uuid::new_v4(), "user".to_string());
assert!(user.has_any_permission(&[Permission::Read, Permission::Write]));
assert!(!user.has_any_permission(&[Permission::Delete, Permission::Admin]));
}
#[test]
fn test_has_all_permissions() {
let admin = AuthenticatedUser::new(Uuid::new_v4(), "admin".to_string())
.with_role(Role::Admin);
assert!(admin.has_all_permissions(&[Permission::Read, Permission::Write, Permission::Delete]));
assert!(!admin.has_all_permissions(&[Permission::ManageSecrets]));
}
#[test]
fn test_highest_role() {
let user = AuthenticatedUser::new(Uuid::new_v4(), "user".to_string())
.with_role(Role::Admin)
.with_role(Role::Moderator);
assert_eq!(user.highest_role(), &Role::Admin);
}
}