nio-client 0.2.1

Client for nio authentication and authorization.
Documentation

nio-client

A Rust client library for the NIO Authorization Service, a high-performance, relationship-based authorization system. This library provides a gRPC client to interact with the service and includes extractors for easy integration with the axum framework (behind the axum feature).

Usage

See the examples directory: check, list, write, watch (plain client) and server (axum integration).

New to nio? Start with the webapp example — a self-contained learning app (sign-in, cookie-session UI, bearer-token API, session resolution, access checks) that runs against an in-process nio stand-in and logs every RPC, so no server is required:

task example-webapp   # then open http://127.0.0.1:8080/

Its README is a guided walkthrough of the nio model and the axum extractors.

Updating gRPC Code

The generated code is built by build.rs (tonic-build) from the proto files in proto. The original proto files track the nio server:

proto/iam.proto and proto/sessions.proto should match the nio server you deploy against so wire fields (e.g. ListResponse.ts, packed write zookies) are visible to this client.

Built-in namespaces and the admin gate

Constants mirror nio's domain crate and check bootstrap. Built-in namespaces are Namespace::iam() (iam) and Namespace::serviceaccount() (serviceaccount) only.

The singleton admin object is iam:root (Namespace::iam() + Obj::root()). Typical gate relations:

  • viewer path: Rel::iam_get() (iam.get)
  • admin path: Rel::iam_update() (iam.update), Rel::serviceaccount_create()

Roles that carry direct grants: Rel::admin(), Rel::editor(), Rel::viewer(). Public subject markers: UserId::all_users(), UserId::authenticated_users(). The pointer object/rel keyword is "..." (Obj::unspecified() / Rel::unspecified()).

Construction

check and nio-client (session) are always separate TCP endpoints. The library does not read environment variables; the process supplies targets, TLS config, and cache config.

use nio_client::{connect_channel, CheckClient};
use nio_client::session::{GrpcSessionResolver, ResolverConfig};

# async fn build() -> Result<(), Box<dyn std::error::Error>> {
// RPC only (check / list / read / write / expand / watch / …)
let check_client = CheckClient::create("http://localhost:50051".parse()?).await?;

// Session resolution for HTTP middleware (axum feature): a second channel
// to am.SessionService on nio-client.
let session_channel = connect_channel("http://localhost:50052".parse()?, None).await?;
let resolver = GrpcSessionResolver::new(session_channel, ResolverConfig::default());
# Ok(())
# }

With the axum feature, axum::AuthState::new(check_client, resolver, prefix) wires both into the WithPrincipal / WithOptPrincipal / Authenticated extractors. Sign-in redirects go to {prefix}/signin?back={original-uri}.

All channels enable HTTP/2 keepalive (30s / 10s / while idle — nio #239). CheckClient::create_with_tls / connect_channel(uri, Some(tls)) take a tonic::transport::ClientTlsConfig for (m)TLS.

Session resolution

Opaque session tokens are resolved via am.SessionService on nio-client (issue #243/#245). The axum extractors hash the cookie or bearer token (sha256, hex — the raw token never leaves the process), resolve it, and send the principal UUID to check. Unknown / expired / revoked tokens redirect to signin with zero check RPCs.

The resolver caches positives (LRU, TTL with downward-only jitter), tombstones unknown tokens, coalesces concurrent misses, refreshes hot entries ahead of expiry, and can optionally serve stale entries during transport errors (ResolverConfig::stale_if_error).

Zookies (timestamps)

Check/list/write use opaque packed zookies (standard Base64 of 7 bytes: [epoch:u8][millis:u48 BE]). Treat them as opaque: store and echo only.

  • Timestamp::empty() (AQAAAAAAAA==) — no fresher-than constraint; server picks a snapshot
  • Write helpers (add_one, add_many, add_parent, delete_one, write) return the commit zookie
  • list / read / expand return the evaluation snapshot zookie in their results
  • Pass a zookie into check / list / read_with_timestamp for read-your-writes
let ts = client.add_one(tuple).await?;
// ...
let res = client.check(ns, obj, rel, user_id, Some(ts)).await?;

write(add, del, precondition) supports atomic multi-tuple commits and an optional OCC precondition zookie (None = unconditional). Tuples may carry an expiry condition (Tuple::with_expires).

content_change_check authorizes a content modification at the freshest snapshot and returns the zookie to store with the new content version.

watch(ns, start_ts) tails the changelog for a namespace (paper §2.4.6). Call recv on the returned stream: empty updates is a heartbeat; non-empty is one atomic write at ts. Resume from any received ts (exclusive).

The Read API supports object filters (ReadFilter::by_object) and reverse subject filters (ReadFilter::by_user, ReadFilter::by_user_set, paper §2.4.3) answered via the reverse index — raw stored edges, no rewrite evaluation. Use expand for the effective userset.

Request-scoped check memoization

memo::RequestMemo memoizes check and list decisions for the lifetime of a single request — a handler running many check/list calls for the same subject collapses to far fewer RPCs:

let memo = nio_client::memo::RequestMemo::new(check_client.clone());
let first = memo.check(ns.clone(), obj.clone(), rel.clone(), user.clone()).await?; // RPC
let again = memo.check(ns, obj, rel, user).await?; // in-request cache hit

Identical (ns, obj, rel, principal) checks (and (ns, rel, principal) lists) are answered from an in-request cache; concurrent identical misses are coalesced so a handler fanning checks across tasks still issues one RPC per key. Errors are never cached.

This is free of staleness risk (a request is one logical instant) but create one memo per request and do not use it in a handler that writes a tuple and then re-checks expecting to observe its own write. RequestMemo::with_timestamp pins the evaluation zookie for the request; RequestMemo::with_observer reports per-lookup hit/miss.

Building and testing

A Taskfile drives the workflow:

task build       # cargo build --features axum
task lint        # clippy, warnings are errors
task test        # unit + in-process mock gRPC server tests
task test-live   # live tests against NIO_CHECK_URI
task ci          # fmt-check + lint + build + test
task example-check -- customer acme customer.update <userid>

License

This project is licensed under the MIT License. See the LICENSE file for details.