Table of Contents

Orleans.Lattice.Api.TenantAdmin.Grpc

This page documents Orleans.Lattice.Api.TenantAdmin.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.

The code-first gRPC binding and public clients for the Orleans.Lattice.Api.TenantAdmin tenant lifecycle and region-residency control facades and their read-only tenant self-service companion. It exposes ILatticeTenantAdmin, ILatticeTenantRegionAdmin, ILatticeTenantAccessAdmin, ILatticeTenantGrantAdmin, ILatticeTenantQuotaUsage, and ILatticeTenantSelfService over a network transport as thin adapters - the control and scoping semantics live in the facades, this package only marshals them.

What is it?

A code-first gRPC binding - Grpc.AspNetCore method definitions whose messages are marshalled with the Orleans binary serializer, with no hand-written .proto - that hosts the tenant-administration facades as a gRPC service and ships strongly-typed clients for calling it remotely. It mirrors the TreeAdmin gRPC binding packaging exactly: server-side registration + endpoint mapping extensions, a LatticeTenantAdminApiGrpcClient, a read-only LatticeTenantSelfServiceApiGrpcClient, a fail-closed auth interceptor, and an auth-scheme advertisement RPC so a client can discover how to authenticate.

Core properties

  • Thin adapter. Each RPC forwards one-to-one to an ILatticeTenantAdmin, ILatticeTenantRegionAdmin, ILatticeTenantAccessAdmin, ILatticeTenantGrantAdmin, ILatticeTenantQuotaUsage, or ILatticeTenantSelfService method; no control logic lives here.
  • Default-deny out of the box. With RequireAuthorization left at its true default, the server interceptor consults the registered ILatticeTenantAdminApiAuthorizer on every admin RPC - and the registered default is DenyTenantAdminApiAuthorizer, which refuses every call. Presenting a credential is therefore not sufficient: a host must deliberately opt in by registering a permissive authorizer (AllowAllTenantAdminApiAuthorizer or its own implementation) before this surface answers at all, or set RequireAuthorization = false when an outer boundary already guards the endpoint. A refusal is returned as PermissionDenied before the call reaches the facade, which then applies its own two-tier gate on top.
  • Credential isolation per call. The caller credential is read from a configurable header and bridged into the facade's authorization context for that call only.
  • Discoverable auth. A GetAuthScheme RPC advertises the accepted credential schemes so a client can self-configure. It is exempt from the authorizer so a client can learn how to sign in before it holds any credential.
  • Self-service reads exempt from default-deny, still scoped. The read-only self-service RPCs are exempt from the tenant-admin authorizer entirely, so they stay reachable by any read-capable caller - including an anonymous one. They are still credential-stamped and active-tenant-stamped, so the facade enforces fail-closed per-tenant scoping at the single narrowest seam: an anonymous caller lists nothing, and a caller only ever sees its own authorized tenants.

Service and RPCs

The gRPC service name is orleans.lattice.api.tenantadmin, so each method's full path is /orleans.lattice.api.tenantadmin/<Rpc>.

The service surfaces the ILatticeTenantAdmin lifecycle operations, the ILatticeTenantRegionAdmin region-residency operations, the ILatticeTenantQuotaUsage usage read, the ILatticeTenantAccessAdmin tenant-admin subject operations, the ILatticeTenantGrantAdmin cross-tenant grant operations, the read-only ILatticeTenantSelfService operations, and the auth-scheme advertisement. Each row below is one bound RPC:

RPC Facade method
CreateTenant CreateTenantAsync (carries the optional admin-subject set)
SuspendTenant SuspendTenantAsync
ResumeTenant ResumeTenantAsync
DeleteTenant DeleteTenantAsync
SetTenantQuotas SetTenantQuotasAsync
GetTenantQuotaUsage ILatticeTenantQuotaUsage.GetQuotaUsageAsync
AuthorizeAllowedRegions ILatticeTenantRegionAdmin.AuthorizeAllowedRegionsAsync (operator-only)
SetTenantResidency ILatticeTenantRegionAdmin.SetResidencyAsync (operator or tenant admin)
GetTenantRegionStatus ILatticeTenantRegionAdmin.GetTenantRegionStatusAsync (operator or tenant admin)
ListTenantAdminSubjects ILatticeTenantAccessAdmin.ListAdminSubjectsAsync
AddTenantAdminSubject ILatticeTenantAccessAdmin.AddAdminSubjectAsync
RemoveTenantAdminSubject ILatticeTenantAccessAdmin.RemoveAdminSubjectAsync
ListCrossTenantGrants ILatticeTenantGrantAdmin.ListGrantsAsync
OfferCrossTenantGrant ILatticeTenantGrantAdmin.OfferGrantAsync
ApproveCrossTenantGrant ILatticeTenantGrantAdmin.ApproveGrantAsync
RejectCrossTenantGrant ILatticeTenantGrantAdmin.RejectGrantAsync
RevokeCrossTenantGrant ILatticeTenantGrantAdmin.RevokeGrantAsync
GetCurrentTenant ILatticeTenantSelfService.GetCurrentTenantAsync (self-service; exempt from default-deny)
ListAccessibleTenants ILatticeTenantSelfService.ListAccessibleTenantsAsync (self-service; exempt from default-deny)
GetTenant ILatticeTenantSelfService.GetTenantAsync (self-service; exempt from default-deny)
GetAuthScheme (binding-local) advertises accepted credential schemes

Region-residency RPCs

The region-residency RPCs bind the ILatticeTenantRegionAdmin facade without widening it. Every one of them is interceptor-enforced: none is on the self-service exemption list, so RequireAuthorization applies, and the facade then re-runs its own two-tier gate. AuthorizeAllowedRegions stays operator-only, while SetTenantResidency and GetTenantRegionStatus stay operator-or-tenant-admin, exactly as in-process and independent of the data-plane DefaultEffect.

AuthorizeAllowedRegions and SetTenantResidency share the TenantAdminRegionSetRequest DTO (TenantId plus the complete replacement region set); GetTenantRegionStatus reuses the existing TenantAdminTenantRequest. The interceptor decodes the authorization target from each, so an audit record names the tenant the call acts on.

ILatticeTenantRegionAdmin is an optional dependency of the service, as are ILatticeTenantAccessAdmin, ILatticeTenantGrantAdmin, and ILatticeTenantQuotaUsage. AddLatticeTenantAdminApi registers all four, so an ordinary silo serves every group; a host that composes the binding without one of them still serves every lifecycle and self-service RPC and answers each RPC of the absent facade with Unimplemented, rather than failing container construction at startup.

Status mapping

Every domain failure maps to an explicit status rather than falling through to a generic fault. The RPC groups (lifecycle, quota usage, region residency, tenant-admin subjects, cross-tenant grants, self-service) share the same vocabulary; the last column notes where an arm applies to only some of them.

Exception gRPC status Why
TenantNotFoundException NotFound The tenant is not registered - or, on the self-service surface, is not one the caller may see.
TenantAlreadyExistsException AlreadyExists CreateTenant was called for an id already registered. Lifecycle only.
TenantRegionNotAllowedException FailedPrecondition The requested residency is outside the operator-authored allowed set, or the revoked region is still resident. The caller must change state first, then retry. Region residency only.
TenantLastRegionException FailedPrecondition The change would remove the tenant's last resident region. Region residency only.
TenantLastAdminSubjectException FailedPrecondition The removal would leave the tenant with no admin subjects. Tenant-admin subjects only.
TenantGrantNotFoundException NotFound No such cross-tenant grant has been offered - reported identically when the granting tenant is not registered. Cross-tenant grants only.
TenantGrantTransitionException FailedPrecondition The grant's lifecycle forbids the requested transition (for example approving a rejected or revoked grant), or the other party's concurrent transition won the merge. Cross-tenant grants only.
ReservedTenantOperationException FailedPrecondition The operation targets the reserved default tenant (suspend, delete, set-quotas, an admin-subject add / remove, or a cross-tenant grant offer).
InvalidOperationException FailedPrecondition A lifecycle or residency precondition the facade refuses on a well-formed request. Not mapped on the quota-usage and self-service RPCs, where it falls through to Internal.
LatticeAuthorizationDeniedException PermissionDenied The caller does not hold the required tier.
LatticeTenantAccessDeniedException PermissionDenied Fail-closed tenant resolution refused the caller's asserted active tenant. Deliberately not Internal, which a client would retry.
ArgumentException InvalidArgument A malformed tenant, region, or subject id, or a grant with a blank scope, an empty operation set, or the same tenant on both sides; also a negative BurstPercent or quota ceiling, and an added admin subject the identity directory cannot resolve (LatticeDirectoryValidationException derives from ArgumentException).
OperationCanceledException Cancelled The caller's deadline or cancellation token fired.
(optional facade not registered) Unimplemented The region-residency, tenant-admin subject, cross-tenant grant, or quota-usage facade is absent from the host, so its RPCs are not served.
anything else Internal The catch-all, logged server-side and returned without echoing the exception text. It includes the tenancy package's TenantRegistryConcurrencyException (sustained write contention on one tenant's registry record), which a client may retry.

Each arm is explicit and separately tested. TenantRegionNotAllowedException and TenantLastRegionException in particular must never reach the catch-all arm: that is the failure mode fixed in #1697, where a domain exception surfaced to callers as an opaque Internal.

Client method signatures

LatticeTenantAdminApiGrpcClient (construct with LatticeTenantAdminApiGrpcClient.Create(CallInvoker callInvoker, IServiceProvider serializerProvider)):

Method Signature
CreateTenantAsync Task<TenantCreationResult> CreateTenantAsync(string tenantId, IReadOnlyCollection<string>? adminSubjects = null, CancellationToken cancellationToken = default)
SuspendTenantAsync Task<TenantStatusChangeResult> SuspendTenantAsync(string tenantId, CancellationToken cancellationToken = default)
ResumeTenantAsync Task<TenantStatusChangeResult> ResumeTenantAsync(string tenantId, CancellationToken cancellationToken = default)
DeleteTenantAsync Task<TenantDeletionResult> DeleteTenantAsync(string tenantId, CancellationToken cancellationToken = default)
SetTenantQuotasAsync Task<TenantQuotasUpdateResult> SetTenantQuotasAsync(string tenantId, TenantQuotasDescriptor quotas, CancellationToken cancellationToken = default)
AuthorizeAllowedRegionsAsync Task<TenantRegionAuthorizationResult> AuthorizeAllowedRegionsAsync(string tenantId, IReadOnlyCollection<string> allowedRegions, CancellationToken cancellationToken = default)
SetTenantResidencyAsync Task<TenantResidencyChangeResult> SetTenantResidencyAsync(string tenantId, IReadOnlyCollection<string> residencyRegions, CancellationToken cancellationToken = default)
GetTenantRegionStatusAsync Task<TenantRegionStatusReport> GetTenantRegionStatusAsync(string tenantId, CancellationToken cancellationToken = default)
GetAuthSchemeAsync Task<IReadOnlyList<AuthSchemeDescriptor>> GetAuthSchemeAsync(CancellationToken cancellationToken = default)
GetTenantQuotaUsageAsync Task<TenantQuotaUsageReport> GetTenantQuotaUsageAsync(string tenantId, CancellationToken cancellationToken = default)
ListTenantAdminSubjectsAsync Task<TenantAdminSubjectReport> ListTenantAdminSubjectsAsync(string tenantId, CancellationToken cancellationToken = default)
AddTenantAdminSubjectAsync Task<TenantAdminSubjectChangeResult> AddTenantAdminSubjectAsync(string tenantId, string subjectId, CancellationToken cancellationToken = default)
RemoveTenantAdminSubjectAsync Task<TenantAdminSubjectChangeResult> RemoveTenantAdminSubjectAsync(string tenantId, string subjectId, CancellationToken cancellationToken = default)
ListCrossTenantGrantsAsync Task<TenantGrantReport> ListCrossTenantGrantsAsync(string tenantId, CancellationToken cancellationToken = default)
OfferCrossTenantGrantAsync Task<TenantGrantChangeResult> OfferCrossTenantGrantAsync(string granterTenantId, string granteeTenantId, string scope, TenantGrantAccess operations, CancellationToken cancellationToken = default)
ApproveCrossTenantGrantAsync Task<TenantGrantChangeResult> ApproveCrossTenantGrantAsync(string granterTenantId, string granteeTenantId, string scope, CancellationToken cancellationToken = default)
RejectCrossTenantGrantAsync Task<TenantGrantChangeResult> RejectCrossTenantGrantAsync(string granterTenantId, string granteeTenantId, string scope, CancellationToken cancellationToken = default)
RevokeCrossTenantGrantAsync Task<TenantGrantChangeResult> RevokeCrossTenantGrantAsync(string granterTenantId, string granteeTenantId, string scope, CancellationToken cancellationToken = default)

LatticeTenantSelfServiceApiGrpcClient (read-only; construct with LatticeTenantSelfServiceApiGrpcClient.Create(CallInvoker callInvoker, IServiceProvider serializerProvider)):

Method Signature
GetCurrentTenantAsync Task<TenantDescriptor> GetCurrentTenantAsync(CancellationToken cancellationToken = default)
ListAccessibleTenantsAsync Task<IReadOnlyList<TenantDescriptor>> ListAccessibleTenantsAsync(CancellationToken cancellationToken = default)
GetTenantAsync Task<TenantStatusReport> GetTenantAsync(string tenantId, CancellationToken cancellationToken = default)

Wire message records

The request records this package defines are Orleans-serialized [GenerateSerializer] records whose stable aliases carry the oitng. prefix (the constants live in the public GrpcTenantAdminTypeAliases class). Responses are the facade result records from Orleans.Lattice.Api.Abstractions, whose aliases carry the oitn. prefix, except ListAccessibleTenants, which wraps its list in this package's TenantSelfDescriptorList, and GetAuthScheme, which answers with this package's AuthSchemeAdvertisement. Properties marked required must be set by the caller.

Record Members Used by
TenantAdminCreateRequest required string TenantId, IReadOnlyList<string> AdminSubjects CreateTenant
TenantAdminTenantRequest required string TenantId SuspendTenant, ResumeTenant, DeleteTenant, GetTenant, GetTenantRegionStatus, GetTenantQuotaUsage, ListTenantAdminSubjects, ListCrossTenantGrants
TenantAdminSetQuotasRequest required string TenantId, TenantQuotasDescriptor Quotas SetTenantQuotas
TenantAdminRegionSetRequest required string TenantId, IReadOnlyList<string> Regions AuthorizeAllowedRegions, SetTenantResidency
TenantAdminSubjectRequest required string TenantId, required string SubjectId AddTenantAdminSubject, RemoveTenantAdminSubject
TenantAdminGrantOfferRequest required string GranterTenantId, required string GranteeTenantId, required string Scope, TenantGrantAccess Operations OfferCrossTenantGrant
TenantAdminGrantRequest required string GranterTenantId, required string GranteeTenantId, required string Scope ApproveCrossTenantGrant, RejectCrossTenantGrant, RevokeCrossTenantGrant
TenantSelfCurrentRequest (empty) GetCurrentTenant
TenantSelfListRequest (empty) ListAccessibleTenants
TenantSelfDescriptorList IReadOnlyList<TenantDescriptor> Tenants The ListAccessibleTenants response.
AuthSchemeAdvertisementRequest (empty) GetAuthScheme

Registration

Server side (an ASP.NET Core host co-located with the silo):

  • AddLatticeTenantAdminApiGrpc(this IServiceCollection services, Action<LatticeTenantAdminApiGrpcOptions>? configure = null) - registers the gRPC service and its method definitions, the auth interceptor, and a set of TryAdd seams a host may pre-empt with its own implementation: the default-deny ILatticeTenantAdminApiAuthorizer, the header-reading ILatticeTenantAdminApiCredentialBridge, and the options-backed ILatticeTenantAdminApiAuthSchemeSource (which advertises nothing by default). Because each is a TryAdd, an implementation you register before this call is kept, and one registered afterwards with AddSingleton also wins, because the last registration of a service is the one resolved; registering a permissive authorizer either way is what opts the surface in. The interceptor is the exception: each call appends it to the gRPC pipeline again, so a repeated call authorizes every call to this service once per registration - call it once.
  • MapLatticeTenantAdminApiGrpc(this IEndpointRouteBuilder endpoints) - maps the gRPC endpoint. The host must have called AddLatticeTenantAdminApiGrpc and must expose ILatticeTenantAdmin (via AddLatticeTenantAdminApi) in the same service provider first.

Configuration

LatticeTenantAdminApiGrpcOptions:

Property Type Default Meaning
RequireAuthorization bool true Whether the interceptor enforces the registered ILatticeTenantAdminApiAuthorizer on every admin call. Left at its default with the default-deny authorizer in place, the binding refuses everything. Set to false only when an outer authentication boundary already guards the endpoint.
CredentialHeaderName string "authorization" The request header the caller credential is read from. The default bridge reads it on every call except the unauthenticated GetAuthScheme; without Orleans.Lattice.Auth registered the core no-op access gate ignores the bridged credential.
CredentialScheme string "Bearer" The scheme stamped on the bridged credential. A case-insensitive scheme prefix on the header value is stripped before the remainder is used as the token.
ActiveTenantHeaderName string "lattice-active-tenant" (LatticeActiveTenantAssertion.DefaultHeaderName) The request header carrying the tenant the caller is acting as. Set to an empty string to disable header-based tenant selection.
AdvertisedAuthSchemes IList<AuthSchemeDescriptor> (get-only, mutate in place) empty The credential schemes the unauthenticated GetAuthScheme RPC advertises, in preference order. Each descriptor must carry only public configuration - never a secret.

Per-tenant selection

On a cluster running the optional tenancy add-on, the caller's active tenant is what the self-service surface reports and what scopes the tenant-local view of the control plane. The binding lifts it from a single request header - lattice-active-tenant by default, configurable through LatticeTenantAdminApiGrpcOptions.ActiveTenantHeaderName - and stamps it onto the call's ambient scope for the duration of the call.

The header carries only an assertion: the tenancy add-on re-validates it against the caller's subject membership downstream, exactly as it validates the caller credential. An absent, blank, or syntactically invalid header asserts no tenant, and the caller resolves the reserved default tenant. GetCurrentTenant and ListAccessibleTenants resolve the assertion first, so one the caller may not use is refused as a PermissionDenied RpcException rather than reported as a tenant the caller does not hold; GetTenant answers only for a tenant the caller administers or its validated current tenant, and reports anything else as NotFound. The lifecycle, region-residency, admin-subject, grant, and quota-usage RPCs authorize on the caller's subject and the tenant the request names, not on the asserted active tenant; every RPC group still maps a fail-closed tenant-resolution refusal to PermissionDenied. Set the option to an empty string to disable header-based tenant selection entirely. The facade itself requires the tenancy add-on, so a host serving this binding always has the resolver that validates the assertion.

Authorization surface

The public seams a host implements or substitutes to open this surface up:

Type Kind Purpose
ILatticeTenantAdminApiAuthorizer interface The transport-level gate the interceptor consults on every admin RPC. Implement its single member, Task<bool> IsAuthorizedAsync(LatticeTenantAdminApiAuthorizationContext authorizationContext, CancellationToken cancellationToken), to apply a host policy.
DenyTenantAdminApiAuthorizer class The registered default: refuses every call, so the surface is closed until a host opts in.
AllowAllTenantAdminApiAuthorizer class Admits every call, deferring entirely to the facade's own gate. For a host whose endpoint is already guarded by an outer boundary.
LatticeTenantAdminApiAuthorizationContext readonly struct What the authorizer is handed: the Operation, the TargetId (the tenant id the request names - for every cross-tenant grant call, including the grantee-side approve and reject, the granting tenant - or null when not tenant-scoped), and the raw ServerCallContext for header / identity / peer inspection.
LatticeTenantAdminApiOperation enum The per-operation discriminator. Tenant lifecycle and quota: CreateTenant, SuspendTenant, ResumeTenant, DeleteTenant, SetTenantQuotas, GetTenantQuotaUsage. Region residency: AuthorizeAllowedRegions, SetTenantResidency, GetTenantRegionStatus. Tenant-admin subjects: ListTenantAdminSubjects, AddTenantAdminSubject, RemoveTenantAdminSubject. Cross-tenant grants: ListCrossTenantGrants, OfferCrossTenantGrant, ApproveCrossTenantGrant, RejectCrossTenantGrant, RevokeCrossTenantGrant. An unrecognised method maps to Unknown, never to a permissive default - so a deny-by-default policy refuses an RPC it has never heard of rather than falling through.
ILatticeTenantAdminApiCredentialBridge interface Lifts the inbound credential (LatticeCredential? Resolve(ServerCallContext context)) into the ambient Lattice credential for the duration of one call. The default reads the configured header; substitute it for a bespoke identity source such as a client certificate.
ILatticeTenantAdminApiAuthSchemeSource interface Supplies what the unauthenticated GetAuthScheme RPC advertises (AuthSchemeAdvertisement GetAdvertisement()). The default projects LatticeTenantAdminApiGrpcOptions.AdvertisedAuthSchemes.
AuthSchemeDescriptor record One advertised credential scheme: its required SchemeId, a friendly DisplayName, and the public Parameters a client needs to run the sign-in challenge.
AuthSchemeAdvertisement record The GetAuthScheme response envelope carrying the descriptor list.

The interceptor itself is internal. It runs the authorizer and scopes enforcement to this service by matching on the service-name prefix, so unrelated gRPC services hosted in the same ASP.NET Core pipeline are unaffected. The credential bridging happens after it, in the service: each RPC lifts the caller credential (through ILatticeTenantAdminApiCredentialBridge) and the asserted active tenant onto the ambient context for that call only, so the facade's fail-closed gate sees the caller's identity and every credential is isolated to its own call.

See also