praetor 0.1.0

A small, strongly typed authorization library for Rust built around composable can and cannot rules.
Documentation
  • Coverage
  • 100%
    14 out of 14 items documented3 out of 3 items with examples
  • Size
  • Source code size: 830.7 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 627.9 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 4s Average build duration of successful builds.
  • all releases: 5s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • Homepage
  • chrisfrazier0/praetor
    0 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • chrisfrazier0

Praetor

Praetor is a small, strongly typed authorization library for Rust built around composable can and cannot rules.

crates.io docs.rs license

Praetor defines authorization mechanics, not your application's authorization model. Actions, subjects, claims, roles, permissions, and JWT formats all stay in your application. Praetor just composes rules, matches them, and returns a decision.

claims + rules + action + subject/resource  ->  allow / deny

Install

[dependencies]
praetor = "0.1"

Praetor targets Rust 1.88+.

Quick start

use praetor::{Ability, AbilitySubject};

#[derive(Clone, Copy, PartialEq, Eq)]
enum Action { Read, Update }

#[derive(Clone, Copy, PartialEq, Eq)]
enum Subject { Message }

struct Claims { user_id: u64 }

struct Message { author_id: u64, deleted: bool }

impl AbilitySubject<Subject> for Message {
    fn subject_type(&self) -> Subject { Subject::Message }
}

let ability = Ability::builder(Claims { user_id: 1 })
    .can(Action::Read, Subject::Message)
    .can_if(Action::Update, Subject::Message, |claims, m: &Message| {
        claims.user_id == m.author_id
    })
    .cannot_if(Action::Read, Subject::Message, |_, m: &Message| m.deleted)
    .build();

let mine = Message { author_id: 1, deleted: false };

// Subject-level check (no instance).
assert!(ability.can(Action::Read, Subject::Message));

// Instance-level check (predicate runs against the resource).
assert!(ability.can(Action::Update, &mine));

// `authorize` maps the same decision to a `Result`.
assert!(ability.authorize(Action::Update, &mine).is_ok());

How it works

A single can / authorize interface accepts either a logical subject (Subject::Message) or a concrete resource (&message). A resource type opts in by implementing AbilitySubject, which maps it to its logical subject.

  • Unconditional rules (can / cannot) match on action and subject.
  • Conditional rules (can_if / cannot_if) additionally run a strongly typed closure Fn(&C, &R) -> bool against the resource instance. A conditional rule can only match when a concrete instance is supplied — a subject-only check never matches one.

Predicates are ordinary Rust closures. Heterogeneous resource types (Message, Room, ...) are stored in one ability via internal type erasure that is invisible to callers.

Evaluation semantics

  • Default deny — if no rule matches, access is denied.
  • Last matching rule wins — later rules override earlier ones, so a cannot_if after a can can carve out an exception.
let ability = Ability::builder(())
    .can(Action::Read, Subject::Message)
    .cannot_if(Action::Read, Subject::Message, |_, m: &Message| m.deleted)
    .build();

let deleted = Message { deleted: true };
assert!(ability.cannot(Action::Read, &deleted)); // later deny wins

What Praetor is not

Praetor deliberately does not provide:

  • JWT validation, OAuth/OIDC, or a claims schema
  • Built-in roles, permissions, or capability registries
  • A policy DSL, predicate AST, or JSON condition format
  • Serialized policies or remote/database-backed evaluation
  • Built-in Manage/wildcard semantics

Authorization is a pure, in-memory operation. Load whatever state you need first, then ask Praetor for a decision. Your application owns its model; Praetor owns the mechanics.

Links

License

Licensed under the MIT License.