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.
LatticeAuthApiGrpcClientexposes 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 exampleAuthGroupRef,AuthPutRule,AuthAck) or a facade DTO reused directly (for exampleAuthGroupPage,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:
- Transport meta-authorizer.
ILatticeAuthApiAuthorizerruns first, in a gRPC interceptor scoped to the auth-API service. It defaults toDenyAllAuthApiAuthorizer; every call is rejected withPermissionDenieduntil the host registers a permissive authorizer (or the opt-inAllowAllAuthApiAuthorizer) or setsRequireAuthorizationtofalse. - 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.