gatehouse 0.2.0

A flexible authorization library that combines role-based (RBAC), attribute-based (ABAC), and relationship-based (ReBAC) access control policies.
Documentation
use actix_web::{http::StatusCode, test, web, App};
use uuid::Uuid;

mod actix_example {
    #![allow(dead_code)]
    include!(concat!(
        env!("CARGO_MANIFEST_DIR"),
        "/examples/actix_web.rs"
    ));
}

macro_rules! init_actix_app {
    () => {{
        let checker = web::Data::new(actix_example::build_permission_checker());
        test::init_service(
            App::new()
                .app_data(checker.clone())
                .route("/posts/{id}", web::put().to(actix_example::edit_post))
                .route(
                    "/posts/{id}/publish",
                    web::post().to(actix_example::publish_post),
                )
                .route("/posts/{id}", web::get().to(actix_example::view_post)),
        )
    }};
}

fn author_id() -> Uuid {
    Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap()
}

#[actix_web::test]
async fn edit_post_allows_author() {
    let app = init_actix_app!().await;
    let post_id = author_id();

    let req = test::TestRequest::put()
        .uri(&format!("/posts/{post_id}"))
        .insert_header(("x-user-id", post_id.to_string()))
        .insert_header(("x-roles", "author"))
        .to_request();

    let resp = test::call_service(&app, req).await;
    assert_eq!(resp.status(), StatusCode::OK);
}

#[actix_web::test]
async fn edit_post_denies_non_author() {
    let app = init_actix_app!().await;
    let post_id = author_id();

    let req = test::TestRequest::put()
        .uri(&format!("/posts/{post_id}"))
        .insert_header(("x-user-id", Uuid::new_v4().to_string()))
        .insert_header(("x-roles", "author"))
        .to_request();

    let resp = test::call_service(&app, req).await;
    assert_eq!(resp.status(), StatusCode::FORBIDDEN);
}

#[actix_web::test]
async fn edit_post_denies_locked_post() {
    let app = init_actix_app!().await;
    let post_id = author_id();

    let req = test::TestRequest::put()
        .uri(&format!("/posts/{post_id}"))
        .insert_header(("x-user-id", post_id.to_string()))
        .insert_header(("x-roles", "author"))
        .insert_header(("x-post-locked", "true"))
        .to_request();

    let resp = test::call_service(&app, req).await;
    assert_eq!(resp.status(), StatusCode::FORBIDDEN);
}

#[actix_web::test]
async fn publish_post_allows_editor() {
    let app = init_actix_app!().await;
    let post_id = author_id();

    let req = test::TestRequest::post()
        .uri(&format!("/posts/{post_id}/publish"))
        .insert_header(("x-user-id", Uuid::new_v4().to_string()))
        .insert_header(("x-roles", "editor"))
        .to_request();

    let resp = test::call_service(&app, req).await;
    assert_eq!(resp.status(), StatusCode::OK);
}

#[actix_web::test]
async fn publish_post_denies_non_editor() {
    let app = init_actix_app!().await;
    let post_id = author_id();

    let req = test::TestRequest::post()
        .uri(&format!("/posts/{post_id}/publish"))
        .insert_header(("x-user-id", Uuid::new_v4().to_string()))
        .insert_header(("x-roles", "author"))
        .to_request();

    let resp = test::call_service(&app, req).await;
    assert_eq!(resp.status(), StatusCode::FORBIDDEN);
}

#[actix_web::test]
async fn publish_post_denies_locked_post() {
    let app = init_actix_app!().await;
    let post_id = author_id();

    let req = test::TestRequest::post()
        .uri(&format!("/posts/{post_id}/publish"))
        .insert_header(("x-user-id", Uuid::new_v4().to_string()))
        .insert_header(("x-roles", "editor"))
        .insert_header(("x-post-locked", "true"))
        .to_request();

    let resp = test::call_service(&app, req).await;
    assert_eq!(resp.status(), StatusCode::FORBIDDEN);
}

#[actix_web::test]
async fn view_post_allows_published_post() {
    let app = init_actix_app!().await;
    let post_id = author_id();

    let req = test::TestRequest::get()
        .uri(&format!("/posts/{post_id}"))
        .to_request();

    let resp = test::call_service(&app, req).await;
    assert_eq!(resp.status(), StatusCode::OK);
}

#[actix_web::test]
async fn view_post_denies_unpublished_anonymous() {
    let app = init_actix_app!().await;
    let post_id = author_id();

    let req = test::TestRequest::get()
        .uri(&format!("/posts/{post_id}"))
        .insert_header(("x-post-published", "false"))
        .to_request();

    let resp = test::call_service(&app, req).await;
    assert_eq!(resp.status(), StatusCode::FORBIDDEN);
}

#[actix_web::test]
async fn view_post_allows_author_on_unpublished_post() {
    let app = init_actix_app!().await;
    let post_id = author_id();

    let req = test::TestRequest::get()
        .uri(&format!("/posts/{post_id}"))
        .insert_header(("x-user-id", post_id.to_string()))
        .insert_header(("x-roles", "author"))
        .insert_header(("x-post-published", "false"))
        .to_request();

    let resp = test::call_service(&app, req).await;
    assert_eq!(resp.status(), StatusCode::OK);
}