Table of Contents

Caller-credential propagation (LatticeCredentialContext)

This page is part of the documentation for Orleans.Lattice 9.9.0 (release line 9.9), built 2026-10-04. It is also published as markdown, with every table and list, at caller-credential-propagation-latticecredentialcontext.md, and llms.txt lists every page.

Part of Lattice Public API Reference.

LatticeCredentialContext is a transport-only ambient seam that carries an opaque caller credential from the client edge down to the silo on the Orleans RequestContext, following the same marker idiom as LatticeOriginContext and LatticeIdempotencyContext. It is the channel the (separately registered) Membership layer resolves into a subject. The core library never interprets it - its resilient scan and range-delete helpers only re-assert the caller's credential around each reconnect - so an unset credential adds no cost and changes no read/write semantics.

Stamp a credential ergonomically at the boundary of a logical operation:

using (LatticeCredentialContext.Use("edge-token", scheme: "Bearer"))
{
    await tree.SetAsync("k", new byte[] { 1 }, cancellationToken);
}

Use accepts the opaque token plus three optional hints an authenticator can consult without re-parsing the token: a scheme / issuer hint, a pre-resolved principalId, and a small metadata bag. With(LatticeCredential?) takes the full payload - a LatticeCredential (Token, Scheme, PrincipalId, Metadata) - directly. When no scope is entered, Current is null and IsActive is false (a single dictionary lookup, no allocation).

The marker carries a user credential only. Library-internal system / maintenance / replication-origin calls are authored by infrastructure and carry no credential: they never open a credential scope. When infrastructure fans a system-origin sub-operation out from within a turn that did carry a user credential, it wraps that sub-operation in LatticeCredentialContext.Suppress() so the ambient credential is stripped for the duration and cannot leak onto a system-authored call.

Access-gate operation flags

A host that registers an ILatticeAccessGate receives one LatticeOperation flag for the logical operation being authorized. Composite calls can carry the union of several flags. The core flags are:

Flag Value Authorizes
Read 1 Single-key reads.
Write 2 Single-key writes.
Delete 4 Single-key deletes.
RangeRead 8 Contiguous range reads and scans.
RangeDelete 16 Contiguous range deletes.
CrdtApply 32 Applying a CRDT delta or merge to a key.
AtomicWrite 64 Initiating a multi-key or cross-tree atomic write. Individual legs still require their own write/delete grant.
BulkLoad 128 Bulk-load or snapshot-restore writes that populate a tree in bulk.
Admin 256 Routine tree administration. On ILattice: SnapshotAsync, MergeAsync, the per-tree event and history-retention overrides, RebuildLeafProjectionAsync, CompactShardAsync, and RepairOrphanedLeavesAsync; on the tree-administration facade: tree creation, alias assignment, and per-tree configuration updates. Existence checks are Read.
Backup 512 Capturing a tree, prefix, or key for backup.
Restore 1024 Restoring a captured backup into a target tree, prefix, or key.
SchemaAdmin 2048 Schema-management changes.
Telemetry 4096 Cluster-wide telemetry reads.
Replication 8192 Runtime replication-management operations.
TreeLifecycle 16384 Destructive or structural whole-tree lifecycle operations: drop, recover, and purge (DeleteTreeAsync, RecoverTreeAsync, PurgeTreeAsync), resize and undo-resize, reshard, WAL-placement moves, or orphaned-leaf repair through the tree-administration facade.
AppInstall 32768 Changing the cluster's installed app set: installing, upgrading, enabling, disabling, and uninstalling an installable app. Scopeless and cluster-wide, like Telemetry.

SchemaAdmin, Telemetry, Replication, TreeLifecycle, and AppInstall are deliberately separate from Admin; granting one does not imply any other capability.

Reading an empty range read under a gate

A denied read does not throw. A denied point read reports the key as absent, and a denied range read resolves to a reject-all key filter and returns a clean, successful, empty result. So KeysAsync, EntriesAsync, their predicate overloads, both CountAsync overloads, and the snapshot cursors all report "you may not look here" and "there is nothing here" identically - no exception, no log, every instrument healthy.

That is deliberate. A denied scan stays cheap and non-fatal, and making it throw would break every existing caller. The cost is that emptiness alone is uninterpretable under a gate, so a caller that draws a conclusion from an empty range read must confirm the range was actually readable:

static async Task<bool> RangeIsGenuinelyEmptyAsync(
    ILattice tree, string startInclusive, string endExclusive, CancellationToken ct)
{
    await foreach (var key in tree.KeysAsync(startInclusive, endExclusive, cancellationToken: ct))
    {
        return false; // Not empty at all.
    }

    // Empty. Ask whether that is a fact about the store or about authorization.
    var coverage = await tree.GetRangeReadGateCoverageAsync(startInclusive, endExclusive, ct);
    return coverage == LatticeRangeReadGateCoverage.Unrestricted;
}

GetRangeReadGateCoverageAsync reports Unrestricted, Filtered, or Denied. Only Unrestricted licenses reading emptiness as absence: under Filtered an unknown subset of keys is withheld, and under Denied every key is. It is a coverage classification and never names the withheld keys, for the same reason GatedMultiReadResult.PrunedByAccessGate is a count - identities would make any range read an authorization oracle.

Call it only when a range read came back empty and you are about to act on that emptiness. The scan hot path pays nothing.

A background component is the classic victim, because its turn carries no caller credential at all: under a fail-closed gate every one of its scans returns empty, so it concludes the store is empty and does nothing, forever, with a healthy log at every layer. If a component reads on a background turn, give it a credential (see the trusted system-origin scope below) rather than relying on this check.

Trusted system-origin scope

A co-hosted infrastructure extension that must run a trusted, gate-bypassing sub-operation on a caller's behalf - for example the Orleans.Lattice.Api.Mcp server resolving a caller's effective permissions before advertising tools - enters that scope through the public LatticeSystemOrigin seam rather than reaching into the library's internals:

using (LatticeSystemOrigin.Enter())
{
    // Nested calls in this scope are treated as system-origin and admitted by
    // the access gate without a user identity. LatticeSystemOrigin.IsActive
    // reports whether such a scope is currently open.
    await tree.SetAsync("k", new byte[] { 1 }, cancellationToken);
}

Entering the scope bypasses the access gate for its lifetime, so it is reserved for co-hosted code that already runs inside the silo's trust boundary. It is the public counterpart of the internal marker the first-party add-ons use, and it exists so an out-of-package extension need not take an internal-visibility grant into the core assembly to perform the bypass.