nio-client 0.3.0

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: User::AllUsers (allUsers) and User::AuthenticatedUsers (authenticatedUsers). The pointer object/rel keyword is "..." (Obj::unspecified() / Rel::unspecified()).

User IDs and subjects

A UserId is a positive 64-bit integer (nio #301). Build one with UserId::try_from(42_i64) or parse the decimal text with "42".parse(). Parsing is strict: it accepts 1 to 9223372036854775807 with no sign and no leading zero. It rejects 0, -5, 01, and any value that does not fit in an i64. UserId::get() returns the i64. Display prints the plain decimal.

A tuple subject is a User:

  • User::UserId(id) is one principal (42)
  • User::AllUsers is everyone, signed in or not (allUsers)
  • User::AuthenticatedUsers is every signed-in principal (authenticatedUsers)
  • User::UserSet { ns, obj, rel } is a userset (group:eng#member)

User parses from and prints to the text form in parentheses. check, list, and content_change_check take a UserId, because nio evaluates those calls for one principal. expand returns the principals in user_ids and reports the wildcards in all_users and authenticated_users.

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's UserId 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.

  • None — no fresher-than constraint; the client leaves ts unset and the server picks its default 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 for any 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).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 42

License

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