SECURITY MODULES ADDED: - security/auth.rs: Full RBAC with roles (Anonymous, User, Moderator, Admin, SuperAdmin, Service, Bot, BotOwner, BotOperator, BotViewer) and permissions - security/cors.rs: Hardened CORS (no wildcard in production, env-based config) - security/panic_handler.rs: Panic catching middleware with safe 500 responses - security/path_guard.rs: Path traversal protection, null byte prevention - security/request_id.rs: UUID request tracking with correlation IDs - security/error_sanitizer.rs: Sensitive data redaction from responses - security/zitadel_auth.rs: Zitadel token introspection and role mapping - security/sql_guard.rs: SQL injection prevention with table whitelist - security/command_guard.rs: Command injection prevention - security/secrets.rs: Zeroizing secret management - security/validation.rs: Input validation utilities - security/rate_limiter.rs: Rate limiting with governor crate - security/headers.rs: Security headers (CSP, HSTS, X-Frame-Options) MAIN.RS UPDATES: - Replaced tower_http::cors::Any with hardened create_cors_layer() - Added panic handler middleware - Added request ID tracking middleware - Set global panic hook SECURITY STATUS: - 0 unwrap() in production code - 0 panic! in production code - 0 unsafe blocks - cargo audit: PASS (no vulnerabilities) - Estimated completion: ~98% Remaining: Wire auth middleware to handlers, audit logs for sensitive data
1316 lines
39 KiB
Rust
1316 lines
39 KiB
Rust
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);
|
|
}
|
|
}
|