rustzen-admin Series (Part 2): Permission System Architecture - Complete Implementation of Declarative Permission System Based on Axum


This article introduces a modern permission system design and implementation based on the Rust Axum framework, focusing on how to build a high-performance, maintainable permission control system through declarative APIs, intelligent caching, and middleware architecture.


I. Introduction: Why Design a Unified Permission System?

In modern web application development, permission control is an unavoidable core requirement. As business complexity grows, we face the following challenges:

πŸ” Pain Points of Traditional Permission Control

Scattered Permission Checks: Each API endpoint requires manual permission checking code

// ❌ Traditional approach: Every handler needs repeated permission checks
async fn user_list_handler(current_user: CurrentUser) -> Result<Json<Vec<User>>, AppError> {
    if !current_user.has_permission("system:user:list") {
        return Err(AppError::PermissionDenied);
    }
    // Business logic...
}

Maintenance Difficulties: Permission logic scattered everywhere, hard to manage and debug uniformly

Performance Issues: Every request requires database queries to fetch permission information

Security Risks: Easy to miss permission checks or have inconsistent permission logic

🎯 Our Solution

Design a centralized, declarative, high-performance permission system that implements:


II. Design Goals and Principles

🎯 Core Goals of Permission System

  1. Centralized Permission Declaration: Permission binding completed during route registration phase, clear at a glance
  2. Unified Middleware Validation: All permission validation logic centralized in middleware layer
  3. Automatic User Information Injection: No need for manual user identity handling
  4. Cache Optimization: Avoid repeated queries, support intelligent refresh

βœ… Design Principles

Minimal Coupling: Complete separation of Authentication and Authorization

// Authentication middleware: Only responsible for identity verification
pub async fn auth_middleware(/* ... */) -> Result<Response, AppError>

// Permission middleware: Only responsible for permission checking
async fn permission_middleware(/* ... */) -> Result<Response, AppError>

Extensibility: Support single permission, any permission, all permissions and other combination modes

High Performance: In-memory caching + expiration refresh, minimize database access

Clean and Readable: Developer-friendly declarative API


III. Permission System Architecture Overview

πŸ”„ Complete Request Processing Flow

graph TD
    A[HTTP Request] --> B[Auth Middleware]
    B --> C{JWT Verification}
    C -->|Failed| D[401 Unauthorized]
    C -->|Success| E[Inject CurrentUser]
    E --> F[Permission Middleware]
    F --> G{Permission Cache Check}
    G -->|Cache Hit| H[Permission Validation]
    G -->|Cache Miss| I[Database Query]
    I --> J[Update Cache]
    J --> H
    H -->|Insufficient Permission| K[403 Forbidden]
    H -->|Permission Granted| L[Execute Business Handler]

πŸ—οΈ Module Architecture Design

ModuleFileResponsibility
JWT Authenticationcore/jwt.rsToken generation, verification, Claims parsing
User Extractionauth/extractor.rsCurrentUser structure definition and extraction logic
Auth Middlewareauth/middleware.rsJWT verification, user information injection
Permission Cacheauth/permission.rsPermission cache management, permission validation logic
Router Extensioncommon/router_ext.rsDeclarative permission binding API

IV. Core Module Details

4.1 JWT Authentication Module

Design Philosophy: JWT only handles identity recognition, does not carry permission information

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims {
    pub user_id: i64,      // User unique identifier
    pub username: String,  // Username
    pub exp: usize,        // Expiration time
    pub iat: usize,        // Issued at time
}

Key Features:

pub fn verify_token(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
    let validation = Validation::new(Algorithm::HS256);
    let token_data = decode::<Claims>(
        token,
        &DecodingKey::from_secret(JWT_CONFIG.secret.as_bytes()),
        &validation,
    )?;
    Ok(token_data.claims)
}

4.2 User Information Extractor CurrentUser

Design Highlight: Implements Axum’s FromRequestParts, supports dependency injection

πŸ“‹ CurrentUser vs Claims Semantic Differences

ConceptPurposeLifecycleInformation Contained
ClaimsJWT Token payload dataToken validity periodBasic identity information
CurrentUserBusiness layer user abstractionSingle requestVerified user information
// Claims: Raw data after JWT parsing, only used for authentication
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims {
    pub user_id: i64,
    pub username: String,
    pub exp: usize,
    pub iat: usize,
}

// CurrentUser: Unified business layer user structure for dependency injection
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CurrentUser {
    pub user_id: i64,
    pub username: String,
}

// 🎯 Key: Implement FromRequestParts for automatic extraction
impl<S> FromRequestParts<S> for CurrentUser
where S: Send + Sync,
{
    type Rejection = AppError;

    fn from_request_parts(/* ... */) -> impl Future<Output = Result<Self, Self::Rejection>> {
        // Get user information from request extensions
        // Data comes from authentication middleware parsing Claims and injecting information
    }
}

Design Principles:

Usage:

// βœ… Extract current user information
async fn user_profile_handler(current_user: CurrentUser) -> Json<UserProfile> {
    // current_user automatically injected, contains user_id and username
}

// πŸ”§ Uniformly use CurrentUser to extract user information
async fn get_user_info_handler(
    current_user: CurrentUser,  // Automatically inject current user information
    State(pool): State<PgPool>,
) -> AppResult<Json<ApiResponse<UserInfoResponse>>> {
    let user_info = AuthService::get_user_info(&pool, current_user.user_id, &current_user.username).await?;
    Ok(ApiResponse::success(user_info))
}

4.3 Intelligent Permission Caching Mechanism

Core Design: In-memory cache + expiration refresh + thread safety

/// Permission cache entry with expiration time
#[derive(Debug, Clone)]
pub struct UserPermissionCache {
    pub permissions: HashSet<String>,  // User permission set
    pub cached_at: DateTime<Utc>,      // Cache time
}

/// Global cache manager, thread-safe
pub struct PermissionCacheManager {
    cache: Arc<RwLock<HashMap<i64, UserPermissionCache>>>,
}

Intelligent Refresh Strategy:

pub async fn get_cached_permissions(
    pool: &PgPool,
    user_id: i64,
) -> Result<Option<UserPermissionCache>, ServiceError> {
    if let Some(cache) = PERMISSION_CACHE.get(user_id) {
        if cache.is_expired() {
            // πŸ”„ Cache expired, automatically refresh from database
            let new_cache = Self::load_user_permissions_from_db(pool, user_id).await?;
            return Ok(Some(new_cache));
        }
        return Ok(Some(cache));
    }
    Ok(None)
}

Performance Optimization:


V. Declarative Route Permission Binding Design

5.1 RouterExt Trait Design

Core Innovation: Extend Axum Router to support permission declaration

pub trait RouterExt<S> {
    fn route_with_permission(
        self,
        path: &str,
        method_router: MethodRouter<S>,
        permissions_check: PermissionsCheck,  // 🎯 Core: Permission check configuration
    ) -> Self;
}

Implementation Principle:

impl RouterExt<PgPool> for Router<PgPool> {
    fn route_with_permission(self, path: &str, method_router: MethodRouter<PgPool>, permissions_check: PermissionsCheck) -> Self {
        self.route(
            path,
            method_router.layer(axum::middleware::from_fn(move |req: Request, next: Next| {
                let permissions_check = permissions_check.clone();
                async move { permission_middleware(req, next, permissions_check).await }
            })),
        )
    }
}

5.2 Flexible Permission Check Modes

PermissionsCheck Enum: Support multiple permission combination logic

#[derive(Debug, Clone)]
pub enum PermissionsCheck {
    Single(&'static str),           // Single permission
    Any(Vec<&'static str>),        // Any permission (OR logic)
    All(Vec<&'static str>),        // All permissions (AND logic)
}

Usage Examples:

// πŸ”Ή Single permission check
router.route_with_permission(
    "/system/users",
    get(user_list_handler),
    PermissionsCheck::Single("system:user:list")
)

// πŸ”Ή Any permission check (admin or user admin can access)
router.route_with_permission(
    "/system/users",
    post(create_user_handler),
    PermissionsCheck::Any(vec!["admin:all", "system:user:create"])
)

// πŸ”Ή All permissions check (requires delete permission AND confirm permission)
router.route_with_permission(
    "/system/users/{id}",
    delete(delete_user_handler),
    PermissionsCheck::All(vec!["system:user:delete", "system:confirm"])
)

5.3 Permission Middleware Core Logic

Unified Permission Validation Flow:

async fn permission_middleware(
    request: Request,
    next: Next,
    permissions_check: PermissionsCheck,
) -> Result<Response, AppError> {
    // 1️⃣ Get current user (injected by auth middleware)
    let current_user = request.extensions().get::<CurrentUser>()
        .ok_or(AppError::from(ServiceError::InvalidCredentials))?;

    // 2️⃣ Get database connection pool
    let pool = request.extensions().get::<PgPool>()
        .ok_or(AppError::from(ServiceError::DatabaseQueryFailed))?;

    // 3️⃣ Check permissions (cache first)
    let has_permission = PermissionService::check_permissions(
        &pool,
        current_user.user_id,
        &permissions_check
    ).await?;

    // 4️⃣ Deny access if insufficient permissions
    if !has_permission {
        return Err(AppError::from(ServiceError::PermissionDenied));
    }

    // 5️⃣ Permission granted, continue execution
    Ok(next.run(request).await)
}

VI. Permission Cache and JWT Collaboration Mechanism

6.1 Dual-Layer Cache Design

Cache TypeStorage LocationExpiration TimeRefresh MechanismPurpose
JWT TokenClient1-2 hoursRe-login on expirationIdentity authentication
Permission CacheServer memory1 hourAuto refreshPermission validation

6.2 Cache Collaboration Flow

Cache Permissions on Login:

// Login triggers permission caching
pub async fn login(pool: &PgPool, request: LoginRequest) -> Result<LoginResponse, ServiceError> {
    let user = Self::verify_login(pool, &request.username, &request.password).await?;
    let token = jwt::generate_token(user.id, &user.username)?;

    // Getting user info automatically caches permissions
    let user_info = Self::get_user_info(pool, user.id, &user.username).await?;
    Ok(LoginResponse { token, user_info })
}

// 🎯 Key location for permission caching
pub async fn get_user_info(pool: &PgPool, user_id: i64, username: &str) -> Result<UserInfoResponse, ServiceError> {
    // Query user permissions
    let permissions = UserRepository::get_user_permissions(pool, user_id).await?;

    // 🎯 Cache permissions to memory (1 hour validity)
    PermissionService::cache_user_permissions(user_id, permissions);

    // Return user info...
}

Cache Strategy for Permission Checking:

pub async fn check_permissions(
    pool: &PgPool,
    user_id: i64,
    permissions_check: &PermissionsCheck,
) -> Result<bool, ServiceError> {
    // πŸ” Try to get permissions from cache
    if let Some(cache) = Self::get_cached_permissions(pool, user_id).await? {
        return Ok(permissions_check.check(&cache.permissions));
    }

    // 🚫 Cache miss, require re-authentication
    // Design rationale: This shouldn't happen in normal business flow,
    // if it occurs, it indicates system anomaly, deny access for security
    // Supports logout after user prohibition/deletion
    Err(ServiceError::InvalidCredentials)
}

6.3 Cache Invalidation and Security

Key Scenarios for Active Cache Clearing:

// 1️⃣ Clear permission cache on logout
async fn logout_handler(current_user: CurrentUser) -> AppResult<Json<ApiResponse<()>>> {
    PermissionService::clear_user_cache(current_user.user_id);
    Ok(ApiResponse::success(()))
}

// 2️⃣ Clear cache when deleting user
pub async fn delete_user(pool: &PgPool, id: i64) -> Result<(), ServiceError> {
    let deleted = UserRepository::soft_delete(pool, id).await?;
    if !deleted {
        return Err(ServiceError::NotFound("User not found".to_string()));
    }

    // 🎯 Key: Clear user's permission cache when deleting user
    PermissionService::clear_user_cache(id);
    tracing::info!("Deleted user {} and cleared associated cache", id);

    Ok(())
}

// 3️⃣ Clear cache when user roles change
pub async fn update_user(/* ... */) -> Result<UserResponse, ServiceError> {
    // When updating user roles
    if let Some(role_ids) = request.role_ids {
        UserRepository::set_user_roles(pool, updated_user.id, &role_ids).await?;

        // 🎯 Clear permission cache after role change, force reload
        PermissionService::clear_user_cache(updated_user.id);
        tracing::info!("Updated user {} roles and cleared permission cache", updated_user.id);
    }

    // When user status changes (especially disabling user)
    if let Some(new_status) = request.status {
        if new_status != user.status {
            PermissionService::clear_user_cache(updated_user.id);
            tracing::info!("Updated user {} status and cleared permission cache", updated_user.id);
        }
    }
}

Cache Security Strategy:


VII. Practical Usage Examples

7.1 System Management Module Route Configuration

pub fn system_routes() -> Router<PgPool> {
    Router::new()
        // User management
        .route_with_permission(
            "/users",
            get(user_list_handler),
            PermissionsCheck::Single("system:user:list")
        )
        .route_with_permission(
            "/users",
            post(create_user_handler),
            PermissionsCheck::Single("system:user:create")
        )
        .route_with_permission(
            "/users/{id}",
            put(update_user_handler),
            PermissionsCheck::Single("system:user:update")
        )
        .route_with_permission(
            "/users/{id}",
            delete(delete_user_handler),
            PermissionsCheck::All(vec!["system:user:delete", "system:confirm"])
        )

        // Role management
        .route_with_permission(
            "/roles",
            get(role_list_handler),
            PermissionsCheck::Any(vec!["system:role:list", "admin:all"])
        )
}

7.2 Handler Implementation

// βœ… Handler focuses on business logic, no need to worry about permission checks
async fn user_list_handler(
    current_user: CurrentUser,  // Automatically injected current user
    State(pool): State<PgPool>,
) -> AppResult<Json<ApiResponse<Vec<UserResponse>>>> {
    // Permission already checked at middleware layer, handle business logic directly here
    let users = UserService::list_users(&pool).await?;
    Ok(ApiResponse::success(users))
}

async fn delete_user_handler(
    current_user: CurrentUser,
    State(pool): State<PgPool>,
    Path(user_id): Path<i64>,
) -> AppResult<Json<ApiResponse<()>>> {
    // Permission check: requires system:user:delete AND system:confirm
    // Already declared at route layer, middleware handles automatically
    UserService::delete_user(&pool, user_id).await?;
    Ok(ApiResponse::success(()))
}

VIII. Summary

🎯 Multiple Benefits from Core Design

Declarative Permission Binding Design:

Intelligent Caching Mechanism:

Unified Middleware Processing:

πŸ“Š Overall Effect

Design FeatureDevelopment EfficiencyPerformance OptimizationSecurity AssuranceMaintainabilityExtensibility
Declarative APIβœ…-βœ…βœ…βœ…
Intelligent Cache-βœ…βœ…-βœ…
Unified Middlewareβœ…-βœ…βœ…-
Modular Designβœ…--βœ…βœ…

Complete Integration Example: main.rs


What permission patterns do you use in your Axum applications? I’d love to hear about your experiences in the comments below!