Skip to content

Refactor authorization middleware to use Axum FromRequestParts extractor pattern #218

@jhodapp

Description

@jhodapp

Summary

Consider migrating the authorization layer from the custom Check trait pattern to Axum's native FromRequestParts extractor pattern for improved idiomatic alignment and code reuse.

Current Architecture

The codebase uses a custom authorization system in web/src/protect/mod.rs:

  • Check trait - Defines reusable authorization rules
  • Predicate struct - Pairs a Check with arguments
  • authorize() function - Runs predicates via from_fn_with_state middleware

Example checks: UserInOrganization, UserIsAdmin, UserCanAccessCoachingSession

Proposed Architecture

Migrate to Axum's FromRequestParts extractor pattern, which:

  1. Is Axum's core abstraction - Extractors are the primary way Axum handles request processing
  2. Enables dual-use - Same type works as middleware (via from_extractor) AND directly in handlers
  3. Provides type-safe context - Extractors can carry validated data to handlers
  4. Reduces redundancy - No need to re-extract path parameters in handlers after middleware validates them

Example Migration

Before (Check trait):

// In protect/users/actions.rs
pub(crate) async fn index(
    State(app_state): State<AppState>,
    AuthenticatedUser(user): AuthenticatedUser,
    Path(user_id): Path<Id>,
    request: Request,
    next: Next,
) -> impl IntoResponse {
    let checks = vec![Predicate::new(UserCanAccessUserData, vec![user_id])];
    authorize(&app_state, user, request, next, checks).await
}

// In controller - must re-extract user_id
pub async fn index(
    Path(user_id): Path<Id>,
    // ...
)

After (Extractor):

// New extractor in extractors/user_data_access.rs
pub struct UserDataAccess {
    pub target_user_id: Id,
    pub authenticated_user: domain::users::Model,
}

impl<S> FromRequestParts<S> for UserDataAccess { /* ... */ }

// In controller - authorization AND extraction in one step
pub async fn index(
    UserDataAccess { target_user_id, .. }: UserDataAccess,
    // ...
)

// Or as middleware
.route_layer(from_extractor::<UserDataAccess>())

Benefits

Aspect Current (Check) Proposed (Extractor)
Axum alignment Custom abstraction Native Axum pattern
Code reuse Middleware only Middleware + handlers
Type safety Manual arg passing Compile-time checked
Documentation Custom docs needed Axum docs apply

Implementation Considerations

  1. State access - Extractors need AppState via Extension layer rather than State. This requires adding .layer(Extension(app_state.clone())) to the router.

  2. Gradual migration - Can migrate one authorization check at a time; both patterns can coexist.

  3. Testing - Extractors are easier to unit test in isolation.

References

Scope

This is a refactoring effort that could be done incrementally. Priority candidates for migration:

  • UserCanAccessCoachingSessionCoachingSessionAccess extractor
  • UserInOrganizationOrganizationMemberAccess extractor
  • UserIsAdminAdminAccess extractor
  • New UserCanAccessUserDataUserDataAccess extractor

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions