Table of Contents

Orleans.Lattice.Api.Auth.Grpc

This page documents Orleans.Lattice.Api.Auth.Grpc 9.9.0, in 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 README.md, and llms.txt lists every page.

Code-first gRPC binding for Orleans.Lattice.Api.Auth - projects the membership and authorization-policy admin facade onto a long-lived gRPC service and a public typed client, marshalled with the Orleans binary serializer over code-first request and response records (which wrap the facade DTOs), with no hand-written .proto.

What is it?

Orleans.Lattice.Api.Auth.Grpc is the remote transport for the cluster's control plane. Hosts reference it when a remote admin tool, a CLI, or a dashboard needs to administer groups, membership, and authorization rules - and search the identity directory and introspect verdicts - over the network rather than in-process. (The identity directory is search / resolve only; there is no user create/update/delete surface here.)

It provides:

  • A code-first gRPC service. A unary RPC per facade operation, bound from C# definitions rather than a .proto.
  • A public typed client. LatticeAuthApiGrpcClient exposes one method per RPC over a caller-supplied gRPC channel.
  • Shared Orleans marshalling. Every wire message is a [GenerateSerializer] record - either one of this package's own request/response envelopes (for example AuthGroupRef, AuthPutRule, AuthAck) or a facade DTO reused directly (for example AuthGroupPage, AuthExplanation) - serialized with the Orleans binary serializer, so client and server stay in lock-step by construction.
  • Two-layer, fail-closed authorization. A transport meta-authorizer gates every RPC at the edge, and the facade's own administrator check re-authorizes the resolved caller. The transport gate denies every call until the host opts in, and the facade check, which cannot be switched off, denies any caller that is not a bootstrap or delegated access administrator.

Administering authorization is the most sensitive surface in the cluster, so the binding fails closed: with no authorizer registered, every admin call is rejected with PermissionDenied.

Core Properties

  • Public client, internal service. Callers consume LatticeAuthApiGrpcClient; the service, marshallers, and method definitions are internal.
  • No transport policy in the client. Address, TLS, retries, deadlines, and credentials live on the caller's GrpcChannel / CallInvoker.
  • Two load-bearing gates. The transport meta-authorizer decides whether a call may run at all; the facade's per-call administrator check then re-authorizes the resolved caller's subject. Neither replaces the other.
  • Fail-closed. Unconfigured, the binding denies every call rather than serving it unauthenticated. An anonymous caller is denied by the facade check even past a permissive transport gate.

RPCs

The service is exposed under the fully-qualified gRPC service name orleans.lattice.api.auth. It is a flat set of unary RPCs, one per facade operation. Each RPC carries a request and a response message: most operations wrap their facade arguments in one of this package's own envelope records (see Request and response envelopes below), while a few reuse a facade DTO directly.

RPC Facade method Request message Response message
UpsertGroup UpsertGroupAsync AuthGroup AuthAck
GetGroup GetGroupAsync AuthGroupRef AuthGroupResult
RemoveGroup RemoveGroupAsync AuthGroupRef AuthAck
ListGroups ListGroupsAsync AuthPageRequest AuthGroupPage
AddMember AddMemberAsync AuthMemberEdge AuthAck
RemoveMember RemoveMemberAsync AuthMemberEdge AuthAck
ListGroupMembers ListGroupMembersAsync AuthGroupRef AuthStringList
ListSubjectGroups ListSubjectGroupsAsync AuthMemberRef AuthStringList
PutRule PutRuleAsync AuthPutRule AuthAck
GetRule GetRuleAsync AuthRuleRef AuthRuleResult
RemoveRule RemoveRuleAsync AuthRuleRef AuthRuleRemoved
ListRules ListRulesAsync AuthPageRequest AuthRulePage
ListRulesForTree ListRulesForTreeAsync AuthTreeRulesPage AuthRulePage
Explain ExplainAsync AuthExplainQuery AuthExplanation
EffectivePermissions EffectivePermissionsAsync AuthSubjectRef AuthEffectivePermissions
SearchDirectory SearchDirectoryAsync DirectorySearchRequest DirectorySearchResult
ResolveDirectoryPrincipal ResolveDirectoryPrincipalAsync AuthPrincipalRef AuthDirectoryPrincipalResult
GetAccessModel GetAccessModelAsync AuthAccessModelQuery AccessModelDescriptor

Request and response envelopes

The gRPC contract does not send the facade method arguments as bare scalars; it wraps them in code-first [GenerateSerializer] records (public, in the Orleans.Lattice.Api.Auth.Grpc namespace) so the wire shape can version additively. A few RPCs reuse a facade DTO from Orleans.Lattice.Api.Auth directly as their message (AuthGroup, AuthPageRequest, AuthGroupPage, AuthRulePage, AuthExplanation, AuthEffectivePermissions, DirectorySearchRequest, DirectorySearchResult, AccessModelDescriptor); the rest use the envelopes below.

Request envelopes:

Record Fields
AuthGroupRef GroupId: string
AuthMemberEdge GroupId: string, MemberId: string, MemberKind: MembershipMemberKind (default User)
AuthMemberRef MemberId: string
AuthPutRule Rule: LatticeAuthorizationRule
AuthRuleRef TreeId: string, RuleId: string
AuthTreeRulesPage TreeId: string, Page: AuthPageRequest
AuthExplainQuery SubjectId: string, Operation: LatticeOperation, Scope: LatticeScope, SubjectKind: LatticeSubjectSelectorKind (default User)
AuthSubjectRef SubjectId: string, SubjectKind: LatticeSubjectSelectorKind (default User)
AuthPrincipalRef PrincipalId: string
AuthAccessModelQuery (empty marker)

Response envelopes:

Record Fields
AuthAck (empty acknowledgement for write RPCs)
AuthGroupResult Group: AuthGroup? (null when no such group)
AuthStringList Values: IReadOnlyList<string>
AuthRuleResult Rule: LatticeAuthorizationRule? (null when no such rule)
AuthRuleRemoved Removed: bool
AuthDirectoryPrincipalResult Principal: DirectoryPrincipalDescriptor? (null when unresolved)

Public surface

Type Role
LatticeAuthApiGrpcClient Public typed client; one method per RPC over a caller-supplied CallInvoker.
LatticeAuthApiGrpcOptions Server-side options (RequireAuthorization, CredentialHeaderName, CredentialScheme, ActiveTenantHeaderName). ActiveTenantHeaderName (default lattice-active-tenant; null or empty disables it) is read only by a ListRules call with AuthPageRequest.ActiveTenantOnly set; there a denied tenant assertion fails the call with PermissionDenied.
ILatticeAuthApiAuthorizer Transport meta-authorization seam.
DenyAllAuthApiAuthorizer Default-deny authorizer, registered by AddLatticeAuthApiGrpc only when the host has not already registered an ILatticeAuthApiAuthorizer; one the host registers afterwards, as the Quick start does, takes precedence.
AllowAllAuthApiAuthorizer Opt-in permissive authorizer for trusted-network use.
LatticeAuthApiAuthorizationContext Per-call description handed to the authorizer (operation, target id, call context).
LatticeAuthApiOperation Enumerates the operation behind each RPC, plus Unknown for an auth-API method the interceptor does not recognise, so a deny-by-default authorizer refuses an unmapped call rather than treating it as a benign operation.
ILatticeAuthApiCredentialBridge Identity seam that lifts the inbound credential onto the ambient context.
Auth* request/response records Public request and response DTOs for the unary RPCs.
AddLatticeAuthApiGrpc / MapLatticeAuthApiGrpc Registration and endpoint-routing extensions.

Two-layer authorization

Every admin call passes through two independent gates, both fail-closed:

  1. Transport meta-authorizer. ILatticeAuthApiAuthorizer runs first, in a gRPC interceptor scoped to the auth-API service. It defaults to DenyAllAuthApiAuthorizer; every call is rejected with PermissionDenied until the host registers a permissive authorizer (or the opt-in AllowAllAuthApiAuthorizer) or sets RequireAuthorization to false.
  2. Facade administrator check. Once past the transport gate, the service stamps the caller identity onto the ambient credential context (via ILatticeAuthApiCredentialBridge, Bearer-aware by default) and invokes the facade. The facade's own per-call administrator check then runs against the resolved caller's subject. An anonymous caller (no credential) is denied here even when the transport gate allowed the call.

A denial from the facade check is mapped to PermissionDenied with response trailers carrying only non-sensitive fields (lattice-denied-tree, lattice-denied-operation, lattice-denied-subject, lattice-denied-reason) - never a policy value.

The transport authorizer receives a LatticeAuthApiAuthorizationContext naming the call's LatticeAuthApiOperation and a TargetId: the group id for UpsertGroup, GetGroup, RemoveGroup, ListGroupMembers, AddMember, and RemoveMember; the member id for ListSubjectGroups; the governed tree id for PutRule (the rule's scope tree), GetRule, RemoveRule, and ListRulesForTree; the subject id for Explain and EffectivePermissions; the principal id for ResolveDirectoryPrincipal; and null for ListGroups, ListRules, SearchDirectory, and GetAccessModel.

Error mapping

Status When
PermissionDenied The transport authorizer refused the call (no trailers), the facade administrator check denied the caller (with the trailers above), or a ListRules call with AuthPageRequest.ActiveTenantOnly set asserted a tenant the caller may not act as (no trailers).
InvalidArgument The facade threw an ArgumentException - for example the policy store rejecting a rule shape it will not persist, a LatticeAppOwnedRuleException for a write or delete of an app-owned (app:) rule id, or a LatticeDirectoryValidationException from identity-directory validation. The status detail is the exception message.
Cancelled The call was cancelled, including while the transport authorizer was deciding.
Internal Any other failure. The server logs it and returns a generic message.

Quick Start

Register the binding on a silo that already has AddLatticeAuthApi, then map its routes:

using Orleans.Lattice.Api.Auth.Grpc;

var builder = WebApplication.CreateBuilder();
builder.Services.AddLatticeAuthApiGrpc(o => o.RequireAuthorization = true);
builder.Services.AddSingleton<ILatticeAuthApiAuthorizer, AllowAllAuthApiAuthorizer>();

var app = builder.Build();
app.MapLatticeAuthApiGrpc();

The host must expose ILatticeAuthAdmin in the same service provider - typically by co-hosting Orleans with AddLattice(...).AddLatticeMembership().AddLatticeAuth(...).AddLatticeAuthApi() on the same host (AddLatticeAuth fails fast unless membership is registered first).

Client

using Grpc.Net.Client;
using Microsoft.Extensions.DependencyInjection;
using Orleans.Lattice.Api.Auth;
using Orleans.Lattice.Api.Auth.Grpc;
using Orleans.Serialization;

var services = new ServiceCollection();
services.AddSerializer();
var serializerProvider = services.BuildServiceProvider();
using var channel = GrpcChannel.ForAddress("https://admin.example:443");
var authClient = LatticeAuthApiGrpcClient.Create(channel.CreateCallInvoker(), serializerProvider);

await authClient.UpsertGroupAsync(new AuthGroup { GroupId = "admins", DisplayName = "Admins" });
await authClient.PutRuleAsync(new AuthPutRule
{
    Rule = new LatticeAuthorizationRule(
        "admins-read-orders",
        LatticeSubjectSelector.Group("admins"),
        LatticeScope.Tree("orders"),
        LatticeOperation.Read,
        LatticeEffect.Allow),
});
var explanation = await authClient.ExplainAsync(new AuthExplainQuery
{
    SubjectId = "alice",
    Operation = LatticeOperation.Read,
    Scope = LatticeScope.Tree("orders"),
});

The serializerProvider must have Orleans serialization registered (AddSerializer()) so the client and server wire marshallers match exactly. Transport concerns (address, TLS, deadlines, retries, call credentials) are configured on the channel the caller supplies. An authorization refusal arrives as a PermissionDenied RpcException; see Error mapping for the other status codes.

AuthExplainQuery.SubjectKind (and AuthSubjectRef.SubjectKind) select whether SubjectId names a user or a group; both default to LatticeSubjectSelectorKind.User, so existing messages deserialize unchanged. Set it to LatticeSubjectSelectorKind.Group to explain (or resolve the effective permissions of) a group subject - the decision is then evaluated for a principal that is a member of that group and its ancestors, so group-scoped rules match exactly as they would for a real member.