webgates-core 1.0.0

Core domain types, permission system, and authorization building blocks for webgates.
Documentation

webgates-core

User-focused Rust building blocks for authentication and authorization.

webgates-core is the smallest crate in the webgates ecosystem. It gives you the domain model and authorization primitives without pulling in HTTP, cookies, JWT handling, sessions, database adapters, or framework integrations.

If you want to understand how webgates works at its foundation, or you want to build your own adapter around another framework, this is the crate to start with.

Who this crate is for

Use webgates-core when you want to:

  • model users as accounts with roles, groups, and direct permissions
  • make authorization decisions in pure Rust domain code
  • build your own authentication or authorization services on top of the core types
  • integrate with a framework that does not yet have a webgates adapter
  • keep dependencies minimal and avoid transport-specific concerns

If you are building a typical web application and want batteries included, you will usually want webgates instead.

What you learn in this crate

Most developers only need to internalize a small core loop:

  • Account represents the current user and their assigned capabilities
  • AccessPolicy expresses what a protected action requires
  • AuthorizationService evaluates whether the account satisfies that policy
  • Permissions add fine-grained capability checks when roles or groups are too broad
  • Credentials and CredentialsVerifier define the authentication boundary without committing you to one transport or backend

What this crate does not do

This crate intentionally does not include:

  • HTTP middleware
  • framework integrations
  • JWT encoding or decoding
  • cookies
  • session issuance or renewal
  • password hashing implementations
  • repository implementations

Those concerns live in sibling crates such as webgates, webgates-axum, webgates-codecs, webgates-repositories, webgates-secrets, and webgates-sessions.

Install

Add this to your Cargo.toml:

[dependencies]
webgates-core = "1.0.0"

Minimum supported Rust version: 1.91.

The mental model

If you are onboarding to the crate, this is the simplest way to think about it:

  1. You represent the current user as an Account.
  2. You express access requirements as an AccessPolicy.
  3. You ask AuthorizationService whether the account satisfies that policy.
  4. You optionally use Credentials and CredentialsVerifier to plug in your own login flow.

That is the core loop.

Quick start

This example shows the main flow you will use in application code.

use webgates_core::accounts::Account;
use webgates_core::authz::access_policy::AccessPolicy;
use webgates_core::authz::authorization_service::AuthorizationService;
use webgates_core::groups::Group;
use webgates_core::roles::Role;

let account = Account::<Role, Group>::new("user-123")
    .with_roles(vec![Role::Admin])
    .with_groups(vec![Group::new("engineering")]);

let policy = AccessPolicy::<Role, Group>::require_role(Role::Admin)
    .or_require_group(Group::new("support-override"));

let authz = AuthorizationService::new(policy);

assert!(authz.is_authorized(&account));

Core concepts

1. Accounts represent the current user

Account<R, G> is the central domain type. It stores:

  • a generated account_id
  • your application-level user_id
  • assigned roles
  • assigned groups
  • direct permissions

Example:

use webgates_core::accounts::Account;
use webgates_core::groups::Group;
use webgates_core::permissions::Permissions;
use webgates_core::roles::Role;

let permissions: Permissions = ["projects:read", "projects:write"].into_iter().collect();

let account = Account::<Role, Group>::new("alice@example.com")
    .with_roles(vec![Role::Moderator])
    .with_groups(vec![Group::new("engineering")])
    .with_permissions(permissions);

assert_eq!(account.user_id, "alice@example.com");
assert!(account.has_role(&Role::Moderator));
assert!(account.is_member_of(&Group::new("engineering")));

A useful onboarding detail: Account::new(...) automatically assigns the default role for your role type. With the built-in Role enum, that default is Role::User.

2. Roles model hierarchical privilege

The built-in Role type is ordered from least privileged to most privileged:

  • Role::User
  • Role::Reporter
  • Role::Moderator
  • Role::Admin

That ordering matters when you use require_role_or_supervisor(...).

Example:

use webgates_core::authz::access_policy::AccessPolicy;
use webgates_core::groups::Group;
use webgates_core::roles::Role;

let exact = AccessPolicy::<Role, Group>::require_role(Role::Moderator);
let hierarchical = AccessPolicy::<Role, Group>::require_role_or_supervisor(Role::Moderator);

assert!(exact.has_requirements());
assert!(hierarchical.has_requirements());

If your application needs its own role hierarchy, define your own enum in least-to-most privileged order and implement AccessHierarchy for it.

3. Groups model exact membership

Groups are useful for non-hierarchical membership such as departments, tenants, teams, or project assignments.

use webgates_core::groups::Group;

let engineering = Group::new("engineering");
let billing = Group::new("billing");

assert_eq!(engineering.name(), "engineering");
assert_eq!(billing.name(), "billing");

Use groups when access depends on belonging to something, not on privilege level.

4. Permissions model fine-grained capabilities

Permissions are string-based capabilities such as projects:read or admin:users:delete.

webgates-core converts them into deterministic PermissionId values internally, so you can do fast checks without maintaining a central numeric registry.

use webgates_core::permissions::Permissions;
use webgates_core::permissions::permission_id::PermissionId;

let mut permissions = Permissions::new();
permissions
    .grant("projects:read")
    .grant(PermissionId::from("projects:write"));

assert!(permissions.has("projects:read"));
assert!(permissions.has_all(["projects:read", "projects:write"]));
assert!(!permissions.has("projects:delete"));

Use permissions when a role is too broad and you need feature-level control.

5. Policies declare what access requires

AccessPolicy<R, G> is how you describe who should be allowed through.

Important onboarding note: policy requirements use OR semantics. If any configured role, group, or permission requirement matches, authorization succeeds.

use webgates_core::authz::access_policy::AccessPolicy;
use webgates_core::groups::Group;
use webgates_core::roles::Role;

let policy = AccessPolicy::<Role, Group>::require_role(Role::Admin)
    .or_require_role_or_supervisor(Role::Moderator)
    .or_require_group(Group::new("security"))
    .or_require_permission("audit:read");

assert!(policy.has_requirements());

This is a good fit for rules like:

  • admins may enter
  • moderators and above may enter
  • members of a specific emergency group may enter
  • anybody with a dedicated override permission may enter

6. The authorization service evaluates the policy

AuthorizationService turns the policy into an authorization decision for a specific account.

use webgates_core::accounts::Account;
use webgates_core::authz::access_policy::AccessPolicy;
use webgates_core::authz::authorization_service::AuthorizationService;
use webgates_core::groups::Group;
use webgates_core::roles::Role;

let account = Account::<Role, Group>::new("bob")
    .with_roles(vec![Role::Reporter]);

let policy = AccessPolicy::<Role, Group>::require_role(Role::Admin)
    .or_require_role(Role::Reporter);

let service = AuthorizationService::new(policy);
assert!(service.is_authorized(&account));

A practical onboarding example

This example shows a realistic setup for an internal admin page.

use webgates_core::accounts::Account;
use webgates_core::authz::access_policy::AccessPolicy;
use webgates_core::authz::authorization_service::AuthorizationService;
use webgates_core::groups::Group;
use webgates_core::roles::Role;

let mut account = Account::<Role, Group>::new("carol@example.com")
    .with_roles(vec![Role::User])
    .with_groups(vec![Group::new("support")]);

account.grant_permission("tickets:read");
account.grant_permission("tickets:escalate");

let policy = AccessPolicy::<Role, Group>::require_role(Role::Admin)
    .or_require_group(Group::new("support"))
    .or_require_permission("tickets:escalate");

let authz = AuthorizationService::new(policy);

assert!(authz.is_authorized(&account));

Here access succeeds even though the user is not an admin, because the policy accepts any matching requirement and the account belongs to the support group.

Authentication boundary: credentials and verification

webgates-core does not implement password hashing or login services for you. Instead, it gives you a clean boundary:

  • Credentials<Id> holds user-supplied identifier + plaintext secret
  • CredentialsVerifier is the trait you implement to check those credentials against your own backend
use webgates_core::credentials::Credentials;

let credentials = Credentials::new(&"alice@example.com".to_string(), "correct horse battery staple");

assert_eq!(credentials.id, "alice@example.com");

This separation is useful because it keeps your domain model independent from:

  • which database you use
  • how you hash passwords
  • which framework handles the request
  • whether login happens over HTTP, gRPC, CLI, or background jobs

Validate permissions during tests

If your application defines many permission strings, validate them in tests so collisions or duplicate definitions are caught early.

use webgates_core::validate_permissions;

validate_permissions![
    "projects:read",
    "projects:write",
    "projects:delete",
    "admin:users:read",
    "admin:users:write",
    "admin:users:delete",
];

This is especially helpful when your permission surface grows over time or is shared across multiple modules.

Recommended onboarding path

If you are new to the crate, I recommend learning it in this order:

  1. accounts::Account
  2. roles::Role and groups::Group
  3. permissions::Permissions
  4. authz::access_policy::AccessPolicy
  5. authz::authorization_service::AuthorizationService
  6. credentials::Credentials and credentials_verifier::CredentialsVerifier

That sequence mirrors how most applications adopt the crate.

Which crate should you use?

Choose based on how much infrastructure you want out of the box:

  • use webgates-core when you only want domain types and authorization primitives
  • use webgates when you want the main user-facing composition crate
  • use webgates-axum when you want Axum integration
  • use webgates-sessions when you want framework-agnostic session lifecycle primitives
  • use webgates-codecs when you need JWT and codec support
  • use webgates-repositories when you want repository contracts and storage integrations
  • use webgates-secrets when you need hashing and secret handling helpers

Design goals

This crate is intentionally designed to be:

  • framework-agnostic
  • transport-agnostic
  • easy to test
  • small in dependency footprint
  • usable in server and WASM contexts
  • explicit about authorization behavior

Security notes

A few important things to keep in mind:

  • Credentials contains plaintext secrets and should be short-lived
  • do not log secrets, tokens, or raw credential data
  • use secure transport when credentials cross a process or network boundary
  • treat permission names as application API and keep them stable once adopted
  • validate your permission set in CI if your app depends heavily on string permissions

Related crates

  • webgates - the main composition crate for most applications
  • webgates-axum - Axum integration
  • webgates-codecs - JWT and codec support
  • webgates-repositories - repository traits and backends
  • webgates-secrets - secret and hashing helpers
  • webgates-sessions - session lifecycle and renewal primitives
  • webgates-tonic - tonic integration

License

MIT