---
title: "Cross-Cluster Authorization"
url: "https://nsta1.github.io/Orleans.Lattice/samples/CrossClusterAuthorization/README.html"
source: "https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/samples/CrossClusterAuthorization/README.md"
documents: "Orleans.Lattice 9.9.0 (release line 9.9)"
built: "2026-10-04"
all-pages: "https://nsta1.github.io/Orleans.Lattice/llms.txt"
---
# Cross-Cluster Authorization

Part of [Samples](../index.md).

> Looking for a minimal, single-silo introduction to the authorization layer?
> Start with the [Authorization](../Authorization/README.md) sample, which
> demonstrates users, groups, and nested groups on one silo. This sample adds a
> second cluster and cross-cluster policy convergence on top of that same layer.

## What it shows

The Orleans.Lattice authorization layer end to end: membership (users and
groups), a default-deny policy, and per-tree / per-key / per-prefix rules
enforced on every operation. It stands up **two independent in-process Orleans
clusters** (`site-a` and `site-b`) wired together with Orleans.Lattice
replication over gRPC, and enrols the reserved membership and policy system
trees into that replication (the system-tree replication special case) so an
authorization change on one cluster converges onto the other.

The demo runs in four acts:

1. **Membership.** Create the groups (`line-operators`, `auditors`) on both
   sites and add `alice` and `bob` to them (users are plain member ids - the
   directory keeps no user records - and `carol` is in no group).
2. **Rules and enforcement.** Author three rules under a default-deny policy -
   a prefix grant (operators read/write/delete/range the `station/` subtree), a
   key grant (only `alice` may read/write the single `config/threshold` key),
   and a tree grant (auditors read the whole tree). Then exercise writes, a
   read, and deletes as `alice` and `bob` and watch the gate allow or deny each
   one.
3. **Read visibility.** A point read of a key the caller lacks read permission
   for is soft-denied (returns absent, not an error), and a range read returns
   **only** the keys the caller is authorized to read - `bob` (auditor) sees all
   keys, `alice` sees only her two remaining stations (her config-key grant
   covers point reads and writes, not range reads), and `carol`, who is in no
   group, sees nothing.
4. **Cross-cluster convergence.** Revoke `alice`'s config-key grant on `site-a`
   **only** and watch the revoke replicate onto `site-b`'s policy tree with no
   direct write to `site-b`.

Every subject's operations run under an ambient credential
(`LatticeCredentialContext.Use`) that flows to the grains on the Orleans request
context; a small custom `ILatticeCredentialAuthenticator` maps a demo token to a
subject id, and the membership directory expands the subject's group
memberships. The groups, memberships, and rules are written through the
silo-side membership directory and policy store, which run under system origin
and never consult the gate, so they need no prior grant; the tree's data keys
are seeded as the configured bootstrap administrator, which the gate allows
before any rule exists.

## Run it

```
dotnet run --project samples/CrossClusterAuthorization
```

## Expected output

```
Starting two Orleans clusters (site-a, site-b) with the auth stack...
Both clusters ready and peered over gRPC.

== Act 1: create users and groups ==
  alice in line-operators, bob in auditors, carol in no group.

== Act 2: author per-tree / per-key / per-prefix rules ==
  As alice (line-operators):
    write station/1/status  -> allowed
    write config/threshold     -> allowed
    write secret/recipe      -> DENIED   (no rule -> deny)
  As bob (auditors, read-only):
    read  secret/recipe      -> 'caramel'
    write station/2/status  -> DENIED   (read-only -> deny)
    delete station/2/status -> DENIED   (read-only -> deny)
  As alice (line-operators):
    delete station/3/status -> allowed   (prefix grant allows delete)

== Act 3: read visibility (point + range) ==
  Point read of the secret recipe:
    bob   -> 'caramel'  (auditor)
    carol -> (hidden)  (low-privilege: soft-denied)
  Range read of the whole tree returns only authorized keys:
    bob   sees 4 keys (auditor: all)
    alice sees 2 keys (her station keys)
    carol sees 0 keys (nothing)

== Act 4: a revoke on site-a converges to site-b ==
  Before: alice writing config/threshold on site-b -> allowed
  Revoked 'alice-config' on site-a only. Waiting for site-b to converge...
  site-b policy tree caught up: True (after 1s)
  site-b live gate already denies alice: False

[OK] the revoke authored on site-a converged onto site-b via system-tree replication.
```

## Convergence semantics

The policy tree is one of the reserved system trees enrolled into replication,
so the revoke propagates to `site-b`'s policy tree within the shipping window
(about a second on loopback). That authoritative state convergence is what the
sample asserts on.

Each site keeps a compiled read-through snapshot of its policy tree that its
gate consults, and refreshes that snapshot when it observes the policy change.
The `site-b live gate already denies alice` line reports whether `site-b`'s gate
has already rebuilt its snapshot from the converged tree at the moment we
poll; it may still read `False` immediately after convergence because the
snapshot refresh is decoupled from the tree write. The revoke is durably
converged either way - the rule is gone from `site-b`.

## When to use

- Multi-cluster deployments that must apply a single authorization policy
  uniformly across regions, where a grant or revoke authored in one region must
  become effective in the others without a central policy service.
- Any deployment that needs per-tree, per-key, or per-prefix authorization with
  a default-deny posture and group-based subjects.

## When not to use

- Single-cluster deployments that do not replicate - you still get the full
  authorization layer from `AddLatticeMembership` + `AddLatticeAuth`, without the
  replication and gRPC wiring this sample adds for the cross-cluster act. See the
  single-silo [Authorization](../Authorization/README.md) sample for that shape.
- Deployments that do not need authorization at all. The layer is opt-in; a host
  that never calls `AddLatticeAuth` pays nothing on the data path.

## Notes on this sample

- Uses plaintext HTTP/2 (h2c) on loopback - each site binds Kestrel to HTTP/2
  with no certificate and grpc-dotnet speaks h2c by prior knowledge over an
  `http://` address - and turns receiver authentication off, because this is a
  loopback demo with no secret material. Production must use `https://` peer
  endpoints and leave receiver authentication on.
- The gRPC ports default to `17001` / `17002`, and the two clusters also bind
  Orleans silo ports `11111` / `11112` and gateway ports `30000` / `30001`;
  change them in `Program.cs` if those are taken on your machine.
- The bootstrap administrator (`root-admin`) seeds the demo data before any rule
  grants access; the reserved membership and policy trees are written by the
  silo-side directory and policy store under system origin, not through it.
  Production should keep the bootstrap set as small as possible and grant
  everything else through rules.

## Feature docs

- [docs/lattice.membership/README.md](../../docs/lattice.membership/README.md)
- [docs/lattice.auth/README.md](../../docs/lattice.auth/README.md)
- [docs/lattice.auth/security-posture.md](../../docs/lattice.auth/security-posture.md)
