From 2d857732cc42028bf0eba4808e154d345c9d96c5 Mon Sep 17 00:00:00 2001 From: saurabhmaheshwari-obin <242053479+saurabhmaheshwari-obin@users.noreply.github.com> Date: Sat, 24 Oct 2026 10:56:58 -0400 Subject: [PATCH 01/17] fix(accounts): a person's capability stays sealed to its audience's installation A person's capability is sealed to the organization's installation of the audience it addresses, and a module or installation principal's to its own. StartTask resolves that seal and remint re-resolves it for a root session, an audience exchange, a renewal and a module's exchange through its operation binding. StartChildSession did not: core's Child keeps the parent's seal, and the request names a free audience, so a member could derive a child for another module that stayed sealed to the first module's installation. StartChildSession now refuses a person's child unless the child's audience resolves to the parent's seal (PermissionDenied; an audience the organization never installed keeps audienceInstallation's FailedPrecondition). A principal that is not a person is sealed to its own installation and is unaffected. Existing callers derive same-audience children only. The invariant becomes load-bearing for the gateway's bearer-less module arm, which reads "sealed to an installation other than the target's" as unattended work. TestAPersonsCapabilityIsSealedToItsAudiencesInstallation pins it over every person mint (StartTask, StartRootSession, ExchangeAudience, RenewWorkContext, StartChildSession, and a module's exchange through its operation binding): each either seals to its audience's installation or is refused, with re-derived parents sealed to a stale installation first so a copied seal fails. TestStartChildSessionRefusesAPersonsChildForAnotherAudiencesInstallation is the cross-installation case alone. Both AGENTS.md files name the test, and WORK_CONTEXTS.md states the rule in its seal contract. The ListInstallations contract comment in installations.proto is not changed here. Editing it changes the accounts descriptors (the exported contracts, their digests, the generated Go and the raw OpenAPI), and a no-change regeneration of the accounts tree with the available toolchain did not come back clean, so the second caller is recorded in accounts' AGENTS.md instead. Co-Authored-By: Claude Opus 5.5 --- module/WORK_CONTEXTS.md | 10 + module/services/accounts/AGENTS.md | 27 +++ .../code/pkg/adapters/work_context_rpcs.go | 38 ++++ .../adapters/work_context_seal_pin_test.go | 182 ++++++++++++++++++ module/services/auth-gateway/AGENTS.md | 8 + 5 files changed, 265 insertions(+) create mode 100644 module/services/accounts/code/pkg/adapters/work_context_seal_pin_test.go diff --git a/module/WORK_CONTEXTS.md b/module/WORK_CONTEXTS.md index 5d7ef077e..855d620e4 100644 --- a/module/WORK_CONTEXTS.md +++ b/module/WORK_CONTEXTS.md @@ -344,6 +344,16 @@ answered by one set of functions for minting and verifying alike is sealed to the organization's installation of it, held while the person is an active member; an organization that never installed the audience is minted nothing. A module's is sealed to the installation its mint resolved. + This is an invariant, not a default: every capability a person owns is sealed + to the installation of the audience it addresses, on every mint that issues + one. `StartChildSession` is the one derivation core cannot re-seal (a child + keeps its parent's seal), so it refuses a person's child for an audience whose + installation is not the parent's seal. Source-operation contexts and + installation tasks are not exceptions: they are minted for a module or an + installation, and sealed to that principal's own installation. The gateway's + bearer-less module arm reads the seal as the owner's kind on this rule + (`TestAPersonsCapabilityIsSealedToItsAudiencesInstallation` pins it over every + person mint). - **Epoch.** The owner's authorization revision in the organization. Membership and role changes advance a person's; installing a module and every change to its installation's revision advance a module principal's (migration 35). diff --git a/module/services/accounts/AGENTS.md b/module/services/accounts/AGENTS.md index 549249825..2ad71622b 100644 --- a/module/services/accounts/AGENTS.md +++ b/module/services/accounts/AGENTS.md @@ -511,6 +511,33 @@ still lists that key accepts it. The capability's lifetime bounds the window: its holder mints a new one under the current key. Rotating the Work Context key therefore briefly refuses questions about capabilities already in flight. +## A person's capability is sealed to its audience's installation + +Every capability a person owns is sealed to the organization's installation of +the audience it addresses; a module or installation principal's is sealed to its +own. That is an invariant every person mint keeps, not a default: `StartTask` +resolves it, `remint` re-resolves it for a root session, an audience exchange, a +renewal and a module's exchange through its operation binding, and +`StartChildSession` (whose core `Child` keeps the parent's seal) refuses a +person's child for an audience sealed elsewhere. + +The gateway's bearer-less module arm depends on it: it admits a capability with +no bearer only when the seal names an active installation other than the +target's, which a person's capability never does. +`TestAPersonsCapabilityIsSealedToItsAudiencesInstallation` +(`pkg/adapters/work_context_seal_pin_test.go`) pins it over every person mint. A +new mint that issues a person's capability goes in that table; one that copies a +parent's seal across audiences reopens the arm to a person's credential without +its bearer. + +The arm reads the tenant's installations through `InstallationService/ListInstallations` +(`EXPOSURE_INTERNAL`). Its contract comment in `installations.proto` names one +caller, the gateway's viewer projection, with the tenant projected from a +verified identity. The arm is a second caller of the same RPC with the same +credential, and its tenant is the one an edge-verified Work Context names: a +signed statement of this issuer's, never a value the request chose. The proto +comment is to name it at the next regeneration of the accounts descriptors. + ## Minting a module Work Context The identity itself is a **Work Context**. The auth-gateway brokers the exchange diff --git a/module/services/accounts/code/pkg/adapters/work_context_rpcs.go b/module/services/accounts/code/pkg/adapters/work_context_rpcs.go index debe897a8..fdaaaf1d2 100644 --- a/module/services/accounts/code/pkg/adapters/work_context_rpcs.go +++ b/module/services/accounts/code/pkg/adapters/work_context_rpcs.go @@ -1418,6 +1418,9 @@ func (s *WorkContextAuthorityServer) StartChildSession( if err != nil { return nil, err } + if err := s.requireChildKeepsAudienceSeal(ctx, parent.Context(), req.GetAudience()); err != nil { + return nil, err + } permissions, scopes, err := workContextScopes(req.GetGrantedScopes(), mintContentReads(req.GetAudience())) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) @@ -1456,6 +1459,41 @@ func (s *WorkContextAuthorityServer) StartChildSession( return issuedWorkContext(token, signed), nil } +// requireChildKeepsAudienceSeal refuses a child of a PERSON's capability that +// would carry another installation's seal. +// +// The invariant: a capability owned by a person is sealed to the organization's +// installation of the audience it addresses. Every other person mint re-derives +// the seal from the audience (StartTask, and remint for StartRootSession, +// ExchangeAudience and RenewWorkContext). Core's Child cannot: a child keeps its +// parent's seal, so a child for another audience would be a person's capability +// sealed to an installation that is not its audience's. The gateway's bearer-less +// module arm reads exactly that difference as "unattended work" (a module or +// installation principal is sealed to its own installation), so such a child +// would be admitted there without the person's bearer. +// +// A same-audience child keeps the seal its audience resolves to now, and is +// minted. A principal that is not a person is sealed to its own installation, +// which the audience does not change, and is not held here. +// TestAPersonsCapabilityIsSealedToItsAudiencesInstallation pins the invariant +// over every person mint. +func (s *WorkContextAuthorityServer) requireChildKeepsAudienceSeal( + ctx context.Context, parent *basev0.WorkContextV1, audience string, +) error { + if parent.GetOwnerPrincipalKind() != business.PrincipalKindHuman { + return nil + } + installation, err := s.audienceInstallation(ctx, parent.GetTenantId(), audience) + if err != nil { + return err + } + if installation != parent.GetSeal().GetInstallationId() { + return status.Error(codes.PermissionDenied, + "a child of a person's Work Context keeps its parent's installation, so it may address only an audience sealed to that installation") + } + return nil +} + // RenewWorkContext lets the current actor of a delegated context re-issue it // with a fresh window while the originating user is offline. It keeps the Task, // the owner and every delegation hop, appends none, and opens a fresh Session; diff --git a/module/services/accounts/code/pkg/adapters/work_context_seal_pin_test.go b/module/services/accounts/code/pkg/adapters/work_context_seal_pin_test.go new file mode 100644 index 000000000..47e8e9581 --- /dev/null +++ b/module/services/accounts/code/pkg/adapters/work_context_seal_pin_test.go @@ -0,0 +1,182 @@ +package adapters + +import ( + "context" + "testing" + + "connectrpc.com/connect" + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + accountsauth "accounts/pkg/auth" + "accounts/pkg/business" + gen "accounts/pkg/gen/saas/accounts/v1" +) + +// THE SEAL FOLLOWS THE OWNER, pinned over every mint a person's capability comes +// from. +// +// A person's capability is sealed to the organization's installation of the +// audience it addresses; a module or installation principal's is sealed to its +// own installation. The gateway's bearer-less module arm depends on that +// difference: it admits a capability for module X without a bearer only when the +// seal names an active installation OTHER than the organization's installation +// of X, which is what unattended work carries and what a person's capability +// never does. A person mint that let the seal drift to another audience's +// installation would be admitted there without the person's bearer. +// +// Every case below either mints a capability sealed to its audience's +// installation, or is refused. Each parent that is re-derived is sealed to a +// stale installation first, so a mint that copied the parent's seal fails here. +// A new person mint belongs in this table. + +// sealPinMint is one person mint: the server it ran on, the audience it asked +// for, and what it answered. +type sealPinMint func(t *testing.T) (server *WorkContextAuthorityServer, audience string, token string, err error) + +func TestAPersonsCapabilityIsSealedToItsAudiencesInstallation(t *testing.T) { + childRequest := func(parent, audience string) *gen.StartChildSessionWorkContextRequest { + return &gen.StartChildSessionWorkContextRequest{ + OrgId: renewOrgID, ParentWorkContextToken: parent, ActorPrincipalId: renewActorID, + Audience: audience, TtlSeconds: 60, + GrantedScopes: []*gen.WorkContextScope{{ResourceKind: "evidence", Actions: []string{"read"}}}, + } + } + issued := func(out *gen.IssuedWorkContext, err error) (string, error) { + return out.GetToken(), err + } + + for _, tc := range []struct { + name string + refused bool + mint sealPinMint + }{ + {name: "StartTask", mint: func(t *testing.T) (*WorkContextAuthorityServer, string, string, error) { + exchangeInstallationService(t) + server := newCoreTestServer(t, &workContextAuthorityFake{facts: &business.WorkContextAuthorityFacts{OrganizationRevision: 12}}, newSealAnswersFake(12)) + token, err := issued(server.StartTask(exchangeCaller(), &gen.StartTaskWorkContextRequest{ + OrgId: renewOrgID, TaskId: renewTaskID, Audience: "documents", TtlSeconds: 300, + AuthorityScopes: []*gen.WorkContextScope{{ResourceKind: "documents", Actions: []string{"read"}}}, + })) + return server, "documents", token, err + }}, + {name: "StartRootSession", mint: func(t *testing.T) (*WorkContextAuthorityServer, string, string, error) { + exchangeInstallationService(t) + server := newRenewTestServer(t) + parent := personCapability(t, server, "tool.test", "installation-retired") + token, err := issued(server.StartRootSession(exchangeCaller(), &gen.StartRootSessionWorkContextRequest{ + OrgId: renewOrgID, ParentWorkContextToken: parent, SessionId: renewSession, + Audience: "documents", TtlSeconds: 300, + })) + return server, "documents", token, err + }}, + {name: "ExchangeAudience", mint: func(t *testing.T) (*WorkContextAuthorityServer, string, string, error) { + exchangeInstallationService(t) + server := newRenewTestServer(t) + parent := personCapability(t, server, "tool.test", "installation-retired") + token, err := issued(server.ExchangeAudience(exchangeCaller(), &gen.ExchangeWorkContextAudienceRequest{ + OrgId: renewOrgID, ParentWorkContextToken: parent, Audience: "documents", TtlSeconds: 300, + AttenuatedScopes: []*gen.WorkContextScope{{ResourceKind: "evidence", Actions: []string{"read"}}}, + })) + return server, "documents", token, err + }}, + {name: "RenewWorkContext", mint: func(t *testing.T) (*WorkContextAuthorityServer, string, string, error) { + exchangeInstallationService(t) + server := newRenewTestServer(t) + parent := mintDelegatedContextSealedTo(t, server, "installation-retired") + token, err := issued(server.RenewWorkContext(exchangeCaller(), &gen.RenewWorkContextRequest{ + OrgId: renewOrgID, ParentWorkContextToken: parent, TtlSeconds: 900, + })) + return server, "tool.test", token, err + }}, + {name: "StartChildSession for the parent's audience", mint: func(t *testing.T) (*WorkContextAuthorityServer, string, string, error) { + exchangeInstallationService(t) + server := newRenewTestServer(t) + parent := personCapability(t, server, "tool.test", testInstallation("tool.test")) + token, err := issued(server.StartChildSession(exchangeCaller(), childRequest(parent, "tool.test"))) + return server, "tool.test", token, err + }}, + {name: "StartChildSession for another audience", refused: true, mint: func(t *testing.T) (*WorkContextAuthorityServer, string, string, error) { + exchangeInstallationService(t) + server := newRenewTestServer(t) + parent := personCapability(t, server, "tool.test", testInstallation("tool.test")) + token, err := issued(server.StartChildSession(exchangeCaller(), childRequest(parent, "documents"))) + return server, "documents", token, err + }}, + {name: "StartChildSession from a parent sealed to a replaced installation", refused: true, mint: func(t *testing.T) (*WorkContextAuthorityServer, string, string, error) { + exchangeInstallationService(t) + server := newRenewTestServer(t) + parent := personCapability(t, server, "tool.test", "installation-retired") + token, err := issued(server.StartChildSession(exchangeCaller(), childRequest(parent, "tool.test"))) + return server, "tool.test", token, err + }}, + {name: "a module's exchange through its operation binding", mint: func(t *testing.T) (*WorkContextAuthorityServer, string, string, error) { + _, _, client, _ := sourceReadFixture(t) + service.SetModuleAuthorityReads(currentModuleAuthority{service: service}, nil) + service.SetModuleCapabilities(nil, nil, installedOperationRegistry(true)) + parent, _ := mintTestCapability(t, workContextSingleton, testCapability{ + Audience: "example", Tenant: readOrg, Owner: readOwner, + Task: "019f6bf7-1111-7111-8111-111111111111", Session: "019f6bf7-2222-7222-8222-222222222222", + Scopes: []*basev0.WorkScopeV1{{ResourceKind: "results", Actions: []string{"read", "write"}, ResourceIds: []string{"result-1"}}}, + Installation: "installation-retired", + }) + module := mintModuleIdentity(t, business.ModulePrincipalID("example"), readOrg) + req := connect.NewRequest(&gen.ModuleExchangeDelegatedOperationAudienceRequest{BindingId: "generate", ParentWorkContextToken: parent}) + req.Header().Set(workContextHeader, module) + req.Header().Set("x-codefly-internal-token", "source-read-test-perimeter") + out, err := client.ExchangeDelegatedOperationAudience(context.Background(), req) + if err != nil { + return workContextSingleton, "example-producer", "", err + } + return workContextSingleton, "example-producer", out.Msg.Token, nil + }}, + } { + t.Run(tc.name, func(t *testing.T) { + server, audience, token, err := tc.mint(t) + if tc.refused { + require.Error(t, err, "a person mint that cannot keep its audience's seal must be refused") + require.Empty(t, token) + return + } + require.NoError(t, err) + minted, err := server.verifyIssued(context.Background(), token) + require.NoError(t, err) + claims := minted.Context() + require.Equal(t, business.PrincipalKindHuman, claims.GetOwnerPrincipalKind()) + require.Equal(t, audience, claims.GetAudience()) + require.Equal(t, testInstallation(audience), claims.GetSeal().GetInstallationId(), + "a person's capability is sealed to its audience's installation, never another's") + }) + } +} + +// The cross-installation case on its own: a person's capability for one audience +// cannot be extended by a child into another audience, because the child would +// keep the first audience's seal. The same-audience child, the hop a runtime +// takes for its own agent, is still minted. +func TestStartChildSessionRefusesAPersonsChildForAnotherAudiencesInstallation(t *testing.T) { + exchangeInstallationService(t) + server := newRenewTestServer(t) + parent := personCapability(t, server, "tool.test", testInstallation("tool.test")) + ctx := accountsauth.WithVerifiedSessionIDString(exchangeCaller(), renewSession) + request := func(audience string) *gen.StartChildSessionWorkContextRequest { + return &gen.StartChildSessionWorkContextRequest{ + OrgId: renewOrgID, ParentWorkContextToken: parent, ActorPrincipalId: renewActorID, + Audience: audience, TtlSeconds: 60, + GrantedScopes: []*gen.WorkContextScope{{ResourceKind: "evidence", Actions: []string{"read"}}}, + } + } + + _, err := server.StartChildSession(ctx, request("documents")) + require.Equal(t, codes.PermissionDenied, status.Code(err), "%v", err) + require.NotEqual(t, testInstallation("tool.test"), testInstallation("documents"), + "the two audiences must resolve to different installations, or the refusal proves nothing") + + same, err := server.StartChildSession(ctx, request("tool.test")) + require.NoError(t, err) + child, err := server.verifyIssued(context.Background(), same.GetToken()) + require.NoError(t, err) + require.Equal(t, testInstallation("tool.test"), child.Context().GetSeal().GetInstallationId()) +} diff --git a/module/services/auth-gateway/AGENTS.md b/module/services/auth-gateway/AGENTS.md index bb2aa391f..adda2e0ba 100644 --- a/module/services/auth-gateway/AGENTS.md +++ b/module/services/auth-gateway/AGENTS.md @@ -106,6 +106,14 @@ constitutes organization consent. Module identity exchanges are described below; accounts owns their authority. +A capability's seal follows its owner: a person's is sealed to the organization's +installation of the audience it addresses, a module or installation principal's +to its own. Accounts holds that as an invariant over every person mint, and +`TestAPersonsCapabilityIsSealedToItsAudiencesInstallation` in accounts +(`pkg/adapters/work_context_seal_pin_test.go`) pins it. The gateway reads no +owner (core's `Inspected` carries none), so any decision this gateway makes from +a seal rests on that test. + ## What a runtime-registered upstream receives, and what it does not A runtime-registered upstream — a federated module prefix or a solution — receives From 24fa55d17f41f7176258219742b9521ba8b8ea6c Mon Sep 17 00:00:00 2001 From: saurabhmaheshwari-obin <242053479+saurabhmaheshwari-obin@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:08:19 -0400 Subject: [PATCH 02/17] feat(auth-gateway): a declared module route admits a headless operation context with no bearer A composed workload with no person present (a solution's release publisher, for example) mints an operation context at /modules/_operation-context, bound to the target module's own audience, and had no way to present it at the one door: /v1//* ran ext_authz, which denies every request without a bearer. The declared module route now has two arms. A request with no Authorization and no Cookie header, on the workload endpoint, presenting an edge-inspected capability addressed to the alias, takes the bearer-less arm (gateway_workload_operation.go); everything else takes the person path unchanged. On the arm: - a deployed gateway requires the waypoint presenter again (the listener already does); - the signed tenant and seal must be non-empty (401 invalid work context); - every identity header is stripped and Authorization / Proxy-Authorization dropped BEFORE the limiter; - the limiter keys on workload-operation:, a namespaced branch of Middleware read from a private context value only the arm sets, and fails closed; - inside the limiter, accounts' InstallationService/ListInstallations (ACTIVE, uncached) must show an installation of the route's target (else 403) and the seal as a DIFFERENT active installation of that tenant (else 401 authentication required). A seal equal to the target's installation is a person's capability, which accounts now guarantees by invariant. An RPC error, an unterminated cursor or no client wired is 503; - the only stamped header is x-codefly-installation-id = the seal; no bearer, cookie or identity header is forwarded; an upstream failure is a static 502; - every admit and refusal is logged with alias, tenant, task, seal and presenter, never the token or an authority error. The ListInstallations messages are vendored as proto/saas/accounts/v1/installation_listing.proto: installations.proto imports the policy options and google.api the gateway does not vendor, so the file carries the six definitions the RPC uses, copied verbatim, and TestVendoredInstallationListingMatchesAccounts holds each byte-identical to accounts'. The client is accountsInstallations, in the shape of accountsSolutionEntitlements. Generated with `codefly generate proto` and `codefly generate contracts saas-starter` (CLI v0.1.207, whose no-change runs came back clean on both): the new .pb.go, the gateway contract copy, its contract.binpb and the digest in module.package.codefly.yaml and catalog.codefly.json. Tests: the admitted case, every refusal with one fact broken, paging, logging, the static 502, the edge refusals (expired, forged, foreign issuer), the empty-tenant guard, caller-asserted headers on the arm, a bearer on the workload listener taking the person path, grpc-metadata stripping, a Basic credential leaving the arm, Proxy-Authorization, the tenant-keyed bucket, limiter fail-closed, and an in-process mint-then-publish round trip. Existing tests pass unedited; test helpers gained an installation and issuer on edgeCapability, a call counter on fakeUpstream, and a token override on the fake operation-context mint. Docs: GATEWAY_ROUTES.md (the two arms), WORK_CONTEXTS.md (how a minted operation context is presented), the auth-gateway AGENTS.md (the arm, SP-GW-07's positive half met on this path, the fourth workload-only surface), and the comments on removePersonsSessionCredential, installationConfinementHeader (its meaning on each arm), gateway_module_capability.go and handleDeclaredModule. Second consumer: any solution with a release-declared headless operation uses this arm as is. Modules calling peers keep their in-mesh seam. Co-Authored-By: Claude Opus 5.5 --- module/GATEWAY_ROUTES.md | 30 + module/WORK_CONTEXTS.md | 14 + .../api/auth-gateway/grpc/contract.binpb | Bin 348325 -> 355469 bytes .../accounts/v1/installation_listing.proto | 113 ++++ module/contracts/api/catalog.codefly.json | 2 +- module/module.package.codefly.yaml | 2 +- module/services/auth-gateway/AGENTS.md | 33 +- .../auth-gateway/code/attack_battery_test.go | 104 ++++ module/services/auth-gateway/code/gateway.go | 33 +- .../code/gateway_module_capability.go | 5 + .../code/gateway_module_confinement_test.go | 42 ++ .../code/gateway_module_routes.go | 32 +- .../code/gateway_module_routes_test.go | 425 +++++++++++++ .../code/gateway_operation_context_test.go | 56 +- .../auth-gateway/code/gateway_test.go | 3 + .../code/gateway_workload_operation.go | 288 +++++++++ .../code/installation_listing_vendor_test.go | 32 + module/services/auth-gateway/code/main.go | 4 + .../code/perimeter_workos_free_test.go | 6 + .../accounts/v1/installation_listing.pb.go | 588 ++++++++++++++++++ .../services/auth-gateway/code/ratelimit.go | 10 + module/services/auth-gateway/code/routing.go | 1 + .../auth-gateway/code/work_context_test.go | 16 +- .../accounts/v1/installation_listing.proto | 113 ++++ 24 files changed, 1931 insertions(+), 21 deletions(-) create mode 100644 module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/installation_listing.proto create mode 100644 module/services/auth-gateway/code/gateway_workload_operation.go create mode 100644 module/services/auth-gateway/code/installation_listing_vendor_test.go create mode 100644 module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go create mode 100644 module/services/auth-gateway/proto/saas/accounts/v1/installation_listing.proto diff --git a/module/GATEWAY_ROUTES.md b/module/GATEWAY_ROUTES.md index dc842b7ab..a539b02b0 100644 --- a/module/GATEWAY_ROUTES.md +++ b/module/GATEWAY_ROUTES.md @@ -112,6 +112,36 @@ correspondence check. The generated and explicit route catalogs own the complete module REST surface. Unknown paths return 404. Module workloads cannot add a proxy target at runtime. +A composed module declared in the solution registry is reached at +`/v1//*`, only after both catalogs found no route, with the path +forwarded unchanged (`handleDeclaredModule`). An edge-inspected Work Context +there may name the alias or `module-capabilities`; anything else is 401 +`invalid work context`. The route then has two arms: + +- **The person's bearer path.** ext_authz verifies the bearer and stamps the + person; accounts admits the viewer to the module's installation per request; + the bearer travels (SA-F-BEARER), the cookie never does; + `x-codefly-installation-id` names the target's admitting installation. +- **The bearer-less arm, on the workload endpoint only**, for a headless + operation context. Selected by: no `Authorization` and no `Cookie` header, the + workload listener, and a capability whose audience is the alias. Then: a + presenter when the gateway is deployed; a non-empty signed tenant and seal; + every identity header stripped, `Authorization` and `Proxy-Authorization` + dropped; the budget keyed `workload-operation:`, failing closed; and + accounts' `ListInstallations` for that tenant (ACTIVE, uncached) must show an + installation of the target (else 403) and the seal as a DIFFERENT active + installation (else 401 `authentication required`, the answer a person's + capability has without its bearer). 503 when the authority cannot answer or + is not wired. Admitted requests carry no bearer and no identity header, and + `x-codefly-installation-id` is set to the seal, which names the CALLER's + installation, not consent on the target's side. An upstream failure is a + static 502. Every admit and refusal is logged with alias, tenant, task, seal + and presenter, never the token. + +Elsewhere the same request is a bearer-less request: 401 on `rest`, 401 with a +`module-capabilities` capability, 401 for a capability sealed to the target's own +installation. + `saas.frontend.plugins.v1` catalogs all 36 Next.js pages and the admin plugin catch-all. The environment topology generator owns the deployed ingress VirtualService, so the target-neutral route catalog does not carry a namespace, diff --git a/module/WORK_CONTEXTS.md b/module/WORK_CONTEXTS.md index 855d620e4..5e298426b 100644 --- a/module/WORK_CONTEXTS.md +++ b/module/WORK_CONTEXTS.md @@ -645,6 +645,20 @@ A composed module reaches it through the gateway's `POST /modules/_operation-context` ([services/auth-gateway/AGENTS.md](./services/auth-gateway/AGENTS.md)), which brokers to accounts exactly as `/modules/_work-context` does. +The minted context is presented at `/v1//*` on the same workload +endpoint, with no bearer: the declared module route's bearer-less arm. A +composed workload with no person present, such as a solution's release +publisher, reaches the module it is bound to this way; modules keep calling +their peers in-mesh. The edge checks what core's `Inspector` can (signature, +issuer, window, the route's audience, shape), then the signed tenant's ACTIVE +installations: the target must be installed there, and the seal must name a +different active installation, which is what unattended work carries and what a +person's capability never does (see the seal rule above). It strips every +identity header, meters on the tenant, forwards the capability as presented and +stamps only `x-codefly-installation-id` with the seal. The callee verifies the +capability at full strength, as for any other carrier. The route's rules are in +[GATEWAY_ROUTES.md](./GATEWAY_ROUTES.md#module-rest-routes). + ## Operation contexts from a source delegation A datasource source's sync is neither of the cases above. It runs as a module diff --git a/module/contracts/api/auth-gateway/grpc/contract.binpb b/module/contracts/api/auth-gateway/grpc/contract.binpb index f40233bbe3c96b0e68eea66a5d736321c2f4e77f..41a9e6c93916b2eeff1a9cf39143c2199ddd35bb 100644 GIT binary patch delta 6263 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 diff --git a/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/installation_listing.proto b/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/installation_listing.proto new file mode 100644 index 000000000..985e1a29c --- /dev/null +++ b/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/installation_listing.proto @@ -0,0 +1,113 @@ +syntax = "proto3"; + +package saas.accounts.v1; + +import "buf/validate/validate.proto"; +import "google/protobuf/timestamp.proto"; + +// ============================================================================ +// The installation listing, as the auth-gateway reads it. +// +// accounts declares these messages in installations.proto beside the +// InstallationService, which imports the policy options and google.api this +// gateway does not vendor. The gateway calls one internal-tier RPC, +// InstallationService/ListInstallations, so it carries the messages that RPC +// takes and answers, copied definition for definition from installations.proto +// and nothing else. TestVendoredInstallationListingMatchesAccounts holds every +// definition here byte-identical to the one accounts declares, so the two +// cannot drift on the wire. +// ============================================================================ + +enum InstallationStatus { + INSTALLATION_STATUS_UNSPECIFIED = 0; + INSTALLATION_STATUS_ACTIVE = 1; + INSTALLATION_STATUS_REVOKED = 2; + // Installed, and waiting for the solution to acknowledge its standard install + // step. Nothing can use a pending installation. + INSTALLATION_STATUS_PENDING = 3; + // The solution refused its install step, or never answered within the + // attempt budget. Nothing can use it; an administrator may retry it. + INSTALLATION_STATUS_FAILED = 4; +} + +enum InstallationHealth { + INSTALLATION_HEALTH_UNSPECIFIED = 0; + INSTALLATION_HEALTH_HEALTHY = 1; + INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER = 2; + INSTALLATION_HEALTH_AGENT_REVOKED = 3; + INSTALLATION_HEALTH_AGENT_DISABLED = 4; + INSTALLATION_HEALTH_STANDING_GRANT_MISSING = 5; +} + +message Installation { + string id = 1 [(buf.validate.field).string.uuid = true]; + string org_id = 2 [(buf.validate.field).string.uuid = true]; + string agent_principal_id = 3 [(buf.validate.field).string.uuid = true]; + // Was `solution_identifier`: the free-text route alias. It was the join key + // every reader used, and because an alias is deliberately reusable by a later + // binding, an installation that named one inherited whatever took it next. + // Replaced by target_id below. Reserved rather than reused so no consumer can + // read an alias out of a field that now carries an identity. + reserved 4; + reserved "solution_identifier"; + string owner_principal_id = 5 [(buf.validate.field).string.uuid = true]; + repeated string co_owner_principal_ids = 6; + string root_scope_node_id = 7 [(buf.validate.field).string.uuid = true]; + InstallationStatus status = 8; + google.protobuf.Timestamp created_at = 9; + optional google.protobuf.Timestamp revoked_at = 10; + // The immutable solution target this installation was consented to: one + // continuous period of one binding's presence on this host. It is never + // reused, so a replacement binding — even one claiming the same route alias — + // is a different target and inherits nothing. + // + // Deliberately NOT accompanied by the route alias. The alias is a property of + // the target that a later generation may move, and resolving it here would + // mean joining the host's presence state into a request-path listing — which + // would require granting request traffic read over every organisation's + // presence to answer about one. A consumer that needs the alias asks + // ListAvailableSolutions, which serves accepted applied state, and joins on + // this id. + // + // Empty only on a revoked historical row whose target could not be + // established at the cutover; an active installation always names one. + string target_id = 11 [ + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + (buf.validate.field).string.uuid = true + ]; + // Why a revoked installation was revoked, when it was not an administrator + // uninstalling: the installation-identity cutover, or a tenancy narrowing that + // withdrew this organisation. Empty on an active row. + string revoked_reason = 12 [(buf.validate.field).string.max_len = 512]; + // Why the install step failed: the solution's bounded refusal reason, or a + // fixed reason when it never answered. Empty unless status is FAILED. + string failure_reason = 13 [(buf.validate.field).string.max_len = 512]; +} + +message ListInstallationsRequest { + string org_id = 1 [(buf.validate.field).string.uuid = true]; + // Restrict to one lifecycle status. UNSPECIFIED returns every status, so a + // caller that must see only live installs asks for ACTIVE explicitly rather + // than leaning on a default that would silently widen if the enum grew. + InstallationStatus status = 2; + // Page bound. 0 -> server default; capped so an organization with many + // installations can never return an unbounded result in one call. + int32 page_size = 3 [(buf.validate.field).int32 = { + gte: 0 + lte: 500 + }]; + // Opaque cursor from a previous response's next_page_token; empty for page one. + string page_token = 4; +} + +message InstallationSummary { + Installation installation = 1; + InstallationHealth health = 2; +} + +message ListInstallationsResponse { + repeated InstallationSummary installations = 1; + // Set when more installations remain; pass it back as page_token. Empty on the + // last page. + string next_page_token = 2; +} diff --git a/module/contracts/api/catalog.codefly.json b/module/contracts/api/catalog.codefly.json index 905df972e..dda7f0a43 100644 --- a/module/contracts/api/catalog.codefly.json +++ b/module/contracts/api/catalog.codefly.json @@ -562,7 +562,7 @@ "kind": "protobuf", "package": "saas.gateway.auth.v1", "path": "contracts/api/auth-gateway/grpc/contract.binpb", - "digest": "sha256:a54c94b3d74017a0493e0b08e799a59c4bb2979779b66b75426d6d1095d31747", + "digest": "sha256:b9747dbf86865b80102acc72d003c1f133ef5df6629f5c5d9adc86302951e9ff", "services": [ { "name": "AuthGatewayService", diff --git a/module/module.package.codefly.yaml b/module/module.package.codefly.yaml index 5401faf8b..7ffdb415f 100644 --- a/module/module.package.codefly.yaml +++ b/module/module.package.codefly.yaml @@ -31,7 +31,7 @@ services: kind: protobuf package: saas.gateway.auth.v1 path: contracts/api/auth-gateway/grpc/contract.binpb - digest: sha256:a54c94b3d74017a0493e0b08e799a59c4bb2979779b66b75426d6d1095d31747 + digest: sha256:b9747dbf86865b80102acc72d003c1f133ef5df6629f5c5d9adc86302951e9ff - name: cache endpoints: [read, write] - name: frontend diff --git a/module/services/auth-gateway/AGENTS.md b/module/services/auth-gateway/AGENTS.md index adda2e0ba..d4d6c4536 100644 --- a/module/services/auth-gateway/AGENTS.md +++ b/module/services/auth-gateway/AGENTS.md @@ -100,6 +100,25 @@ live entitlement to the resolved target, without caching that decision. Accounts intersects the delivered authority, envelope and organization's installation scope. The path is forwarded unchanged; a module owns its `/v1/` surface. +**A declared module route has two arms.** A request carrying a bearer (or a +cookie, or arriving on `rest`) takes the person path above. A request on the +workload endpoint with no `Authorization` and no `Cookie`, presenting a +capability addressed to the alias, takes the bearer-less arm +(`gateway_workload_operation.go`): a composed workload's headless operation +context, minted at `/modules/_operation-context`. The arm reads no principal. It +requires the signed tenant to have an ACTIVE installation of the target and the +seal to name a different ACTIVE installation in that tenant, read from accounts' +`ListInstallations` per request and uncached; a seal equal to the target's +installation is a person's capability and answers 401 as before. It strips +identity before its limiter, meters on `workload-operation:` (fail +closed), forwards no bearer, stamps only `x-codefly-installation-id` (the seal, +so the CALLER's installation on this arm and the target's admitting installation +on the person path; equal to the presented seal on both), answers an upstream +failure with a static 502, and logs every admit and refusal with alias, tenant, +task, seal and presenter. No installations client wired answers 503. It is the +gateway's half of a solution's release-declared headless operation; a module +calling a peer keeps its in-mesh seam instead. + The registry's `SolutionDeclaredBinding.kind` positively identifies each surface. An unspecified kind is refused by both. Neither presence nor a reusable alias constitutes organization consent. @@ -149,9 +168,12 @@ An upstream also receives the identity `ext_authz` stamped: the subject, the ten the session, the credential kind and the scope ceiling. That names the person independently of the bearer beside it. -**SP-GW-07's second half is open, recorded as `SA-F-BEARER`.** The invariant is that -an upstream receives a host-minted context bound to THAT upstream's own audience — -not merely that it stops receiving the person's session. The host access token +**SP-GW-07's second half is open on the person path, recorded as `SA-F-BEARER`.** +The invariant is that an upstream receives a host-minted context bound to THAT +upstream's own audience — not merely that it stops receiving the person's session. +The bearer-less module arm is the first path that meets it: the upstream receives +an operation context accounts minted for its own audience, and no bearer, no +cookie and no identity header. A person's request still carries the bearer. The host access token carries one host-wide audience and the person's full authority, so a declared upstream holding it holds more than it needs, and that exposure is real while this stays open. What closes it: a host-minted context bound to the upstream's own @@ -387,6 +409,11 @@ admits the waypoint alone; comparing `By` does not establish that network fact. The three module credential exchanges (`/modules/_work-context`, `/modules/_operation-context`, `/modules/_source-operation-context`) are served on this endpoint and nowhere else: on `rest` they are unexposed paths (`404`). +The fourth workload-only surface is the bearer-less arm of `/v1//*` +(above): on `rest` the same request is an unauthenticated one, `401`. Unlike the +exchanges it proves no workload with a projected token: it admits a capability +on the strength of its signature, its seal and, on a cell, the presenter. A +deployed gateway requires that presenter again in the arm itself. The listener puts the authenticated presenter in private request context and removes the raw headers. `moduleMintContext` sends that principal to accounts with the existing gateway-provenance credential, separately from the internal diff --git a/module/services/auth-gateway/code/attack_battery_test.go b/module/services/auth-gateway/code/attack_battery_test.go index 9892d9d8b..0f246ff2f 100644 --- a/module/services/auth-gateway/code/attack_battery_test.go +++ b/module/services/auth-gateway/code/attack_battery_test.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "net/http" "net/http/httptest" "strings" @@ -37,3 +38,106 @@ func TestAttack_PerimeterStripsGrpcMetadataIdentityHeaders(t *testing.T) { "caller-supplied %s reached the accounts upstream", name) } } + +// TestAttack_TheBearerLessArmStripsGrpcMetadataIdentityHeaders: the arm runs no +// ext_authz and stamps no identity, so a grpc-metadata spelling of one is the +// only identity an upstream could read. It never arrives. +func TestAttack_TheBearerLessArmStripsGrpcMetadataIdentityHeaders(t *testing.T) { + gw, module, _, kid, wcPriv := operationHarness(t) + req := onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) + req.Header.Set("Grpc-Metadata-X-User-Id", "attacker-chosen") + req.Header.Set("Grpc-Metadata-X-Org-Id", "attacker-chosen") + + w := httptest.NewRecorder() + gw.ServeHTTP(w, req) + require.Equal(t, http.StatusOK, w.Code, w.Body.String()) + + for name := range module.lastHeaders { + require.Falsef(t, strings.HasPrefix(strings.ToLower(name), "grpc-metadata-"), + "caller-supplied %s reached the module upstream", name) + } +} + +// TestAttack_AnyAuthorizationSchemeLeavesTheArm: the arm is selected only with no +// Authorization header at all. A Basic credential is not "no bearer"; it takes +// the person path, which accepts no such scheme. +func TestAttack_AnyAuthorizationSchemeLeavesTheArm(t *testing.T) { + gw, module, installations, kid, wcPriv := operationHarness(t) + req := onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) + req.Header.Set("Authorization", "Basic dXNlcjpwYXNz") + + w := httptest.NewRecorder() + gw.ServeHTTP(w, req) + + require.Equal(t, http.StatusUnauthorized, w.Code) + require.Zero(t, module.calls) + require.Empty(t, installations.calls, "the arm was not selected") +} + +// TestAttack_TheBearerLessArmForwardsNoProxyCredential: a Proxy-Authorization +// the caller sent is not a credential this gateway reads, and never reaches the +// module. +func TestAttack_TheBearerLessArmForwardsNoProxyCredential(t *testing.T) { + gw, module, _, kid, wcPriv := operationHarness(t) + req := onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) + req.Header.Set("Proxy-Authorization", "Basic cHJveHk6c2VjcmV0") + + w := httptest.NewRecorder() + gw.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code, w.Body.String()) + require.Empty(t, module.lastHeaders.Values("Proxy-Authorization")) +} + +// TestAttack_TheBearerLessArmIsMeteredOnItsSignedTenant: with no X-Org-Id the +// generic key would be the waypoint's address, one bucket for every tenant on a +// cell. The arm's budget is the tenant the capability is signed for, in its own +// namespace, read after the strip: a caller's X-Org-Id and its source address +// open no new bucket. Exhausting it refuses before the authority is asked. +func TestAttack_TheBearerLessArmIsMeteredOnItsSignedTenant(t *testing.T) { + gw, module, installations, kid, wcPriv := operationHarness(t) + backend := &memoryBackend{} + gw.rateLimiter = &RateLimiter{backend: backend, limit: 2, burst: 1, stop: make(chan struct{}), proxies: newProxyTrust("")} + capability := operationCapability(t, kid, wcPriv, "documents", publisherSeal) + logs := captureLog(t) + + serve := func(i int) int { + req := onWorkloadEndpoint(publishRequest(capability)) + req.Header.Set("X-Org-Id", fmt.Sprintf("caller-org-%d", i)) + req.RemoteAddr = fmt.Sprintf("198.51.100.%d:1234", i+1) + w := httptest.NewRecorder() + gw.ServeHTTP(w, req) + return w.Code + } + for i := 0; i < 3; i++ { + require.Equal(t, http.StatusOK, serve(i), "request %d is within limit plus burst", i) + } + require.Equal(t, http.StatusTooManyRequests, serve(3)) + require.Equal(t, 3, module.calls) + require.Len(t, installations.calls, 3, "a limited request never reaches the admission read") + + var keys []string + backend.windows.Range(func(key, _ any) bool { + keys = append(keys, key.(string)) + return true + }) + require.Len(t, keys, 1, "one bucket, whatever X-Org-Id and source address the caller sent") + require.True(t, strings.HasPrefix(keys[0], "rl:workload-operation:"+operationTenant+":"), keys[0]) + require.Contains(t, logs.String(), "reason=workload_operation_limited") +} + +// TestAttack_TheBearerLessArmFailsClosedWithoutItsLimiter: the arm's budget is a +// security budget. A limiter backend that cannot answer refuses the request; it +// does not wave it through to the authority read. +func TestAttack_TheBearerLessArmFailsClosedWithoutItsLimiter(t *testing.T) { + gw, module, installations, kid, wcPriv := operationHarness(t) + gw.rateLimiter = &RateLimiter{backend: failingRateLimitBackend{}, limit: 100, burst: 20, stop: make(chan struct{}), proxies: newProxyTrust("")} + + w := httptest.NewRecorder() + gw.ServeHTTP(w, onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal)))) + + require.Equal(t, http.StatusServiceUnavailable, w.Code) + require.Contains(t, w.Body.String(), "rate limit service unavailable") + require.Zero(t, module.calls) + require.Empty(t, installations.calls) +} diff --git a/module/services/auth-gateway/code/gateway.go b/module/services/auth-gateway/code/gateway.go index e3b25c7d7..1a8d750de 100644 --- a/module/services/auth-gateway/code/gateway.go +++ b/module/services/auth-gateway/code/gateway.go @@ -62,6 +62,14 @@ type Gateway struct { // solution a viewer is currently using. solutionEntitlements solutionEntitlementClient workContext *workContextVerifier + // installations answers an organization's ACTIVE installations for the + // bearer-less module arm (gateway_workload_operation.go). Set after + // construction; nil answers 503 there and never admits. + installations installationListingClient + // requireWorkloadPresenter makes the bearer-less arm require the waypoint + // presenter the workload listener authenticated. Set outside native + // development, where the listener already requires one. + requireWorkloadPresenter bool } // NewGateway constructs a gateway with explicit route matching. @@ -513,6 +521,13 @@ func (g *Gateway) proxyTo(w http.ResponseWriter, r *http.Request, upstream *url. proxy.Transport = g.registeredTransport } proxy.ErrorHandler = func(w http.ResponseWriter, _ *http.Request, err error) { + if entry != nil && entry.staticUpstreamError { + // The dial detail stays in this log (SP-GW-13): a caller of a route + // that asks for it learns only that the upstream failed. + log.Printf("WARN: upstream error: service=%s err=%v", entry.Service, err) + httpError(w, http.StatusBadGateway, "upstream error") + return + } httpError(w, http.StatusBadGateway, "upstream error: "+err.Error()) } proxy.ServeHTTP(w, r) @@ -575,8 +590,14 @@ func (g *Gateway) proxyTo(w http.ResponseWriter, r *http.Request, upstream *url. // // For every other upstream path the host-wide session is still removed, and an upstream // that needs to act on the person's behalf there still needs a host-minted capability -// bound to its own audience. **That remains unbuilt: see the audience-bound context -// note in services/auth-gateway/AGENTS.md.** +// bound to its own audience. **That remains unbuilt for a person: see the +// audience-bound context note in services/auth-gateway/AGENTS.md.** +// +// A HEADLESS caller now gets exactly that capability. The declared module route's +// bearer-less arm (gateway_workload_operation.go) forwards an operation context +// accounts minted for the upstream's own audience, with no bearer, no cookie and +// no identity header: the first path that meets SP-GW-07's positive half. The +// person's path stays open. // // Catalog routes are untouched: accounts is the host's own API, where the session // is the credential. @@ -896,6 +917,14 @@ const resourceConfinementHeader = "x-codefly-resource" // resourceConfinementHeader is, because ext_authz does not consult the // entitlement authority and an empty value here would be a claim this process // cannot make. +// +// WHAT IT MEANS differs by arm, and one statement holds on both: it equals the +// seal of the capability presented with the request. On a person's path it is +// the TARGET's admitting installation, and a person's capability for that target +// is sealed to it. On a declared module route's bearer-less arm it is the seal of +// the headless operation context, which names the CALLER's own installation. +// There it is not consent on the target's side, and an upstream must not read it +// as such; the capability itself is what the upstream verifies. const installationConfinementHeader = "x-codefly-installation-id" // solutionIdentityHeader and solutionPublisherHeader are the headers accounts diff --git a/module/services/auth-gateway/code/gateway_module_capability.go b/module/services/auth-gateway/code/gateway_module_capability.go index 8fd686681..17095a64e 100644 --- a/module/services/auth-gateway/code/gateway_module_capability.go +++ b/module/services/auth-gateway/code/gateway_module_capability.go @@ -38,6 +38,11 @@ package main // browser session, whose session id is its own, and never by anyone else. The // owner is therefore not compared here, which keeps this hop to what core's // Inspector exposes: a hop routes, it never reads the principal it would act as. +// +// This pairing is the BEARER path's rule only. The route's bearer-less arm +// (gateway_workload_operation.go) has no bearer to pair a capability with: it +// pairs nothing, reads no principal, and decides by the capability's seal against +// the target's installation in the signed tenant. import ( "context" diff --git a/module/services/auth-gateway/code/gateway_module_confinement_test.go b/module/services/auth-gateway/code/gateway_module_confinement_test.go index d822d4ded..3b67bf708 100644 --- a/module/services/auth-gateway/code/gateway_module_confinement_test.go +++ b/module/services/auth-gateway/code/gateway_module_confinement_test.go @@ -13,6 +13,7 @@ package main import ( "crypto/ed25519" + "errors" "net/http" "net/http/httptest" "testing" @@ -265,3 +266,44 @@ func TestModuleRoute_TheSessionIsTheBindingAndTheOwnerIsNotRead(t *testing.T) { require.Equal(t, http.StatusOK, w.Code) require.NotNil(t, module.lastHeaders) } + +// ON THE BEARER-LESS ARM a caller can assert no identity and no installation: +// everything it sent under a gateway-owned name is gone, and the installation +// header is the seal the capability is signed with. +func TestModuleRoute_TheBearerLessArmTakesNoIdentityFromTheCaller(t *testing.T) { + gw, module, _, kid, wcPriv := operationHarness(t) + req := onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) + req.Header.Set(installationConfinementHeader, targetInstallation) + req.Header.Set(resourceConfinementHeader, solutionMCPResource(testPublicBase, "something-else")) + req.Header.Set("X-Org-Id", "55555555-5555-4555-8555-555555555555") + req.Header.Set("X-User-Id", "attacker") + req.Header.Set(workloadPresenterHeader, "spiffe://cluster.local/ns/other/sa/other") + + w := httptest.NewRecorder() + gw.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code, w.Body.String()) + require.Equal(t, publisherSeal, module.lastHeaders.Get(installationConfinementHeader), + "the installation is the signed seal, not the one the caller named") + for _, header := range []string{resourceConfinementHeader, "X-Org-Id", "X-User-Id", workloadPresenterHeader} { + require.Empty(t, module.lastHeaders.Values(header), "%s is the gateway's, never the caller's", header) + } +} + +// A bearer beside an alias capability on the WORKLOAD listener is a person's +// request and takes the person path: ext_authz verifies the bearer and stamps the +// person, and the viewer admission names the target's installation. +func TestModuleRoute_ABearerOnTheWorkloadListenerTakesThePersonPath(t *testing.T) { + gw, module, _, caller, kid, wcPriv, _ := confinedModuleHarness(t) + gw.installations = &installationsFake{err: errors.New("the arm must not be consulted")} + capability := mintViewerCapability(t, kid, wcPriv, "documents", caller.subject, caller.org, caller.session) + + w := httptest.NewRecorder() + gw.ServeHTTP(w, onWorkloadEndpoint(moduleRequest(caller, capability))) + + require.Equal(t, http.StatusOK, w.Code, w.Body.String()) + require.Equal(t, caller.subject, module.lastHeaders.Get("X-User-Id"), "ext_authz ran and stamped the person") + require.Equal(t, "install-"+fakeSolutionTarget("documents"), module.lastHeaders.Get(installationConfinementHeader), + "the viewer admission ran and named the target's installation") + require.Equal(t, "Bearer "+caller.bearer, module.lastHeaders.Get("Authorization")) +} diff --git a/module/services/auth-gateway/code/gateway_module_routes.go b/module/services/auth-gateway/code/gateway_module_routes.go index eb16bfe45..2122c82ee 100644 --- a/module/services/auth-gateway/code/gateway_module_routes.go +++ b/module/services/auth-gateway/code/gateway_module_routes.go @@ -156,9 +156,21 @@ func (g *Gateway) handleDeclaredModule(w http.ResponseWriter, r *http.Request) b return true } - // Same identity discipline as every protected route: drop caller-supplied - // identity, run ext_authz, require a valid credential. A bearer-less call is - // denied here, so a declared module adds a proxy target and never widens the + // TWO ARMS from here. A request with no bearer and no cookie, on the + // workload endpoint, presenting a capability for THIS alias, is a composed + // workload's headless operation context: there is no person, so the person + // path below cannot admit it, and gateway_workload_operation.go decides it + // from the capability's signed tenant and seal. Everything else takes the + // person path, unchanged. + if selectsWorkloadOperation(r, capability, alias) { + g.serveWorkloadOperation(w, r, alias, routing, capability) + return true + } + + // The person path. Same identity discipline as every protected route: drop + // caller-supplied identity, run ext_authz, require a valid credential. A + // bearer-less call that the arm above did not select is denied here, so on + // this path a declared module adds a proxy target and never widens the // authenticated surface. // // The verified capability is carried INTO the check, because it is what lets a @@ -189,13 +201,13 @@ func (g *Gateway) handleDeclaredModule(w http.ResponseWriter, r *http.Request) b injectHeaders(r, checkResp.GetOkResponse().GetHeaders()) // Forward the full /v1//... path unchanged: the module owns and serves - // its own /v1/ surface. The caller's bearer travels with it, because a - // declared upstream authenticates the request by it and the audience-bound - // capability that should replace it exists on neither side yet (SA-F-BEARER, - // open — see removePersonsSessionCredential in gateway.go). The caller's - // COOKIE does not travel, and a resource-bound token cannot be replayed off - // its own solution, so what an upstream holds is the person's host session - // and nothing wider. + // its own /v1/ surface. On this path the caller's bearer travels with + // it, because a declared upstream authenticates a PERSON's request by it and + // no capability minted from a browser session replaces it yet (SA-F-BEARER, + // open on this path; see removePersonsSessionCredential in gateway.go). The + // bearer-less arm above forwards none. The caller's COOKIE does not travel, + // and a resource-bound token cannot be replayed off its own solution, so what + // an upstream holds is the person's host session and nothing wider. // // A module data endpoint and its admission read must consume the // same per-org/per-IP budget an equivalent catalog route does, or /v1//* diff --git a/module/services/auth-gateway/code/gateway_module_routes_test.go b/module/services/auth-gateway/code/gateway_module_routes_test.go index cce968e42..bd9485bb9 100644 --- a/module/services/auth-gateway/code/gateway_module_routes_test.go +++ b/module/services/auth-gateway/code/gateway_module_routes_test.go @@ -1,13 +1,22 @@ package main import ( + "bytes" "context" + "crypto/ed25519" + "errors" + "fmt" + "log" "net/http" "net/http/httptest" + "strings" "testing" + "time" accountsv1 "auth-gateway/pkg/gen/saas/accounts/v1" + "github.com/codefly-dev/core/workcontext" + carrier "github.com/codefly-dev/sdk-go/workcontext" "github.com/stretchr/testify/require" ) @@ -303,3 +312,419 @@ func TestAModuleIsServingOnItsBackendHalfAlone(t *testing.T) { require.Contains(t, w.Body.String(), "module registration not active") require.Nil(t, fake.lastHeaders) } + +// --------------------------------------------------------------------------- +// The bearer-less arm (gateway_workload_operation.go): a composed workload's +// headless operation context, presented on the workload endpoint. +// --------------------------------------------------------------------------- + +const ( + // operationTenant is the tenant every operation capability here is signed for. + operationTenant = "11111111-1111-4111-8111-111111111111" + // publisherSeal is the installation a module operation context is sealed to: + // the MINTING workload's own installation in the tenant. + publisherSeal = "22222222-2222-4222-8222-222222222222" + // targetInstallation is the tenant's installation of the target module, the + // one a person's capability for it would be sealed to. + targetInstallation = "33333333-3333-4333-8333-333333333333" +) + +// installationsFake answers ListInstallations from pages, page N answering the +// cursor "page-N" (page 0 the empty cursor), and records every request. +type installationsFake struct { + pages []*accountsv1.ListInstallationsResponse + err error + // endless answers every page with a further cursor. + endless bool + calls []*accountsv1.ListInstallationsRequest +} + +func (f *installationsFake) List( + _ context.Context, req *accountsv1.ListInstallationsRequest, +) (*accountsv1.ListInstallationsResponse, error) { + f.calls = append(f.calls, req) + if f.err != nil { + return nil, f.err + } + if f.endless { + return &accountsv1.ListInstallationsResponse{NextPageToken: "more"}, nil + } + for index, page := range f.pages { + if req.GetPageToken() == pageCursor(index) { + return page, nil + } + } + return &accountsv1.ListInstallationsResponse{}, nil +} + +func pageCursor(index int) string { + if index == 0 { + return "" + } + return fmt.Sprintf("page-%d", index) +} + +// activeInstallation is one ACTIVE row of operationTenant. +func activeInstallation(id, target string) *accountsv1.InstallationSummary { + return &accountsv1.InstallationSummary{Installation: &accountsv1.Installation{ + Id: id, OrgId: operationTenant, TargetId: target, + Status: accountsv1.InstallationStatus_INSTALLATION_STATUS_ACTIVE, + }} +} + +// tenantInstalled is a tenant that installed the target module (E) and the +// publishing workload (W). +func tenantInstalled() *installationsFake { + return &installationsFake{pages: []*accountsv1.ListInstallationsResponse{{ + Installations: []*accountsv1.InstallationSummary{ + activeInstallation(targetInstallation, fakeSolutionTarget("documents")), + activeInstallation(publisherSeal, fakeSolutionTarget("publisher")), + }, + }}} +} + +// operationHarness is a gateway with the edge verifier, the module "documents" +// declared, and a tenant that installed it and the publishing workload. +func operationHarness(t *testing.T) (gw *Gateway, module *fakeUpstream, installations *installationsFake, kid string, wcPriv ed25519.PrivateKey) { + t.Helper() + gw, _, _, _ = newGatewayHarness(t) + pub, wcPriv := mustEd25519(t) + kid = "key-1" + jwks := jwksServer(t, jwksDocument(map[string]ed25519.PublicKey{kid: pub}), nil) + gw.workContext = newWorkContextVerifier(jwks.URL) + module = declareModule(t, gw, "documents") + installations = tenantInstalled() + gw.installations = installations + return gw, module, installations, kid, wcPriv +} + +// operationCapability is a module operation context for audience, sealed to seal. +func operationCapability(t *testing.T, kid string, priv ed25519.PrivateKey, audience, seal string) string { + t.Helper() + return mintCapability(t, kid, priv, edgeCapability{ + Audience: audience, Tenant: operationTenant, Owner: "module-principal", Session: "session-op", Installation: seal, + }) +} + +func publishRequest(capability string) *http.Request { + req := httptest.NewRequest(http.MethodPost, "/v1/documents/definitions:publish", strings.NewReader(`{"bundle":"b"}`)) + if capability != "" { + req.Header.Set(carrier.HeaderName, capability) + } + return req +} + +// captureLog sends the standard logger to a buffer for the rest of the test. +func captureLog(t *testing.T) *bytes.Buffer { + t.Helper() + var buf bytes.Buffer + previous := log.Writer() + log.SetOutput(&buf) + t.Cleanup(func() { log.SetOutput(previous) }) + return &buf +} + +// THE ARM ADMITS A HEADLESS OPERATION CONTEXT. No bearer, the workload endpoint, +// a capability for this alias sealed to the publisher's own installation (W) +// in a tenant that installed the target (E, not W). +// +// What the upstream holds is the capability and the one header stating its +// seal, and nothing that names a person. ext_authz is not consulted: it denies +// every request without a bearer, so a 200 here is a request it never saw, and +// it stamps x-user-id and x-credential-kind on every allow, which are absent. +func TestModuleRoute_AWorkloadOperationContextIsAdmittedWithoutABearer(t *testing.T) { + gw, module, installations, kid, wcPriv := operationHarness(t) + logs := captureLog(t) + capability := operationCapability(t, kid, wcPriv, "documents", publisherSeal) + + w := httptest.NewRecorder() + gw.ServeHTTP(w, onWorkloadEndpoint(publishRequest(capability))) + + require.Equal(t, http.StatusOK, w.Code, w.Body.String()) + require.Equal(t, 1, module.calls) + require.Equal(t, "/v1/documents/definitions:publish", module.lastPath, "the path is forwarded unchanged") + require.Equal(t, `{"bundle":"b"}`, module.lastBody) + require.Equal(t, capability, module.lastHeaders.Get(carrier.HeaderName), "the capability is forwarded as presented") + require.Equal(t, publisherSeal, module.lastHeaders.Get(installationConfinementHeader), + "the one stamped header is the capability's own seal") + for _, header := range []string{ + "Authorization", "Cookie", "Proxy-Authorization", "X-User-Id", "X-Org-Id", "X-Session-Id", "X-Scopes", + "X-Credential-Kind", resourceConfinementHeader, "X-Codefly-Gateway-Token", + } { + require.Empty(t, module.lastHeaders.Values(header), "%s must not reach the upstream on the bearer-less arm", header) + } + + require.Len(t, installations.calls, 1) + require.Equal(t, operationTenant, installations.calls[0].GetOrgId(), "the tenant is the capability's, signed") + require.Equal(t, accountsv1.InstallationStatus_INSTALLATION_STATUS_ACTIVE, installations.calls[0].GetStatus()) + + line := logs.String() + require.Contains(t, line, "reason=workload_operation_admit") + for _, fact := range []string{`alias="documents"`, `tenant="` + operationTenant + `"`, `task="task-1"`, `seal="` + publisherSeal + `"`, `presenter="local"`} { + require.Contains(t, line, fact) + } + require.NotContains(t, line, capability, "the token is never logged") +} + +// Every refusal, each with one fact broken and the rest held equal. +func TestModuleRoute_TheBearerLessArmRefuses(t *testing.T) { + for name, test := range map[string]struct { + prepare func(t *testing.T, gw *Gateway, installations *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request + code int + body string + asked bool + }{ + // The arm is the workload endpoint's alone: the public listener never + // selects it, so the request takes the person path and has no bearer. + "the same request on the public listener": { + prepare: func(t *testing.T, _ *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + return publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal)) + }, + code: http.StatusUnauthorized, body: "authentication required", + }, + // A module's own identity verifies at this route, and is never a + // headless operation context. + "a module identity capability": { + prepare: func(t *testing.T, _ *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, moduleCapabilitiesAudience, publisherSeal))) + }, + code: http.StatusUnauthorized, body: "authentication required", + }, + "a capability for another module": { + prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + declareModule(t, gw, "ledgers") + return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "ledgers", publisherSeal))) + }, + code: http.StatusUnauthorized, body: "invalid work context", + }, + // A person's capability for this module is sealed to the target's own + // installation, and without its bearer it is answered as it always was. + "a person-sealed capability": { + prepare: func(t *testing.T, _ *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", targetInstallation))) + }, + code: http.StatusUnauthorized, body: "authentication required", asked: true, + }, + "a seal that names no active installation in the tenant": { + prepare: func(t *testing.T, _ *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", "44444444-4444-4444-8444-444444444444"))) + }, + code: http.StatusUnauthorized, body: "authentication required", asked: true, + }, + "a tenant with no active installation of the target": { + prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + gw.installations = &installationsFake{pages: []*accountsv1.ListInstallationsResponse{{ + Installations: []*accountsv1.InstallationSummary{activeInstallation(publisherSeal, fakeSolutionTarget("publisher"))}, + }}} + return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) + }, + code: http.StatusForbidden, body: "module is not installed for this organization", + }, + "a route that resolves no target": { + prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + registry := solutionRegistryFake(t, gw) + registry.mu.Lock() + registry.revision++ + record := registry.records["documents"] + record.Revision = registry.revision + record.Declared.TargetId = "" + registry.mu.Unlock() + require.NoError(t, gw.solutions.refresh(context.Background())) + return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) + }, + code: http.StatusForbidden, body: "module is not installed for this organization", + }, + "an authority that errors": { + prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + gw.installations = &installationsFake{err: errors.New("accounts down: dial tcp 10.0.0.7:9090")} + return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) + }, + code: http.StatusServiceUnavailable, body: "module installation authority unavailable", + }, + "a cursor that never terminates": { + prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + gw.installations = &installationsFake{endless: true} + return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) + }, + code: http.StatusServiceUnavailable, body: "module installation authority unavailable", + }, + "no installation authority wired": { + prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + gw.installations = nil + return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) + }, + code: http.StatusServiceUnavailable, body: "module installation authority unavailable", + }, + // No edge verifier means no inspected capability, so the arm is never + // selected and the person path answers. + "no edge verifier": { + prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + capability := operationCapability(t, kid, wcPriv, "documents", publisherSeal) + gw.workContext = nil + return onWorkloadEndpoint(publishRequest(capability)) + }, + code: http.StatusUnauthorized, body: "authentication required", + }, + "a cookie and no bearer": { + prepare: func(t *testing.T, _ *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + req := onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) + req.Header.Set("Cookie", "session=abc") + return req + }, + code: http.StatusUnauthorized, body: "a session cookie is not a credential at this surface", + }, + // On a cell the listener requires the waypoint presenter; the arm + // requires it again, so a listener wired without it admits nothing. + "a cell gateway with no presenter": { + prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + gw.requireWorkloadPresenter = true + return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) + }, + code: http.StatusUnauthorized, body: "authentication required", + }, + } { + t.Run(name, func(t *testing.T) { + gw, module, installations, kid, wcPriv := operationHarness(t) + logs := captureLog(t) + req := test.prepare(t, gw, installations, kid, wcPriv) + + w := httptest.NewRecorder() + gw.ServeHTTP(w, req) + + require.Equal(t, test.code, w.Code, w.Body.String()) + require.Contains(t, w.Body.String(), test.body) + require.Zero(t, module.calls, "nothing reaches the upstream") + if test.asked { + require.Len(t, installations.calls, 1) + } + require.NotContains(t, logs.String(), req.Header.Get(carrier.HeaderName), "no refusal logs the token") + require.NotContains(t, w.Body.String(), "10.0.0.7", "no refusal carries the authority's error") + require.NotContains(t, logs.String(), "10.0.0.7", "nor logs it") + }) + } +} + +// Every refusal ON THE ARM is a log line naming the signed facts and the +// presenter, because the arm runs no ext_authz and this line is its only record. +func TestModuleRoute_TheBearerLessArmLogsEachRefusal(t *testing.T) { + gw, _, _, kid, wcPriv := operationHarness(t) + logs := captureLog(t) + capability := operationCapability(t, kid, wcPriv, "documents", targetInstallation) + req := onWorkloadEndpoint(publishRequest(capability)) + req = req.WithContext(context.WithValue(req.Context(), workloadPresenterKey{}, "spiffe://cluster.local/ns/apps/sa/publisher")) + + w := httptest.NewRecorder() + gw.ServeHTTP(w, req) + + require.Equal(t, http.StatusUnauthorized, w.Code) + line := logs.String() + require.Contains(t, line, "reason=workload_operation_person_sealed") + for _, fact := range []string{`alias="documents"`, `tenant="` + operationTenant + `"`, `task="task-1"`, `seal="` + targetInstallation + `"`, `presenter="spiffe://cluster.local/ns/apps/sa/publisher"`} { + require.Contains(t, line, fact) + } + require.NotContains(t, line, capability) +} + +// An upstream failure on the arm answers a static 502: the dial detail stays in +// the gateway's log. +func TestModuleRoute_TheBearerLessArmAnswersAStaticBadGateway(t *testing.T) { + gw, _, _, kid, wcPriv := operationHarness(t) + dead := httptest.NewServer(http.NotFoundHandler()) + deadURL := dead.URL + dead.Close() + registry := solutionRegistryFake(t, gw) + registry.mu.Lock() + registry.revision++ + registry.records["documents"].Revision = registry.revision + registry.route("documents", deadURL) + registry.mu.Unlock() + require.NoError(t, gw.solutions.refresh(context.Background())) + + w := httptest.NewRecorder() + gw.ServeHTTP(w, onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal)))) + + require.Equal(t, http.StatusBadGateway, w.Code) + require.Equal(t, "upstream error", strings.TrimSpace(w.Body.String())) + require.NotContains(t, w.Body.String(), strings.TrimPrefix(deadURL, "http://")) +} + +// The edge check still runs first: a capability the Inspector refuses never +// reaches the arm, whatever its seal. +func TestModuleRoute_TheBearerLessArmRefusesWhatTheEdgeRefuses(t *testing.T) { + for name, mint := range map[string]func(t *testing.T, kid string, wcPriv ed25519.PrivateKey) string{ + "expired": func(t *testing.T, kid string, wcPriv ed25519.PrivateKey) string { + return mintCapability(t, kid, wcPriv, edgeCapability{ + Audience: "documents", Tenant: operationTenant, Session: "s", Installation: publisherSeal, + Now: func() time.Time { return time.Now().Add(-time.Hour) }, + }) + }, + "a forged signature": func(t *testing.T, kid string, _ ed25519.PrivateKey) string { + _, attacker := mustEd25519(t) + return operationCapability(t, kid, attacker, "documents", publisherSeal) + }, + "a foreign issuer": func(t *testing.T, kid string, wcPriv ed25519.PrivateKey) string { + return mintCapability(t, kid, wcPriv, edgeCapability{ + Audience: "documents", Tenant: operationTenant, Session: "s", Installation: publisherSeal, Issuer: "https://issuer.example.com", + }) + }, + } { + t.Run(name, func(t *testing.T) { + gw, module, installations, kid, wcPriv := operationHarness(t) + + w := httptest.NewRecorder() + gw.ServeHTTP(w, onWorkloadEndpoint(publishRequest(mint(t, kid, wcPriv)))) + + require.Equal(t, http.StatusUnauthorized, w.Code) + require.Contains(t, w.Body.String(), "invalid work context") + require.Zero(t, module.calls) + require.Empty(t, installations.calls) + }) + } +} + +// An empty tenant or seal cannot come from core: its mint and its decode path +// both refuse one (tenant_id is required, and every capability is sealed). The +// arm checks both anyway, before anything is stripped or asked, and this holds +// that check to its answer with the only empty capability that can be built. +func TestModuleRoute_TheBearerLessArmRefusesACapabilityWithNoTenantOrSeal(t *testing.T) { + gw, module, installations, _, _ := operationHarness(t) + routing, resolution := gw.solutions.resolveModuleRouting(context.Background(), "documents") + require.Equal(t, solutionRoutable, resolution) + + w := httptest.NewRecorder() + gw.serveWorkloadOperation(w, onWorkloadEndpoint(publishRequest("")), "documents", routing, &workcontext.Inspected{}) + + require.Equal(t, http.StatusUnauthorized, w.Code) + require.Contains(t, w.Body.String(), "invalid work context") + require.Zero(t, module.calls) + require.Empty(t, installations.calls) +} + +// The two facts may sit on different pages: the arm follows the cursor until it +// holds both, and a row the authority should not have returned (another tenant, +// not ACTIVE) counts for neither. +func TestModuleRoute_TheBearerLessArmReadsEveryPageItNeeds(t *testing.T) { + gw, module, _, kid, wcPriv := operationHarness(t) + foreign := activeInstallation(publisherSeal, fakeSolutionTarget("publisher")) + foreign.Installation.OrgId = "66666666-6666-4666-8666-666666666666" + installations := &installationsFake{pages: []*accountsv1.ListInstallationsResponse{ + {Installations: []*accountsv1.InstallationSummary{activeInstallation(targetInstallation, fakeSolutionTarget("documents")), foreign}, NextPageToken: pageCursor(1)}, + {Installations: []*accountsv1.InstallationSummary{activeInstallation(publisherSeal, fakeSolutionTarget("publisher"))}}, + }} + gw.installations = installations + + w := httptest.NewRecorder() + gw.ServeHTTP(w, onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal)))) + require.Equal(t, http.StatusOK, w.Code, w.Body.String()) + require.Equal(t, 1, module.calls) + require.Len(t, installations.calls, 2) + require.Equal(t, pageCursor(1), installations.calls[1].GetPageToken()) + + // The seal only on the foreign row: not an active installation of THIS tenant. + installations.pages = installations.pages[:1] + installations.pages[0].NextPageToken = "" + w = httptest.NewRecorder() + gw.ServeHTTP(w, onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal)))) + require.Equal(t, http.StatusUnauthorized, w.Code) + require.Equal(t, 1, module.calls) +} diff --git a/module/services/auth-gateway/code/gateway_operation_context_test.go b/module/services/auth-gateway/code/gateway_operation_context_test.go index be171d68e..fe0f59d6b 100644 --- a/module/services/auth-gateway/code/gateway_operation_context_test.go +++ b/module/services/auth-gateway/code/gateway_operation_context_test.go @@ -2,6 +2,7 @@ package main import ( "context" + "crypto/ed25519" "encoding/json" "fmt" "net" @@ -13,6 +14,7 @@ import ( accountsv1 "auth-gateway/pkg/gen/saas/accounts/v1" + carrier "github.com/codefly-dev/sdk-go/workcontext" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -25,6 +27,8 @@ import ( // fakeAccountsOperationContextMint stands in for accounts' headless operation // mint on the internal listener, recording what the gateway forwarded. type fakeAccountsOperationContextMint struct { + // token, when set, is the context the mint answers instead of a placeholder. + token string code codes.Code requestCount int lastPrefix string @@ -51,8 +55,12 @@ func (f *fakeAccountsOperationContextMint) handle(ctx context.Context, dec func( if f.code != codes.OK { return nil, status.Error(f.code, "denied") } + token := f.token + if token == "" { + token = "operation-context-for-" + req.GetPrefix() + "-" + req.GetBinding() + } return &accountsv1.ModuleMintOperationContextResponse{ - Token: "operation-context-for-" + req.GetPrefix() + "-" + req.GetBinding(), + Token: token, ExpiresAt: timestamppb.New(f.expiresAt), PrincipalId: "00000000-0000-4000-8000-00000000beef", Tenant: "11111111-1111-4111-8111-111111111111", @@ -202,3 +210,49 @@ func TestGateway_ModuleOperationContext_MethodNotAllowed(t *testing.T) { require.Equal(t, http.StatusMethodNotAllowed, w.Code) require.Zero(t, mint.requestCount) } + +// THE WHOLE HEADLESS PUBLISH, in process: a composed workload mints its +// operation context at /modules/_operation-context and presents it, with no +// bearer, at the target module's route on the same workload endpoint. Accounts +// is faked to answer a capability signed by the key the edge trusts, for the +// alias, sealed to the publisher's own installation. +func TestGateway_AMintedOperationContextReachesItsModuleWithoutABearer(t *testing.T) { + gw, mint := newOperationContextExchangeHarness(t) + pub, wcPriv := mustEd25519(t) + jwks := jwksServer(t, jwksDocument(map[string]ed25519.PublicKey{"key-1": pub}), nil) + gw.workContext = newWorkContextVerifier(jwks.URL) + module := declareModule(t, gw, "documents") + gw.installations = tenantInstalled() + mint.token = operationCapability(t, "key-1", wcPriv, "documents", publisherSeal) + logs := captureLog(t) + + w := httptest.NewRecorder() + gw.ServeHTTP(w, operationContextRequest(operationContextBody("publisher", "documents"), "publisher-projected-token", "test-internal-token")) + require.Equal(t, http.StatusOK, w.Code, w.Body.String()) + var minted map[string]string + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &minted)) + capability := minted["work_context"] + require.Equal(t, mint.token, capability) + + w = httptest.NewRecorder() + gw.ServeHTTP(w, onWorkloadEndpoint(publishRequest(capability))) + require.Equal(t, http.StatusOK, w.Code, w.Body.String()) + require.Equal(t, 1, module.calls) + require.Equal(t, "/v1/documents/definitions:publish", module.lastPath) + require.Equal(t, capability, module.lastHeaders.Get(carrier.HeaderName)) + + // The same context on the public listener is a bearer-less request and + // nothing more. + w = httptest.NewRecorder() + gw.ServeHTTP(w, publishRequest(capability)) + require.Equal(t, http.StatusUnauthorized, w.Code) + require.Equal(t, 1, module.calls) + + // And a refusal on the arm logs the facts, never the token. + gw.installations = &installationsFake{} + w = httptest.NewRecorder() + gw.ServeHTTP(w, onWorkloadEndpoint(publishRequest(capability))) + require.Equal(t, http.StatusForbidden, w.Code) + require.Contains(t, logs.String(), "reason=workload_operation_not_installed") + require.NotContains(t, logs.String(), capability) +} diff --git a/module/services/auth-gateway/code/gateway_test.go b/module/services/auth-gateway/code/gateway_test.go index 80cd57136..2be9b4d10 100644 --- a/module/services/auth-gateway/code/gateway_test.go +++ b/module/services/auth-gateway/code/gateway_test.go @@ -19,6 +19,8 @@ import ( // fakeUpstream captures the headers an upstream would see after the // gateway forwards the request. type fakeUpstream struct { + // calls counts the requests that reached this upstream. + calls int lastHeaders http.Header lastPath string lastMethod string @@ -31,6 +33,7 @@ type fakeUpstream struct { } func (f *fakeUpstream) ServeHTTP(w http.ResponseWriter, r *http.Request) { + f.calls++ f.lastHeaders = r.Header.Clone() f.lastPath = r.URL.Path f.lastMethod = r.Method diff --git a/module/services/auth-gateway/code/gateway_workload_operation.go b/module/services/auth-gateway/code/gateway_workload_operation.go new file mode 100644 index 000000000..c275a3d37 --- /dev/null +++ b/module/services/auth-gateway/code/gateway_workload_operation.go @@ -0,0 +1,288 @@ +package main + +// The BEARER-LESS ARM of a declared module route: a host-minted headless +// operation context presented at /v1//* on the workload endpoint. +// +// A composed workload that mints an operation context at +// /modules/_operation-context holds a capability bound to the target module's +// own audience, and no person's bearer: there is no person. The person path +// (ext_authz, then per-viewer installation admission) cannot admit it, and +// before this arm such a context could reach its callee only by dialling it +// in-mesh. The arm admits it at the one door instead, without widening what a +// bearer-less request can reach anywhere else. +// +// Selection is narrow and every other request takes the person path unchanged: +// no Authorization and no Cookie header, the workload listener, and an +// edge-inspected capability addressed to the alias this route resolved. Once +// selected, the arm decides from three signed facts and one authority read: +// +// - the tenant and the seal the capability names (core's Inspector checked the +// signature, issuer, window, audience and shape at the route); +// - the tenant's ACTIVE installations, read from accounts per request. +// +// It admits only when the tenant has an active installation of the TARGET, and +// the seal names a DIFFERENT active installation in that tenant. The second half +// is how a hop that reads no owner tells unattended work from a person: a +// person's capability is sealed to the organization's installation of the +// audience it addresses, which here is the target's own, while a module or +// installation principal's is sealed to its own installation. Accounts holds +// that as an invariant (TestAPersonsCapabilityIsSealedToItsAudiencesInstallation +// in accounts); this arm is sound only while it holds. A misreading refuses, +// never widens, and the callee verifies the capability at full strength. +// +// What the upstream receives: the capability as presented, the path unchanged, +// no bearer, no cookie, no identity header, and x-codefly-installation-id set to +// the seal. Nothing names a principal, because nothing here read one. + +import ( + "context" + "log" + "net/http" + + accountsv1 "auth-gateway/pkg/gen/saas/accounts/v1" + + "github.com/codefly-dev/core/workcontext" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +const listInstallationsMethod = "/saas.accounts.v1.InstallationService/ListInstallations" + +// installationListingPageSize and installationListingPageLimit bound one +// admission read the way the entitlement read beside it is bounded: a cursor +// that never terminates is undecidable, never a short answer. +const ( + installationListingPageSize = 200 + installationListingPageLimit = solutionEntitlementPageLimit +) + +// installationListingClient is the authority surface the arm consumes. The +// interface exists so the arm can be exercised without an accounts process. +type installationListingClient interface { + List(ctx context.Context, req *accountsv1.ListInstallationsRequest) (*accountsv1.ListInstallationsResponse, error) +} + +// accountsInstallations invokes the organization's installation listing by +// method name over the existing accounts connection, with the gateway's +// cluster-internal credential: the shape of accountsSolutionEntitlements, and for +// the same reason (only the vendored message types, no client stub). +type accountsInstallations struct { + conn *grpc.ClientConn + internalToken string +} + +func (c *accountsInstallations) List( + ctx context.Context, req *accountsv1.ListInstallationsRequest, +) (*accountsv1.ListInstallationsResponse, error) { + ctx, cancel := context.WithTimeout(ctx, solutionEntitlementCallTimeout) + defer cancel() + ctx = metadata.AppendToOutgoingContext(ctx, "x-codefly-internal-token", c.internalToken) + resp := &accountsv1.ListInstallationsResponse{} + return resp, c.conn.Invoke(ctx, listInstallationsMethod, req, resp) +} + +// workloadOperationTenantKey carries the signed tenant from the arm to the rate +// limiter. A private context key, set only by serveWorkloadOperation: no header +// and no other route can select the budget it keys. +type workloadOperationTenantKey struct{} + +// workloadOperationTenant reports the tenant the arm admitted the request under, +// for the limiter's namespaced budget. +func workloadOperationTenant(ctx context.Context) (string, bool) { + tenant, ok := ctx.Value(workloadOperationTenantKey{}).(string) + return tenant, ok && tenant != "" +} + +// selectsWorkloadOperation is conditions 1 to 5: whether the request takes the +// bearer-less arm at all. Any one failing leaves the person path to answer +// exactly as it did before the arm existed. +func selectsWorkloadOperation(r *http.Request, capability *workcontext.Inspected, alias string) bool { + return len(r.Header.Values("Authorization")) == 0 && + len(r.Header.Values("Cookie")) == 0 && + onWorkloadListener(r.Context()) && + capability != nil && + alias != moduleCapabilitiesAudience && + capability.Audience() == alias +} + +// workloadOperationAdmission is the arm's authority verdict. Like the viewer +// admission it is not a boolean: "not installed", "not unattended work" and "I +// could not ask" are different answers. +type workloadOperationAdmission int + +const ( + workloadOperationAdmitted workloadOperationAdmission = iota + // workloadOperationNotInstalled: the tenant has no ACTIVE installation of + // the target, or the route resolved no target. + workloadOperationNotInstalled + // workloadOperationPersonSealed: the seal is the target's own installation, + // which is what a person's capability for this module carries. + workloadOperationPersonSealed + // workloadOperationSealInactive: the seal names no ACTIVE installation in + // the tenant (revoked, another tenant's, unknown). + workloadOperationSealInactive + // workloadOperationUndecidable: no client wired, an RPC error, or a cursor + // that did not terminate. Answered 503, never admitted. + workloadOperationUndecidable +) + +// admitWorkloadOperation reads the tenant's ACTIVE installations and decides. +// Uncached, for the reason the viewer admission is: an uninstall changes the +// answer with no write this replica would see. +func (g *Gateway) admitWorkloadOperation( + ctx context.Context, tenant, seal string, routing *solutionRouting, +) workloadOperationAdmission { + targetID := routing.GetTargetID() + if targetID == "" { + return workloadOperationNotInstalled + } + if g.installations == nil { + return workloadOperationUndecidable + } + targetInstallation, sealActive := "", false + token := "" + for page := 0; page < installationListingPageLimit; page++ { + resp, err := g.installations.List(ctx, &accountsv1.ListInstallationsRequest{ + OrgId: tenant, + Status: accountsv1.InstallationStatus_INSTALLATION_STATUS_ACTIVE, + PageSize: installationListingPageSize, + PageToken: token, + }) + if err != nil { + return workloadOperationUndecidable + } + for _, summary := range resp.GetInstallations() { + installation := summary.GetInstallation() + // The request asked for ACTIVE rows of this tenant; a row that is + // neither is not counted, whatever the authority returned. + if installation.GetId() == "" || installation.GetOrgId() != tenant || + installation.GetStatus() != accountsv1.InstallationStatus_INSTALLATION_STATUS_ACTIVE { + continue + } + if installation.GetTargetId() == targetID { + targetInstallation = installation.GetId() + } + if installation.GetId() == seal { + sealActive = true + } + } + // Both positive facts settle it on a partial read; a missing one is + // only conclusive after the whole set, so it waits for the last page. + if targetInstallation != "" && sealActive { + break + } + token = resp.GetNextPageToken() + if token == "" { + break + } + if page == installationListingPageLimit-1 { + return workloadOperationUndecidable + } + } + switch { + case targetInstallation == "": + return workloadOperationNotInstalled + case seal == targetInstallation: + return workloadOperationPersonSealed + case !sealActive: + return workloadOperationSealInactive + } + return workloadOperationAdmitted +} + +// workloadOperationRecord is what every admit and refusal on the arm logs. The +// arm runs no ext_authz, so this line is its only decision record. It names the +// signed facts and the presenter, never the token and never an authority error. +type workloadOperationRecord struct { + method, path, alias, tenant, task, seal, presenter string +} + +func (rec workloadOperationRecord) log(verdict, reason string) { + presenter := rec.presenter + if presenter == "" { + presenter = "local" + } + log.Printf("%s request: method=%s path=%q reason=workload_operation_%s alias=%q tenant=%q task=%q seal=%q presenter=%q", + verdict, rec.method, rec.path, reason, rec.alias, rec.tenant, rec.task, rec.seal, presenter) +} + +func (rec workloadOperationRecord) refuse(w http.ResponseWriter, code int, body, reason string) { + rec.log("WARN: blocked", reason) + httpError(w, code, body) +} + +// serveWorkloadOperation is conditions 6 to 12, for a request +// selectsWorkloadOperation chose. +func (g *Gateway) serveWorkloadOperation( + w http.ResponseWriter, r *http.Request, alias string, routing *solutionRouting, capability *workcontext.Inspected, +) { + rec := workloadOperationRecord{ + method: r.Method, path: r.URL.Path, alias: alias, + tenant: capability.TenantID(), task: capability.TaskID(), seal: capability.InstallationID(), + presenter: workloadPresenter(r.Context()), + } + // On a cell the workload listener has already required the waypoint's + // presenter. Required again here, so a listener wired without it cannot + // admit a capability that nothing in the mesh vouched for. + if g.requireWorkloadPresenter && rec.presenter == "" { + rec.refuse(w, http.StatusUnauthorized, "authentication required", "presenter_missing") + return + } + if rec.tenant == "" { + rec.refuse(w, http.StatusUnauthorized, "invalid work context", "no_tenant") + return + } + if rec.seal == "" { + rec.refuse(w, http.StatusUnauthorized, "invalid work context", "no_seal") + return + } + + // Strip BEFORE the limiter, as the person path does: a caller's X-Org-Id + // must never reach the key, and nothing below may read a header the caller + // wrote. No bearer and no cookie arrived (selection), and none leaves. + stripAllIdentityHeaders(r) + r.Header.Del("Authorization") + r.Header.Del("Proxy-Authorization") + + // The budget is the signed tenant's own namespace, fail closed: with no + // X-Org-Id the generic key would be the waypoint's address, one bucket for + // every tenant on the cell. The admission read sits behind it. + entry := &RouteEntry{ + Service: moduleServicePrefix + alias, + Protected: true, + RateLimitBackendFailClosed: true, + staticUpstreamError: true, + } + r = r.WithContext(context.WithValue(r.Context(), workloadOperationTenantKey{}, rec.tenant)) + reached := false + g.rateLimitThenServe(w, r, entry, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + reached = true + switch g.admitWorkloadOperation(r.Context(), rec.tenant, rec.seal, routing) { + case workloadOperationUndecidable: + rec.refuse(w, http.StatusServiceUnavailable, "module installation authority unavailable", "authority_unavailable") + return + case workloadOperationNotInstalled: + rec.refuse(w, http.StatusForbidden, "module is not installed for this organization", "not_installed") + return + case workloadOperationPersonSealed: + // The answer a person's capability without its bearer has always had. + rec.refuse(w, http.StatusUnauthorized, "authentication required", "person_sealed") + return + case workloadOperationSealInactive: + rec.refuse(w, http.StatusUnauthorized, "authentication required", "seal_inactive") + return + } + // The one header this arm stamps: the seal of the capability it admitted. + // An sdk consumer reading the carrier requires this header to equal the + // seal, and on the person path it equals the seal too. Here it names the + // CALLER's installation, not consent on the target's side. + r.Header.Set(installationConfinementHeader, rec.seal) + rec.log("INFO: admitted", "admit") + g.proxyTo(w, r, routing.Upstream, entry) + })) + if !reached { + // Refused by the limiter (429, or 503 with its backend down) or by the + // cross-origin binding, each of which wrote its own answer. + rec.log("WARN: blocked", "limited") + } +} diff --git a/module/services/auth-gateway/code/installation_listing_vendor_test.go b/module/services/auth-gateway/code/installation_listing_vendor_test.go new file mode 100644 index 000000000..5c9f85ea5 --- /dev/null +++ b/module/services/auth-gateway/code/installation_listing_vendor_test.go @@ -0,0 +1,32 @@ +package main + +import ( + "os" + "path/filepath" + "regexp" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestVendoredInstallationListingMatchesAccounts holds every definition in the +// gateway's installation_listing.proto byte-identical to the one accounts +// declares in installations.proto. The gateway carries only the messages +// ListInstallations takes and answers, because installations.proto imports +// the policy options and google.api this service does not vendor; a copy that +// drifted would decode a different wire shape from the one accounts writes, +// silently. +func TestVendoredInstallationListingMatchesAccounts(t *testing.T) { + vendored, err := os.ReadFile(filepath.Join("..", "proto", "saas", "accounts", "v1", "installation_listing.proto")) + require.NoError(t, err) + owner, err := os.ReadFile(filepath.Join("..", "..", "accounts", "proto", "saas", "accounts", "v1", "installations.proto")) + require.NoError(t, err) + + definitions := regexp.MustCompile(`(?ms)^(?:message|enum) \w+ \{\n.*?^\}\n`).FindAllString(string(vendored), -1) + require.Len(t, definitions, 6, "the vendored file declares the two enums and four messages ListInstallations uses") + for _, definition := range definitions { + require.True(t, strings.Contains(string(owner), definition), + "installation_listing.proto drifted from accounts' installations.proto:\n%s", definition) + } +} diff --git a/module/services/auth-gateway/code/main.go b/module/services/auth-gateway/code/main.go index 96bbd6bc9..2eaf8dc31 100644 --- a/module/services/auth-gateway/code/main.go +++ b/module/services/auth-gateway/code/main.go @@ -255,6 +255,10 @@ func main() { // connection. It is not a cache: the answer is per viewer and must not // outlive a revoke, so every request asks. gateway.solutionEntitlements = &accountsSolutionEntitlements{conn: internalAPIConn, internalToken: authz.internalToken} + // The organization-level installation read the bearer-less module arm + // admits on, over the same connection and uncached for the same reason. + gateway.installations = &accountsInstallations{conn: internalAPIConn, internalToken: authz.internalToken} + gateway.requireWorkloadPresenter = !codefly.IsLocal() go gateway.solutions.reconcile(ctx) go gateway.clients.reconcile(ctx) if apiHTTPURL != "" { diff --git a/module/services/auth-gateway/code/perimeter_workos_free_test.go b/module/services/auth-gateway/code/perimeter_workos_free_test.go index 76eb7ad40..9da2d865c 100644 --- a/module/services/auth-gateway/code/perimeter_workos_free_test.go +++ b/module/services/auth-gateway/code/perimeter_workos_free_test.go @@ -187,6 +187,12 @@ var httpAllowedPerimeterFiles = map[string]bool{ // is unchanged and local — and its only network call is the internal gRPC hop to // accounts for the installation and grant state. It reaches no external IdP. "gateway_solution_entitlements.go": true, + // gateway_workload_operation.go is the bearer-less arm of a declared module + // route. It verifies no token itself (core's Inspector already ran at the + // route, with local keys), and its only network calls are the internal gRPC + // hop to accounts for the tenant's installations and the proxy hop to the + // declared upstream. It reaches no external IdP. + "gateway_workload_operation.go": true, // gateway_cors.go answers cross-origin access for registered clients. It // decides from the registry snapshot and from the client id the ext_authz // check already resolved from a locally verified token; it makes no network diff --git a/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go b/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go new file mode 100644 index 000000000..276a835fb --- /dev/null +++ b/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go @@ -0,0 +1,588 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: saas/accounts/v1/installation_listing.proto + +package accountsv1 + +import ( + reflect "reflect" + sync "sync" + unsafe "unsafe" + + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type InstallationStatus int32 + +const ( + InstallationStatus_INSTALLATION_STATUS_UNSPECIFIED InstallationStatus = 0 + InstallationStatus_INSTALLATION_STATUS_ACTIVE InstallationStatus = 1 + InstallationStatus_INSTALLATION_STATUS_REVOKED InstallationStatus = 2 + // Installed, and waiting for the solution to acknowledge its standard install + // step. Nothing can use a pending installation. + InstallationStatus_INSTALLATION_STATUS_PENDING InstallationStatus = 3 + // The solution refused its install step, or never answered within the + // attempt budget. Nothing can use it; an administrator may retry it. + InstallationStatus_INSTALLATION_STATUS_FAILED InstallationStatus = 4 +) + +// Enum value maps for InstallationStatus. +var ( + InstallationStatus_name = map[int32]string{ + 0: "INSTALLATION_STATUS_UNSPECIFIED", + 1: "INSTALLATION_STATUS_ACTIVE", + 2: "INSTALLATION_STATUS_REVOKED", + 3: "INSTALLATION_STATUS_PENDING", + 4: "INSTALLATION_STATUS_FAILED", + } + InstallationStatus_value = map[string]int32{ + "INSTALLATION_STATUS_UNSPECIFIED": 0, + "INSTALLATION_STATUS_ACTIVE": 1, + "INSTALLATION_STATUS_REVOKED": 2, + "INSTALLATION_STATUS_PENDING": 3, + "INSTALLATION_STATUS_FAILED": 4, + } +) + +func (x InstallationStatus) Enum() *InstallationStatus { + p := new(InstallationStatus) + *p = x + return p +} + +func (x InstallationStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (InstallationStatus) Descriptor() protoreflect.EnumDescriptor { + return file_saas_accounts_v1_installation_listing_proto_enumTypes[0].Descriptor() +} + +func (InstallationStatus) Type() protoreflect.EnumType { + return &file_saas_accounts_v1_installation_listing_proto_enumTypes[0] +} + +func (x InstallationStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use InstallationStatus.Descriptor instead. +func (InstallationStatus) EnumDescriptor() ([]byte, []int) { + return file_saas_accounts_v1_installation_listing_proto_rawDescGZIP(), []int{0} +} + +type InstallationHealth int32 + +const ( + InstallationHealth_INSTALLATION_HEALTH_UNSPECIFIED InstallationHealth = 0 + InstallationHealth_INSTALLATION_HEALTH_HEALTHY InstallationHealth = 1 + InstallationHealth_INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER InstallationHealth = 2 + InstallationHealth_INSTALLATION_HEALTH_AGENT_REVOKED InstallationHealth = 3 + InstallationHealth_INSTALLATION_HEALTH_AGENT_DISABLED InstallationHealth = 4 + InstallationHealth_INSTALLATION_HEALTH_STANDING_GRANT_MISSING InstallationHealth = 5 +) + +// Enum value maps for InstallationHealth. +var ( + InstallationHealth_name = map[int32]string{ + 0: "INSTALLATION_HEALTH_UNSPECIFIED", + 1: "INSTALLATION_HEALTH_HEALTHY", + 2: "INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER", + 3: "INSTALLATION_HEALTH_AGENT_REVOKED", + 4: "INSTALLATION_HEALTH_AGENT_DISABLED", + 5: "INSTALLATION_HEALTH_STANDING_GRANT_MISSING", + } + InstallationHealth_value = map[string]int32{ + "INSTALLATION_HEALTH_UNSPECIFIED": 0, + "INSTALLATION_HEALTH_HEALTHY": 1, + "INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER": 2, + "INSTALLATION_HEALTH_AGENT_REVOKED": 3, + "INSTALLATION_HEALTH_AGENT_DISABLED": 4, + "INSTALLATION_HEALTH_STANDING_GRANT_MISSING": 5, + } +) + +func (x InstallationHealth) Enum() *InstallationHealth { + p := new(InstallationHealth) + *p = x + return p +} + +func (x InstallationHealth) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (InstallationHealth) Descriptor() protoreflect.EnumDescriptor { + return file_saas_accounts_v1_installation_listing_proto_enumTypes[1].Descriptor() +} + +func (InstallationHealth) Type() protoreflect.EnumType { + return &file_saas_accounts_v1_installation_listing_proto_enumTypes[1] +} + +func (x InstallationHealth) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use InstallationHealth.Descriptor instead. +func (InstallationHealth) EnumDescriptor() ([]byte, []int) { + return file_saas_accounts_v1_installation_listing_proto_rawDescGZIP(), []int{1} +} + +type Installation struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + OrgId string `protobuf:"bytes,2,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` + AgentPrincipalId string `protobuf:"bytes,3,opt,name=agent_principal_id,json=agentPrincipalId,proto3" json:"agent_principal_id,omitempty"` + OwnerPrincipalId string `protobuf:"bytes,5,opt,name=owner_principal_id,json=ownerPrincipalId,proto3" json:"owner_principal_id,omitempty"` + CoOwnerPrincipalIds []string `protobuf:"bytes,6,rep,name=co_owner_principal_ids,json=coOwnerPrincipalIds,proto3" json:"co_owner_principal_ids,omitempty"` + RootScopeNodeId string `protobuf:"bytes,7,opt,name=root_scope_node_id,json=rootScopeNodeId,proto3" json:"root_scope_node_id,omitempty"` + Status InstallationStatus `protobuf:"varint,8,opt,name=status,proto3,enum=saas.accounts.v1.InstallationStatus" json:"status,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + RevokedAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=revoked_at,json=revokedAt,proto3,oneof" json:"revoked_at,omitempty"` + // The immutable solution target this installation was consented to: one + // continuous period of one binding's presence on this host. It is never + // reused, so a replacement binding — even one claiming the same route alias — + // is a different target and inherits nothing. + // + // Deliberately NOT accompanied by the route alias. The alias is a property of + // the target that a later generation may move, and resolving it here would + // mean joining the host's presence state into a request-path listing — which + // would require granting request traffic read over every organisation's + // presence to answer about one. A consumer that needs the alias asks + // ListAvailableSolutions, which serves accepted applied state, and joins on + // this id. + // + // Empty only on a revoked historical row whose target could not be + // established at the cutover; an active installation always names one. + TargetId string `protobuf:"bytes,11,opt,name=target_id,json=targetId,proto3" json:"target_id,omitempty"` + // Why a revoked installation was revoked, when it was not an administrator + // uninstalling: the installation-identity cutover, or a tenancy narrowing that + // withdrew this organisation. Empty on an active row. + RevokedReason string `protobuf:"bytes,12,opt,name=revoked_reason,json=revokedReason,proto3" json:"revoked_reason,omitempty"` + // Why the install step failed: the solution's bounded refusal reason, or a + // fixed reason when it never answered. Empty unless status is FAILED. + FailureReason string `protobuf:"bytes,13,opt,name=failure_reason,json=failureReason,proto3" json:"failure_reason,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Installation) Reset() { + *x = Installation{} + mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Installation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Installation) ProtoMessage() {} + +func (x *Installation) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Installation.ProtoReflect.Descriptor instead. +func (*Installation) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_installation_listing_proto_rawDescGZIP(), []int{0} +} + +func (x *Installation) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Installation) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + +func (x *Installation) GetAgentPrincipalId() string { + if x != nil { + return x.AgentPrincipalId + } + return "" +} + +func (x *Installation) GetOwnerPrincipalId() string { + if x != nil { + return x.OwnerPrincipalId + } + return "" +} + +func (x *Installation) GetCoOwnerPrincipalIds() []string { + if x != nil { + return x.CoOwnerPrincipalIds + } + return nil +} + +func (x *Installation) GetRootScopeNodeId() string { + if x != nil { + return x.RootScopeNodeId + } + return "" +} + +func (x *Installation) GetStatus() InstallationStatus { + if x != nil { + return x.Status + } + return InstallationStatus_INSTALLATION_STATUS_UNSPECIFIED +} + +func (x *Installation) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *Installation) GetRevokedAt() *timestamppb.Timestamp { + if x != nil { + return x.RevokedAt + } + return nil +} + +func (x *Installation) GetTargetId() string { + if x != nil { + return x.TargetId + } + return "" +} + +func (x *Installation) GetRevokedReason() string { + if x != nil { + return x.RevokedReason + } + return "" +} + +func (x *Installation) GetFailureReason() string { + if x != nil { + return x.FailureReason + } + return "" +} + +type ListInstallationsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` + // Restrict to one lifecycle status. UNSPECIFIED returns every status, so a + // caller that must see only live installs asks for ACTIVE explicitly rather + // than leaning on a default that would silently widen if the enum grew. + Status InstallationStatus `protobuf:"varint,2,opt,name=status,proto3,enum=saas.accounts.v1.InstallationStatus" json:"status,omitempty"` + // Page bound. 0 -> server default; capped so an organization with many + // installations can never return an unbounded result in one call. + PageSize int32 `protobuf:"varint,3,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Opaque cursor from a previous response's next_page_token; empty for page one. + PageToken string `protobuf:"bytes,4,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListInstallationsRequest) Reset() { + *x = ListInstallationsRequest{} + mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListInstallationsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListInstallationsRequest) ProtoMessage() {} + +func (x *ListInstallationsRequest) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListInstallationsRequest.ProtoReflect.Descriptor instead. +func (*ListInstallationsRequest) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_installation_listing_proto_rawDescGZIP(), []int{1} +} + +func (x *ListInstallationsRequest) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + +func (x *ListInstallationsRequest) GetStatus() InstallationStatus { + if x != nil { + return x.Status + } + return InstallationStatus_INSTALLATION_STATUS_UNSPECIFIED +} + +func (x *ListInstallationsRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListInstallationsRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +type InstallationSummary struct { + state protoimpl.MessageState `protogen:"open.v1"` + Installation *Installation `protobuf:"bytes,1,opt,name=installation,proto3" json:"installation,omitempty"` + Health InstallationHealth `protobuf:"varint,2,opt,name=health,proto3,enum=saas.accounts.v1.InstallationHealth" json:"health,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InstallationSummary) Reset() { + *x = InstallationSummary{} + mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InstallationSummary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InstallationSummary) ProtoMessage() {} + +func (x *InstallationSummary) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InstallationSummary.ProtoReflect.Descriptor instead. +func (*InstallationSummary) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_installation_listing_proto_rawDescGZIP(), []int{2} +} + +func (x *InstallationSummary) GetInstallation() *Installation { + if x != nil { + return x.Installation + } + return nil +} + +func (x *InstallationSummary) GetHealth() InstallationHealth { + if x != nil { + return x.Health + } + return InstallationHealth_INSTALLATION_HEALTH_UNSPECIFIED +} + +type ListInstallationsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Installations []*InstallationSummary `protobuf:"bytes,1,rep,name=installations,proto3" json:"installations,omitempty"` + // Set when more installations remain; pass it back as page_token. Empty on the + // last page. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListInstallationsResponse) Reset() { + *x = ListInstallationsResponse{} + mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListInstallationsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListInstallationsResponse) ProtoMessage() {} + +func (x *ListInstallationsResponse) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListInstallationsResponse.ProtoReflect.Descriptor instead. +func (*ListInstallationsResponse) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_installation_listing_proto_rawDescGZIP(), []int{3} +} + +func (x *ListInstallationsResponse) GetInstallations() []*InstallationSummary { + if x != nil { + return x.Installations + } + return nil +} + +func (x *ListInstallationsResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +var File_saas_accounts_v1_installation_listing_proto protoreflect.FileDescriptor + +const file_saas_accounts_v1_installation_listing_proto_rawDesc = "" + + "\n" + + "+saas/accounts/v1/installation_listing.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x94\x05\n" + + "\fInstallation\x12\x18\n" + + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id\x12\x1f\n" + + "\x06org_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x126\n" + + "\x12agent_principal_id\x18\x03 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x10agentPrincipalId\x126\n" + + "\x12owner_principal_id\x18\x05 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x10ownerPrincipalId\x123\n" + + "\x16co_owner_principal_ids\x18\x06 \x03(\tR\x13coOwnerPrincipalIds\x125\n" + + "\x12root_scope_node_id\x18\a \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0frootScopeNodeId\x12<\n" + + "\x06status\x18\b \x01(\x0e2$.saas.accounts.v1.InstallationStatusR\x06status\x129\n" + + "\n" + + "created_at\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12>\n" + + "\n" + + "revoked_at\x18\n" + + " \x01(\v2\x1a.google.protobuf.TimestampH\x00R\trevokedAt\x88\x01\x01\x12(\n" + + "\ttarget_id\x18\v \x01(\tB\v\xbaH\b\xd8\x01\x01r\x03\xb0\x01\x01R\btargetId\x12/\n" + + "\x0erevoked_reason\x18\f \x01(\tB\b\xbaH\x05r\x03\x18\x80\x04R\rrevokedReason\x12/\n" + + "\x0efailure_reason\x18\r \x01(\tB\b\xbaH\x05r\x03\x18\x80\x04R\rfailureReasonB\r\n" + + "\v_revoked_atJ\x04\b\x04\x10\x05R\x13solution_identifier\"\xc1\x01\n" + + "\x18ListInstallationsRequest\x12\x1f\n" + + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x12<\n" + + "\x06status\x18\x02 \x01(\x0e2$.saas.accounts.v1.InstallationStatusR\x06status\x12'\n" + + "\tpage_size\x18\x03 \x01(\x05B\n" + + "\xbaH\a\x1a\x05\x18\xf4\x03(\x00R\bpageSize\x12\x1d\n" + + "\n" + + "page_token\x18\x04 \x01(\tR\tpageToken\"\x97\x01\n" + + "\x13InstallationSummary\x12B\n" + + "\finstallation\x18\x01 \x01(\v2\x1e.saas.accounts.v1.InstallationR\finstallation\x12<\n" + + "\x06health\x18\x02 \x01(\x0e2$.saas.accounts.v1.InstallationHealthR\x06health\"\x90\x01\n" + + "\x19ListInstallationsResponse\x12K\n" + + "\rinstallations\x18\x01 \x03(\v2%.saas.accounts.v1.InstallationSummaryR\rinstallations\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken*\xbb\x01\n" + + "\x12InstallationStatus\x12#\n" + + "\x1fINSTALLATION_STATUS_UNSPECIFIED\x10\x00\x12\x1e\n" + + "\x1aINSTALLATION_STATUS_ACTIVE\x10\x01\x12\x1f\n" + + "\x1bINSTALLATION_STATUS_REVOKED\x10\x02\x12\x1f\n" + + "\x1bINSTALLATION_STATUS_PENDING\x10\x03\x12\x1e\n" + + "\x1aINSTALLATION_STATUS_FAILED\x10\x04*\x84\x02\n" + + "\x12InstallationHealth\x12#\n" + + "\x1fINSTALLATION_HEALTH_UNSPECIFIED\x10\x00\x12\x1f\n" + + "\x1bINSTALLATION_HEALTH_HEALTHY\x10\x01\x12)\n" + + "%INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER\x10\x02\x12%\n" + + "!INSTALLATION_HEALTH_AGENT_REVOKED\x10\x03\x12&\n" + + "\"INSTALLATION_HEALTH_AGENT_DISABLED\x10\x04\x12.\n" + + "*INSTALLATION_HEALTH_STANDING_GRANT_MISSING\x10\x05B\xc4\x01\n" + + "\x14com.saas.accounts.v1B\x18InstallationListingProtoP\x01Z0auth-gateway/pkg/gen/saas/accounts/v1;accountsv1\xa2\x02\x03SAX\xaa\x02\x10Saas.Accounts.V1\xca\x02\x10Saas\\Accounts\\V1\xe2\x02\x1cSaas\\Accounts\\V1\\GPBMetadata\xea\x02\x12Saas::Accounts::V1b\x06proto3" + +var ( + file_saas_accounts_v1_installation_listing_proto_rawDescOnce sync.Once + file_saas_accounts_v1_installation_listing_proto_rawDescData []byte +) + +func file_saas_accounts_v1_installation_listing_proto_rawDescGZIP() []byte { + file_saas_accounts_v1_installation_listing_proto_rawDescOnce.Do(func() { + file_saas_accounts_v1_installation_listing_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_installation_listing_proto_rawDesc), len(file_saas_accounts_v1_installation_listing_proto_rawDesc))) + }) + return file_saas_accounts_v1_installation_listing_proto_rawDescData +} + +var file_saas_accounts_v1_installation_listing_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_saas_accounts_v1_installation_listing_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_saas_accounts_v1_installation_listing_proto_goTypes = []any{ + (InstallationStatus)(0), // 0: saas.accounts.v1.InstallationStatus + (InstallationHealth)(0), // 1: saas.accounts.v1.InstallationHealth + (*Installation)(nil), // 2: saas.accounts.v1.Installation + (*ListInstallationsRequest)(nil), // 3: saas.accounts.v1.ListInstallationsRequest + (*InstallationSummary)(nil), // 4: saas.accounts.v1.InstallationSummary + (*ListInstallationsResponse)(nil), // 5: saas.accounts.v1.ListInstallationsResponse + (*timestamppb.Timestamp)(nil), // 6: google.protobuf.Timestamp +} +var file_saas_accounts_v1_installation_listing_proto_depIdxs = []int32{ + 0, // 0: saas.accounts.v1.Installation.status:type_name -> saas.accounts.v1.InstallationStatus + 6, // 1: saas.accounts.v1.Installation.created_at:type_name -> google.protobuf.Timestamp + 6, // 2: saas.accounts.v1.Installation.revoked_at:type_name -> google.protobuf.Timestamp + 0, // 3: saas.accounts.v1.ListInstallationsRequest.status:type_name -> saas.accounts.v1.InstallationStatus + 2, // 4: saas.accounts.v1.InstallationSummary.installation:type_name -> saas.accounts.v1.Installation + 1, // 5: saas.accounts.v1.InstallationSummary.health:type_name -> saas.accounts.v1.InstallationHealth + 4, // 6: saas.accounts.v1.ListInstallationsResponse.installations:type_name -> saas.accounts.v1.InstallationSummary + 7, // [7:7] is the sub-list for method output_type + 7, // [7:7] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name +} + +func init() { file_saas_accounts_v1_installation_listing_proto_init() } +func file_saas_accounts_v1_installation_listing_proto_init() { + if File_saas_accounts_v1_installation_listing_proto != nil { + return + } + file_saas_accounts_v1_installation_listing_proto_msgTypes[0].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_installation_listing_proto_rawDesc), len(file_saas_accounts_v1_installation_listing_proto_rawDesc)), + NumEnums: 2, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_saas_accounts_v1_installation_listing_proto_goTypes, + DependencyIndexes: file_saas_accounts_v1_installation_listing_proto_depIdxs, + EnumInfos: file_saas_accounts_v1_installation_listing_proto_enumTypes, + MessageInfos: file_saas_accounts_v1_installation_listing_proto_msgTypes, + }.Build() + File_saas_accounts_v1_installation_listing_proto = out.File + file_saas_accounts_v1_installation_listing_proto_goTypes = nil + file_saas_accounts_v1_installation_listing_proto_depIdxs = nil +} diff --git a/module/services/auth-gateway/code/ratelimit.go b/module/services/auth-gateway/code/ratelimit.go index 1d2dd6d41..aa08d114c 100644 --- a/module/services/auth-gateway/code/ratelimit.go +++ b/module/services/auth-gateway/code/ratelimit.go @@ -312,6 +312,16 @@ func (rl *RateLimiter) Middleware(mode limiterFailureMode, class edgeRateLimitCl key = "webhook:" + rl.proxies.clientIP(r) keyKind = "webhook-ip" } + if tenant, ok := workloadOperationTenant(r.Context()); ok { + // A headless operation context on the bearer-less module arm carries + // no X-Org-Id (the arm strips caller identity and ext_authz never + // runs), so the generic key would be the waypoint's address: every + // tenant on the cell in one bucket. Its budget is the tenant the + // capability is signed for, in a namespace of its own. Only the arm + // sets this context value; no header and no other route can. + key = "workload-operation:" + tenant + keyKind = "workload-operation-tenant" + } if authenticationFactorAttempt { // Login MFA completion is public and has no canonical user/org // yet. Always bind the edge budget to the trusted client IP and diff --git a/module/services/auth-gateway/code/routing.go b/module/services/auth-gateway/code/routing.go index 55744dbea..61bf16195 100644 --- a/module/services/auth-gateway/code/routing.go +++ b/module/services/auth-gateway/code/routing.go @@ -49,6 +49,7 @@ type RouteEntry struct { RateLimitBackendFailClosed bool // preserve the security budget when the limiter backend fails AuthenticationFactorAttempt bool // use the dedicated per-client-IP login-factor budget PolicySHA256 string // stable fingerprint of the complete method policy + staticUpstreamError bool // answer an upstream failure with a static body, never the dial error artifactUpstream *routeArtifactUpstream artifactExposure edgeExposure hasArtifactExposure bool diff --git a/module/services/auth-gateway/code/work_context_test.go b/module/services/auth-gateway/code/work_context_test.go index fbd1236e1..f2e9f0667 100644 --- a/module/services/auth-gateway/code/work_context_test.go +++ b/module/services/auth-gateway/code/work_context_test.go @@ -64,6 +64,10 @@ type edgeCapability struct { Owner string Session string Now func() time.Time + // Installation is the seal; empty seals to "installation-1". + Installation string + // Issuer defaults to the issuer this edge trusts. + Issuer string } // mintCapability signs c with core's Authority under kid/priv, sealed against @@ -78,11 +82,17 @@ func mintCapability(t *testing.T, kid string, priv ed25519.PrivateKey, c edgeCap if c.Owner == "" { c.Owner = "user-1" } + if c.Installation == "" { + c.Installation = "installation-1" + } + if c.Issuer == "" { + c.Issuer = workContextIssuer + } seals := workcontext.NewMemorySealSource() - require.NoError(t, seals.Put(c.Owner, workcontext.Seal{InstallationID: "installation-1", InstallationRevision: 1})) + require.NoError(t, seals.Put(c.Owner, workcontext.Seal{InstallationID: c.Installation, InstallationRevision: 1})) require.NoError(t, seals.PutEpoch(c.Owner, 1)) authority := &workcontext.Authority{ - Issuer: workContextIssuer, KeyID: kid, Key: priv, + Issuer: c.Issuer, KeyID: kid, Key: priv, Revisions: workcontext.FixedRevision(1), Seals: seals, Now: c.Now, } token, _, err := authority.Start(context.Background(), workcontext.StartInput{ @@ -93,7 +103,7 @@ func mintCapability(t *testing.T, kid string, priv ed25519.PrivateKey, c edgeCap TaskID: "task-1", Audience: c.Audience, AuthorityScopes: []*basev0.WorkScopeV1{{ResourceKind: "audit", Actions: []string{"read"}}}, - InstallationID: "installation-1", + InstallationID: c.Installation, SessionID: c.Session, TTL: 5 * time.Minute, }) diff --git a/module/services/auth-gateway/proto/saas/accounts/v1/installation_listing.proto b/module/services/auth-gateway/proto/saas/accounts/v1/installation_listing.proto new file mode 100644 index 000000000..985e1a29c --- /dev/null +++ b/module/services/auth-gateway/proto/saas/accounts/v1/installation_listing.proto @@ -0,0 +1,113 @@ +syntax = "proto3"; + +package saas.accounts.v1; + +import "buf/validate/validate.proto"; +import "google/protobuf/timestamp.proto"; + +// ============================================================================ +// The installation listing, as the auth-gateway reads it. +// +// accounts declares these messages in installations.proto beside the +// InstallationService, which imports the policy options and google.api this +// gateway does not vendor. The gateway calls one internal-tier RPC, +// InstallationService/ListInstallations, so it carries the messages that RPC +// takes and answers, copied definition for definition from installations.proto +// and nothing else. TestVendoredInstallationListingMatchesAccounts holds every +// definition here byte-identical to the one accounts declares, so the two +// cannot drift on the wire. +// ============================================================================ + +enum InstallationStatus { + INSTALLATION_STATUS_UNSPECIFIED = 0; + INSTALLATION_STATUS_ACTIVE = 1; + INSTALLATION_STATUS_REVOKED = 2; + // Installed, and waiting for the solution to acknowledge its standard install + // step. Nothing can use a pending installation. + INSTALLATION_STATUS_PENDING = 3; + // The solution refused its install step, or never answered within the + // attempt budget. Nothing can use it; an administrator may retry it. + INSTALLATION_STATUS_FAILED = 4; +} + +enum InstallationHealth { + INSTALLATION_HEALTH_UNSPECIFIED = 0; + INSTALLATION_HEALTH_HEALTHY = 1; + INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER = 2; + INSTALLATION_HEALTH_AGENT_REVOKED = 3; + INSTALLATION_HEALTH_AGENT_DISABLED = 4; + INSTALLATION_HEALTH_STANDING_GRANT_MISSING = 5; +} + +message Installation { + string id = 1 [(buf.validate.field).string.uuid = true]; + string org_id = 2 [(buf.validate.field).string.uuid = true]; + string agent_principal_id = 3 [(buf.validate.field).string.uuid = true]; + // Was `solution_identifier`: the free-text route alias. It was the join key + // every reader used, and because an alias is deliberately reusable by a later + // binding, an installation that named one inherited whatever took it next. + // Replaced by target_id below. Reserved rather than reused so no consumer can + // read an alias out of a field that now carries an identity. + reserved 4; + reserved "solution_identifier"; + string owner_principal_id = 5 [(buf.validate.field).string.uuid = true]; + repeated string co_owner_principal_ids = 6; + string root_scope_node_id = 7 [(buf.validate.field).string.uuid = true]; + InstallationStatus status = 8; + google.protobuf.Timestamp created_at = 9; + optional google.protobuf.Timestamp revoked_at = 10; + // The immutable solution target this installation was consented to: one + // continuous period of one binding's presence on this host. It is never + // reused, so a replacement binding — even one claiming the same route alias — + // is a different target and inherits nothing. + // + // Deliberately NOT accompanied by the route alias. The alias is a property of + // the target that a later generation may move, and resolving it here would + // mean joining the host's presence state into a request-path listing — which + // would require granting request traffic read over every organisation's + // presence to answer about one. A consumer that needs the alias asks + // ListAvailableSolutions, which serves accepted applied state, and joins on + // this id. + // + // Empty only on a revoked historical row whose target could not be + // established at the cutover; an active installation always names one. + string target_id = 11 [ + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + (buf.validate.field).string.uuid = true + ]; + // Why a revoked installation was revoked, when it was not an administrator + // uninstalling: the installation-identity cutover, or a tenancy narrowing that + // withdrew this organisation. Empty on an active row. + string revoked_reason = 12 [(buf.validate.field).string.max_len = 512]; + // Why the install step failed: the solution's bounded refusal reason, or a + // fixed reason when it never answered. Empty unless status is FAILED. + string failure_reason = 13 [(buf.validate.field).string.max_len = 512]; +} + +message ListInstallationsRequest { + string org_id = 1 [(buf.validate.field).string.uuid = true]; + // Restrict to one lifecycle status. UNSPECIFIED returns every status, so a + // caller that must see only live installs asks for ACTIVE explicitly rather + // than leaning on a default that would silently widen if the enum grew. + InstallationStatus status = 2; + // Page bound. 0 -> server default; capped so an organization with many + // installations can never return an unbounded result in one call. + int32 page_size = 3 [(buf.validate.field).int32 = { + gte: 0 + lte: 500 + }]; + // Opaque cursor from a previous response's next_page_token; empty for page one. + string page_token = 4; +} + +message InstallationSummary { + Installation installation = 1; + InstallationHealth health = 2; +} + +message ListInstallationsResponse { + repeated InstallationSummary installations = 1; + // Set when more installations remain; pass it back as page_token. Empty on the + // last page. + string next_page_token = 2; +} From 840a391c92be69f9bbcef4a62adad378e51e964a Mon Sep 17 00:00:00 2001 From: saurabhmaheshwari-obin <242053479+saurabhmaheshwari-obin@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:28:41 -0400 Subject: [PATCH 03/17] fix(auth-gateway): the bearer-less arm admits one capability and reads every target row Review fixes on the declared module route's bearer-less arm: - Exactly one capability header. Core's Inspector verifies the first value only, so a request carrying two is refused with 401 invalid work context before anything is stripped or asked. The person path is unchanged. - A seal equal to ANY active installation of the route's target is read as a person's. The admission read kept only the last target row it saw, so with two active rows a seal equal to the first was admitted. Every page is now read before deciding. - X-Forwarded-Client-Cert is removed before forwarding. The authenticated listener removes it on a cell; natively nothing did. - The log reason for a request that never reached the admission read is limited_or_origin, since the cross-origin binding refuses there too. - installationConfinementHeader's comment and the AGENTS.md line say the header equals the presented seal whenever a capability is presented. - The AGENTS.md release note: deploy accounts before the gateway, since an older accounts can mint a cross-sealed child valid for up to 900 seconds. - Test fixtures use a neutral path, /v1/documents/items:write. Tests: two capability headers, a seal equal to the first of two target rows, and the header absent upstream on an admitted request. Each fails on the previous arm. Co-Authored-By: Claude Opus 5.5 --- module/GATEWAY_ROUTES.md | 16 ++++--- module/services/auth-gateway/AGENTS.md | 24 ++++++---- .../auth-gateway/code/attack_battery_test.go | 2 +- module/services/auth-gateway/code/gateway.go | 5 ++- .../code/gateway_module_routes_test.go | 35 +++++++++++++-- .../code/gateway_operation_context_test.go | 2 +- .../code/gateway_workload_operation.go | 45 +++++++++++++------ 7 files changed, 92 insertions(+), 37 deletions(-) diff --git a/module/GATEWAY_ROUTES.md b/module/GATEWAY_ROUTES.md index a539b02b0..b0ccf38ac 100644 --- a/module/GATEWAY_ROUTES.md +++ b/module/GATEWAY_ROUTES.md @@ -125,13 +125,15 @@ there may name the alias or `module-capabilities`; anything else is 401 - **The bearer-less arm, on the workload endpoint only**, for a headless operation context. Selected by: no `Authorization` and no `Cookie` header, the workload listener, and a capability whose audience is the alias. Then: a - presenter when the gateway is deployed; a non-empty signed tenant and seal; - every identity header stripped, `Authorization` and `Proxy-Authorization` - dropped; the budget keyed `workload-operation:`, failing closed; and - accounts' `ListInstallations` for that tenant (ACTIVE, uncached) must show an - installation of the target (else 403) and the seal as a DIFFERENT active - installation (else 401 `authentication required`, the answer a person's - capability has without its bearer). 503 when the authority cannot answer or + presenter when the gateway is deployed; exactly one capability header and a + non-empty signed tenant and seal (else 401 `invalid work context`); every + identity header stripped, `Authorization`, `Proxy-Authorization` and + `X-Forwarded-Client-Cert` dropped; the budget keyed + `workload-operation:`, failing closed; and accounts' + `ListInstallations` for that tenant (ACTIVE, uncached) must show an + installation of the target (else 403) and the seal as an active installation + that is none of the target's (else 401 `authentication required`, the answer a + person's capability has without its bearer). 503 when the authority cannot answer or is not wired. Admitted requests carry no bearer and no identity header, and `x-codefly-installation-id` is set to the seal, which names the CALLER's installation, not consent on the target's side. An upstream failure is a diff --git a/module/services/auth-gateway/AGENTS.md b/module/services/auth-gateway/AGENTS.md index d4d6c4536..2cad3c86e 100644 --- a/module/services/auth-gateway/AGENTS.md +++ b/module/services/auth-gateway/AGENTS.md @@ -106,19 +106,27 @@ workload endpoint with no `Authorization` and no `Cookie`, presenting a capability addressed to the alias, takes the bearer-less arm (`gateway_workload_operation.go`): a composed workload's headless operation context, minted at `/modules/_operation-context`. The arm reads no principal. It -requires the signed tenant to have an ACTIVE installation of the target and the -seal to name a different ACTIVE installation in that tenant, read from accounts' -`ListInstallations` per request and uncached; a seal equal to the target's -installation is a person's capability and answers 401 as before. It strips -identity before its limiter, meters on `workload-operation:` (fail -closed), forwards no bearer, stamps only `x-codefly-installation-id` (the seal, -so the CALLER's installation on this arm and the target's admitting installation -on the person path; equal to the presented seal on both), answers an upstream +requires exactly one capability header (401 `invalid work context` otherwise), +the signed tenant to have an ACTIVE installation of the target, and the seal to +name an ACTIVE installation in that tenant that is none of the target's, read +from accounts' `ListInstallations` per request and uncached; a seal equal to any +active installation of the target is a person's capability and answers 401 as +before. It strips identity (and `X-Forwarded-Client-Cert`) before its limiter, +meters on `workload-operation:` (fail closed), forwards no bearer, stamps +only `x-codefly-installation-id` (the seal, so the CALLER's installation on this +arm and the target's admitting installation on the person path; equal to the +presented seal whenever a capability is presented), answers an upstream failure with a static 502, and logs every admit and refusal with alias, tenant, task, seal and presenter. No installations client wired answers 503. It is the gateway's half of a solution's release-declared headless operation; a module calling a peer keeps its in-mesh seam instead. +**Release note: deploy accounts before this gateway.** The arm relies on +accounts refusing a person's child for another audience's installation +(`StartChildSession`). An older accounts mints such a child, valid for up to 900 +seconds, which this arm would read as unattended work. Roll accounts first, and +roll this gateway only once no older accounts replica is serving. + The registry's `SolutionDeclaredBinding.kind` positively identifies each surface. An unspecified kind is refused by both. Neither presence nor a reusable alias constitutes organization consent. diff --git a/module/services/auth-gateway/code/attack_battery_test.go b/module/services/auth-gateway/code/attack_battery_test.go index 0f246ff2f..50e7dcfe7 100644 --- a/module/services/auth-gateway/code/attack_battery_test.go +++ b/module/services/auth-gateway/code/attack_battery_test.go @@ -123,7 +123,7 @@ func TestAttack_TheBearerLessArmIsMeteredOnItsSignedTenant(t *testing.T) { }) require.Len(t, keys, 1, "one bucket, whatever X-Org-Id and source address the caller sent") require.True(t, strings.HasPrefix(keys[0], "rl:workload-operation:"+operationTenant+":"), keys[0]) - require.Contains(t, logs.String(), "reason=workload_operation_limited") + require.Contains(t, logs.String(), "reason=workload_operation_limited_or_origin") } // TestAttack_TheBearerLessArmFailsClosedWithoutItsLimiter: the arm's budget is a diff --git a/module/services/auth-gateway/code/gateway.go b/module/services/auth-gateway/code/gateway.go index 1a8d750de..fb42290ec 100644 --- a/module/services/auth-gateway/code/gateway.go +++ b/module/services/auth-gateway/code/gateway.go @@ -918,8 +918,9 @@ const resourceConfinementHeader = "x-codefly-resource" // entitlement authority and an empty value here would be a claim this process // cannot make. // -// WHAT IT MEANS differs by arm, and one statement holds on both: it equals the -// seal of the capability presented with the request. On a person's path it is +// WHAT IT MEANS differs by arm, and one statement holds on both: when a +// capability is presented with the request, it equals that capability's seal. +// On a person's path it is // the TARGET's admitting installation, and a person's capability for that target // is sealed to it. On a declared module route's bearer-less arm it is the seal of // the headless operation context, which names the CALLER's own installation. diff --git a/module/services/auth-gateway/code/gateway_module_routes_test.go b/module/services/auth-gateway/code/gateway_module_routes_test.go index bd9485bb9..de3981386 100644 --- a/module/services/auth-gateway/code/gateway_module_routes_test.go +++ b/module/services/auth-gateway/code/gateway_module_routes_test.go @@ -407,7 +407,7 @@ func operationCapability(t *testing.T, kid string, priv ed25519.PrivateKey, audi } func publishRequest(capability string) *http.Request { - req := httptest.NewRequest(http.MethodPost, "/v1/documents/definitions:publish", strings.NewReader(`{"bundle":"b"}`)) + req := httptest.NewRequest(http.MethodPost, "/v1/documents/items:write", strings.NewReader(`{"bundle":"b"}`)) if capability != "" { req.Header.Set(carrier.HeaderName, capability) } @@ -436,20 +436,22 @@ func TestModuleRoute_AWorkloadOperationContextIsAdmittedWithoutABearer(t *testin gw, module, installations, kid, wcPriv := operationHarness(t) logs := captureLog(t) capability := operationCapability(t, kid, wcPriv, "documents", publisherSeal) + req := onWorkloadEndpoint(publishRequest(capability)) + req.Header.Set("X-Forwarded-Client-Cert", "By=spiffe://cluster.local/ns/edge/sa/gateway;URI=spiffe://cluster.local/ns/apps/sa/publisher") w := httptest.NewRecorder() - gw.ServeHTTP(w, onWorkloadEndpoint(publishRequest(capability))) + gw.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code, w.Body.String()) require.Equal(t, 1, module.calls) - require.Equal(t, "/v1/documents/definitions:publish", module.lastPath, "the path is forwarded unchanged") + require.Equal(t, "/v1/documents/items:write", module.lastPath, "the path is forwarded unchanged") require.Equal(t, `{"bundle":"b"}`, module.lastBody) require.Equal(t, capability, module.lastHeaders.Get(carrier.HeaderName), "the capability is forwarded as presented") require.Equal(t, publisherSeal, module.lastHeaders.Get(installationConfinementHeader), "the one stamped header is the capability's own seal") for _, header := range []string{ "Authorization", "Cookie", "Proxy-Authorization", "X-User-Id", "X-Org-Id", "X-Session-Id", "X-Scopes", - "X-Credential-Kind", resourceConfinementHeader, "X-Codefly-Gateway-Token", + "X-Credential-Kind", resourceConfinementHeader, "X-Codefly-Gateway-Token", "X-Forwarded-Client-Cert", } { require.Empty(t, module.lastHeaders.Values(header), "%s must not reach the upstream on the bearer-less arm", header) } @@ -505,6 +507,31 @@ func TestModuleRoute_TheBearerLessArmRefuses(t *testing.T) { }, code: http.StatusUnauthorized, body: "authentication required", asked: true, }, + // The tenant may hold more than one ACTIVE row of the target. A seal + // equal to ANY of them is a person's, not only the last row read. + "a capability sealed to the first of two rows of the target": { + prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + gw.installations = &installationsFake{pages: []*accountsv1.ListInstallationsResponse{{ + Installations: []*accountsv1.InstallationSummary{ + activeInstallation(targetInstallation, fakeSolutionTarget("documents")), + activeInstallation("55555555-5555-4555-8555-555555555555", fakeSolutionTarget("documents")), + activeInstallation(publisherSeal, fakeSolutionTarget("publisher")), + }, + }}} + return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", targetInstallation))) + }, + code: http.StatusUnauthorized, body: "authentication required", + }, + // Core's Inspector verified the first capability header only; a second + // is a value nothing verified, so the arm admits exactly one. + "two capability headers": { + prepare: func(t *testing.T, _ *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + req := onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) + req.Header.Add(carrier.HeaderName, operationCapability(t, kid, wcPriv, "documents", targetInstallation)) + return req + }, + code: http.StatusUnauthorized, body: "invalid work context", + }, "a seal that names no active installation in the tenant": { prepare: func(t *testing.T, _ *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", "44444444-4444-4444-8444-444444444444"))) diff --git a/module/services/auth-gateway/code/gateway_operation_context_test.go b/module/services/auth-gateway/code/gateway_operation_context_test.go index fe0f59d6b..d80f6e132 100644 --- a/module/services/auth-gateway/code/gateway_operation_context_test.go +++ b/module/services/auth-gateway/code/gateway_operation_context_test.go @@ -238,7 +238,7 @@ func TestGateway_AMintedOperationContextReachesItsModuleWithoutABearer(t *testin gw.ServeHTTP(w, onWorkloadEndpoint(publishRequest(capability))) require.Equal(t, http.StatusOK, w.Code, w.Body.String()) require.Equal(t, 1, module.calls) - require.Equal(t, "/v1/documents/definitions:publish", module.lastPath) + require.Equal(t, "/v1/documents/items:write", module.lastPath) require.Equal(t, capability, module.lastHeaders.Get(carrier.HeaderName)) // The same context on the public listener is a bearer-less request and diff --git a/module/services/auth-gateway/code/gateway_workload_operation.go b/module/services/auth-gateway/code/gateway_workload_operation.go index c275a3d37..3c5fb32e4 100644 --- a/module/services/auth-gateway/code/gateway_workload_operation.go +++ b/module/services/auth-gateway/code/gateway_workload_operation.go @@ -21,7 +21,8 @@ package main // - the tenant's ACTIVE installations, read from accounts per request. // // It admits only when the tenant has an active installation of the TARGET, and -// the seal names a DIFFERENT active installation in that tenant. The second half +// the seal names an active installation in that tenant that is none of the +// target's. The second half // is how a hop that reads no owner tells unattended work from a person: a // person's capability is sealed to the organization's installation of the // audience it addresses, which here is the target's own, while a module or @@ -42,6 +43,7 @@ import ( accountsv1 "auth-gateway/pkg/gen/saas/accounts/v1" "github.com/codefly-dev/core/workcontext" + carrier "github.com/codefly-dev/sdk-go/workcontext" "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) @@ -115,8 +117,9 @@ const ( // workloadOperationNotInstalled: the tenant has no ACTIVE installation of // the target, or the route resolved no target. workloadOperationNotInstalled - // workloadOperationPersonSealed: the seal is the target's own installation, - // which is what a person's capability for this module carries. + // workloadOperationPersonSealed: the seal is one of the target's own + // installations, which is what a person's capability for this module + // carries. workloadOperationPersonSealed // workloadOperationSealInactive: the seal names no ACTIVE installation in // the tenant (revoked, another tenant's, unknown). @@ -139,7 +142,11 @@ func (g *Gateway) admitWorkloadOperation( if g.installations == nil { return workloadOperationUndecidable } - targetInstallation, sealActive := "", false + // Every ACTIVE row of the target counts: the seal of a person's capability + // for this module is one of them, and keeping only the last row seen would + // read the others as some other installation's. + targetInstallations := map[string]bool{} + sealActive := false token := "" for page := 0; page < installationListingPageLimit; page++ { resp, err := g.installations.List(ctx, &accountsv1.ListInstallationsRequest{ @@ -160,17 +167,14 @@ func (g *Gateway) admitWorkloadOperation( continue } if installation.GetTargetId() == targetID { - targetInstallation = installation.GetId() + targetInstallations[installation.GetId()] = true } if installation.GetId() == seal { sealActive = true } } - // Both positive facts settle it on a partial read; a missing one is - // only conclusive after the whole set, so it waits for the last page. - if targetInstallation != "" && sealActive { - break - } + // No early exit: whether the seal is ANOTHER row of the target is only + // conclusive after the whole set, so every page is read. token = resp.GetNextPageToken() if token == "" { break @@ -180,9 +184,9 @@ func (g *Gateway) admitWorkloadOperation( } } switch { - case targetInstallation == "": + case len(targetInstallations) == 0: return workloadOperationNotInstalled - case seal == targetInstallation: + case targetInstallations[seal]: return workloadOperationPersonSealed case !sealActive: return workloadOperationSealInactive @@ -228,6 +232,13 @@ func (g *Gateway) serveWorkloadOperation( rec.refuse(w, http.StatusUnauthorized, "authentication required", "presenter_missing") return } + // Exactly one capability. Core's Inspector read the first value the + // request carried; a second one is a value nothing here verified, and an + // upstream that read it instead would act on it. + if len(r.Header.Values(carrier.HeaderName)) != 1 { + rec.refuse(w, http.StatusUnauthorized, "invalid work context", "capability_not_single") + return + } if rec.tenant == "" { rec.refuse(w, http.StatusUnauthorized, "invalid work context", "no_tenant") return @@ -243,6 +254,11 @@ func (g *Gateway) serveWorkloadOperation( stripAllIdentityHeaders(r) r.Header.Del("Authorization") r.Header.Del("Proxy-Authorization") + // A client certificate chain is the mesh's statement about the hop that + // reached this gateway, not about the caller, and nothing upstream may read + // one off a request this arm forwards. The authenticated listener already + // removed it on a cell; natively nothing did. + r.Header.Del("X-Forwarded-Client-Cert") // The budget is the signed tenant's own namespace, fail closed: with no // X-Org-Id the generic key would be the waypoint's address, one bucket for @@ -282,7 +298,8 @@ func (g *Gateway) serveWorkloadOperation( })) if !reached { // Refused by the limiter (429, or 503 with its backend down) or by the - // cross-origin binding, each of which wrote its own answer. - rec.log("WARN: blocked", "limited") + // cross-origin binding, each of which wrote its own answer; the log + // cannot tell which, and the reason says both. + rec.log("WARN: blocked", "limited_or_origin") } } From de9a9718afbaf4728e377c44058c14ab29cfbe18 Mon Sep 17 00:00:00 2001 From: saurabhmaheshwari-obin <242053479+saurabhmaheshwari-obin@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:33:43 -0400 Subject: [PATCH 04/17] fix(accounts): classify every mint by where it seals, and allow-list child owner kinds The seal rule's documentation said source-operation contexts and installation tasks are minted for a module or an installation. Both are owned by a person and sealed elsewhere, by design, and the bearer-less module arm admits them: - a source-operation context (startSourceOperation) is sealed to the installation of the module using the person's source delegation, and only that module's identity exercising the delegation mints one; - an installation task (StartInstallationTask) is owned by the installation's owner of record, sealed to that installation, and reached only with an EXPOSURE_INTERNAL credential. WORK_CONTEXTS.md, both AGENTS.md files and the arm's header comment now name them as the two person-owned kinds sealed elsewhere, with their gates, and the arm's admitted family as module operation contexts plus these two. TestEveryWorkContextMintIsClassifiedBySeal enumerates every saas.accounts.v1 RPC whose response carries a token from the generated descriptors (eleven today) and fails on one listed neither as person-sealed, naming the sealPinCases row that holds it, nor as sealed elsewhere, with a one-line reason. A listed name that no longer exists fails too. The seal pin table gains the module read exchange (ExchangeDelegatedReadAudience), which re-mints a person's capability like the operation exchange does. StartChildSession's guard is an allow-list: a person's child is held to its audience's seal, a service principal's passes, and any other owner kind (agent, empty, unknown) is refused with PermissionDenied. TestStartChildSessionGuardAllowsOnlyKnownOwnerKinds pins it. Co-Authored-By: Claude Opus 5.5 --- module/WORK_CONTEXTS.md | 34 ++-- module/services/accounts/AGENTS.md | 40 +++-- .../code/pkg/adapters/work_context_rpcs.go | 13 +- .../adapters/work_context_seal_pin_test.go | 151 ++++++++++++++++-- module/services/auth-gateway/AGENTS.md | 16 +- .../code/gateway_workload_operation.go | 8 +- 6 files changed, 213 insertions(+), 49 deletions(-) diff --git a/module/WORK_CONTEXTS.md b/module/WORK_CONTEXTS.md index 5e298426b..e0d63e0af 100644 --- a/module/WORK_CONTEXTS.md +++ b/module/WORK_CONTEXTS.md @@ -344,16 +344,30 @@ answered by one set of functions for minting and verifying alike is sealed to the organization's installation of it, held while the person is an active member; an organization that never installed the audience is minted nothing. A module's is sealed to the installation its mint resolved. - This is an invariant, not a default: every capability a person owns is sealed - to the installation of the audience it addresses, on every mint that issues - one. `StartChildSession` is the one derivation core cannot re-seal (a child - keeps its parent's seal), so it refuses a person's child for an audience whose - installation is not the parent's seal. Source-operation contexts and - installation tasks are not exceptions: they are minted for a module or an - installation, and sealed to that principal's own installation. The gateway's - bearer-less module arm reads the seal as the owner's kind on this rule - (`TestAPersonsCapabilityIsSealedToItsAudiencesInstallation` pins it over every - person mint). + This is an invariant, not a default: every capability a person's own session + or a module's re-mint for that person issues is sealed to the installation of + the audience it addresses. `StartChildSession` is the one derivation core + cannot re-seal (a child keeps its parent's seal), so it refuses a person's + child for an audience whose installation is not the parent's seal. + Two kinds of person-owned capability are sealed elsewhere, by design, and + each has its own mint gate: + - **Source-operation contexts** (`startSourceOperation`, + `pkg/adapters/source_delegation.go`): owned by the person, sealed to the + installation of the module USING the person's source delegation. Only that + module's identity, exercising a delegation the person granted to one of its + bindings, mints one. + - **Installation tasks** (`mintInstallationTask`, + `pkg/adapters/work_context_rpcs.go`): owned by the installation's owner of + record, sealed to the installation the authority is drawn from. Only an + `EXPOSURE_INTERNAL` credential reaches `StartInstallationTask`. + + The gateway's bearer-less module arm admits a capability whose seal is an + active installation other than the target's, so it admits these two kinds as + well as module operation contexts: that is the unattended-work family it is + for, and no person's interactive session can produce one. + `TestAPersonsCapabilityIsSealedToItsAudiencesInstallation` pins the rule over + every person mint, and `TestEveryWorkContextMintIsClassifiedBySeal` fails on + any mint RPC that names neither where it seals nor why. - **Epoch.** The owner's authorization revision in the organization. Membership and role changes advance a person's; installing a module and every change to its installation's revision advance a module principal's (migration 35). diff --git a/module/services/accounts/AGENTS.md b/module/services/accounts/AGENTS.md index 2ad71622b..a0ce815b8 100644 --- a/module/services/accounts/AGENTS.md +++ b/module/services/accounts/AGENTS.md @@ -513,22 +513,38 @@ therefore briefly refuses questions about capabilities already in flight. ## A person's capability is sealed to its audience's installation -Every capability a person owns is sealed to the organization's installation of -the audience it addresses; a module or installation principal's is sealed to its -own. That is an invariant every person mint keeps, not a default: `StartTask` -resolves it, `remint` re-resolves it for a root session, an audience exchange, a -renewal and a module's exchange through its operation binding, and -`StartChildSession` (whose core `Child` keeps the parent's seal) refuses a -person's child for an audience sealed elsewhere. +A capability a person's session issues, or a module re-mints for that person, +is sealed to the organization's installation of the audience it addresses; a +module principal's is sealed to its own. That is an invariant every such mint +keeps, not a default: `StartTask` resolves it, `remint` re-resolves it for a +root session, an audience exchange, a renewal and a module's exchange through +its operation or read binding, and `StartChildSession` (whose core `Child` keeps +the parent's seal) refuses a person's child for an audience sealed elsewhere. It +holds a child of a service principal's capability as it is, and refuses a child +of any other owner kind. + +Two person-owned kinds are sealed elsewhere by design, each behind its own gate: + +- a **source-operation context** (`startSourceOperation`): sealed to the + installation of the module using the person's source delegation, and minted + only by that module's identity exercising the delegation + (`MintSourceOperationContext`, or the delegation arm of + `ExchangeDelegatedOperationAudience`); +- an **installation task** (`StartInstallationTask`): owned by the + installation's owner of record, sealed to that installation, and reached only + with an `EXPOSURE_INTERNAL` credential. The gateway's bearer-less module arm depends on it: it admits a capability with no bearer only when the seal names an active installation other than the -target's, which a person's capability never does. +target's. That admits module operation contexts and the two kinds above, the +unattended-work family, and never a person's interactive capability. `TestAPersonsCapabilityIsSealedToItsAudiencesInstallation` -(`pkg/adapters/work_context_seal_pin_test.go`) pins it over every person mint. A -new mint that issues a person's capability goes in that table; one that copies a -parent's seal across audiences reopens the arm to a person's credential without -its bearer. +(`pkg/adapters/work_context_seal_pin_test.go`) pins it over every person mint, +and `TestEveryWorkContextMintIsClassifiedBySeal` enumerates every mint RPC from +the generated descriptors and fails on one listed neither as person-sealed (with +its case in that table) nor as sealed elsewhere (with its reason). A mint that +copies a parent's seal across audiences reopens the arm to a person's credential +without its bearer. The arm reads the tenant's installations through `InstallationService/ListInstallations` (`EXPOSURE_INTERNAL`). Its contract comment in `installations.proto` names one diff --git a/module/services/accounts/code/pkg/adapters/work_context_rpcs.go b/module/services/accounts/code/pkg/adapters/work_context_rpcs.go index fdaaaf1d2..a339ac48f 100644 --- a/module/services/accounts/code/pkg/adapters/work_context_rpcs.go +++ b/module/services/accounts/code/pkg/adapters/work_context_rpcs.go @@ -1473,15 +1473,22 @@ func (s *WorkContextAuthorityServer) StartChildSession( // would be admitted there without the person's bearer. // // A same-audience child keeps the seal its audience resolves to now, and is -// minted. A principal that is not a person is sealed to its own installation, -// which the audience does not change, and is not held here. +// minted. A service principal is sealed to its own installation, which the +// audience does not change, and is not held here. Any other owner kind is +// refused: the rule is stated for these two, and a kind it does not name is not +// read as either. // TestAPersonsCapabilityIsSealedToItsAudiencesInstallation pins the invariant // over every person mint. func (s *WorkContextAuthorityServer) requireChildKeepsAudienceSeal( ctx context.Context, parent *basev0.WorkContextV1, audience string, ) error { - if parent.GetOwnerPrincipalKind() != business.PrincipalKindHuman { + switch parent.GetOwnerPrincipalKind() { + case business.PrincipalKindHuman: + case business.PrincipalKindService: return nil + default: + return status.Error(codes.PermissionDenied, + "a child Work Context is derived only from a person's or a service principal's") } installation, err := s.audienceInstallation(ctx, parent.GetTenantId(), audience) if err != nil { diff --git a/module/services/accounts/code/pkg/adapters/work_context_seal_pin_test.go b/module/services/accounts/code/pkg/adapters/work_context_seal_pin_test.go index 47e8e9581..15cdb075d 100644 --- a/module/services/accounts/code/pkg/adapters/work_context_seal_pin_test.go +++ b/module/services/accounts/code/pkg/adapters/work_context_seal_pin_test.go @@ -9,6 +9,8 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/reflect/protoregistry" accountsauth "accounts/pkg/auth" "accounts/pkg/business" @@ -36,23 +38,31 @@ import ( // for, and what it answered. type sealPinMint func(t *testing.T) (server *WorkContextAuthorityServer, audience string, token string, err error) -func TestAPersonsCapabilityIsSealedToItsAudiencesInstallation(t *testing.T) { - childRequest := func(parent, audience string) *gen.StartChildSessionWorkContextRequest { - return &gen.StartChildSessionWorkContextRequest{ - OrgId: renewOrgID, ParentWorkContextToken: parent, ActorPrincipalId: renewActorID, - Audience: audience, TtlSeconds: 60, - GrantedScopes: []*gen.WorkContextScope{{ResourceKind: "evidence", Actions: []string{"read"}}}, - } - } - issued := func(out *gen.IssuedWorkContext, err error) (string, error) { - return out.GetToken(), err +// sealPinCase is one row of the table: a person mint, and whether it must be +// refused rather than minted. +type sealPinCase struct { + name string + refused bool + mint sealPinMint +} + +func sealPinChildRequest(parent, audience string) *gen.StartChildSessionWorkContextRequest { + return &gen.StartChildSessionWorkContextRequest{ + OrgId: renewOrgID, ParentWorkContextToken: parent, ActorPrincipalId: renewActorID, + Audience: audience, TtlSeconds: 60, + GrantedScopes: []*gen.WorkContextScope{{ResourceKind: "evidence", Actions: []string{"read"}}}, } +} - for _, tc := range []struct { - name string - refused bool - mint sealPinMint - }{ +func sealPinIssued(out *gen.IssuedWorkContext, err error) (string, error) { + return out.GetToken(), err +} + +// sealPinCases is every person mint the table runs. Each RPC listed in +// personSealedMints names the case here that covers it. +func sealPinCases() []sealPinCase { + childRequest, issued := sealPinChildRequest, sealPinIssued + return []sealPinCase{ {name: "StartTask", mint: func(t *testing.T) (*WorkContextAuthorityServer, string, string, error) { exchangeInstallationService(t) server := newCoreTestServer(t, &workContextAuthorityFake{facts: &business.WorkContextAuthorityFacts{OrganizationRevision: 12}}, newSealAnswersFake(12)) @@ -132,7 +142,26 @@ func TestAPersonsCapabilityIsSealedToItsAudiencesInstallation(t *testing.T) { } return workContextSingleton, "example-producer", out.Msg.Token, nil }}, - } { + {name: "a module's read exchange through its read binding", mint: func(t *testing.T) (*WorkContextAuthorityServer, string, string, error) { + _, _, client, _ := sourceReadFixture(t) + installReadBinding() + parent, _ := mintTestCapability(t, workContextSingleton, testCapability{ + Audience: "example", Tenant: readOrg, Owner: readOwner, + Task: "019f6bf7-1111-7111-8111-111111111111", Session: "019f6bf7-2222-7222-8222-222222222222", + Scopes: []*basev0.WorkScopeV1{{ResourceKind: "results", Actions: []string{"read"}}}, + Installation: "installation-retired", + }) + out, err := client.ExchangeDelegatedReadAudience(context.Background(), readExchangeRequest(t, parent)) + if err != nil { + return workContextSingleton, "example-producer", "", err + } + return workContextSingleton, "example-producer", out.Msg.Token, nil + }}, + } +} + +func TestAPersonsCapabilityIsSealedToItsAudiencesInstallation(t *testing.T) { + for _, tc := range sealPinCases() { t.Run(tc.name, func(t *testing.T) { server, audience, token, err := tc.mint(t) if tc.refused { @@ -180,3 +209,93 @@ func TestStartChildSessionRefusesAPersonsChildForAnotherAudiencesInstallation(t require.NoError(t, err) require.Equal(t, testInstallation("tool.test"), child.Context().GetSeal().GetInstallationId()) } + +// personSealedMints is every RPC that issues a person's capability sealed to +// its audience's installation, with the case in sealPinCases that holds it there. +var personSealedMints = map[string]string{ + "WorkContextService/StartTask": "StartTask", + "WorkContextService/StartRootSession": "StartRootSession", + "WorkContextService/ExchangeAudience": "ExchangeAudience", + "WorkContextService/RenewWorkContext": "RenewWorkContext", + "WorkContextService/StartChildSession": "StartChildSession for another audience", + // A plain parent is re-minted for the binding's audience. Its other arms + // are listed in notAudienceSealedMints. + "ModuleCapabilitiesService/ExchangeDelegatedOperationAudience": "a module's exchange through its operation binding", + "ModuleCapabilitiesService/ExchangeDelegatedReadAudience": "a module's read exchange through its read binding", +} + +// notAudienceSealedMints is every RPC (or arm of one) that issues a capability +// NOT sealed to its audience's installation, each by design and with the gate +// that keeps it from a person's hands. The gateway's bearer-less module arm +// admits these: a seal other than the target's is what they carry. +var notAudienceSealedMints = map[string]string{ + "ModuleCapabilitiesService/MintModuleWorkContext": "a module's own identity (StartModuleTask): owned by the module principal and sealed to its own installation", + "ModuleCapabilitiesService/MintModuleOperationContext": "a module operation context (StartModuleOperationTask): owned by the module principal and sealed to its own installation", + "ModuleCapabilitiesService/MintSourceOperationContext": "a source-operation context (StartSourceOperationTask): owned by the person, sealed to the USING module's installation, minted only by that module's identity exercising the person's source delegation", + "ModuleCapabilitiesService/ExchangeDelegatedOperationAudience": "its delegation_id arm and a source-delegation parent re-mint a source-operation context (startSourceOperation), sealed and gated as MintSourceOperationContext is", + "WorkContextService/StartInstallationTask": "an installation task: owned by the installation's owner of record, sealed to the installation it draws on, minted only with an EXPOSURE_INTERNAL credential", +} + +// TestEveryWorkContextMintIsClassifiedBySeal enumerates every accounts RPC that +// answers a signed capability (a `token` field in its response) from the +// generated descriptors, and fails on one in neither list. A new mint therefore +// cannot ship without saying where it seals; one that issues a person's +// capability goes in sealPinCases too. +func TestEveryWorkContextMintIsClassifiedBySeal(t *testing.T) { + cases := map[string]bool{} + for _, tc := range sealPinCases() { + cases[tc.name] = true + } + for rpc, name := range personSealedMints { + require.True(t, cases[name], "%s names case %q, which sealPinCases does not run", rpc, name) + } + + mints := map[string]bool{} + protoregistry.GlobalFiles.RangeFilesByPackage("saas.accounts.v1", func(file protoreflect.FileDescriptor) bool { + for i := 0; i < file.Services().Len(); i++ { + service := file.Services().Get(i) + for j := 0; j < service.Methods().Len(); j++ { + method := service.Methods().Get(j) + if method.Output().Fields().ByName("token") != nil { + mints[string(service.Name())+"/"+string(method.Name())] = true + } + } + } + return true + }) + require.NotEmpty(t, mints) + for rpc := range mints { + _, person := personSealedMints[rpc] + _, elsewhere := notAudienceSealedMints[rpc] + require.True(t, person || elsewhere, + "%s issues a capability and is in neither personSealedMints nor notAudienceSealedMints: say where it seals", rpc) + } + for _, listed := range []map[string]string{personSealedMints, notAudienceSealedMints} { + for rpc := range listed { + require.True(t, mints[rpc], "%s is listed but no generated service answers a token from it", rpc) + } + } +} + +// The child guard is an allow-list of owner kinds: a person's child is held to +// its audience's seal, a service principal's passes, and any other kind +// (an agent, an empty or unknown one) is refused rather than read as either. +func TestStartChildSessionGuardAllowsOnlyKnownOwnerKinds(t *testing.T) { + exchangeInstallationService(t) + server := newRenewTestServer(t) + for name, allowed := range map[string]bool{ + business.PrincipalKindService: true, + business.PrincipalKindAgent: false, + "": false, + "Human": false, + } { + kind := name + err := server.requireChildKeepsAudienceSeal(context.Background(), + &basev0.WorkContextV1{OwnerPrincipalKind: &kind, TenantId: renewOrgID}, "documents") + if allowed { + require.NoError(t, err, "kind %q", kind) + continue + } + require.Equal(t, codes.PermissionDenied, status.Code(err), "kind %q: %v", kind, err) + } +} diff --git a/module/services/auth-gateway/AGENTS.md b/module/services/auth-gateway/AGENTS.md index 2cad3c86e..e85493e4f 100644 --- a/module/services/auth-gateway/AGENTS.md +++ b/module/services/auth-gateway/AGENTS.md @@ -133,11 +133,17 @@ constitutes organization consent. Module identity exchanges are described below; accounts owns their authority. -A capability's seal follows its owner: a person's is sealed to the organization's -installation of the audience it addresses, a module or installation principal's -to its own. Accounts holds that as an invariant over every person mint, and -`TestAPersonsCapabilityIsSealedToItsAudiencesInstallation` in accounts -(`pkg/adapters/work_context_seal_pin_test.go`) pins it. The gateway reads no +A person's interactive capability is sealed to the organization's installation +of the audience it addresses, a module principal's to its own. Two person-owned +kinds are sealed elsewhere by design: a source-operation context, sealed to the +installation of the module using the person's source delegation and minted only +by that module's identity, and an installation task, sealed to its installation +and minted only with an `EXPOSURE_INTERNAL` credential. The bearer-less arm +admits all three kinds (the unattended-work family) and never a person's +interactive capability. Accounts holds that as an invariant over every person +mint: `TestAPersonsCapabilityIsSealedToItsAudiencesInstallation` and +`TestEveryWorkContextMintIsClassifiedBySeal` in accounts +(`pkg/adapters/work_context_seal_pin_test.go`) pin it. The gateway reads no owner (core's `Inspected` carries none), so any decision this gateway makes from a seal rests on that test. diff --git a/module/services/auth-gateway/code/gateway_workload_operation.go b/module/services/auth-gateway/code/gateway_workload_operation.go index 3c5fb32e4..f5f1bd732 100644 --- a/module/services/auth-gateway/code/gateway_workload_operation.go +++ b/module/services/auth-gateway/code/gateway_workload_operation.go @@ -24,9 +24,11 @@ package main // the seal names an active installation in that tenant that is none of the // target's. The second half // is how a hop that reads no owner tells unattended work from a person: a -// person's capability is sealed to the organization's installation of the -// audience it addresses, which here is the target's own, while a module or -// installation principal's is sealed to its own installation. Accounts holds +// person's interactive capability is sealed to the organization's installation +// of the audience it addresses, which here is the target's own, while a module +// operation context, a source-operation context and an installation task are +// each sealed to another installation (the unattended-work family, admitted by +// design; see WORK_CONTEXTS.md). Accounts holds // that as an invariant (TestAPersonsCapabilityIsSealedToItsAudiencesInstallation // in accounts); this arm is sound only while it holds. A misreading refuses, // never widens, and the callee verifies the capability at full strength. From fb7d279e2defc24cc41152f75c84b67b7f84f11d Mon Sep 17 00:00:00 2001 From: saurabhmaheshwari-obin <242053479+saurabhmaheshwari-obin@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:47:07 -0400 Subject: [PATCH 05/17] contracts: accounts owns installation_listing.proto, and the gateway vendors it verbatim The gateway's installation_listing.proto declared, in accounts' package, a file accounts does not have. Each published contract loaded alone, but loading accounts' and the gateway's into one descriptor pool failed with a name conflict over saas.accounts.v1.Installation. The six definitions ListInstallations takes and answers (InstallationStatus, InstallationHealth, Installation, ListInstallationsRequest, InstallationSummary, ListInstallationsResponse) move, with their comments, from installations.proto into an accounts-owned saas/accounts/v1/installation_listing.proto with minimal imports, the pattern module_registration.proto and solution_entitlements.proto follow. installations.proto and platform_admin.proto import it. Same package, same fully-qualified names, same wire format; every RPC stays where it was. buf.yaml gains scoped MESSAGE_NO_DELETE and ENUM_NO_DELETE waivers for installations.proto, as for the earlier moves; buf breaking against the previous tree and buf lint pass. The gateway's copy is now that file byte for byte. The regex drift test is replaced by TestVendoredAccountsProtosAreAccounts, which holds every vendored accounts proto in the gateway byte-identical to accounts' file of the same name (five files today). The ListInstallations contract comment, the ListOrganizationInstallations comment and the ListInstallations handler comment now name the gateway's second read: its bearer-less module arm, whose tenant is the one an edge-verified Work Context names. The accounts AGENTS.md note that deferred this to the next regeneration is replaced. Regenerated with the CI's Codefly source candidate 5234ce24 (proto companion 0.0.16): codefly generate proto --path saas for accounts (a no-change run with --path saas was clean first; without it the CLI also generates the vendored codefly/ tree and reorders the raw OpenAPI), codefly generate proto for the gateway, go generate for accounts' business, adapters and cataloggen, codefly generate contracts saas-starter, and the saas-sdk library through its own scripts/generate.mjs --force. The frontend's imports of the moved types point at installation_listing_pb (biome check clean), and ui-inventory.json records the new import. Both published contracts, and all three together, now load into one pool. Co-Authored-By: Claude Opus 5.5 --- .../api/accounts/authority/contract.binpb | Bin 1199552 -> 1200659 bytes .../api/accounts/authority/proto/buf.yaml | 11 + .../accounts/v1/installation_listing.proto | 131 +++ .../saas/accounts/v1/installations.proto | 132 +-- .../saas/accounts/v1/platform_admin.proto | 2 +- .../api/accounts/connect/contract.binpb | Bin 1199552 -> 1200659 bytes .../api/accounts/connect/proto/buf.yaml | 11 + .../accounts/v1/installation_listing.proto | 131 +++ .../saas/accounts/v1/installations.proto | 132 +-- .../saas/accounts/v1/platform_admin.proto | 2 +- .../api/auth-gateway/grpc/contract.binpb | Bin 355469 -> 356835 bytes .../accounts/v1/installation_listing.proto | 40 +- module/contracts/api/catalog.codefly.json | 6 +- module/module.package.codefly.yaml | 6 +- module/services/accounts/AGENTS.md | 14 +- .../code/pkg/adapters/installation_rpcs.go | 6 +- .../installations.connect.go | 34 +- .../accounts/v1/installation_listing.pb.go | 607 +++++++++++++ .../gen/saas/accounts/v1/installations.pb.go | 826 ++++-------------- .../saas/accounts/v1/installations_grpc.pb.go | 34 +- .../gen/saas/accounts/v1/platform_admin.pb.go | 4 +- .../generated/openapi-raw/api.swagger.json | 2 +- .../accounts/openapi/api.swagger.json | 2 +- module/services/accounts/proto/buf.yaml | 11 + .../accounts/v1/installation_listing.proto | 131 +++ .../saas/accounts/v1/installations.proto | 132 +-- .../saas/accounts/v1/platform_admin.proto | 2 +- .../code/installation_listing_vendor_test.go | 32 - .../accounts/v1/installation_listing.pb.go | 19 + .../code/vendored_accounts_protos_test.go | 31 + .../accounts/v1/installation_listing.proto | 40 +- .../generated/contract/catalog.codefly.json | 2 +- .../generated/contract/contract.binpb | Bin 1199552 -> 1200659 bytes .../saas-sdk/generated/library.codefly.yaml | 4 +- .../accounts/v1/installation_listing_pb.ts | 306 +++++++ .../gen/saas/accounts/v1/installations_pb.ts | 361 +------- .../gen/saas/accounts/v1/platform_admin_pb.ts | 6 +- .../model/installations.test.ts | 4 +- .../model/installations.ts | 8 +- .../solution-installations/service/queries.ts | 8 +- .../ui/installation-setup-page.tsx | 2 +- .../ui/solution-installations-page.tsx | 6 +- .../accounts/v1/installation_listing_pb.ts | 307 +++++++ .../gen/saas/accounts/v1/installations_pb.ts | 361 +------- .../gen/saas/accounts/v1/platform_admin_pb.ts | 6 +- .../services/frontend/code/ui-inventory.json | 2 + 46 files changed, 2115 insertions(+), 1799 deletions(-) create mode 100644 module/contracts/api/accounts/authority/proto/saas/accounts/v1/installation_listing.proto create mode 100644 module/contracts/api/accounts/connect/proto/saas/accounts/v1/installation_listing.proto create mode 100644 module/services/accounts/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go create mode 100644 module/services/accounts/proto/saas/accounts/v1/installation_listing.proto delete mode 100644 module/services/auth-gateway/code/installation_listing_vendor_test.go create mode 100644 module/services/auth-gateway/code/vendored_accounts_protos_test.go create mode 100644 module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installation_listing_pb.ts create mode 100644 module/services/frontend/code/src/gen/saas/accounts/v1/installation_listing_pb.ts diff --git a/module/contracts/api/accounts/authority/contract.binpb b/module/contracts/api/accounts/authority/contract.binpb index 590c0a8eb1d2d00c9520361083a0d0e6c244e78f..18d4889d20273ab7985cefe5699eb283f050e078 100644 GIT binary patch delta 15827 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 delta 15097 zcmZ8odzcl)u|M6@-Fwc=z{0S*EDJ1)0*br^m6uOIP@^KE5;alKHIU@fT*0gNn(sS4 zRs}UN8j?{IglI$*1yneKfC};ui73cZQ9+~P6T?ehdVf{jGqcw}_}E`{cXd_$s;hf? z=KS@^b6$J&oZA{igKY2`RD*6X4P_1ahC+k?Qn2SaZ}N9CzQ`+Y5g%O_gz?4ef*a#? zFNW>o73+ddaVuR`TN?j-5X;iZ?*vD5$)6YO7NuUBf)+vP@C(5kqBLknP}?MU^hxQL zMLL)J+{8QXE^iktbu&(hq&Db229)uZ?W)2fiXipi|N$fjV?_NT6d|*~WEb&^~EG$F}Y29Fl0O zm2F)|G}X#uk|siR?fq7EZVx@%qvs^SlY2mM^}^u3w(Zf!c2Vfk0kg)oivo&YTNpf2 z-U0Ky>A^!0scyMGrLl{GH$}0V^GmQ_-JM@Rk=Nb%1pq6$JHLb^zj`=xfe2W00no9B zGet;h>nV?8zkmpIWh(&)ozTTElF>qvU%lk9t|#2;C69MKJ@o9=={SeTyAx#x*AdR0 zC_5)j=y>9B$uM~7^~Q9>l8_*{x9sIQYIN+~qo@C@@MWmLci-GKqGpe6XUr00w=%R3q5K}E%adFAhNK?5%rvfgJ7X0 zN+BBtOWzU2pj*;}DF+A56NFg?N1P}yM`4!1T{tg+GR=u&iUlwsV^ zHe7mOP8r4x?Rr4he#iVI6xU2ug;X*vFv)+#iKld}%oK z3y~TnuTV9m-!2QrdcPV~h?0I7HL9|u--P2*$fIBN6`o)2=V4a?p=cd>zf>o?+#&%j zh09eJH{QoQS7Dy_QL!)rnR8X#V`Z@YysHXab4i9NuBvI141hydwH@XZ`lOKBYlph_ z01C%I;Z&49k9An1E z|GhUDRYVKajW1L>vn6a8U!6E15s&dTL!6D$5iq{>V9tC|z|fmhyU71W2y+mk`Ye1j z-4fV5!L9;ZZiOxX6i7w*jwx@AFWDE&xXA7}x28d)QgbVUNJ|@Su;$iUTMP-={JOQB ztu#U^=q*C;63Y(OS}`8FKUfHEXn!genmI*OTZ%30PJ%Y5Emof9oK%kVV%4dRJJcR^ zhk)LjICeTRKzE6F#DQR3kro&BF=WCW0!7F{k07c=xoyqUkh;4B(VwU8n4@*1?i5&k z&6$2znN)*7Mpz|AMQWlzv1qXFn8*O$CsfUZx{GhR=9>EPTQI82V(CI-rRny3prfdq-OhZD?5GrYh71J^+@~~o>i0YD7D4QmZ?dYt? z({RygMruXgS@C#g1&i2<>6sO%$7&Yk$8^tEL@z4xEK%xtG>F9&HuzX55TPz zSDMiu#bK7HKGQMbDltT@&Ts>KI68+M{a&)CKEYC(pMtT%1sug1qLh3rIKapje+uJLgFgpL3#Ul4A_BRSGj$mrB$k!ElMvr5=Byk@wQb0DgK!%diY>XNBJ;Y)z2V$K zT~@EdE%r*WSId%#0V4%uy#hktWyQoTU`)I$aSIR=FH77aXeVBlxP|P5Tgx-;PzPk) zLL)=x6(E!^PrL$z^5x`JkrI-y)xJD&3%kNfk$cCvMf<@@k?+cGX?AN>GBIGJfUH|U z=({TM3J?>oO1uKZ#H$jo0E=$oRf$`g-CCV#hdLnZ78)5kw*aAib>bEvw69LcB51u@ zoh$@R=LWTgln=o|07M~LBjS5>IPgkZhmc@`HKNeYxr)sk@EBX^(TWMyhz{p--V?MF ztPvf5;RblnxRy%WnLGiq9%xj)t?0JS zRdEv7eeGXK6?Y=u*WRXmjj|G(>w1yf=;*`IU#%DUVlCTfn7<84gaIQ3WcdR^=M4#e zfUsvn!XF?4up!}(pbfx=gg(Rcy_YG6HXx?2sL;yT@dpU)?JLi>9Oe*~@e_mZr{ z=DbVTMKXA*Tl#ARm^b%Hi7JCmsA*@4fAqnm&ApLiVmS4N#)VCrQYlD$ptS_`?z7++#yP^Yze=Dk5`m2ePLS*)d+z+Dk$F;#}mI*qq z>=Sv*VSzJcf08MHkpi-r0))Q%lSBc8?)^!e3LNJH6lXdE07eQZ8A>5OkfaJ4G4w!@ z#?q4XBXdaPesZ}>2bx34(ZZKGxOkYV2B`&P)QiIbgycLd0=s$vG4!x#XNxf)W;rZy z&qSf|dHMZFu^Y>pXhi|Gt+_nDeRz0Ay!-8--Ersz9X|@U3<06zM_PufaS$@O$TY~@ zdn&#nHK@hv- zy;40U4dn(4Uj8$3<6ap&CN(8OB7#siwpG7S7YTYZf09nW-|E4)+twNQgX`MlLj#l||}tY0CWt zmIM)*nNq$gz0#f_>@A8jrQvfiT*keqnklO}7YKrdnR38zXNLd_PfN8ZWkHbnBnuX$ zEU*kL%*t3`5i~!OvA`l?;aRXSCuM=fU}0{?0*jc17cv&C5Gr5HSYR1gn4iUxMPOk; z#sZ7LLa7l8ECUNK|7;{!SeUUu-oGN%(v$_EEDNv5U}?&N6@rDu(hP7cSQ%JYLM%Lm z0*Tn7KrWZ^ed)zdG!369E|-Q6=hOnfmdjQrx|Km?a=AQpgd+hb4)caon^F>_CE*Pj zY)VOB8Aw=>k-#EoUYU`=A|hcGNO&tHfyE$UZAJo%n1r__PH{H5tPm>KWhAf+B)pr2 zl0_h4eMSO{K*EMbB(Mx5y!W$_AYo%h0(rk#s% zwh{@mED1>7kEA>R5|%d$pDKQotpr@>tBGGZI)tBzyuAK2J$tF-Z6#BY{Os!k5`futKQ(DkFhqAmN)Vlq>=X z-)1DR2qb*hhy<2_gxx7=Vq)4JY1`zvcoX9Ch#x_c=1(^4tj|C zQL4Xq$@ZWu9Y4z8FJ7`eunctkl+nQ=Xui!$w+9vx9k+W$bC;KF4=e^9cYEpfz#^t& zqL=UI9JE5HoaCk31Is|ieO|gfun2VA@1@%Vi$KSpyli`58R+=)&qjid2dJCf9?1I# zJvABL+wDO)Iv(_b$#CDURx1P@4|!(L|C51^zj~l!w_Ou>a+~7G*`62wzB2r#IK@l% zXuOb8Q@nJK#+wDeQ!a33XqfS+r=Cek(3XTpz2KRY1eSq>$GmipW)U<`^-LGHN3)1X zmUG(d29zDM9H#d&HenS1V-F;^-J-sdP z(mme5t_5DY$Kyo~;DC{iMclHQlBZrzSu~c#k{7(5vdA)Mec3bZT@*-!yAbS{p6S>~ z=7Fz88DBh-_s2?f0}2Jpz}IWk?JZo% zLWZZl^`86!kzZu$;vHWGRi(4qgx$U3de86`81;co>%F#ot4k13S?>+G#zh5}yk>)^ zcBWA&vr*aL1v}HIunbXoFUxHfLG#9}z_5sNdlPbdTbkP}My%e?V#Ok|>w_#-EJLie zXR%@tV)bE`+blw?KFVUnBE;(BMguQ(xxFLHZMx0=+*89`(93KkfhZX2a@&ev)#qN_ z+5UktjxZkG4tnxF<<)#vRu})YEofWXQ5SwCT6C=&ef_m{qwB^{%1)@i9+_DRj}149 z;z2Jh?s;fC=%vLykJumdF22RZ9+%qYkf$c8q`2p8><@XtB$X6*mLc|sv)Ho;nvZ0$ zXA#BzC}MxRN{Ty+5&Jt-T31*^vA$4DoA!0D}q(`s=B`Zvo^c&xcab?^Hiy4r?9hE zd{`NN$%Z5@py$J?mT!m&BCZdsq1U;%77*7*l$xE!wP54=hze$>ab+3eIz^@Rnnlq3 zs7g0Y7ExRuQ$;g9jVp^0*BKO7`%J|m-ZY<3`3^3ntPm=nROzP4G6egnEM_c1uxF}t z(_|5X{d5}aqGTDOJ?m#9Q52p@yV+jlsL%>0M7xhtK%T=kiGbLBwBYv9=TtBk5wSat z6@segReG$kGVG&spaSO_Tk!F~zDUUps`SMPcz!P~Qih-N!GsTA7peAVIpXPU#3FV6 z7-t7wZkSh;dOIb?x5T`ng11v*SO#JiXT-1wnwMn6u!x9R3SyR}#IP8|EYFBx5fif_ zBgP7$a%Dyg%RtQPEW9iNF>hwXun5GgNh4mwVb4D7zV)+_AZBga&GrH@|H_D=fUHw$ zeM*e)h*_tC^(iq{2x8t*W|$+(%0SG!@Si?Zkt#stRk@Gw4whac1PI|KK%~s`Z$pBGmx2mcSaLYrtWF+8QvaPD+ z2g<&?bTZtMZ9^}rCP_D_dSAI)vVgDbJ|t)9mJASEh~i7K&U6z50Sx(2xl1xYjQ&vJ zl57H^7~*+F<$jFM?-}0k>ku73QSPNh$k$DuDtC_x7%3oo#RY`EpDK5a3J9g2DtC<< z+FMBFu2BJT#i-(%Ug4lV0F2!IBOpe8mW`B1=dYD);U7Q-sgS(+y7XYLaJ;}!QuMW| zsG;>nZ5aBsYHRPqLcT5hj^gkn4!~$e>vwVA-r;nNq>IAul)L8wq&DT=n}zi9M(tI& zb$h~2O0X36!I7XY!>WTl$|`cL>KXpA>!!cjzd1P*vL(l7JX`P~n3) z8yDLAkCG7|+mTo|N8`08haa3AN_^piw!pqX1caias+E124hTg@RhO>*cHIBrF~Qua zb2D`O;VI$f3n}<_YRz}2c;i6{(4ZxK4@7TEkR3q$j-L={YtZ;S2nGQX<#%h@!ynFl z8O5q!cwR@eq3hlHM2-=<(kv6T?C$@PyP|QTjwkdBM|MIRBO4)fB~I_BIp2eugbBvq z_6ygcFCAg;*WG(K3y8G`wCv4axS%g0^ni{(=^x&PzVuM~fIj&Y*Oz#itYshn2dft( zPS)|H0pT9>rDvzf`joz|FA))wXLK6`y@+nC<9`ecXP_^g(BsTC{#ZcEQ|#B2631CJ zMaSb#4I9vx&L~rK{{c=fy)u}tM&nbFp@GKBMb6<-+&TC!-i<}WO%FL(k<8_{>R zM#@=piKyqvgbUaN@C0;BEY1ptR(s6U^JK&`l%Lp6UeviII==d>@W~5l&M(Uzq8I-#{l^nzW+x zLY*q63|*82Us06-vcU&J-$mL5Uva5etWicTW`mKSi*@|R!QlnfXhYY%FT$ys z;0?CU{Y%F?2Zs$;HNhOO>%6VCO?Wd{re!txq5$zu8oZEsgwS`HZe>3T1B9o`T&kdz zUQe&ksLKygIRYjauNV^k=_CL~_=3r5qTOJH#wSb`l54AJD3|mm6yMc4zUS=l@ge|* zuGVfJ2gK0T+U?^_Bt5Xcsoe`2Kt>&$USj4=U1{I?p^;nN%iShi@z-cc-*1w=-~uRd zj)+>Lqb|0Bq7`G;Xt$L#p$|IMTC(^u9?K|xXJ|O$IJ9BtTJ64;0mR6)y0Xe&j<2Lu z)5vVpxvjc%&aiNaIJPPJZq#`@Yg2om(YUDV3AKPYqv*KvxnY+g0J?70W%i{RAk=Qw zG7VUN#kn%LBh zs2_dJ*!pYFzoE3@+%WQvEyu{ub)Me>sd6@V7e!$(%mu^?LLKKX2v6?=fUdiA!M;5N z#3Z{k&PBv{IVa0EG;%8?3ox!f<6AEXH}=Ng1hm`40WtEMB=Y4b&A9KRk@Y+fsj@r1 z^TKd+5ltAlTNi9r0b<~8-I8)ys%E$Y)wu)Oo+F!~=N_Hs;-s4K>$ts1@Bp#bYk4x8 z3WVV8)q#x`AcpSM9qh|GKuoe%_c_ULMUFLN$M%zX7s5P1yiJV%I6OSBh`$LWegR_S ze)5aVYmO&!ox8(W^O~dQL7nG%t(tR~50OqvR6x8+)b5xIguaK8T~z>t_9IDE0EG4<8ecZjk(i*>enh)Z8=Lb{@OA_3Q|Ukl z9I4w)sr!hqTATv}?YEm$JFWiPjjK6mh4$M`s@;9~4H2}j)u1Gb%mXGj&6FOvI9w@@ zjiBuTrM4JoB<}3g)Cx#>Sog0WmZ-IQ>&rMjWU| zXy`~DiN;4veAB4#LQG0wdL#)GAUC=&MSNzSYEV~Rv6G_AOf~VrQQ?9j8nG6q8uw`n zAT&)iku8~k&@`3Irvz^indv6?oJ;T)=sDfwc~?{|c!|w0lD_{}1aSvpu$7VRKOz-&^*&j-K+OWUsqYef+Vs%9HkL;;~{wsA!i5UOSy{y~XU6_J@^atoZZ73ev~ z_cdl`p9U!#MHQ4OGVKTukX_9c9FZ52L;{5N z7n1b`2<y4n*{sOHxVzPphbUrD6f>I3_FNdMMX0oUo8Q=2jFg^z|crZVkgOYWA zHV3Wg`Pm%Fnx0Q{&{0xVMrM)8Z8q_!tHLLbt%R~gCf|zZuVi8t)BNXSdO#eSOx*73 zaM)!4=(^arJslA9E~X?T%Kru2clAZ;MT}jZG!f@*3&>gnR>z)N3YQdUg2W zNdS!8+69F2*OIkc$;*B@4W*x6VJL#W+{EvW4j<|RfT7Ec+wuT0bh*KnXYHa-&sNaT z(|9BrSC}{+6ZR`sawJ!nf<5>Fa-)05uH==yl15UJ0^&}|$fPf=^OXkQ+}l}bov$?b z=-xs)8m-EPT4awNm3%f@WeQyr3+ZsQDoMdgN#B64p|SiM3t)ort=EJP!(w^}Ta#H# zPs3|WYB7DX{Z=+Dp1OeSHAW>r|Gs712kw9v`j&Cm7?m=Qq3;-+x|UE00cKcP^SrDZ zdE-s>bvKW#zdpX}PQP{Oxog9w0*ujf+dHPr9=-sf@0}zKDtdB(((?YPV5sA=&^j@wTM25VSeGEh#NkoWt7Z8z>x3=mUoryUSCw=@!$V%tsC$HrcYk$^A7 zwwsn88+$3{WC-Mk=*8>GiJ4e>;(vqY0Sz;j|uEC6Z$^c^M3Nj!J{LI|5c3g;mP4KU2Y3;c3Ue}42;vd(QAEMu< gM*N#qF}_s$Zuz*rqWe7)3(fGad1+!Hx^LM30XnN;Jpcdz diff --git a/module/contracts/api/accounts/authority/proto/buf.yaml b/module/contracts/api/accounts/authority/proto/buf.yaml index 509da1b1f..1a1cfc5ca 100644 --- a/module/contracts/api/accounts/authority/proto/buf.yaml +++ b/module/contracts/api/accounts/authority/proto/buf.yaml @@ -94,12 +94,23 @@ breaking: # SolutionBackendBinding (#1118): the reported upstream half, deleted # with the report — a solution's address is resolved, never recorded. - saas/accounts/v1/solution_registry.proto + # Installation, ListInstallationsRequest, InstallationSummary and + # ListInstallationsResponse moved to installation_listing.proto so the + # auth-gateway can generate the ListInstallations messages from the same + # source file without vendoring the policy options and google.api this + # file imports. Same package, same fully-qualified names, same wire + # format; every RPC stays where it was. + - saas/accounts/v1/installations.proto # ModuleMintRegistrationRequest/Response and # SolutionMintRegistrationRequest/Response (#1056): the registration # credentials and the secrets that minted them are deleted. Presence is # delivered, so nothing registers at runtime and nothing verifies either # credential. - saas/accounts/v1/module_registration.proto + # InstallationStatus and InstallationHealth moved to + # installation_listing.proto with the messages above, for the same reason. + ENUM_NO_DELETE: + - saas/accounts/v1/installations.proto # ListMyAccessibleScopes moved from PermissionService to the new # AccessibleScopeService in accessible_scopes.proto (issue #577), which changes # its Connect/gRPC procedure path. Its REST route (GET /v1/accessible-scopes) is diff --git a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installation_listing.proto b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installation_listing.proto new file mode 100644 index 000000000..57150f92b --- /dev/null +++ b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installation_listing.proto @@ -0,0 +1,131 @@ +syntax = "proto3"; + +package saas.accounts.v1; + +import "buf/validate/validate.proto"; +import "google/protobuf/timestamp.proto"; + +// The installation listing: an installation, its lifecycle and health, and the +// messages InstallationService/ListInstallations takes and answers. +// +// These definitions live in their own file, with deliberately minimal imports, +// so the auth-gateway can generate the same types from the same source proto +// and call ListInstallations over the internal listener, as it does for +// module_registration.proto and solution_entitlements.proto. installations.proto +// imports this file; the service and every other installation message stay +// there. Sharing the generated definition rather than restating the wire shape +// is what keeps the two sides from drifting: the gateway's copy is this file, +// byte for byte. + +// InstallationStatus is the lifecycle of an installation row. An uninstall is a +// soft delete (status=revoked, revoked_at set) so the agent, grants, and audit +// trail remain resolvable. +enum InstallationStatus { + INSTALLATION_STATUS_UNSPECIFIED = 0; + INSTALLATION_STATUS_ACTIVE = 1; + INSTALLATION_STATUS_REVOKED = 2; + // Installed, and waiting for the solution to acknowledge its standard install + // step. Nothing can use a pending installation. + INSTALLATION_STATUS_PENDING = 3; + // The solution refused its install step, or never answered within the + // attempt budget. Nothing can use it; an administrator may retry it. + INSTALLATION_STATUS_FAILED = 4; +} + +// InstallationHealth is resolved live from current authority facts, never +// stored. It is the observable form of the fail-closed mint rule: a +// StartInstallationTask fails exactly when health is not HEALTHY, and transfer +// or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record +// nor any co-owner is currently an org admin (the Zapier-class "connection owner +// left the org" failure), surfaced rather than silently reassigned. +enum InstallationHealth { + INSTALLATION_HEALTH_UNSPECIFIED = 0; + INSTALLATION_HEALTH_HEALTHY = 1; + INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER = 2; + INSTALLATION_HEALTH_AGENT_REVOKED = 3; + INSTALLATION_HEALTH_AGENT_DISABLED = 4; + INSTALLATION_HEALTH_STANDING_GRANT_MISSING = 5; +} + +// Installation is one solution's bound identity in an org. Server-assigned ids; +// the browser cannot attest to tenancy, the agent principal, or the scope node. +message Installation { + string id = 1 [(buf.validate.field).string.uuid = true]; + string org_id = 2 [(buf.validate.field).string.uuid = true]; + string agent_principal_id = 3 [(buf.validate.field).string.uuid = true]; + // Was `solution_identifier`: the free-text route alias. It was the join key + // every reader used, and because an alias is deliberately reusable by a later + // binding, an installation that named one inherited whatever took it next. + // Replaced by target_id below. Reserved rather than reused so no consumer can + // read an alias out of a field that now carries an identity. + reserved 4; + reserved "solution_identifier"; + string owner_principal_id = 5 [(buf.validate.field).string.uuid = true]; + repeated string co_owner_principal_ids = 6; + string root_scope_node_id = 7 [(buf.validate.field).string.uuid = true]; + InstallationStatus status = 8; + google.protobuf.Timestamp created_at = 9; + optional google.protobuf.Timestamp revoked_at = 10; + // The immutable solution target this installation was consented to: one + // continuous period of one binding's presence on this host. It is never + // reused, so a replacement binding — even one claiming the same route alias — + // is a different target and inherits nothing. + // + // Deliberately NOT accompanied by the route alias. The alias is a property of + // the target that a later generation may move, and resolving it here would + // mean joining the host's presence state into a request-path listing — which + // would require granting request traffic read over every organisation's + // presence to answer about one. A consumer that needs the alias asks + // ListAvailableSolutions, which serves accepted applied state, and joins on + // this id. + // + // Empty only on a revoked historical row whose target could not be + // established at the cutover; an active installation always names one. + string target_id = 11 [ + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + (buf.validate.field).string.uuid = true + ]; + // Why a revoked installation was revoked, when it was not an administrator + // uninstalling: the installation-identity cutover, or a tenancy narrowing that + // withdrew this organisation. Empty on an active row. + string revoked_reason = 12 [(buf.validate.field).string.max_len = 512]; + // Why the install step failed: the solution's bounded refusal reason, or a + // fixed reason when it never answered. Empty unless status is FAILED. + string failure_reason = 13 [(buf.validate.field).string.max_len = 512]; +} + +// ListInstallationsRequest asks what one organization has installed. Every other +// RPC on this service names a single installation, so until this existed nothing +// could answer "what has this org installed?" — which is the authority read a +// per-organization solution projection is built on (issue #949). +message ListInstallationsRequest { + string org_id = 1 [(buf.validate.field).string.uuid = true]; + // Restrict to one lifecycle status. UNSPECIFIED returns every status, so a + // caller that must see only live installs asks for ACTIVE explicitly rather + // than leaning on a default that would silently widen if the enum grew. + InstallationStatus status = 2; + // Page bound. 0 -> server default; capped so an organization with many + // installations can never return an unbounded result in one call. + int32 page_size = 3 [(buf.validate.field).int32 = { + gte: 0 + lte: 500 + }]; + // Opaque cursor from a previous response's next_page_token; empty for page one. + string page_token = 4; +} + +// InstallationSummary is one row of the listing: the installation, plus the health +// resolved live beside it exactly as GetInstallation resolves it for one. Health +// travels with the row because a consumer that renders an installed solution as +// unavailable would otherwise need one GetInstallation per installation. +message InstallationSummary { + Installation installation = 1; + InstallationHealth health = 2; +} + +message ListInstallationsResponse { + repeated InstallationSummary installations = 1; + // Set when more installations remain; pass it back as page_token. Empty on the + // last page. + string next_page_token = 2; +} diff --git a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installations.proto b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installations.proto index 82739f9de..1149a82cf 100644 --- a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installations.proto +++ b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installations.proto @@ -5,8 +5,8 @@ package saas.accounts.v1; import "buf/validate/validate.proto"; import "google/api/annotations.proto"; import "google/protobuf/empty.proto"; -import "google/protobuf/timestamp.proto"; import "saas/accounts/v1/authorization.proto"; +import "saas/accounts/v1/installation_listing.proto"; import "saas/policy/v1/options.proto"; // ============================================================================ @@ -22,83 +22,6 @@ import "saas/policy/v1/options.proto"; // (StartInstallationTask) beside the other mints. // ============================================================================ -// InstallationStatus is the lifecycle of an installation row. An uninstall is a -// soft delete (status=revoked, revoked_at set) so the agent, grants, and audit -// trail remain resolvable. -enum InstallationStatus { - INSTALLATION_STATUS_UNSPECIFIED = 0; - INSTALLATION_STATUS_ACTIVE = 1; - INSTALLATION_STATUS_REVOKED = 2; - // Installed, and waiting for the solution to acknowledge its standard install - // step. Nothing can use a pending installation. - INSTALLATION_STATUS_PENDING = 3; - // The solution refused its install step, or never answered within the - // attempt budget. Nothing can use it; an administrator may retry it. - INSTALLATION_STATUS_FAILED = 4; -} - -// InstallationHealth is resolved live from current authority facts, never -// stored. It is the observable form of the fail-closed mint rule: a -// StartInstallationTask fails exactly when health is not HEALTHY, and transfer -// or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record -// nor any co-owner is currently an org admin (the Zapier-class "connection owner -// left the org" failure), surfaced rather than silently reassigned. -enum InstallationHealth { - INSTALLATION_HEALTH_UNSPECIFIED = 0; - INSTALLATION_HEALTH_HEALTHY = 1; - INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER = 2; - INSTALLATION_HEALTH_AGENT_REVOKED = 3; - INSTALLATION_HEALTH_AGENT_DISABLED = 4; - INSTALLATION_HEALTH_STANDING_GRANT_MISSING = 5; -} - -// Installation is one solution's bound identity in an org. Server-assigned ids; -// the browser cannot attest to tenancy, the agent principal, or the scope node. -message Installation { - string id = 1 [(buf.validate.field).string.uuid = true]; - string org_id = 2 [(buf.validate.field).string.uuid = true]; - string agent_principal_id = 3 [(buf.validate.field).string.uuid = true]; - // Was `solution_identifier`: the free-text route alias. It was the join key - // every reader used, and because an alias is deliberately reusable by a later - // binding, an installation that named one inherited whatever took it next. - // Replaced by target_id below. Reserved rather than reused so no consumer can - // read an alias out of a field that now carries an identity. - reserved 4; - reserved "solution_identifier"; - string owner_principal_id = 5 [(buf.validate.field).string.uuid = true]; - repeated string co_owner_principal_ids = 6; - string root_scope_node_id = 7 [(buf.validate.field).string.uuid = true]; - InstallationStatus status = 8; - google.protobuf.Timestamp created_at = 9; - optional google.protobuf.Timestamp revoked_at = 10; - // The immutable solution target this installation was consented to: one - // continuous period of one binding's presence on this host. It is never - // reused, so a replacement binding — even one claiming the same route alias — - // is a different target and inherits nothing. - // - // Deliberately NOT accompanied by the route alias. The alias is a property of - // the target that a later generation may move, and resolving it here would - // mean joining the host's presence state into a request-path listing — which - // would require granting request traffic read over every organisation's - // presence to answer about one. A consumer that needs the alias asks - // ListAvailableSolutions, which serves accepted applied state, and joins on - // this id. - // - // Empty only on a revoked historical row whose target could not be - // established at the cutover; an active installation always names one. - string target_id = 11 [ - (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, - (buf.validate.field).string.uuid = true - ]; - // Why a revoked installation was revoked, when it was not an administrator - // uninstalling: the installation-identity cutover, or a tenancy narrowing that - // withdrew this organisation. Empty on an active row. - string revoked_reason = 12 [(buf.validate.field).string.max_len = 512]; - // Why the install step failed: the solution's bounded refusal reason, or a - // fixed reason when it never answered. Empty unless status is FAILED. - string failure_reason = 13 [(buf.validate.field).string.max_len = 512]; -} - message InstallSolutionRequest { string org_id = 1 [(buf.validate.field).string.uuid = true]; // Canonical "publisher/name:version" of the solution's agent. The agent @@ -219,42 +142,6 @@ message GetInstallationResponse { InstallationHealth health = 2; } -// ListInstallationsRequest asks what one organization has installed. Every other -// RPC on this service names a single installation, so until this existed nothing -// could answer "what has this org installed?" — which is the authority read a -// per-organization solution projection is built on (issue #949). -message ListInstallationsRequest { - string org_id = 1 [(buf.validate.field).string.uuid = true]; - // Restrict to one lifecycle status. UNSPECIFIED returns every status, so a - // caller that must see only live installs asks for ACTIVE explicitly rather - // than leaning on a default that would silently widen if the enum grew. - InstallationStatus status = 2; - // Page bound. 0 -> server default; capped so an organization with many - // installations can never return an unbounded result in one call. - int32 page_size = 3 [(buf.validate.field).int32 = { - gte: 0 - lte: 500 - }]; - // Opaque cursor from a previous response's next_page_token; empty for page one. - string page_token = 4; -} - -// InstallationSummary is one row of the listing: the installation, plus the health -// resolved live beside it exactly as GetInstallation resolves it for one. Health -// travels with the row because a consumer that renders an installed solution as -// unavailable would otherwise need one GetInstallation per installation. -message InstallationSummary { - Installation installation = 1; - InstallationHealth health = 2; -} - -message ListInstallationsResponse { - repeated InstallationSummary installations = 1; - // Set when more installations remain; pass it back as page_token. Empty on the - // last page. - string next_page_token = 2; -} - // InstallationService composes and administers installations. The headless mint // that turns an installation into a short-lived Work Context is // WorkContextService.StartInstallationTask. @@ -431,11 +318,15 @@ service InstallationService { // ListInstallations enumerates one organization's installations with their live // health. It is internal-tier on purpose: it takes the organization as a request // field, so only a caller holding the cluster-internal credential may ask, and - // the tenant it asks about is the one the auth-gateway projected from a verified - // identity — never one a browser supplied. It sits beside - // PermissionService.ListAccessibleScopes for that reason: the two are read - // together to answer what a viewer's organization installed and what that viewer - // was granted. + // the tenant it asks about is never one a browser supplied. The auth-gateway is + // its caller, for two reads. Its viewer projection asks about the tenant it + // projected from a verified identity; it sits beside + // PermissionService.ListAccessibleScopes for that reason, the two read together + // to answer what a viewer's organization installed and what that viewer was + // granted. Its declared module route's bearer-less arm asks about the tenant an + // edge-verified Work Context names, a value this issuer signed, to admit a + // headless operation context only when the target is installed there and the + // context is sealed to another active installation. rpc ListInstallations(ListInstallationsRequest) returns (ListInstallationsResponse) { option (saas.policy.v1.method_policy) = { exposure: EXPOSURE_INTERNAL @@ -496,7 +387,8 @@ service InstallationService { // It is the org-admin companion to the internal ListInstallations, and it is a // separate method rather than a relaxed policy on that one: ListInstallations // is read by the gateway for an organisation it projected from a verified - // identity, and a bare tenant credential must never reach it. This one binds + // identity or that an edge-verified Work Context names, and a bare tenant + // credential must never reach it. This one binds // the organisation to the caller's own admin membership, exactly as // ListAvailableSolutions does, so no organisation's administrator can read // another's. The one exception is inherited, not added here: a platform diff --git a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto index 64acd9ec3..dd6ffc5f5 100644 --- a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto +++ b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto @@ -8,7 +8,7 @@ import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; import "saas/accounts/v1/authorization.proto"; import "saas/accounts/v1/common.proto"; -import "saas/accounts/v1/installations.proto"; +import "saas/accounts/v1/installation_listing.proto"; import "saas/accounts/v1/solution_registry.proto"; import "saas/events/v1/operations.proto"; import "saas/jobs/v1/jobs.proto"; diff --git a/module/contracts/api/accounts/connect/contract.binpb b/module/contracts/api/accounts/connect/contract.binpb index 590c0a8eb1d2d00c9520361083a0d0e6c244e78f..18d4889d20273ab7985cefe5699eb283f050e078 100644 GIT binary patch delta 15827 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 delta 15097 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 diff --git a/module/contracts/api/accounts/connect/proto/buf.yaml b/module/contracts/api/accounts/connect/proto/buf.yaml index 509da1b1f..1a1cfc5ca 100644 --- a/module/contracts/api/accounts/connect/proto/buf.yaml +++ b/module/contracts/api/accounts/connect/proto/buf.yaml @@ -94,12 +94,23 @@ breaking: # SolutionBackendBinding (#1118): the reported upstream half, deleted # with the report — a solution's address is resolved, never recorded. - saas/accounts/v1/solution_registry.proto + # Installation, ListInstallationsRequest, InstallationSummary and + # ListInstallationsResponse moved to installation_listing.proto so the + # auth-gateway can generate the ListInstallations messages from the same + # source file without vendoring the policy options and google.api this + # file imports. Same package, same fully-qualified names, same wire + # format; every RPC stays where it was. + - saas/accounts/v1/installations.proto # ModuleMintRegistrationRequest/Response and # SolutionMintRegistrationRequest/Response (#1056): the registration # credentials and the secrets that minted them are deleted. Presence is # delivered, so nothing registers at runtime and nothing verifies either # credential. - saas/accounts/v1/module_registration.proto + # InstallationStatus and InstallationHealth moved to + # installation_listing.proto with the messages above, for the same reason. + ENUM_NO_DELETE: + - saas/accounts/v1/installations.proto # ListMyAccessibleScopes moved from PermissionService to the new # AccessibleScopeService in accessible_scopes.proto (issue #577), which changes # its Connect/gRPC procedure path. Its REST route (GET /v1/accessible-scopes) is diff --git a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installation_listing.proto b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installation_listing.proto new file mode 100644 index 000000000..57150f92b --- /dev/null +++ b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installation_listing.proto @@ -0,0 +1,131 @@ +syntax = "proto3"; + +package saas.accounts.v1; + +import "buf/validate/validate.proto"; +import "google/protobuf/timestamp.proto"; + +// The installation listing: an installation, its lifecycle and health, and the +// messages InstallationService/ListInstallations takes and answers. +// +// These definitions live in their own file, with deliberately minimal imports, +// so the auth-gateway can generate the same types from the same source proto +// and call ListInstallations over the internal listener, as it does for +// module_registration.proto and solution_entitlements.proto. installations.proto +// imports this file; the service and every other installation message stay +// there. Sharing the generated definition rather than restating the wire shape +// is what keeps the two sides from drifting: the gateway's copy is this file, +// byte for byte. + +// InstallationStatus is the lifecycle of an installation row. An uninstall is a +// soft delete (status=revoked, revoked_at set) so the agent, grants, and audit +// trail remain resolvable. +enum InstallationStatus { + INSTALLATION_STATUS_UNSPECIFIED = 0; + INSTALLATION_STATUS_ACTIVE = 1; + INSTALLATION_STATUS_REVOKED = 2; + // Installed, and waiting for the solution to acknowledge its standard install + // step. Nothing can use a pending installation. + INSTALLATION_STATUS_PENDING = 3; + // The solution refused its install step, or never answered within the + // attempt budget. Nothing can use it; an administrator may retry it. + INSTALLATION_STATUS_FAILED = 4; +} + +// InstallationHealth is resolved live from current authority facts, never +// stored. It is the observable form of the fail-closed mint rule: a +// StartInstallationTask fails exactly when health is not HEALTHY, and transfer +// or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record +// nor any co-owner is currently an org admin (the Zapier-class "connection owner +// left the org" failure), surfaced rather than silently reassigned. +enum InstallationHealth { + INSTALLATION_HEALTH_UNSPECIFIED = 0; + INSTALLATION_HEALTH_HEALTHY = 1; + INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER = 2; + INSTALLATION_HEALTH_AGENT_REVOKED = 3; + INSTALLATION_HEALTH_AGENT_DISABLED = 4; + INSTALLATION_HEALTH_STANDING_GRANT_MISSING = 5; +} + +// Installation is one solution's bound identity in an org. Server-assigned ids; +// the browser cannot attest to tenancy, the agent principal, or the scope node. +message Installation { + string id = 1 [(buf.validate.field).string.uuid = true]; + string org_id = 2 [(buf.validate.field).string.uuid = true]; + string agent_principal_id = 3 [(buf.validate.field).string.uuid = true]; + // Was `solution_identifier`: the free-text route alias. It was the join key + // every reader used, and because an alias is deliberately reusable by a later + // binding, an installation that named one inherited whatever took it next. + // Replaced by target_id below. Reserved rather than reused so no consumer can + // read an alias out of a field that now carries an identity. + reserved 4; + reserved "solution_identifier"; + string owner_principal_id = 5 [(buf.validate.field).string.uuid = true]; + repeated string co_owner_principal_ids = 6; + string root_scope_node_id = 7 [(buf.validate.field).string.uuid = true]; + InstallationStatus status = 8; + google.protobuf.Timestamp created_at = 9; + optional google.protobuf.Timestamp revoked_at = 10; + // The immutable solution target this installation was consented to: one + // continuous period of one binding's presence on this host. It is never + // reused, so a replacement binding — even one claiming the same route alias — + // is a different target and inherits nothing. + // + // Deliberately NOT accompanied by the route alias. The alias is a property of + // the target that a later generation may move, and resolving it here would + // mean joining the host's presence state into a request-path listing — which + // would require granting request traffic read over every organisation's + // presence to answer about one. A consumer that needs the alias asks + // ListAvailableSolutions, which serves accepted applied state, and joins on + // this id. + // + // Empty only on a revoked historical row whose target could not be + // established at the cutover; an active installation always names one. + string target_id = 11 [ + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + (buf.validate.field).string.uuid = true + ]; + // Why a revoked installation was revoked, when it was not an administrator + // uninstalling: the installation-identity cutover, or a tenancy narrowing that + // withdrew this organisation. Empty on an active row. + string revoked_reason = 12 [(buf.validate.field).string.max_len = 512]; + // Why the install step failed: the solution's bounded refusal reason, or a + // fixed reason when it never answered. Empty unless status is FAILED. + string failure_reason = 13 [(buf.validate.field).string.max_len = 512]; +} + +// ListInstallationsRequest asks what one organization has installed. Every other +// RPC on this service names a single installation, so until this existed nothing +// could answer "what has this org installed?" — which is the authority read a +// per-organization solution projection is built on (issue #949). +message ListInstallationsRequest { + string org_id = 1 [(buf.validate.field).string.uuid = true]; + // Restrict to one lifecycle status. UNSPECIFIED returns every status, so a + // caller that must see only live installs asks for ACTIVE explicitly rather + // than leaning on a default that would silently widen if the enum grew. + InstallationStatus status = 2; + // Page bound. 0 -> server default; capped so an organization with many + // installations can never return an unbounded result in one call. + int32 page_size = 3 [(buf.validate.field).int32 = { + gte: 0 + lte: 500 + }]; + // Opaque cursor from a previous response's next_page_token; empty for page one. + string page_token = 4; +} + +// InstallationSummary is one row of the listing: the installation, plus the health +// resolved live beside it exactly as GetInstallation resolves it for one. Health +// travels with the row because a consumer that renders an installed solution as +// unavailable would otherwise need one GetInstallation per installation. +message InstallationSummary { + Installation installation = 1; + InstallationHealth health = 2; +} + +message ListInstallationsResponse { + repeated InstallationSummary installations = 1; + // Set when more installations remain; pass it back as page_token. Empty on the + // last page. + string next_page_token = 2; +} diff --git a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installations.proto b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installations.proto index 82739f9de..1149a82cf 100644 --- a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installations.proto +++ b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installations.proto @@ -5,8 +5,8 @@ package saas.accounts.v1; import "buf/validate/validate.proto"; import "google/api/annotations.proto"; import "google/protobuf/empty.proto"; -import "google/protobuf/timestamp.proto"; import "saas/accounts/v1/authorization.proto"; +import "saas/accounts/v1/installation_listing.proto"; import "saas/policy/v1/options.proto"; // ============================================================================ @@ -22,83 +22,6 @@ import "saas/policy/v1/options.proto"; // (StartInstallationTask) beside the other mints. // ============================================================================ -// InstallationStatus is the lifecycle of an installation row. An uninstall is a -// soft delete (status=revoked, revoked_at set) so the agent, grants, and audit -// trail remain resolvable. -enum InstallationStatus { - INSTALLATION_STATUS_UNSPECIFIED = 0; - INSTALLATION_STATUS_ACTIVE = 1; - INSTALLATION_STATUS_REVOKED = 2; - // Installed, and waiting for the solution to acknowledge its standard install - // step. Nothing can use a pending installation. - INSTALLATION_STATUS_PENDING = 3; - // The solution refused its install step, or never answered within the - // attempt budget. Nothing can use it; an administrator may retry it. - INSTALLATION_STATUS_FAILED = 4; -} - -// InstallationHealth is resolved live from current authority facts, never -// stored. It is the observable form of the fail-closed mint rule: a -// StartInstallationTask fails exactly when health is not HEALTHY, and transfer -// or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record -// nor any co-owner is currently an org admin (the Zapier-class "connection owner -// left the org" failure), surfaced rather than silently reassigned. -enum InstallationHealth { - INSTALLATION_HEALTH_UNSPECIFIED = 0; - INSTALLATION_HEALTH_HEALTHY = 1; - INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER = 2; - INSTALLATION_HEALTH_AGENT_REVOKED = 3; - INSTALLATION_HEALTH_AGENT_DISABLED = 4; - INSTALLATION_HEALTH_STANDING_GRANT_MISSING = 5; -} - -// Installation is one solution's bound identity in an org. Server-assigned ids; -// the browser cannot attest to tenancy, the agent principal, or the scope node. -message Installation { - string id = 1 [(buf.validate.field).string.uuid = true]; - string org_id = 2 [(buf.validate.field).string.uuid = true]; - string agent_principal_id = 3 [(buf.validate.field).string.uuid = true]; - // Was `solution_identifier`: the free-text route alias. It was the join key - // every reader used, and because an alias is deliberately reusable by a later - // binding, an installation that named one inherited whatever took it next. - // Replaced by target_id below. Reserved rather than reused so no consumer can - // read an alias out of a field that now carries an identity. - reserved 4; - reserved "solution_identifier"; - string owner_principal_id = 5 [(buf.validate.field).string.uuid = true]; - repeated string co_owner_principal_ids = 6; - string root_scope_node_id = 7 [(buf.validate.field).string.uuid = true]; - InstallationStatus status = 8; - google.protobuf.Timestamp created_at = 9; - optional google.protobuf.Timestamp revoked_at = 10; - // The immutable solution target this installation was consented to: one - // continuous period of one binding's presence on this host. It is never - // reused, so a replacement binding — even one claiming the same route alias — - // is a different target and inherits nothing. - // - // Deliberately NOT accompanied by the route alias. The alias is a property of - // the target that a later generation may move, and resolving it here would - // mean joining the host's presence state into a request-path listing — which - // would require granting request traffic read over every organisation's - // presence to answer about one. A consumer that needs the alias asks - // ListAvailableSolutions, which serves accepted applied state, and joins on - // this id. - // - // Empty only on a revoked historical row whose target could not be - // established at the cutover; an active installation always names one. - string target_id = 11 [ - (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, - (buf.validate.field).string.uuid = true - ]; - // Why a revoked installation was revoked, when it was not an administrator - // uninstalling: the installation-identity cutover, or a tenancy narrowing that - // withdrew this organisation. Empty on an active row. - string revoked_reason = 12 [(buf.validate.field).string.max_len = 512]; - // Why the install step failed: the solution's bounded refusal reason, or a - // fixed reason when it never answered. Empty unless status is FAILED. - string failure_reason = 13 [(buf.validate.field).string.max_len = 512]; -} - message InstallSolutionRequest { string org_id = 1 [(buf.validate.field).string.uuid = true]; // Canonical "publisher/name:version" of the solution's agent. The agent @@ -219,42 +142,6 @@ message GetInstallationResponse { InstallationHealth health = 2; } -// ListInstallationsRequest asks what one organization has installed. Every other -// RPC on this service names a single installation, so until this existed nothing -// could answer "what has this org installed?" — which is the authority read a -// per-organization solution projection is built on (issue #949). -message ListInstallationsRequest { - string org_id = 1 [(buf.validate.field).string.uuid = true]; - // Restrict to one lifecycle status. UNSPECIFIED returns every status, so a - // caller that must see only live installs asks for ACTIVE explicitly rather - // than leaning on a default that would silently widen if the enum grew. - InstallationStatus status = 2; - // Page bound. 0 -> server default; capped so an organization with many - // installations can never return an unbounded result in one call. - int32 page_size = 3 [(buf.validate.field).int32 = { - gte: 0 - lte: 500 - }]; - // Opaque cursor from a previous response's next_page_token; empty for page one. - string page_token = 4; -} - -// InstallationSummary is one row of the listing: the installation, plus the health -// resolved live beside it exactly as GetInstallation resolves it for one. Health -// travels with the row because a consumer that renders an installed solution as -// unavailable would otherwise need one GetInstallation per installation. -message InstallationSummary { - Installation installation = 1; - InstallationHealth health = 2; -} - -message ListInstallationsResponse { - repeated InstallationSummary installations = 1; - // Set when more installations remain; pass it back as page_token. Empty on the - // last page. - string next_page_token = 2; -} - // InstallationService composes and administers installations. The headless mint // that turns an installation into a short-lived Work Context is // WorkContextService.StartInstallationTask. @@ -431,11 +318,15 @@ service InstallationService { // ListInstallations enumerates one organization's installations with their live // health. It is internal-tier on purpose: it takes the organization as a request // field, so only a caller holding the cluster-internal credential may ask, and - // the tenant it asks about is the one the auth-gateway projected from a verified - // identity — never one a browser supplied. It sits beside - // PermissionService.ListAccessibleScopes for that reason: the two are read - // together to answer what a viewer's organization installed and what that viewer - // was granted. + // the tenant it asks about is never one a browser supplied. The auth-gateway is + // its caller, for two reads. Its viewer projection asks about the tenant it + // projected from a verified identity; it sits beside + // PermissionService.ListAccessibleScopes for that reason, the two read together + // to answer what a viewer's organization installed and what that viewer was + // granted. Its declared module route's bearer-less arm asks about the tenant an + // edge-verified Work Context names, a value this issuer signed, to admit a + // headless operation context only when the target is installed there and the + // context is sealed to another active installation. rpc ListInstallations(ListInstallationsRequest) returns (ListInstallationsResponse) { option (saas.policy.v1.method_policy) = { exposure: EXPOSURE_INTERNAL @@ -496,7 +387,8 @@ service InstallationService { // It is the org-admin companion to the internal ListInstallations, and it is a // separate method rather than a relaxed policy on that one: ListInstallations // is read by the gateway for an organisation it projected from a verified - // identity, and a bare tenant credential must never reach it. This one binds + // identity or that an edge-verified Work Context names, and a bare tenant + // credential must never reach it. This one binds // the organisation to the caller's own admin membership, exactly as // ListAvailableSolutions does, so no organisation's administrator can read // another's. The one exception is inherited, not added here: a platform diff --git a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto index 64acd9ec3..dd6ffc5f5 100644 --- a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto +++ b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto @@ -8,7 +8,7 @@ import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; import "saas/accounts/v1/authorization.proto"; import "saas/accounts/v1/common.proto"; -import "saas/accounts/v1/installations.proto"; +import "saas/accounts/v1/installation_listing.proto"; import "saas/accounts/v1/solution_registry.proto"; import "saas/events/v1/operations.proto"; import "saas/jobs/v1/jobs.proto"; diff --git a/module/contracts/api/auth-gateway/grpc/contract.binpb b/module/contracts/api/auth-gateway/grpc/contract.binpb index 41a9e6c93916b2eeff1a9cf39143c2199ddd35bb..76aca22241b72ebe6e39ac9c395d0936bcce2421 100644 GIT binary patch delta 4069 zcmZWs+ix6K8Q(c`*4{naCe7kY>Rf!{I*BjqO-XHw+#FvNC5EU)aax3hYmIl0y+g(` z&djWxHA<$fBB2N&AyT?Rh~7}2KuRAf1w@~CsDyxrJ|Gm8zEFvW{sZs^zwgYMtqo81 zZ@=sL&hLERH$Q&T`^6W%uidxqv-|c({BIA~@8=&sdBFeFRs83q=q~4Txi{KB`3;{) z{?jE`K3C>_IcW_%w&bE8#g6AWu^aeHu@g5V=|(bcDCxQDs@AG`N(SrF@uj;*Q-;A# zRnGac>6@R)$`LXO)?-;$o{E(mfoJ%;6sqmuma11I{z>S+RVS8_ibtWXBRLz&k1KK` zbo@A~AXZ&E&AJ;48HbMR!EV!Wef)_6Z`)b(R8^e)ZC>6)lKYbK+$eVajr0GTZAH3q zl%8iuABZ9}l;g#XG#ZX8RKf67mtlCg73;YgH=AML0xstQ3NBZc#=yS_^m zdw8L1us!;SAG1Vz{IC2(yYYyRoe8aIFZNMzlcT(L7gE-I*+n@f0A6c*NJ9CuEg>*N!f)J;S8@Pyd*HagCX(JVG zyEUcl6(YA}fWwCSd}Jq=xUUv!4!L@GsC%wkMh$06iT2$O`B-vZ``d70wd1r-N3s@d zwNk2>fE6LvTDW$cME`?&31R1RR@v^$_lu`QKA&TR`q>Fl5}Xi$Q<;bX4WBf~g5qIz z)F@izmw`@XGH4zilm;%*<>k9);oqO9!`s+IQdNKRR}_qr#yw zh^XyUg#)!u{*3=T8AP~0+ITuvnUCboSOXv1`<~7<%_gcx%2HJa-?mYVFxZr}W*CCu zAa&dbLN{*7b*Dy5_vuRTXyPDL^{QNqv$q$l(ZvzfshpcK!4t&2z|Qq1YF-e*|E3F< zA@~Bm3yHTI?qk|bC%UEeBdP8noY#^&4dol&Qy@Qx<+Uqw*Kc0?u6YJ{fYCZ-gewfy zgz|L;I8j0w5?QPA#g|sETwlDpIDh@h>Pz2w@yZR*2WB{ntC>Y<20K1DhMYrH3&OfU zs1Ouvfix4T4B<^xkQbUu5N=4Pjw8ttaNQ2LMOVQdx>1b ztN@#3&GCtGP8_QUcZb#we)+XlMc<>I0dgyJ{hGVwcom``-EV|Gqh#U-b##4dD4bfz z3xys!2}cXTxh7eG4oQf>`Ah`G$zmx(sOjW^gBdCSJ#$VxBZ_2J_+jxtAAULX4I{VU zcBbn!@r z;CA}>ph3e7ey8Q4@ym?g=@Zw)zZun}Shg=d{#TptTFJ%IQ>j&vDzyh88X;XnQMNCo zOy@a)s$DASc8Xfq$Z)2UA+nh1WJnyH>11e=tdpUQ1ZOD0L|4={8O|WX^rASJdYK7x zQ5sBSB80qLI+P(qxm?yHMmii(E|2Rs1C)p6Y$rJlvhoqzXOUczhReflwv$~R+0Ay! zmp94Hc9QGrD%*3c$0VmgR*x?Do$Jl6r3-H7I&Qn6InRz5vu*<8i8)3$?B1wU^)J?<6Z7hb0)SbR3r8aHZq0WE`%*;l~Vp45PW83$1}i?ElxU8|rqG z+75lv*36j^n$#deM>%+Qw+dO+1gL@s+REB_VHodjS^cNN9@LPUSmT8Kr~q z;%{J3?_EVPhyv`!F_u?tqV8a!!&E{8Y(QIJ5H`JRvy?+RA&Re+6B>rbo2p+PlK*(^ zP2}v>8rgdA)gpn31~)=(q4(}P4RvijFtI;=-^@*>{82Ljiy1G5fG&uBiwknLZ`5P0wk+`-Jx-^u46lHocUC@l}~Fs`|@nTX}d3 z*xIyjN@Y#eoF+bS9PrbRnw#kYg)LM0b$kKQu~j8Y?hVV*Q)im3oYkVWB>pF delta 2769 zcmY*aOK;p%6t=JLOvZQGrgifuNt52Rd61b&3Tc`L1tcC4O&e$+rC9_LT#qwj>W&B7 zX_VD~R0$P?R21k4BtYV^0f`k-)@)$G4?tDLszRW`f&~kNK;paC*KWuvpU*kp`ObIm zJ#+VN_dB<{U%#$jC)bT{=#MwZ&$GL?Z_pp|=FJkzTU67U?XPdrO6MDSMsk`(yR<=l z>d)Mc{npbve-Lw>@r!krdyOczecz6~puv4FioHgy!tIF12-=%*eWGT^?v}mHL)WfG z+>0j}uDf{pQoJ-wFQ@S5ki z6~0yXoI3ZeHiHm+qH8noJ!hK-O`$2`cB9H`K~VGENxSJG!;2VCMO6bdYXmXB<~FK9 zI4NgJgO1(r`w@f6_Iqpvp*X>v& zGalPl5EN4EMzrOIQH47}({rnEV8d&8(!Px#{2vU1tHOnzmZ*csI=tnsAGwg}M)4&n z*{$xiWidH{XYJUjyU`P=&Go>C_wF?}+-7@Q)ZNhKm$zeg0wy$bemon+}PX)*j6IZd~Wu3RtM$8tH12-Ho6SdmeI z1Q<<21Za3fAsvZ5eCvF8|9h#=NW22>(Kz2vwIozMsokf(J9GW(EuLzm4B zp!PrS(YFWsvJ{Gx0d#FADHN#v`TO*P)&Tl3JOPwQ_(0Rpe*@0+q@NiOGh^3F(1OFxW78 zFbyGbcyvr5G2aLoR*4vIgp8*lB#w-wWf;^LMK^IyI?1&JdRS%VkU4ttA>jwbav0Yx zN-`hxA3gHC5b~l$*k{ zJqUdT!n>gwvy4;8eZv3n?^IDvX0Sa;7t_;wDUIQVkKyq#BSqEu@yEw}#R> z4Xw$MQCx`AgsCHwU}C4SufHX41sYTEoi58e2$X}2nM{TRsb%8&%w#fhD4WS-22k&fmdJ=i?&StzFNS!17s;)x-`97eu17&kZ<)RS? zWpm>tHFN=U&SyhI+dMg)hQK&q9A>kLJq57LkDN?{V40tqR>;Ko3-IDzUS4<8q6=hk zckyHTs`2uIX{A}HS?KO%`{fHU3&=XMXs}LJfGiVE^jpR`I-@R=_#iI-d793_&}9i- z?qEHk0yku-efU%Q)A=P+E?DsmEtdL@s)h{nEDfAhgiH{M_~0`tUIgJX2tP^)i$Lu& zpV3cR%Nb!215roSbVNnq-gHUV(Y{yGlHQ5GY&0Q)a2Zt4iMl0&B$nChIUM&C; zSJbNoLc0~~O!902(QidPTY(ZsR@Ae_#>5e=;*3~V~2?*!3)?-t+SU?Z)#tnSwwGHX3 z`U2i{%JzN%$tyH@=vc~#J22oSsS_cG9O=8jFEs7fHeK#)hEaRwSNdGbG1Jk-<8^v^ zrLmB?p?Qh1;%8x_kh}pxG#F4L2jr~>5+Ly$0ExBEED>}yWdnX%P*zuuO#aZ+ld={C zLS0?i2ZXvh>cshC>?>L`ks&tCm4f_jr3TUPihT8ofCP{~7b1jKesNG*3xqQNSh7NZ uKkp`~_+TDMlTjM%vajgv@BRajfr738 diff --git a/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/installation_listing.proto b/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/installation_listing.proto index 985e1a29c..57150f92b 100644 --- a/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/installation_listing.proto +++ b/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/installation_listing.proto @@ -5,19 +5,21 @@ package saas.accounts.v1; import "buf/validate/validate.proto"; import "google/protobuf/timestamp.proto"; -// ============================================================================ -// The installation listing, as the auth-gateway reads it. +// The installation listing: an installation, its lifecycle and health, and the +// messages InstallationService/ListInstallations takes and answers. // -// accounts declares these messages in installations.proto beside the -// InstallationService, which imports the policy options and google.api this -// gateway does not vendor. The gateway calls one internal-tier RPC, -// InstallationService/ListInstallations, so it carries the messages that RPC -// takes and answers, copied definition for definition from installations.proto -// and nothing else. TestVendoredInstallationListingMatchesAccounts holds every -// definition here byte-identical to the one accounts declares, so the two -// cannot drift on the wire. -// ============================================================================ +// These definitions live in their own file, with deliberately minimal imports, +// so the auth-gateway can generate the same types from the same source proto +// and call ListInstallations over the internal listener, as it does for +// module_registration.proto and solution_entitlements.proto. installations.proto +// imports this file; the service and every other installation message stay +// there. Sharing the generated definition rather than restating the wire shape +// is what keeps the two sides from drifting: the gateway's copy is this file, +// byte for byte. +// InstallationStatus is the lifecycle of an installation row. An uninstall is a +// soft delete (status=revoked, revoked_at set) so the agent, grants, and audit +// trail remain resolvable. enum InstallationStatus { INSTALLATION_STATUS_UNSPECIFIED = 0; INSTALLATION_STATUS_ACTIVE = 1; @@ -30,6 +32,12 @@ enum InstallationStatus { INSTALLATION_STATUS_FAILED = 4; } +// InstallationHealth is resolved live from current authority facts, never +// stored. It is the observable form of the fail-closed mint rule: a +// StartInstallationTask fails exactly when health is not HEALTHY, and transfer +// or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record +// nor any co-owner is currently an org admin (the Zapier-class "connection owner +// left the org" failure), surfaced rather than silently reassigned. enum InstallationHealth { INSTALLATION_HEALTH_UNSPECIFIED = 0; INSTALLATION_HEALTH_HEALTHY = 1; @@ -39,6 +47,8 @@ enum InstallationHealth { INSTALLATION_HEALTH_STANDING_GRANT_MISSING = 5; } +// Installation is one solution's bound identity in an org. Server-assigned ids; +// the browser cannot attest to tenancy, the agent principal, or the scope node. message Installation { string id = 1 [(buf.validate.field).string.uuid = true]; string org_id = 2 [(buf.validate.field).string.uuid = true]; @@ -84,6 +94,10 @@ message Installation { string failure_reason = 13 [(buf.validate.field).string.max_len = 512]; } +// ListInstallationsRequest asks what one organization has installed. Every other +// RPC on this service names a single installation, so until this existed nothing +// could answer "what has this org installed?" — which is the authority read a +// per-organization solution projection is built on (issue #949). message ListInstallationsRequest { string org_id = 1 [(buf.validate.field).string.uuid = true]; // Restrict to one lifecycle status. UNSPECIFIED returns every status, so a @@ -100,6 +114,10 @@ message ListInstallationsRequest { string page_token = 4; } +// InstallationSummary is one row of the listing: the installation, plus the health +// resolved live beside it exactly as GetInstallation resolves it for one. Health +// travels with the row because a consumer that renders an installed solution as +// unavailable would otherwise need one GetInstallation per installation. message InstallationSummary { Installation installation = 1; InstallationHealth health = 2; diff --git a/module/contracts/api/catalog.codefly.json b/module/contracts/api/catalog.codefly.json index dda7f0a43..fc3322c3c 100644 --- a/module/contracts/api/catalog.codefly.json +++ b/module/contracts/api/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/authority/contract.binpb", - "digest": "sha256:373a8f3a22b7968c36dd9faffb9be5ef64213b1de2a532f5e567007bd65caaa7", + "digest": "sha256:72172e6df496c728bdc8eec06a8f10f23c74a55b97a9eb86f08ea4d3e1bdd35c", "services": [ { "name": "ModuleCapabilitiesService", @@ -65,7 +65,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/connect/contract.binpb", - "digest": "sha256:373a8f3a22b7968c36dd9faffb9be5ef64213b1de2a532f5e567007bd65caaa7", + "digest": "sha256:72172e6df496c728bdc8eec06a8f10f23c74a55b97a9eb86f08ea4d3e1bdd35c", "services": [ { "name": "APIKeyService", @@ -562,7 +562,7 @@ "kind": "protobuf", "package": "saas.gateway.auth.v1", "path": "contracts/api/auth-gateway/grpc/contract.binpb", - "digest": "sha256:b9747dbf86865b80102acc72d003c1f133ef5df6629f5c5d9adc86302951e9ff", + "digest": "sha256:78b298b17f102c488eeff5d2dbc93115e27975995ce78dfad8c308bd2da523d7", "services": [ { "name": "AuthGatewayService", diff --git a/module/module.package.codefly.yaml b/module/module.package.codefly.yaml index 7ffdb415f..1710dbc54 100644 --- a/module/module.package.codefly.yaml +++ b/module/module.package.codefly.yaml @@ -18,12 +18,12 @@ services: kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/authority/contract.binpb - digest: sha256:373a8f3a22b7968c36dd9faffb9be5ef64213b1de2a532f5e567007bd65caaa7 + digest: sha256:72172e6df496c728bdc8eec06a8f10f23c74a55b97a9eb86f08ea4d3e1bdd35c - endpoint: connect kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/connect/contract.binpb - digest: sha256:373a8f3a22b7968c36dd9faffb9be5ef64213b1de2a532f5e567007bd65caaa7 + digest: sha256:72172e6df496c728bdc8eec06a8f10f23c74a55b97a9eb86f08ea4d3e1bdd35c - name: auth-gateway endpoints: [grpc, rest] api-contracts: @@ -31,7 +31,7 @@ services: kind: protobuf package: saas.gateway.auth.v1 path: contracts/api/auth-gateway/grpc/contract.binpb - digest: sha256:b9747dbf86865b80102acc72d003c1f133ef5df6629f5c5d9adc86302951e9ff + digest: sha256:78b298b17f102c488eeff5d2dbc93115e27975995ce78dfad8c308bd2da523d7 - name: cache endpoints: [read, write] - name: frontend diff --git a/module/services/accounts/AGENTS.md b/module/services/accounts/AGENTS.md index a0ce815b8..bb534f89b 100644 --- a/module/services/accounts/AGENTS.md +++ b/module/services/accounts/AGENTS.md @@ -547,12 +547,14 @@ copies a parent's seal across audiences reopens the arm to a person's credential without its bearer. The arm reads the tenant's installations through `InstallationService/ListInstallations` -(`EXPOSURE_INTERNAL`). Its contract comment in `installations.proto` names one -caller, the gateway's viewer projection, with the tenant projected from a -verified identity. The arm is a second caller of the same RPC with the same -credential, and its tenant is the one an edge-verified Work Context names: a -signed statement of this issuer's, never a value the request chose. The proto -comment is to name it at the next regeneration of the accounts descriptors. +(`EXPOSURE_INTERNAL`). Its contract comment in `installations.proto` names both +of the gateway's reads: the viewer projection, with the tenant projected from a +verified identity, and the arm, whose tenant is the one an edge-verified Work +Context names, a signed statement of this issuer's and never a value the request +chose. The messages the RPC takes and answers live in +`installation_listing.proto`, which `installations.proto` imports and the +gateway vendors byte for byte, as it does `module_registration.proto` and +`solution_entitlements.proto`. ## Minting a module Work Context diff --git a/module/services/accounts/code/pkg/adapters/installation_rpcs.go b/module/services/accounts/code/pkg/adapters/installation_rpcs.go index 44a87309e..89167b47c 100644 --- a/module/services/accounts/code/pkg/adapters/installation_rpcs.go +++ b/module/services/accounts/code/pkg/adapters/installation_rpcs.go @@ -134,8 +134,10 @@ func (s *InstallationServer) GetInstallation(ctx context.Context, req *gen.GetIn // ListInstallations enumerates one organization's installations. Internal-tier, // like the scope listing it is read beside: the organization is a request field, -// so a bare tenant JWT must never be able to ask this — the caller that may is -// the auth-gateway, naming the tenant it projected from a verified identity. +// so a bare tenant JWT must never be able to ask this. The caller that may is +// the auth-gateway, naming either the tenant it projected from a verified +// identity (its viewer projection) or the tenant an edge-verified Work Context +// names (its declared module route's bearer-less arm), never one a request chose. func (s *InstallationServer) ListInstallations(ctx context.Context, req *gen.ListInstallationsRequest) (*gen.ListInstallationsResponse, error) { if err := Validate(req); err != nil { return nil, err diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/installations.connect.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/installations.connect.go index 7e6285a5f..b9b0f2d38 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/installations.connect.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/installations.connect.go @@ -100,11 +100,15 @@ type InstallationServiceClient interface { // ListInstallations enumerates one organization's installations with their live // health. It is internal-tier on purpose: it takes the organization as a request // field, so only a caller holding the cluster-internal credential may ask, and - // the tenant it asks about is the one the auth-gateway projected from a verified - // identity — never one a browser supplied. It sits beside - // PermissionService.ListAccessibleScopes for that reason: the two are read - // together to answer what a viewer's organization installed and what that viewer - // was granted. + // the tenant it asks about is never one a browser supplied. The auth-gateway is + // its caller, for two reads. Its viewer projection asks about the tenant it + // projected from a verified identity; it sits beside + // PermissionService.ListAccessibleScopes for that reason, the two read together + // to answer what a viewer's organization installed and what that viewer was + // granted. Its declared module route's bearer-less arm asks about the tenant an + // edge-verified Work Context names, a value this issuer signed, to admit a + // headless operation context only when the target is installed there and the + // context is sealed to another active installation. ListInstallations(context.Context, *connect.Request[v1.ListInstallationsRequest]) (*connect.Response[v1.ListInstallationsResponse], error) // ListAvailableSolutions is the catalogue: the solution targets an // administrator may install right now. @@ -129,7 +133,8 @@ type InstallationServiceClient interface { // It is the org-admin companion to the internal ListInstallations, and it is a // separate method rather than a relaxed policy on that one: ListInstallations // is read by the gateway for an organisation it projected from a verified - // identity, and a bare tenant credential must never reach it. This one binds + // identity or that an edge-verified Work Context names, and a bare tenant + // credential must never reach it. This one binds // the organisation to the caller's own admin membership, exactly as // ListAvailableSolutions does, so no organisation's administrator can read // another's. The one exception is inherited, not added here: a platform @@ -342,11 +347,15 @@ type InstallationServiceHandler interface { // ListInstallations enumerates one organization's installations with their live // health. It is internal-tier on purpose: it takes the organization as a request // field, so only a caller holding the cluster-internal credential may ask, and - // the tenant it asks about is the one the auth-gateway projected from a verified - // identity — never one a browser supplied. It sits beside - // PermissionService.ListAccessibleScopes for that reason: the two are read - // together to answer what a viewer's organization installed and what that viewer - // was granted. + // the tenant it asks about is never one a browser supplied. The auth-gateway is + // its caller, for two reads. Its viewer projection asks about the tenant it + // projected from a verified identity; it sits beside + // PermissionService.ListAccessibleScopes for that reason, the two read together + // to answer what a viewer's organization installed and what that viewer was + // granted. Its declared module route's bearer-less arm asks about the tenant an + // edge-verified Work Context names, a value this issuer signed, to admit a + // headless operation context only when the target is installed there and the + // context is sealed to another active installation. ListInstallations(context.Context, *connect.Request[v1.ListInstallationsRequest]) (*connect.Response[v1.ListInstallationsResponse], error) // ListAvailableSolutions is the catalogue: the solution targets an // administrator may install right now. @@ -371,7 +380,8 @@ type InstallationServiceHandler interface { // It is the org-admin companion to the internal ListInstallations, and it is a // separate method rather than a relaxed policy on that one: ListInstallations // is read by the gateway for an organisation it projected from a verified - // identity, and a bare tenant credential must never reach it. This one binds + // identity or that an edge-verified Work Context names, and a bare tenant + // credential must never reach it. This one binds // the organisation to the caller's own admin membership, exactly as // ListAvailableSolutions does, so no organisation's administrator can read // another's. The one exception is inherited, not added here: a platform diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go new file mode 100644 index 000000000..10897223b --- /dev/null +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go @@ -0,0 +1,607 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: saas/accounts/v1/installation_listing.proto + +package accountsv1 + +import ( + reflect "reflect" + sync "sync" + unsafe "unsafe" + + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// InstallationStatus is the lifecycle of an installation row. An uninstall is a +// soft delete (status=revoked, revoked_at set) so the agent, grants, and audit +// trail remain resolvable. +type InstallationStatus int32 + +const ( + InstallationStatus_INSTALLATION_STATUS_UNSPECIFIED InstallationStatus = 0 + InstallationStatus_INSTALLATION_STATUS_ACTIVE InstallationStatus = 1 + InstallationStatus_INSTALLATION_STATUS_REVOKED InstallationStatus = 2 + // Installed, and waiting for the solution to acknowledge its standard install + // step. Nothing can use a pending installation. + InstallationStatus_INSTALLATION_STATUS_PENDING InstallationStatus = 3 + // The solution refused its install step, or never answered within the + // attempt budget. Nothing can use it; an administrator may retry it. + InstallationStatus_INSTALLATION_STATUS_FAILED InstallationStatus = 4 +) + +// Enum value maps for InstallationStatus. +var ( + InstallationStatus_name = map[int32]string{ + 0: "INSTALLATION_STATUS_UNSPECIFIED", + 1: "INSTALLATION_STATUS_ACTIVE", + 2: "INSTALLATION_STATUS_REVOKED", + 3: "INSTALLATION_STATUS_PENDING", + 4: "INSTALLATION_STATUS_FAILED", + } + InstallationStatus_value = map[string]int32{ + "INSTALLATION_STATUS_UNSPECIFIED": 0, + "INSTALLATION_STATUS_ACTIVE": 1, + "INSTALLATION_STATUS_REVOKED": 2, + "INSTALLATION_STATUS_PENDING": 3, + "INSTALLATION_STATUS_FAILED": 4, + } +) + +func (x InstallationStatus) Enum() *InstallationStatus { + p := new(InstallationStatus) + *p = x + return p +} + +func (x InstallationStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (InstallationStatus) Descriptor() protoreflect.EnumDescriptor { + return file_saas_accounts_v1_installation_listing_proto_enumTypes[0].Descriptor() +} + +func (InstallationStatus) Type() protoreflect.EnumType { + return &file_saas_accounts_v1_installation_listing_proto_enumTypes[0] +} + +func (x InstallationStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use InstallationStatus.Descriptor instead. +func (InstallationStatus) EnumDescriptor() ([]byte, []int) { + return file_saas_accounts_v1_installation_listing_proto_rawDescGZIP(), []int{0} +} + +// InstallationHealth is resolved live from current authority facts, never +// stored. It is the observable form of the fail-closed mint rule: a +// StartInstallationTask fails exactly when health is not HEALTHY, and transfer +// or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record +// nor any co-owner is currently an org admin (the Zapier-class "connection owner +// left the org" failure), surfaced rather than silently reassigned. +type InstallationHealth int32 + +const ( + InstallationHealth_INSTALLATION_HEALTH_UNSPECIFIED InstallationHealth = 0 + InstallationHealth_INSTALLATION_HEALTH_HEALTHY InstallationHealth = 1 + InstallationHealth_INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER InstallationHealth = 2 + InstallationHealth_INSTALLATION_HEALTH_AGENT_REVOKED InstallationHealth = 3 + InstallationHealth_INSTALLATION_HEALTH_AGENT_DISABLED InstallationHealth = 4 + InstallationHealth_INSTALLATION_HEALTH_STANDING_GRANT_MISSING InstallationHealth = 5 +) + +// Enum value maps for InstallationHealth. +var ( + InstallationHealth_name = map[int32]string{ + 0: "INSTALLATION_HEALTH_UNSPECIFIED", + 1: "INSTALLATION_HEALTH_HEALTHY", + 2: "INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER", + 3: "INSTALLATION_HEALTH_AGENT_REVOKED", + 4: "INSTALLATION_HEALTH_AGENT_DISABLED", + 5: "INSTALLATION_HEALTH_STANDING_GRANT_MISSING", + } + InstallationHealth_value = map[string]int32{ + "INSTALLATION_HEALTH_UNSPECIFIED": 0, + "INSTALLATION_HEALTH_HEALTHY": 1, + "INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER": 2, + "INSTALLATION_HEALTH_AGENT_REVOKED": 3, + "INSTALLATION_HEALTH_AGENT_DISABLED": 4, + "INSTALLATION_HEALTH_STANDING_GRANT_MISSING": 5, + } +) + +func (x InstallationHealth) Enum() *InstallationHealth { + p := new(InstallationHealth) + *p = x + return p +} + +func (x InstallationHealth) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (InstallationHealth) Descriptor() protoreflect.EnumDescriptor { + return file_saas_accounts_v1_installation_listing_proto_enumTypes[1].Descriptor() +} + +func (InstallationHealth) Type() protoreflect.EnumType { + return &file_saas_accounts_v1_installation_listing_proto_enumTypes[1] +} + +func (x InstallationHealth) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use InstallationHealth.Descriptor instead. +func (InstallationHealth) EnumDescriptor() ([]byte, []int) { + return file_saas_accounts_v1_installation_listing_proto_rawDescGZIP(), []int{1} +} + +// Installation is one solution's bound identity in an org. Server-assigned ids; +// the browser cannot attest to tenancy, the agent principal, or the scope node. +type Installation struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + OrgId string `protobuf:"bytes,2,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` + AgentPrincipalId string `protobuf:"bytes,3,opt,name=agent_principal_id,json=agentPrincipalId,proto3" json:"agent_principal_id,omitempty"` + OwnerPrincipalId string `protobuf:"bytes,5,opt,name=owner_principal_id,json=ownerPrincipalId,proto3" json:"owner_principal_id,omitempty"` + CoOwnerPrincipalIds []string `protobuf:"bytes,6,rep,name=co_owner_principal_ids,json=coOwnerPrincipalIds,proto3" json:"co_owner_principal_ids,omitempty"` + RootScopeNodeId string `protobuf:"bytes,7,opt,name=root_scope_node_id,json=rootScopeNodeId,proto3" json:"root_scope_node_id,omitempty"` + Status InstallationStatus `protobuf:"varint,8,opt,name=status,proto3,enum=saas.accounts.v1.InstallationStatus" json:"status,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + RevokedAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=revoked_at,json=revokedAt,proto3,oneof" json:"revoked_at,omitempty"` + // The immutable solution target this installation was consented to: one + // continuous period of one binding's presence on this host. It is never + // reused, so a replacement binding — even one claiming the same route alias — + // is a different target and inherits nothing. + // + // Deliberately NOT accompanied by the route alias. The alias is a property of + // the target that a later generation may move, and resolving it here would + // mean joining the host's presence state into a request-path listing — which + // would require granting request traffic read over every organisation's + // presence to answer about one. A consumer that needs the alias asks + // ListAvailableSolutions, which serves accepted applied state, and joins on + // this id. + // + // Empty only on a revoked historical row whose target could not be + // established at the cutover; an active installation always names one. + TargetId string `protobuf:"bytes,11,opt,name=target_id,json=targetId,proto3" json:"target_id,omitempty"` + // Why a revoked installation was revoked, when it was not an administrator + // uninstalling: the installation-identity cutover, or a tenancy narrowing that + // withdrew this organisation. Empty on an active row. + RevokedReason string `protobuf:"bytes,12,opt,name=revoked_reason,json=revokedReason,proto3" json:"revoked_reason,omitempty"` + // Why the install step failed: the solution's bounded refusal reason, or a + // fixed reason when it never answered. Empty unless status is FAILED. + FailureReason string `protobuf:"bytes,13,opt,name=failure_reason,json=failureReason,proto3" json:"failure_reason,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Installation) Reset() { + *x = Installation{} + mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Installation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Installation) ProtoMessage() {} + +func (x *Installation) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Installation.ProtoReflect.Descriptor instead. +func (*Installation) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_installation_listing_proto_rawDescGZIP(), []int{0} +} + +func (x *Installation) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Installation) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + +func (x *Installation) GetAgentPrincipalId() string { + if x != nil { + return x.AgentPrincipalId + } + return "" +} + +func (x *Installation) GetOwnerPrincipalId() string { + if x != nil { + return x.OwnerPrincipalId + } + return "" +} + +func (x *Installation) GetCoOwnerPrincipalIds() []string { + if x != nil { + return x.CoOwnerPrincipalIds + } + return nil +} + +func (x *Installation) GetRootScopeNodeId() string { + if x != nil { + return x.RootScopeNodeId + } + return "" +} + +func (x *Installation) GetStatus() InstallationStatus { + if x != nil { + return x.Status + } + return InstallationStatus_INSTALLATION_STATUS_UNSPECIFIED +} + +func (x *Installation) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *Installation) GetRevokedAt() *timestamppb.Timestamp { + if x != nil { + return x.RevokedAt + } + return nil +} + +func (x *Installation) GetTargetId() string { + if x != nil { + return x.TargetId + } + return "" +} + +func (x *Installation) GetRevokedReason() string { + if x != nil { + return x.RevokedReason + } + return "" +} + +func (x *Installation) GetFailureReason() string { + if x != nil { + return x.FailureReason + } + return "" +} + +// ListInstallationsRequest asks what one organization has installed. Every other +// RPC on this service names a single installation, so until this existed nothing +// could answer "what has this org installed?" — which is the authority read a +// per-organization solution projection is built on (issue #949). +type ListInstallationsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` + // Restrict to one lifecycle status. UNSPECIFIED returns every status, so a + // caller that must see only live installs asks for ACTIVE explicitly rather + // than leaning on a default that would silently widen if the enum grew. + Status InstallationStatus `protobuf:"varint,2,opt,name=status,proto3,enum=saas.accounts.v1.InstallationStatus" json:"status,omitempty"` + // Page bound. 0 -> server default; capped so an organization with many + // installations can never return an unbounded result in one call. + PageSize int32 `protobuf:"varint,3,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Opaque cursor from a previous response's next_page_token; empty for page one. + PageToken string `protobuf:"bytes,4,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListInstallationsRequest) Reset() { + *x = ListInstallationsRequest{} + mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListInstallationsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListInstallationsRequest) ProtoMessage() {} + +func (x *ListInstallationsRequest) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListInstallationsRequest.ProtoReflect.Descriptor instead. +func (*ListInstallationsRequest) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_installation_listing_proto_rawDescGZIP(), []int{1} +} + +func (x *ListInstallationsRequest) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + +func (x *ListInstallationsRequest) GetStatus() InstallationStatus { + if x != nil { + return x.Status + } + return InstallationStatus_INSTALLATION_STATUS_UNSPECIFIED +} + +func (x *ListInstallationsRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListInstallationsRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +// InstallationSummary is one row of the listing: the installation, plus the health +// resolved live beside it exactly as GetInstallation resolves it for one. Health +// travels with the row because a consumer that renders an installed solution as +// unavailable would otherwise need one GetInstallation per installation. +type InstallationSummary struct { + state protoimpl.MessageState `protogen:"open.v1"` + Installation *Installation `protobuf:"bytes,1,opt,name=installation,proto3" json:"installation,omitempty"` + Health InstallationHealth `protobuf:"varint,2,opt,name=health,proto3,enum=saas.accounts.v1.InstallationHealth" json:"health,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InstallationSummary) Reset() { + *x = InstallationSummary{} + mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InstallationSummary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InstallationSummary) ProtoMessage() {} + +func (x *InstallationSummary) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InstallationSummary.ProtoReflect.Descriptor instead. +func (*InstallationSummary) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_installation_listing_proto_rawDescGZIP(), []int{2} +} + +func (x *InstallationSummary) GetInstallation() *Installation { + if x != nil { + return x.Installation + } + return nil +} + +func (x *InstallationSummary) GetHealth() InstallationHealth { + if x != nil { + return x.Health + } + return InstallationHealth_INSTALLATION_HEALTH_UNSPECIFIED +} + +type ListInstallationsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Installations []*InstallationSummary `protobuf:"bytes,1,rep,name=installations,proto3" json:"installations,omitempty"` + // Set when more installations remain; pass it back as page_token. Empty on the + // last page. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListInstallationsResponse) Reset() { + *x = ListInstallationsResponse{} + mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListInstallationsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListInstallationsResponse) ProtoMessage() {} + +func (x *ListInstallationsResponse) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListInstallationsResponse.ProtoReflect.Descriptor instead. +func (*ListInstallationsResponse) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_installation_listing_proto_rawDescGZIP(), []int{3} +} + +func (x *ListInstallationsResponse) GetInstallations() []*InstallationSummary { + if x != nil { + return x.Installations + } + return nil +} + +func (x *ListInstallationsResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +var File_saas_accounts_v1_installation_listing_proto protoreflect.FileDescriptor + +const file_saas_accounts_v1_installation_listing_proto_rawDesc = "" + + "\n" + + "+saas/accounts/v1/installation_listing.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x94\x05\n" + + "\fInstallation\x12\x18\n" + + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id\x12\x1f\n" + + "\x06org_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x126\n" + + "\x12agent_principal_id\x18\x03 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x10agentPrincipalId\x126\n" + + "\x12owner_principal_id\x18\x05 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x10ownerPrincipalId\x123\n" + + "\x16co_owner_principal_ids\x18\x06 \x03(\tR\x13coOwnerPrincipalIds\x125\n" + + "\x12root_scope_node_id\x18\a \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0frootScopeNodeId\x12<\n" + + "\x06status\x18\b \x01(\x0e2$.saas.accounts.v1.InstallationStatusR\x06status\x129\n" + + "\n" + + "created_at\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12>\n" + + "\n" + + "revoked_at\x18\n" + + " \x01(\v2\x1a.google.protobuf.TimestampH\x00R\trevokedAt\x88\x01\x01\x12(\n" + + "\ttarget_id\x18\v \x01(\tB\v\xbaH\b\xd8\x01\x01r\x03\xb0\x01\x01R\btargetId\x12/\n" + + "\x0erevoked_reason\x18\f \x01(\tB\b\xbaH\x05r\x03\x18\x80\x04R\rrevokedReason\x12/\n" + + "\x0efailure_reason\x18\r \x01(\tB\b\xbaH\x05r\x03\x18\x80\x04R\rfailureReasonB\r\n" + + "\v_revoked_atJ\x04\b\x04\x10\x05R\x13solution_identifier\"\xc1\x01\n" + + "\x18ListInstallationsRequest\x12\x1f\n" + + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x12<\n" + + "\x06status\x18\x02 \x01(\x0e2$.saas.accounts.v1.InstallationStatusR\x06status\x12'\n" + + "\tpage_size\x18\x03 \x01(\x05B\n" + + "\xbaH\a\x1a\x05\x18\xf4\x03(\x00R\bpageSize\x12\x1d\n" + + "\n" + + "page_token\x18\x04 \x01(\tR\tpageToken\"\x97\x01\n" + + "\x13InstallationSummary\x12B\n" + + "\finstallation\x18\x01 \x01(\v2\x1e.saas.accounts.v1.InstallationR\finstallation\x12<\n" + + "\x06health\x18\x02 \x01(\x0e2$.saas.accounts.v1.InstallationHealthR\x06health\"\x90\x01\n" + + "\x19ListInstallationsResponse\x12K\n" + + "\rinstallations\x18\x01 \x03(\v2%.saas.accounts.v1.InstallationSummaryR\rinstallations\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken*\xbb\x01\n" + + "\x12InstallationStatus\x12#\n" + + "\x1fINSTALLATION_STATUS_UNSPECIFIED\x10\x00\x12\x1e\n" + + "\x1aINSTALLATION_STATUS_ACTIVE\x10\x01\x12\x1f\n" + + "\x1bINSTALLATION_STATUS_REVOKED\x10\x02\x12\x1f\n" + + "\x1bINSTALLATION_STATUS_PENDING\x10\x03\x12\x1e\n" + + "\x1aINSTALLATION_STATUS_FAILED\x10\x04*\x84\x02\n" + + "\x12InstallationHealth\x12#\n" + + "\x1fINSTALLATION_HEALTH_UNSPECIFIED\x10\x00\x12\x1f\n" + + "\x1bINSTALLATION_HEALTH_HEALTHY\x10\x01\x12)\n" + + "%INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER\x10\x02\x12%\n" + + "!INSTALLATION_HEALTH_AGENT_REVOKED\x10\x03\x12&\n" + + "\"INSTALLATION_HEALTH_AGENT_DISABLED\x10\x04\x12.\n" + + "*INSTALLATION_HEALTH_STANDING_GRANT_MISSING\x10\x05B\xc0\x01\n" + + "\x14com.saas.accounts.v1B\x18InstallationListingProtoP\x01Z,accounts/pkg/gen/saas/accounts/v1;accountsv1\xa2\x02\x03SAX\xaa\x02\x10Saas.Accounts.V1\xca\x02\x10Saas\\Accounts\\V1\xe2\x02\x1cSaas\\Accounts\\V1\\GPBMetadata\xea\x02\x12Saas::Accounts::V1b\x06proto3" + +var ( + file_saas_accounts_v1_installation_listing_proto_rawDescOnce sync.Once + file_saas_accounts_v1_installation_listing_proto_rawDescData []byte +) + +func file_saas_accounts_v1_installation_listing_proto_rawDescGZIP() []byte { + file_saas_accounts_v1_installation_listing_proto_rawDescOnce.Do(func() { + file_saas_accounts_v1_installation_listing_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_installation_listing_proto_rawDesc), len(file_saas_accounts_v1_installation_listing_proto_rawDesc))) + }) + return file_saas_accounts_v1_installation_listing_proto_rawDescData +} + +var file_saas_accounts_v1_installation_listing_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_saas_accounts_v1_installation_listing_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_saas_accounts_v1_installation_listing_proto_goTypes = []any{ + (InstallationStatus)(0), // 0: saas.accounts.v1.InstallationStatus + (InstallationHealth)(0), // 1: saas.accounts.v1.InstallationHealth + (*Installation)(nil), // 2: saas.accounts.v1.Installation + (*ListInstallationsRequest)(nil), // 3: saas.accounts.v1.ListInstallationsRequest + (*InstallationSummary)(nil), // 4: saas.accounts.v1.InstallationSummary + (*ListInstallationsResponse)(nil), // 5: saas.accounts.v1.ListInstallationsResponse + (*timestamppb.Timestamp)(nil), // 6: google.protobuf.Timestamp +} +var file_saas_accounts_v1_installation_listing_proto_depIdxs = []int32{ + 0, // 0: saas.accounts.v1.Installation.status:type_name -> saas.accounts.v1.InstallationStatus + 6, // 1: saas.accounts.v1.Installation.created_at:type_name -> google.protobuf.Timestamp + 6, // 2: saas.accounts.v1.Installation.revoked_at:type_name -> google.protobuf.Timestamp + 0, // 3: saas.accounts.v1.ListInstallationsRequest.status:type_name -> saas.accounts.v1.InstallationStatus + 2, // 4: saas.accounts.v1.InstallationSummary.installation:type_name -> saas.accounts.v1.Installation + 1, // 5: saas.accounts.v1.InstallationSummary.health:type_name -> saas.accounts.v1.InstallationHealth + 4, // 6: saas.accounts.v1.ListInstallationsResponse.installations:type_name -> saas.accounts.v1.InstallationSummary + 7, // [7:7] is the sub-list for method output_type + 7, // [7:7] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name +} + +func init() { file_saas_accounts_v1_installation_listing_proto_init() } +func file_saas_accounts_v1_installation_listing_proto_init() { + if File_saas_accounts_v1_installation_listing_proto != nil { + return + } + file_saas_accounts_v1_installation_listing_proto_msgTypes[0].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_installation_listing_proto_rawDesc), len(file_saas_accounts_v1_installation_listing_proto_rawDesc)), + NumEnums: 2, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_saas_accounts_v1_installation_listing_proto_goTypes, + DependencyIndexes: file_saas_accounts_v1_installation_listing_proto_depIdxs, + EnumInfos: file_saas_accounts_v1_installation_listing_proto_enumTypes, + MessageInfos: file_saas_accounts_v1_installation_listing_proto_msgTypes, + }.Build() + File_saas_accounts_v1_installation_listing_proto = out.File + file_saas_accounts_v1_installation_listing_proto_goTypes = nil + file_saas_accounts_v1_installation_listing_proto_depIdxs = nil +} diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations.pb.go index 051a29951..9503ce6cc 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations.pb.go @@ -17,7 +17,6 @@ import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" emptypb "google.golang.org/protobuf/types/known/emptypb" - timestamppb "google.golang.org/protobuf/types/known/timestamppb" ) const ( @@ -27,132 +26,6 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// InstallationStatus is the lifecycle of an installation row. An uninstall is a -// soft delete (status=revoked, revoked_at set) so the agent, grants, and audit -// trail remain resolvable. -type InstallationStatus int32 - -const ( - InstallationStatus_INSTALLATION_STATUS_UNSPECIFIED InstallationStatus = 0 - InstallationStatus_INSTALLATION_STATUS_ACTIVE InstallationStatus = 1 - InstallationStatus_INSTALLATION_STATUS_REVOKED InstallationStatus = 2 - // Installed, and waiting for the solution to acknowledge its standard install - // step. Nothing can use a pending installation. - InstallationStatus_INSTALLATION_STATUS_PENDING InstallationStatus = 3 - // The solution refused its install step, or never answered within the - // attempt budget. Nothing can use it; an administrator may retry it. - InstallationStatus_INSTALLATION_STATUS_FAILED InstallationStatus = 4 -) - -// Enum value maps for InstallationStatus. -var ( - InstallationStatus_name = map[int32]string{ - 0: "INSTALLATION_STATUS_UNSPECIFIED", - 1: "INSTALLATION_STATUS_ACTIVE", - 2: "INSTALLATION_STATUS_REVOKED", - 3: "INSTALLATION_STATUS_PENDING", - 4: "INSTALLATION_STATUS_FAILED", - } - InstallationStatus_value = map[string]int32{ - "INSTALLATION_STATUS_UNSPECIFIED": 0, - "INSTALLATION_STATUS_ACTIVE": 1, - "INSTALLATION_STATUS_REVOKED": 2, - "INSTALLATION_STATUS_PENDING": 3, - "INSTALLATION_STATUS_FAILED": 4, - } -) - -func (x InstallationStatus) Enum() *InstallationStatus { - p := new(InstallationStatus) - *p = x - return p -} - -func (x InstallationStatus) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (InstallationStatus) Descriptor() protoreflect.EnumDescriptor { - return file_saas_accounts_v1_installations_proto_enumTypes[0].Descriptor() -} - -func (InstallationStatus) Type() protoreflect.EnumType { - return &file_saas_accounts_v1_installations_proto_enumTypes[0] -} - -func (x InstallationStatus) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use InstallationStatus.Descriptor instead. -func (InstallationStatus) EnumDescriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{0} -} - -// InstallationHealth is resolved live from current authority facts, never -// stored. It is the observable form of the fail-closed mint rule: a -// StartInstallationTask fails exactly when health is not HEALTHY, and transfer -// or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record -// nor any co-owner is currently an org admin (the Zapier-class "connection owner -// left the org" failure), surfaced rather than silently reassigned. -type InstallationHealth int32 - -const ( - InstallationHealth_INSTALLATION_HEALTH_UNSPECIFIED InstallationHealth = 0 - InstallationHealth_INSTALLATION_HEALTH_HEALTHY InstallationHealth = 1 - InstallationHealth_INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER InstallationHealth = 2 - InstallationHealth_INSTALLATION_HEALTH_AGENT_REVOKED InstallationHealth = 3 - InstallationHealth_INSTALLATION_HEALTH_AGENT_DISABLED InstallationHealth = 4 - InstallationHealth_INSTALLATION_HEALTH_STANDING_GRANT_MISSING InstallationHealth = 5 -) - -// Enum value maps for InstallationHealth. -var ( - InstallationHealth_name = map[int32]string{ - 0: "INSTALLATION_HEALTH_UNSPECIFIED", - 1: "INSTALLATION_HEALTH_HEALTHY", - 2: "INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER", - 3: "INSTALLATION_HEALTH_AGENT_REVOKED", - 4: "INSTALLATION_HEALTH_AGENT_DISABLED", - 5: "INSTALLATION_HEALTH_STANDING_GRANT_MISSING", - } - InstallationHealth_value = map[string]int32{ - "INSTALLATION_HEALTH_UNSPECIFIED": 0, - "INSTALLATION_HEALTH_HEALTHY": 1, - "INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER": 2, - "INSTALLATION_HEALTH_AGENT_REVOKED": 3, - "INSTALLATION_HEALTH_AGENT_DISABLED": 4, - "INSTALLATION_HEALTH_STANDING_GRANT_MISSING": 5, - } -) - -func (x InstallationHealth) Enum() *InstallationHealth { - p := new(InstallationHealth) - *p = x - return p -} - -func (x InstallationHealth) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (InstallationHealth) Descriptor() protoreflect.EnumDescriptor { - return file_saas_accounts_v1_installations_proto_enumTypes[1].Descriptor() -} - -func (InstallationHealth) Type() protoreflect.EnumType { - return &file_saas_accounts_v1_installations_proto_enumTypes[1] -} - -func (x InstallationHealth) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use InstallationHealth.Descriptor instead. -func (InstallationHealth) EnumDescriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{1} -} - // ReadinessConditionKind is what kind of thing is unmet. A reader acts on the // words in `detail`; the kind is for the surface to choose an action and for a // consumer to count one kind without parsing prose. @@ -227,11 +100,11 @@ func (x ReadinessConditionKind) String() string { } func (ReadinessConditionKind) Descriptor() protoreflect.EnumDescriptor { - return file_saas_accounts_v1_installations_proto_enumTypes[2].Descriptor() + return file_saas_accounts_v1_installations_proto_enumTypes[0].Descriptor() } func (ReadinessConditionKind) Type() protoreflect.EnumType { - return &file_saas_accounts_v1_installations_proto_enumTypes[2] + return &file_saas_accounts_v1_installations_proto_enumTypes[0] } func (x ReadinessConditionKind) Number() protoreflect.EnumNumber { @@ -240,7 +113,7 @@ func (x ReadinessConditionKind) Number() protoreflect.EnumNumber { // Deprecated: Use ReadinessConditionKind.Descriptor instead. func (ReadinessConditionKind) EnumDescriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{2} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{0} } // ReadinessClearedBy is who can clear a condition. It is stated rather than @@ -288,11 +161,11 @@ func (x ReadinessClearedBy) String() string { } func (ReadinessClearedBy) Descriptor() protoreflect.EnumDescriptor { - return file_saas_accounts_v1_installations_proto_enumTypes[3].Descriptor() + return file_saas_accounts_v1_installations_proto_enumTypes[1].Descriptor() } func (ReadinessClearedBy) Type() protoreflect.EnumType { - return &file_saas_accounts_v1_installations_proto_enumTypes[3] + return &file_saas_accounts_v1_installations_proto_enumTypes[1] } func (x ReadinessClearedBy) Number() protoreflect.EnumNumber { @@ -301,7 +174,7 @@ func (x ReadinessClearedBy) Number() protoreflect.EnumNumber { // Deprecated: Use ReadinessClearedBy.Descriptor instead. func (ReadinessClearedBy) EnumDescriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{3} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{1} } // ReadinessAction is what clears the condition, named as the act rather than as @@ -353,11 +226,11 @@ func (x ReadinessAction) String() string { } func (ReadinessAction) Descriptor() protoreflect.EnumDescriptor { - return file_saas_accounts_v1_installations_proto_enumTypes[4].Descriptor() + return file_saas_accounts_v1_installations_proto_enumTypes[2].Descriptor() } func (ReadinessAction) Type() protoreflect.EnumType { - return &file_saas_accounts_v1_installations_proto_enumTypes[4] + return &file_saas_accounts_v1_installations_proto_enumTypes[2] } func (x ReadinessAction) Number() protoreflect.EnumNumber { @@ -366,161 +239,7 @@ func (x ReadinessAction) Number() protoreflect.EnumNumber { // Deprecated: Use ReadinessAction.Descriptor instead. func (ReadinessAction) EnumDescriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{4} -} - -// Installation is one solution's bound identity in an org. Server-assigned ids; -// the browser cannot attest to tenancy, the agent principal, or the scope node. -type Installation struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - OrgId string `protobuf:"bytes,2,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` - AgentPrincipalId string `protobuf:"bytes,3,opt,name=agent_principal_id,json=agentPrincipalId,proto3" json:"agent_principal_id,omitempty"` - OwnerPrincipalId string `protobuf:"bytes,5,opt,name=owner_principal_id,json=ownerPrincipalId,proto3" json:"owner_principal_id,omitempty"` - CoOwnerPrincipalIds []string `protobuf:"bytes,6,rep,name=co_owner_principal_ids,json=coOwnerPrincipalIds,proto3" json:"co_owner_principal_ids,omitempty"` - RootScopeNodeId string `protobuf:"bytes,7,opt,name=root_scope_node_id,json=rootScopeNodeId,proto3" json:"root_scope_node_id,omitempty"` - Status InstallationStatus `protobuf:"varint,8,opt,name=status,proto3,enum=saas.accounts.v1.InstallationStatus" json:"status,omitempty"` - CreatedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` - RevokedAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=revoked_at,json=revokedAt,proto3,oneof" json:"revoked_at,omitempty"` - // The immutable solution target this installation was consented to: one - // continuous period of one binding's presence on this host. It is never - // reused, so a replacement binding — even one claiming the same route alias — - // is a different target and inherits nothing. - // - // Deliberately NOT accompanied by the route alias. The alias is a property of - // the target that a later generation may move, and resolving it here would - // mean joining the host's presence state into a request-path listing — which - // would require granting request traffic read over every organisation's - // presence to answer about one. A consumer that needs the alias asks - // ListAvailableSolutions, which serves accepted applied state, and joins on - // this id. - // - // Empty only on a revoked historical row whose target could not be - // established at the cutover; an active installation always names one. - TargetId string `protobuf:"bytes,11,opt,name=target_id,json=targetId,proto3" json:"target_id,omitempty"` - // Why a revoked installation was revoked, when it was not an administrator - // uninstalling: the installation-identity cutover, or a tenancy narrowing that - // withdrew this organisation. Empty on an active row. - RevokedReason string `protobuf:"bytes,12,opt,name=revoked_reason,json=revokedReason,proto3" json:"revoked_reason,omitempty"` - // Why the install step failed: the solution's bounded refusal reason, or a - // fixed reason when it never answered. Empty unless status is FAILED. - FailureReason string `protobuf:"bytes,13,opt,name=failure_reason,json=failureReason,proto3" json:"failure_reason,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Installation) Reset() { - *x = Installation{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Installation) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Installation) ProtoMessage() {} - -func (x *Installation) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Installation.ProtoReflect.Descriptor instead. -func (*Installation) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{0} -} - -func (x *Installation) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *Installation) GetOrgId() string { - if x != nil { - return x.OrgId - } - return "" -} - -func (x *Installation) GetAgentPrincipalId() string { - if x != nil { - return x.AgentPrincipalId - } - return "" -} - -func (x *Installation) GetOwnerPrincipalId() string { - if x != nil { - return x.OwnerPrincipalId - } - return "" -} - -func (x *Installation) GetCoOwnerPrincipalIds() []string { - if x != nil { - return x.CoOwnerPrincipalIds - } - return nil -} - -func (x *Installation) GetRootScopeNodeId() string { - if x != nil { - return x.RootScopeNodeId - } - return "" -} - -func (x *Installation) GetStatus() InstallationStatus { - if x != nil { - return x.Status - } - return InstallationStatus_INSTALLATION_STATUS_UNSPECIFIED -} - -func (x *Installation) GetCreatedAt() *timestamppb.Timestamp { - if x != nil { - return x.CreatedAt - } - return nil -} - -func (x *Installation) GetRevokedAt() *timestamppb.Timestamp { - if x != nil { - return x.RevokedAt - } - return nil -} - -func (x *Installation) GetTargetId() string { - if x != nil { - return x.TargetId - } - return "" -} - -func (x *Installation) GetRevokedReason() string { - if x != nil { - return x.RevokedReason - } - return "" -} - -func (x *Installation) GetFailureReason() string { - if x != nil { - return x.FailureReason - } - return "" + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{2} } type InstallSolutionRequest struct { @@ -580,7 +299,7 @@ type InstallSolutionRequest struct { func (x *InstallSolutionRequest) Reset() { *x = InstallSolutionRequest{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[1] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -592,7 +311,7 @@ func (x *InstallSolutionRequest) String() string { func (*InstallSolutionRequest) ProtoMessage() {} func (x *InstallSolutionRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[1] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -605,7 +324,7 @@ func (x *InstallSolutionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use InstallSolutionRequest.ProtoReflect.Descriptor instead. func (*InstallSolutionRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{1} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{0} } func (x *InstallSolutionRequest) GetOrgId() string { @@ -699,7 +418,7 @@ type NarrowInstallationRequest struct { func (x *NarrowInstallationRequest) Reset() { *x = NarrowInstallationRequest{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[2] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -711,7 +430,7 @@ func (x *NarrowInstallationRequest) String() string { func (*NarrowInstallationRequest) ProtoMessage() {} func (x *NarrowInstallationRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[2] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -724,7 +443,7 @@ func (x *NarrowInstallationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use NarrowInstallationRequest.ProtoReflect.Descriptor instead. func (*NarrowInstallationRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{2} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{1} } func (x *NarrowInstallationRequest) GetOrgId() string { @@ -758,7 +477,7 @@ type RetryInstallationRequest struct { func (x *RetryInstallationRequest) Reset() { *x = RetryInstallationRequest{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[3] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -770,7 +489,7 @@ func (x *RetryInstallationRequest) String() string { func (*RetryInstallationRequest) ProtoMessage() {} func (x *RetryInstallationRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[3] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -783,7 +502,7 @@ func (x *RetryInstallationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RetryInstallationRequest.ProtoReflect.Descriptor instead. func (*RetryInstallationRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{3} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{2} } func (x *RetryInstallationRequest) GetOrgId() string { @@ -810,7 +529,7 @@ type UninstallSolutionRequest struct { func (x *UninstallSolutionRequest) Reset() { *x = UninstallSolutionRequest{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[4] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -822,7 +541,7 @@ func (x *UninstallSolutionRequest) String() string { func (*UninstallSolutionRequest) ProtoMessage() {} func (x *UninstallSolutionRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[4] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -835,7 +554,7 @@ func (x *UninstallSolutionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UninstallSolutionRequest.ProtoReflect.Descriptor instead. func (*UninstallSolutionRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{4} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{3} } func (x *UninstallSolutionRequest) GetOrgId() string { @@ -866,7 +585,7 @@ type TransferInstallationOwnershipRequest struct { func (x *TransferInstallationOwnershipRequest) Reset() { *x = TransferInstallationOwnershipRequest{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[5] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -878,7 +597,7 @@ func (x *TransferInstallationOwnershipRequest) String() string { func (*TransferInstallationOwnershipRequest) ProtoMessage() {} func (x *TransferInstallationOwnershipRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[5] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -891,7 +610,7 @@ func (x *TransferInstallationOwnershipRequest) ProtoReflect() protoreflect.Messa // Deprecated: Use TransferInstallationOwnershipRequest.ProtoReflect.Descriptor instead. func (*TransferInstallationOwnershipRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{5} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{4} } func (x *TransferInstallationOwnershipRequest) GetOrgId() string { @@ -932,7 +651,7 @@ type GetInstallationRequest struct { func (x *GetInstallationRequest) Reset() { *x = GetInstallationRequest{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[6] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -944,7 +663,7 @@ func (x *GetInstallationRequest) String() string { func (*GetInstallationRequest) ProtoMessage() {} func (x *GetInstallationRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[6] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -957,7 +676,7 @@ func (x *GetInstallationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetInstallationRequest.ProtoReflect.Descriptor instead. func (*GetInstallationRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{6} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{5} } func (x *GetInstallationRequest) GetOrgId() string { @@ -985,7 +704,7 @@ type GetInstallationResponse struct { func (x *GetInstallationResponse) Reset() { *x = GetInstallationResponse{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[7] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -997,7 +716,7 @@ func (x *GetInstallationResponse) String() string { func (*GetInstallationResponse) ProtoMessage() {} func (x *GetInstallationResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[7] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1010,7 +729,7 @@ func (x *GetInstallationResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetInstallationResponse.ProtoReflect.Descriptor instead. func (*GetInstallationResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{7} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{6} } func (x *GetInstallationResponse) GetInstallation() *Installation { @@ -1027,194 +746,6 @@ func (x *GetInstallationResponse) GetHealth() InstallationHealth { return InstallationHealth_INSTALLATION_HEALTH_UNSPECIFIED } -// ListInstallationsRequest asks what one organization has installed. Every other -// RPC on this service names a single installation, so until this existed nothing -// could answer "what has this org installed?" — which is the authority read a -// per-organization solution projection is built on (issue #949). -type ListInstallationsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` - // Restrict to one lifecycle status. UNSPECIFIED returns every status, so a - // caller that must see only live installs asks for ACTIVE explicitly rather - // than leaning on a default that would silently widen if the enum grew. - Status InstallationStatus `protobuf:"varint,2,opt,name=status,proto3,enum=saas.accounts.v1.InstallationStatus" json:"status,omitempty"` - // Page bound. 0 -> server default; capped so an organization with many - // installations can never return an unbounded result in one call. - PageSize int32 `protobuf:"varint,3,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` - // Opaque cursor from a previous response's next_page_token; empty for page one. - PageToken string `protobuf:"bytes,4,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListInstallationsRequest) Reset() { - *x = ListInstallationsRequest{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListInstallationsRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListInstallationsRequest) ProtoMessage() {} - -func (x *ListInstallationsRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[8] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ListInstallationsRequest.ProtoReflect.Descriptor instead. -func (*ListInstallationsRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{8} -} - -func (x *ListInstallationsRequest) GetOrgId() string { - if x != nil { - return x.OrgId - } - return "" -} - -func (x *ListInstallationsRequest) GetStatus() InstallationStatus { - if x != nil { - return x.Status - } - return InstallationStatus_INSTALLATION_STATUS_UNSPECIFIED -} - -func (x *ListInstallationsRequest) GetPageSize() int32 { - if x != nil { - return x.PageSize - } - return 0 -} - -func (x *ListInstallationsRequest) GetPageToken() string { - if x != nil { - return x.PageToken - } - return "" -} - -// InstallationSummary is one row of the listing: the installation, plus the health -// resolved live beside it exactly as GetInstallation resolves it for one. Health -// travels with the row because a consumer that renders an installed solution as -// unavailable would otherwise need one GetInstallation per installation. -type InstallationSummary struct { - state protoimpl.MessageState `protogen:"open.v1"` - Installation *Installation `protobuf:"bytes,1,opt,name=installation,proto3" json:"installation,omitempty"` - Health InstallationHealth `protobuf:"varint,2,opt,name=health,proto3,enum=saas.accounts.v1.InstallationHealth" json:"health,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *InstallationSummary) Reset() { - *x = InstallationSummary{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *InstallationSummary) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*InstallationSummary) ProtoMessage() {} - -func (x *InstallationSummary) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[9] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use InstallationSummary.ProtoReflect.Descriptor instead. -func (*InstallationSummary) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{9} -} - -func (x *InstallationSummary) GetInstallation() *Installation { - if x != nil { - return x.Installation - } - return nil -} - -func (x *InstallationSummary) GetHealth() InstallationHealth { - if x != nil { - return x.Health - } - return InstallationHealth_INSTALLATION_HEALTH_UNSPECIFIED -} - -type ListInstallationsResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Installations []*InstallationSummary `protobuf:"bytes,1,rep,name=installations,proto3" json:"installations,omitempty"` - // Set when more installations remain; pass it back as page_token. Empty on the - // last page. - NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListInstallationsResponse) Reset() { - *x = ListInstallationsResponse{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListInstallationsResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListInstallationsResponse) ProtoMessage() {} - -func (x *ListInstallationsResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[10] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ListInstallationsResponse.ProtoReflect.Descriptor instead. -func (*ListInstallationsResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{10} -} - -func (x *ListInstallationsResponse) GetInstallations() []*InstallationSummary { - if x != nil { - return x.Installations - } - return nil -} - -func (x *ListInstallationsResponse) GetNextPageToken() string { - if x != nil { - return x.NextPageToken - } - return "" -} - type ListOrganizationInstallationsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` @@ -1228,7 +759,7 @@ type ListOrganizationInstallationsRequest struct { func (x *ListOrganizationInstallationsRequest) Reset() { *x = ListOrganizationInstallationsRequest{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[11] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1240,7 +771,7 @@ func (x *ListOrganizationInstallationsRequest) String() string { func (*ListOrganizationInstallationsRequest) ProtoMessage() {} func (x *ListOrganizationInstallationsRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[11] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1253,7 +784,7 @@ func (x *ListOrganizationInstallationsRequest) ProtoReflect() protoreflect.Messa // Deprecated: Use ListOrganizationInstallationsRequest.ProtoReflect.Descriptor instead. func (*ListOrganizationInstallationsRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{11} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{7} } func (x *ListOrganizationInstallationsRequest) GetOrgId() string { @@ -1313,7 +844,7 @@ type OrganizationInstallation struct { func (x *OrganizationInstallation) Reset() { *x = OrganizationInstallation{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[12] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1325,7 +856,7 @@ func (x *OrganizationInstallation) String() string { func (*OrganizationInstallation) ProtoMessage() {} func (x *OrganizationInstallation) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[12] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1338,7 +869,7 @@ func (x *OrganizationInstallation) ProtoReflect() protoreflect.Message { // Deprecated: Use OrganizationInstallation.ProtoReflect.Descriptor instead. func (*OrganizationInstallation) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{12} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{8} } func (x *OrganizationInstallation) GetInstallation() *Installation { @@ -1418,7 +949,7 @@ type ReadinessCondition struct { func (x *ReadinessCondition) Reset() { *x = ReadinessCondition{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[13] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1430,7 +961,7 @@ func (x *ReadinessCondition) String() string { func (*ReadinessCondition) ProtoMessage() {} func (x *ReadinessCondition) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[13] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1443,7 +974,7 @@ func (x *ReadinessCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadinessCondition.ProtoReflect.Descriptor instead. func (*ReadinessCondition) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{13} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{9} } func (x *ReadinessCondition) GetKind() ReadinessConditionKind { @@ -1505,7 +1036,7 @@ type InstallationReadiness struct { func (x *InstallationReadiness) Reset() { *x = InstallationReadiness{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[14] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1517,7 +1048,7 @@ func (x *InstallationReadiness) String() string { func (*InstallationReadiness) ProtoMessage() {} func (x *InstallationReadiness) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[14] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1530,7 +1061,7 @@ func (x *InstallationReadiness) ProtoReflect() protoreflect.Message { // Deprecated: Use InstallationReadiness.ProtoReflect.Descriptor instead. func (*InstallationReadiness) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{14} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{10} } func (x *InstallationReadiness) GetReady() bool { @@ -1576,7 +1107,7 @@ type ReachedModule struct { func (x *ReachedModule) Reset() { *x = ReachedModule{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[15] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1588,7 +1119,7 @@ func (x *ReachedModule) String() string { func (*ReachedModule) ProtoMessage() {} func (x *ReachedModule) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[15] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1601,7 +1132,7 @@ func (x *ReachedModule) ProtoReflect() protoreflect.Message { // Deprecated: Use ReachedModule.ProtoReflect.Descriptor instead. func (*ReachedModule) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{15} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{11} } func (x *ReachedModule) GetAudience() string { @@ -1636,7 +1167,7 @@ type ListOrganizationInstallationsResponse struct { func (x *ListOrganizationInstallationsResponse) Reset() { *x = ListOrganizationInstallationsResponse{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[16] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1648,7 +1179,7 @@ func (x *ListOrganizationInstallationsResponse) String() string { func (*ListOrganizationInstallationsResponse) ProtoMessage() {} func (x *ListOrganizationInstallationsResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[16] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1661,7 +1192,7 @@ func (x *ListOrganizationInstallationsResponse) ProtoReflect() protoreflect.Mess // Deprecated: Use ListOrganizationInstallationsResponse.ProtoReflect.Descriptor instead. func (*ListOrganizationInstallationsResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{16} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{12} } func (x *ListOrganizationInstallationsResponse) GetInstallations() []*OrganizationInstallation { @@ -1694,7 +1225,7 @@ type GrantInstallationToTeamRequest struct { func (x *GrantInstallationToTeamRequest) Reset() { *x = GrantInstallationToTeamRequest{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[17] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1706,7 +1237,7 @@ func (x *GrantInstallationToTeamRequest) String() string { func (*GrantInstallationToTeamRequest) ProtoMessage() {} func (x *GrantInstallationToTeamRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[17] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1719,7 +1250,7 @@ func (x *GrantInstallationToTeamRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GrantInstallationToTeamRequest.ProtoReflect.Descriptor instead. func (*GrantInstallationToTeamRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{17} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{13} } func (x *GrantInstallationToTeamRequest) GetOrgId() string { @@ -1783,7 +1314,7 @@ type CarriedModuleAccess struct { func (x *CarriedModuleAccess) Reset() { *x = CarriedModuleAccess{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[18] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1795,7 +1326,7 @@ func (x *CarriedModuleAccess) String() string { func (*CarriedModuleAccess) ProtoMessage() {} func (x *CarriedModuleAccess) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[18] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1808,7 +1339,7 @@ func (x *CarriedModuleAccess) ProtoReflect() protoreflect.Message { // Deprecated: Use CarriedModuleAccess.ProtoReflect.Descriptor instead. func (*CarriedModuleAccess) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{18} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{14} } func (x *CarriedModuleAccess) GetAudience() string { @@ -1866,7 +1397,7 @@ type GrantInstallationToTeamResponse struct { func (x *GrantInstallationToTeamResponse) Reset() { *x = GrantInstallationToTeamResponse{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[19] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1878,7 +1409,7 @@ func (x *GrantInstallationToTeamResponse) String() string { func (*GrantInstallationToTeamResponse) ProtoMessage() {} func (x *GrantInstallationToTeamResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[19] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1891,7 +1422,7 @@ func (x *GrantInstallationToTeamResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GrantInstallationToTeamResponse.ProtoReflect.Descriptor instead. func (*GrantInstallationToTeamResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{19} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{15} } func (x *GrantInstallationToTeamResponse) GetGrant() *ScopeGrant { @@ -1929,7 +1460,7 @@ type RevokeInstallationFromTeamRequest struct { func (x *RevokeInstallationFromTeamRequest) Reset() { *x = RevokeInstallationFromTeamRequest{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[20] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1941,7 +1472,7 @@ func (x *RevokeInstallationFromTeamRequest) String() string { func (*RevokeInstallationFromTeamRequest) ProtoMessage() {} func (x *RevokeInstallationFromTeamRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[20] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1954,7 +1485,7 @@ func (x *RevokeInstallationFromTeamRequest) ProtoReflect() protoreflect.Message // Deprecated: Use RevokeInstallationFromTeamRequest.ProtoReflect.Descriptor instead. func (*RevokeInstallationFromTeamRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{20} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{16} } func (x *RevokeInstallationFromTeamRequest) GetOrgId() string { @@ -2023,7 +1554,7 @@ type AvailableSolution struct { func (x *AvailableSolution) Reset() { *x = AvailableSolution{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[21] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2035,7 +1566,7 @@ func (x *AvailableSolution) String() string { func (*AvailableSolution) ProtoMessage() {} func (x *AvailableSolution) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[21] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2048,7 +1579,7 @@ func (x *AvailableSolution) ProtoReflect() protoreflect.Message { // Deprecated: Use AvailableSolution.ProtoReflect.Descriptor instead. func (*AvailableSolution) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{21} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{17} } func (x *AvailableSolution) GetTargetId() string { @@ -2134,7 +1665,7 @@ type ListAvailableSolutionsRequest struct { func (x *ListAvailableSolutionsRequest) Reset() { *x = ListAvailableSolutionsRequest{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[22] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2146,7 +1677,7 @@ func (x *ListAvailableSolutionsRequest) String() string { func (*ListAvailableSolutionsRequest) ProtoMessage() {} func (x *ListAvailableSolutionsRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[22] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2159,7 +1690,7 @@ func (x *ListAvailableSolutionsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAvailableSolutionsRequest.ProtoReflect.Descriptor instead. func (*ListAvailableSolutionsRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{22} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{18} } func (x *ListAvailableSolutionsRequest) GetOrgId() string { @@ -2193,7 +1724,7 @@ type ListAvailableSolutionsResponse struct { func (x *ListAvailableSolutionsResponse) Reset() { *x = ListAvailableSolutionsResponse{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[23] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2205,7 +1736,7 @@ func (x *ListAvailableSolutionsResponse) String() string { func (*ListAvailableSolutionsResponse) ProtoMessage() {} func (x *ListAvailableSolutionsResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[23] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2218,7 +1749,7 @@ func (x *ListAvailableSolutionsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAvailableSolutionsResponse.ProtoReflect.Descriptor instead. func (*ListAvailableSolutionsResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{23} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{19} } func (x *ListAvailableSolutionsResponse) GetSolutions() []*AvailableSolution { @@ -2239,24 +1770,7 @@ var File_saas_accounts_v1_installations_proto protoreflect.FileDescriptor const file_saas_accounts_v1_installations_proto_rawDesc = "" + "\n" + - "$saas/accounts/v1/installations.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$saas/accounts/v1/authorization.proto\x1a\x1csaas/policy/v1/options.proto\"\x94\x05\n" + - "\fInstallation\x12\x18\n" + - "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id\x12\x1f\n" + - "\x06org_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x126\n" + - "\x12agent_principal_id\x18\x03 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x10agentPrincipalId\x126\n" + - "\x12owner_principal_id\x18\x05 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x10ownerPrincipalId\x123\n" + - "\x16co_owner_principal_ids\x18\x06 \x03(\tR\x13coOwnerPrincipalIds\x125\n" + - "\x12root_scope_node_id\x18\a \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0frootScopeNodeId\x12<\n" + - "\x06status\x18\b \x01(\x0e2$.saas.accounts.v1.InstallationStatusR\x06status\x129\n" + - "\n" + - "created_at\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12>\n" + - "\n" + - "revoked_at\x18\n" + - " \x01(\v2\x1a.google.protobuf.TimestampH\x00R\trevokedAt\x88\x01\x01\x12(\n" + - "\ttarget_id\x18\v \x01(\tB\v\xbaH\b\xd8\x01\x01r\x03\xb0\x01\x01R\btargetId\x12/\n" + - "\x0erevoked_reason\x18\f \x01(\tB\b\xbaH\x05r\x03\x18\x80\x04R\rrevokedReason\x12/\n" + - "\x0efailure_reason\x18\r \x01(\tB\b\xbaH\x05r\x03\x18\x80\x04R\rfailureReasonB\r\n" + - "\v_revoked_atJ\x04\b\x04\x10\x05R\x13solution_identifier\"\xf6\x04\n" + + "$saas/accounts/v1/installations.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a$saas/accounts/v1/authorization.proto\x1a+saas/accounts/v1/installation_listing.proto\x1a\x1csaas/policy/v1/options.proto\"\xf6\x04\n" + "\x16InstallSolutionRequest\x12\x1f\n" + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x125\n" + "\x10agent_identifier\x18\x02 \x01(\tB\n" + @@ -2292,20 +1806,7 @@ const file_saas_accounts_v1_installations_proto_rawDesc = "" + "\x0finstallation_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0einstallationId\"\x9b\x01\n" + "\x17GetInstallationResponse\x12B\n" + "\finstallation\x18\x01 \x01(\v2\x1e.saas.accounts.v1.InstallationR\finstallation\x12<\n" + - "\x06health\x18\x02 \x01(\x0e2$.saas.accounts.v1.InstallationHealthR\x06health\"\xc1\x01\n" + - "\x18ListInstallationsRequest\x12\x1f\n" + - "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x12<\n" + - "\x06status\x18\x02 \x01(\x0e2$.saas.accounts.v1.InstallationStatusR\x06status\x12'\n" + - "\tpage_size\x18\x03 \x01(\x05B\n" + - "\xbaH\a\x1a\x05\x18\xf4\x03(\x00R\bpageSize\x12\x1d\n" + - "\n" + - "page_token\x18\x04 \x01(\tR\tpageToken\"\x97\x01\n" + - "\x13InstallationSummary\x12B\n" + - "\finstallation\x18\x01 \x01(\v2\x1e.saas.accounts.v1.InstallationR\finstallation\x12<\n" + - "\x06health\x18\x02 \x01(\x0e2$.saas.accounts.v1.InstallationHealthR\x06health\"\x90\x01\n" + - "\x19ListInstallationsResponse\x12K\n" + - "\rinstallations\x18\x01 \x03(\v2%.saas.accounts.v1.InstallationSummaryR\rinstallations\x12&\n" + - "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\x8f\x01\n" + + "\x06health\x18\x02 \x01(\x0e2$.saas.accounts.v1.InstallationHealthR\x06health\"\x8f\x01\n" + "$ListOrganizationInstallationsRequest\x12\x1f\n" + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x12'\n" + "\tpage_size\x18\x02 \x01(\x05B\n" + @@ -2383,20 +1884,7 @@ const file_saas_accounts_v1_installations_proto_rawDesc = "" + "page_token\x18\x03 \x01(\tR\tpageToken\"\x8b\x01\n" + "\x1eListAvailableSolutionsResponse\x12A\n" + "\tsolutions\x18\x01 \x03(\v2#.saas.accounts.v1.AvailableSolutionR\tsolutions\x12&\n" + - "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken*\xbb\x01\n" + - "\x12InstallationStatus\x12#\n" + - "\x1fINSTALLATION_STATUS_UNSPECIFIED\x10\x00\x12\x1e\n" + - "\x1aINSTALLATION_STATUS_ACTIVE\x10\x01\x12\x1f\n" + - "\x1bINSTALLATION_STATUS_REVOKED\x10\x02\x12\x1f\n" + - "\x1bINSTALLATION_STATUS_PENDING\x10\x03\x12\x1e\n" + - "\x1aINSTALLATION_STATUS_FAILED\x10\x04*\x84\x02\n" + - "\x12InstallationHealth\x12#\n" + - "\x1fINSTALLATION_HEALTH_UNSPECIFIED\x10\x00\x12\x1f\n" + - "\x1bINSTALLATION_HEALTH_HEALTHY\x10\x01\x12)\n" + - "%INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER\x10\x02\x12%\n" + - "!INSTALLATION_HEALTH_AGENT_REVOKED\x10\x03\x12&\n" + - "\"INSTALLATION_HEALTH_AGENT_DISABLED\x10\x04\x12.\n" + - "*INSTALLATION_HEALTH_STANDING_GRANT_MISSING\x10\x05*\xf3\x03\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken*\xf3\x03\n" + "\x16ReadinessConditionKind\x12(\n" + "$READINESS_CONDITION_KIND_UNSPECIFIED\x10\x00\x121\n" + "-READINESS_CONDITION_KIND_INSTALL_STEP_PENDING\x10\x01\x120\n" + @@ -2464,96 +1952,86 @@ func file_saas_accounts_v1_installations_proto_rawDescGZIP() []byte { return file_saas_accounts_v1_installations_proto_rawDescData } -var file_saas_accounts_v1_installations_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_saas_accounts_v1_installations_proto_msgTypes = make([]protoimpl.MessageInfo, 24) +var file_saas_accounts_v1_installations_proto_enumTypes = make([]protoimpl.EnumInfo, 3) +var file_saas_accounts_v1_installations_proto_msgTypes = make([]protoimpl.MessageInfo, 20) var file_saas_accounts_v1_installations_proto_goTypes = []any{ - (InstallationStatus)(0), // 0: saas.accounts.v1.InstallationStatus - (InstallationHealth)(0), // 1: saas.accounts.v1.InstallationHealth - (ReadinessConditionKind)(0), // 2: saas.accounts.v1.ReadinessConditionKind - (ReadinessClearedBy)(0), // 3: saas.accounts.v1.ReadinessClearedBy - (ReadinessAction)(0), // 4: saas.accounts.v1.ReadinessAction - (*Installation)(nil), // 5: saas.accounts.v1.Installation - (*InstallSolutionRequest)(nil), // 6: saas.accounts.v1.InstallSolutionRequest - (*NarrowInstallationRequest)(nil), // 7: saas.accounts.v1.NarrowInstallationRequest - (*RetryInstallationRequest)(nil), // 8: saas.accounts.v1.RetryInstallationRequest - (*UninstallSolutionRequest)(nil), // 9: saas.accounts.v1.UninstallSolutionRequest - (*TransferInstallationOwnershipRequest)(nil), // 10: saas.accounts.v1.TransferInstallationOwnershipRequest - (*GetInstallationRequest)(nil), // 11: saas.accounts.v1.GetInstallationRequest - (*GetInstallationResponse)(nil), // 12: saas.accounts.v1.GetInstallationResponse - (*ListInstallationsRequest)(nil), // 13: saas.accounts.v1.ListInstallationsRequest - (*InstallationSummary)(nil), // 14: saas.accounts.v1.InstallationSummary - (*ListInstallationsResponse)(nil), // 15: saas.accounts.v1.ListInstallationsResponse - (*ListOrganizationInstallationsRequest)(nil), // 16: saas.accounts.v1.ListOrganizationInstallationsRequest - (*OrganizationInstallation)(nil), // 17: saas.accounts.v1.OrganizationInstallation - (*ReadinessCondition)(nil), // 18: saas.accounts.v1.ReadinessCondition - (*InstallationReadiness)(nil), // 19: saas.accounts.v1.InstallationReadiness - (*ReachedModule)(nil), // 20: saas.accounts.v1.ReachedModule - (*ListOrganizationInstallationsResponse)(nil), // 21: saas.accounts.v1.ListOrganizationInstallationsResponse - (*GrantInstallationToTeamRequest)(nil), // 22: saas.accounts.v1.GrantInstallationToTeamRequest - (*CarriedModuleAccess)(nil), // 23: saas.accounts.v1.CarriedModuleAccess - (*GrantInstallationToTeamResponse)(nil), // 24: saas.accounts.v1.GrantInstallationToTeamResponse - (*RevokeInstallationFromTeamRequest)(nil), // 25: saas.accounts.v1.RevokeInstallationFromTeamRequest - (*AvailableSolution)(nil), // 26: saas.accounts.v1.AvailableSolution - (*ListAvailableSolutionsRequest)(nil), // 27: saas.accounts.v1.ListAvailableSolutionsRequest - (*ListAvailableSolutionsResponse)(nil), // 28: saas.accounts.v1.ListAvailableSolutionsResponse - (*timestamppb.Timestamp)(nil), // 29: google.protobuf.Timestamp - (*CollectionReadGrant)(nil), // 30: saas.accounts.v1.CollectionReadGrant - (*ScopeGrant)(nil), // 31: saas.accounts.v1.ScopeGrant - (*emptypb.Empty)(nil), // 32: google.protobuf.Empty + (ReadinessConditionKind)(0), // 0: saas.accounts.v1.ReadinessConditionKind + (ReadinessClearedBy)(0), // 1: saas.accounts.v1.ReadinessClearedBy + (ReadinessAction)(0), // 2: saas.accounts.v1.ReadinessAction + (*InstallSolutionRequest)(nil), // 3: saas.accounts.v1.InstallSolutionRequest + (*NarrowInstallationRequest)(nil), // 4: saas.accounts.v1.NarrowInstallationRequest + (*RetryInstallationRequest)(nil), // 5: saas.accounts.v1.RetryInstallationRequest + (*UninstallSolutionRequest)(nil), // 6: saas.accounts.v1.UninstallSolutionRequest + (*TransferInstallationOwnershipRequest)(nil), // 7: saas.accounts.v1.TransferInstallationOwnershipRequest + (*GetInstallationRequest)(nil), // 8: saas.accounts.v1.GetInstallationRequest + (*GetInstallationResponse)(nil), // 9: saas.accounts.v1.GetInstallationResponse + (*ListOrganizationInstallationsRequest)(nil), // 10: saas.accounts.v1.ListOrganizationInstallationsRequest + (*OrganizationInstallation)(nil), // 11: saas.accounts.v1.OrganizationInstallation + (*ReadinessCondition)(nil), // 12: saas.accounts.v1.ReadinessCondition + (*InstallationReadiness)(nil), // 13: saas.accounts.v1.InstallationReadiness + (*ReachedModule)(nil), // 14: saas.accounts.v1.ReachedModule + (*ListOrganizationInstallationsResponse)(nil), // 15: saas.accounts.v1.ListOrganizationInstallationsResponse + (*GrantInstallationToTeamRequest)(nil), // 16: saas.accounts.v1.GrantInstallationToTeamRequest + (*CarriedModuleAccess)(nil), // 17: saas.accounts.v1.CarriedModuleAccess + (*GrantInstallationToTeamResponse)(nil), // 18: saas.accounts.v1.GrantInstallationToTeamResponse + (*RevokeInstallationFromTeamRequest)(nil), // 19: saas.accounts.v1.RevokeInstallationFromTeamRequest + (*AvailableSolution)(nil), // 20: saas.accounts.v1.AvailableSolution + (*ListAvailableSolutionsRequest)(nil), // 21: saas.accounts.v1.ListAvailableSolutionsRequest + (*ListAvailableSolutionsResponse)(nil), // 22: saas.accounts.v1.ListAvailableSolutionsResponse + (*Installation)(nil), // 23: saas.accounts.v1.Installation + (InstallationHealth)(0), // 24: saas.accounts.v1.InstallationHealth + (*CollectionReadGrant)(nil), // 25: saas.accounts.v1.CollectionReadGrant + (*ScopeGrant)(nil), // 26: saas.accounts.v1.ScopeGrant + (*ListInstallationsRequest)(nil), // 27: saas.accounts.v1.ListInstallationsRequest + (*emptypb.Empty)(nil), // 28: google.protobuf.Empty + (*ListInstallationsResponse)(nil), // 29: saas.accounts.v1.ListInstallationsResponse } var file_saas_accounts_v1_installations_proto_depIdxs = []int32{ - 0, // 0: saas.accounts.v1.Installation.status:type_name -> saas.accounts.v1.InstallationStatus - 29, // 1: saas.accounts.v1.Installation.created_at:type_name -> google.protobuf.Timestamp - 29, // 2: saas.accounts.v1.Installation.revoked_at:type_name -> google.protobuf.Timestamp - 5, // 3: saas.accounts.v1.GetInstallationResponse.installation:type_name -> saas.accounts.v1.Installation - 1, // 4: saas.accounts.v1.GetInstallationResponse.health:type_name -> saas.accounts.v1.InstallationHealth - 0, // 5: saas.accounts.v1.ListInstallationsRequest.status:type_name -> saas.accounts.v1.InstallationStatus - 5, // 6: saas.accounts.v1.InstallationSummary.installation:type_name -> saas.accounts.v1.Installation - 1, // 7: saas.accounts.v1.InstallationSummary.health:type_name -> saas.accounts.v1.InstallationHealth - 14, // 8: saas.accounts.v1.ListInstallationsResponse.installations:type_name -> saas.accounts.v1.InstallationSummary - 5, // 9: saas.accounts.v1.OrganizationInstallation.installation:type_name -> saas.accounts.v1.Installation - 1, // 10: saas.accounts.v1.OrganizationInstallation.health:type_name -> saas.accounts.v1.InstallationHealth - 26, // 11: saas.accounts.v1.OrganizationInstallation.target:type_name -> saas.accounts.v1.AvailableSolution - 30, // 12: saas.accounts.v1.OrganizationInstallation.granted_teams:type_name -> saas.accounts.v1.CollectionReadGrant - 30, // 13: saas.accounts.v1.OrganizationInstallation.inherited_teams:type_name -> saas.accounts.v1.CollectionReadGrant - 19, // 14: saas.accounts.v1.OrganizationInstallation.readiness:type_name -> saas.accounts.v1.InstallationReadiness - 20, // 15: saas.accounts.v1.OrganizationInstallation.reached_modules:type_name -> saas.accounts.v1.ReachedModule - 2, // 16: saas.accounts.v1.ReadinessCondition.kind:type_name -> saas.accounts.v1.ReadinessConditionKind - 3, // 17: saas.accounts.v1.ReadinessCondition.cleared_by:type_name -> saas.accounts.v1.ReadinessClearedBy - 4, // 18: saas.accounts.v1.ReadinessCondition.action:type_name -> saas.accounts.v1.ReadinessAction - 18, // 19: saas.accounts.v1.InstallationReadiness.unmet:type_name -> saas.accounts.v1.ReadinessCondition - 17, // 20: saas.accounts.v1.ListOrganizationInstallationsResponse.installations:type_name -> saas.accounts.v1.OrganizationInstallation - 31, // 21: saas.accounts.v1.GrantInstallationToTeamResponse.grant:type_name -> saas.accounts.v1.ScopeGrant - 23, // 22: saas.accounts.v1.GrantInstallationToTeamResponse.carried:type_name -> saas.accounts.v1.CarriedModuleAccess - 18, // 23: saas.accounts.v1.GrantInstallationToTeamResponse.unmet:type_name -> saas.accounts.v1.ReadinessCondition - 26, // 24: saas.accounts.v1.ListAvailableSolutionsResponse.solutions:type_name -> saas.accounts.v1.AvailableSolution - 6, // 25: saas.accounts.v1.InstallationService.InstallSolution:input_type -> saas.accounts.v1.InstallSolutionRequest - 9, // 26: saas.accounts.v1.InstallationService.UninstallSolution:input_type -> saas.accounts.v1.UninstallSolutionRequest - 7, // 27: saas.accounts.v1.InstallationService.NarrowInstallation:input_type -> saas.accounts.v1.NarrowInstallationRequest - 8, // 28: saas.accounts.v1.InstallationService.RetryInstallation:input_type -> saas.accounts.v1.RetryInstallationRequest - 10, // 29: saas.accounts.v1.InstallationService.TransferInstallationOwnership:input_type -> saas.accounts.v1.TransferInstallationOwnershipRequest - 11, // 30: saas.accounts.v1.InstallationService.GetInstallation:input_type -> saas.accounts.v1.GetInstallationRequest - 13, // 31: saas.accounts.v1.InstallationService.ListInstallations:input_type -> saas.accounts.v1.ListInstallationsRequest - 27, // 32: saas.accounts.v1.InstallationService.ListAvailableSolutions:input_type -> saas.accounts.v1.ListAvailableSolutionsRequest - 16, // 33: saas.accounts.v1.InstallationService.ListOrganizationInstallations:input_type -> saas.accounts.v1.ListOrganizationInstallationsRequest - 22, // 34: saas.accounts.v1.InstallationService.GrantInstallationToTeam:input_type -> saas.accounts.v1.GrantInstallationToTeamRequest - 25, // 35: saas.accounts.v1.InstallationService.RevokeInstallationFromTeam:input_type -> saas.accounts.v1.RevokeInstallationFromTeamRequest - 5, // 36: saas.accounts.v1.InstallationService.InstallSolution:output_type -> saas.accounts.v1.Installation - 32, // 37: saas.accounts.v1.InstallationService.UninstallSolution:output_type -> google.protobuf.Empty - 32, // 38: saas.accounts.v1.InstallationService.NarrowInstallation:output_type -> google.protobuf.Empty - 5, // 39: saas.accounts.v1.InstallationService.RetryInstallation:output_type -> saas.accounts.v1.Installation - 5, // 40: saas.accounts.v1.InstallationService.TransferInstallationOwnership:output_type -> saas.accounts.v1.Installation - 12, // 41: saas.accounts.v1.InstallationService.GetInstallation:output_type -> saas.accounts.v1.GetInstallationResponse - 15, // 42: saas.accounts.v1.InstallationService.ListInstallations:output_type -> saas.accounts.v1.ListInstallationsResponse - 28, // 43: saas.accounts.v1.InstallationService.ListAvailableSolutions:output_type -> saas.accounts.v1.ListAvailableSolutionsResponse - 21, // 44: saas.accounts.v1.InstallationService.ListOrganizationInstallations:output_type -> saas.accounts.v1.ListOrganizationInstallationsResponse - 24, // 45: saas.accounts.v1.InstallationService.GrantInstallationToTeam:output_type -> saas.accounts.v1.GrantInstallationToTeamResponse - 32, // 46: saas.accounts.v1.InstallationService.RevokeInstallationFromTeam:output_type -> google.protobuf.Empty - 36, // [36:47] is the sub-list for method output_type - 25, // [25:36] is the sub-list for method input_type - 25, // [25:25] is the sub-list for extension type_name - 25, // [25:25] is the sub-list for extension extendee - 0, // [0:25] is the sub-list for field type_name + 23, // 0: saas.accounts.v1.GetInstallationResponse.installation:type_name -> saas.accounts.v1.Installation + 24, // 1: saas.accounts.v1.GetInstallationResponse.health:type_name -> saas.accounts.v1.InstallationHealth + 23, // 2: saas.accounts.v1.OrganizationInstallation.installation:type_name -> saas.accounts.v1.Installation + 24, // 3: saas.accounts.v1.OrganizationInstallation.health:type_name -> saas.accounts.v1.InstallationHealth + 20, // 4: saas.accounts.v1.OrganizationInstallation.target:type_name -> saas.accounts.v1.AvailableSolution + 25, // 5: saas.accounts.v1.OrganizationInstallation.granted_teams:type_name -> saas.accounts.v1.CollectionReadGrant + 25, // 6: saas.accounts.v1.OrganizationInstallation.inherited_teams:type_name -> saas.accounts.v1.CollectionReadGrant + 13, // 7: saas.accounts.v1.OrganizationInstallation.readiness:type_name -> saas.accounts.v1.InstallationReadiness + 14, // 8: saas.accounts.v1.OrganizationInstallation.reached_modules:type_name -> saas.accounts.v1.ReachedModule + 0, // 9: saas.accounts.v1.ReadinessCondition.kind:type_name -> saas.accounts.v1.ReadinessConditionKind + 1, // 10: saas.accounts.v1.ReadinessCondition.cleared_by:type_name -> saas.accounts.v1.ReadinessClearedBy + 2, // 11: saas.accounts.v1.ReadinessCondition.action:type_name -> saas.accounts.v1.ReadinessAction + 12, // 12: saas.accounts.v1.InstallationReadiness.unmet:type_name -> saas.accounts.v1.ReadinessCondition + 11, // 13: saas.accounts.v1.ListOrganizationInstallationsResponse.installations:type_name -> saas.accounts.v1.OrganizationInstallation + 26, // 14: saas.accounts.v1.GrantInstallationToTeamResponse.grant:type_name -> saas.accounts.v1.ScopeGrant + 17, // 15: saas.accounts.v1.GrantInstallationToTeamResponse.carried:type_name -> saas.accounts.v1.CarriedModuleAccess + 12, // 16: saas.accounts.v1.GrantInstallationToTeamResponse.unmet:type_name -> saas.accounts.v1.ReadinessCondition + 20, // 17: saas.accounts.v1.ListAvailableSolutionsResponse.solutions:type_name -> saas.accounts.v1.AvailableSolution + 3, // 18: saas.accounts.v1.InstallationService.InstallSolution:input_type -> saas.accounts.v1.InstallSolutionRequest + 6, // 19: saas.accounts.v1.InstallationService.UninstallSolution:input_type -> saas.accounts.v1.UninstallSolutionRequest + 4, // 20: saas.accounts.v1.InstallationService.NarrowInstallation:input_type -> saas.accounts.v1.NarrowInstallationRequest + 5, // 21: saas.accounts.v1.InstallationService.RetryInstallation:input_type -> saas.accounts.v1.RetryInstallationRequest + 7, // 22: saas.accounts.v1.InstallationService.TransferInstallationOwnership:input_type -> saas.accounts.v1.TransferInstallationOwnershipRequest + 8, // 23: saas.accounts.v1.InstallationService.GetInstallation:input_type -> saas.accounts.v1.GetInstallationRequest + 27, // 24: saas.accounts.v1.InstallationService.ListInstallations:input_type -> saas.accounts.v1.ListInstallationsRequest + 21, // 25: saas.accounts.v1.InstallationService.ListAvailableSolutions:input_type -> saas.accounts.v1.ListAvailableSolutionsRequest + 10, // 26: saas.accounts.v1.InstallationService.ListOrganizationInstallations:input_type -> saas.accounts.v1.ListOrganizationInstallationsRequest + 16, // 27: saas.accounts.v1.InstallationService.GrantInstallationToTeam:input_type -> saas.accounts.v1.GrantInstallationToTeamRequest + 19, // 28: saas.accounts.v1.InstallationService.RevokeInstallationFromTeam:input_type -> saas.accounts.v1.RevokeInstallationFromTeamRequest + 23, // 29: saas.accounts.v1.InstallationService.InstallSolution:output_type -> saas.accounts.v1.Installation + 28, // 30: saas.accounts.v1.InstallationService.UninstallSolution:output_type -> google.protobuf.Empty + 28, // 31: saas.accounts.v1.InstallationService.NarrowInstallation:output_type -> google.protobuf.Empty + 23, // 32: saas.accounts.v1.InstallationService.RetryInstallation:output_type -> saas.accounts.v1.Installation + 23, // 33: saas.accounts.v1.InstallationService.TransferInstallationOwnership:output_type -> saas.accounts.v1.Installation + 9, // 34: saas.accounts.v1.InstallationService.GetInstallation:output_type -> saas.accounts.v1.GetInstallationResponse + 29, // 35: saas.accounts.v1.InstallationService.ListInstallations:output_type -> saas.accounts.v1.ListInstallationsResponse + 22, // 36: saas.accounts.v1.InstallationService.ListAvailableSolutions:output_type -> saas.accounts.v1.ListAvailableSolutionsResponse + 15, // 37: saas.accounts.v1.InstallationService.ListOrganizationInstallations:output_type -> saas.accounts.v1.ListOrganizationInstallationsResponse + 18, // 38: saas.accounts.v1.InstallationService.GrantInstallationToTeam:output_type -> saas.accounts.v1.GrantInstallationToTeamResponse + 28, // 39: saas.accounts.v1.InstallationService.RevokeInstallationFromTeam:output_type -> google.protobuf.Empty + 29, // [29:40] is the sub-list for method output_type + 18, // [18:29] is the sub-list for method input_type + 18, // [18:18] is the sub-list for extension type_name + 18, // [18:18] is the sub-list for extension extendee + 0, // [0:18] is the sub-list for field type_name } func init() { file_saas_accounts_v1_installations_proto_init() } @@ -2562,15 +2040,15 @@ func file_saas_accounts_v1_installations_proto_init() { return } file_saas_accounts_v1_authorization_proto_init() + file_saas_accounts_v1_installation_listing_proto_init() file_saas_accounts_v1_installations_proto_msgTypes[0].OneofWrappers = []any{} - file_saas_accounts_v1_installations_proto_msgTypes[1].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_installations_proto_rawDesc), len(file_saas_accounts_v1_installations_proto_rawDesc)), - NumEnums: 5, - NumMessages: 24, + NumEnums: 3, + NumMessages: 20, NumExtensions: 0, NumServices: 1, }, diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations_grpc.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations_grpc.pb.go index cee20fabe..25f190592 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations_grpc.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations_grpc.pb.go @@ -70,11 +70,15 @@ type InstallationServiceClient interface { // ListInstallations enumerates one organization's installations with their live // health. It is internal-tier on purpose: it takes the organization as a request // field, so only a caller holding the cluster-internal credential may ask, and - // the tenant it asks about is the one the auth-gateway projected from a verified - // identity — never one a browser supplied. It sits beside - // PermissionService.ListAccessibleScopes for that reason: the two are read - // together to answer what a viewer's organization installed and what that viewer - // was granted. + // the tenant it asks about is never one a browser supplied. The auth-gateway is + // its caller, for two reads. Its viewer projection asks about the tenant it + // projected from a verified identity; it sits beside + // PermissionService.ListAccessibleScopes for that reason, the two read together + // to answer what a viewer's organization installed and what that viewer was + // granted. Its declared module route's bearer-less arm asks about the tenant an + // edge-verified Work Context names, a value this issuer signed, to admit a + // headless operation context only when the target is installed there and the + // context is sealed to another active installation. ListInstallations(ctx context.Context, in *ListInstallationsRequest, opts ...grpc.CallOption) (*ListInstallationsResponse, error) // ListAvailableSolutions is the catalogue: the solution targets an // administrator may install right now. @@ -99,7 +103,8 @@ type InstallationServiceClient interface { // It is the org-admin companion to the internal ListInstallations, and it is a // separate method rather than a relaxed policy on that one: ListInstallations // is read by the gateway for an organisation it projected from a verified - // identity, and a bare tenant credential must never reach it. This one binds + // identity or that an edge-verified Work Context names, and a bare tenant + // credential must never reach it. This one binds // the organisation to the caller's own admin membership, exactly as // ListAvailableSolutions does, so no organisation's administrator can read // another's. The one exception is inherited, not added here: a platform @@ -283,11 +288,15 @@ type InstallationServiceServer interface { // ListInstallations enumerates one organization's installations with their live // health. It is internal-tier on purpose: it takes the organization as a request // field, so only a caller holding the cluster-internal credential may ask, and - // the tenant it asks about is the one the auth-gateway projected from a verified - // identity — never one a browser supplied. It sits beside - // PermissionService.ListAccessibleScopes for that reason: the two are read - // together to answer what a viewer's organization installed and what that viewer - // was granted. + // the tenant it asks about is never one a browser supplied. The auth-gateway is + // its caller, for two reads. Its viewer projection asks about the tenant it + // projected from a verified identity; it sits beside + // PermissionService.ListAccessibleScopes for that reason, the two read together + // to answer what a viewer's organization installed and what that viewer was + // granted. Its declared module route's bearer-less arm asks about the tenant an + // edge-verified Work Context names, a value this issuer signed, to admit a + // headless operation context only when the target is installed there and the + // context is sealed to another active installation. ListInstallations(context.Context, *ListInstallationsRequest) (*ListInstallationsResponse, error) // ListAvailableSolutions is the catalogue: the solution targets an // administrator may install right now. @@ -312,7 +321,8 @@ type InstallationServiceServer interface { // It is the org-admin companion to the internal ListInstallations, and it is a // separate method rather than a relaxed policy on that one: ListInstallations // is read by the gateway for an organisation it projected from a verified - // identity, and a bare tenant credential must never reach it. This one binds + // identity or that an edge-verified Work Context names, and a bare tenant + // credential must never reach it. This one binds // the organisation to the caller's own admin membership, exactly as // ListAvailableSolutions does, so no organisation's administrator can read // another's. The one exception is inherited, not added here: a platform diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go index 2eff4d354..f0caf2fe3 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go @@ -3861,7 +3861,7 @@ var File_saas_accounts_v1_platform_admin_proto protoreflect.FileDescriptor const file_saas_accounts_v1_platform_admin_proto_rawDesc = "" + "\n" + - "%saas/accounts/v1/platform_admin.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$saas/accounts/v1/authorization.proto\x1a\x1dsaas/accounts/v1/common.proto\x1a$saas/accounts/v1/installations.proto\x1a(saas/accounts/v1/solution_registry.proto\x1a\x1fsaas/events/v1/operations.proto\x1a\x17saas/jobs/v1/jobs.proto\x1a\x1csaas/policy/v1/options.proto\"q\n" + + "%saas/accounts/v1/platform_admin.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$saas/accounts/v1/authorization.proto\x1a\x1dsaas/accounts/v1/common.proto\x1a+saas/accounts/v1/installation_listing.proto\x1a(saas/accounts/v1/solution_registry.proto\x1a\x1fsaas/events/v1/operations.proto\x1a\x17saas/jobs/v1/jobs.proto\x1a\x1csaas/policy/v1/options.proto\"q\n" + "\x12SearchUsersRequest\x12\x14\n" + "\x05query\x18\x01 \x01(\tR\x05query\x12&\n" + "\tpage_size\x18\x02 \x01(\x05B\t\xbaH\x06\x1a\x04\x18d \x00R\bpageSize\x12\x1d\n" + @@ -4380,7 +4380,7 @@ func file_saas_accounts_v1_platform_admin_proto_init() { } file_saas_accounts_v1_authorization_proto_init() file_saas_accounts_v1_common_proto_init() - file_saas_accounts_v1_installations_proto_init() + file_saas_accounts_v1_installation_listing_proto_init() file_saas_accounts_v1_solution_registry_proto_init() file_saas_accounts_v1_platform_admin_proto_msgTypes[34].OneofWrappers = []any{ (*CatalogueDesired_DeclaredRevision)(nil), diff --git a/module/services/accounts/generated/openapi-raw/api.swagger.json b/module/services/accounts/generated/openapi-raw/api.swagger.json index 92a8f0b83..595d4790d 100644 --- a/module/services/accounts/generated/openapi-raw/api.swagger.json +++ b/module/services/accounts/generated/openapi-raw/api.swagger.json @@ -1733,7 +1733,7 @@ "/v1/installations": { "get": { "summary": "ListOrganizationInstallations is an organisation administrator's view of\nwhat that organisation has installed: each active installation with its live\nhealth, the catalogue row of the target it installed, the scope path of its\nauthority root, and the teams a grant reaches there.", - "description": "It is the org-admin companion to the internal ListInstallations, and it is a\nseparate method rather than a relaxed policy on that one: ListInstallations\nis read by the gateway for an organisation it projected from a verified\nidentity, and a bare tenant credential must never reach it. This one binds\nthe organisation to the caller's own admin membership, exactly as\nListAvailableSolutions does, so no organisation's administrator can read\nanother's. The one exception is inherited, not added here: a platform\nsuper administrator (not impersonating) passes the organisation-admin check\nfor every organisation, as on every other organisation-admin method, and so\nmay list any organisation's installations.", + "description": "It is the org-admin companion to the internal ListInstallations, and it is a\nseparate method rather than a relaxed policy on that one: ListInstallations\nis read by the gateway for an organisation it projected from a verified\nidentity or that an edge-verified Work Context names, and a bare tenant\ncredential must never reach it. This one binds\nthe organisation to the caller's own admin membership, exactly as\nListAvailableSolutions does, so no organisation's administrator can read\nanother's. The one exception is inherited, not added here: a platform\nsuper administrator (not impersonating) passes the organisation-admin check\nfor every organisation, as on every other organisation-admin method, and so\nmay list any organisation's installations.", "operationId": "InstallationService_ListOrganizationInstallations", "responses": { "200": { diff --git a/module/services/accounts/openapi/api.swagger.json b/module/services/accounts/openapi/api.swagger.json index 9fb8507fd..a24754824 100644 --- a/module/services/accounts/openapi/api.swagger.json +++ b/module/services/accounts/openapi/api.swagger.json @@ -7756,7 +7756,7 @@ }, "/v1/installations": { "get": { - "description": "It is the org-admin companion to the internal ListInstallations, and it is a\nseparate method rather than a relaxed policy on that one: ListInstallations\nis read by the gateway for an organisation it projected from a verified\nidentity, and a bare tenant credential must never reach it. This one binds\nthe organisation to the caller's own admin membership, exactly as\nListAvailableSolutions does, so no organisation's administrator can read\nanother's. The one exception is inherited, not added here: a platform\nsuper administrator (not impersonating) passes the organisation-admin check\nfor every organisation, as on every other organisation-admin method, and so\nmay list any organisation's installations.", + "description": "It is the org-admin companion to the internal ListInstallations, and it is a\nseparate method rather than a relaxed policy on that one: ListInstallations\nis read by the gateway for an organisation it projected from a verified\nidentity or that an edge-verified Work Context names, and a bare tenant\ncredential must never reach it. This one binds\nthe organisation to the caller's own admin membership, exactly as\nListAvailableSolutions does, so no organisation's administrator can read\nanother's. The one exception is inherited, not added here: a platform\nsuper administrator (not impersonating) passes the organisation-admin check\nfor every organisation, as on every other organisation-admin method, and so\nmay list any organisation's installations.", "operationId": "InstallationService_ListOrganizationInstallations", "parameters": [ { diff --git a/module/services/accounts/proto/buf.yaml b/module/services/accounts/proto/buf.yaml index 509da1b1f..1a1cfc5ca 100644 --- a/module/services/accounts/proto/buf.yaml +++ b/module/services/accounts/proto/buf.yaml @@ -94,12 +94,23 @@ breaking: # SolutionBackendBinding (#1118): the reported upstream half, deleted # with the report — a solution's address is resolved, never recorded. - saas/accounts/v1/solution_registry.proto + # Installation, ListInstallationsRequest, InstallationSummary and + # ListInstallationsResponse moved to installation_listing.proto so the + # auth-gateway can generate the ListInstallations messages from the same + # source file without vendoring the policy options and google.api this + # file imports. Same package, same fully-qualified names, same wire + # format; every RPC stays where it was. + - saas/accounts/v1/installations.proto # ModuleMintRegistrationRequest/Response and # SolutionMintRegistrationRequest/Response (#1056): the registration # credentials and the secrets that minted them are deleted. Presence is # delivered, so nothing registers at runtime and nothing verifies either # credential. - saas/accounts/v1/module_registration.proto + # InstallationStatus and InstallationHealth moved to + # installation_listing.proto with the messages above, for the same reason. + ENUM_NO_DELETE: + - saas/accounts/v1/installations.proto # ListMyAccessibleScopes moved from PermissionService to the new # AccessibleScopeService in accessible_scopes.proto (issue #577), which changes # its Connect/gRPC procedure path. Its REST route (GET /v1/accessible-scopes) is diff --git a/module/services/accounts/proto/saas/accounts/v1/installation_listing.proto b/module/services/accounts/proto/saas/accounts/v1/installation_listing.proto new file mode 100644 index 000000000..57150f92b --- /dev/null +++ b/module/services/accounts/proto/saas/accounts/v1/installation_listing.proto @@ -0,0 +1,131 @@ +syntax = "proto3"; + +package saas.accounts.v1; + +import "buf/validate/validate.proto"; +import "google/protobuf/timestamp.proto"; + +// The installation listing: an installation, its lifecycle and health, and the +// messages InstallationService/ListInstallations takes and answers. +// +// These definitions live in their own file, with deliberately minimal imports, +// so the auth-gateway can generate the same types from the same source proto +// and call ListInstallations over the internal listener, as it does for +// module_registration.proto and solution_entitlements.proto. installations.proto +// imports this file; the service and every other installation message stay +// there. Sharing the generated definition rather than restating the wire shape +// is what keeps the two sides from drifting: the gateway's copy is this file, +// byte for byte. + +// InstallationStatus is the lifecycle of an installation row. An uninstall is a +// soft delete (status=revoked, revoked_at set) so the agent, grants, and audit +// trail remain resolvable. +enum InstallationStatus { + INSTALLATION_STATUS_UNSPECIFIED = 0; + INSTALLATION_STATUS_ACTIVE = 1; + INSTALLATION_STATUS_REVOKED = 2; + // Installed, and waiting for the solution to acknowledge its standard install + // step. Nothing can use a pending installation. + INSTALLATION_STATUS_PENDING = 3; + // The solution refused its install step, or never answered within the + // attempt budget. Nothing can use it; an administrator may retry it. + INSTALLATION_STATUS_FAILED = 4; +} + +// InstallationHealth is resolved live from current authority facts, never +// stored. It is the observable form of the fail-closed mint rule: a +// StartInstallationTask fails exactly when health is not HEALTHY, and transfer +// or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record +// nor any co-owner is currently an org admin (the Zapier-class "connection owner +// left the org" failure), surfaced rather than silently reassigned. +enum InstallationHealth { + INSTALLATION_HEALTH_UNSPECIFIED = 0; + INSTALLATION_HEALTH_HEALTHY = 1; + INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER = 2; + INSTALLATION_HEALTH_AGENT_REVOKED = 3; + INSTALLATION_HEALTH_AGENT_DISABLED = 4; + INSTALLATION_HEALTH_STANDING_GRANT_MISSING = 5; +} + +// Installation is one solution's bound identity in an org. Server-assigned ids; +// the browser cannot attest to tenancy, the agent principal, or the scope node. +message Installation { + string id = 1 [(buf.validate.field).string.uuid = true]; + string org_id = 2 [(buf.validate.field).string.uuid = true]; + string agent_principal_id = 3 [(buf.validate.field).string.uuid = true]; + // Was `solution_identifier`: the free-text route alias. It was the join key + // every reader used, and because an alias is deliberately reusable by a later + // binding, an installation that named one inherited whatever took it next. + // Replaced by target_id below. Reserved rather than reused so no consumer can + // read an alias out of a field that now carries an identity. + reserved 4; + reserved "solution_identifier"; + string owner_principal_id = 5 [(buf.validate.field).string.uuid = true]; + repeated string co_owner_principal_ids = 6; + string root_scope_node_id = 7 [(buf.validate.field).string.uuid = true]; + InstallationStatus status = 8; + google.protobuf.Timestamp created_at = 9; + optional google.protobuf.Timestamp revoked_at = 10; + // The immutable solution target this installation was consented to: one + // continuous period of one binding's presence on this host. It is never + // reused, so a replacement binding — even one claiming the same route alias — + // is a different target and inherits nothing. + // + // Deliberately NOT accompanied by the route alias. The alias is a property of + // the target that a later generation may move, and resolving it here would + // mean joining the host's presence state into a request-path listing — which + // would require granting request traffic read over every organisation's + // presence to answer about one. A consumer that needs the alias asks + // ListAvailableSolutions, which serves accepted applied state, and joins on + // this id. + // + // Empty only on a revoked historical row whose target could not be + // established at the cutover; an active installation always names one. + string target_id = 11 [ + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + (buf.validate.field).string.uuid = true + ]; + // Why a revoked installation was revoked, when it was not an administrator + // uninstalling: the installation-identity cutover, or a tenancy narrowing that + // withdrew this organisation. Empty on an active row. + string revoked_reason = 12 [(buf.validate.field).string.max_len = 512]; + // Why the install step failed: the solution's bounded refusal reason, or a + // fixed reason when it never answered. Empty unless status is FAILED. + string failure_reason = 13 [(buf.validate.field).string.max_len = 512]; +} + +// ListInstallationsRequest asks what one organization has installed. Every other +// RPC on this service names a single installation, so until this existed nothing +// could answer "what has this org installed?" — which is the authority read a +// per-organization solution projection is built on (issue #949). +message ListInstallationsRequest { + string org_id = 1 [(buf.validate.field).string.uuid = true]; + // Restrict to one lifecycle status. UNSPECIFIED returns every status, so a + // caller that must see only live installs asks for ACTIVE explicitly rather + // than leaning on a default that would silently widen if the enum grew. + InstallationStatus status = 2; + // Page bound. 0 -> server default; capped so an organization with many + // installations can never return an unbounded result in one call. + int32 page_size = 3 [(buf.validate.field).int32 = { + gte: 0 + lte: 500 + }]; + // Opaque cursor from a previous response's next_page_token; empty for page one. + string page_token = 4; +} + +// InstallationSummary is one row of the listing: the installation, plus the health +// resolved live beside it exactly as GetInstallation resolves it for one. Health +// travels with the row because a consumer that renders an installed solution as +// unavailable would otherwise need one GetInstallation per installation. +message InstallationSummary { + Installation installation = 1; + InstallationHealth health = 2; +} + +message ListInstallationsResponse { + repeated InstallationSummary installations = 1; + // Set when more installations remain; pass it back as page_token. Empty on the + // last page. + string next_page_token = 2; +} diff --git a/module/services/accounts/proto/saas/accounts/v1/installations.proto b/module/services/accounts/proto/saas/accounts/v1/installations.proto index 82739f9de..1149a82cf 100644 --- a/module/services/accounts/proto/saas/accounts/v1/installations.proto +++ b/module/services/accounts/proto/saas/accounts/v1/installations.proto @@ -5,8 +5,8 @@ package saas.accounts.v1; import "buf/validate/validate.proto"; import "google/api/annotations.proto"; import "google/protobuf/empty.proto"; -import "google/protobuf/timestamp.proto"; import "saas/accounts/v1/authorization.proto"; +import "saas/accounts/v1/installation_listing.proto"; import "saas/policy/v1/options.proto"; // ============================================================================ @@ -22,83 +22,6 @@ import "saas/policy/v1/options.proto"; // (StartInstallationTask) beside the other mints. // ============================================================================ -// InstallationStatus is the lifecycle of an installation row. An uninstall is a -// soft delete (status=revoked, revoked_at set) so the agent, grants, and audit -// trail remain resolvable. -enum InstallationStatus { - INSTALLATION_STATUS_UNSPECIFIED = 0; - INSTALLATION_STATUS_ACTIVE = 1; - INSTALLATION_STATUS_REVOKED = 2; - // Installed, and waiting for the solution to acknowledge its standard install - // step. Nothing can use a pending installation. - INSTALLATION_STATUS_PENDING = 3; - // The solution refused its install step, or never answered within the - // attempt budget. Nothing can use it; an administrator may retry it. - INSTALLATION_STATUS_FAILED = 4; -} - -// InstallationHealth is resolved live from current authority facts, never -// stored. It is the observable form of the fail-closed mint rule: a -// StartInstallationTask fails exactly when health is not HEALTHY, and transfer -// or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record -// nor any co-owner is currently an org admin (the Zapier-class "connection owner -// left the org" failure), surfaced rather than silently reassigned. -enum InstallationHealth { - INSTALLATION_HEALTH_UNSPECIFIED = 0; - INSTALLATION_HEALTH_HEALTHY = 1; - INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER = 2; - INSTALLATION_HEALTH_AGENT_REVOKED = 3; - INSTALLATION_HEALTH_AGENT_DISABLED = 4; - INSTALLATION_HEALTH_STANDING_GRANT_MISSING = 5; -} - -// Installation is one solution's bound identity in an org. Server-assigned ids; -// the browser cannot attest to tenancy, the agent principal, or the scope node. -message Installation { - string id = 1 [(buf.validate.field).string.uuid = true]; - string org_id = 2 [(buf.validate.field).string.uuid = true]; - string agent_principal_id = 3 [(buf.validate.field).string.uuid = true]; - // Was `solution_identifier`: the free-text route alias. It was the join key - // every reader used, and because an alias is deliberately reusable by a later - // binding, an installation that named one inherited whatever took it next. - // Replaced by target_id below. Reserved rather than reused so no consumer can - // read an alias out of a field that now carries an identity. - reserved 4; - reserved "solution_identifier"; - string owner_principal_id = 5 [(buf.validate.field).string.uuid = true]; - repeated string co_owner_principal_ids = 6; - string root_scope_node_id = 7 [(buf.validate.field).string.uuid = true]; - InstallationStatus status = 8; - google.protobuf.Timestamp created_at = 9; - optional google.protobuf.Timestamp revoked_at = 10; - // The immutable solution target this installation was consented to: one - // continuous period of one binding's presence on this host. It is never - // reused, so a replacement binding — even one claiming the same route alias — - // is a different target and inherits nothing. - // - // Deliberately NOT accompanied by the route alias. The alias is a property of - // the target that a later generation may move, and resolving it here would - // mean joining the host's presence state into a request-path listing — which - // would require granting request traffic read over every organisation's - // presence to answer about one. A consumer that needs the alias asks - // ListAvailableSolutions, which serves accepted applied state, and joins on - // this id. - // - // Empty only on a revoked historical row whose target could not be - // established at the cutover; an active installation always names one. - string target_id = 11 [ - (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, - (buf.validate.field).string.uuid = true - ]; - // Why a revoked installation was revoked, when it was not an administrator - // uninstalling: the installation-identity cutover, or a tenancy narrowing that - // withdrew this organisation. Empty on an active row. - string revoked_reason = 12 [(buf.validate.field).string.max_len = 512]; - // Why the install step failed: the solution's bounded refusal reason, or a - // fixed reason when it never answered. Empty unless status is FAILED. - string failure_reason = 13 [(buf.validate.field).string.max_len = 512]; -} - message InstallSolutionRequest { string org_id = 1 [(buf.validate.field).string.uuid = true]; // Canonical "publisher/name:version" of the solution's agent. The agent @@ -219,42 +142,6 @@ message GetInstallationResponse { InstallationHealth health = 2; } -// ListInstallationsRequest asks what one organization has installed. Every other -// RPC on this service names a single installation, so until this existed nothing -// could answer "what has this org installed?" — which is the authority read a -// per-organization solution projection is built on (issue #949). -message ListInstallationsRequest { - string org_id = 1 [(buf.validate.field).string.uuid = true]; - // Restrict to one lifecycle status. UNSPECIFIED returns every status, so a - // caller that must see only live installs asks for ACTIVE explicitly rather - // than leaning on a default that would silently widen if the enum grew. - InstallationStatus status = 2; - // Page bound. 0 -> server default; capped so an organization with many - // installations can never return an unbounded result in one call. - int32 page_size = 3 [(buf.validate.field).int32 = { - gte: 0 - lte: 500 - }]; - // Opaque cursor from a previous response's next_page_token; empty for page one. - string page_token = 4; -} - -// InstallationSummary is one row of the listing: the installation, plus the health -// resolved live beside it exactly as GetInstallation resolves it for one. Health -// travels with the row because a consumer that renders an installed solution as -// unavailable would otherwise need one GetInstallation per installation. -message InstallationSummary { - Installation installation = 1; - InstallationHealth health = 2; -} - -message ListInstallationsResponse { - repeated InstallationSummary installations = 1; - // Set when more installations remain; pass it back as page_token. Empty on the - // last page. - string next_page_token = 2; -} - // InstallationService composes and administers installations. The headless mint // that turns an installation into a short-lived Work Context is // WorkContextService.StartInstallationTask. @@ -431,11 +318,15 @@ service InstallationService { // ListInstallations enumerates one organization's installations with their live // health. It is internal-tier on purpose: it takes the organization as a request // field, so only a caller holding the cluster-internal credential may ask, and - // the tenant it asks about is the one the auth-gateway projected from a verified - // identity — never one a browser supplied. It sits beside - // PermissionService.ListAccessibleScopes for that reason: the two are read - // together to answer what a viewer's organization installed and what that viewer - // was granted. + // the tenant it asks about is never one a browser supplied. The auth-gateway is + // its caller, for two reads. Its viewer projection asks about the tenant it + // projected from a verified identity; it sits beside + // PermissionService.ListAccessibleScopes for that reason, the two read together + // to answer what a viewer's organization installed and what that viewer was + // granted. Its declared module route's bearer-less arm asks about the tenant an + // edge-verified Work Context names, a value this issuer signed, to admit a + // headless operation context only when the target is installed there and the + // context is sealed to another active installation. rpc ListInstallations(ListInstallationsRequest) returns (ListInstallationsResponse) { option (saas.policy.v1.method_policy) = { exposure: EXPOSURE_INTERNAL @@ -496,7 +387,8 @@ service InstallationService { // It is the org-admin companion to the internal ListInstallations, and it is a // separate method rather than a relaxed policy on that one: ListInstallations // is read by the gateway for an organisation it projected from a verified - // identity, and a bare tenant credential must never reach it. This one binds + // identity or that an edge-verified Work Context names, and a bare tenant + // credential must never reach it. This one binds // the organisation to the caller's own admin membership, exactly as // ListAvailableSolutions does, so no organisation's administrator can read // another's. The one exception is inherited, not added here: a platform diff --git a/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto b/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto index 64acd9ec3..dd6ffc5f5 100644 --- a/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto +++ b/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto @@ -8,7 +8,7 @@ import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; import "saas/accounts/v1/authorization.proto"; import "saas/accounts/v1/common.proto"; -import "saas/accounts/v1/installations.proto"; +import "saas/accounts/v1/installation_listing.proto"; import "saas/accounts/v1/solution_registry.proto"; import "saas/events/v1/operations.proto"; import "saas/jobs/v1/jobs.proto"; diff --git a/module/services/auth-gateway/code/installation_listing_vendor_test.go b/module/services/auth-gateway/code/installation_listing_vendor_test.go deleted file mode 100644 index 5c9f85ea5..000000000 --- a/module/services/auth-gateway/code/installation_listing_vendor_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package main - -import ( - "os" - "path/filepath" - "regexp" - "strings" - "testing" - - "github.com/stretchr/testify/require" -) - -// TestVendoredInstallationListingMatchesAccounts holds every definition in the -// gateway's installation_listing.proto byte-identical to the one accounts -// declares in installations.proto. The gateway carries only the messages -// ListInstallations takes and answers, because installations.proto imports -// the policy options and google.api this service does not vendor; a copy that -// drifted would decode a different wire shape from the one accounts writes, -// silently. -func TestVendoredInstallationListingMatchesAccounts(t *testing.T) { - vendored, err := os.ReadFile(filepath.Join("..", "proto", "saas", "accounts", "v1", "installation_listing.proto")) - require.NoError(t, err) - owner, err := os.ReadFile(filepath.Join("..", "..", "accounts", "proto", "saas", "accounts", "v1", "installations.proto")) - require.NoError(t, err) - - definitions := regexp.MustCompile(`(?ms)^(?:message|enum) \w+ \{\n.*?^\}\n`).FindAllString(string(vendored), -1) - require.Len(t, definitions, 6, "the vendored file declares the two enums and four messages ListInstallations uses") - for _, definition := range definitions { - require.True(t, strings.Contains(string(owner), definition), - "installation_listing.proto drifted from accounts' installations.proto:\n%s", definition) - } -} diff --git a/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go b/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go index 276a835fb..0ea283153 100644 --- a/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go +++ b/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go @@ -24,6 +24,9 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +// InstallationStatus is the lifecycle of an installation row. An uninstall is a +// soft delete (status=revoked, revoked_at set) so the agent, grants, and audit +// trail remain resolvable. type InstallationStatus int32 const ( @@ -83,6 +86,12 @@ func (InstallationStatus) EnumDescriptor() ([]byte, []int) { return file_saas_accounts_v1_installation_listing_proto_rawDescGZIP(), []int{0} } +// InstallationHealth is resolved live from current authority facts, never +// stored. It is the observable form of the fail-closed mint rule: a +// StartInstallationTask fails exactly when health is not HEALTHY, and transfer +// or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record +// nor any co-owner is currently an org admin (the Zapier-class "connection owner +// left the org" failure), surfaced rather than silently reassigned. type InstallationHealth int32 const ( @@ -141,6 +150,8 @@ func (InstallationHealth) EnumDescriptor() ([]byte, []int) { return file_saas_accounts_v1_installation_listing_proto_rawDescGZIP(), []int{1} } +// Installation is one solution's bound identity in an org. Server-assigned ids; +// the browser cannot attest to tenancy, the agent principal, or the scope node. type Installation struct { state protoimpl.MessageState `protogen:"open.v1"` Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` @@ -293,6 +304,10 @@ func (x *Installation) GetFailureReason() string { return "" } +// ListInstallationsRequest asks what one organization has installed. Every other +// RPC on this service names a single installation, so until this existed nothing +// could answer "what has this org installed?" — which is the authority read a +// per-organization solution projection is built on (issue #949). type ListInstallationsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` @@ -367,6 +382,10 @@ func (x *ListInstallationsRequest) GetPageToken() string { return "" } +// InstallationSummary is one row of the listing: the installation, plus the health +// resolved live beside it exactly as GetInstallation resolves it for one. Health +// travels with the row because a consumer that renders an installed solution as +// unavailable would otherwise need one GetInstallation per installation. type InstallationSummary struct { state protoimpl.MessageState `protogen:"open.v1"` Installation *Installation `protobuf:"bytes,1,opt,name=installation,proto3" json:"installation,omitempty"` diff --git a/module/services/auth-gateway/code/vendored_accounts_protos_test.go b/module/services/auth-gateway/code/vendored_accounts_protos_test.go new file mode 100644 index 000000000..24cd85ff5 --- /dev/null +++ b/module/services/auth-gateway/code/vendored_accounts_protos_test.go @@ -0,0 +1,31 @@ +package main + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestVendoredAccountsProtosAreAccounts holds every accounts proto this gateway +// generates from byte-identical to the file accounts owns. The gateway carries +// only the files whose imports it can resolve (installation_listing.proto, +// module_registration.proto, solution_entitlements.proto and their peers), and +// accounts keeps those files import-minimal for that reason. A copy that +// drifted would decode a different wire shape from the one accounts writes, +// silently, and would also stop the two published contracts from loading into +// one descriptor pool. +func TestVendoredAccountsProtosAreAccounts(t *testing.T) { + vendored, err := filepath.Glob(filepath.Join("..", "proto", "saas", "accounts", "v1", "*.proto")) + require.NoError(t, err) + require.NotEmpty(t, vendored) + for _, path := range vendored { + copied, err := os.ReadFile(path) + require.NoError(t, err) + owned, err := os.ReadFile(filepath.Join("..", "..", "accounts", "proto", "saas", "accounts", "v1", filepath.Base(path))) + require.NoError(t, err, "%s has no accounts-owned original", filepath.Base(path)) + require.True(t, bytes.Equal(owned, copied), "%s drifted from accounts' file of the same name", filepath.Base(path)) + } +} diff --git a/module/services/auth-gateway/proto/saas/accounts/v1/installation_listing.proto b/module/services/auth-gateway/proto/saas/accounts/v1/installation_listing.proto index 985e1a29c..57150f92b 100644 --- a/module/services/auth-gateway/proto/saas/accounts/v1/installation_listing.proto +++ b/module/services/auth-gateway/proto/saas/accounts/v1/installation_listing.proto @@ -5,19 +5,21 @@ package saas.accounts.v1; import "buf/validate/validate.proto"; import "google/protobuf/timestamp.proto"; -// ============================================================================ -// The installation listing, as the auth-gateway reads it. +// The installation listing: an installation, its lifecycle and health, and the +// messages InstallationService/ListInstallations takes and answers. // -// accounts declares these messages in installations.proto beside the -// InstallationService, which imports the policy options and google.api this -// gateway does not vendor. The gateway calls one internal-tier RPC, -// InstallationService/ListInstallations, so it carries the messages that RPC -// takes and answers, copied definition for definition from installations.proto -// and nothing else. TestVendoredInstallationListingMatchesAccounts holds every -// definition here byte-identical to the one accounts declares, so the two -// cannot drift on the wire. -// ============================================================================ +// These definitions live in their own file, with deliberately minimal imports, +// so the auth-gateway can generate the same types from the same source proto +// and call ListInstallations over the internal listener, as it does for +// module_registration.proto and solution_entitlements.proto. installations.proto +// imports this file; the service and every other installation message stay +// there. Sharing the generated definition rather than restating the wire shape +// is what keeps the two sides from drifting: the gateway's copy is this file, +// byte for byte. +// InstallationStatus is the lifecycle of an installation row. An uninstall is a +// soft delete (status=revoked, revoked_at set) so the agent, grants, and audit +// trail remain resolvable. enum InstallationStatus { INSTALLATION_STATUS_UNSPECIFIED = 0; INSTALLATION_STATUS_ACTIVE = 1; @@ -30,6 +32,12 @@ enum InstallationStatus { INSTALLATION_STATUS_FAILED = 4; } +// InstallationHealth is resolved live from current authority facts, never +// stored. It is the observable form of the fail-closed mint rule: a +// StartInstallationTask fails exactly when health is not HEALTHY, and transfer +// or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record +// nor any co-owner is currently an org admin (the Zapier-class "connection owner +// left the org" failure), surfaced rather than silently reassigned. enum InstallationHealth { INSTALLATION_HEALTH_UNSPECIFIED = 0; INSTALLATION_HEALTH_HEALTHY = 1; @@ -39,6 +47,8 @@ enum InstallationHealth { INSTALLATION_HEALTH_STANDING_GRANT_MISSING = 5; } +// Installation is one solution's bound identity in an org. Server-assigned ids; +// the browser cannot attest to tenancy, the agent principal, or the scope node. message Installation { string id = 1 [(buf.validate.field).string.uuid = true]; string org_id = 2 [(buf.validate.field).string.uuid = true]; @@ -84,6 +94,10 @@ message Installation { string failure_reason = 13 [(buf.validate.field).string.max_len = 512]; } +// ListInstallationsRequest asks what one organization has installed. Every other +// RPC on this service names a single installation, so until this existed nothing +// could answer "what has this org installed?" — which is the authority read a +// per-organization solution projection is built on (issue #949). message ListInstallationsRequest { string org_id = 1 [(buf.validate.field).string.uuid = true]; // Restrict to one lifecycle status. UNSPECIFIED returns every status, so a @@ -100,6 +114,10 @@ message ListInstallationsRequest { string page_token = 4; } +// InstallationSummary is one row of the listing: the installation, plus the health +// resolved live beside it exactly as GetInstallation resolves it for one. Health +// travels with the row because a consumer that renders an installed solution as +// unavailable would otherwise need one GetInstallation per installation. message InstallationSummary { Installation installation = 1; InstallationHealth health = 2; diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json index 363d105db..441ca77ef 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json +++ b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contract.binpb", - "digest": "sha256:373a8f3a22b7968c36dd9faffb9be5ef64213b1de2a532f5e567007bd65caaa7", + "digest": "sha256:72172e6df496c728bdc8eec06a8f10f23c74a55b97a9eb86f08ea4d3e1bdd35c", "services": [ { "name": "APIKeyService", diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb b/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb index 590c0a8eb1d2d00c9520361083a0d0e6c244e78f..18d4889d20273ab7985cefe5699eb283f050e078 100644 GIT binary patch delta 15827 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 delta 15097 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 diff --git a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml index e907bd78a..f41dac214 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml +++ b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml @@ -12,7 +12,7 @@ sources: module: saas-starter service: accounts endpoint: connect - contract-digest: sha256:373a8f3a22b7968c36dd9faffb9be5ef64213b1de2a532f5e567007bd65caaa7 + contract-digest: sha256:72172e6df496c728bdc8eec06a8f10f23c74a55b97a9eb86f08ea4d3e1bdd35c services: - APIKeyService - AccessibleScopeService @@ -52,6 +52,6 @@ sources: - WorkContextService as: accounts generated-by: - codefly: 0.1.191 + codefly: source-5234ce248b092edf1138a16ec4140dfbc8745522 companion: ghcr.io/codefly-dev/proto:0.0.16 facade: true diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installation_listing_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installation_listing_pb.ts new file mode 100644 index 000000000..934ddc7e4 --- /dev/null +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installation_listing_pb.ts @@ -0,0 +1,306 @@ +// @generated by protoc-gen-es v2.11.0 with parameter "target=ts,import_extension=none" +// @generated from file saas/accounts/v1/installation_listing.proto (package saas.accounts.v1, syntax proto3) +/* eslint-disable */ + +import type { GenEnum, GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import { enumDesc, fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; +import { file_buf_validate_validate } from "../../../buf/validate/validate_pb"; +import type { Timestamp } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file saas/accounts/v1/installation_listing.proto. + */ +export const file_saas_accounts_v1_installation_listing: GenFile = /*@__PURE__*/ + fileDesc("CitzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbl9saXN0aW5nLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIvkDCgxJbnN0YWxsYXRpb24SFAoCaWQYASABKAlCCLpIBXIDsAEBEhgKBm9yZ19pZBgCIAEoCUIIukgFcgOwAQESJAoSYWdlbnRfcHJpbmNpcGFsX2lkGAMgASgJQgi6SAVyA7ABARIkChJvd25lcl9wcmluY2lwYWxfaWQYBSABKAlCCLpIBXIDsAEBEh4KFmNvX293bmVyX3ByaW5jaXBhbF9pZHMYBiADKAkSJAoScm9vdF9zY29wZV9ub2RlX2lkGAcgASgJQgi6SAVyA7ABARI0CgZzdGF0dXMYCCABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN0YXR1cxIuCgpjcmVhdGVkX2F0GAkgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIzCgpyZXZva2VkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgAiAEBEh4KCXRhcmdldF9pZBgLIAEoCUILukgI2AEBcgOwAQESIAoOcmV2b2tlZF9yZWFzb24YDCABKAlCCLpIBXIDGIAEEiAKDmZhaWx1cmVfcmVhc29uGA0gASgJQgi6SAVyAxiABEINCgtfcmV2b2tlZF9hdEoECAQQBVITc29sdXRpb25faWRlbnRpZmllciKdAQoYTGlzdEluc3RhbGxhdGlvbnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoGc3RhdHVzGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdGF0dXMSHQoJcGFnZV9zaXplGAMgASgFQgq6SAcaBRj0AygAEhIKCnBhZ2VfdG9rZW4YBCABKAkigQEKE0luc3RhbGxhdGlvblN1bW1hcnkSNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SNAoGaGVhbHRoGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25IZWFsdGgicgoZTGlzdEluc3RhbGxhdGlvbnNSZXNwb25zZRI8Cg1pbnN0YWxsYXRpb25zGAEgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdW1tYXJ5EhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSq7AQoSSW5zdGFsbGF0aW9uU3RhdHVzEiMKH0lOU1RBTExBVElPTl9TVEFUVVNfVU5TUEVDSUZJRUQQABIeChpJTlNUQUxMQVRJT05fU1RBVFVTX0FDVElWRRABEh8KG0lOU1RBTExBVElPTl9TVEFUVVNfUkVWT0tFRBACEh8KG0lOU1RBTExBVElPTl9TVEFUVVNfUEVORElORxADEh4KGklOU1RBTExBVElPTl9TVEFUVVNfRkFJTEVEEAQqhAIKEkluc3RhbGxhdGlvbkhlYWx0aBIjCh9JTlNUQUxMQVRJT05fSEVBTFRIX1VOU1BFQ0lGSUVEEAASHwobSU5TVEFMTEFUSU9OX0hFQUxUSF9IRUFMVEhZEAESKQolSU5TVEFMTEFUSU9OX0hFQUxUSF9OT19FTElHSUJMRV9PV05FUhACEiUKIUlOU1RBTExBVElPTl9IRUFMVEhfQUdFTlRfUkVWT0tFRBADEiYKIklOU1RBTExBVElPTl9IRUFMVEhfQUdFTlRfRElTQUJMRUQQBBIuCipJTlNUQUxMQVRJT05fSEVBTFRIX1NUQU5ESU5HX0dSQU5UX01JU1NJTkcQBWIGcHJvdG8z", [file_buf_validate_validate, file_google_protobuf_timestamp]); + +/** + * Installation is one solution's bound identity in an org. Server-assigned ids; + * the browser cannot attest to tenancy, the agent principal, or the scope node. + * + * @generated from message saas.accounts.v1.Installation + */ +export type Installation = Message<"saas.accounts.v1.Installation"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: string org_id = 2; + */ + orgId: string; + + /** + * @generated from field: string agent_principal_id = 3; + */ + agentPrincipalId: string; + + /** + * @generated from field: string owner_principal_id = 5; + */ + ownerPrincipalId: string; + + /** + * @generated from field: repeated string co_owner_principal_ids = 6; + */ + coOwnerPrincipalIds: string[]; + + /** + * @generated from field: string root_scope_node_id = 7; + */ + rootScopeNodeId: string; + + /** + * @generated from field: saas.accounts.v1.InstallationStatus status = 8; + */ + status: InstallationStatus; + + /** + * @generated from field: google.protobuf.Timestamp created_at = 9; + */ + createdAt?: Timestamp; + + /** + * @generated from field: optional google.protobuf.Timestamp revoked_at = 10; + */ + revokedAt?: Timestamp; + + /** + * The immutable solution target this installation was consented to: one + * continuous period of one binding's presence on this host. It is never + * reused, so a replacement binding — even one claiming the same route alias — + * is a different target and inherits nothing. + * + * Deliberately NOT accompanied by the route alias. The alias is a property of + * the target that a later generation may move, and resolving it here would + * mean joining the host's presence state into a request-path listing — which + * would require granting request traffic read over every organisation's + * presence to answer about one. A consumer that needs the alias asks + * ListAvailableSolutions, which serves accepted applied state, and joins on + * this id. + * + * Empty only on a revoked historical row whose target could not be + * established at the cutover; an active installation always names one. + * + * @generated from field: string target_id = 11; + */ + targetId: string; + + /** + * Why a revoked installation was revoked, when it was not an administrator + * uninstalling: the installation-identity cutover, or a tenancy narrowing that + * withdrew this organisation. Empty on an active row. + * + * @generated from field: string revoked_reason = 12; + */ + revokedReason: string; + + /** + * Why the install step failed: the solution's bounded refusal reason, or a + * fixed reason when it never answered. Empty unless status is FAILED. + * + * @generated from field: string failure_reason = 13; + */ + failureReason: string; +}; + +/** + * Describes the message saas.accounts.v1.Installation. + * Use `create(InstallationSchema)` to create a new message. + */ +export const InstallationSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_installation_listing, 0); + +/** + * ListInstallationsRequest asks what one organization has installed. Every other + * RPC on this service names a single installation, so until this existed nothing + * could answer "what has this org installed?" — which is the authority read a + * per-organization solution projection is built on (issue #949). + * + * @generated from message saas.accounts.v1.ListInstallationsRequest + */ +export type ListInstallationsRequest = Message<"saas.accounts.v1.ListInstallationsRequest"> & { + /** + * @generated from field: string org_id = 1; + */ + orgId: string; + + /** + * Restrict to one lifecycle status. UNSPECIFIED returns every status, so a + * caller that must see only live installs asks for ACTIVE explicitly rather + * than leaning on a default that would silently widen if the enum grew. + * + * @generated from field: saas.accounts.v1.InstallationStatus status = 2; + */ + status: InstallationStatus; + + /** + * Page bound. 0 -> server default; capped so an organization with many + * installations can never return an unbounded result in one call. + * + * @generated from field: int32 page_size = 3; + */ + pageSize: number; + + /** + * Opaque cursor from a previous response's next_page_token; empty for page one. + * + * @generated from field: string page_token = 4; + */ + pageToken: string; +}; + +/** + * Describes the message saas.accounts.v1.ListInstallationsRequest. + * Use `create(ListInstallationsRequestSchema)` to create a new message. + */ +export const ListInstallationsRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_installation_listing, 1); + +/** + * InstallationSummary is one row of the listing: the installation, plus the health + * resolved live beside it exactly as GetInstallation resolves it for one. Health + * travels with the row because a consumer that renders an installed solution as + * unavailable would otherwise need one GetInstallation per installation. + * + * @generated from message saas.accounts.v1.InstallationSummary + */ +export type InstallationSummary = Message<"saas.accounts.v1.InstallationSummary"> & { + /** + * @generated from field: saas.accounts.v1.Installation installation = 1; + */ + installation?: Installation; + + /** + * @generated from field: saas.accounts.v1.InstallationHealth health = 2; + */ + health: InstallationHealth; +}; + +/** + * Describes the message saas.accounts.v1.InstallationSummary. + * Use `create(InstallationSummarySchema)` to create a new message. + */ +export const InstallationSummarySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_installation_listing, 2); + +/** + * @generated from message saas.accounts.v1.ListInstallationsResponse + */ +export type ListInstallationsResponse = Message<"saas.accounts.v1.ListInstallationsResponse"> & { + /** + * @generated from field: repeated saas.accounts.v1.InstallationSummary installations = 1; + */ + installations: InstallationSummary[]; + + /** + * Set when more installations remain; pass it back as page_token. Empty on the + * last page. + * + * @generated from field: string next_page_token = 2; + */ + nextPageToken: string; +}; + +/** + * Describes the message saas.accounts.v1.ListInstallationsResponse. + * Use `create(ListInstallationsResponseSchema)` to create a new message. + */ +export const ListInstallationsResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_installation_listing, 3); + +/** + * InstallationStatus is the lifecycle of an installation row. An uninstall is a + * soft delete (status=revoked, revoked_at set) so the agent, grants, and audit + * trail remain resolvable. + * + * @generated from enum saas.accounts.v1.InstallationStatus + */ +export enum InstallationStatus { + /** + * @generated from enum value: INSTALLATION_STATUS_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: INSTALLATION_STATUS_ACTIVE = 1; + */ + ACTIVE = 1, + + /** + * @generated from enum value: INSTALLATION_STATUS_REVOKED = 2; + */ + REVOKED = 2, + + /** + * Installed, and waiting for the solution to acknowledge its standard install + * step. Nothing can use a pending installation. + * + * @generated from enum value: INSTALLATION_STATUS_PENDING = 3; + */ + PENDING = 3, + + /** + * The solution refused its install step, or never answered within the + * attempt budget. Nothing can use it; an administrator may retry it. + * + * @generated from enum value: INSTALLATION_STATUS_FAILED = 4; + */ + FAILED = 4, +} + +/** + * Describes the enum saas.accounts.v1.InstallationStatus. + */ +export const InstallationStatusSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_saas_accounts_v1_installation_listing, 0); + +/** + * InstallationHealth is resolved live from current authority facts, never + * stored. It is the observable form of the fail-closed mint rule: a + * StartInstallationTask fails exactly when health is not HEALTHY, and transfer + * or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record + * nor any co-owner is currently an org admin (the Zapier-class "connection owner + * left the org" failure), surfaced rather than silently reassigned. + * + * @generated from enum saas.accounts.v1.InstallationHealth + */ +export enum InstallationHealth { + /** + * @generated from enum value: INSTALLATION_HEALTH_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: INSTALLATION_HEALTH_HEALTHY = 1; + */ + HEALTHY = 1, + + /** + * @generated from enum value: INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER = 2; + */ + NO_ELIGIBLE_OWNER = 2, + + /** + * @generated from enum value: INSTALLATION_HEALTH_AGENT_REVOKED = 3; + */ + AGENT_REVOKED = 3, + + /** + * @generated from enum value: INSTALLATION_HEALTH_AGENT_DISABLED = 4; + */ + AGENT_DISABLED = 4, + + /** + * @generated from enum value: INSTALLATION_HEALTH_STANDING_GRANT_MISSING = 5; + */ + STANDING_GRANT_MISSING = 5, +} + +/** + * Describes the enum saas.accounts.v1.InstallationHealth. + */ +export const InstallationHealthSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_saas_accounts_v1_installation_listing, 1); diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installations_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installations_pb.ts index eb139cd43..c7380521e 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installations_pb.ts +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installations_pb.ts @@ -6,10 +6,12 @@ import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobu import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; import { file_buf_validate_validate } from "../../../buf/validate/validate_pb"; import { file_google_api_annotations } from "../../../google/api/annotations_pb"; -import type { EmptySchema, Timestamp } from "@bufbuild/protobuf/wkt"; -import { file_google_protobuf_empty, file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; +import type { EmptySchema } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_empty } from "@bufbuild/protobuf/wkt"; import type { CollectionReadGrant, ScopeGrant } from "./authorization_pb"; import { file_saas_accounts_v1_authorization } from "./authorization_pb"; +import type { Installation, InstallationHealth, InstallationSchema, ListInstallationsRequestSchema, ListInstallationsResponseSchema } from "./installation_listing_pb"; +import { file_saas_accounts_v1_installation_listing } from "./installation_listing_pb"; import { file_saas_policy_v1_options } from "../../policy/v1/options_pb"; import type { Message } from "@bufbuild/protobuf"; @@ -17,105 +19,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/installations.proto. */ export const file_saas_accounts_v1_installations: GenFile = /*@__PURE__*/ - fileDesc("CiRzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbnMucHJvdG8SEHNhYXMuYWNjb3VudHMudjEi+QMKDEluc3RhbGxhdGlvbhIUCgJpZBgBIAEoCUIIukgFcgOwAQESGAoGb3JnX2lkGAIgASgJQgi6SAVyA7ABARIkChJhZ2VudF9wcmluY2lwYWxfaWQYAyABKAlCCLpIBXIDsAEBEiQKEm93bmVyX3ByaW5jaXBhbF9pZBgFIAEoCUIIukgFcgOwAQESHgoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgGIAMoCRIkChJyb290X3Njb3BlX25vZGVfaWQYByABKAlCCLpIBXIDsAEBEjQKBnN0YXR1cxgIIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uU3RhdHVzEi4KCmNyZWF0ZWRfYXQYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKCnJldm9rZWRfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSACIAQESHgoJdGFyZ2V0X2lkGAsgASgJQgu6SAjYAQFyA7ABARIgCg5yZXZva2VkX3JlYXNvbhgMIAEoCUIIukgFcgMYgAQSIAoOZmFpbHVyZV9yZWFzb24YDSABKAlCCLpIBXIDGIAEQg0KC19yZXZva2VkX2F0SgQIBBAFUhNzb2x1dGlvbl9pZGVudGlmaWVyIuADChZJbnN0YWxsU29sdXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESJAoQYWdlbnRfaWRlbnRpZmllchgCIAEoCUIKukgHcgUQARiABBIeCgxkaXNwbGF5X25hbWUYBCABKAlCCLpIBXIDGIAEEiIKEHJvb3Rfc2NvcGVfbGFiZWwYBiABKAlCCLpIBXIDGIAEEhkKB3JvbGVfaWQYByABKAlCCLpIBXIDsAEBEicKEm93bmVyX3ByaW5jaXBhbF9pZBgIIAEoCUILukgI2AEBcgOwAQESLwoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgJIAMoCUIPukgMkgEJECAiBXIDsAEBEioKEWFsbG93ZWRfYXVkaWVuY2VzGAogAygJQg+6SAySAQkQQCIFcgMYgAQSJwoOYWxsb3dlZF9zY29wZXMYCyADKAlCD7pIDJIBCRBAIgVyAxiAARIbCgl0YXJnZXRfaWQYDCABKAlCCLpIBXIDsAEBEh8KBXNjb3BlGA0gASgJQgu6SAhyBhACKICAQEgAiAEBQggKBl9zY29wZUoECAMQBEoECAUQBlITc29sdXRpb25faWRlbnRpZmllclIPcm9vdF9zY29wZV9wYXRoInQKGU5hcnJvd0luc3RhbGxhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhoKBXNjb3BlGAMgASgJQgu6SAhyBhACKICAQCJXChhSZXRyeUluc3RhbGxhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBIlcKGFVuaW5zdGFsbFNvbHV0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEivgEKJFRyYW5zZmVySW5zdGFsbGF0aW9uT3duZXJzaGlwUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESKAoWbmV3X293bmVyX3ByaW5jaXBhbF9pZBgDIAEoCUIIukgFcgOwAQESLwoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgEIAMoCUIPukgMkgEJECAiBXIDsAEBIlUKFkdldEluc3RhbGxhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBIoUBChdHZXRJbnN0YWxsYXRpb25SZXNwb25zZRI0CgxpbnN0YWxsYXRpb24YASABKAsyHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbhI0CgZoZWFsdGgYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbkhlYWx0aCKdAQoYTGlzdEluc3RhbGxhdGlvbnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoGc3RhdHVzGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdGF0dXMSHQoJcGFnZV9zaXplGAMgASgFQgq6SAcaBRj0AygAEhIKCnBhZ2VfdG9rZW4YBCABKAkigQEKE0luc3RhbGxhdGlvblN1bW1hcnkSNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SNAoGaGVhbHRoGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25IZWFsdGgicgoZTGlzdEluc3RhbGxhdGlvbnNSZXNwb25zZRI8Cg1pbnN0YWxsYXRpb25zGAEgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdW1tYXJ5EhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJzCiRMaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIdCglwYWdlX3NpemUYAiABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgDIAEoCSLIAwoYT3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEjQKBmhlYWx0aBgCIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uSGVhbHRoEjMKBnRhcmdldBgDIAEoCzIjLnNhYXMuYWNjb3VudHMudjEuQXZhaWxhYmxlU29sdXRpb24SFwoPcm9vdF9zY29wZV9wYXRoGAQgASgJEjwKDWdyYW50ZWRfdGVhbXMYBSADKAsyJS5zYWFzLmFjY291bnRzLnYxLkNvbGxlY3Rpb25SZWFkR3JhbnQSPgoPaW5oZXJpdGVkX3RlYW1zGAYgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50EjoKCXJlYWRpbmVzcxgHIAEoCzInLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uUmVhZGluZXNzEjgKD3JlYWNoZWRfbW9kdWxlcxgIIAMoCzIfLnNhYXMuYWNjb3VudHMudjEuUmVhY2hlZE1vZHVsZSLtAQoSUmVhZGluZXNzQ29uZGl0aW9uEjYKBGtpbmQYASABKA4yKC5zYWFzLmFjY291bnRzLnYxLlJlYWRpbmVzc0NvbmRpdGlvbktpbmQSDwoHc3ViamVjdBgCIAEoCRIOCgZkZXRhaWwYAyABKAkSOAoKY2xlYXJlZF9ieRgEIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQ2xlYXJlZEJ5EjEKBmFjdGlvbhgFIAEoDjIhLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQWN0aW9uEhEKCXNvdXJjZV9pZBgGIAEoCSJbChVJbnN0YWxsYXRpb25SZWFkaW5lc3MSDQoFcmVhZHkYASABKAgSMwoFdW5tZXQYAiADKAsyJC5zYWFzLmFjY291bnRzLnYxLlJlYWRpbmVzc0NvbmRpdGlvbiJPCg1SZWFjaGVkTW9kdWxlEhAKCGF1ZGllbmNlGAEgASgJEhcKD2luc3RhbGxhdGlvbl9pZBgCIAEoCRITCgtyb3V0ZV9hbGlhcxgDIAEoCSKDAQolTGlzdE9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbnNSZXNwb25zZRJBCg1pbnN0YWxsYXRpb25zGAEgAygLMiouc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb25JbnN0YWxsYXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIpMBCh5HcmFudEluc3RhbGxhdGlvblRvVGVhbVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhkKB3RlYW1faWQYAyABKAlCCLpIBXIDsAEBEhkKB3JvbGVfaWQYBCABKAlCCLpIBXIDsAEBInUKE0NhcnJpZWRNb2R1bGVBY2Nlc3MSEAoIYXVkaWVuY2UYASABKAkSFwoPaW5zdGFsbGF0aW9uX2lkGAIgASgJEhIKCnNjb3BlX3BhdGgYAyABKAkSDwoHcm9sZV9pZBgEIAEoCRIOCgZzY29wZXMYBSADKAkiuwEKH0dyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVzcG9uc2USKwoFZ3JhbnQYASABKAsyHC5zYWFzLmFjY291bnRzLnYxLlNjb3BlR3JhbnQSNgoHY2FycmllZBgCIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ2FycmllZE1vZHVsZUFjY2VzcxIzCgV1bm1ldBgDIAMoCzIkLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQ29uZGl0aW9uIpYBCiFSZXZva2VJbnN0YWxsYXRpb25Gcm9tVGVhbVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhkKB3RlYW1faWQYAyABKAlCCLpIBXIDsAEBEhkKB3JvbGVfaWQYBCABKAlCCLpIBXIDsAEBIoQCChFBdmFpbGFibGVTb2x1dGlvbhIbCgl0YXJnZXRfaWQYASABKAlCCLpIBXIDsAEBEhIKCmJpbmRpbmdfaWQYAiABKAkSEwoLcm91dGVfYWxpYXMYAyABKAkSGQoRcmVsZWFzZV9wdWJsaXNoZXIYBCABKAkSFAoMcmVsZWFzZV9uYW1lGAUgASgJEhcKD3JlbGVhc2VfdmVyc2lvbhgGIAEoCRIZChFvcGVuZWRfZ2VuZXJhdGlvbhgHIAEoBBIaChJhcHBsaWVkX2dlbmVyYXRpb24YCCABKAQSEQoJaW5zdGFsbGVkGAkgASgIEhUKBGtpbmQYCiABKAlCB7pIBHICGCAibAodTGlzdEF2YWlsYWJsZVNvbHV0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIdCglwYWdlX3NpemUYAiABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgDIAEoCSJxCh5MaXN0QXZhaWxhYmxlU29sdXRpb25zUmVzcG9uc2USNgoJc29sdXRpb25zGAEgAygLMiMuc2Fhcy5hY2NvdW50cy52MS5BdmFpbGFibGVTb2x1dGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkquwEKEkluc3RhbGxhdGlvblN0YXR1cxIjCh9JTlNUQUxMQVRJT05fU1RBVFVTX1VOU1BFQ0lGSUVEEAASHgoaSU5TVEFMTEFUSU9OX1NUQVRVU19BQ1RJVkUQARIfChtJTlNUQUxMQVRJT05fU1RBVFVTX1JFVk9LRUQQAhIfChtJTlNUQUxMQVRJT05fU1RBVFVTX1BFTkRJTkcQAxIeChpJTlNUQUxMQVRJT05fU1RBVFVTX0ZBSUxFRBAEKoQCChJJbnN0YWxsYXRpb25IZWFsdGgSIwofSU5TVEFMTEFUSU9OX0hFQUxUSF9VTlNQRUNJRklFRBAAEh8KG0lOU1RBTExBVElPTl9IRUFMVEhfSEVBTFRIWRABEikKJUlOU1RBTExBVElPTl9IRUFMVEhfTk9fRUxJR0lCTEVfT1dORVIQAhIlCiFJTlNUQUxMQVRJT05fSEVBTFRIX0FHRU5UX1JFVk9LRUQQAxImCiJJTlNUQUxMQVRJT05fSEVBTFRIX0FHRU5UX0RJU0FCTEVEEAQSLgoqSU5TVEFMTEFUSU9OX0hFQUxUSF9TVEFORElOR19HUkFOVF9NSVNTSU5HEAUq8wMKFlJlYWRpbmVzc0NvbmRpdGlvbktpbmQSKAokUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX1VOU1BFQ0lGSUVEEAASMQotUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX0lOU1RBTExfU1RFUF9QRU5ESU5HEAESMAosUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX0lOU1RBTExfU1RFUF9GQUlMRUQQAhItCilSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfUkVDT1JEX1VOSEVBTFRIWRADEjUKMVJFQURJTkVTU19DT05ESVRJT05fS0lORF9NT0RVTEVfUFJJTkNJUEFMX09NSVRURUQQBBI2CjJSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfT1BFUkFUSU9OX0JJTkRJTkdfT01JVFRFRBAFEjEKLVJFQURJTkVTU19DT05ESVRJT05fS0lORF9TT1VSQ0VfTk9UX0RFTEVHQVRFRBAGEjkKNVJFQURJTkVTU19DT05ESVRJT05fS0lORF9SRUFDSEVEX01PRFVMRV9OT1RfSU5TVEFMTEVEEAcSPgo6UkVBRElORVNTX0NPTkRJVElPTl9LSU5EX0NBUlJJRURfTU9EVUxFX0FDQ0VTU19OT1RfREVSSVZFRBAIKtUBChJSZWFkaW5lc3NDbGVhcmVkQnkSJAogUkVBRElORVNTX0NMRUFSRURfQllfVU5TUEVDSUZJRUQQABIfChtSRUFESU5FU1NfQ0xFQVJFRF9CWV9WSUVXRVIQARIrCidSRUFESU5FU1NfQ0xFQVJFRF9CWV9PUkdBTklaQVRJT05fQURNSU4QAhIqCiZSRUFESU5FU1NfQ0xFQVJFRF9CWV9QTEFURk9STV9PUEVSQVRPUhADEh8KG1JFQURJTkVTU19DTEVBUkVEX0JZX05PQk9EWRAEKp8CCg9SZWFkaW5lc3NBY3Rpb24SIAocUkVBRElORVNTX0FDVElPTl9VTlNQRUNJRklFRBAAEikKJVJFQURJTkVTU19BQ1RJT05fTk9ORV9BVF9USElTX1NVUkZBQ0UQARInCiNSRUFESU5FU1NfQUNUSU9OX1JFVFJZX0lOU1RBTExfU1RFUBACEiUKIVJFQURJTkVTU19BQ1RJT05fUkVDT05ORUNUX1NPVVJDRRADEicKI1JFQURJTkVTU19BQ1RJT05fVFJBTlNGRVJfT1dORVJTSElQEAQSKwonUkVBRElORVNTX0FDVElPTl9JTlNUQUxMX1JFQUNIRURfTU9EVUxFEAUSGQoVUkVBRElORVNTX0FDVElPTl9XQUlUEAYyqhIKE0luc3RhbGxhdGlvblNlcnZpY2USugEKD0luc3RhbGxTb2x1dGlvbhIoLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbFNvbHV0aW9uUmVxdWVzdBoeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uIl3C8xg9CAIQBCoMCgZvcmdfaWQQAhgBMAE6HQoZc2Fhcy5pbnN0YWxsYXRpb24uY3JlYXRlZBACQAFIBFADWANgAYLT5JMCFjoBKiIRL3YxL2luc3RhbGxhdGlvbnMS0gEKEVVuaW5zdGFsbFNvbHV0aW9uEiouc2Fhcy5hY2NvdW50cy52MS5Vbmluc3RhbGxTb2x1dGlvblJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiecLzGD0IAhAEKgwKBm9yZ19pZBACGAEwATodChlzYWFzLmluc3RhbGxhdGlvbi5yZXZva2VkEAJAAUgEUANYA2ABgtPkkwIyOgEqIi0vdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfTp1bmluc3RhbGwS0gEKEk5hcnJvd0luc3RhbGxhdGlvbhIrLnNhYXMuYWNjb3VudHMudjEuTmFycm93SW5zdGFsbGF0aW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJ3wvMYPggCEAQqDAoGb3JnX2lkEAIYATABOh4KGnNhYXMuaW5zdGFsbGF0aW9uLm5hcnJvd2VkEAJAAUgEUANYA2ABgtPkkwIvOgEqIiovdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfTpuYXJyb3cS1gEKEVJldHJ5SW5zdGFsbGF0aW9uEiouc2Fhcy5hY2NvdW50cy52MS5SZXRyeUluc3RhbGxhdGlvblJlcXVlc3QaHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbiJ1wvMYPQgCEAQqDAoGb3JnX2lkEAIYATABOh0KGXNhYXMuaW5zdGFsbGF0aW9uLnJldHJpZWQQAkABSARQA1gDYAGC0+STAi46ASoiKS92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9OnJldHJ5EokCCh1UcmFuc2Zlckluc3RhbGxhdGlvbk93bmVyc2hpcBI2LnNhYXMuYWNjb3VudHMudjEuVHJhbnNmZXJJbnN0YWxsYXRpb25Pd25lcnNoaXBSZXF1ZXN0Gh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24ijwHC8xhLCAIQBCoMCgZvcmdfaWQQAhgBMAE6Kwonc2Fhcy5pbnN0YWxsYXRpb24ub3duZXJzaGlwX3RyYW5zZmVycmVkEAJAAUgEUANYA2ABgtPkkwI6OgEqIjUvdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfTp0cmFuc2Zlck93bmVyc2hpcBK5AQoPR2V0SW5zdGFsbGF0aW9uEiguc2Fhcy5hY2NvdW50cy52MS5HZXRJbnN0YWxsYXRpb25SZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5HZXRJbnN0YWxsYXRpb25SZXNwb25zZSJRwvMYIggCEAMqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAiUSIy92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9EoYBChFMaXN0SW5zdGFsbGF0aW9ucxIqLnNhYXMuYWNjb3VudHMudjEuTGlzdEluc3RhbGxhdGlvbnNSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5MaXN0SW5zdGFsbGF0aW9uc1Jlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESygEKFkxpc3RBdmFpbGFibGVTb2x1dGlvbnMSLy5zYWFzLmFjY291bnRzLnYxLkxpc3RBdmFpbGFibGVTb2x1dGlvbnNSZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5MaXN0QXZhaWxhYmxlU29sdXRpb25zUmVzcG9uc2UiTcLzGCIIAhAEKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIhEh8vdjEvaW5zdGFsbGF0aW9uczpsaXN0QXZhaWxhYmxlEtEBCh1MaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9ucxI2LnNhYXMuYWNjb3VudHMudjEuTGlzdE9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbnNSZXF1ZXN0Gjcuc2Fhcy5hY2NvdW50cy52MS5MaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1Jlc3BvbnNlIj/C8xgiCAIQBCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCExIRL3YxL2luc3RhbGxhdGlvbnMS8AEKF0dyYW50SW5zdGFsbGF0aW9uVG9UZWFtEjAuc2Fhcy5hY2NvdW50cy52MS5HcmFudEluc3RhbGxhdGlvblRvVGVhbVJlcXVlc3QaMS5zYWFzLmFjY291bnRzLnYxLkdyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVzcG9uc2UicMLzGDgIAhAEKgwKBm9yZ19pZBACGAEwAToWChJzYWFzLnNjb3BlLmdyYW50ZWQQAkABSARQA1gDYAF4AoLT5JMCLjoBKiIpL3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH0vdGVhbXMS7QEKGlJldm9rZUluc3RhbGxhdGlvbkZyb21UZWFtEjMuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VJbnN0YWxsYXRpb25Gcm9tVGVhbVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkigQHC8xg4CAIQBCoMCgZvcmdfaWQQAhgBMAE6FgoSc2Fhcy5zY29wZS5yZXZva2VkEAJAAUgEUANYA2ABeAKC0+STAj86ASoiOi92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9L3RlYW1zL3t0ZWFtX2lkfTpyZXZva2ViBnByb3RvMw", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_policy_v1_options]); - -/** - * Installation is one solution's bound identity in an org. Server-assigned ids; - * the browser cannot attest to tenancy, the agent principal, or the scope node. - * - * @generated from message saas.accounts.v1.Installation - */ -export type Installation = Message<"saas.accounts.v1.Installation"> & { - /** - * @generated from field: string id = 1; - */ - id: string; - - /** - * @generated from field: string org_id = 2; - */ - orgId: string; - - /** - * @generated from field: string agent_principal_id = 3; - */ - agentPrincipalId: string; - - /** - * @generated from field: string owner_principal_id = 5; - */ - ownerPrincipalId: string; - - /** - * @generated from field: repeated string co_owner_principal_ids = 6; - */ - coOwnerPrincipalIds: string[]; - - /** - * @generated from field: string root_scope_node_id = 7; - */ - rootScopeNodeId: string; - - /** - * @generated from field: saas.accounts.v1.InstallationStatus status = 8; - */ - status: InstallationStatus; - - /** - * @generated from field: google.protobuf.Timestamp created_at = 9; - */ - createdAt?: Timestamp; - - /** - * @generated from field: optional google.protobuf.Timestamp revoked_at = 10; - */ - revokedAt?: Timestamp; - - /** - * The immutable solution target this installation was consented to: one - * continuous period of one binding's presence on this host. It is never - * reused, so a replacement binding — even one claiming the same route alias — - * is a different target and inherits nothing. - * - * Deliberately NOT accompanied by the route alias. The alias is a property of - * the target that a later generation may move, and resolving it here would - * mean joining the host's presence state into a request-path listing — which - * would require granting request traffic read over every organisation's - * presence to answer about one. A consumer that needs the alias asks - * ListAvailableSolutions, which serves accepted applied state, and joins on - * this id. - * - * Empty only on a revoked historical row whose target could not be - * established at the cutover; an active installation always names one. - * - * @generated from field: string target_id = 11; - */ - targetId: string; - - /** - * Why a revoked installation was revoked, when it was not an administrator - * uninstalling: the installation-identity cutover, or a tenancy narrowing that - * withdrew this organisation. Empty on an active row. - * - * @generated from field: string revoked_reason = 12; - */ - revokedReason: string; - - /** - * Why the install step failed: the solution's bounded refusal reason, or a - * fixed reason when it never answered. Empty unless status is FAILED. - * - * @generated from field: string failure_reason = 13; - */ - failureReason: string; -}; - -/** - * Describes the message saas.accounts.v1.Installation. - * Use `create(InstallationSchema)` to create a new message. - */ -export const InstallationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 0); + fileDesc("CiRzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbnMucHJvdG8SEHNhYXMuYWNjb3VudHMudjEi4AMKFkluc3RhbGxTb2x1dGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIkChBhZ2VudF9pZGVudGlmaWVyGAIgASgJQgq6SAdyBRABGIAEEh4KDGRpc3BsYXlfbmFtZRgEIAEoCUIIukgFcgMYgAQSIgoQcm9vdF9zY29wZV9sYWJlbBgGIAEoCUIIukgFcgMYgAQSGQoHcm9sZV9pZBgHIAEoCUIIukgFcgOwAQESJwoSb3duZXJfcHJpbmNpcGFsX2lkGAggASgJQgu6SAjYAQFyA7ABARIvChZjb19vd25lcl9wcmluY2lwYWxfaWRzGAkgAygJQg+6SAySAQkQICIFcgOwAQESKgoRYWxsb3dlZF9hdWRpZW5jZXMYCiADKAlCD7pIDJIBCRBAIgVyAxiABBInCg5hbGxvd2VkX3Njb3BlcxgLIAMoCUIPukgMkgEJEEAiBXIDGIABEhsKCXRhcmdldF9pZBgMIAEoCUIIukgFcgOwAQESHwoFc2NvcGUYDSABKAlCC7pICHIGEAIogIBASACIAQFCCAoGX3Njb3BlSgQIAxAESgQIBRAGUhNzb2x1dGlvbl9pZGVudGlmaWVyUg9yb290X3Njb3BlX3BhdGgidAoZTmFycm93SW5zdGFsbGF0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESGgoFc2NvcGUYAyABKAlCC7pICHIGEAIogIBAIlcKGFJldHJ5SW5zdGFsbGF0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEiVwoYVW5pbnN0YWxsU29sdXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABASK+AQokVHJhbnNmZXJJbnN0YWxsYXRpb25Pd25lcnNoaXBSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABARIoChZuZXdfb3duZXJfcHJpbmNpcGFsX2lkGAMgASgJQgi6SAVyA7ABARIvChZjb19vd25lcl9wcmluY2lwYWxfaWRzGAQgAygJQg+6SAySAQkQICIFcgOwAQEiVQoWR2V0SW5zdGFsbGF0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEihQEKF0dldEluc3RhbGxhdGlvblJlc3BvbnNlEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEjQKBmhlYWx0aBgCIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uSGVhbHRoInMKJExpc3RPcmdhbml6YXRpb25JbnN0YWxsYXRpb25zUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEh0KCXBhZ2Vfc2l6ZRgCIAEoBUIKukgHGgUY9AMoABISCgpwYWdlX3Rva2VuGAMgASgJIsgDChhPcmdhbml6YXRpb25JbnN0YWxsYXRpb24SNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SNAoGaGVhbHRoGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25IZWFsdGgSMwoGdGFyZ2V0GAMgASgLMiMuc2Fhcy5hY2NvdW50cy52MS5BdmFpbGFibGVTb2x1dGlvbhIXCg9yb290X3Njb3BlX3BhdGgYBCABKAkSPAoNZ3JhbnRlZF90ZWFtcxgFIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ29sbGVjdGlvblJlYWRHcmFudBI+Cg9pbmhlcml0ZWRfdGVhbXMYBiADKAsyJS5zYWFzLmFjY291bnRzLnYxLkNvbGxlY3Rpb25SZWFkR3JhbnQSOgoJcmVhZGluZXNzGAcgASgLMicuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25SZWFkaW5lc3MSOAoPcmVhY2hlZF9tb2R1bGVzGAggAygLMh8uc2Fhcy5hY2NvdW50cy52MS5SZWFjaGVkTW9kdWxlIu0BChJSZWFkaW5lc3NDb25kaXRpb24SNgoEa2luZBgBIAEoDjIoLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQ29uZGl0aW9uS2luZBIPCgdzdWJqZWN0GAIgASgJEg4KBmRldGFpbBgDIAEoCRI4CgpjbGVhcmVkX2J5GAQgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5SZWFkaW5lc3NDbGVhcmVkQnkSMQoGYWN0aW9uGAUgASgOMiEuc2Fhcy5hY2NvdW50cy52MS5SZWFkaW5lc3NBY3Rpb24SEQoJc291cmNlX2lkGAYgASgJIlsKFUluc3RhbGxhdGlvblJlYWRpbmVzcxINCgVyZWFkeRgBIAEoCBIzCgV1bm1ldBgCIAMoCzIkLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQ29uZGl0aW9uIk8KDVJlYWNoZWRNb2R1bGUSEAoIYXVkaWVuY2UYASABKAkSFwoPaW5zdGFsbGF0aW9uX2lkGAIgASgJEhMKC3JvdXRlX2FsaWFzGAMgASgJIoMBCiVMaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1Jlc3BvbnNlEkEKDWluc3RhbGxhdGlvbnMYASADKAsyKi5zYWFzLmFjY291bnRzLnYxLk9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkikwEKHkdyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESGQoHdGVhbV9pZBgDIAEoCUIIukgFcgOwAQESGQoHcm9sZV9pZBgEIAEoCUIIukgFcgOwAQEidQoTQ2FycmllZE1vZHVsZUFjY2VzcxIQCghhdWRpZW5jZRgBIAEoCRIXCg9pbnN0YWxsYXRpb25faWQYAiABKAkSEgoKc2NvcGVfcGF0aBgDIAEoCRIPCgdyb2xlX2lkGAQgASgJEg4KBnNjb3BlcxgFIAMoCSK7AQofR3JhbnRJbnN0YWxsYXRpb25Ub1RlYW1SZXNwb25zZRIrCgVncmFudBgBIAEoCzIcLnNhYXMuYWNjb3VudHMudjEuU2NvcGVHcmFudBI2CgdjYXJyaWVkGAIgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5DYXJyaWVkTW9kdWxlQWNjZXNzEjMKBXVubWV0GAMgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5SZWFkaW5lc3NDb25kaXRpb24ilgEKIVJldm9rZUluc3RhbGxhdGlvbkZyb21UZWFtUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESGQoHdGVhbV9pZBgDIAEoCUIIukgFcgOwAQESGQoHcm9sZV9pZBgEIAEoCUIIukgFcgOwAQEihAIKEUF2YWlsYWJsZVNvbHV0aW9uEhsKCXRhcmdldF9pZBgBIAEoCUIIukgFcgOwAQESEgoKYmluZGluZ19pZBgCIAEoCRITCgtyb3V0ZV9hbGlhcxgDIAEoCRIZChFyZWxlYXNlX3B1Ymxpc2hlchgEIAEoCRIUCgxyZWxlYXNlX25hbWUYBSABKAkSFwoPcmVsZWFzZV92ZXJzaW9uGAYgASgJEhkKEW9wZW5lZF9nZW5lcmF0aW9uGAcgASgEEhoKEmFwcGxpZWRfZ2VuZXJhdGlvbhgIIAEoBBIRCglpbnN0YWxsZWQYCSABKAgSFQoEa2luZBgKIAEoCUIHukgEcgIYICJsCh1MaXN0QXZhaWxhYmxlU29sdXRpb25zUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEh0KCXBhZ2Vfc2l6ZRgCIAEoBUIKukgHGgUY9AMoABISCgpwYWdlX3Rva2VuGAMgASgJInEKHkxpc3RBdmFpbGFibGVTb2x1dGlvbnNSZXNwb25zZRI2Cglzb2x1dGlvbnMYASADKAsyIy5zYWFzLmFjY291bnRzLnYxLkF2YWlsYWJsZVNvbHV0aW9uEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSrzAwoWUmVhZGluZXNzQ29uZGl0aW9uS2luZBIoCiRSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfVU5TUEVDSUZJRUQQABIxCi1SRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfSU5TVEFMTF9TVEVQX1BFTkRJTkcQARIwCixSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfSU5TVEFMTF9TVEVQX0ZBSUxFRBACEi0KKVJFQURJTkVTU19DT05ESVRJT05fS0lORF9SRUNPUkRfVU5IRUFMVEhZEAMSNQoxUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX01PRFVMRV9QUklOQ0lQQUxfT01JVFRFRBAEEjYKMlJFQURJTkVTU19DT05ESVRJT05fS0lORF9PUEVSQVRJT05fQklORElOR19PTUlUVEVEEAUSMQotUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX1NPVVJDRV9OT1RfREVMRUdBVEVEEAYSOQo1UkVBRElORVNTX0NPTkRJVElPTl9LSU5EX1JFQUNIRURfTU9EVUxFX05PVF9JTlNUQUxMRUQQBxI+CjpSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfQ0FSUklFRF9NT0RVTEVfQUNDRVNTX05PVF9ERVJJVkVEEAgq1QEKElJlYWRpbmVzc0NsZWFyZWRCeRIkCiBSRUFESU5FU1NfQ0xFQVJFRF9CWV9VTlNQRUNJRklFRBAAEh8KG1JFQURJTkVTU19DTEVBUkVEX0JZX1ZJRVdFUhABEisKJ1JFQURJTkVTU19DTEVBUkVEX0JZX09SR0FOSVpBVElPTl9BRE1JThACEioKJlJFQURJTkVTU19DTEVBUkVEX0JZX1BMQVRGT1JNX09QRVJBVE9SEAMSHwobUkVBRElORVNTX0NMRUFSRURfQllfTk9CT0RZEAQqnwIKD1JlYWRpbmVzc0FjdGlvbhIgChxSRUFESU5FU1NfQUNUSU9OX1VOU1BFQ0lGSUVEEAASKQolUkVBRElORVNTX0FDVElPTl9OT05FX0FUX1RISVNfU1VSRkFDRRABEicKI1JFQURJTkVTU19BQ1RJT05fUkVUUllfSU5TVEFMTF9TVEVQEAISJQohUkVBRElORVNTX0FDVElPTl9SRUNPTk5FQ1RfU09VUkNFEAMSJwojUkVBRElORVNTX0FDVElPTl9UUkFOU0ZFUl9PV05FUlNISVAQBBIrCidSRUFESU5FU1NfQUNUSU9OX0lOU1RBTExfUkVBQ0hFRF9NT0RVTEUQBRIZChVSRUFESU5FU1NfQUNUSU9OX1dBSVQQBjKqEgoTSW5zdGFsbGF0aW9uU2VydmljZRK6AQoPSW5zdGFsbFNvbHV0aW9uEiguc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsU29sdXRpb25SZXF1ZXN0Gh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24iXcLzGD0IAhAEKgwKBm9yZ19pZBACGAEwATodChlzYWFzLmluc3RhbGxhdGlvbi5jcmVhdGVkEAJAAUgEUANYA2ABgtPkkwIWOgEqIhEvdjEvaW5zdGFsbGF0aW9ucxLSAQoRVW5pbnN0YWxsU29sdXRpb24SKi5zYWFzLmFjY291bnRzLnYxLlVuaW5zdGFsbFNvbHV0aW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJ5wvMYPQgCEAQqDAoGb3JnX2lkEAIYATABOh0KGXNhYXMuaW5zdGFsbGF0aW9uLnJldm9rZWQQAkABSARQA1gDYAGC0+STAjI6ASoiLS92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9OnVuaW5zdGFsbBLSAQoSTmFycm93SW5zdGFsbGF0aW9uEisuc2Fhcy5hY2NvdW50cy52MS5OYXJyb3dJbnN0YWxsYXRpb25SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5InfC8xg+CAIQBCoMCgZvcmdfaWQQAhgBMAE6Hgoac2Fhcy5pbnN0YWxsYXRpb24ubmFycm93ZWQQAkABSARQA1gDYAGC0+STAi86ASoiKi92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9Om5hcnJvdxLWAQoRUmV0cnlJbnN0YWxsYXRpb24SKi5zYWFzLmFjY291bnRzLnYxLlJldHJ5SW5zdGFsbGF0aW9uUmVxdWVzdBoeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uInXC8xg9CAIQBCoMCgZvcmdfaWQQAhgBMAE6HQoZc2Fhcy5pbnN0YWxsYXRpb24ucmV0cmllZBACQAFIBFADWANgAYLT5JMCLjoBKiIpL3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH06cmV0cnkSiQIKHVRyYW5zZmVySW5zdGFsbGF0aW9uT3duZXJzaGlwEjYuc2Fhcy5hY2NvdW50cy52MS5UcmFuc2Zlckluc3RhbGxhdGlvbk93bmVyc2hpcFJlcXVlc3QaHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbiKPAcLzGEsIAhAEKgwKBm9yZ19pZBACGAEwATorCidzYWFzLmluc3RhbGxhdGlvbi5vd25lcnNoaXBfdHJhbnNmZXJyZWQQAkABSARQA1gDYAGC0+STAjo6ASoiNS92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9OnRyYW5zZmVyT3duZXJzaGlwErkBCg9HZXRJbnN0YWxsYXRpb24SKC5zYWFzLmFjY291bnRzLnYxLkdldEluc3RhbGxhdGlvblJlcXVlc3QaKS5zYWFzLmFjY291bnRzLnYxLkdldEluc3RhbGxhdGlvblJlc3BvbnNlIlHC8xgiCAIQAyoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCJRIjL3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH0ShgEKEUxpc3RJbnN0YWxsYXRpb25zEiouc2Fhcy5hY2NvdW50cy52MS5MaXN0SW5zdGFsbGF0aW9uc1JlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLkxpc3RJbnN0YWxsYXRpb25zUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARLKAQoWTGlzdEF2YWlsYWJsZVNvbHV0aW9ucxIvLnNhYXMuYWNjb3VudHMudjEuTGlzdEF2YWlsYWJsZVNvbHV0aW9uc1JlcXVlc3QaMC5zYWFzLmFjY291bnRzLnYxLkxpc3RBdmFpbGFibGVTb2x1dGlvbnNSZXNwb25zZSJNwvMYIggCEAQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAiESHy92MS9pbnN0YWxsYXRpb25zOmxpc3RBdmFpbGFibGUS0QEKHUxpc3RPcmdhbml6YXRpb25JbnN0YWxsYXRpb25zEjYuc2Fhcy5hY2NvdW50cy52MS5MaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1JlcXVlc3QaNy5zYWFzLmFjY291bnRzLnYxLkxpc3RPcmdhbml6YXRpb25JbnN0YWxsYXRpb25zUmVzcG9uc2UiP8LzGCIIAhAEKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwITEhEvdjEvaW5zdGFsbGF0aW9ucxLwAQoXR3JhbnRJbnN0YWxsYXRpb25Ub1RlYW0SMC5zYWFzLmFjY291bnRzLnYxLkdyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVxdWVzdBoxLnNhYXMuYWNjb3VudHMudjEuR3JhbnRJbnN0YWxsYXRpb25Ub1RlYW1SZXNwb25zZSJwwvMYOAgCEAQqDAoGb3JnX2lkEAIYATABOhYKEnNhYXMuc2NvcGUuZ3JhbnRlZBACQAFIBFADWANgAXgCgtPkkwIuOgEqIikvdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfS90ZWFtcxLtAQoaUmV2b2tlSW5zdGFsbGF0aW9uRnJvbVRlYW0SMy5zYWFzLmFjY291bnRzLnYxLlJldm9rZUluc3RhbGxhdGlvbkZyb21UZWFtUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSKBAcLzGDgIAhAEKgwKBm9yZ19pZBACGAEwAToWChJzYWFzLnNjb3BlLnJldm9rZWQQAkABSARQA1gDYAF4AoLT5JMCPzoBKiI6L3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH0vdGVhbXMve3RlYW1faWR9OnJldm9rZWIGcHJvdG8z", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_saas_accounts_v1_authorization, file_saas_accounts_v1_installation_listing, file_saas_policy_v1_options]); /** * @generated from message saas.accounts.v1.InstallSolutionRequest @@ -229,7 +133,7 @@ export type InstallSolutionRequest = Message<"saas.accounts.v1.InstallSolutionRe * Use `create(InstallSolutionRequestSchema)` to create a new message. */ export const InstallSolutionRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 1); + messageDesc(file_saas_accounts_v1_installations, 0); /** * An org administrator may only remove installed authority. The host reads @@ -261,7 +165,7 @@ export type NarrowInstallationRequest = Message<"saas.accounts.v1.NarrowInstalla * Use `create(NarrowInstallationRequestSchema)` to create a new message. */ export const NarrowInstallationRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 2); + messageDesc(file_saas_accounts_v1_installations, 1); /** * @generated from message saas.accounts.v1.RetryInstallationRequest @@ -283,7 +187,7 @@ export type RetryInstallationRequest = Message<"saas.accounts.v1.RetryInstallati * Use `create(RetryInstallationRequestSchema)` to create a new message. */ export const RetryInstallationRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 3); + messageDesc(file_saas_accounts_v1_installations, 2); /** * @generated from message saas.accounts.v1.UninstallSolutionRequest @@ -305,7 +209,7 @@ export type UninstallSolutionRequest = Message<"saas.accounts.v1.UninstallSoluti * Use `create(UninstallSolutionRequestSchema)` to create a new message. */ export const UninstallSolutionRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 4); + messageDesc(file_saas_accounts_v1_installations, 3); /** * @generated from message saas.accounts.v1.TransferInstallationOwnershipRequest @@ -341,7 +245,7 @@ export type TransferInstallationOwnershipRequest = Message<"saas.accounts.v1.Tra * Use `create(TransferInstallationOwnershipRequestSchema)` to create a new message. */ export const TransferInstallationOwnershipRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 5); + messageDesc(file_saas_accounts_v1_installations, 4); /** * @generated from message saas.accounts.v1.GetInstallationRequest @@ -363,7 +267,7 @@ export type GetInstallationRequest = Message<"saas.accounts.v1.GetInstallationRe * Use `create(GetInstallationRequestSchema)` to create a new message. */ export const GetInstallationRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 6); + messageDesc(file_saas_accounts_v1_installations, 5); /** * @generated from message saas.accounts.v1.GetInstallationResponse @@ -387,105 +291,7 @@ export type GetInstallationResponse = Message<"saas.accounts.v1.GetInstallationR * Use `create(GetInstallationResponseSchema)` to create a new message. */ export const GetInstallationResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 7); - -/** - * ListInstallationsRequest asks what one organization has installed. Every other - * RPC on this service names a single installation, so until this existed nothing - * could answer "what has this org installed?" — which is the authority read a - * per-organization solution projection is built on (issue #949). - * - * @generated from message saas.accounts.v1.ListInstallationsRequest - */ -export type ListInstallationsRequest = Message<"saas.accounts.v1.ListInstallationsRequest"> & { - /** - * @generated from field: string org_id = 1; - */ - orgId: string; - - /** - * Restrict to one lifecycle status. UNSPECIFIED returns every status, so a - * caller that must see only live installs asks for ACTIVE explicitly rather - * than leaning on a default that would silently widen if the enum grew. - * - * @generated from field: saas.accounts.v1.InstallationStatus status = 2; - */ - status: InstallationStatus; - - /** - * Page bound. 0 -> server default; capped so an organization with many - * installations can never return an unbounded result in one call. - * - * @generated from field: int32 page_size = 3; - */ - pageSize: number; - - /** - * Opaque cursor from a previous response's next_page_token; empty for page one. - * - * @generated from field: string page_token = 4; - */ - pageToken: string; -}; - -/** - * Describes the message saas.accounts.v1.ListInstallationsRequest. - * Use `create(ListInstallationsRequestSchema)` to create a new message. - */ -export const ListInstallationsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 8); - -/** - * InstallationSummary is one row of the listing: the installation, plus the health - * resolved live beside it exactly as GetInstallation resolves it for one. Health - * travels with the row because a consumer that renders an installed solution as - * unavailable would otherwise need one GetInstallation per installation. - * - * @generated from message saas.accounts.v1.InstallationSummary - */ -export type InstallationSummary = Message<"saas.accounts.v1.InstallationSummary"> & { - /** - * @generated from field: saas.accounts.v1.Installation installation = 1; - */ - installation?: Installation; - - /** - * @generated from field: saas.accounts.v1.InstallationHealth health = 2; - */ - health: InstallationHealth; -}; - -/** - * Describes the message saas.accounts.v1.InstallationSummary. - * Use `create(InstallationSummarySchema)` to create a new message. - */ -export const InstallationSummarySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 9); - -/** - * @generated from message saas.accounts.v1.ListInstallationsResponse - */ -export type ListInstallationsResponse = Message<"saas.accounts.v1.ListInstallationsResponse"> & { - /** - * @generated from field: repeated saas.accounts.v1.InstallationSummary installations = 1; - */ - installations: InstallationSummary[]; - - /** - * Set when more installations remain; pass it back as page_token. Empty on the - * last page. - * - * @generated from field: string next_page_token = 2; - */ - nextPageToken: string; -}; - -/** - * Describes the message saas.accounts.v1.ListInstallationsResponse. - * Use `create(ListInstallationsResponseSchema)` to create a new message. - */ -export const ListInstallationsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 10); + messageDesc(file_saas_accounts_v1_installations, 6); /** * @generated from message saas.accounts.v1.ListOrganizationInstallationsRequest @@ -516,7 +322,7 @@ export type ListOrganizationInstallationsRequest = Message<"saas.accounts.v1.Lis * Use `create(ListOrganizationInstallationsRequestSchema)` to create a new message. */ export const ListOrganizationInstallationsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 11); + messageDesc(file_saas_accounts_v1_installations, 7); /** * OrganizationInstallation is one active installation as its organisation's @@ -595,7 +401,7 @@ export type OrganizationInstallation = Message<"saas.accounts.v1.OrganizationIns * Use `create(OrganizationInstallationSchema)` to create a new message. */ export const OrganizationInstallationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 12); + messageDesc(file_saas_accounts_v1_installations, 8); /** * ReadinessCondition is one thing that is unmet, in the words of whoever owns @@ -648,7 +454,7 @@ export type ReadinessCondition = Message<"saas.accounts.v1.ReadinessCondition"> * Use `create(ReadinessConditionSchema)` to create a new message. */ export const ReadinessConditionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 13); + messageDesc(file_saas_accounts_v1_installations, 9); /** * InstallationReadiness is whether the installation will work, which is NOT @@ -679,7 +485,7 @@ export type InstallationReadiness = Message<"saas.accounts.v1.InstallationReadin * Use `create(InstallationReadinessSchema)` to create a new message. */ export const InstallationReadinessSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 14); + messageDesc(file_saas_accounts_v1_installations, 10); /** * ReachedModule is one module an installed target declares it reaches on the @@ -728,7 +534,7 @@ export type ReachedModule = Message<"saas.accounts.v1.ReachedModule"> & { * Use `create(ReachedModuleSchema)` to create a new message. */ export const ReachedModuleSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 15); + messageDesc(file_saas_accounts_v1_installations, 11); /** * @generated from message saas.accounts.v1.ListOrganizationInstallationsResponse @@ -752,7 +558,7 @@ export type ListOrganizationInstallationsResponse = Message<"saas.accounts.v1.Li * Use `create(ListOrganizationInstallationsResponseSchema)` to create a new message. */ export const ListOrganizationInstallationsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 16); + messageDesc(file_saas_accounts_v1_installations, 12); /** * @generated from message saas.accounts.v1.GrantInstallationToTeamRequest @@ -789,7 +595,7 @@ export type GrantInstallationToTeamRequest = Message<"saas.accounts.v1.GrantInst * Use `create(GrantInstallationToTeamRequestSchema)` to create a new message. */ export const GrantInstallationToTeamRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 17); + messageDesc(file_saas_accounts_v1_installations, 13); /** * CarriedModuleAccess is one derived grant a solution's grant carried: the @@ -852,7 +658,7 @@ export type CarriedModuleAccess = Message<"saas.accounts.v1.CarriedModuleAccess" * Use `create(CarriedModuleAccessSchema)` to create a new message. */ export const CarriedModuleAccessSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 18); + messageDesc(file_saas_accounts_v1_installations, 14); /** * @generated from message saas.accounts.v1.GrantInstallationToTeamResponse @@ -890,7 +696,7 @@ export type GrantInstallationToTeamResponse = Message<"saas.accounts.v1.GrantIns * Use `create(GrantInstallationToTeamResponseSchema)` to create a new message. */ export const GrantInstallationToTeamResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 19); + messageDesc(file_saas_accounts_v1_installations, 15); /** * @generated from message saas.accounts.v1.RevokeInstallationFromTeamRequest @@ -925,7 +731,7 @@ export type RevokeInstallationFromTeamRequest = Message<"saas.accounts.v1.Revoke * Use `create(RevokeInstallationFromTeamRequestSchema)` to create a new message. */ export const RevokeInstallationFromTeamRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 20); + messageDesc(file_saas_accounts_v1_installations, 16); /** * AvailableSolution is one installable solution target: its immutable identity, @@ -1015,7 +821,7 @@ export type AvailableSolution = Message<"saas.accounts.v1.AvailableSolution"> & * Use `create(AvailableSolutionSchema)` to create a new message. */ export const AvailableSolutionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 21); + messageDesc(file_saas_accounts_v1_installations, 17); /** * @generated from message saas.accounts.v1.ListAvailableSolutionsRequest @@ -1045,7 +851,7 @@ export type ListAvailableSolutionsRequest = Message<"saas.accounts.v1.ListAvaila * Use `create(ListAvailableSolutionsRequestSchema)` to create a new message. */ export const ListAvailableSolutionsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 22); + messageDesc(file_saas_accounts_v1_installations, 18); /** * @generated from message saas.accounts.v1.ListAvailableSolutionsResponse @@ -1067,101 +873,7 @@ export type ListAvailableSolutionsResponse = Message<"saas.accounts.v1.ListAvail * Use `create(ListAvailableSolutionsResponseSchema)` to create a new message. */ export const ListAvailableSolutionsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 23); - -/** - * InstallationStatus is the lifecycle of an installation row. An uninstall is a - * soft delete (status=revoked, revoked_at set) so the agent, grants, and audit - * trail remain resolvable. - * - * @generated from enum saas.accounts.v1.InstallationStatus - */ -export enum InstallationStatus { - /** - * @generated from enum value: INSTALLATION_STATUS_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - - /** - * @generated from enum value: INSTALLATION_STATUS_ACTIVE = 1; - */ - ACTIVE = 1, - - /** - * @generated from enum value: INSTALLATION_STATUS_REVOKED = 2; - */ - REVOKED = 2, - - /** - * Installed, and waiting for the solution to acknowledge its standard install - * step. Nothing can use a pending installation. - * - * @generated from enum value: INSTALLATION_STATUS_PENDING = 3; - */ - PENDING = 3, - - /** - * The solution refused its install step, or never answered within the - * attempt budget. Nothing can use it; an administrator may retry it. - * - * @generated from enum value: INSTALLATION_STATUS_FAILED = 4; - */ - FAILED = 4, -} - -/** - * Describes the enum saas.accounts.v1.InstallationStatus. - */ -export const InstallationStatusSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_saas_accounts_v1_installations, 0); - -/** - * InstallationHealth is resolved live from current authority facts, never - * stored. It is the observable form of the fail-closed mint rule: a - * StartInstallationTask fails exactly when health is not HEALTHY, and transfer - * or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record - * nor any co-owner is currently an org admin (the Zapier-class "connection owner - * left the org" failure), surfaced rather than silently reassigned. - * - * @generated from enum saas.accounts.v1.InstallationHealth - */ -export enum InstallationHealth { - /** - * @generated from enum value: INSTALLATION_HEALTH_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - - /** - * @generated from enum value: INSTALLATION_HEALTH_HEALTHY = 1; - */ - HEALTHY = 1, - - /** - * @generated from enum value: INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER = 2; - */ - NO_ELIGIBLE_OWNER = 2, - - /** - * @generated from enum value: INSTALLATION_HEALTH_AGENT_REVOKED = 3; - */ - AGENT_REVOKED = 3, - - /** - * @generated from enum value: INSTALLATION_HEALTH_AGENT_DISABLED = 4; - */ - AGENT_DISABLED = 4, - - /** - * @generated from enum value: INSTALLATION_HEALTH_STANDING_GRANT_MISSING = 5; - */ - STANDING_GRANT_MISSING = 5, -} - -/** - * Describes the enum saas.accounts.v1.InstallationHealth. - */ -export const InstallationHealthSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_saas_accounts_v1_installations, 1); + messageDesc(file_saas_accounts_v1_installations, 19); /** * ReadinessConditionKind is what kind of thing is unmet. A reader acts on the @@ -1249,7 +961,7 @@ export enum ReadinessConditionKind { * Describes the enum saas.accounts.v1.ReadinessConditionKind. */ export const ReadinessConditionKindSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_saas_accounts_v1_installations, 2); + enumDesc(file_saas_accounts_v1_installations, 0); /** * ReadinessClearedBy is who can clear a condition. It is stated rather than @@ -1294,7 +1006,7 @@ export enum ReadinessClearedBy { * Describes the enum saas.accounts.v1.ReadinessClearedBy. */ export const ReadinessClearedBySchema: GenEnum = /*@__PURE__*/ - enumDesc(file_saas_accounts_v1_installations, 3); + enumDesc(file_saas_accounts_v1_installations, 1); /** * ReadinessAction is what clears the condition, named as the act rather than as @@ -1347,7 +1059,7 @@ export enum ReadinessAction { * Describes the enum saas.accounts.v1.ReadinessAction. */ export const ReadinessActionSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_saas_accounts_v1_installations, 4); + enumDesc(file_saas_accounts_v1_installations, 2); /** * InstallationService composes and administers installations. The headless mint @@ -1434,11 +1146,15 @@ export const InstallationService: GenService<{ * ListInstallations enumerates one organization's installations with their live * health. It is internal-tier on purpose: it takes the organization as a request * field, so only a caller holding the cluster-internal credential may ask, and - * the tenant it asks about is the one the auth-gateway projected from a verified - * identity — never one a browser supplied. It sits beside - * PermissionService.ListAccessibleScopes for that reason: the two are read - * together to answer what a viewer's organization installed and what that viewer - * was granted. + * the tenant it asks about is never one a browser supplied. The auth-gateway is + * its caller, for two reads. Its viewer projection asks about the tenant it + * projected from a verified identity; it sits beside + * PermissionService.ListAccessibleScopes for that reason, the two read together + * to answer what a viewer's organization installed and what that viewer was + * granted. Its declared module route's bearer-less arm asks about the tenant an + * edge-verified Work Context names, a value this issuer signed, to admit a + * headless operation context only when the target is installed there and the + * context is sealed to another active installation. * * @generated from rpc saas.accounts.v1.InstallationService.ListInstallations */ @@ -1479,7 +1195,8 @@ export const InstallationService: GenService<{ * It is the org-admin companion to the internal ListInstallations, and it is a * separate method rather than a relaxed policy on that one: ListInstallations * is read by the gateway for an organisation it projected from a verified - * identity, and a bare tenant credential must never reach it. This one binds + * identity or that an edge-verified Work Context names, and a bare tenant + * credential must never reach it. This one binds * the organisation to the caller's own admin membership, exactly as * ListAvailableSolutions does, so no organisation's administrator can read * another's. The one exception is inherited, not added here: a platform diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts index d1a1a0dcf..fcc7d03ce 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts @@ -12,8 +12,8 @@ import type { CollectionReadGrant } from "./authorization_pb"; import { file_saas_accounts_v1_authorization } from "./authorization_pb"; import type { Organization, OrgMembership, User } from "./common_pb"; import { file_saas_accounts_v1_common } from "./common_pb"; -import type { Installation } from "./installations_pb"; -import { file_saas_accounts_v1_installations } from "./installations_pb"; +import type { Installation } from "./installation_listing_pb"; +import { file_saas_accounts_v1_installation_listing } from "./installation_listing_pb"; import type { SolutionRegistration } from "./solution_registry_pb"; import { file_saas_accounts_v1_solution_registry } from "./solution_registry_pb"; import type { GetEventOperationsRequestSchema, GetEventOperationsResponseSchema, ListEventSubscriptionsRequestSchema, ListEventSubscriptionsResponseSchema } from "../../events/v1/operations_pb"; @@ -27,7 +27,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/platform_admin.proto. */ export const file_saas_accounts_v1_platform_admin: GenFile = /*@__PURE__*/ - fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEiVAoMQ2F0YWxvZ3VlR2FwEjQKBnJlYXNvbhgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwUmVhc29uEg4KBmRldGFpbBgCIAEoCSJEChBDYXRhbG9ndWVSZWxlYXNlEhEKCXB1Ymxpc2hlchgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkipQIKEENhdGFsb2d1ZURlc2lyZWQSGwoRZGVjbGFyZWRfcmV2aXNpb24YASABKANIABI/ChVkZWNsYXJlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEj4KEGRlY2xhcmVkX3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIARI+ChRkZWNsYXJlZF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGQoXZGVjbGFyZWRfcmV2aXNpb25fdmFsdWVCGAoWZGVjbGFyZWRfcmVsZWFzZV92YWx1ZSKCAQoWQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbhIbChNhdXRob3JpemVkX3JldmlzaW9uGAEgASgDEhgKEGludmVudG9yeV9kaWdlc3QYAiABKAkSFgoObWVtYmVyX2JpbmRpbmcYAyABKAkSGQoRYnVpbGRfaW5jYXJuYXRpb24YBCABKAQiKAoWQ2F0YWxvZ3VlTm90QXV0aG9yaXplZBIOCgZkZXRhaWwYASABKAki8AEKE0NhdGFsb2d1ZUF1dGhvcml6ZWQSQQoNYXV0aG9yaXphdGlvbhgBIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbkgAEkIKDm5vdF9hdXRob3JpemVkGAIgASgLMiguc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVOb3RBdXRob3JpemVkSAASOwoRYXV0aG9yaXphdGlvbl9nYXAYAyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAQhUKE2F1dGhvcml6YXRpb25fdmFsdWUiiAEKEENhdGFsb2d1ZUFwcGxpZWQSGgoQYXBwbGllZF9yZXZpc2lvbhgBIAEoA0gAEj4KFGFwcGxpZWRfcmV2aXNpb25fZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAEIYChZhcHBsaWVkX3JldmlzaW9uX3ZhbHVlIt8BChpDYXRhbG9ndWVPYnNlcnZlZEV4ZWN1dGlvbhJoChdjb250YWluZXJfaW1hZ2VfZGlnZXN0cxgBIAMoCzJHLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb24uQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSGQoRYnVpbGRfaW5jYXJuYXRpb24YAiABKAQaPAoaQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASLUBAoRQ2F0YWxvZ3VlT2JzZXJ2ZWQSGwoRb2JzZXJ2ZWRfcmV2aXNpb24YASABKANIABI/ChVvYnNlcnZlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEjEKC29ic2VydmVkX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgBEkMKGW9ic2VydmF0aW9uX2ZyZXNobmVzc19nYXAYBCABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkoKEm9ic2VydmVkX2V4ZWN1dGlvbhgFIAEoCzIsLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb25IAhJAChZvYnNlcnZlZF9leGVjdXRpb25fZ2FwGAYgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAhI9Cgd2ZXJkaWN0GAcgASgOMiouc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVPYnNlcnZlZFZlcmRpY3RIAxI1Cgt2ZXJkaWN0X2dhcBgIIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSANCGQoXb2JzZXJ2ZWRfcmV2aXNpb25fdmFsdWVCHQobb2JzZXJ2YXRpb25fZnJlc2huZXNzX3ZhbHVlQhoKGG9ic2VydmVkX2V4ZWN1dGlvbl92YWx1ZUIPCg12ZXJkaWN0X3ZhbHVlIuABChRDYXRhbG9ndWVXaXRoZHJhd2luZxI+ChR3aXRoZHJhd2FsX3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASSQofY3JlZGVudGlhbF9yZXZvY2F0aW9uX3N0YXRlX2dhcBgCIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGAoWd2l0aGRyYXdhbF9zdGF0ZV92YWx1ZUIjCiFjcmVkZW50aWFsX3Jldm9jYXRpb25fc3RhdGVfdmFsdWUibAoQQ2F0YWxvZ3VlUmV0aXJlZBI+ChRyZXRpcmVtZW50X3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSABCGAoWcmV0aXJlbWVudF9zdGF0ZV92YWx1ZSK4AwoVQ2F0YWxvZ3VlSW5zdGFsbGF0aW9uEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEhAKCG9yZ19uYW1lGAIgASgJEjsKDWFnZW50X3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIABI7ChFhZ2VudF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASNgoMcmV2aXNpb25fZ2FwGAUgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIARI8Cg1ncmFudGVkX3RlYW1zGAYgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50Ej4KD2luaGVyaXRlZF90ZWFtcxgHIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ29sbGVjdGlvblJlYWRHcmFudEIVChNhZ2VudF9yZWxlYXNlX3ZhbHVlQhAKDnJldmlzaW9uX3ZhbHVlIuMFCg5DYXRhbG9ndWVFbnRyeRIyCgRraW5kGAEgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVFbnRyeUtpbmQSDAoEbmFtZRgCIAEoCRITCglwdWJsaXNoZXIYAyABKAlIABI3Cg1wdWJsaXNoZXJfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABIzCgdkZXNpcmVkGAUgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVEZXNpcmVkEjkKCmF1dGhvcml6ZWQYBiABKAsyJS5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUF1dGhvcml6ZWQSMwoHYXBwbGllZBgHIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXBwbGllZBI1CghvYnNlcnZlZBgIIAEoCzIjLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWQSOwoLd2l0aGRyYXdpbmcYCSABKAsyJi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVdpdGhkcmF3aW5nEjMKB3JldGlyZWQYCiABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJldGlyZWQSOAoOYnVpbGRfc2l6ZV9nYXAYCyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkEKDHJlZ2lzdHJhdGlvbhgMIAEoCzImLnNhYXMuYWNjb3VudHMudjEuU29sdXRpb25SZWdpc3RyYXRpb25IAogBARI+Cg1pbnN0YWxsYXRpb25zGA0gAygLMicuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVJbnN0YWxsYXRpb25CEQoPcHVibGlzaGVyX3ZhbHVlQhIKEGJ1aWxkX3NpemVfdmFsdWVCDwoNX3JlZ2lzdHJhdGlvbiI6ChxMaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0EhoKEmluY2x1ZGVfdG9tYnN0b25lZBgBIAEoCCKRAgodTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVzcG9uc2USMQoHZW50cmllcxgBIAMoCzIgLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRW50cnkSGQoRcmVnaXN0cnlfcmV2aXNpb24YAiABKAMSZgoUYXBwcm92ZWRfaW52ZW50b3JpZXMYAyADKAsySC5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlLkFwcHJvdmVkSW52ZW50b3JpZXNFbnRyeRo6ChhBcHByb3ZlZEludmVudG9yaWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgMOgI4ASJ4Cg5Nb2R1bGVFbnZlbG9wZRIQCghyZXZpc2lvbhgBIAEoAxIQCghkb2N1bWVudBgCIAEoCRISCgp3cml0dGVuX2J5GAMgASgJEi4KCndyaXR0ZW5fYXQYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIhoKGEdldE1vZHVsZUVudmVsb3BlUmVxdWVzdCKeAQoZR2V0TW9kdWxlRW52ZWxvcGVSZXNwb25zZRI3CghlbnZlbG9wZRgBIAEoCzIgLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlRW52ZWxvcGVIAIgBARI7CglyZXF1ZXN0ZWQYAiADKAsyKC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUF1dGhvcml0eVJlcXVlc3RCCwoJX2VudmVsb3BlIoMBChZNb2R1bGVBdXRob3JpdHlSZXF1ZXN0EhEKCXByaW5jaXBhbBgBIAEoCRIPCgdiaW5kaW5nGAIgASgJEhIKCmdlbmVyYXRpb24YAyABKAMSDQoFYWxpYXMYBCABKAkSDwoHYWxsb3dlZBgFIAEoCBIRCglhdXRob3JpdHkYBiABKAkiXQoYUHV0TW9kdWxlRW52ZWxvcGVSZXF1ZXN0EiIKEWV4cGVjdGVkX3JldmlzaW9uGAEgASgDQge6SAQiAigAEh0KCGRvY3VtZW50GAIgASgJQgu6SAhyBhABKICAQCJPChlQdXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlEjIKCGVudmVsb3BlGAEgASgLMiAuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFbnZlbG9wZSp+ChJDYXRhbG9ndWVFbnRyeUtpbmQSJAogQ0FUQUxPR1VFX0VOVFJZX0tJTkRfVU5TUEVDSUZJRUQQABIfChtDQVRBTE9HVUVfRU5UUllfS0lORF9NT0RVTEUQARIhCh1DQVRBTE9HVUVfRU5UUllfS0lORF9TT0xVVElPThACKq8BChJDYXRhbG9ndWVHYXBSZWFzb24SJAogQ0FUQUxPR1VFX0dBUF9SRUFTT05fVU5TUEVDSUZJRUQQABIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVDT1JERUQQARIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVQT1JURUQQAhIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfT0JTRVJWRUQQAyreAQoYQ2F0YWxvZ3VlT2JzZXJ2ZWRWZXJkaWN0EioKJkNBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1VOU1BFQ0lGSUVEEAASMQotQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19BVVRIT1JJWkVEEAESLgoqQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19ESUZGRVJTEAISMwovQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19VTkFVVEhPUklaRUQQAzKiIwoUUGxhdGZvcm1BZG1pblNlcnZpY2USjgEKC1NlYXJjaFVzZXJzEiQuc2Fhcy5hY2NvdW50cy52MS5TZWFyY2hVc2Vyc1JlcXVlc3QaJS5zYWFzLmFjY291bnRzLnYxLlNlYXJjaFVzZXJzUmVzcG9uc2UiMsLzGBQIAhABMAE6AhABQAFIBVADWANgA4LT5JMCFBISL3YxL3BsYXRmb3JtL3VzZXJzEqsBCgtTdXNwZW5kVXNlchIkLnNhYXMuYWNjb3VudHMudjEuU3VzcGVuZFVzZXJSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5Il7C8xgrCAIQATABOhcKE3NhYXMudXNlci5zdXNwZW5kZWQQAkABSAVQA1gDYAV4AoLT5JMCKToBKiIkL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTpzdXNwZW5kErMBCg1VbnN1c3BlbmRVc2VyEiYuc2Fhcy5hY2NvdW50cy52MS5VbnN1c3BlbmRVc2VyUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJiwvMYLQgCEAEwAToZChVzYWFzLnVzZXIudW5zdXNwZW5kZWQQAkABSAVQA1gDYAV4AoLT5JMCKzoBKiImL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTp1bnN1c3BlbmQS1gEKD0ltcGVyc29uYXRlVXNlchIoLnNhYXMuYWNjb3VudHMudjEuSW1wZXJzb25hdGVVc2VyUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuSW1wZXJzb25hdGVVc2VyUmVzcG9uc2UibsLzGDcIAhABMAQ6Iwofc2Fhcy5wbGF0Zm9ybS51c2VyX2ltcGVyc29uYXRlZBACQAFIBVADWARgA3gCgtPkkwItOgEqIigvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OmltcGVyc29uYXRlEtgBChFTdG9wSW1wZXJzb25hdGlvbhIqLnNhYXMuYWNjb3VudHMudjEuU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5TdG9wSW1wZXJzb25hdGlvblJlc3BvbnNlImrC8xg8CAIQATABOioKJnNhYXMucGxhdGZvcm0udXNlcl9pbXBlcnNvbmF0aW9uX2VuZGVkEAJAAUgFUANYA2ABgtPkkwIkOgEqIh8vdjEvcGxhdGZvcm0vaW1wZXJzb25hdGlvbjpzdG9wEqYBChJMaXN0QWN0aXZlU2Vzc2lvbnMSKy5zYWFzLmFjY291bnRzLnYxLkxpc3RBY3RpdmVTZXNzaW9uc1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkxpc3RBY3RpdmVTZXNzaW9uc1Jlc3BvbnNlIjXC8xgUCAIQATABOgIQAUABSAVQA1gDYAOC0+STAhcSFS92MS9wbGF0Zm9ybS9zZXNzaW9ucxKpAQoNUmV2b2tlU2Vzc2lvbhImLnNhYXMuYWNjb3VudHMudjEuUmV2b2tlU2Vzc2lvblJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiWMLzGCoIAhABMAE6GAoUc2Fhcy5zZXNzaW9uLnJldm9rZWQQAkABSAVQA1gDYAOC0+STAiQqIi92MS9wbGF0Zm9ybS9zZXNzaW9ucy97c2Vzc2lvbl9pZH0SzwEKEkdldE9yZ0VudGl0bGVtZW50cxIrLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnRW50aXRsZW1lbnRzUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2UiXsLzGCIIAhADKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIyEjAvdjEvcGxhdGZvcm0vb3JnYW5pemF0aW9ucy97b3JnX2lkfS9lbnRpdGxlbWVudHMS5AEKE092ZXJyaWRlRW50aXRsZW1lbnQSLC5zYWFzLmFjY291bnRzLnYxLk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0Gi0uc2Fhcy5hY2NvdW50cy52MS5PdmVycmlkZUVudGl0bGVtZW50UmVzcG9uc2UicMLzGDEIAhABMAE6HQoZc2Fhcy5lbnRpdGxlbWVudC5vdmVycmlkZRACQAFIBVADWANgAngCgtPkkwI1OgEqIjAvdjEvcGxhdGZvcm0vb3JnYW5pemF0aW9ucy97b3JnX2lkfS9lbnRpdGxlbWVudHMSrQEKEUdyYW50UGxhdGZvcm1Sb2xlEiouc2Fhcy5hY2NvdW50cy52MS5HcmFudFBsYXRmb3JtUm9sZVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiVMLzGDIIAhABMAQ6Hgoac2Fhcy5wbGF0Zm9ybS5yb2xlX2dyYW50ZWQQAkABSAVQA1gDYAV4AoLT5JMCGDoBKiITL3YxL3BsYXRmb3JtL2FkbWlucxK2AQoSUmV2b2tlUGxhdGZvcm1Sb2xlEisuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VQbGF0Zm9ybVJvbGVSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IlvC8xgyCAIQATAEOh4KGnNhYXMucGxhdGZvcm0ucm9sZV9yZXZva2VkEAJAAUgFUANYA2AFeAKC0+STAh8qHS92MS9wbGF0Zm9ybS9hZG1pbnMve3VzZXJfaWR9EqQBChJMaXN0UGxhdGZvcm1BZG1pbnMSKy5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUFkbWluc1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUFkbWluc1Jlc3BvbnNlIjPC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhUSEy92MS9wbGF0Zm9ybS9hZG1pbnMSjwEKFExpc3RBbGxPcmdhbml6YXRpb25zEi0uc2Fhcy5hY2NvdW50cy52MS5MaXN0QWxsT3JnYW5pemF0aW9uc1JlcXVlc3QaLi5zYWFzLmFjY291bnRzLnYxLkxpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2UiGMLzGBQIAhABMAE6AhABQAFIBVADWANgAxKSAQoVR2V0T3JnYW5pemF0aW9uUm9zdGVyEi4uc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZSIYwvMYFAgCEAEwAToCEAFAAUgFUANYA2ADEqUBChBMaXN0RmVhdHVyZUZsYWdzEikuc2Fhcy5hY2NvdW50cy52MS5MaXN0RmVhdHVyZUZsYWdzUmVxdWVzdBoqLnNhYXMuYWNjb3VudHMudjEuTGlzdEZlYXR1cmVGbGFnc1Jlc3BvbnNlIjrC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhwSGi92MS9wbGF0Zm9ybS9mZWF0dXJlLWZsYWdzEtIBChFVcHNlcnRGZWF0dXJlRmxhZxIqLnNhYXMuYWNjb3VudHMudjEuVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5VcHNlcnRGZWF0dXJlRmxhZ1Jlc3BvbnNlImSIAgHC8xgxCAIQATABOh0KGXNhYXMuZmVhdHVyZV9mbGFnLnVwZGF0ZWQQAkABSAVQA1gDYAV4AoLT5JMCJjoBKhohL3YxL3BsYXRmb3JtL2ZlYXR1cmUtZmxhZ3Mve25hbWV9Ep8BChBHZXRKb2JPcGVyYXRpb25zEiUuc2Fhcy5qb2JzLnYxLkdldEpvYk9wZXJhdGlvbnNSZXF1ZXN0GiYuc2Fhcy5qb2JzLnYxLkdldEpvYk9wZXJhdGlvbnNSZXNwb25zZSI8wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIeEhwvdjEvcGxhdGZvcm0vam9icy9vcGVyYXRpb25zEnwKCExpc3RKb2JzEh0uc2Fhcy5qb2JzLnYxLkxpc3RKb2JzUmVxdWVzdBoeLnNhYXMuam9icy52MS5MaXN0Sm9ic1Jlc3BvbnNlIjHC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhMSES92MS9wbGF0Zm9ybS9qb2JzEn8KBkdldEpvYhIbLnNhYXMuam9icy52MS5HZXRKb2JSZXF1ZXN0Ghwuc2Fhcy5qb2JzLnYxLkdldEpvYlJlc3BvbnNlIjrC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhwSGi92MS9wbGF0Zm9ybS9qb2JzL3tqb2JfaWR9EqwBCglSZXBsYXlKb2ISHi5zYWFzLmpvYnMudjEuUmVwbGF5Sm9iUmVxdWVzdBofLnNhYXMuam9icy52MS5SZXBsYXlKb2JSZXNwb25zZSJewvMYJwgCEAEwAzoVChFzYWFzLmpvYi5yZXBsYXllZBACQANIBVADWANgBYLT5JMCLToBKiIoL3YxL3BsYXRmb3JtL2pvYnMve3NvdXJjZV9qb2JfaWR9OnJlcGxheRKrAQoSR2V0RXZlbnRPcGVyYXRpb25zEikuc2Fhcy5ldmVudHMudjEuR2V0RXZlbnRPcGVyYXRpb25zUmVxdWVzdBoqLnNhYXMuZXZlbnRzLnYxLkdldEV2ZW50T3BlcmF0aW9uc1Jlc3BvbnNlIj7C8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAiASHi92MS9wbGF0Zm9ybS9ldmVudHMvb3BlcmF0aW9ucxK6AQoWTGlzdEV2ZW50U3Vic2NyaXB0aW9ucxItLnNhYXMuZXZlbnRzLnYxLkxpc3RFdmVudFN1YnNjcmlwdGlvbnNSZXF1ZXN0Gi4uc2Fhcy5ldmVudHMudjEuTGlzdEV2ZW50U3Vic2NyaXB0aW9uc1Jlc3BvbnNlIkHC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAiMSIS92MS9wbGF0Zm9ybS9ldmVudHMvc3Vic2NyaXB0aW9ucxKwAQoVTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlEi4uc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXNwb25zZSI2wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIYEhYvdjEvcGxhdGZvcm0vY2F0YWxvZ3VlEsABChFHZXRNb2R1bGVFbnZlbG9wZRIqLnNhYXMuYWNjb3VudHMudjEuR2V0TW9kdWxlRW52ZWxvcGVSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5HZXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlIlLC8xgqCAIQASIUbW9kdWxlX2VudmVsb3BlOnJlYWQwAToCEAFAAUgFUANYA2AFgtPkkwIeEhwvdjEvcGxhdGZvcm0vbW9kdWxlLWVudmVsb3BlEu0BChFQdXRNb2R1bGVFbnZlbG9wZRIqLnNhYXMuYWNjb3VudHMudjEuUHV0TW9kdWxlRW52ZWxvcGVSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5QdXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlIn/C8xhUCAIQASIVbW9kdWxlX2VudmVsb3BlOndyaXRlMAM6KQolc2Fhcy5wbGF0Zm9ybS5tb2R1bGVfZW52ZWxvcGVfd3JpdHRlbhACQAFIBVADWANgBXgCgtPkkwIhOgEqGhwvdjEvcGxhdGZvcm0vbW9kdWxlLWVudmVsb3BlYgZwcm90bzM", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_accounts_v1_common, file_saas_accounts_v1_installations, file_saas_accounts_v1_solution_registry, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); + fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEiVAoMQ2F0YWxvZ3VlR2FwEjQKBnJlYXNvbhgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwUmVhc29uEg4KBmRldGFpbBgCIAEoCSJEChBDYXRhbG9ndWVSZWxlYXNlEhEKCXB1Ymxpc2hlchgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkipQIKEENhdGFsb2d1ZURlc2lyZWQSGwoRZGVjbGFyZWRfcmV2aXNpb24YASABKANIABI/ChVkZWNsYXJlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEj4KEGRlY2xhcmVkX3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIARI+ChRkZWNsYXJlZF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGQoXZGVjbGFyZWRfcmV2aXNpb25fdmFsdWVCGAoWZGVjbGFyZWRfcmVsZWFzZV92YWx1ZSKCAQoWQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbhIbChNhdXRob3JpemVkX3JldmlzaW9uGAEgASgDEhgKEGludmVudG9yeV9kaWdlc3QYAiABKAkSFgoObWVtYmVyX2JpbmRpbmcYAyABKAkSGQoRYnVpbGRfaW5jYXJuYXRpb24YBCABKAQiKAoWQ2F0YWxvZ3VlTm90QXV0aG9yaXplZBIOCgZkZXRhaWwYASABKAki8AEKE0NhdGFsb2d1ZUF1dGhvcml6ZWQSQQoNYXV0aG9yaXphdGlvbhgBIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbkgAEkIKDm5vdF9hdXRob3JpemVkGAIgASgLMiguc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVOb3RBdXRob3JpemVkSAASOwoRYXV0aG9yaXphdGlvbl9nYXAYAyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAQhUKE2F1dGhvcml6YXRpb25fdmFsdWUiiAEKEENhdGFsb2d1ZUFwcGxpZWQSGgoQYXBwbGllZF9yZXZpc2lvbhgBIAEoA0gAEj4KFGFwcGxpZWRfcmV2aXNpb25fZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAEIYChZhcHBsaWVkX3JldmlzaW9uX3ZhbHVlIt8BChpDYXRhbG9ndWVPYnNlcnZlZEV4ZWN1dGlvbhJoChdjb250YWluZXJfaW1hZ2VfZGlnZXN0cxgBIAMoCzJHLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb24uQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSGQoRYnVpbGRfaW5jYXJuYXRpb24YAiABKAQaPAoaQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASLUBAoRQ2F0YWxvZ3VlT2JzZXJ2ZWQSGwoRb2JzZXJ2ZWRfcmV2aXNpb24YASABKANIABI/ChVvYnNlcnZlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEjEKC29ic2VydmVkX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgBEkMKGW9ic2VydmF0aW9uX2ZyZXNobmVzc19nYXAYBCABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkoKEm9ic2VydmVkX2V4ZWN1dGlvbhgFIAEoCzIsLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb25IAhJAChZvYnNlcnZlZF9leGVjdXRpb25fZ2FwGAYgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAhI9Cgd2ZXJkaWN0GAcgASgOMiouc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVPYnNlcnZlZFZlcmRpY3RIAxI1Cgt2ZXJkaWN0X2dhcBgIIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSANCGQoXb2JzZXJ2ZWRfcmV2aXNpb25fdmFsdWVCHQobb2JzZXJ2YXRpb25fZnJlc2huZXNzX3ZhbHVlQhoKGG9ic2VydmVkX2V4ZWN1dGlvbl92YWx1ZUIPCg12ZXJkaWN0X3ZhbHVlIuABChRDYXRhbG9ndWVXaXRoZHJhd2luZxI+ChR3aXRoZHJhd2FsX3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASSQofY3JlZGVudGlhbF9yZXZvY2F0aW9uX3N0YXRlX2dhcBgCIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGAoWd2l0aGRyYXdhbF9zdGF0ZV92YWx1ZUIjCiFjcmVkZW50aWFsX3Jldm9jYXRpb25fc3RhdGVfdmFsdWUibAoQQ2F0YWxvZ3VlUmV0aXJlZBI+ChRyZXRpcmVtZW50X3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSABCGAoWcmV0aXJlbWVudF9zdGF0ZV92YWx1ZSK4AwoVQ2F0YWxvZ3VlSW5zdGFsbGF0aW9uEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEhAKCG9yZ19uYW1lGAIgASgJEjsKDWFnZW50X3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIABI7ChFhZ2VudF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASNgoMcmV2aXNpb25fZ2FwGAUgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIARI8Cg1ncmFudGVkX3RlYW1zGAYgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50Ej4KD2luaGVyaXRlZF90ZWFtcxgHIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ29sbGVjdGlvblJlYWRHcmFudEIVChNhZ2VudF9yZWxlYXNlX3ZhbHVlQhAKDnJldmlzaW9uX3ZhbHVlIuMFCg5DYXRhbG9ndWVFbnRyeRIyCgRraW5kGAEgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVFbnRyeUtpbmQSDAoEbmFtZRgCIAEoCRITCglwdWJsaXNoZXIYAyABKAlIABI3Cg1wdWJsaXNoZXJfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABIzCgdkZXNpcmVkGAUgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVEZXNpcmVkEjkKCmF1dGhvcml6ZWQYBiABKAsyJS5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUF1dGhvcml6ZWQSMwoHYXBwbGllZBgHIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXBwbGllZBI1CghvYnNlcnZlZBgIIAEoCzIjLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWQSOwoLd2l0aGRyYXdpbmcYCSABKAsyJi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVdpdGhkcmF3aW5nEjMKB3JldGlyZWQYCiABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJldGlyZWQSOAoOYnVpbGRfc2l6ZV9nYXAYCyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkEKDHJlZ2lzdHJhdGlvbhgMIAEoCzImLnNhYXMuYWNjb3VudHMudjEuU29sdXRpb25SZWdpc3RyYXRpb25IAogBARI+Cg1pbnN0YWxsYXRpb25zGA0gAygLMicuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVJbnN0YWxsYXRpb25CEQoPcHVibGlzaGVyX3ZhbHVlQhIKEGJ1aWxkX3NpemVfdmFsdWVCDwoNX3JlZ2lzdHJhdGlvbiI6ChxMaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0EhoKEmluY2x1ZGVfdG9tYnN0b25lZBgBIAEoCCKRAgodTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVzcG9uc2USMQoHZW50cmllcxgBIAMoCzIgLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRW50cnkSGQoRcmVnaXN0cnlfcmV2aXNpb24YAiABKAMSZgoUYXBwcm92ZWRfaW52ZW50b3JpZXMYAyADKAsySC5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlLkFwcHJvdmVkSW52ZW50b3JpZXNFbnRyeRo6ChhBcHByb3ZlZEludmVudG9yaWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgMOgI4ASJ4Cg5Nb2R1bGVFbnZlbG9wZRIQCghyZXZpc2lvbhgBIAEoAxIQCghkb2N1bWVudBgCIAEoCRISCgp3cml0dGVuX2J5GAMgASgJEi4KCndyaXR0ZW5fYXQYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIhoKGEdldE1vZHVsZUVudmVsb3BlUmVxdWVzdCKeAQoZR2V0TW9kdWxlRW52ZWxvcGVSZXNwb25zZRI3CghlbnZlbG9wZRgBIAEoCzIgLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlRW52ZWxvcGVIAIgBARI7CglyZXF1ZXN0ZWQYAiADKAsyKC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUF1dGhvcml0eVJlcXVlc3RCCwoJX2VudmVsb3BlIoMBChZNb2R1bGVBdXRob3JpdHlSZXF1ZXN0EhEKCXByaW5jaXBhbBgBIAEoCRIPCgdiaW5kaW5nGAIgASgJEhIKCmdlbmVyYXRpb24YAyABKAMSDQoFYWxpYXMYBCABKAkSDwoHYWxsb3dlZBgFIAEoCBIRCglhdXRob3JpdHkYBiABKAkiXQoYUHV0TW9kdWxlRW52ZWxvcGVSZXF1ZXN0EiIKEWV4cGVjdGVkX3JldmlzaW9uGAEgASgDQge6SAQiAigAEh0KCGRvY3VtZW50GAIgASgJQgu6SAhyBhABKICAQCJPChlQdXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlEjIKCGVudmVsb3BlGAEgASgLMiAuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFbnZlbG9wZSp+ChJDYXRhbG9ndWVFbnRyeUtpbmQSJAogQ0FUQUxPR1VFX0VOVFJZX0tJTkRfVU5TUEVDSUZJRUQQABIfChtDQVRBTE9HVUVfRU5UUllfS0lORF9NT0RVTEUQARIhCh1DQVRBTE9HVUVfRU5UUllfS0lORF9TT0xVVElPThACKq8BChJDYXRhbG9ndWVHYXBSZWFzb24SJAogQ0FUQUxPR1VFX0dBUF9SRUFTT05fVU5TUEVDSUZJRUQQABIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVDT1JERUQQARIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVQT1JURUQQAhIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfT0JTRVJWRUQQAyreAQoYQ2F0YWxvZ3VlT2JzZXJ2ZWRWZXJkaWN0EioKJkNBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1VOU1BFQ0lGSUVEEAASMQotQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19BVVRIT1JJWkVEEAESLgoqQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19ESUZGRVJTEAISMwovQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19VTkFVVEhPUklaRUQQAzKiIwoUUGxhdGZvcm1BZG1pblNlcnZpY2USjgEKC1NlYXJjaFVzZXJzEiQuc2Fhcy5hY2NvdW50cy52MS5TZWFyY2hVc2Vyc1JlcXVlc3QaJS5zYWFzLmFjY291bnRzLnYxLlNlYXJjaFVzZXJzUmVzcG9uc2UiMsLzGBQIAhABMAE6AhABQAFIBVADWANgA4LT5JMCFBISL3YxL3BsYXRmb3JtL3VzZXJzEqsBCgtTdXNwZW5kVXNlchIkLnNhYXMuYWNjb3VudHMudjEuU3VzcGVuZFVzZXJSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5Il7C8xgrCAIQATABOhcKE3NhYXMudXNlci5zdXNwZW5kZWQQAkABSAVQA1gDYAV4AoLT5JMCKToBKiIkL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTpzdXNwZW5kErMBCg1VbnN1c3BlbmRVc2VyEiYuc2Fhcy5hY2NvdW50cy52MS5VbnN1c3BlbmRVc2VyUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJiwvMYLQgCEAEwAToZChVzYWFzLnVzZXIudW5zdXNwZW5kZWQQAkABSAVQA1gDYAV4AoLT5JMCKzoBKiImL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTp1bnN1c3BlbmQS1gEKD0ltcGVyc29uYXRlVXNlchIoLnNhYXMuYWNjb3VudHMudjEuSW1wZXJzb25hdGVVc2VyUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuSW1wZXJzb25hdGVVc2VyUmVzcG9uc2UibsLzGDcIAhABMAQ6Iwofc2Fhcy5wbGF0Zm9ybS51c2VyX2ltcGVyc29uYXRlZBACQAFIBVADWARgA3gCgtPkkwItOgEqIigvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OmltcGVyc29uYXRlEtgBChFTdG9wSW1wZXJzb25hdGlvbhIqLnNhYXMuYWNjb3VudHMudjEuU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5TdG9wSW1wZXJzb25hdGlvblJlc3BvbnNlImrC8xg8CAIQATABOioKJnNhYXMucGxhdGZvcm0udXNlcl9pbXBlcnNvbmF0aW9uX2VuZGVkEAJAAUgFUANYA2ABgtPkkwIkOgEqIh8vdjEvcGxhdGZvcm0vaW1wZXJzb25hdGlvbjpzdG9wEqYBChJMaXN0QWN0aXZlU2Vzc2lvbnMSKy5zYWFzLmFjY291bnRzLnYxLkxpc3RBY3RpdmVTZXNzaW9uc1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkxpc3RBY3RpdmVTZXNzaW9uc1Jlc3BvbnNlIjXC8xgUCAIQATABOgIQAUABSAVQA1gDYAOC0+STAhcSFS92MS9wbGF0Zm9ybS9zZXNzaW9ucxKpAQoNUmV2b2tlU2Vzc2lvbhImLnNhYXMuYWNjb3VudHMudjEuUmV2b2tlU2Vzc2lvblJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiWMLzGCoIAhABMAE6GAoUc2Fhcy5zZXNzaW9uLnJldm9rZWQQAkABSAVQA1gDYAOC0+STAiQqIi92MS9wbGF0Zm9ybS9zZXNzaW9ucy97c2Vzc2lvbl9pZH0SzwEKEkdldE9yZ0VudGl0bGVtZW50cxIrLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnRW50aXRsZW1lbnRzUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2UiXsLzGCIIAhADKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIyEjAvdjEvcGxhdGZvcm0vb3JnYW5pemF0aW9ucy97b3JnX2lkfS9lbnRpdGxlbWVudHMS5AEKE092ZXJyaWRlRW50aXRsZW1lbnQSLC5zYWFzLmFjY291bnRzLnYxLk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0Gi0uc2Fhcy5hY2NvdW50cy52MS5PdmVycmlkZUVudGl0bGVtZW50UmVzcG9uc2UicMLzGDEIAhABMAE6HQoZc2Fhcy5lbnRpdGxlbWVudC5vdmVycmlkZRACQAFIBVADWANgAngCgtPkkwI1OgEqIjAvdjEvcGxhdGZvcm0vb3JnYW5pemF0aW9ucy97b3JnX2lkfS9lbnRpdGxlbWVudHMSrQEKEUdyYW50UGxhdGZvcm1Sb2xlEiouc2Fhcy5hY2NvdW50cy52MS5HcmFudFBsYXRmb3JtUm9sZVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiVMLzGDIIAhABMAQ6Hgoac2Fhcy5wbGF0Zm9ybS5yb2xlX2dyYW50ZWQQAkABSAVQA1gDYAV4AoLT5JMCGDoBKiITL3YxL3BsYXRmb3JtL2FkbWlucxK2AQoSUmV2b2tlUGxhdGZvcm1Sb2xlEisuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VQbGF0Zm9ybVJvbGVSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IlvC8xgyCAIQATAEOh4KGnNhYXMucGxhdGZvcm0ucm9sZV9yZXZva2VkEAJAAUgFUANYA2AFeAKC0+STAh8qHS92MS9wbGF0Zm9ybS9hZG1pbnMve3VzZXJfaWR9EqQBChJMaXN0UGxhdGZvcm1BZG1pbnMSKy5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUFkbWluc1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUFkbWluc1Jlc3BvbnNlIjPC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhUSEy92MS9wbGF0Zm9ybS9hZG1pbnMSjwEKFExpc3RBbGxPcmdhbml6YXRpb25zEi0uc2Fhcy5hY2NvdW50cy52MS5MaXN0QWxsT3JnYW5pemF0aW9uc1JlcXVlc3QaLi5zYWFzLmFjY291bnRzLnYxLkxpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2UiGMLzGBQIAhABMAE6AhABQAFIBVADWANgAxKSAQoVR2V0T3JnYW5pemF0aW9uUm9zdGVyEi4uc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZSIYwvMYFAgCEAEwAToCEAFAAUgFUANYA2ADEqUBChBMaXN0RmVhdHVyZUZsYWdzEikuc2Fhcy5hY2NvdW50cy52MS5MaXN0RmVhdHVyZUZsYWdzUmVxdWVzdBoqLnNhYXMuYWNjb3VudHMudjEuTGlzdEZlYXR1cmVGbGFnc1Jlc3BvbnNlIjrC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhwSGi92MS9wbGF0Zm9ybS9mZWF0dXJlLWZsYWdzEtIBChFVcHNlcnRGZWF0dXJlRmxhZxIqLnNhYXMuYWNjb3VudHMudjEuVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5VcHNlcnRGZWF0dXJlRmxhZ1Jlc3BvbnNlImSIAgHC8xgxCAIQATABOh0KGXNhYXMuZmVhdHVyZV9mbGFnLnVwZGF0ZWQQAkABSAVQA1gDYAV4AoLT5JMCJjoBKhohL3YxL3BsYXRmb3JtL2ZlYXR1cmUtZmxhZ3Mve25hbWV9Ep8BChBHZXRKb2JPcGVyYXRpb25zEiUuc2Fhcy5qb2JzLnYxLkdldEpvYk9wZXJhdGlvbnNSZXF1ZXN0GiYuc2Fhcy5qb2JzLnYxLkdldEpvYk9wZXJhdGlvbnNSZXNwb25zZSI8wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIeEhwvdjEvcGxhdGZvcm0vam9icy9vcGVyYXRpb25zEnwKCExpc3RKb2JzEh0uc2Fhcy5qb2JzLnYxLkxpc3RKb2JzUmVxdWVzdBoeLnNhYXMuam9icy52MS5MaXN0Sm9ic1Jlc3BvbnNlIjHC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhMSES92MS9wbGF0Zm9ybS9qb2JzEn8KBkdldEpvYhIbLnNhYXMuam9icy52MS5HZXRKb2JSZXF1ZXN0Ghwuc2Fhcy5qb2JzLnYxLkdldEpvYlJlc3BvbnNlIjrC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhwSGi92MS9wbGF0Zm9ybS9qb2JzL3tqb2JfaWR9EqwBCglSZXBsYXlKb2ISHi5zYWFzLmpvYnMudjEuUmVwbGF5Sm9iUmVxdWVzdBofLnNhYXMuam9icy52MS5SZXBsYXlKb2JSZXNwb25zZSJewvMYJwgCEAEwAzoVChFzYWFzLmpvYi5yZXBsYXllZBACQANIBVADWANgBYLT5JMCLToBKiIoL3YxL3BsYXRmb3JtL2pvYnMve3NvdXJjZV9qb2JfaWR9OnJlcGxheRKrAQoSR2V0RXZlbnRPcGVyYXRpb25zEikuc2Fhcy5ldmVudHMudjEuR2V0RXZlbnRPcGVyYXRpb25zUmVxdWVzdBoqLnNhYXMuZXZlbnRzLnYxLkdldEV2ZW50T3BlcmF0aW9uc1Jlc3BvbnNlIj7C8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAiASHi92MS9wbGF0Zm9ybS9ldmVudHMvb3BlcmF0aW9ucxK6AQoWTGlzdEV2ZW50U3Vic2NyaXB0aW9ucxItLnNhYXMuZXZlbnRzLnYxLkxpc3RFdmVudFN1YnNjcmlwdGlvbnNSZXF1ZXN0Gi4uc2Fhcy5ldmVudHMudjEuTGlzdEV2ZW50U3Vic2NyaXB0aW9uc1Jlc3BvbnNlIkHC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAiMSIS92MS9wbGF0Zm9ybS9ldmVudHMvc3Vic2NyaXB0aW9ucxKwAQoVTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlEi4uc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXNwb25zZSI2wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIYEhYvdjEvcGxhdGZvcm0vY2F0YWxvZ3VlEsABChFHZXRNb2R1bGVFbnZlbG9wZRIqLnNhYXMuYWNjb3VudHMudjEuR2V0TW9kdWxlRW52ZWxvcGVSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5HZXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlIlLC8xgqCAIQASIUbW9kdWxlX2VudmVsb3BlOnJlYWQwAToCEAFAAUgFUANYA2AFgtPkkwIeEhwvdjEvcGxhdGZvcm0vbW9kdWxlLWVudmVsb3BlEu0BChFQdXRNb2R1bGVFbnZlbG9wZRIqLnNhYXMuYWNjb3VudHMudjEuUHV0TW9kdWxlRW52ZWxvcGVSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5QdXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlIn/C8xhUCAIQASIVbW9kdWxlX2VudmVsb3BlOndyaXRlMAM6KQolc2Fhcy5wbGF0Zm9ybS5tb2R1bGVfZW52ZWxvcGVfd3JpdHRlbhACQAFIBVADWANgBXgCgtPkkwIhOgEqGhwvdjEvcGxhdGZvcm0vbW9kdWxlLWVudmVsb3BlYgZwcm90bzM", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_accounts_v1_common, file_saas_accounts_v1_installation_listing, file_saas_accounts_v1_solution_registry, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); /** * @generated from message saas.accounts.v1.SearchUsersRequest diff --git a/module/services/frontend/code/src/features/solution-installations/model/installations.test.ts b/module/services/frontend/code/src/features/solution-installations/model/installations.test.ts index e1faa77ae..fa4bf76b8 100644 --- a/module/services/frontend/code/src/features/solution-installations/model/installations.test.ts +++ b/module/services/frontend/code/src/features/solution-installations/model/installations.test.ts @@ -3,9 +3,11 @@ import { Code, ConnectError } from "@connectrpc/connect"; import { describe, expect, it } from "vitest"; import { RoleSchema } from "@/gen/saas/accounts/v1/common_pb"; import { - AvailableSolutionSchema, InstallationHealth, InstallationSchema, +} from "@/gen/saas/accounts/v1/installation_listing_pb"; +import { + AvailableSolutionSchema, OrganizationInstallationSchema, } from "@/gen/saas/accounts/v1/installations_pb"; import { diff --git a/module/services/frontend/code/src/features/solution-installations/model/installations.ts b/module/services/frontend/code/src/features/solution-installations/model/installations.ts index 520f67daa..184c8fa90 100644 --- a/module/services/frontend/code/src/features/solution-installations/model/installations.ts +++ b/module/services/frontend/code/src/features/solution-installations/model/installations.ts @@ -4,11 +4,13 @@ import type { ComponentProps } from "react"; import type { CollectionReadGrant } from "@/gen/saas/accounts/v1/authorization_pb"; import type { Role } from "@/gen/saas/accounts/v1/common_pb"; import { - type AvailableSolution, InstallationHealth, - type InstallationReadiness, InstallationStatus, - type OrganizationInstallation, +} from "@/gen/saas/accounts/v1/installation_listing_pb"; +import type { + AvailableSolution, + InstallationReadiness, + OrganizationInstallation, } from "@/gen/saas/accounts/v1/installations_pb"; import type { Badge } from "@/shared/ui"; diff --git a/module/services/frontend/code/src/features/solution-installations/service/queries.ts b/module/services/frontend/code/src/features/solution-installations/service/queries.ts index 60accf833..b0af97cda 100644 --- a/module/services/frontend/code/src/features/solution-installations/service/queries.ts +++ b/module/services/frontend/code/src/features/solution-installations/service/queries.ts @@ -1,8 +1,8 @@ import { useQuery } from "@tanstack/react-query"; -import { - type AvailableSolution, - InstallationStatus, - type OrganizationInstallation, +import { InstallationStatus } from "@/gen/saas/accounts/v1/installation_listing_pb"; +import type { + AvailableSolution, + OrganizationInstallation, } from "@/gen/saas/accounts/v1/installations_pb"; import { useInstallationService } from "@/lib/hooks/use-api-client"; diff --git a/module/services/frontend/code/src/features/solution-installations/ui/installation-setup-page.tsx b/module/services/frontend/code/src/features/solution-installations/ui/installation-setup-page.tsx index e14b2fd17..c7612503b 100644 --- a/module/services/frontend/code/src/features/solution-installations/ui/installation-setup-page.tsx +++ b/module/services/frontend/code/src/features/solution-installations/ui/installation-setup-page.tsx @@ -5,11 +5,11 @@ import Link from "next/link"; import { useState } from "react"; import { toast } from "sonner"; import { EmptyState } from "@/components/empty-state"; +import { InstallationStatus } from "@/gen/saas/accounts/v1/installation_listing_pb"; import type { GrantInstallationToTeamResponse, OrganizationInstallation, } from "@/gen/saas/accounts/v1/installations_pb"; -import { InstallationStatus } from "@/gen/saas/accounts/v1/installations_pb"; import { useAuth } from "@/lib/auth"; import { Badge, diff --git a/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.tsx b/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.tsx index 62141032f..9f435c99a 100644 --- a/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.tsx +++ b/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.tsx @@ -6,10 +6,8 @@ import { useState } from "react"; import { toast } from "sonner"; import { EmptyState } from "@/components/empty-state"; import { OrgSelector } from "@/components/org-selector"; -import { - type AvailableSolution, - InstallationStatus, -} from "@/gen/saas/accounts/v1/installations_pb"; +import { InstallationStatus } from "@/gen/saas/accounts/v1/installation_listing_pb"; +import type { AvailableSolution } from "@/gen/saas/accounts/v1/installations_pb"; import { useAuth } from "@/lib/auth"; import { mayKeepRetainedRows, diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/installation_listing_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/installation_listing_pb.ts new file mode 100644 index 000000000..c425a987b --- /dev/null +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/installation_listing_pb.ts @@ -0,0 +1,307 @@ +// @generated by protoc-gen-es v2.11.0 with parameter "target=ts,import_extension=none" +// @generated from file saas/accounts/v1/installation_listing.proto (package saas.accounts.v1, syntax proto3) +/* eslint-disable */ + +import type { GenEnum, GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import { enumDesc, fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; +import { file_buf_validate_validate } from "../../../buf/validate/validate_pb"; +import type { Timestamp } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file saas/accounts/v1/installation_listing.proto. + */ +export const file_saas_accounts_v1_installation_listing: GenFile = /*@__PURE__*/ + fileDesc("CitzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbl9saXN0aW5nLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIvkDCgxJbnN0YWxsYXRpb24SFAoCaWQYASABKAlCCLpIBXIDsAEBEhgKBm9yZ19pZBgCIAEoCUIIukgFcgOwAQESJAoSYWdlbnRfcHJpbmNpcGFsX2lkGAMgASgJQgi6SAVyA7ABARIkChJvd25lcl9wcmluY2lwYWxfaWQYBSABKAlCCLpIBXIDsAEBEh4KFmNvX293bmVyX3ByaW5jaXBhbF9pZHMYBiADKAkSJAoScm9vdF9zY29wZV9ub2RlX2lkGAcgASgJQgi6SAVyA7ABARI0CgZzdGF0dXMYCCABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN0YXR1cxIuCgpjcmVhdGVkX2F0GAkgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIzCgpyZXZva2VkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgAiAEBEh4KCXRhcmdldF9pZBgLIAEoCUILukgI2AEBcgOwAQESIAoOcmV2b2tlZF9yZWFzb24YDCABKAlCCLpIBXIDGIAEEiAKDmZhaWx1cmVfcmVhc29uGA0gASgJQgi6SAVyAxiABEINCgtfcmV2b2tlZF9hdEoECAQQBVITc29sdXRpb25faWRlbnRpZmllciKdAQoYTGlzdEluc3RhbGxhdGlvbnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoGc3RhdHVzGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdGF0dXMSHQoJcGFnZV9zaXplGAMgASgFQgq6SAcaBRj0AygAEhIKCnBhZ2VfdG9rZW4YBCABKAkigQEKE0luc3RhbGxhdGlvblN1bW1hcnkSNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SNAoGaGVhbHRoGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25IZWFsdGgicgoZTGlzdEluc3RhbGxhdGlvbnNSZXNwb25zZRI8Cg1pbnN0YWxsYXRpb25zGAEgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdW1tYXJ5EhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSq7AQoSSW5zdGFsbGF0aW9uU3RhdHVzEiMKH0lOU1RBTExBVElPTl9TVEFUVVNfVU5TUEVDSUZJRUQQABIeChpJTlNUQUxMQVRJT05fU1RBVFVTX0FDVElWRRABEh8KG0lOU1RBTExBVElPTl9TVEFUVVNfUkVWT0tFRBACEh8KG0lOU1RBTExBVElPTl9TVEFUVVNfUEVORElORxADEh4KGklOU1RBTExBVElPTl9TVEFUVVNfRkFJTEVEEAQqhAIKEkluc3RhbGxhdGlvbkhlYWx0aBIjCh9JTlNUQUxMQVRJT05fSEVBTFRIX1VOU1BFQ0lGSUVEEAASHwobSU5TVEFMTEFUSU9OX0hFQUxUSF9IRUFMVEhZEAESKQolSU5TVEFMTEFUSU9OX0hFQUxUSF9OT19FTElHSUJMRV9PV05FUhACEiUKIUlOU1RBTExBVElPTl9IRUFMVEhfQUdFTlRfUkVWT0tFRBADEiYKIklOU1RBTExBVElPTl9IRUFMVEhfQUdFTlRfRElTQUJMRUQQBBIuCipJTlNUQUxMQVRJT05fSEVBTFRIX1NUQU5ESU5HX0dSQU5UX01JU1NJTkcQBULAAQoUY29tLnNhYXMuYWNjb3VudHMudjFCGEluc3RhbGxhdGlvbkxpc3RpbmdQcm90b1ABWixhY2NvdW50cy9wa2cvZ2VuL3NhYXMvYWNjb3VudHMvdjE7YWNjb3VudHN2MaICA1NBWKoCEFNhYXMuQWNjb3VudHMuVjHKAhBTYWFzXEFjY291bnRzXFYx4gIcU2Fhc1xBY2NvdW50c1xWMVxHUEJNZXRhZGF0YeoCElNhYXM6OkFjY291bnRzOjpWMWIGcHJvdG8z", [file_buf_validate_validate, file_google_protobuf_timestamp]); + +/** + * Installation is one solution's bound identity in an org. Server-assigned ids; + * the browser cannot attest to tenancy, the agent principal, or the scope node. + * + * @generated from message saas.accounts.v1.Installation + */ +export type Installation = Message<"saas.accounts.v1.Installation"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: string org_id = 2; + */ + orgId: string; + + /** + * @generated from field: string agent_principal_id = 3; + */ + agentPrincipalId: string; + + /** + * @generated from field: string owner_principal_id = 5; + */ + ownerPrincipalId: string; + + /** + * @generated from field: repeated string co_owner_principal_ids = 6; + */ + coOwnerPrincipalIds: string[]; + + /** + * @generated from field: string root_scope_node_id = 7; + */ + rootScopeNodeId: string; + + /** + * @generated from field: saas.accounts.v1.InstallationStatus status = 8; + */ + status: InstallationStatus; + + /** + * @generated from field: google.protobuf.Timestamp created_at = 9; + */ + createdAt?: Timestamp; + + /** + * @generated from field: optional google.protobuf.Timestamp revoked_at = 10; + */ + revokedAt?: Timestamp; + + /** + * The immutable solution target this installation was consented to: one + * continuous period of one binding's presence on this host. It is never + * reused, so a replacement binding — even one claiming the same route alias — + * is a different target and inherits nothing. + * + * Deliberately NOT accompanied by the route alias. The alias is a property of + * the target that a later generation may move, and resolving it here would + * mean joining the host's presence state into a request-path listing — which + * would require granting request traffic read over every organisation's + * presence to answer about one. A consumer that needs the alias asks + * ListAvailableSolutions, which serves accepted applied state, and joins on + * this id. + * + * Empty only on a revoked historical row whose target could not be + * established at the cutover; an active installation always names one. + * + * @generated from field: string target_id = 11; + */ + targetId: string; + + /** + * Why a revoked installation was revoked, when it was not an administrator + * uninstalling: the installation-identity cutover, or a tenancy narrowing that + * withdrew this organisation. Empty on an active row. + * + * @generated from field: string revoked_reason = 12; + */ + revokedReason: string; + + /** + * Why the install step failed: the solution's bounded refusal reason, or a + * fixed reason when it never answered. Empty unless status is FAILED. + * + * @generated from field: string failure_reason = 13; + */ + failureReason: string; +}; + +/** + * Describes the message saas.accounts.v1.Installation. + * Use `create(InstallationSchema)` to create a new message. + */ +export const InstallationSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_installation_listing, 0); + +/** + * ListInstallationsRequest asks what one organization has installed. Every other + * RPC on this service names a single installation, so until this existed nothing + * could answer "what has this org installed?" — which is the authority read a + * per-organization solution projection is built on (issue #949). + * + * @generated from message saas.accounts.v1.ListInstallationsRequest + */ +export type ListInstallationsRequest = Message<"saas.accounts.v1.ListInstallationsRequest"> & { + /** + * @generated from field: string org_id = 1; + */ + orgId: string; + + /** + * Restrict to one lifecycle status. UNSPECIFIED returns every status, so a + * caller that must see only live installs asks for ACTIVE explicitly rather + * than leaning on a default that would silently widen if the enum grew. + * + * @generated from field: saas.accounts.v1.InstallationStatus status = 2; + */ + status: InstallationStatus; + + /** + * Page bound. 0 -> server default; capped so an organization with many + * installations can never return an unbounded result in one call. + * + * @generated from field: int32 page_size = 3; + */ + pageSize: number; + + /** + * Opaque cursor from a previous response's next_page_token; empty for page one. + * + * @generated from field: string page_token = 4; + */ + pageToken: string; +}; + +/** + * Describes the message saas.accounts.v1.ListInstallationsRequest. + * Use `create(ListInstallationsRequestSchema)` to create a new message. + */ +export const ListInstallationsRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_installation_listing, 1); + +/** + * InstallationSummary is one row of the listing: the installation, plus the health + * resolved live beside it exactly as GetInstallation resolves it for one. Health + * travels with the row because a consumer that renders an installed solution as + * unavailable would otherwise need one GetInstallation per installation. + * + * @generated from message saas.accounts.v1.InstallationSummary + */ +export type InstallationSummary = Message<"saas.accounts.v1.InstallationSummary"> & { + /** + * @generated from field: saas.accounts.v1.Installation installation = 1; + */ + installation?: Installation; + + /** + * @generated from field: saas.accounts.v1.InstallationHealth health = 2; + */ + health: InstallationHealth; +}; + +/** + * Describes the message saas.accounts.v1.InstallationSummary. + * Use `create(InstallationSummarySchema)` to create a new message. + */ +export const InstallationSummarySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_installation_listing, 2); + +/** + * @generated from message saas.accounts.v1.ListInstallationsResponse + */ +export type ListInstallationsResponse = Message<"saas.accounts.v1.ListInstallationsResponse"> & { + /** + * @generated from field: repeated saas.accounts.v1.InstallationSummary installations = 1; + */ + installations: InstallationSummary[]; + + /** + * Set when more installations remain; pass it back as page_token. Empty on the + * last page. + * + * @generated from field: string next_page_token = 2; + */ + nextPageToken: string; +}; + +/** + * Describes the message saas.accounts.v1.ListInstallationsResponse. + * Use `create(ListInstallationsResponseSchema)` to create a new message. + */ +export const ListInstallationsResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_installation_listing, 3); + +/** + * InstallationStatus is the lifecycle of an installation row. An uninstall is a + * soft delete (status=revoked, revoked_at set) so the agent, grants, and audit + * trail remain resolvable. + * + * @generated from enum saas.accounts.v1.InstallationStatus + */ +export enum InstallationStatus { + /** + * @generated from enum value: INSTALLATION_STATUS_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: INSTALLATION_STATUS_ACTIVE = 1; + */ + ACTIVE = 1, + + /** + * @generated from enum value: INSTALLATION_STATUS_REVOKED = 2; + */ + REVOKED = 2, + + /** + * Installed, and waiting for the solution to acknowledge its standard install + * step. Nothing can use a pending installation. + * + * @generated from enum value: INSTALLATION_STATUS_PENDING = 3; + */ + PENDING = 3, + + /** + * The solution refused its install step, or never answered within the + * attempt budget. Nothing can use it; an administrator may retry it. + * + * @generated from enum value: INSTALLATION_STATUS_FAILED = 4; + */ + FAILED = 4, +} + +/** + * Describes the enum saas.accounts.v1.InstallationStatus. + */ +export const InstallationStatusSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_saas_accounts_v1_installation_listing, 0); + +/** + * InstallationHealth is resolved live from current authority facts, never + * stored. It is the observable form of the fail-closed mint rule: a + * StartInstallationTask fails exactly when health is not HEALTHY, and transfer + * or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record + * nor any co-owner is currently an org admin (the Zapier-class "connection owner + * left the org" failure), surfaced rather than silently reassigned. + * + * @generated from enum saas.accounts.v1.InstallationHealth + */ +export enum InstallationHealth { + /** + * @generated from enum value: INSTALLATION_HEALTH_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: INSTALLATION_HEALTH_HEALTHY = 1; + */ + HEALTHY = 1, + + /** + * @generated from enum value: INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER = 2; + */ + NO_ELIGIBLE_OWNER = 2, + + /** + * @generated from enum value: INSTALLATION_HEALTH_AGENT_REVOKED = 3; + */ + AGENT_REVOKED = 3, + + /** + * @generated from enum value: INSTALLATION_HEALTH_AGENT_DISABLED = 4; + */ + AGENT_DISABLED = 4, + + /** + * @generated from enum value: INSTALLATION_HEALTH_STANDING_GRANT_MISSING = 5; + */ + STANDING_GRANT_MISSING = 5, +} + +/** + * Describes the enum saas.accounts.v1.InstallationHealth. + */ +export const InstallationHealthSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_saas_accounts_v1_installation_listing, 1); + diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/installations_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/installations_pb.ts index f377c7fc7..beb3d2ec5 100644 --- a/module/services/frontend/code/src/gen/saas/accounts/v1/installations_pb.ts +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/installations_pb.ts @@ -6,10 +6,12 @@ import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobu import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; import { file_buf_validate_validate } from "../../../buf/validate/validate_pb"; import { file_google_api_annotations } from "../../../google/api/annotations_pb"; -import type { EmptySchema, Timestamp } from "@bufbuild/protobuf/wkt"; -import { file_google_protobuf_empty, file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; +import type { EmptySchema } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_empty } from "@bufbuild/protobuf/wkt"; import type { CollectionReadGrant, ScopeGrant } from "./authorization_pb"; import { file_saas_accounts_v1_authorization } from "./authorization_pb"; +import type { Installation, InstallationHealth, InstallationSchema, ListInstallationsRequestSchema, ListInstallationsResponseSchema } from "./installation_listing_pb"; +import { file_saas_accounts_v1_installation_listing } from "./installation_listing_pb"; import { file_saas_policy_v1_options } from "../../policy/v1/options_pb"; import type { Message } from "@bufbuild/protobuf"; @@ -17,105 +19,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/installations.proto. */ export const file_saas_accounts_v1_installations: GenFile = /*@__PURE__*/ - fileDesc("CiRzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbnMucHJvdG8SEHNhYXMuYWNjb3VudHMudjEi+QMKDEluc3RhbGxhdGlvbhIUCgJpZBgBIAEoCUIIukgFcgOwAQESGAoGb3JnX2lkGAIgASgJQgi6SAVyA7ABARIkChJhZ2VudF9wcmluY2lwYWxfaWQYAyABKAlCCLpIBXIDsAEBEiQKEm93bmVyX3ByaW5jaXBhbF9pZBgFIAEoCUIIukgFcgOwAQESHgoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgGIAMoCRIkChJyb290X3Njb3BlX25vZGVfaWQYByABKAlCCLpIBXIDsAEBEjQKBnN0YXR1cxgIIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uU3RhdHVzEi4KCmNyZWF0ZWRfYXQYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKCnJldm9rZWRfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSACIAQESHgoJdGFyZ2V0X2lkGAsgASgJQgu6SAjYAQFyA7ABARIgCg5yZXZva2VkX3JlYXNvbhgMIAEoCUIIukgFcgMYgAQSIAoOZmFpbHVyZV9yZWFzb24YDSABKAlCCLpIBXIDGIAEQg0KC19yZXZva2VkX2F0SgQIBBAFUhNzb2x1dGlvbl9pZGVudGlmaWVyIuADChZJbnN0YWxsU29sdXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESJAoQYWdlbnRfaWRlbnRpZmllchgCIAEoCUIKukgHcgUQARiABBIeCgxkaXNwbGF5X25hbWUYBCABKAlCCLpIBXIDGIAEEiIKEHJvb3Rfc2NvcGVfbGFiZWwYBiABKAlCCLpIBXIDGIAEEhkKB3JvbGVfaWQYByABKAlCCLpIBXIDsAEBEicKEm93bmVyX3ByaW5jaXBhbF9pZBgIIAEoCUILukgI2AEBcgOwAQESLwoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgJIAMoCUIPukgMkgEJECAiBXIDsAEBEioKEWFsbG93ZWRfYXVkaWVuY2VzGAogAygJQg+6SAySAQkQQCIFcgMYgAQSJwoOYWxsb3dlZF9zY29wZXMYCyADKAlCD7pIDJIBCRBAIgVyAxiAARIbCgl0YXJnZXRfaWQYDCABKAlCCLpIBXIDsAEBEh8KBXNjb3BlGA0gASgJQgu6SAhyBhACKICAQEgAiAEBQggKBl9zY29wZUoECAMQBEoECAUQBlITc29sdXRpb25faWRlbnRpZmllclIPcm9vdF9zY29wZV9wYXRoInQKGU5hcnJvd0luc3RhbGxhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhoKBXNjb3BlGAMgASgJQgu6SAhyBhACKICAQCJXChhSZXRyeUluc3RhbGxhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBIlcKGFVuaW5zdGFsbFNvbHV0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEivgEKJFRyYW5zZmVySW5zdGFsbGF0aW9uT3duZXJzaGlwUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESKAoWbmV3X293bmVyX3ByaW5jaXBhbF9pZBgDIAEoCUIIukgFcgOwAQESLwoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgEIAMoCUIPukgMkgEJECAiBXIDsAEBIlUKFkdldEluc3RhbGxhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBIoUBChdHZXRJbnN0YWxsYXRpb25SZXNwb25zZRI0CgxpbnN0YWxsYXRpb24YASABKAsyHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbhI0CgZoZWFsdGgYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbkhlYWx0aCKdAQoYTGlzdEluc3RhbGxhdGlvbnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoGc3RhdHVzGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdGF0dXMSHQoJcGFnZV9zaXplGAMgASgFQgq6SAcaBRj0AygAEhIKCnBhZ2VfdG9rZW4YBCABKAkigQEKE0luc3RhbGxhdGlvblN1bW1hcnkSNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SNAoGaGVhbHRoGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25IZWFsdGgicgoZTGlzdEluc3RhbGxhdGlvbnNSZXNwb25zZRI8Cg1pbnN0YWxsYXRpb25zGAEgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdW1tYXJ5EhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJzCiRMaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIdCglwYWdlX3NpemUYAiABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgDIAEoCSLIAwoYT3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEjQKBmhlYWx0aBgCIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uSGVhbHRoEjMKBnRhcmdldBgDIAEoCzIjLnNhYXMuYWNjb3VudHMudjEuQXZhaWxhYmxlU29sdXRpb24SFwoPcm9vdF9zY29wZV9wYXRoGAQgASgJEjwKDWdyYW50ZWRfdGVhbXMYBSADKAsyJS5zYWFzLmFjY291bnRzLnYxLkNvbGxlY3Rpb25SZWFkR3JhbnQSPgoPaW5oZXJpdGVkX3RlYW1zGAYgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50EjoKCXJlYWRpbmVzcxgHIAEoCzInLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uUmVhZGluZXNzEjgKD3JlYWNoZWRfbW9kdWxlcxgIIAMoCzIfLnNhYXMuYWNjb3VudHMudjEuUmVhY2hlZE1vZHVsZSLtAQoSUmVhZGluZXNzQ29uZGl0aW9uEjYKBGtpbmQYASABKA4yKC5zYWFzLmFjY291bnRzLnYxLlJlYWRpbmVzc0NvbmRpdGlvbktpbmQSDwoHc3ViamVjdBgCIAEoCRIOCgZkZXRhaWwYAyABKAkSOAoKY2xlYXJlZF9ieRgEIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQ2xlYXJlZEJ5EjEKBmFjdGlvbhgFIAEoDjIhLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQWN0aW9uEhEKCXNvdXJjZV9pZBgGIAEoCSJbChVJbnN0YWxsYXRpb25SZWFkaW5lc3MSDQoFcmVhZHkYASABKAgSMwoFdW5tZXQYAiADKAsyJC5zYWFzLmFjY291bnRzLnYxLlJlYWRpbmVzc0NvbmRpdGlvbiJPCg1SZWFjaGVkTW9kdWxlEhAKCGF1ZGllbmNlGAEgASgJEhcKD2luc3RhbGxhdGlvbl9pZBgCIAEoCRITCgtyb3V0ZV9hbGlhcxgDIAEoCSKDAQolTGlzdE9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbnNSZXNwb25zZRJBCg1pbnN0YWxsYXRpb25zGAEgAygLMiouc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb25JbnN0YWxsYXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIpMBCh5HcmFudEluc3RhbGxhdGlvblRvVGVhbVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhkKB3RlYW1faWQYAyABKAlCCLpIBXIDsAEBEhkKB3JvbGVfaWQYBCABKAlCCLpIBXIDsAEBInUKE0NhcnJpZWRNb2R1bGVBY2Nlc3MSEAoIYXVkaWVuY2UYASABKAkSFwoPaW5zdGFsbGF0aW9uX2lkGAIgASgJEhIKCnNjb3BlX3BhdGgYAyABKAkSDwoHcm9sZV9pZBgEIAEoCRIOCgZzY29wZXMYBSADKAkiuwEKH0dyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVzcG9uc2USKwoFZ3JhbnQYASABKAsyHC5zYWFzLmFjY291bnRzLnYxLlNjb3BlR3JhbnQSNgoHY2FycmllZBgCIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ2FycmllZE1vZHVsZUFjY2VzcxIzCgV1bm1ldBgDIAMoCzIkLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQ29uZGl0aW9uIpYBCiFSZXZva2VJbnN0YWxsYXRpb25Gcm9tVGVhbVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhkKB3RlYW1faWQYAyABKAlCCLpIBXIDsAEBEhkKB3JvbGVfaWQYBCABKAlCCLpIBXIDsAEBIoQCChFBdmFpbGFibGVTb2x1dGlvbhIbCgl0YXJnZXRfaWQYASABKAlCCLpIBXIDsAEBEhIKCmJpbmRpbmdfaWQYAiABKAkSEwoLcm91dGVfYWxpYXMYAyABKAkSGQoRcmVsZWFzZV9wdWJsaXNoZXIYBCABKAkSFAoMcmVsZWFzZV9uYW1lGAUgASgJEhcKD3JlbGVhc2VfdmVyc2lvbhgGIAEoCRIZChFvcGVuZWRfZ2VuZXJhdGlvbhgHIAEoBBIaChJhcHBsaWVkX2dlbmVyYXRpb24YCCABKAQSEQoJaW5zdGFsbGVkGAkgASgIEhUKBGtpbmQYCiABKAlCB7pIBHICGCAibAodTGlzdEF2YWlsYWJsZVNvbHV0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIdCglwYWdlX3NpemUYAiABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgDIAEoCSJxCh5MaXN0QXZhaWxhYmxlU29sdXRpb25zUmVzcG9uc2USNgoJc29sdXRpb25zGAEgAygLMiMuc2Fhcy5hY2NvdW50cy52MS5BdmFpbGFibGVTb2x1dGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkquwEKEkluc3RhbGxhdGlvblN0YXR1cxIjCh9JTlNUQUxMQVRJT05fU1RBVFVTX1VOU1BFQ0lGSUVEEAASHgoaSU5TVEFMTEFUSU9OX1NUQVRVU19BQ1RJVkUQARIfChtJTlNUQUxMQVRJT05fU1RBVFVTX1JFVk9LRUQQAhIfChtJTlNUQUxMQVRJT05fU1RBVFVTX1BFTkRJTkcQAxIeChpJTlNUQUxMQVRJT05fU1RBVFVTX0ZBSUxFRBAEKoQCChJJbnN0YWxsYXRpb25IZWFsdGgSIwofSU5TVEFMTEFUSU9OX0hFQUxUSF9VTlNQRUNJRklFRBAAEh8KG0lOU1RBTExBVElPTl9IRUFMVEhfSEVBTFRIWRABEikKJUlOU1RBTExBVElPTl9IRUFMVEhfTk9fRUxJR0lCTEVfT1dORVIQAhIlCiFJTlNUQUxMQVRJT05fSEVBTFRIX0FHRU5UX1JFVk9LRUQQAxImCiJJTlNUQUxMQVRJT05fSEVBTFRIX0FHRU5UX0RJU0FCTEVEEAQSLgoqSU5TVEFMTEFUSU9OX0hFQUxUSF9TVEFORElOR19HUkFOVF9NSVNTSU5HEAUq8wMKFlJlYWRpbmVzc0NvbmRpdGlvbktpbmQSKAokUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX1VOU1BFQ0lGSUVEEAASMQotUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX0lOU1RBTExfU1RFUF9QRU5ESU5HEAESMAosUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX0lOU1RBTExfU1RFUF9GQUlMRUQQAhItCilSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfUkVDT1JEX1VOSEVBTFRIWRADEjUKMVJFQURJTkVTU19DT05ESVRJT05fS0lORF9NT0RVTEVfUFJJTkNJUEFMX09NSVRURUQQBBI2CjJSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfT1BFUkFUSU9OX0JJTkRJTkdfT01JVFRFRBAFEjEKLVJFQURJTkVTU19DT05ESVRJT05fS0lORF9TT1VSQ0VfTk9UX0RFTEVHQVRFRBAGEjkKNVJFQURJTkVTU19DT05ESVRJT05fS0lORF9SRUFDSEVEX01PRFVMRV9OT1RfSU5TVEFMTEVEEAcSPgo6UkVBRElORVNTX0NPTkRJVElPTl9LSU5EX0NBUlJJRURfTU9EVUxFX0FDQ0VTU19OT1RfREVSSVZFRBAIKtUBChJSZWFkaW5lc3NDbGVhcmVkQnkSJAogUkVBRElORVNTX0NMRUFSRURfQllfVU5TUEVDSUZJRUQQABIfChtSRUFESU5FU1NfQ0xFQVJFRF9CWV9WSUVXRVIQARIrCidSRUFESU5FU1NfQ0xFQVJFRF9CWV9PUkdBTklaQVRJT05fQURNSU4QAhIqCiZSRUFESU5FU1NfQ0xFQVJFRF9CWV9QTEFURk9STV9PUEVSQVRPUhADEh8KG1JFQURJTkVTU19DTEVBUkVEX0JZX05PQk9EWRAEKp8CCg9SZWFkaW5lc3NBY3Rpb24SIAocUkVBRElORVNTX0FDVElPTl9VTlNQRUNJRklFRBAAEikKJVJFQURJTkVTU19BQ1RJT05fTk9ORV9BVF9USElTX1NVUkZBQ0UQARInCiNSRUFESU5FU1NfQUNUSU9OX1JFVFJZX0lOU1RBTExfU1RFUBACEiUKIVJFQURJTkVTU19BQ1RJT05fUkVDT05ORUNUX1NPVVJDRRADEicKI1JFQURJTkVTU19BQ1RJT05fVFJBTlNGRVJfT1dORVJTSElQEAQSKwonUkVBRElORVNTX0FDVElPTl9JTlNUQUxMX1JFQUNIRURfTU9EVUxFEAUSGQoVUkVBRElORVNTX0FDVElPTl9XQUlUEAYyqhIKE0luc3RhbGxhdGlvblNlcnZpY2USugEKD0luc3RhbGxTb2x1dGlvbhIoLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbFNvbHV0aW9uUmVxdWVzdBoeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uIl3C8xg9CAIQBCoMCgZvcmdfaWQQAhgBMAE6HQoZc2Fhcy5pbnN0YWxsYXRpb24uY3JlYXRlZBACQAFIBFADWANgAYLT5JMCFjoBKiIRL3YxL2luc3RhbGxhdGlvbnMS0gEKEVVuaW5zdGFsbFNvbHV0aW9uEiouc2Fhcy5hY2NvdW50cy52MS5Vbmluc3RhbGxTb2x1dGlvblJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiecLzGD0IAhAEKgwKBm9yZ19pZBACGAEwATodChlzYWFzLmluc3RhbGxhdGlvbi5yZXZva2VkEAJAAUgEUANYA2ABgtPkkwIyOgEqIi0vdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfTp1bmluc3RhbGwS0gEKEk5hcnJvd0luc3RhbGxhdGlvbhIrLnNhYXMuYWNjb3VudHMudjEuTmFycm93SW5zdGFsbGF0aW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJ3wvMYPggCEAQqDAoGb3JnX2lkEAIYATABOh4KGnNhYXMuaW5zdGFsbGF0aW9uLm5hcnJvd2VkEAJAAUgEUANYA2ABgtPkkwIvOgEqIiovdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfTpuYXJyb3cS1gEKEVJldHJ5SW5zdGFsbGF0aW9uEiouc2Fhcy5hY2NvdW50cy52MS5SZXRyeUluc3RhbGxhdGlvblJlcXVlc3QaHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbiJ1wvMYPQgCEAQqDAoGb3JnX2lkEAIYATABOh0KGXNhYXMuaW5zdGFsbGF0aW9uLnJldHJpZWQQAkABSARQA1gDYAGC0+STAi46ASoiKS92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9OnJldHJ5EokCCh1UcmFuc2Zlckluc3RhbGxhdGlvbk93bmVyc2hpcBI2LnNhYXMuYWNjb3VudHMudjEuVHJhbnNmZXJJbnN0YWxsYXRpb25Pd25lcnNoaXBSZXF1ZXN0Gh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24ijwHC8xhLCAIQBCoMCgZvcmdfaWQQAhgBMAE6Kwonc2Fhcy5pbnN0YWxsYXRpb24ub3duZXJzaGlwX3RyYW5zZmVycmVkEAJAAUgEUANYA2ABgtPkkwI6OgEqIjUvdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfTp0cmFuc2Zlck93bmVyc2hpcBK5AQoPR2V0SW5zdGFsbGF0aW9uEiguc2Fhcy5hY2NvdW50cy52MS5HZXRJbnN0YWxsYXRpb25SZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5HZXRJbnN0YWxsYXRpb25SZXNwb25zZSJRwvMYIggCEAMqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAiUSIy92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9EoYBChFMaXN0SW5zdGFsbGF0aW9ucxIqLnNhYXMuYWNjb3VudHMudjEuTGlzdEluc3RhbGxhdGlvbnNSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5MaXN0SW5zdGFsbGF0aW9uc1Jlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESygEKFkxpc3RBdmFpbGFibGVTb2x1dGlvbnMSLy5zYWFzLmFjY291bnRzLnYxLkxpc3RBdmFpbGFibGVTb2x1dGlvbnNSZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5MaXN0QXZhaWxhYmxlU29sdXRpb25zUmVzcG9uc2UiTcLzGCIIAhAEKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIhEh8vdjEvaW5zdGFsbGF0aW9uczpsaXN0QXZhaWxhYmxlEtEBCh1MaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9ucxI2LnNhYXMuYWNjb3VudHMudjEuTGlzdE9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbnNSZXF1ZXN0Gjcuc2Fhcy5hY2NvdW50cy52MS5MaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1Jlc3BvbnNlIj/C8xgiCAIQBCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCExIRL3YxL2luc3RhbGxhdGlvbnMS8AEKF0dyYW50SW5zdGFsbGF0aW9uVG9UZWFtEjAuc2Fhcy5hY2NvdW50cy52MS5HcmFudEluc3RhbGxhdGlvblRvVGVhbVJlcXVlc3QaMS5zYWFzLmFjY291bnRzLnYxLkdyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVzcG9uc2UicMLzGDgIAhAEKgwKBm9yZ19pZBACGAEwAToWChJzYWFzLnNjb3BlLmdyYW50ZWQQAkABSARQA1gDYAF4AoLT5JMCLjoBKiIpL3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH0vdGVhbXMS7QEKGlJldm9rZUluc3RhbGxhdGlvbkZyb21UZWFtEjMuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VJbnN0YWxsYXRpb25Gcm9tVGVhbVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkigQHC8xg4CAIQBCoMCgZvcmdfaWQQAhgBMAE6FgoSc2Fhcy5zY29wZS5yZXZva2VkEAJAAUgEUANYA2ABeAKC0+STAj86ASoiOi92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9L3RlYW1zL3t0ZWFtX2lkfTpyZXZva2VCugEKFGNvbS5zYWFzLmFjY291bnRzLnYxQhJJbnN0YWxsYXRpb25zUHJvdG9QAVosYWNjb3VudHMvcGtnL2dlbi9zYWFzL2FjY291bnRzL3YxO2FjY291bnRzdjGiAgNTQViqAhBTYWFzLkFjY291bnRzLlYxygIQU2Fhc1xBY2NvdW50c1xWMeICHFNhYXNcQWNjb3VudHNcVjFcR1BCTWV0YWRhdGHqAhJTYWFzOjpBY2NvdW50czo6VjFiBnByb3RvMw", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_policy_v1_options]); - -/** - * Installation is one solution's bound identity in an org. Server-assigned ids; - * the browser cannot attest to tenancy, the agent principal, or the scope node. - * - * @generated from message saas.accounts.v1.Installation - */ -export type Installation = Message<"saas.accounts.v1.Installation"> & { - /** - * @generated from field: string id = 1; - */ - id: string; - - /** - * @generated from field: string org_id = 2; - */ - orgId: string; - - /** - * @generated from field: string agent_principal_id = 3; - */ - agentPrincipalId: string; - - /** - * @generated from field: string owner_principal_id = 5; - */ - ownerPrincipalId: string; - - /** - * @generated from field: repeated string co_owner_principal_ids = 6; - */ - coOwnerPrincipalIds: string[]; - - /** - * @generated from field: string root_scope_node_id = 7; - */ - rootScopeNodeId: string; - - /** - * @generated from field: saas.accounts.v1.InstallationStatus status = 8; - */ - status: InstallationStatus; - - /** - * @generated from field: google.protobuf.Timestamp created_at = 9; - */ - createdAt?: Timestamp; - - /** - * @generated from field: optional google.protobuf.Timestamp revoked_at = 10; - */ - revokedAt?: Timestamp; - - /** - * The immutable solution target this installation was consented to: one - * continuous period of one binding's presence on this host. It is never - * reused, so a replacement binding — even one claiming the same route alias — - * is a different target and inherits nothing. - * - * Deliberately NOT accompanied by the route alias. The alias is a property of - * the target that a later generation may move, and resolving it here would - * mean joining the host's presence state into a request-path listing — which - * would require granting request traffic read over every organisation's - * presence to answer about one. A consumer that needs the alias asks - * ListAvailableSolutions, which serves accepted applied state, and joins on - * this id. - * - * Empty only on a revoked historical row whose target could not be - * established at the cutover; an active installation always names one. - * - * @generated from field: string target_id = 11; - */ - targetId: string; - - /** - * Why a revoked installation was revoked, when it was not an administrator - * uninstalling: the installation-identity cutover, or a tenancy narrowing that - * withdrew this organisation. Empty on an active row. - * - * @generated from field: string revoked_reason = 12; - */ - revokedReason: string; - - /** - * Why the install step failed: the solution's bounded refusal reason, or a - * fixed reason when it never answered. Empty unless status is FAILED. - * - * @generated from field: string failure_reason = 13; - */ - failureReason: string; -}; - -/** - * Describes the message saas.accounts.v1.Installation. - * Use `create(InstallationSchema)` to create a new message. - */ -export const InstallationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 0); + fileDesc("CiRzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbnMucHJvdG8SEHNhYXMuYWNjb3VudHMudjEi4AMKFkluc3RhbGxTb2x1dGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIkChBhZ2VudF9pZGVudGlmaWVyGAIgASgJQgq6SAdyBRABGIAEEh4KDGRpc3BsYXlfbmFtZRgEIAEoCUIIukgFcgMYgAQSIgoQcm9vdF9zY29wZV9sYWJlbBgGIAEoCUIIukgFcgMYgAQSGQoHcm9sZV9pZBgHIAEoCUIIukgFcgOwAQESJwoSb3duZXJfcHJpbmNpcGFsX2lkGAggASgJQgu6SAjYAQFyA7ABARIvChZjb19vd25lcl9wcmluY2lwYWxfaWRzGAkgAygJQg+6SAySAQkQICIFcgOwAQESKgoRYWxsb3dlZF9hdWRpZW5jZXMYCiADKAlCD7pIDJIBCRBAIgVyAxiABBInCg5hbGxvd2VkX3Njb3BlcxgLIAMoCUIPukgMkgEJEEAiBXIDGIABEhsKCXRhcmdldF9pZBgMIAEoCUIIukgFcgOwAQESHwoFc2NvcGUYDSABKAlCC7pICHIGEAIogIBASACIAQFCCAoGX3Njb3BlSgQIAxAESgQIBRAGUhNzb2x1dGlvbl9pZGVudGlmaWVyUg9yb290X3Njb3BlX3BhdGgidAoZTmFycm93SW5zdGFsbGF0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESGgoFc2NvcGUYAyABKAlCC7pICHIGEAIogIBAIlcKGFJldHJ5SW5zdGFsbGF0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEiVwoYVW5pbnN0YWxsU29sdXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABASK+AQokVHJhbnNmZXJJbnN0YWxsYXRpb25Pd25lcnNoaXBSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABARIoChZuZXdfb3duZXJfcHJpbmNpcGFsX2lkGAMgASgJQgi6SAVyA7ABARIvChZjb19vd25lcl9wcmluY2lwYWxfaWRzGAQgAygJQg+6SAySAQkQICIFcgOwAQEiVQoWR2V0SW5zdGFsbGF0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEihQEKF0dldEluc3RhbGxhdGlvblJlc3BvbnNlEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEjQKBmhlYWx0aBgCIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uSGVhbHRoInMKJExpc3RPcmdhbml6YXRpb25JbnN0YWxsYXRpb25zUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEh0KCXBhZ2Vfc2l6ZRgCIAEoBUIKukgHGgUY9AMoABISCgpwYWdlX3Rva2VuGAMgASgJIsgDChhPcmdhbml6YXRpb25JbnN0YWxsYXRpb24SNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SNAoGaGVhbHRoGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25IZWFsdGgSMwoGdGFyZ2V0GAMgASgLMiMuc2Fhcy5hY2NvdW50cy52MS5BdmFpbGFibGVTb2x1dGlvbhIXCg9yb290X3Njb3BlX3BhdGgYBCABKAkSPAoNZ3JhbnRlZF90ZWFtcxgFIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ29sbGVjdGlvblJlYWRHcmFudBI+Cg9pbmhlcml0ZWRfdGVhbXMYBiADKAsyJS5zYWFzLmFjY291bnRzLnYxLkNvbGxlY3Rpb25SZWFkR3JhbnQSOgoJcmVhZGluZXNzGAcgASgLMicuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25SZWFkaW5lc3MSOAoPcmVhY2hlZF9tb2R1bGVzGAggAygLMh8uc2Fhcy5hY2NvdW50cy52MS5SZWFjaGVkTW9kdWxlIu0BChJSZWFkaW5lc3NDb25kaXRpb24SNgoEa2luZBgBIAEoDjIoLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQ29uZGl0aW9uS2luZBIPCgdzdWJqZWN0GAIgASgJEg4KBmRldGFpbBgDIAEoCRI4CgpjbGVhcmVkX2J5GAQgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5SZWFkaW5lc3NDbGVhcmVkQnkSMQoGYWN0aW9uGAUgASgOMiEuc2Fhcy5hY2NvdW50cy52MS5SZWFkaW5lc3NBY3Rpb24SEQoJc291cmNlX2lkGAYgASgJIlsKFUluc3RhbGxhdGlvblJlYWRpbmVzcxINCgVyZWFkeRgBIAEoCBIzCgV1bm1ldBgCIAMoCzIkLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQ29uZGl0aW9uIk8KDVJlYWNoZWRNb2R1bGUSEAoIYXVkaWVuY2UYASABKAkSFwoPaW5zdGFsbGF0aW9uX2lkGAIgASgJEhMKC3JvdXRlX2FsaWFzGAMgASgJIoMBCiVMaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1Jlc3BvbnNlEkEKDWluc3RhbGxhdGlvbnMYASADKAsyKi5zYWFzLmFjY291bnRzLnYxLk9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkikwEKHkdyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESGQoHdGVhbV9pZBgDIAEoCUIIukgFcgOwAQESGQoHcm9sZV9pZBgEIAEoCUIIukgFcgOwAQEidQoTQ2FycmllZE1vZHVsZUFjY2VzcxIQCghhdWRpZW5jZRgBIAEoCRIXCg9pbnN0YWxsYXRpb25faWQYAiABKAkSEgoKc2NvcGVfcGF0aBgDIAEoCRIPCgdyb2xlX2lkGAQgASgJEg4KBnNjb3BlcxgFIAMoCSK7AQofR3JhbnRJbnN0YWxsYXRpb25Ub1RlYW1SZXNwb25zZRIrCgVncmFudBgBIAEoCzIcLnNhYXMuYWNjb3VudHMudjEuU2NvcGVHcmFudBI2CgdjYXJyaWVkGAIgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5DYXJyaWVkTW9kdWxlQWNjZXNzEjMKBXVubWV0GAMgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5SZWFkaW5lc3NDb25kaXRpb24ilgEKIVJldm9rZUluc3RhbGxhdGlvbkZyb21UZWFtUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESGQoHdGVhbV9pZBgDIAEoCUIIukgFcgOwAQESGQoHcm9sZV9pZBgEIAEoCUIIukgFcgOwAQEihAIKEUF2YWlsYWJsZVNvbHV0aW9uEhsKCXRhcmdldF9pZBgBIAEoCUIIukgFcgOwAQESEgoKYmluZGluZ19pZBgCIAEoCRITCgtyb3V0ZV9hbGlhcxgDIAEoCRIZChFyZWxlYXNlX3B1Ymxpc2hlchgEIAEoCRIUCgxyZWxlYXNlX25hbWUYBSABKAkSFwoPcmVsZWFzZV92ZXJzaW9uGAYgASgJEhkKEW9wZW5lZF9nZW5lcmF0aW9uGAcgASgEEhoKEmFwcGxpZWRfZ2VuZXJhdGlvbhgIIAEoBBIRCglpbnN0YWxsZWQYCSABKAgSFQoEa2luZBgKIAEoCUIHukgEcgIYICJsCh1MaXN0QXZhaWxhYmxlU29sdXRpb25zUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEh0KCXBhZ2Vfc2l6ZRgCIAEoBUIKukgHGgUY9AMoABISCgpwYWdlX3Rva2VuGAMgASgJInEKHkxpc3RBdmFpbGFibGVTb2x1dGlvbnNSZXNwb25zZRI2Cglzb2x1dGlvbnMYASADKAsyIy5zYWFzLmFjY291bnRzLnYxLkF2YWlsYWJsZVNvbHV0aW9uEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSrzAwoWUmVhZGluZXNzQ29uZGl0aW9uS2luZBIoCiRSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfVU5TUEVDSUZJRUQQABIxCi1SRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfSU5TVEFMTF9TVEVQX1BFTkRJTkcQARIwCixSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfSU5TVEFMTF9TVEVQX0ZBSUxFRBACEi0KKVJFQURJTkVTU19DT05ESVRJT05fS0lORF9SRUNPUkRfVU5IRUFMVEhZEAMSNQoxUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX01PRFVMRV9QUklOQ0lQQUxfT01JVFRFRBAEEjYKMlJFQURJTkVTU19DT05ESVRJT05fS0lORF9PUEVSQVRJT05fQklORElOR19PTUlUVEVEEAUSMQotUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX1NPVVJDRV9OT1RfREVMRUdBVEVEEAYSOQo1UkVBRElORVNTX0NPTkRJVElPTl9LSU5EX1JFQUNIRURfTU9EVUxFX05PVF9JTlNUQUxMRUQQBxI+CjpSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfQ0FSUklFRF9NT0RVTEVfQUNDRVNTX05PVF9ERVJJVkVEEAgq1QEKElJlYWRpbmVzc0NsZWFyZWRCeRIkCiBSRUFESU5FU1NfQ0xFQVJFRF9CWV9VTlNQRUNJRklFRBAAEh8KG1JFQURJTkVTU19DTEVBUkVEX0JZX1ZJRVdFUhABEisKJ1JFQURJTkVTU19DTEVBUkVEX0JZX09SR0FOSVpBVElPTl9BRE1JThACEioKJlJFQURJTkVTU19DTEVBUkVEX0JZX1BMQVRGT1JNX09QRVJBVE9SEAMSHwobUkVBRElORVNTX0NMRUFSRURfQllfTk9CT0RZEAQqnwIKD1JlYWRpbmVzc0FjdGlvbhIgChxSRUFESU5FU1NfQUNUSU9OX1VOU1BFQ0lGSUVEEAASKQolUkVBRElORVNTX0FDVElPTl9OT05FX0FUX1RISVNfU1VSRkFDRRABEicKI1JFQURJTkVTU19BQ1RJT05fUkVUUllfSU5TVEFMTF9TVEVQEAISJQohUkVBRElORVNTX0FDVElPTl9SRUNPTk5FQ1RfU09VUkNFEAMSJwojUkVBRElORVNTX0FDVElPTl9UUkFOU0ZFUl9PV05FUlNISVAQBBIrCidSRUFESU5FU1NfQUNUSU9OX0lOU1RBTExfUkVBQ0hFRF9NT0RVTEUQBRIZChVSRUFESU5FU1NfQUNUSU9OX1dBSVQQBjKqEgoTSW5zdGFsbGF0aW9uU2VydmljZRK6AQoPSW5zdGFsbFNvbHV0aW9uEiguc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsU29sdXRpb25SZXF1ZXN0Gh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24iXcLzGD0IAhAEKgwKBm9yZ19pZBACGAEwATodChlzYWFzLmluc3RhbGxhdGlvbi5jcmVhdGVkEAJAAUgEUANYA2ABgtPkkwIWOgEqIhEvdjEvaW5zdGFsbGF0aW9ucxLSAQoRVW5pbnN0YWxsU29sdXRpb24SKi5zYWFzLmFjY291bnRzLnYxLlVuaW5zdGFsbFNvbHV0aW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJ5wvMYPQgCEAQqDAoGb3JnX2lkEAIYATABOh0KGXNhYXMuaW5zdGFsbGF0aW9uLnJldm9rZWQQAkABSARQA1gDYAGC0+STAjI6ASoiLS92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9OnVuaW5zdGFsbBLSAQoSTmFycm93SW5zdGFsbGF0aW9uEisuc2Fhcy5hY2NvdW50cy52MS5OYXJyb3dJbnN0YWxsYXRpb25SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5InfC8xg+CAIQBCoMCgZvcmdfaWQQAhgBMAE6Hgoac2Fhcy5pbnN0YWxsYXRpb24ubmFycm93ZWQQAkABSARQA1gDYAGC0+STAi86ASoiKi92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9Om5hcnJvdxLWAQoRUmV0cnlJbnN0YWxsYXRpb24SKi5zYWFzLmFjY291bnRzLnYxLlJldHJ5SW5zdGFsbGF0aW9uUmVxdWVzdBoeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uInXC8xg9CAIQBCoMCgZvcmdfaWQQAhgBMAE6HQoZc2Fhcy5pbnN0YWxsYXRpb24ucmV0cmllZBACQAFIBFADWANgAYLT5JMCLjoBKiIpL3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH06cmV0cnkSiQIKHVRyYW5zZmVySW5zdGFsbGF0aW9uT3duZXJzaGlwEjYuc2Fhcy5hY2NvdW50cy52MS5UcmFuc2Zlckluc3RhbGxhdGlvbk93bmVyc2hpcFJlcXVlc3QaHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbiKPAcLzGEsIAhAEKgwKBm9yZ19pZBACGAEwATorCidzYWFzLmluc3RhbGxhdGlvbi5vd25lcnNoaXBfdHJhbnNmZXJyZWQQAkABSARQA1gDYAGC0+STAjo6ASoiNS92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9OnRyYW5zZmVyT3duZXJzaGlwErkBCg9HZXRJbnN0YWxsYXRpb24SKC5zYWFzLmFjY291bnRzLnYxLkdldEluc3RhbGxhdGlvblJlcXVlc3QaKS5zYWFzLmFjY291bnRzLnYxLkdldEluc3RhbGxhdGlvblJlc3BvbnNlIlHC8xgiCAIQAyoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCJRIjL3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH0ShgEKEUxpc3RJbnN0YWxsYXRpb25zEiouc2Fhcy5hY2NvdW50cy52MS5MaXN0SW5zdGFsbGF0aW9uc1JlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLkxpc3RJbnN0YWxsYXRpb25zUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARLKAQoWTGlzdEF2YWlsYWJsZVNvbHV0aW9ucxIvLnNhYXMuYWNjb3VudHMudjEuTGlzdEF2YWlsYWJsZVNvbHV0aW9uc1JlcXVlc3QaMC5zYWFzLmFjY291bnRzLnYxLkxpc3RBdmFpbGFibGVTb2x1dGlvbnNSZXNwb25zZSJNwvMYIggCEAQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAiESHy92MS9pbnN0YWxsYXRpb25zOmxpc3RBdmFpbGFibGUS0QEKHUxpc3RPcmdhbml6YXRpb25JbnN0YWxsYXRpb25zEjYuc2Fhcy5hY2NvdW50cy52MS5MaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1JlcXVlc3QaNy5zYWFzLmFjY291bnRzLnYxLkxpc3RPcmdhbml6YXRpb25JbnN0YWxsYXRpb25zUmVzcG9uc2UiP8LzGCIIAhAEKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwITEhEvdjEvaW5zdGFsbGF0aW9ucxLwAQoXR3JhbnRJbnN0YWxsYXRpb25Ub1RlYW0SMC5zYWFzLmFjY291bnRzLnYxLkdyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVxdWVzdBoxLnNhYXMuYWNjb3VudHMudjEuR3JhbnRJbnN0YWxsYXRpb25Ub1RlYW1SZXNwb25zZSJwwvMYOAgCEAQqDAoGb3JnX2lkEAIYATABOhYKEnNhYXMuc2NvcGUuZ3JhbnRlZBACQAFIBFADWANgAXgCgtPkkwIuOgEqIikvdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfS90ZWFtcxLtAQoaUmV2b2tlSW5zdGFsbGF0aW9uRnJvbVRlYW0SMy5zYWFzLmFjY291bnRzLnYxLlJldm9rZUluc3RhbGxhdGlvbkZyb21UZWFtUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSKBAcLzGDgIAhAEKgwKBm9yZ19pZBACGAEwAToWChJzYWFzLnNjb3BlLnJldm9rZWQQAkABSARQA1gDYAF4AoLT5JMCPzoBKiI6L3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH0vdGVhbXMve3RlYW1faWR9OnJldm9rZUK6AQoUY29tLnNhYXMuYWNjb3VudHMudjFCEkluc3RhbGxhdGlvbnNQcm90b1ABWixhY2NvdW50cy9wa2cvZ2VuL3NhYXMvYWNjb3VudHMvdjE7YWNjb3VudHN2MaICA1NBWKoCEFNhYXMuQWNjb3VudHMuVjHKAhBTYWFzXEFjY291bnRzXFYx4gIcU2Fhc1xBY2NvdW50c1xWMVxHUEJNZXRhZGF0YeoCElNhYXM6OkFjY291bnRzOjpWMWIGcHJvdG8z", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_saas_accounts_v1_authorization, file_saas_accounts_v1_installation_listing, file_saas_policy_v1_options]); /** * @generated from message saas.accounts.v1.InstallSolutionRequest @@ -229,7 +133,7 @@ export type InstallSolutionRequest = Message<"saas.accounts.v1.InstallSolutionRe * Use `create(InstallSolutionRequestSchema)` to create a new message. */ export const InstallSolutionRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 1); + messageDesc(file_saas_accounts_v1_installations, 0); /** * An org administrator may only remove installed authority. The host reads @@ -261,7 +165,7 @@ export type NarrowInstallationRequest = Message<"saas.accounts.v1.NarrowInstalla * Use `create(NarrowInstallationRequestSchema)` to create a new message. */ export const NarrowInstallationRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 2); + messageDesc(file_saas_accounts_v1_installations, 1); /** * @generated from message saas.accounts.v1.RetryInstallationRequest @@ -283,7 +187,7 @@ export type RetryInstallationRequest = Message<"saas.accounts.v1.RetryInstallati * Use `create(RetryInstallationRequestSchema)` to create a new message. */ export const RetryInstallationRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 3); + messageDesc(file_saas_accounts_v1_installations, 2); /** * @generated from message saas.accounts.v1.UninstallSolutionRequest @@ -305,7 +209,7 @@ export type UninstallSolutionRequest = Message<"saas.accounts.v1.UninstallSoluti * Use `create(UninstallSolutionRequestSchema)` to create a new message. */ export const UninstallSolutionRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 4); + messageDesc(file_saas_accounts_v1_installations, 3); /** * @generated from message saas.accounts.v1.TransferInstallationOwnershipRequest @@ -341,7 +245,7 @@ export type TransferInstallationOwnershipRequest = Message<"saas.accounts.v1.Tra * Use `create(TransferInstallationOwnershipRequestSchema)` to create a new message. */ export const TransferInstallationOwnershipRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 5); + messageDesc(file_saas_accounts_v1_installations, 4); /** * @generated from message saas.accounts.v1.GetInstallationRequest @@ -363,7 +267,7 @@ export type GetInstallationRequest = Message<"saas.accounts.v1.GetInstallationRe * Use `create(GetInstallationRequestSchema)` to create a new message. */ export const GetInstallationRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 6); + messageDesc(file_saas_accounts_v1_installations, 5); /** * @generated from message saas.accounts.v1.GetInstallationResponse @@ -387,105 +291,7 @@ export type GetInstallationResponse = Message<"saas.accounts.v1.GetInstallationR * Use `create(GetInstallationResponseSchema)` to create a new message. */ export const GetInstallationResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 7); - -/** - * ListInstallationsRequest asks what one organization has installed. Every other - * RPC on this service names a single installation, so until this existed nothing - * could answer "what has this org installed?" — which is the authority read a - * per-organization solution projection is built on (issue #949). - * - * @generated from message saas.accounts.v1.ListInstallationsRequest - */ -export type ListInstallationsRequest = Message<"saas.accounts.v1.ListInstallationsRequest"> & { - /** - * @generated from field: string org_id = 1; - */ - orgId: string; - - /** - * Restrict to one lifecycle status. UNSPECIFIED returns every status, so a - * caller that must see only live installs asks for ACTIVE explicitly rather - * than leaning on a default that would silently widen if the enum grew. - * - * @generated from field: saas.accounts.v1.InstallationStatus status = 2; - */ - status: InstallationStatus; - - /** - * Page bound. 0 -> server default; capped so an organization with many - * installations can never return an unbounded result in one call. - * - * @generated from field: int32 page_size = 3; - */ - pageSize: number; - - /** - * Opaque cursor from a previous response's next_page_token; empty for page one. - * - * @generated from field: string page_token = 4; - */ - pageToken: string; -}; - -/** - * Describes the message saas.accounts.v1.ListInstallationsRequest. - * Use `create(ListInstallationsRequestSchema)` to create a new message. - */ -export const ListInstallationsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 8); - -/** - * InstallationSummary is one row of the listing: the installation, plus the health - * resolved live beside it exactly as GetInstallation resolves it for one. Health - * travels with the row because a consumer that renders an installed solution as - * unavailable would otherwise need one GetInstallation per installation. - * - * @generated from message saas.accounts.v1.InstallationSummary - */ -export type InstallationSummary = Message<"saas.accounts.v1.InstallationSummary"> & { - /** - * @generated from field: saas.accounts.v1.Installation installation = 1; - */ - installation?: Installation; - - /** - * @generated from field: saas.accounts.v1.InstallationHealth health = 2; - */ - health: InstallationHealth; -}; - -/** - * Describes the message saas.accounts.v1.InstallationSummary. - * Use `create(InstallationSummarySchema)` to create a new message. - */ -export const InstallationSummarySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 9); - -/** - * @generated from message saas.accounts.v1.ListInstallationsResponse - */ -export type ListInstallationsResponse = Message<"saas.accounts.v1.ListInstallationsResponse"> & { - /** - * @generated from field: repeated saas.accounts.v1.InstallationSummary installations = 1; - */ - installations: InstallationSummary[]; - - /** - * Set when more installations remain; pass it back as page_token. Empty on the - * last page. - * - * @generated from field: string next_page_token = 2; - */ - nextPageToken: string; -}; - -/** - * Describes the message saas.accounts.v1.ListInstallationsResponse. - * Use `create(ListInstallationsResponseSchema)` to create a new message. - */ -export const ListInstallationsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 10); + messageDesc(file_saas_accounts_v1_installations, 6); /** * @generated from message saas.accounts.v1.ListOrganizationInstallationsRequest @@ -516,7 +322,7 @@ export type ListOrganizationInstallationsRequest = Message<"saas.accounts.v1.Lis * Use `create(ListOrganizationInstallationsRequestSchema)` to create a new message. */ export const ListOrganizationInstallationsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 11); + messageDesc(file_saas_accounts_v1_installations, 7); /** * OrganizationInstallation is one active installation as its organisation's @@ -595,7 +401,7 @@ export type OrganizationInstallation = Message<"saas.accounts.v1.OrganizationIns * Use `create(OrganizationInstallationSchema)` to create a new message. */ export const OrganizationInstallationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 12); + messageDesc(file_saas_accounts_v1_installations, 8); /** * ReadinessCondition is one thing that is unmet, in the words of whoever owns @@ -648,7 +454,7 @@ export type ReadinessCondition = Message<"saas.accounts.v1.ReadinessCondition"> * Use `create(ReadinessConditionSchema)` to create a new message. */ export const ReadinessConditionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 13); + messageDesc(file_saas_accounts_v1_installations, 9); /** * InstallationReadiness is whether the installation will work, which is NOT @@ -679,7 +485,7 @@ export type InstallationReadiness = Message<"saas.accounts.v1.InstallationReadin * Use `create(InstallationReadinessSchema)` to create a new message. */ export const InstallationReadinessSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 14); + messageDesc(file_saas_accounts_v1_installations, 10); /** * ReachedModule is one module an installed target declares it reaches on the @@ -728,7 +534,7 @@ export type ReachedModule = Message<"saas.accounts.v1.ReachedModule"> & { * Use `create(ReachedModuleSchema)` to create a new message. */ export const ReachedModuleSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 15); + messageDesc(file_saas_accounts_v1_installations, 11); /** * @generated from message saas.accounts.v1.ListOrganizationInstallationsResponse @@ -752,7 +558,7 @@ export type ListOrganizationInstallationsResponse = Message<"saas.accounts.v1.Li * Use `create(ListOrganizationInstallationsResponseSchema)` to create a new message. */ export const ListOrganizationInstallationsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 16); + messageDesc(file_saas_accounts_v1_installations, 12); /** * @generated from message saas.accounts.v1.GrantInstallationToTeamRequest @@ -789,7 +595,7 @@ export type GrantInstallationToTeamRequest = Message<"saas.accounts.v1.GrantInst * Use `create(GrantInstallationToTeamRequestSchema)` to create a new message. */ export const GrantInstallationToTeamRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 17); + messageDesc(file_saas_accounts_v1_installations, 13); /** * CarriedModuleAccess is one derived grant a solution's grant carried: the @@ -852,7 +658,7 @@ export type CarriedModuleAccess = Message<"saas.accounts.v1.CarriedModuleAccess" * Use `create(CarriedModuleAccessSchema)` to create a new message. */ export const CarriedModuleAccessSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 18); + messageDesc(file_saas_accounts_v1_installations, 14); /** * @generated from message saas.accounts.v1.GrantInstallationToTeamResponse @@ -890,7 +696,7 @@ export type GrantInstallationToTeamResponse = Message<"saas.accounts.v1.GrantIns * Use `create(GrantInstallationToTeamResponseSchema)` to create a new message. */ export const GrantInstallationToTeamResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 19); + messageDesc(file_saas_accounts_v1_installations, 15); /** * @generated from message saas.accounts.v1.RevokeInstallationFromTeamRequest @@ -925,7 +731,7 @@ export type RevokeInstallationFromTeamRequest = Message<"saas.accounts.v1.Revoke * Use `create(RevokeInstallationFromTeamRequestSchema)` to create a new message. */ export const RevokeInstallationFromTeamRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 20); + messageDesc(file_saas_accounts_v1_installations, 16); /** * AvailableSolution is one installable solution target: its immutable identity, @@ -1015,7 +821,7 @@ export type AvailableSolution = Message<"saas.accounts.v1.AvailableSolution"> & * Use `create(AvailableSolutionSchema)` to create a new message. */ export const AvailableSolutionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 21); + messageDesc(file_saas_accounts_v1_installations, 17); /** * @generated from message saas.accounts.v1.ListAvailableSolutionsRequest @@ -1045,7 +851,7 @@ export type ListAvailableSolutionsRequest = Message<"saas.accounts.v1.ListAvaila * Use `create(ListAvailableSolutionsRequestSchema)` to create a new message. */ export const ListAvailableSolutionsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 22); + messageDesc(file_saas_accounts_v1_installations, 18); /** * @generated from message saas.accounts.v1.ListAvailableSolutionsResponse @@ -1067,101 +873,7 @@ export type ListAvailableSolutionsResponse = Message<"saas.accounts.v1.ListAvail * Use `create(ListAvailableSolutionsResponseSchema)` to create a new message. */ export const ListAvailableSolutionsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 23); - -/** - * InstallationStatus is the lifecycle of an installation row. An uninstall is a - * soft delete (status=revoked, revoked_at set) so the agent, grants, and audit - * trail remain resolvable. - * - * @generated from enum saas.accounts.v1.InstallationStatus - */ -export enum InstallationStatus { - /** - * @generated from enum value: INSTALLATION_STATUS_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - - /** - * @generated from enum value: INSTALLATION_STATUS_ACTIVE = 1; - */ - ACTIVE = 1, - - /** - * @generated from enum value: INSTALLATION_STATUS_REVOKED = 2; - */ - REVOKED = 2, - - /** - * Installed, and waiting for the solution to acknowledge its standard install - * step. Nothing can use a pending installation. - * - * @generated from enum value: INSTALLATION_STATUS_PENDING = 3; - */ - PENDING = 3, - - /** - * The solution refused its install step, or never answered within the - * attempt budget. Nothing can use it; an administrator may retry it. - * - * @generated from enum value: INSTALLATION_STATUS_FAILED = 4; - */ - FAILED = 4, -} - -/** - * Describes the enum saas.accounts.v1.InstallationStatus. - */ -export const InstallationStatusSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_saas_accounts_v1_installations, 0); - -/** - * InstallationHealth is resolved live from current authority facts, never - * stored. It is the observable form of the fail-closed mint rule: a - * StartInstallationTask fails exactly when health is not HEALTHY, and transfer - * or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record - * nor any co-owner is currently an org admin (the Zapier-class "connection owner - * left the org" failure), surfaced rather than silently reassigned. - * - * @generated from enum saas.accounts.v1.InstallationHealth - */ -export enum InstallationHealth { - /** - * @generated from enum value: INSTALLATION_HEALTH_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - - /** - * @generated from enum value: INSTALLATION_HEALTH_HEALTHY = 1; - */ - HEALTHY = 1, - - /** - * @generated from enum value: INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER = 2; - */ - NO_ELIGIBLE_OWNER = 2, - - /** - * @generated from enum value: INSTALLATION_HEALTH_AGENT_REVOKED = 3; - */ - AGENT_REVOKED = 3, - - /** - * @generated from enum value: INSTALLATION_HEALTH_AGENT_DISABLED = 4; - */ - AGENT_DISABLED = 4, - - /** - * @generated from enum value: INSTALLATION_HEALTH_STANDING_GRANT_MISSING = 5; - */ - STANDING_GRANT_MISSING = 5, -} - -/** - * Describes the enum saas.accounts.v1.InstallationHealth. - */ -export const InstallationHealthSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_saas_accounts_v1_installations, 1); + messageDesc(file_saas_accounts_v1_installations, 19); /** * ReadinessConditionKind is what kind of thing is unmet. A reader acts on the @@ -1249,7 +961,7 @@ export enum ReadinessConditionKind { * Describes the enum saas.accounts.v1.ReadinessConditionKind. */ export const ReadinessConditionKindSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_saas_accounts_v1_installations, 2); + enumDesc(file_saas_accounts_v1_installations, 0); /** * ReadinessClearedBy is who can clear a condition. It is stated rather than @@ -1294,7 +1006,7 @@ export enum ReadinessClearedBy { * Describes the enum saas.accounts.v1.ReadinessClearedBy. */ export const ReadinessClearedBySchema: GenEnum = /*@__PURE__*/ - enumDesc(file_saas_accounts_v1_installations, 3); + enumDesc(file_saas_accounts_v1_installations, 1); /** * ReadinessAction is what clears the condition, named as the act rather than as @@ -1347,7 +1059,7 @@ export enum ReadinessAction { * Describes the enum saas.accounts.v1.ReadinessAction. */ export const ReadinessActionSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_saas_accounts_v1_installations, 4); + enumDesc(file_saas_accounts_v1_installations, 2); /** * InstallationService composes and administers installations. The headless mint @@ -1434,11 +1146,15 @@ export const InstallationService: GenService<{ * ListInstallations enumerates one organization's installations with their live * health. It is internal-tier on purpose: it takes the organization as a request * field, so only a caller holding the cluster-internal credential may ask, and - * the tenant it asks about is the one the auth-gateway projected from a verified - * identity — never one a browser supplied. It sits beside - * PermissionService.ListAccessibleScopes for that reason: the two are read - * together to answer what a viewer's organization installed and what that viewer - * was granted. + * the tenant it asks about is never one a browser supplied. The auth-gateway is + * its caller, for two reads. Its viewer projection asks about the tenant it + * projected from a verified identity; it sits beside + * PermissionService.ListAccessibleScopes for that reason, the two read together + * to answer what a viewer's organization installed and what that viewer was + * granted. Its declared module route's bearer-less arm asks about the tenant an + * edge-verified Work Context names, a value this issuer signed, to admit a + * headless operation context only when the target is installed there and the + * context is sealed to another active installation. * * @generated from rpc saas.accounts.v1.InstallationService.ListInstallations */ @@ -1479,7 +1195,8 @@ export const InstallationService: GenService<{ * It is the org-admin companion to the internal ListInstallations, and it is a * separate method rather than a relaxed policy on that one: ListInstallations * is read by the gateway for an organisation it projected from a verified - * identity, and a bare tenant credential must never reach it. This one binds + * identity or that an edge-verified Work Context names, and a bare tenant + * credential must never reach it. This one binds * the organisation to the caller's own admin membership, exactly as * ListAvailableSolutions does, so no organisation's administrator can read * another's. The one exception is inherited, not added here: a platform diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts index d55800756..07297b21f 100644 --- a/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts @@ -12,8 +12,8 @@ import type { CollectionReadGrant } from "./authorization_pb"; import { file_saas_accounts_v1_authorization } from "./authorization_pb"; import type { Organization, OrgMembership, User } from "./common_pb"; import { file_saas_accounts_v1_common } from "./common_pb"; -import type { Installation } from "./installations_pb"; -import { file_saas_accounts_v1_installations } from "./installations_pb"; +import type { Installation } from "./installation_listing_pb"; +import { file_saas_accounts_v1_installation_listing } from "./installation_listing_pb"; import type { SolutionRegistration } from "./solution_registry_pb"; import { file_saas_accounts_v1_solution_registry } from "./solution_registry_pb"; import type { GetEventOperationsRequestSchema, GetEventOperationsResponseSchema, ListEventSubscriptionsRequestSchema, ListEventSubscriptionsResponseSchema } from "../../events/v1/operations_pb"; @@ -27,7 +27,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/platform_admin.proto. */ export const file_saas_accounts_v1_platform_admin: GenFile = /*@__PURE__*/ - fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEiVAoMQ2F0YWxvZ3VlR2FwEjQKBnJlYXNvbhgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwUmVhc29uEg4KBmRldGFpbBgCIAEoCSJEChBDYXRhbG9ndWVSZWxlYXNlEhEKCXB1Ymxpc2hlchgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkipQIKEENhdGFsb2d1ZURlc2lyZWQSGwoRZGVjbGFyZWRfcmV2aXNpb24YASABKANIABI/ChVkZWNsYXJlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEj4KEGRlY2xhcmVkX3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIARI+ChRkZWNsYXJlZF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGQoXZGVjbGFyZWRfcmV2aXNpb25fdmFsdWVCGAoWZGVjbGFyZWRfcmVsZWFzZV92YWx1ZSKCAQoWQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbhIbChNhdXRob3JpemVkX3JldmlzaW9uGAEgASgDEhgKEGludmVudG9yeV9kaWdlc3QYAiABKAkSFgoObWVtYmVyX2JpbmRpbmcYAyABKAkSGQoRYnVpbGRfaW5jYXJuYXRpb24YBCABKAQiKAoWQ2F0YWxvZ3VlTm90QXV0aG9yaXplZBIOCgZkZXRhaWwYASABKAki8AEKE0NhdGFsb2d1ZUF1dGhvcml6ZWQSQQoNYXV0aG9yaXphdGlvbhgBIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbkgAEkIKDm5vdF9hdXRob3JpemVkGAIgASgLMiguc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVOb3RBdXRob3JpemVkSAASOwoRYXV0aG9yaXphdGlvbl9nYXAYAyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAQhUKE2F1dGhvcml6YXRpb25fdmFsdWUiiAEKEENhdGFsb2d1ZUFwcGxpZWQSGgoQYXBwbGllZF9yZXZpc2lvbhgBIAEoA0gAEj4KFGFwcGxpZWRfcmV2aXNpb25fZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAEIYChZhcHBsaWVkX3JldmlzaW9uX3ZhbHVlIt8BChpDYXRhbG9ndWVPYnNlcnZlZEV4ZWN1dGlvbhJoChdjb250YWluZXJfaW1hZ2VfZGlnZXN0cxgBIAMoCzJHLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb24uQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSGQoRYnVpbGRfaW5jYXJuYXRpb24YAiABKAQaPAoaQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASLUBAoRQ2F0YWxvZ3VlT2JzZXJ2ZWQSGwoRb2JzZXJ2ZWRfcmV2aXNpb24YASABKANIABI/ChVvYnNlcnZlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEjEKC29ic2VydmVkX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgBEkMKGW9ic2VydmF0aW9uX2ZyZXNobmVzc19nYXAYBCABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkoKEm9ic2VydmVkX2V4ZWN1dGlvbhgFIAEoCzIsLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb25IAhJAChZvYnNlcnZlZF9leGVjdXRpb25fZ2FwGAYgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAhI9Cgd2ZXJkaWN0GAcgASgOMiouc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVPYnNlcnZlZFZlcmRpY3RIAxI1Cgt2ZXJkaWN0X2dhcBgIIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSANCGQoXb2JzZXJ2ZWRfcmV2aXNpb25fdmFsdWVCHQobb2JzZXJ2YXRpb25fZnJlc2huZXNzX3ZhbHVlQhoKGG9ic2VydmVkX2V4ZWN1dGlvbl92YWx1ZUIPCg12ZXJkaWN0X3ZhbHVlIuABChRDYXRhbG9ndWVXaXRoZHJhd2luZxI+ChR3aXRoZHJhd2FsX3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASSQofY3JlZGVudGlhbF9yZXZvY2F0aW9uX3N0YXRlX2dhcBgCIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGAoWd2l0aGRyYXdhbF9zdGF0ZV92YWx1ZUIjCiFjcmVkZW50aWFsX3Jldm9jYXRpb25fc3RhdGVfdmFsdWUibAoQQ2F0YWxvZ3VlUmV0aXJlZBI+ChRyZXRpcmVtZW50X3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSABCGAoWcmV0aXJlbWVudF9zdGF0ZV92YWx1ZSK4AwoVQ2F0YWxvZ3VlSW5zdGFsbGF0aW9uEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEhAKCG9yZ19uYW1lGAIgASgJEjsKDWFnZW50X3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIABI7ChFhZ2VudF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASNgoMcmV2aXNpb25fZ2FwGAUgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIARI8Cg1ncmFudGVkX3RlYW1zGAYgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50Ej4KD2luaGVyaXRlZF90ZWFtcxgHIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ29sbGVjdGlvblJlYWRHcmFudEIVChNhZ2VudF9yZWxlYXNlX3ZhbHVlQhAKDnJldmlzaW9uX3ZhbHVlIuMFCg5DYXRhbG9ndWVFbnRyeRIyCgRraW5kGAEgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVFbnRyeUtpbmQSDAoEbmFtZRgCIAEoCRITCglwdWJsaXNoZXIYAyABKAlIABI3Cg1wdWJsaXNoZXJfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABIzCgdkZXNpcmVkGAUgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVEZXNpcmVkEjkKCmF1dGhvcml6ZWQYBiABKAsyJS5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUF1dGhvcml6ZWQSMwoHYXBwbGllZBgHIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXBwbGllZBI1CghvYnNlcnZlZBgIIAEoCzIjLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWQSOwoLd2l0aGRyYXdpbmcYCSABKAsyJi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVdpdGhkcmF3aW5nEjMKB3JldGlyZWQYCiABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJldGlyZWQSOAoOYnVpbGRfc2l6ZV9nYXAYCyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkEKDHJlZ2lzdHJhdGlvbhgMIAEoCzImLnNhYXMuYWNjb3VudHMudjEuU29sdXRpb25SZWdpc3RyYXRpb25IAogBARI+Cg1pbnN0YWxsYXRpb25zGA0gAygLMicuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVJbnN0YWxsYXRpb25CEQoPcHVibGlzaGVyX3ZhbHVlQhIKEGJ1aWxkX3NpemVfdmFsdWVCDwoNX3JlZ2lzdHJhdGlvbiI6ChxMaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0EhoKEmluY2x1ZGVfdG9tYnN0b25lZBgBIAEoCCKRAgodTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVzcG9uc2USMQoHZW50cmllcxgBIAMoCzIgLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRW50cnkSGQoRcmVnaXN0cnlfcmV2aXNpb24YAiABKAMSZgoUYXBwcm92ZWRfaW52ZW50b3JpZXMYAyADKAsySC5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlLkFwcHJvdmVkSW52ZW50b3JpZXNFbnRyeRo6ChhBcHByb3ZlZEludmVudG9yaWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgMOgI4ASJ4Cg5Nb2R1bGVFbnZlbG9wZRIQCghyZXZpc2lvbhgBIAEoAxIQCghkb2N1bWVudBgCIAEoCRISCgp3cml0dGVuX2J5GAMgASgJEi4KCndyaXR0ZW5fYXQYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIhoKGEdldE1vZHVsZUVudmVsb3BlUmVxdWVzdCKeAQoZR2V0TW9kdWxlRW52ZWxvcGVSZXNwb25zZRI3CghlbnZlbG9wZRgBIAEoCzIgLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlRW52ZWxvcGVIAIgBARI7CglyZXF1ZXN0ZWQYAiADKAsyKC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUF1dGhvcml0eVJlcXVlc3RCCwoJX2VudmVsb3BlIoMBChZNb2R1bGVBdXRob3JpdHlSZXF1ZXN0EhEKCXByaW5jaXBhbBgBIAEoCRIPCgdiaW5kaW5nGAIgASgJEhIKCmdlbmVyYXRpb24YAyABKAMSDQoFYWxpYXMYBCABKAkSDwoHYWxsb3dlZBgFIAEoCBIRCglhdXRob3JpdHkYBiABKAkiXQoYUHV0TW9kdWxlRW52ZWxvcGVSZXF1ZXN0EiIKEWV4cGVjdGVkX3JldmlzaW9uGAEgASgDQge6SAQiAigAEh0KCGRvY3VtZW50GAIgASgJQgu6SAhyBhABKICAQCJPChlQdXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlEjIKCGVudmVsb3BlGAEgASgLMiAuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFbnZlbG9wZSp+ChJDYXRhbG9ndWVFbnRyeUtpbmQSJAogQ0FUQUxPR1VFX0VOVFJZX0tJTkRfVU5TUEVDSUZJRUQQABIfChtDQVRBTE9HVUVfRU5UUllfS0lORF9NT0RVTEUQARIhCh1DQVRBTE9HVUVfRU5UUllfS0lORF9TT0xVVElPThACKq8BChJDYXRhbG9ndWVHYXBSZWFzb24SJAogQ0FUQUxPR1VFX0dBUF9SRUFTT05fVU5TUEVDSUZJRUQQABIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVDT1JERUQQARIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVQT1JURUQQAhIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfT0JTRVJWRUQQAyreAQoYQ2F0YWxvZ3VlT2JzZXJ2ZWRWZXJkaWN0EioKJkNBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1VOU1BFQ0lGSUVEEAASMQotQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19BVVRIT1JJWkVEEAESLgoqQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19ESUZGRVJTEAISMwovQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19VTkFVVEhPUklaRUQQAzKiIwoUUGxhdGZvcm1BZG1pblNlcnZpY2USjgEKC1NlYXJjaFVzZXJzEiQuc2Fhcy5hY2NvdW50cy52MS5TZWFyY2hVc2Vyc1JlcXVlc3QaJS5zYWFzLmFjY291bnRzLnYxLlNlYXJjaFVzZXJzUmVzcG9uc2UiMsLzGBQIAhABMAE6AhABQAFIBVADWANgA4LT5JMCFBISL3YxL3BsYXRmb3JtL3VzZXJzEqsBCgtTdXNwZW5kVXNlchIkLnNhYXMuYWNjb3VudHMudjEuU3VzcGVuZFVzZXJSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5Il7C8xgrCAIQATABOhcKE3NhYXMudXNlci5zdXNwZW5kZWQQAkABSAVQA1gDYAV4AoLT5JMCKToBKiIkL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTpzdXNwZW5kErMBCg1VbnN1c3BlbmRVc2VyEiYuc2Fhcy5hY2NvdW50cy52MS5VbnN1c3BlbmRVc2VyUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJiwvMYLQgCEAEwAToZChVzYWFzLnVzZXIudW5zdXNwZW5kZWQQAkABSAVQA1gDYAV4AoLT5JMCKzoBKiImL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTp1bnN1c3BlbmQS1gEKD0ltcGVyc29uYXRlVXNlchIoLnNhYXMuYWNjb3VudHMudjEuSW1wZXJzb25hdGVVc2VyUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuSW1wZXJzb25hdGVVc2VyUmVzcG9uc2UibsLzGDcIAhABMAQ6Iwofc2Fhcy5wbGF0Zm9ybS51c2VyX2ltcGVyc29uYXRlZBACQAFIBVADWARgA3gCgtPkkwItOgEqIigvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OmltcGVyc29uYXRlEtgBChFTdG9wSW1wZXJzb25hdGlvbhIqLnNhYXMuYWNjb3VudHMudjEuU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5TdG9wSW1wZXJzb25hdGlvblJlc3BvbnNlImrC8xg8CAIQATABOioKJnNhYXMucGxhdGZvcm0udXNlcl9pbXBlcnNvbmF0aW9uX2VuZGVkEAJAAUgFUANYA2ABgtPkkwIkOgEqIh8vdjEvcGxhdGZvcm0vaW1wZXJzb25hdGlvbjpzdG9wEqYBChJMaXN0QWN0aXZlU2Vzc2lvbnMSKy5zYWFzLmFjY291bnRzLnYxLkxpc3RBY3RpdmVTZXNzaW9uc1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkxpc3RBY3RpdmVTZXNzaW9uc1Jlc3BvbnNlIjXC8xgUCAIQATABOgIQAUABSAVQA1gDYAOC0+STAhcSFS92MS9wbGF0Zm9ybS9zZXNzaW9ucxKpAQoNUmV2b2tlU2Vzc2lvbhImLnNhYXMuYWNjb3VudHMudjEuUmV2b2tlU2Vzc2lvblJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiWMLzGCoIAhABMAE6GAoUc2Fhcy5zZXNzaW9uLnJldm9rZWQQAkABSAVQA1gDYAOC0+STAiQqIi92MS9wbGF0Zm9ybS9zZXNzaW9ucy97c2Vzc2lvbl9pZH0SzwEKEkdldE9yZ0VudGl0bGVtZW50cxIrLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnRW50aXRsZW1lbnRzUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2UiXsLzGCIIAhADKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIyEjAvdjEvcGxhdGZvcm0vb3JnYW5pemF0aW9ucy97b3JnX2lkfS9lbnRpdGxlbWVudHMS5AEKE092ZXJyaWRlRW50aXRsZW1lbnQSLC5zYWFzLmFjY291bnRzLnYxLk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0Gi0uc2Fhcy5hY2NvdW50cy52MS5PdmVycmlkZUVudGl0bGVtZW50UmVzcG9uc2UicMLzGDEIAhABMAE6HQoZc2Fhcy5lbnRpdGxlbWVudC5vdmVycmlkZRACQAFIBVADWANgAngCgtPkkwI1OgEqIjAvdjEvcGxhdGZvcm0vb3JnYW5pemF0aW9ucy97b3JnX2lkfS9lbnRpdGxlbWVudHMSrQEKEUdyYW50UGxhdGZvcm1Sb2xlEiouc2Fhcy5hY2NvdW50cy52MS5HcmFudFBsYXRmb3JtUm9sZVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiVMLzGDIIAhABMAQ6Hgoac2Fhcy5wbGF0Zm9ybS5yb2xlX2dyYW50ZWQQAkABSAVQA1gDYAV4AoLT5JMCGDoBKiITL3YxL3BsYXRmb3JtL2FkbWlucxK2AQoSUmV2b2tlUGxhdGZvcm1Sb2xlEisuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VQbGF0Zm9ybVJvbGVSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IlvC8xgyCAIQATAEOh4KGnNhYXMucGxhdGZvcm0ucm9sZV9yZXZva2VkEAJAAUgFUANYA2AFeAKC0+STAh8qHS92MS9wbGF0Zm9ybS9hZG1pbnMve3VzZXJfaWR9EqQBChJMaXN0UGxhdGZvcm1BZG1pbnMSKy5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUFkbWluc1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUFkbWluc1Jlc3BvbnNlIjPC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhUSEy92MS9wbGF0Zm9ybS9hZG1pbnMSjwEKFExpc3RBbGxPcmdhbml6YXRpb25zEi0uc2Fhcy5hY2NvdW50cy52MS5MaXN0QWxsT3JnYW5pemF0aW9uc1JlcXVlc3QaLi5zYWFzLmFjY291bnRzLnYxLkxpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2UiGMLzGBQIAhABMAE6AhABQAFIBVADWANgAxKSAQoVR2V0T3JnYW5pemF0aW9uUm9zdGVyEi4uc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZSIYwvMYFAgCEAEwAToCEAFAAUgFUANYA2ADEqUBChBMaXN0RmVhdHVyZUZsYWdzEikuc2Fhcy5hY2NvdW50cy52MS5MaXN0RmVhdHVyZUZsYWdzUmVxdWVzdBoqLnNhYXMuYWNjb3VudHMudjEuTGlzdEZlYXR1cmVGbGFnc1Jlc3BvbnNlIjrC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhwSGi92MS9wbGF0Zm9ybS9mZWF0dXJlLWZsYWdzEtIBChFVcHNlcnRGZWF0dXJlRmxhZxIqLnNhYXMuYWNjb3VudHMudjEuVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5VcHNlcnRGZWF0dXJlRmxhZ1Jlc3BvbnNlImSIAgHC8xgxCAIQATABOh0KGXNhYXMuZmVhdHVyZV9mbGFnLnVwZGF0ZWQQAkABSAVQA1gDYAV4AoLT5JMCJjoBKhohL3YxL3BsYXRmb3JtL2ZlYXR1cmUtZmxhZ3Mve25hbWV9Ep8BChBHZXRKb2JPcGVyYXRpb25zEiUuc2Fhcy5qb2JzLnYxLkdldEpvYk9wZXJhdGlvbnNSZXF1ZXN0GiYuc2Fhcy5qb2JzLnYxLkdldEpvYk9wZXJhdGlvbnNSZXNwb25zZSI8wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIeEhwvdjEvcGxhdGZvcm0vam9icy9vcGVyYXRpb25zEnwKCExpc3RKb2JzEh0uc2Fhcy5qb2JzLnYxLkxpc3RKb2JzUmVxdWVzdBoeLnNhYXMuam9icy52MS5MaXN0Sm9ic1Jlc3BvbnNlIjHC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhMSES92MS9wbGF0Zm9ybS9qb2JzEn8KBkdldEpvYhIbLnNhYXMuam9icy52MS5HZXRKb2JSZXF1ZXN0Ghwuc2Fhcy5qb2JzLnYxLkdldEpvYlJlc3BvbnNlIjrC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhwSGi92MS9wbGF0Zm9ybS9qb2JzL3tqb2JfaWR9EqwBCglSZXBsYXlKb2ISHi5zYWFzLmpvYnMudjEuUmVwbGF5Sm9iUmVxdWVzdBofLnNhYXMuam9icy52MS5SZXBsYXlKb2JSZXNwb25zZSJewvMYJwgCEAEwAzoVChFzYWFzLmpvYi5yZXBsYXllZBACQANIBVADWANgBYLT5JMCLToBKiIoL3YxL3BsYXRmb3JtL2pvYnMve3NvdXJjZV9qb2JfaWR9OnJlcGxheRKrAQoSR2V0RXZlbnRPcGVyYXRpb25zEikuc2Fhcy5ldmVudHMudjEuR2V0RXZlbnRPcGVyYXRpb25zUmVxdWVzdBoqLnNhYXMuZXZlbnRzLnYxLkdldEV2ZW50T3BlcmF0aW9uc1Jlc3BvbnNlIj7C8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAiASHi92MS9wbGF0Zm9ybS9ldmVudHMvb3BlcmF0aW9ucxK6AQoWTGlzdEV2ZW50U3Vic2NyaXB0aW9ucxItLnNhYXMuZXZlbnRzLnYxLkxpc3RFdmVudFN1YnNjcmlwdGlvbnNSZXF1ZXN0Gi4uc2Fhcy5ldmVudHMudjEuTGlzdEV2ZW50U3Vic2NyaXB0aW9uc1Jlc3BvbnNlIkHC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAiMSIS92MS9wbGF0Zm9ybS9ldmVudHMvc3Vic2NyaXB0aW9ucxKwAQoVTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlEi4uc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXNwb25zZSI2wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIYEhYvdjEvcGxhdGZvcm0vY2F0YWxvZ3VlEsABChFHZXRNb2R1bGVFbnZlbG9wZRIqLnNhYXMuYWNjb3VudHMudjEuR2V0TW9kdWxlRW52ZWxvcGVSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5HZXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlIlLC8xgqCAIQASIUbW9kdWxlX2VudmVsb3BlOnJlYWQwAToCEAFAAUgFUANYA2AFgtPkkwIeEhwvdjEvcGxhdGZvcm0vbW9kdWxlLWVudmVsb3BlEu0BChFQdXRNb2R1bGVFbnZlbG9wZRIqLnNhYXMuYWNjb3VudHMudjEuUHV0TW9kdWxlRW52ZWxvcGVSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5QdXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlIn/C8xhUCAIQASIVbW9kdWxlX2VudmVsb3BlOndyaXRlMAM6KQolc2Fhcy5wbGF0Zm9ybS5tb2R1bGVfZW52ZWxvcGVfd3JpdHRlbhACQAFIBVADWANgBXgCgtPkkwIhOgEqGhwvdjEvcGxhdGZvcm0vbW9kdWxlLWVudmVsb3BlQroBChRjb20uc2Fhcy5hY2NvdW50cy52MUISUGxhdGZvcm1BZG1pblByb3RvUAFaLGFjY291bnRzL3BrZy9nZW4vc2Fhcy9hY2NvdW50cy92MTthY2NvdW50c3YxogIDU0FYqgIQU2Fhcy5BY2NvdW50cy5WMcoCEFNhYXNcQWNjb3VudHNcVjHiAhxTYWFzXEFjY291bnRzXFYxXEdQQk1ldGFkYXRh6gISU2Fhczo6QWNjb3VudHM6OlYxYgZwcm90bzM", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_accounts_v1_common, file_saas_accounts_v1_installations, file_saas_accounts_v1_solution_registry, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); + fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEiVAoMQ2F0YWxvZ3VlR2FwEjQKBnJlYXNvbhgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwUmVhc29uEg4KBmRldGFpbBgCIAEoCSJEChBDYXRhbG9ndWVSZWxlYXNlEhEKCXB1Ymxpc2hlchgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkipQIKEENhdGFsb2d1ZURlc2lyZWQSGwoRZGVjbGFyZWRfcmV2aXNpb24YASABKANIABI/ChVkZWNsYXJlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEj4KEGRlY2xhcmVkX3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIARI+ChRkZWNsYXJlZF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGQoXZGVjbGFyZWRfcmV2aXNpb25fdmFsdWVCGAoWZGVjbGFyZWRfcmVsZWFzZV92YWx1ZSKCAQoWQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbhIbChNhdXRob3JpemVkX3JldmlzaW9uGAEgASgDEhgKEGludmVudG9yeV9kaWdlc3QYAiABKAkSFgoObWVtYmVyX2JpbmRpbmcYAyABKAkSGQoRYnVpbGRfaW5jYXJuYXRpb24YBCABKAQiKAoWQ2F0YWxvZ3VlTm90QXV0aG9yaXplZBIOCgZkZXRhaWwYASABKAki8AEKE0NhdGFsb2d1ZUF1dGhvcml6ZWQSQQoNYXV0aG9yaXphdGlvbhgBIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbkgAEkIKDm5vdF9hdXRob3JpemVkGAIgASgLMiguc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVOb3RBdXRob3JpemVkSAASOwoRYXV0aG9yaXphdGlvbl9nYXAYAyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAQhUKE2F1dGhvcml6YXRpb25fdmFsdWUiiAEKEENhdGFsb2d1ZUFwcGxpZWQSGgoQYXBwbGllZF9yZXZpc2lvbhgBIAEoA0gAEj4KFGFwcGxpZWRfcmV2aXNpb25fZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAEIYChZhcHBsaWVkX3JldmlzaW9uX3ZhbHVlIt8BChpDYXRhbG9ndWVPYnNlcnZlZEV4ZWN1dGlvbhJoChdjb250YWluZXJfaW1hZ2VfZGlnZXN0cxgBIAMoCzJHLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb24uQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSGQoRYnVpbGRfaW5jYXJuYXRpb24YAiABKAQaPAoaQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASLUBAoRQ2F0YWxvZ3VlT2JzZXJ2ZWQSGwoRb2JzZXJ2ZWRfcmV2aXNpb24YASABKANIABI/ChVvYnNlcnZlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEjEKC29ic2VydmVkX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgBEkMKGW9ic2VydmF0aW9uX2ZyZXNobmVzc19nYXAYBCABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkoKEm9ic2VydmVkX2V4ZWN1dGlvbhgFIAEoCzIsLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb25IAhJAChZvYnNlcnZlZF9leGVjdXRpb25fZ2FwGAYgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAhI9Cgd2ZXJkaWN0GAcgASgOMiouc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVPYnNlcnZlZFZlcmRpY3RIAxI1Cgt2ZXJkaWN0X2dhcBgIIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSANCGQoXb2JzZXJ2ZWRfcmV2aXNpb25fdmFsdWVCHQobb2JzZXJ2YXRpb25fZnJlc2huZXNzX3ZhbHVlQhoKGG9ic2VydmVkX2V4ZWN1dGlvbl92YWx1ZUIPCg12ZXJkaWN0X3ZhbHVlIuABChRDYXRhbG9ndWVXaXRoZHJhd2luZxI+ChR3aXRoZHJhd2FsX3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASSQofY3JlZGVudGlhbF9yZXZvY2F0aW9uX3N0YXRlX2dhcBgCIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGAoWd2l0aGRyYXdhbF9zdGF0ZV92YWx1ZUIjCiFjcmVkZW50aWFsX3Jldm9jYXRpb25fc3RhdGVfdmFsdWUibAoQQ2F0YWxvZ3VlUmV0aXJlZBI+ChRyZXRpcmVtZW50X3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSABCGAoWcmV0aXJlbWVudF9zdGF0ZV92YWx1ZSK4AwoVQ2F0YWxvZ3VlSW5zdGFsbGF0aW9uEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEhAKCG9yZ19uYW1lGAIgASgJEjsKDWFnZW50X3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIABI7ChFhZ2VudF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASNgoMcmV2aXNpb25fZ2FwGAUgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIARI8Cg1ncmFudGVkX3RlYW1zGAYgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50Ej4KD2luaGVyaXRlZF90ZWFtcxgHIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ29sbGVjdGlvblJlYWRHcmFudEIVChNhZ2VudF9yZWxlYXNlX3ZhbHVlQhAKDnJldmlzaW9uX3ZhbHVlIuMFCg5DYXRhbG9ndWVFbnRyeRIyCgRraW5kGAEgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVFbnRyeUtpbmQSDAoEbmFtZRgCIAEoCRITCglwdWJsaXNoZXIYAyABKAlIABI3Cg1wdWJsaXNoZXJfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABIzCgdkZXNpcmVkGAUgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVEZXNpcmVkEjkKCmF1dGhvcml6ZWQYBiABKAsyJS5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUF1dGhvcml6ZWQSMwoHYXBwbGllZBgHIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXBwbGllZBI1CghvYnNlcnZlZBgIIAEoCzIjLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWQSOwoLd2l0aGRyYXdpbmcYCSABKAsyJi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVdpdGhkcmF3aW5nEjMKB3JldGlyZWQYCiABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJldGlyZWQSOAoOYnVpbGRfc2l6ZV9nYXAYCyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkEKDHJlZ2lzdHJhdGlvbhgMIAEoCzImLnNhYXMuYWNjb3VudHMudjEuU29sdXRpb25SZWdpc3RyYXRpb25IAogBARI+Cg1pbnN0YWxsYXRpb25zGA0gAygLMicuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVJbnN0YWxsYXRpb25CEQoPcHVibGlzaGVyX3ZhbHVlQhIKEGJ1aWxkX3NpemVfdmFsdWVCDwoNX3JlZ2lzdHJhdGlvbiI6ChxMaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0EhoKEmluY2x1ZGVfdG9tYnN0b25lZBgBIAEoCCKRAgodTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVzcG9uc2USMQoHZW50cmllcxgBIAMoCzIgLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRW50cnkSGQoRcmVnaXN0cnlfcmV2aXNpb24YAiABKAMSZgoUYXBwcm92ZWRfaW52ZW50b3JpZXMYAyADKAsySC5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlLkFwcHJvdmVkSW52ZW50b3JpZXNFbnRyeRo6ChhBcHByb3ZlZEludmVudG9yaWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgMOgI4ASJ4Cg5Nb2R1bGVFbnZlbG9wZRIQCghyZXZpc2lvbhgBIAEoAxIQCghkb2N1bWVudBgCIAEoCRISCgp3cml0dGVuX2J5GAMgASgJEi4KCndyaXR0ZW5fYXQYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIhoKGEdldE1vZHVsZUVudmVsb3BlUmVxdWVzdCKeAQoZR2V0TW9kdWxlRW52ZWxvcGVSZXNwb25zZRI3CghlbnZlbG9wZRgBIAEoCzIgLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlRW52ZWxvcGVIAIgBARI7CglyZXF1ZXN0ZWQYAiADKAsyKC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUF1dGhvcml0eVJlcXVlc3RCCwoJX2VudmVsb3BlIoMBChZNb2R1bGVBdXRob3JpdHlSZXF1ZXN0EhEKCXByaW5jaXBhbBgBIAEoCRIPCgdiaW5kaW5nGAIgASgJEhIKCmdlbmVyYXRpb24YAyABKAMSDQoFYWxpYXMYBCABKAkSDwoHYWxsb3dlZBgFIAEoCBIRCglhdXRob3JpdHkYBiABKAkiXQoYUHV0TW9kdWxlRW52ZWxvcGVSZXF1ZXN0EiIKEWV4cGVjdGVkX3JldmlzaW9uGAEgASgDQge6SAQiAigAEh0KCGRvY3VtZW50GAIgASgJQgu6SAhyBhABKICAQCJPChlQdXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlEjIKCGVudmVsb3BlGAEgASgLMiAuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFbnZlbG9wZSp+ChJDYXRhbG9ndWVFbnRyeUtpbmQSJAogQ0FUQUxPR1VFX0VOVFJZX0tJTkRfVU5TUEVDSUZJRUQQABIfChtDQVRBTE9HVUVfRU5UUllfS0lORF9NT0RVTEUQARIhCh1DQVRBTE9HVUVfRU5UUllfS0lORF9TT0xVVElPThACKq8BChJDYXRhbG9ndWVHYXBSZWFzb24SJAogQ0FUQUxPR1VFX0dBUF9SRUFTT05fVU5TUEVDSUZJRUQQABIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVDT1JERUQQARIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVQT1JURUQQAhIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfT0JTRVJWRUQQAyreAQoYQ2F0YWxvZ3VlT2JzZXJ2ZWRWZXJkaWN0EioKJkNBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1VOU1BFQ0lGSUVEEAASMQotQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19BVVRIT1JJWkVEEAESLgoqQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19ESUZGRVJTEAISMwovQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19VTkFVVEhPUklaRUQQAzKiIwoUUGxhdGZvcm1BZG1pblNlcnZpY2USjgEKC1NlYXJjaFVzZXJzEiQuc2Fhcy5hY2NvdW50cy52MS5TZWFyY2hVc2Vyc1JlcXVlc3QaJS5zYWFzLmFjY291bnRzLnYxLlNlYXJjaFVzZXJzUmVzcG9uc2UiMsLzGBQIAhABMAE6AhABQAFIBVADWANgA4LT5JMCFBISL3YxL3BsYXRmb3JtL3VzZXJzEqsBCgtTdXNwZW5kVXNlchIkLnNhYXMuYWNjb3VudHMudjEuU3VzcGVuZFVzZXJSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5Il7C8xgrCAIQATABOhcKE3NhYXMudXNlci5zdXNwZW5kZWQQAkABSAVQA1gDYAV4AoLT5JMCKToBKiIkL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTpzdXNwZW5kErMBCg1VbnN1c3BlbmRVc2VyEiYuc2Fhcy5hY2NvdW50cy52MS5VbnN1c3BlbmRVc2VyUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJiwvMYLQgCEAEwAToZChVzYWFzLnVzZXIudW5zdXNwZW5kZWQQAkABSAVQA1gDYAV4AoLT5JMCKzoBKiImL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTp1bnN1c3BlbmQS1gEKD0ltcGVyc29uYXRlVXNlchIoLnNhYXMuYWNjb3VudHMudjEuSW1wZXJzb25hdGVVc2VyUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuSW1wZXJzb25hdGVVc2VyUmVzcG9uc2UibsLzGDcIAhABMAQ6Iwofc2Fhcy5wbGF0Zm9ybS51c2VyX2ltcGVyc29uYXRlZBACQAFIBVADWARgA3gCgtPkkwItOgEqIigvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OmltcGVyc29uYXRlEtgBChFTdG9wSW1wZXJzb25hdGlvbhIqLnNhYXMuYWNjb3VudHMudjEuU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5TdG9wSW1wZXJzb25hdGlvblJlc3BvbnNlImrC8xg8CAIQATABOioKJnNhYXMucGxhdGZvcm0udXNlcl9pbXBlcnNvbmF0aW9uX2VuZGVkEAJAAUgFUANYA2ABgtPkkwIkOgEqIh8vdjEvcGxhdGZvcm0vaW1wZXJzb25hdGlvbjpzdG9wEqYBChJMaXN0QWN0aXZlU2Vzc2lvbnMSKy5zYWFzLmFjY291bnRzLnYxLkxpc3RBY3RpdmVTZXNzaW9uc1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkxpc3RBY3RpdmVTZXNzaW9uc1Jlc3BvbnNlIjXC8xgUCAIQATABOgIQAUABSAVQA1gDYAOC0+STAhcSFS92MS9wbGF0Zm9ybS9zZXNzaW9ucxKpAQoNUmV2b2tlU2Vzc2lvbhImLnNhYXMuYWNjb3VudHMudjEuUmV2b2tlU2Vzc2lvblJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiWMLzGCoIAhABMAE6GAoUc2Fhcy5zZXNzaW9uLnJldm9rZWQQAkABSAVQA1gDYAOC0+STAiQqIi92MS9wbGF0Zm9ybS9zZXNzaW9ucy97c2Vzc2lvbl9pZH0SzwEKEkdldE9yZ0VudGl0bGVtZW50cxIrLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnRW50aXRsZW1lbnRzUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2UiXsLzGCIIAhADKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIyEjAvdjEvcGxhdGZvcm0vb3JnYW5pemF0aW9ucy97b3JnX2lkfS9lbnRpdGxlbWVudHMS5AEKE092ZXJyaWRlRW50aXRsZW1lbnQSLC5zYWFzLmFjY291bnRzLnYxLk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0Gi0uc2Fhcy5hY2NvdW50cy52MS5PdmVycmlkZUVudGl0bGVtZW50UmVzcG9uc2UicMLzGDEIAhABMAE6HQoZc2Fhcy5lbnRpdGxlbWVudC5vdmVycmlkZRACQAFIBVADWANgAngCgtPkkwI1OgEqIjAvdjEvcGxhdGZvcm0vb3JnYW5pemF0aW9ucy97b3JnX2lkfS9lbnRpdGxlbWVudHMSrQEKEUdyYW50UGxhdGZvcm1Sb2xlEiouc2Fhcy5hY2NvdW50cy52MS5HcmFudFBsYXRmb3JtUm9sZVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiVMLzGDIIAhABMAQ6Hgoac2Fhcy5wbGF0Zm9ybS5yb2xlX2dyYW50ZWQQAkABSAVQA1gDYAV4AoLT5JMCGDoBKiITL3YxL3BsYXRmb3JtL2FkbWlucxK2AQoSUmV2b2tlUGxhdGZvcm1Sb2xlEisuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VQbGF0Zm9ybVJvbGVSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IlvC8xgyCAIQATAEOh4KGnNhYXMucGxhdGZvcm0ucm9sZV9yZXZva2VkEAJAAUgFUANYA2AFeAKC0+STAh8qHS92MS9wbGF0Zm9ybS9hZG1pbnMve3VzZXJfaWR9EqQBChJMaXN0UGxhdGZvcm1BZG1pbnMSKy5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUFkbWluc1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUFkbWluc1Jlc3BvbnNlIjPC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhUSEy92MS9wbGF0Zm9ybS9hZG1pbnMSjwEKFExpc3RBbGxPcmdhbml6YXRpb25zEi0uc2Fhcy5hY2NvdW50cy52MS5MaXN0QWxsT3JnYW5pemF0aW9uc1JlcXVlc3QaLi5zYWFzLmFjY291bnRzLnYxLkxpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2UiGMLzGBQIAhABMAE6AhABQAFIBVADWANgAxKSAQoVR2V0T3JnYW5pemF0aW9uUm9zdGVyEi4uc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZSIYwvMYFAgCEAEwAToCEAFAAUgFUANYA2ADEqUBChBMaXN0RmVhdHVyZUZsYWdzEikuc2Fhcy5hY2NvdW50cy52MS5MaXN0RmVhdHVyZUZsYWdzUmVxdWVzdBoqLnNhYXMuYWNjb3VudHMudjEuTGlzdEZlYXR1cmVGbGFnc1Jlc3BvbnNlIjrC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhwSGi92MS9wbGF0Zm9ybS9mZWF0dXJlLWZsYWdzEtIBChFVcHNlcnRGZWF0dXJlRmxhZxIqLnNhYXMuYWNjb3VudHMudjEuVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5VcHNlcnRGZWF0dXJlRmxhZ1Jlc3BvbnNlImSIAgHC8xgxCAIQATABOh0KGXNhYXMuZmVhdHVyZV9mbGFnLnVwZGF0ZWQQAkABSAVQA1gDYAV4AoLT5JMCJjoBKhohL3YxL3BsYXRmb3JtL2ZlYXR1cmUtZmxhZ3Mve25hbWV9Ep8BChBHZXRKb2JPcGVyYXRpb25zEiUuc2Fhcy5qb2JzLnYxLkdldEpvYk9wZXJhdGlvbnNSZXF1ZXN0GiYuc2Fhcy5qb2JzLnYxLkdldEpvYk9wZXJhdGlvbnNSZXNwb25zZSI8wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIeEhwvdjEvcGxhdGZvcm0vam9icy9vcGVyYXRpb25zEnwKCExpc3RKb2JzEh0uc2Fhcy5qb2JzLnYxLkxpc3RKb2JzUmVxdWVzdBoeLnNhYXMuam9icy52MS5MaXN0Sm9ic1Jlc3BvbnNlIjHC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhMSES92MS9wbGF0Zm9ybS9qb2JzEn8KBkdldEpvYhIbLnNhYXMuam9icy52MS5HZXRKb2JSZXF1ZXN0Ghwuc2Fhcy5qb2JzLnYxLkdldEpvYlJlc3BvbnNlIjrC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhwSGi92MS9wbGF0Zm9ybS9qb2JzL3tqb2JfaWR9EqwBCglSZXBsYXlKb2ISHi5zYWFzLmpvYnMudjEuUmVwbGF5Sm9iUmVxdWVzdBofLnNhYXMuam9icy52MS5SZXBsYXlKb2JSZXNwb25zZSJewvMYJwgCEAEwAzoVChFzYWFzLmpvYi5yZXBsYXllZBACQANIBVADWANgBYLT5JMCLToBKiIoL3YxL3BsYXRmb3JtL2pvYnMve3NvdXJjZV9qb2JfaWR9OnJlcGxheRKrAQoSR2V0RXZlbnRPcGVyYXRpb25zEikuc2Fhcy5ldmVudHMudjEuR2V0RXZlbnRPcGVyYXRpb25zUmVxdWVzdBoqLnNhYXMuZXZlbnRzLnYxLkdldEV2ZW50T3BlcmF0aW9uc1Jlc3BvbnNlIj7C8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAiASHi92MS9wbGF0Zm9ybS9ldmVudHMvb3BlcmF0aW9ucxK6AQoWTGlzdEV2ZW50U3Vic2NyaXB0aW9ucxItLnNhYXMuZXZlbnRzLnYxLkxpc3RFdmVudFN1YnNjcmlwdGlvbnNSZXF1ZXN0Gi4uc2Fhcy5ldmVudHMudjEuTGlzdEV2ZW50U3Vic2NyaXB0aW9uc1Jlc3BvbnNlIkHC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAiMSIS92MS9wbGF0Zm9ybS9ldmVudHMvc3Vic2NyaXB0aW9ucxKwAQoVTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlEi4uc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXNwb25zZSI2wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIYEhYvdjEvcGxhdGZvcm0vY2F0YWxvZ3VlEsABChFHZXRNb2R1bGVFbnZlbG9wZRIqLnNhYXMuYWNjb3VudHMudjEuR2V0TW9kdWxlRW52ZWxvcGVSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5HZXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlIlLC8xgqCAIQASIUbW9kdWxlX2VudmVsb3BlOnJlYWQwAToCEAFAAUgFUANYA2AFgtPkkwIeEhwvdjEvcGxhdGZvcm0vbW9kdWxlLWVudmVsb3BlEu0BChFQdXRNb2R1bGVFbnZlbG9wZRIqLnNhYXMuYWNjb3VudHMudjEuUHV0TW9kdWxlRW52ZWxvcGVSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5QdXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlIn/C8xhUCAIQASIVbW9kdWxlX2VudmVsb3BlOndyaXRlMAM6KQolc2Fhcy5wbGF0Zm9ybS5tb2R1bGVfZW52ZWxvcGVfd3JpdHRlbhACQAFIBVADWANgBXgCgtPkkwIhOgEqGhwvdjEvcGxhdGZvcm0vbW9kdWxlLWVudmVsb3BlQroBChRjb20uc2Fhcy5hY2NvdW50cy52MUISUGxhdGZvcm1BZG1pblByb3RvUAFaLGFjY291bnRzL3BrZy9nZW4vc2Fhcy9hY2NvdW50cy92MTthY2NvdW50c3YxogIDU0FYqgIQU2Fhcy5BY2NvdW50cy5WMcoCEFNhYXNcQWNjb3VudHNcVjHiAhxTYWFzXEFjY291bnRzXFYxXEdQQk1ldGFkYXRh6gISU2Fhczo6QWNjb3VudHM6OlYxYgZwcm90bzM", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_accounts_v1_common, file_saas_accounts_v1_installation_listing, file_saas_accounts_v1_solution_registry, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); /** * @generated from message saas.accounts.v1.SearchUsersRequest diff --git a/module/services/frontend/code/ui-inventory.json b/module/services/frontend/code/ui-inventory.json index c025c5387..ff76670a6 100644 --- a/module/services/frontend/code/ui-inventory.json +++ b/module/services/frontend/code/ui-inventory.json @@ -5018,6 +5018,7 @@ "react", "sonner", "@/components/empty-state", + "@/gen/saas/accounts/v1/installation_listing_pb", "@/gen/saas/accounts/v1/installations_pb", "@/lib/auth", "@/shared/ui", @@ -5043,6 +5044,7 @@ "sonner", "@/components/empty-state", "@/components/org-selector", + "@/gen/saas/accounts/v1/installation_listing_pb", "@/gen/saas/accounts/v1/installations_pb", "@/lib/auth", "@/shared/lib/read-outcome", From f942a7e7cac5f7fd9336c7d8a13c009e36795134 Mon Sep 17 00:00:00 2001 From: saurabhmaheshwari-obin <242053479+saurabhmaheshwari-obin@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:57:16 -0400 Subject: [PATCH 06/17] fix(auth-gateway): refuse a seal on the alias's module principal, read from accounts A module's route alias and its principal prefix are independent names. Accounts seals a person's capability for audience X to the active installation whose registry principal is ModulePrincipalID(X), while the bearer-less arm found "the target's installation" by the route alias. With module A (alias storage, principal documents) and module B (alias documents), a person's StartTask(audience="documents") is sealed to A's installation, /v1/documents routes to B, and the arm read A's seal as unattended work and admitted it without the person's bearer. A withdrawn binding's row left ACTIVE under the principal had the same effect. Installation gains registry_principal_id = 14 in the accounts-owned installation_listing.proto: the principal the installation was installed under, written at install and never changed. The store's canonical installation columns carry it, so ListInstallations, GetInstallation, the catalogue listing and every RETURNING row report it consistently. The gateway's copy stays byte identical. The arm now also refuses (401 authentication required) a seal whose active row has registry_principal_id equal to modulePrincipalID(alias), or empty: an unknown principal never admits. modulePrincipalID restates accounts' ModulePrincipalID (neither core nor the sdk exports it, and the gateway cannot import accounts); TestModulePrincipalIDMatchesAccounts in the gateway and TestModulePrincipalIDIsPinnedForTheGateway in accounts pin the same values. Tests: the alias/principal mismatch, a stale row under the alias's principal and a seal row with no principal are refused, and each is admitted without the new check; the existing admit case still admits (its fixture rows now carry their principals). TestInstallationReadsCarryTheRegistryPrincipal (Postgres) holds InstallSolution, GetInstallation and ListInstallations to the column. Regenerated with Codefly source candidate 5234ce24 (--path saas for accounts), go generate, contracts and the saas-sdk library. buf breaking and lint pass. Co-Authored-By: Claude Opus 5.5 --- module/GATEWAY_ROUTES.md | 7 +- .../api/accounts/authority/contract.binpb | Bin 1200659 -> 1201411 bytes .../accounts/v1/installation_listing.proto | 12 +++ .../api/accounts/connect/contract.binpb | Bin 1200659 -> 1201411 bytes .../accounts/v1/installation_listing.proto | 12 +++ .../api/auth-gateway/grpc/contract.binpb | Bin 356835 -> 357587 bytes .../accounts/v1/installation_listing.proto | 12 +++ module/contracts/api/catalog.codefly.json | 6 +- module/module.package.codefly.yaml | 6 +- .../pkg/business/module_capabilities_test.go | 16 ++++ .../accounts/v1/installation_listing.pb.go | 25 +++++- .../pkg/infra/installation_dynamic_test.go | 25 ++++++ .../code/pkg/infra/postgres_installations.go | 10 ++- .../generated/openapi-raw/api.swagger.json | 4 + .../accounts/openapi/api.swagger.json | 4 + .../accounts/v1/installation_listing.proto | 12 +++ module/services/auth-gateway/AGENTS.md | 11 ++- .../code/gateway_module_routes_test.go | 77 ++++++++++++++++-- .../code/gateway_workload_operation.go | 32 +++++++- .../accounts/v1/installation_listing.pb.go | 25 +++++- .../accounts/v1/installation_listing.proto | 12 +++ .../generated/contract/catalog.codefly.json | 2 +- .../generated/contract/contract.binpb | Bin 1200659 -> 1201411 bytes .../saas-sdk/generated/library.codefly.yaml | 2 +- .../accounts/v1/installation_listing_pb.ts | 16 +++- .../accounts/v1/installation_listing_pb.ts | 16 +++- 26 files changed, 304 insertions(+), 40 deletions(-) diff --git a/module/GATEWAY_ROUTES.md b/module/GATEWAY_ROUTES.md index b0ccf38ac..d9991d0bd 100644 --- a/module/GATEWAY_ROUTES.md +++ b/module/GATEWAY_ROUTES.md @@ -132,8 +132,11 @@ there may name the alias or `module-capabilities`; anything else is 401 `workload-operation:`, failing closed; and accounts' `ListInstallations` for that tenant (ACTIVE, uncached) must show an installation of the target (else 403) and the seal as an active installation - that is none of the target's (else 401 `authentication required`, the answer a - person's capability has without its bearer). 503 when the authority cannot answer or + that is none of the target's and whose registry principal is known and is not + the alias's module principal (else 401 `authentication required`, the answer + a person's capability has without its bearer). The principal check is what + holds when a module's alias and principal differ: accounts seals a person's + capability by principal, the route resolves by alias. 503 when the authority cannot answer or is not wired. Admitted requests carry no bearer and no identity header, and `x-codefly-installation-id` is set to the seal, which names the CALLER's installation, not consent on the target's side. An upstream failure is a diff --git a/module/contracts/api/accounts/authority/contract.binpb b/module/contracts/api/accounts/authority/contract.binpb index 18d4889d20273ab7985cefe5699eb283f050e078..828546096daf56301a66ca03fdb954fdf36f4432 100644 GIT binary patch delta 1502 zcmY*YO=uit7~b!jFPqtKHb0&GG;A>Yr5Y-j#(42iwY7rKoQ%O!5zCjIFWE7(Gn@Hw zH;r-qnKU$zUc}DLlP5)6p|Bv^@dEV!H=ku?Y zKKpv<>ib+zf+Zq05==c;W@4GZ#o=?&wD%la>b_}U#pR>tzm<-Bmo>i%}eIMPaA zoack)K%sCmIeJn&*Zt?NaZxY6Pbyy$k^J@c=y><7d&Uz!Tjm3-K+6NBwnlaS6XPs} zCR?L6tK4SdorJ6mjdes=w*z6jK`0$ZhPLO5mOKt9OGK`v{PQSNBG<&lv3$|0dw~*` za_kS3k8&A>bqmmhG<3ixw(ElahMiO`414$xcsS zuKa{>Y#r7m=<@+*gLx5n80;&Fm9wm9L_sJ*Yq`*q*^Bo*<7UEO!tqm{S>U{XGz3Re zn#qv{H2W+c>K&Ah6MD|p_z2h2gVOg()26o0Ugo2`)DH|#np&N`z<;ECh%#cbX5k}J zxlKhf{Jk;NUA}KrNvElcjDXMrZgdkjYT!hHQg9(pLCI!mBu7xPX^!Pc2O57Z<3$^U zq`aFGulDn#XlfB1=f9I|E+$1(RDLC5Md24rp8jkcor@->^J>hFW~zNeA*oFV`K?|r zHCdZ(C;vV$Zh39}R9=LZ_V`3*451Xpv?sS2^ypZFLXU>X>20mAFpFUB7UL%)7;=r3 z`y(~vcVNZ4nPjAP67tY^uewvpx7O0&}S#j7edSbU%D(Tp%5TYmCC~nCR1) zDoP7QlkE~ZjQNZKW=P~o)b5gz<67w5CGa49X-eTAyG~$WuNAdGc`L~v-0Rn(^?E1m zEkl-O7H^QkO+xUb8q-jEgXptasYVOTWc!4sx5gM?hF;2(;M^z1Ob*eyPpb1d1j{~o zaj_4{fXNOB+^T1j2p$0NnaY*qljZX3$)AsmxzPhsn##-3eXuuoXy7a38^Sk?Zv@|H Lckb}i(@*~cn+jQY delta 792 zcmYk3&ubGw6vubo%qF`tX@0CpS|V+?g0~i<2zsa$#7j>F>q&|rHqg{)Oqya-X=36+ zuY%IOdGaJE2+AH*@Z>@3uiUC0%*BHTFa865XR|9F-r+Ovz3+VIJ9i&1-hZ@s$73FO z+!LPZCA_3(dA28?t3OZp%X8BIHc+$v`jJf8FTYF7um?fwsk%mi`%DeF{qv+uI;LUt z{oz4kE;#iwzjdsZqHLCkxKd8NK&C10?SgGX;ax8oWSr{tT z1^ig>L-50KGG6DA!|(*U*MrHxR{2Xc?^rO_Y}0rJGk8n-%x!W^=>; zd?Iz;fAcXbCmVJg1=o|sarsuK46|Ft?O^4TT3}r%xgb1!%NjkD$UrP@N{4BKG>kq1 zfWda=-ylTWQ6fY-&26NqKMABEUSpb~Ako6wHr5oZ@UCO-7=6a1_Q5f?W6gw5ko(>L3ns;U_y7O^ diff --git a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installation_listing.proto b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installation_listing.proto index 57150f92b..7e50d1a27 100644 --- a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installation_listing.proto +++ b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installation_listing.proto @@ -92,6 +92,18 @@ message Installation { // Why the install step failed: the solution's bounded refusal reason, or a // fixed reason when it never answered. Empty unless status is FAILED. string failure_reason = 13 [(buf.validate.field).string.max_len = 512]; + // The module principal this installation was installed under: the registry + // principal whose delivered authority named the target's binding, written at + // install and never changed. A person's capability for a module audience is + // sealed to the active installation whose registry principal is that + // audience's, so a reader that must tell a person's seal from unattended work + // compares this, not the route alias. Empty on a solution's installation and + // on a row installed before the column existed; a reader treats empty as + // unknown, never as "not this principal". + string registry_principal_id = 14 [ + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + (buf.validate.field).string.uuid = true + ]; } // ListInstallationsRequest asks what one organization has installed. Every other diff --git a/module/contracts/api/accounts/connect/contract.binpb b/module/contracts/api/accounts/connect/contract.binpb index 18d4889d20273ab7985cefe5699eb283f050e078..828546096daf56301a66ca03fdb954fdf36f4432 100644 GIT binary patch delta 1502 zcmY*YO=uit7~b!jFPqtKHb0&GG;A>Yr5Y-j#(42iwY7rKoQ%O!5zCjIFWE7(Gn@Hw zH;r-qnKU$zUc}DLlP5)6p|Bv^@dEV!H=ku?Y zKKpv<>ib+zf+Zq05==c;W@4GZ#o=?&wD%la>b_}U#pR>tzm<-Bmo>i%}eIMPaA zoack)K%sCmIeJn&*Zt?NaZxY6Pbyy$k^J@c=y><7d&Uz!Tjm3-K+6NBwnlaS6XPs} zCR?L6tK4SdorJ6mjdes=w*z6jK`0$ZhPLO5mOKt9OGK`v{PQSNBG<&lv3$|0dw~*` za_kS3k8&A>bqmmhG<3ixw(ElahMiO`414$xcsS zuKa{>Y#r7m=<@+*gLx5n80;&Fm9wm9L_sJ*Yq`*q*^Bo*<7UEO!tqm{S>U{XGz3Re zn#qv{H2W+c>K&Ah6MD|p_z2h2gVOg()26o0Ugo2`)DH|#np&N`z<;ECh%#cbX5k}J zxlKhf{Jk;NUA}KrNvElcjDXMrZgdkjYT!hHQg9(pLCI!mBu7xPX^!Pc2O57Z<3$^U zq`aFGulDn#XlfB1=f9I|E+$1(RDLC5Md24rp8jkcor@->^J>hFW~zNeA*oFV`K?|r zHCdZ(C;vV$Zh39}R9=LZ_V`3*451Xpv?sS2^ypZFLXU>X>20mAFpFUB7UL%)7;=r3 z`y(~vcVNZ4nPjAP67tY^uewvpx7O0&}S#j7edSbU%D(Tp%5TYmCC~nCR1) zDoP7QlkE~ZjQNZKW=P~o)b5gz<67w5CGa49X-eTAyG~$WuNAdGc`L~v-0Rn(^?E1m zEkl-O7H^QkO+xUb8q-jEgXptasYVOTWc!4sx5gM?hF;2(;M^z1Ob*eyPpb1d1j{~o zaj_4{fXNOB+^T1j2p$0NnaY*qljZX3$)AsmxzPhsn##-3eXuuoXy7a38^Sk?Zv@|H Lckb}i(@*~cn+jQY delta 792 zcmYk3&ubGw6vubo%qF`tX@0CpS|V+?g0~i<2zsa$#7j>F>q&|rHqg{)Oqya-X=36+ zuY%IOdGaJE2+AH*@Z>@3uiUC0%*BHTFa865XR|9F-r+Ovz3+VIJ9i&1-hZ@s$73FO z+!LPZCA_3(dA28?t3OZp%X8BIHc+$v`jJf8FTYF7um?fwsk%mi`%DeF{qv+uI;LUt z{oz4kE;#iwzjdsZqHLCkxKd8NK&C10?SgGX;ax8oWSr{tT z1^ig>L-50KGG6DA!|(*U*MrHxR{2Xc?^rO_Y}0rJGk8n-%x!W^=>; zd?Iz;fAcXbCmVJg1=o|sarsuK46|Ft?O^4TT3}r%xgb1!%NjkD$UrP@N{4BKG>kq1 zfWda=-ylTWQ6fY-&26NqKMABEUSpb~Ako6wHr5oZ@UCO-7=6a1_Q5f?W6gw5ko(>L3ns;U_y7O^ diff --git a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installation_listing.proto b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installation_listing.proto index 57150f92b..7e50d1a27 100644 --- a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installation_listing.proto +++ b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installation_listing.proto @@ -92,6 +92,18 @@ message Installation { // Why the install step failed: the solution's bounded refusal reason, or a // fixed reason when it never answered. Empty unless status is FAILED. string failure_reason = 13 [(buf.validate.field).string.max_len = 512]; + // The module principal this installation was installed under: the registry + // principal whose delivered authority named the target's binding, written at + // install and never changed. A person's capability for a module audience is + // sealed to the active installation whose registry principal is that + // audience's, so a reader that must tell a person's seal from unattended work + // compares this, not the route alias. Empty on a solution's installation and + // on a row installed before the column existed; a reader treats empty as + // unknown, never as "not this principal". + string registry_principal_id = 14 [ + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + (buf.validate.field).string.uuid = true + ]; } // ListInstallationsRequest asks what one organization has installed. Every other diff --git a/module/contracts/api/auth-gateway/grpc/contract.binpb b/module/contracts/api/auth-gateway/grpc/contract.binpb index 76aca22241b72ebe6e39ac9c395d0936bcce2421..066debe0eee09219955750cfcb91725d081cdae4 100644 GIT binary patch delta 1432 zcmYjQO=~1o6z%uwCR6m^;2>Q~+M zVRsCfcF~z3jTzlIjT=`A;vl*Z8bkyU#I+)R;AWVGA4`9M=T%pQEYG{=-gEA$TMr-B ze*UNSPM`I8-}#DuJ>vJT4u3zQTkMz7>jT>5SG*TQE!I0hlE!;CT5(|NK}&@<0)J^n zvL~u9PJMmV{f)8M`I52gb0zcDlK8Se`u<&d;8Z`jD9(5jmCD`G=kHVKvYVsj59vR# zJ}oAw!lx%ZXPdj?TM}fUN83E0@G3Y8w8&Mpjd(x;Tul@1pPppx|TsAWTKVzoJP5> zs;6*O3aB)QOyRfO9=aUVYL{N&gLI!zRHnSv^sSZQDpLnyfayyQi zNK4tpe3Q@+$}VJnzZ&&z&-k1=2+7Tt*?L%y8Mi| zH}2&gb@~2q{V%%L>$;DYB(!wrYk3-sbEMH-*eCdLu?B}92QQ2JPT??r!Q4F(r(ZDS zZJI7dI;bBYiZ^o0C>`*np9io74m4nxIH)`z#KhW7NI#{ zQ>!IJ>wv9oln^Qd_VhCaWD_18Go;tZCoxoyhwptxH_slk>QYIvucPXH`s0WH0g%N) AlmGw# delta 728 zcmYk3OKTKC6otEP-8R)Vc}zOflQ5E`1L8_iAmK zeK?x=XpyKuFqY8td?L4nA2qK fN_Z(+4eu>Q>Em6#UhWES!Y<%Dy4jyy{<`-UuxDl* diff --git a/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/installation_listing.proto b/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/installation_listing.proto index 57150f92b..7e50d1a27 100644 --- a/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/installation_listing.proto +++ b/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/installation_listing.proto @@ -92,6 +92,18 @@ message Installation { // Why the install step failed: the solution's bounded refusal reason, or a // fixed reason when it never answered. Empty unless status is FAILED. string failure_reason = 13 [(buf.validate.field).string.max_len = 512]; + // The module principal this installation was installed under: the registry + // principal whose delivered authority named the target's binding, written at + // install and never changed. A person's capability for a module audience is + // sealed to the active installation whose registry principal is that + // audience's, so a reader that must tell a person's seal from unattended work + // compares this, not the route alias. Empty on a solution's installation and + // on a row installed before the column existed; a reader treats empty as + // unknown, never as "not this principal". + string registry_principal_id = 14 [ + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + (buf.validate.field).string.uuid = true + ]; } // ListInstallationsRequest asks what one organization has installed. Every other diff --git a/module/contracts/api/catalog.codefly.json b/module/contracts/api/catalog.codefly.json index fc3322c3c..f9d9b1929 100644 --- a/module/contracts/api/catalog.codefly.json +++ b/module/contracts/api/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/authority/contract.binpb", - "digest": "sha256:72172e6df496c728bdc8eec06a8f10f23c74a55b97a9eb86f08ea4d3e1bdd35c", + "digest": "sha256:35eb88dcfeb5febada3d7f051607af72afe9b2f48cfcab6899366f3aaf490204", "services": [ { "name": "ModuleCapabilitiesService", @@ -65,7 +65,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/connect/contract.binpb", - "digest": "sha256:72172e6df496c728bdc8eec06a8f10f23c74a55b97a9eb86f08ea4d3e1bdd35c", + "digest": "sha256:35eb88dcfeb5febada3d7f051607af72afe9b2f48cfcab6899366f3aaf490204", "services": [ { "name": "APIKeyService", @@ -562,7 +562,7 @@ "kind": "protobuf", "package": "saas.gateway.auth.v1", "path": "contracts/api/auth-gateway/grpc/contract.binpb", - "digest": "sha256:78b298b17f102c488eeff5d2dbc93115e27975995ce78dfad8c308bd2da523d7", + "digest": "sha256:c0437ee2b2451fff02460abbef5b10946c35ab9728a04786b5fc22d134c1f6b0", "services": [ { "name": "AuthGatewayService", diff --git a/module/module.package.codefly.yaml b/module/module.package.codefly.yaml index 1710dbc54..cdab4a6fd 100644 --- a/module/module.package.codefly.yaml +++ b/module/module.package.codefly.yaml @@ -18,12 +18,12 @@ services: kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/authority/contract.binpb - digest: sha256:72172e6df496c728bdc8eec06a8f10f23c74a55b97a9eb86f08ea4d3e1bdd35c + digest: sha256:35eb88dcfeb5febada3d7f051607af72afe9b2f48cfcab6899366f3aaf490204 - endpoint: connect kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/connect/contract.binpb - digest: sha256:72172e6df496c728bdc8eec06a8f10f23c74a55b97a9eb86f08ea4d3e1bdd35c + digest: sha256:35eb88dcfeb5febada3d7f051607af72afe9b2f48cfcab6899366f3aaf490204 - name: auth-gateway endpoints: [grpc, rest] api-contracts: @@ -31,7 +31,7 @@ services: kind: protobuf package: saas.gateway.auth.v1 path: contracts/api/auth-gateway/grpc/contract.binpb - digest: sha256:78b298b17f102c488eeff5d2dbc93115e27975995ce78dfad8c308bd2da523d7 + digest: sha256:c0437ee2b2451fff02460abbef5b10946c35ab9728a04786b5fc22d134c1f6b0 - name: cache endpoints: [read, write] - name: frontend diff --git a/module/services/accounts/code/pkg/business/module_capabilities_test.go b/module/services/accounts/code/pkg/business/module_capabilities_test.go index 007822a09..bb897e991 100644 --- a/module/services/accounts/code/pkg/business/module_capabilities_test.go +++ b/module/services/accounts/code/pkg/business/module_capabilities_test.go @@ -1813,3 +1813,19 @@ func TestACredentialForThePrincipalsOldTenantIsRefused(t *testing.T) { t.Fatalf("a refused credential enqueued %d jobs", len(backend.enqueued)) } } + +// The auth-gateway cannot import this package, so it restates ModulePrincipalID +// (modulePrincipalID in gateway_workload_operation.go) to compare an +// installation's registry principal with a route alias's. These are the values +// TestModulePrincipalIDMatchesAccounts holds the gateway to; changing the +// derivation here fails this test, and must change the gateway's too. +func TestModulePrincipalIDIsPinnedForTheGateway(t *testing.T) { + for prefix, id := range map[string]string{ + "documents": "8d4a32d7-50a7-5009-a69d-71bf529b22a2", + "storage": "44943497-cee3-5b83-9ada-8ec1384050cf", + } { + if got := business.ModulePrincipalID(prefix); got != id { + t.Fatalf("ModulePrincipalID(%q) = %s, the gateway pins %s", prefix, got, id) + } + } +} diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go index 10897223b..aa3a96850 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go @@ -186,8 +186,17 @@ type Installation struct { // Why the install step failed: the solution's bounded refusal reason, or a // fixed reason when it never answered. Empty unless status is FAILED. FailureReason string `protobuf:"bytes,13,opt,name=failure_reason,json=failureReason,proto3" json:"failure_reason,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The module principal this installation was installed under: the registry + // principal whose delivered authority named the target's binding, written at + // install and never changed. A person's capability for a module audience is + // sealed to the active installation whose registry principal is that + // audience's, so a reader that must tell a person's seal from unattended work + // compares this, not the route alias. Empty on a solution's installation and + // on a row installed before the column existed; a reader treats empty as + // unknown, never as "not this principal". + RegistryPrincipalId string `protobuf:"bytes,14,opt,name=registry_principal_id,json=registryPrincipalId,proto3" json:"registry_principal_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Installation) Reset() { @@ -304,6 +313,13 @@ func (x *Installation) GetFailureReason() string { return "" } +func (x *Installation) GetRegistryPrincipalId() string { + if x != nil { + return x.RegistryPrincipalId + } + return "" +} + // ListInstallationsRequest asks what one organization has installed. Every other // RPC on this service names a single installation, so until this existed nothing // could answer "what has this org installed?" — which is the authority read a @@ -496,7 +512,7 @@ var File_saas_accounts_v1_installation_listing_proto protoreflect.FileDescriptor const file_saas_accounts_v1_installation_listing_proto_rawDesc = "" + "\n" + - "+saas/accounts/v1/installation_listing.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x94\x05\n" + + "+saas/accounts/v1/installation_listing.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xd5\x05\n" + "\fInstallation\x12\x18\n" + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id\x12\x1f\n" + "\x06org_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x126\n" + @@ -512,7 +528,8 @@ const file_saas_accounts_v1_installation_listing_proto_rawDesc = "" + " \x01(\v2\x1a.google.protobuf.TimestampH\x00R\trevokedAt\x88\x01\x01\x12(\n" + "\ttarget_id\x18\v \x01(\tB\v\xbaH\b\xd8\x01\x01r\x03\xb0\x01\x01R\btargetId\x12/\n" + "\x0erevoked_reason\x18\f \x01(\tB\b\xbaH\x05r\x03\x18\x80\x04R\rrevokedReason\x12/\n" + - "\x0efailure_reason\x18\r \x01(\tB\b\xbaH\x05r\x03\x18\x80\x04R\rfailureReasonB\r\n" + + "\x0efailure_reason\x18\r \x01(\tB\b\xbaH\x05r\x03\x18\x80\x04R\rfailureReason\x12?\n" + + "\x15registry_principal_id\x18\x0e \x01(\tB\v\xbaH\b\xd8\x01\x01r\x03\xb0\x01\x01R\x13registryPrincipalIdB\r\n" + "\v_revoked_atJ\x04\b\x04\x10\x05R\x13solution_identifier\"\xc1\x01\n" + "\x18ListInstallationsRequest\x12\x1f\n" + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x12<\n" + diff --git a/module/services/accounts/code/pkg/infra/installation_dynamic_test.go b/module/services/accounts/code/pkg/infra/installation_dynamic_test.go index 9b6114abb..affc32fdb 100644 --- a/module/services/accounts/code/pkg/infra/installation_dynamic_test.go +++ b/module/services/accounts/code/pkg/infra/installation_dynamic_test.go @@ -235,3 +235,28 @@ func TestDynamicInstallationDBTenancyChangeRevokesOnlyForbiddenOrganizations(t * return err })) } + +// The registry principal an installation was installed under travels on every +// read of it: ListInstallations (which the gateway's bearer-less module arm +// reads to tell a person's seal from unattended work), GetInstallation, and the +// row InstallSolution returns. +func TestInstallationReadsCarryTheRegistryPrincipal(t *testing.T) { + a := newDynamicInstall(t, "", "", "") + require.NotEmpty(t, a.principal) + require.Equal(t, a.principal, a.installation.GetRegistryPrincipalId()) + require.NoError(t, testStore.WithOrgTx(testCtx, a.org, func(ctx context.Context) error { + got, _, err := testStore.GetInstallation(ctx, a.org, a.installation.Id) + if err != nil { + return err + } + require.Equal(t, a.principal, got.GetRegistryPrincipalId()) + summaries, err := testStore.ListInstallations(ctx, a.org, gen.InstallationStatus_INSTALLATION_STATUS_UNSPECIFIED, "", 100) + if err != nil { + return err + } + require.Len(t, summaries, 1) + require.Equal(t, a.installation.Id, summaries[0].GetInstallation().GetId()) + require.Equal(t, a.principal, summaries[0].GetInstallation().GetRegistryPrincipalId()) + return nil + })) +} diff --git a/module/services/accounts/code/pkg/infra/postgres_installations.go b/module/services/accounts/code/pkg/infra/postgres_installations.go index ffd10ac6a..0214de5fa 100644 --- a/module/services/accounts/code/pkg/infra/postgres_installations.go +++ b/module/services/accounts/code/pkg/infra/postgres_installations.go @@ -29,18 +29,20 @@ import ( const installationColumns = `id::text, org_id::text, agent_principal_id::text, owner_principal_id::text, co_owner_principal_ids::text[], root_scope_node_id::text, status, created_at, revoked_at, - COALESCE(target_id::text, ''), COALESCE(revoked_reason, ''), COALESCE(failure_reason, '')` + COALESCE(target_id::text, ''), COALESCE(revoked_reason, ''), COALESCE(failure_reason, ''), + COALESCE(registry_principal_id::text, '')` func scanInstallation(row rowScanner) (*gen.Installation, error) { var ( id, orgID, agentID, ownerID, rootNodeID, status string targetID, revokedReason, failureReason string + registryPrincipalID string coOwners []string createdAt time.Time revokedAt *time.Time ) if err := row.Scan(&id, &orgID, &agentID, &ownerID, &coOwners, - &rootNodeID, &status, &createdAt, &revokedAt, &targetID, &revokedReason, &failureReason); err != nil { + &rootNodeID, &status, &createdAt, &revokedAt, &targetID, &revokedReason, &failureReason, ®istryPrincipalID); err != nil { return nil, err } out := &gen.Installation{ @@ -55,6 +57,7 @@ func scanInstallation(row rowScanner) (*gen.Installation, error) { TargetId: targetID, RevokedReason: revokedReason, FailureReason: failureReason, + RegistryPrincipalId: registryPrincipalID, } if revokedAt != nil { out.RevokedAt = timestamppb.New(*revokedAt) @@ -493,7 +496,8 @@ func installationHealthFromLabel(label string) gen.InstallationHealth { const installationListingColumns = `i.id::text, i.org_id::text, i.agent_principal_id::text, i.owner_principal_id::text, i.co_owner_principal_ids::text[], i.root_scope_node_id::text, i.status, i.created_at, i.revoked_at, - COALESCE(i.target_id::text, ''), COALESCE(i.revoked_reason, ''), COALESCE(i.failure_reason, ''), ` + installationHealthLabel + COALESCE(i.target_id::text, ''), COALESCE(i.revoked_reason, ''), COALESCE(i.failure_reason, ''), + COALESCE(i.registry_principal_id::text, ''), ` + installationHealthLabel // withHealthLabel scans one listing row: the installation's own columns through // scanInstallation, so the wire mapping has one definition, then the label. diff --git a/module/services/accounts/generated/openapi-raw/api.swagger.json b/module/services/accounts/generated/openapi-raw/api.swagger.json index 595d4790d..8e9e3a14c 100644 --- a/module/services/accounts/generated/openapi-raw/api.swagger.json +++ b/module/services/accounts/generated/openapi-raw/api.swagger.json @@ -8997,6 +8997,10 @@ "failureReason": { "type": "string", "description": "Why the install step failed: the solution's bounded refusal reason, or a\nfixed reason when it never answered. Empty unless status is FAILED." + }, + "registryPrincipalId": { + "type": "string", + "description": "The module principal this installation was installed under: the registry\nprincipal whose delivered authority named the target's binding, written at\ninstall and never changed. A person's capability for a module audience is\nsealed to the active installation whose registry principal is that\naudience's, so a reader that must tell a person's seal from unattended work\ncompares this, not the route alias. Empty on a solution's installation and\non a row installed before the column existed; a reader treats empty as\nunknown, never as \"not this principal\"." } }, "description": "Installation is one solution's bound identity in an org. Server-assigned ids;\nthe browser cannot attest to tenancy, the agent principal, or the scope node." diff --git a/module/services/accounts/openapi/api.swagger.json b/module/services/accounts/openapi/api.swagger.json index a24754824..951a3faba 100644 --- a/module/services/accounts/openapi/api.swagger.json +++ b/module/services/accounts/openapi/api.swagger.json @@ -2493,6 +2493,10 @@ "ownerPrincipalId": { "type": "string" }, + "registryPrincipalId": { + "description": "The module principal this installation was installed under: the registry\nprincipal whose delivered authority named the target's binding, written at\ninstall and never changed. A person's capability for a module audience is\nsealed to the active installation whose registry principal is that\naudience's, so a reader that must tell a person's seal from unattended work\ncompares this, not the route alias. Empty on a solution's installation and\non a row installed before the column existed; a reader treats empty as\nunknown, never as \"not this principal\".", + "type": "string" + }, "revokedAt": { "format": "date-time", "type": "string" diff --git a/module/services/accounts/proto/saas/accounts/v1/installation_listing.proto b/module/services/accounts/proto/saas/accounts/v1/installation_listing.proto index 57150f92b..7e50d1a27 100644 --- a/module/services/accounts/proto/saas/accounts/v1/installation_listing.proto +++ b/module/services/accounts/proto/saas/accounts/v1/installation_listing.proto @@ -92,6 +92,18 @@ message Installation { // Why the install step failed: the solution's bounded refusal reason, or a // fixed reason when it never answered. Empty unless status is FAILED. string failure_reason = 13 [(buf.validate.field).string.max_len = 512]; + // The module principal this installation was installed under: the registry + // principal whose delivered authority named the target's binding, written at + // install and never changed. A person's capability for a module audience is + // sealed to the active installation whose registry principal is that + // audience's, so a reader that must tell a person's seal from unattended work + // compares this, not the route alias. Empty on a solution's installation and + // on a row installed before the column existed; a reader treats empty as + // unknown, never as "not this principal". + string registry_principal_id = 14 [ + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + (buf.validate.field).string.uuid = true + ]; } // ListInstallationsRequest asks what one organization has installed. Every other diff --git a/module/services/auth-gateway/AGENTS.md b/module/services/auth-gateway/AGENTS.md index e85493e4f..c97eb3d3b 100644 --- a/module/services/auth-gateway/AGENTS.md +++ b/module/services/auth-gateway/AGENTS.md @@ -108,10 +108,13 @@ capability addressed to the alias, takes the bearer-less arm context, minted at `/modules/_operation-context`. The arm reads no principal. It requires exactly one capability header (401 `invalid work context` otherwise), the signed tenant to have an ACTIVE installation of the target, and the seal to -name an ACTIVE installation in that tenant that is none of the target's, read -from accounts' `ListInstallations` per request and uncached; a seal equal to any -active installation of the target is a person's capability and answers 401 as -before. It strips identity (and `X-Forwarded-Client-Cert`) before its limiter, +name an ACTIVE installation in that tenant that is none of the target's and +whose `registry_principal_id` is set and is not `modulePrincipalID(alias)`, read +from accounts' `ListInstallations` per request and uncached; any other seal may +be a person's capability (accounts seals one by the audience's principal, which +need not be the route alias's target) and answers 401 as before. +`modulePrincipalID` restates accounts' `ModulePrincipalID`, pinned on both sides +by test. It strips identity (and `X-Forwarded-Client-Cert`) before its limiter, meters on `workload-operation:` (fail closed), forwards no bearer, stamps only `x-codefly-installation-id` (the seal, so the CALLER's installation on this arm and the target's admitting installation on the person path; equal to the diff --git a/module/services/auth-gateway/code/gateway_module_routes_test.go b/module/services/auth-gateway/code/gateway_module_routes_test.go index de3981386..80871840d 100644 --- a/module/services/auth-gateway/code/gateway_module_routes_test.go +++ b/module/services/auth-gateway/code/gateway_module_routes_test.go @@ -372,13 +372,22 @@ func activeInstallation(id, target string) *accountsv1.InstallationSummary { }} } +// moduleInstallation is the ACTIVE row of operationTenant for the module whose +// alias and principal prefix are both name: its target, installed under its own +// module principal. +func moduleInstallation(id, name string) *accountsv1.InstallationSummary { + summary := activeInstallation(id, fakeSolutionTarget(name)) + summary.Installation.RegistryPrincipalId = modulePrincipalID(name) + return summary +} + // tenantInstalled is a tenant that installed the target module (E) and the // publishing workload (W). func tenantInstalled() *installationsFake { return &installationsFake{pages: []*accountsv1.ListInstallationsResponse{{ Installations: []*accountsv1.InstallationSummary{ - activeInstallation(targetInstallation, fakeSolutionTarget("documents")), - activeInstallation(publisherSeal, fakeSolutionTarget("publisher")), + moduleInstallation(targetInstallation, "documents"), + moduleInstallation(publisherSeal, "publisher"), }, }}} } @@ -513,15 +522,57 @@ func TestModuleRoute_TheBearerLessArmRefuses(t *testing.T) { prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { gw.installations = &installationsFake{pages: []*accountsv1.ListInstallationsResponse{{ Installations: []*accountsv1.InstallationSummary{ - activeInstallation(targetInstallation, fakeSolutionTarget("documents")), - activeInstallation("55555555-5555-4555-8555-555555555555", fakeSolutionTarget("documents")), - activeInstallation(publisherSeal, fakeSolutionTarget("publisher")), + moduleInstallation(targetInstallation, "documents"), + moduleInstallation("55555555-5555-4555-8555-555555555555", "documents"), + moduleInstallation(publisherSeal, "publisher"), }, }}} return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", targetInstallation))) }, code: http.StatusUnauthorized, body: "authentication required", }, + // The alias and the module principal are independent names. Module A is + // routed at "storage" but its principal is "documents"; module B is + // routed at "documents" under principal "docstore". Accounts seals a + // person's StartTask(audience="documents") to A's installation, which is + // not B's target row, so only the registry principal tells it apart. + "a person's capability sealed to the alias's principal on another target": { + prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + moduleA := activeInstallation("77777777-7777-4777-8777-777777777777", fakeSolutionTarget("storage")) + moduleA.Installation.RegistryPrincipalId = modulePrincipalID("documents") + moduleB := activeInstallation(targetInstallation, fakeSolutionTarget("documents")) + moduleB.Installation.RegistryPrincipalId = modulePrincipalID("docstore") + gw.installations = &installationsFake{pages: []*accountsv1.ListInstallationsResponse{{ + Installations: []*accountsv1.InstallationSummary{moduleA, moduleB, moduleInstallation(publisherSeal, "publisher")}, + }}} + return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", moduleA.Installation.Id))) + }, + code: http.StatusUnauthorized, body: "authentication required", + }, + // A row a withdrawn binding left ACTIVE under the alias's principal is + // still where accounts may seal a person's capability for the alias. + "a person's capability sealed to a stale row of the alias's principal": { + prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + stale := activeInstallation("88888888-8888-4888-8888-888888888888", fakeSolutionTarget("documents-withdrawn")) + stale.Installation.RegistryPrincipalId = modulePrincipalID("documents") + gw.installations = &installationsFake{pages: []*accountsv1.ListInstallationsResponse{{ + Installations: []*accountsv1.InstallationSummary{moduleInstallation(targetInstallation, "documents"), stale, moduleInstallation(publisherSeal, "publisher")}, + }}} + return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", stale.Installation.Id))) + }, + code: http.StatusUnauthorized, body: "authentication required", + }, + // A seal row with no registry principal is unknown, and unknown never + // admits. + "a seal row with no registry principal": { + prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + gw.installations = &installationsFake{pages: []*accountsv1.ListInstallationsResponse{{ + Installations: []*accountsv1.InstallationSummary{moduleInstallation(targetInstallation, "documents"), activeInstallation(publisherSeal, fakeSolutionTarget("publisher"))}, + }}} + return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) + }, + code: http.StatusUnauthorized, body: "authentication required", + }, // Core's Inspector verified the first capability header only; a second // is a value nothing verified, so the arm admits exactly one. "two capability headers": { @@ -541,7 +592,7 @@ func TestModuleRoute_TheBearerLessArmRefuses(t *testing.T) { "a tenant with no active installation of the target": { prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { gw.installations = &installationsFake{pages: []*accountsv1.ListInstallationsResponse{{ - Installations: []*accountsv1.InstallationSummary{activeInstallation(publisherSeal, fakeSolutionTarget("publisher"))}, + Installations: []*accountsv1.InstallationSummary{moduleInstallation(publisherSeal, "publisher")}, }}} return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) }, @@ -732,11 +783,11 @@ func TestModuleRoute_TheBearerLessArmRefusesACapabilityWithNoTenantOrSeal(t *tes // not ACTIVE) counts for neither. func TestModuleRoute_TheBearerLessArmReadsEveryPageItNeeds(t *testing.T) { gw, module, _, kid, wcPriv := operationHarness(t) - foreign := activeInstallation(publisherSeal, fakeSolutionTarget("publisher")) + foreign := moduleInstallation(publisherSeal, "publisher") foreign.Installation.OrgId = "66666666-6666-4666-8666-666666666666" installations := &installationsFake{pages: []*accountsv1.ListInstallationsResponse{ - {Installations: []*accountsv1.InstallationSummary{activeInstallation(targetInstallation, fakeSolutionTarget("documents")), foreign}, NextPageToken: pageCursor(1)}, - {Installations: []*accountsv1.InstallationSummary{activeInstallation(publisherSeal, fakeSolutionTarget("publisher"))}}, + {Installations: []*accountsv1.InstallationSummary{moduleInstallation(targetInstallation, "documents"), foreign}, NextPageToken: pageCursor(1)}, + {Installations: []*accountsv1.InstallationSummary{moduleInstallation(publisherSeal, "publisher")}}, }} gw.installations = installations @@ -755,3 +806,11 @@ func TestModuleRoute_TheBearerLessArmReadsEveryPageItNeeds(t *testing.T) { require.Equal(t, http.StatusUnauthorized, w.Code) require.Equal(t, 1, module.calls) } + +// modulePrincipalID restates accounts' business.ModulePrincipalID. These values +// are accounts' own, and TestModulePrincipalIDIsPinnedForTheGateway in accounts +// holds accounts to the same ones, so a change on either side fails a test. +func TestModulePrincipalIDMatchesAccounts(t *testing.T) { + require.Equal(t, "8d4a32d7-50a7-5009-a69d-71bf529b22a2", modulePrincipalID("documents")) + require.Equal(t, "44943497-cee3-5b83-9ada-8ec1384050cf", modulePrincipalID("storage")) +} diff --git a/module/services/auth-gateway/code/gateway_workload_operation.go b/module/services/auth-gateway/code/gateway_workload_operation.go index f5f1bd732..a18ec33ba 100644 --- a/module/services/auth-gateway/code/gateway_workload_operation.go +++ b/module/services/auth-gateway/code/gateway_workload_operation.go @@ -46,10 +46,21 @@ import ( "github.com/codefly-dev/core/workcontext" carrier "github.com/codefly-dev/sdk-go/workcontext" + "github.com/google/uuid" "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) +// modulePrincipalID is accounts' business.ModulePrincipalID: the registry +// principal id of the module whose principal prefix is prefix, a UUIDv5 under +// the URL namespace. Neither core nor the sdk exports it and the gateway cannot +// import accounts, so it is restated here and pinned to accounts' value by +// TestModulePrincipalIDMatchesAccounts on this side and +// TestModulePrincipalIDIsPinnedForTheGateway on accounts' side. +func modulePrincipalID(prefix string) string { + return uuid.NewSHA1(uuid.NameSpaceURL, []byte("https://codefly.dev/module-principal/"+prefix)).String() +} + const listInstallationsMethod = "/saas.accounts.v1.InstallationService/ListInstallations" // installationListingPageSize and installationListingPageLimit bound one @@ -120,8 +131,9 @@ const ( // the target, or the route resolved no target. workloadOperationNotInstalled // workloadOperationPersonSealed: the seal is one of the target's own - // installations, which is what a person's capability for this module - // carries. + // installations, or an installation of the alias's module principal (or of + // an unknown one), which is where a person's capability for this module is + // sealed. workloadOperationPersonSealed // workloadOperationSealInactive: the seal names no ACTIVE installation in // the tenant (revoked, another tenant's, unknown). @@ -135,7 +147,7 @@ const ( // Uncached, for the reason the viewer admission is: an uninstall changes the // answer with no write this replica would see. func (g *Gateway) admitWorkloadOperation( - ctx context.Context, tenant, seal string, routing *solutionRouting, + ctx context.Context, tenant, seal, alias string, routing *solutionRouting, ) workloadOperationAdmission { targetID := routing.GetTargetID() if targetID == "" { @@ -149,6 +161,12 @@ func (g *Gateway) admitWorkloadOperation( // read the others as some other installation's. targetInstallations := map[string]bool{} sealActive := false + // The registry principal of the seal's row. A person's capability for this + // alias is sealed to the active installation installed under the alias's + // module principal, which need not be the route's target: the route + // resolves the alias to a target, accounts resolves the audience to a + // principal, and the two are independent names. + sealPrincipal := "" token := "" for page := 0; page < installationListingPageLimit; page++ { resp, err := g.installations.List(ctx, &accountsv1.ListInstallationsRequest{ @@ -173,6 +191,7 @@ func (g *Gateway) admitWorkloadOperation( } if installation.GetId() == seal { sealActive = true + sealPrincipal = installation.GetRegistryPrincipalId() } } // No early exit: whether the seal is ANOTHER row of the target is only @@ -190,6 +209,11 @@ func (g *Gateway) admitWorkloadOperation( return workloadOperationNotInstalled case targetInstallations[seal]: return workloadOperationPersonSealed + case sealActive && (sealPrincipal == "" || sealPrincipal == modulePrincipalID(alias)): + // Sealed to the installation of the alias's own module principal, which + // is where accounts seals a person's capability for this audience. An + // empty principal is unknown, and unknown never admits. + return workloadOperationPersonSealed case !sealActive: return workloadOperationSealInactive } @@ -275,7 +299,7 @@ func (g *Gateway) serveWorkloadOperation( reached := false g.rateLimitThenServe(w, r, entry, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { reached = true - switch g.admitWorkloadOperation(r.Context(), rec.tenant, rec.seal, routing) { + switch g.admitWorkloadOperation(r.Context(), rec.tenant, rec.seal, alias, routing) { case workloadOperationUndecidable: rec.refuse(w, http.StatusServiceUnavailable, "module installation authority unavailable", "authority_unavailable") return diff --git a/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go b/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go index 0ea283153..843b79fc1 100644 --- a/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go +++ b/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go @@ -186,8 +186,17 @@ type Installation struct { // Why the install step failed: the solution's bounded refusal reason, or a // fixed reason when it never answered. Empty unless status is FAILED. FailureReason string `protobuf:"bytes,13,opt,name=failure_reason,json=failureReason,proto3" json:"failure_reason,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The module principal this installation was installed under: the registry + // principal whose delivered authority named the target's binding, written at + // install and never changed. A person's capability for a module audience is + // sealed to the active installation whose registry principal is that + // audience's, so a reader that must tell a person's seal from unattended work + // compares this, not the route alias. Empty on a solution's installation and + // on a row installed before the column existed; a reader treats empty as + // unknown, never as "not this principal". + RegistryPrincipalId string `protobuf:"bytes,14,opt,name=registry_principal_id,json=registryPrincipalId,proto3" json:"registry_principal_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Installation) Reset() { @@ -304,6 +313,13 @@ func (x *Installation) GetFailureReason() string { return "" } +func (x *Installation) GetRegistryPrincipalId() string { + if x != nil { + return x.RegistryPrincipalId + } + return "" +} + // ListInstallationsRequest asks what one organization has installed. Every other // RPC on this service names a single installation, so until this existed nothing // could answer "what has this org installed?" — which is the authority read a @@ -496,7 +512,7 @@ var File_saas_accounts_v1_installation_listing_proto protoreflect.FileDescriptor const file_saas_accounts_v1_installation_listing_proto_rawDesc = "" + "\n" + - "+saas/accounts/v1/installation_listing.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x94\x05\n" + + "+saas/accounts/v1/installation_listing.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xd5\x05\n" + "\fInstallation\x12\x18\n" + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id\x12\x1f\n" + "\x06org_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x126\n" + @@ -512,7 +528,8 @@ const file_saas_accounts_v1_installation_listing_proto_rawDesc = "" + " \x01(\v2\x1a.google.protobuf.TimestampH\x00R\trevokedAt\x88\x01\x01\x12(\n" + "\ttarget_id\x18\v \x01(\tB\v\xbaH\b\xd8\x01\x01r\x03\xb0\x01\x01R\btargetId\x12/\n" + "\x0erevoked_reason\x18\f \x01(\tB\b\xbaH\x05r\x03\x18\x80\x04R\rrevokedReason\x12/\n" + - "\x0efailure_reason\x18\r \x01(\tB\b\xbaH\x05r\x03\x18\x80\x04R\rfailureReasonB\r\n" + + "\x0efailure_reason\x18\r \x01(\tB\b\xbaH\x05r\x03\x18\x80\x04R\rfailureReason\x12?\n" + + "\x15registry_principal_id\x18\x0e \x01(\tB\v\xbaH\b\xd8\x01\x01r\x03\xb0\x01\x01R\x13registryPrincipalIdB\r\n" + "\v_revoked_atJ\x04\b\x04\x10\x05R\x13solution_identifier\"\xc1\x01\n" + "\x18ListInstallationsRequest\x12\x1f\n" + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x12<\n" + diff --git a/module/services/auth-gateway/proto/saas/accounts/v1/installation_listing.proto b/module/services/auth-gateway/proto/saas/accounts/v1/installation_listing.proto index 57150f92b..7e50d1a27 100644 --- a/module/services/auth-gateway/proto/saas/accounts/v1/installation_listing.proto +++ b/module/services/auth-gateway/proto/saas/accounts/v1/installation_listing.proto @@ -92,6 +92,18 @@ message Installation { // Why the install step failed: the solution's bounded refusal reason, or a // fixed reason when it never answered. Empty unless status is FAILED. string failure_reason = 13 [(buf.validate.field).string.max_len = 512]; + // The module principal this installation was installed under: the registry + // principal whose delivered authority named the target's binding, written at + // install and never changed. A person's capability for a module audience is + // sealed to the active installation whose registry principal is that + // audience's, so a reader that must tell a person's seal from unattended work + // compares this, not the route alias. Empty on a solution's installation and + // on a row installed before the column existed; a reader treats empty as + // unknown, never as "not this principal". + string registry_principal_id = 14 [ + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + (buf.validate.field).string.uuid = true + ]; } // ListInstallationsRequest asks what one organization has installed. Every other diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json index 441ca77ef..ad36113c3 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json +++ b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contract.binpb", - "digest": "sha256:72172e6df496c728bdc8eec06a8f10f23c74a55b97a9eb86f08ea4d3e1bdd35c", + "digest": "sha256:35eb88dcfeb5febada3d7f051607af72afe9b2f48cfcab6899366f3aaf490204", "services": [ { "name": "APIKeyService", diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb b/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb index 18d4889d20273ab7985cefe5699eb283f050e078..828546096daf56301a66ca03fdb954fdf36f4432 100644 GIT binary patch delta 1502 zcmY*YO=uit7~b!jFPqtKHb0&GG;A>Yr5Y-j#(42iwY7rKoQ%O!5zCjIFWE7(Gn@Hw zH;r-qnKU$zUc}DLlP5)6p|Bv^@dEV!H=ku?Y zKKpv<>ib+zf+Zq05==c;W@4GZ#o=?&wD%la>b_}U#pR>tzm<-Bmo>i%}eIMPaA zoack)K%sCmIeJn&*Zt?NaZxY6Pbyy$k^J@c=y><7d&Uz!Tjm3-K+6NBwnlaS6XPs} zCR?L6tK4SdorJ6mjdes=w*z6jK`0$ZhPLO5mOKt9OGK`v{PQSNBG<&lv3$|0dw~*` za_kS3k8&A>bqmmhG<3ixw(ElahMiO`414$xcsS zuKa{>Y#r7m=<@+*gLx5n80;&Fm9wm9L_sJ*Yq`*q*^Bo*<7UEO!tqm{S>U{XGz3Re zn#qv{H2W+c>K&Ah6MD|p_z2h2gVOg()26o0Ugo2`)DH|#np&N`z<;ECh%#cbX5k}J zxlKhf{Jk;NUA}KrNvElcjDXMrZgdkjYT!hHQg9(pLCI!mBu7xPX^!Pc2O57Z<3$^U zq`aFGulDn#XlfB1=f9I|E+$1(RDLC5Md24rp8jkcor@->^J>hFW~zNeA*oFV`K?|r zHCdZ(C;vV$Zh39}R9=LZ_V`3*451Xpv?sS2^ypZFLXU>X>20mAFpFUB7UL%)7;=r3 z`y(~vcVNZ4nPjAP67tY^uewvpx7O0&}S#j7edSbU%D(Tp%5TYmCC~nCR1) zDoP7QlkE~ZjQNZKW=P~o)b5gz<67w5CGa49X-eTAyG~$WuNAdGc`L~v-0Rn(^?E1m zEkl-O7H^QkO+xUb8q-jEgXptasYVOTWc!4sx5gM?hF;2(;M^z1Ob*eyPpb1d1j{~o zaj_4{fXNOB+^T1j2p$0NnaY*qljZX3$)AsmxzPhsn##-3eXuuoXy7a38^Sk?Zv@|H Lckb}i(@*~cn+jQY delta 792 zcmYk3&ubGw6vubo%qF`tX@0CpS|V+?g0~i<2zsa$#7j>F>q&|rHqg{)Oqya-X=36+ zuY%IOdGaJE2+AH*@Z>@3uiUC0%*BHTFa865XR|9F-r+Ovz3+VIJ9i&1-hZ@s$73FO z+!LPZCA_3(dA28?t3OZp%X8BIHc+$v`jJf8FTYF7um?fwsk%mi`%DeF{qv+uI;LUt z{oz4kE;#iwzjdsZqHLCkxKd8NK&C10?SgGX;ax8oWSr{tT z1^ig>L-50KGG6DA!|(*U*MrHxR{2Xc?^rO_Y}0rJGk8n-%x!W^=>; zd?Iz;fAcXbCmVJg1=o|sarsuK46|Ft?O^4TT3}r%xgb1!%NjkD$UrP@N{4BKG>kq1 zfWda=-ylTWQ6fY-&26NqKMABEUSpb~Ako6wHr5oZ@UCO-7=6a1_Q5f?W6gw5ko(>L3ns;U_y7O^ diff --git a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml index f41dac214..722553097 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml +++ b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml @@ -12,7 +12,7 @@ sources: module: saas-starter service: accounts endpoint: connect - contract-digest: sha256:72172e6df496c728bdc8eec06a8f10f23c74a55b97a9eb86f08ea4d3e1bdd35c + contract-digest: sha256:35eb88dcfeb5febada3d7f051607af72afe9b2f48cfcab6899366f3aaf490204 services: - APIKeyService - AccessibleScopeService diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installation_listing_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installation_listing_pb.ts index 934ddc7e4..b0a743686 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installation_listing_pb.ts +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installation_listing_pb.ts @@ -13,7 +13,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/installation_listing.proto. */ export const file_saas_accounts_v1_installation_listing: GenFile = /*@__PURE__*/ - fileDesc("CitzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbl9saXN0aW5nLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIvkDCgxJbnN0YWxsYXRpb24SFAoCaWQYASABKAlCCLpIBXIDsAEBEhgKBm9yZ19pZBgCIAEoCUIIukgFcgOwAQESJAoSYWdlbnRfcHJpbmNpcGFsX2lkGAMgASgJQgi6SAVyA7ABARIkChJvd25lcl9wcmluY2lwYWxfaWQYBSABKAlCCLpIBXIDsAEBEh4KFmNvX293bmVyX3ByaW5jaXBhbF9pZHMYBiADKAkSJAoScm9vdF9zY29wZV9ub2RlX2lkGAcgASgJQgi6SAVyA7ABARI0CgZzdGF0dXMYCCABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN0YXR1cxIuCgpjcmVhdGVkX2F0GAkgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIzCgpyZXZva2VkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgAiAEBEh4KCXRhcmdldF9pZBgLIAEoCUILukgI2AEBcgOwAQESIAoOcmV2b2tlZF9yZWFzb24YDCABKAlCCLpIBXIDGIAEEiAKDmZhaWx1cmVfcmVhc29uGA0gASgJQgi6SAVyAxiABEINCgtfcmV2b2tlZF9hdEoECAQQBVITc29sdXRpb25faWRlbnRpZmllciKdAQoYTGlzdEluc3RhbGxhdGlvbnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoGc3RhdHVzGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdGF0dXMSHQoJcGFnZV9zaXplGAMgASgFQgq6SAcaBRj0AygAEhIKCnBhZ2VfdG9rZW4YBCABKAkigQEKE0luc3RhbGxhdGlvblN1bW1hcnkSNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SNAoGaGVhbHRoGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25IZWFsdGgicgoZTGlzdEluc3RhbGxhdGlvbnNSZXNwb25zZRI8Cg1pbnN0YWxsYXRpb25zGAEgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdW1tYXJ5EhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSq7AQoSSW5zdGFsbGF0aW9uU3RhdHVzEiMKH0lOU1RBTExBVElPTl9TVEFUVVNfVU5TUEVDSUZJRUQQABIeChpJTlNUQUxMQVRJT05fU1RBVFVTX0FDVElWRRABEh8KG0lOU1RBTExBVElPTl9TVEFUVVNfUkVWT0tFRBACEh8KG0lOU1RBTExBVElPTl9TVEFUVVNfUEVORElORxADEh4KGklOU1RBTExBVElPTl9TVEFUVVNfRkFJTEVEEAQqhAIKEkluc3RhbGxhdGlvbkhlYWx0aBIjCh9JTlNUQUxMQVRJT05fSEVBTFRIX1VOU1BFQ0lGSUVEEAASHwobSU5TVEFMTEFUSU9OX0hFQUxUSF9IRUFMVEhZEAESKQolSU5TVEFMTEFUSU9OX0hFQUxUSF9OT19FTElHSUJMRV9PV05FUhACEiUKIUlOU1RBTExBVElPTl9IRUFMVEhfQUdFTlRfUkVWT0tFRBADEiYKIklOU1RBTExBVElPTl9IRUFMVEhfQUdFTlRfRElTQUJMRUQQBBIuCipJTlNUQUxMQVRJT05fSEVBTFRIX1NUQU5ESU5HX0dSQU5UX01JU1NJTkcQBWIGcHJvdG8z", [file_buf_validate_validate, file_google_protobuf_timestamp]); + fileDesc("CitzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbl9saXN0aW5nLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIqUECgxJbnN0YWxsYXRpb24SFAoCaWQYASABKAlCCLpIBXIDsAEBEhgKBm9yZ19pZBgCIAEoCUIIukgFcgOwAQESJAoSYWdlbnRfcHJpbmNpcGFsX2lkGAMgASgJQgi6SAVyA7ABARIkChJvd25lcl9wcmluY2lwYWxfaWQYBSABKAlCCLpIBXIDsAEBEh4KFmNvX293bmVyX3ByaW5jaXBhbF9pZHMYBiADKAkSJAoScm9vdF9zY29wZV9ub2RlX2lkGAcgASgJQgi6SAVyA7ABARI0CgZzdGF0dXMYCCABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN0YXR1cxIuCgpjcmVhdGVkX2F0GAkgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIzCgpyZXZva2VkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgAiAEBEh4KCXRhcmdldF9pZBgLIAEoCUILukgI2AEBcgOwAQESIAoOcmV2b2tlZF9yZWFzb24YDCABKAlCCLpIBXIDGIAEEiAKDmZhaWx1cmVfcmVhc29uGA0gASgJQgi6SAVyAxiABBIqChVyZWdpc3RyeV9wcmluY2lwYWxfaWQYDiABKAlCC7pICNgBAXIDsAEBQg0KC19yZXZva2VkX2F0SgQIBBAFUhNzb2x1dGlvbl9pZGVudGlmaWVyIp0BChhMaXN0SW5zdGFsbGF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARI0CgZzdGF0dXMYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN0YXR1cxIdCglwYWdlX3NpemUYAyABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgEIAEoCSKBAQoTSW5zdGFsbGF0aW9uU3VtbWFyeRI0CgxpbnN0YWxsYXRpb24YASABKAsyHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbhI0CgZoZWFsdGgYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbkhlYWx0aCJyChlMaXN0SW5zdGFsbGF0aW9uc1Jlc3BvbnNlEjwKDWluc3RhbGxhdGlvbnMYASADKAsyJS5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN1bW1hcnkSFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJKrsBChJJbnN0YWxsYXRpb25TdGF0dXMSIwofSU5TVEFMTEFUSU9OX1NUQVRVU19VTlNQRUNJRklFRBAAEh4KGklOU1RBTExBVElPTl9TVEFUVVNfQUNUSVZFEAESHwobSU5TVEFMTEFUSU9OX1NUQVRVU19SRVZPS0VEEAISHwobSU5TVEFMTEFUSU9OX1NUQVRVU19QRU5ESU5HEAMSHgoaSU5TVEFMTEFUSU9OX1NUQVRVU19GQUlMRUQQBCqEAgoSSW5zdGFsbGF0aW9uSGVhbHRoEiMKH0lOU1RBTExBVElPTl9IRUFMVEhfVU5TUEVDSUZJRUQQABIfChtJTlNUQUxMQVRJT05fSEVBTFRIX0hFQUxUSFkQARIpCiVJTlNUQUxMQVRJT05fSEVBTFRIX05PX0VMSUdJQkxFX09XTkVSEAISJQohSU5TVEFMTEFUSU9OX0hFQUxUSF9BR0VOVF9SRVZPS0VEEAMSJgoiSU5TVEFMTEFUSU9OX0hFQUxUSF9BR0VOVF9ESVNBQkxFRBAEEi4KKklOU1RBTExBVElPTl9IRUFMVEhfU1RBTkRJTkdfR1JBTlRfTUlTU0lORxAFYgZwcm90bzM", [file_buf_validate_validate, file_google_protobuf_timestamp]); /** * Installation is one solution's bound identity in an org. Server-assigned ids; @@ -104,6 +104,20 @@ export type Installation = Message<"saas.accounts.v1.Installation"> & { * @generated from field: string failure_reason = 13; */ failureReason: string; + + /** + * The module principal this installation was installed under: the registry + * principal whose delivered authority named the target's binding, written at + * install and never changed. A person's capability for a module audience is + * sealed to the active installation whose registry principal is that + * audience's, so a reader that must tell a person's seal from unattended work + * compares this, not the route alias. Empty on a solution's installation and + * on a row installed before the column existed; a reader treats empty as + * unknown, never as "not this principal". + * + * @generated from field: string registry_principal_id = 14; + */ + registryPrincipalId: string; }; /** diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/installation_listing_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/installation_listing_pb.ts index c425a987b..dca1d31f7 100644 --- a/module/services/frontend/code/src/gen/saas/accounts/v1/installation_listing_pb.ts +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/installation_listing_pb.ts @@ -13,7 +13,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/installation_listing.proto. */ export const file_saas_accounts_v1_installation_listing: GenFile = /*@__PURE__*/ - fileDesc("CitzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbl9saXN0aW5nLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIvkDCgxJbnN0YWxsYXRpb24SFAoCaWQYASABKAlCCLpIBXIDsAEBEhgKBm9yZ19pZBgCIAEoCUIIukgFcgOwAQESJAoSYWdlbnRfcHJpbmNpcGFsX2lkGAMgASgJQgi6SAVyA7ABARIkChJvd25lcl9wcmluY2lwYWxfaWQYBSABKAlCCLpIBXIDsAEBEh4KFmNvX293bmVyX3ByaW5jaXBhbF9pZHMYBiADKAkSJAoScm9vdF9zY29wZV9ub2RlX2lkGAcgASgJQgi6SAVyA7ABARI0CgZzdGF0dXMYCCABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN0YXR1cxIuCgpjcmVhdGVkX2F0GAkgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIzCgpyZXZva2VkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgAiAEBEh4KCXRhcmdldF9pZBgLIAEoCUILukgI2AEBcgOwAQESIAoOcmV2b2tlZF9yZWFzb24YDCABKAlCCLpIBXIDGIAEEiAKDmZhaWx1cmVfcmVhc29uGA0gASgJQgi6SAVyAxiABEINCgtfcmV2b2tlZF9hdEoECAQQBVITc29sdXRpb25faWRlbnRpZmllciKdAQoYTGlzdEluc3RhbGxhdGlvbnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoGc3RhdHVzGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdGF0dXMSHQoJcGFnZV9zaXplGAMgASgFQgq6SAcaBRj0AygAEhIKCnBhZ2VfdG9rZW4YBCABKAkigQEKE0luc3RhbGxhdGlvblN1bW1hcnkSNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SNAoGaGVhbHRoGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25IZWFsdGgicgoZTGlzdEluc3RhbGxhdGlvbnNSZXNwb25zZRI8Cg1pbnN0YWxsYXRpb25zGAEgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdW1tYXJ5EhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSq7AQoSSW5zdGFsbGF0aW9uU3RhdHVzEiMKH0lOU1RBTExBVElPTl9TVEFUVVNfVU5TUEVDSUZJRUQQABIeChpJTlNUQUxMQVRJT05fU1RBVFVTX0FDVElWRRABEh8KG0lOU1RBTExBVElPTl9TVEFUVVNfUkVWT0tFRBACEh8KG0lOU1RBTExBVElPTl9TVEFUVVNfUEVORElORxADEh4KGklOU1RBTExBVElPTl9TVEFUVVNfRkFJTEVEEAQqhAIKEkluc3RhbGxhdGlvbkhlYWx0aBIjCh9JTlNUQUxMQVRJT05fSEVBTFRIX1VOU1BFQ0lGSUVEEAASHwobSU5TVEFMTEFUSU9OX0hFQUxUSF9IRUFMVEhZEAESKQolSU5TVEFMTEFUSU9OX0hFQUxUSF9OT19FTElHSUJMRV9PV05FUhACEiUKIUlOU1RBTExBVElPTl9IRUFMVEhfQUdFTlRfUkVWT0tFRBADEiYKIklOU1RBTExBVElPTl9IRUFMVEhfQUdFTlRfRElTQUJMRUQQBBIuCipJTlNUQUxMQVRJT05fSEVBTFRIX1NUQU5ESU5HX0dSQU5UX01JU1NJTkcQBULAAQoUY29tLnNhYXMuYWNjb3VudHMudjFCGEluc3RhbGxhdGlvbkxpc3RpbmdQcm90b1ABWixhY2NvdW50cy9wa2cvZ2VuL3NhYXMvYWNjb3VudHMvdjE7YWNjb3VudHN2MaICA1NBWKoCEFNhYXMuQWNjb3VudHMuVjHKAhBTYWFzXEFjY291bnRzXFYx4gIcU2Fhc1xBY2NvdW50c1xWMVxHUEJNZXRhZGF0YeoCElNhYXM6OkFjY291bnRzOjpWMWIGcHJvdG8z", [file_buf_validate_validate, file_google_protobuf_timestamp]); + fileDesc("CitzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbl9saXN0aW5nLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIqUECgxJbnN0YWxsYXRpb24SFAoCaWQYASABKAlCCLpIBXIDsAEBEhgKBm9yZ19pZBgCIAEoCUIIukgFcgOwAQESJAoSYWdlbnRfcHJpbmNpcGFsX2lkGAMgASgJQgi6SAVyA7ABARIkChJvd25lcl9wcmluY2lwYWxfaWQYBSABKAlCCLpIBXIDsAEBEh4KFmNvX293bmVyX3ByaW5jaXBhbF9pZHMYBiADKAkSJAoScm9vdF9zY29wZV9ub2RlX2lkGAcgASgJQgi6SAVyA7ABARI0CgZzdGF0dXMYCCABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN0YXR1cxIuCgpjcmVhdGVkX2F0GAkgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIzCgpyZXZva2VkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgAiAEBEh4KCXRhcmdldF9pZBgLIAEoCUILukgI2AEBcgOwAQESIAoOcmV2b2tlZF9yZWFzb24YDCABKAlCCLpIBXIDGIAEEiAKDmZhaWx1cmVfcmVhc29uGA0gASgJQgi6SAVyAxiABBIqChVyZWdpc3RyeV9wcmluY2lwYWxfaWQYDiABKAlCC7pICNgBAXIDsAEBQg0KC19yZXZva2VkX2F0SgQIBBAFUhNzb2x1dGlvbl9pZGVudGlmaWVyIp0BChhMaXN0SW5zdGFsbGF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARI0CgZzdGF0dXMYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN0YXR1cxIdCglwYWdlX3NpemUYAyABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgEIAEoCSKBAQoTSW5zdGFsbGF0aW9uU3VtbWFyeRI0CgxpbnN0YWxsYXRpb24YASABKAsyHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbhI0CgZoZWFsdGgYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbkhlYWx0aCJyChlMaXN0SW5zdGFsbGF0aW9uc1Jlc3BvbnNlEjwKDWluc3RhbGxhdGlvbnMYASADKAsyJS5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN1bW1hcnkSFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJKrsBChJJbnN0YWxsYXRpb25TdGF0dXMSIwofSU5TVEFMTEFUSU9OX1NUQVRVU19VTlNQRUNJRklFRBAAEh4KGklOU1RBTExBVElPTl9TVEFUVVNfQUNUSVZFEAESHwobSU5TVEFMTEFUSU9OX1NUQVRVU19SRVZPS0VEEAISHwobSU5TVEFMTEFUSU9OX1NUQVRVU19QRU5ESU5HEAMSHgoaSU5TVEFMTEFUSU9OX1NUQVRVU19GQUlMRUQQBCqEAgoSSW5zdGFsbGF0aW9uSGVhbHRoEiMKH0lOU1RBTExBVElPTl9IRUFMVEhfVU5TUEVDSUZJRUQQABIfChtJTlNUQUxMQVRJT05fSEVBTFRIX0hFQUxUSFkQARIpCiVJTlNUQUxMQVRJT05fSEVBTFRIX05PX0VMSUdJQkxFX09XTkVSEAISJQohSU5TVEFMTEFUSU9OX0hFQUxUSF9BR0VOVF9SRVZPS0VEEAMSJgoiSU5TVEFMTEFUSU9OX0hFQUxUSF9BR0VOVF9ESVNBQkxFRBAEEi4KKklOU1RBTExBVElPTl9IRUFMVEhfU1RBTkRJTkdfR1JBTlRfTUlTU0lORxAFQsABChRjb20uc2Fhcy5hY2NvdW50cy52MUIYSW5zdGFsbGF0aW9uTGlzdGluZ1Byb3RvUAFaLGFjY291bnRzL3BrZy9nZW4vc2Fhcy9hY2NvdW50cy92MTthY2NvdW50c3YxogIDU0FYqgIQU2Fhcy5BY2NvdW50cy5WMcoCEFNhYXNcQWNjb3VudHNcVjHiAhxTYWFzXEFjY291bnRzXFYxXEdQQk1ldGFkYXRh6gISU2Fhczo6QWNjb3VudHM6OlYxYgZwcm90bzM", [file_buf_validate_validate, file_google_protobuf_timestamp]); /** * Installation is one solution's bound identity in an org. Server-assigned ids; @@ -104,6 +104,20 @@ export type Installation = Message<"saas.accounts.v1.Installation"> & { * @generated from field: string failure_reason = 13; */ failureReason: string; + + /** + * The module principal this installation was installed under: the registry + * principal whose delivered authority named the target's binding, written at + * install and never changed. A person's capability for a module audience is + * sealed to the active installation whose registry principal is that + * audience's, so a reader that must tell a person's seal from unattended work + * compares this, not the route alias. Empty on a solution's installation and + * on a row installed before the column existed; a reader treats empty as + * unknown, never as "not this principal". + * + * @generated from field: string registry_principal_id = 14; + */ + registryPrincipalId: string; }; /** From 8c34f381ff9e921c2db17455dcf49afc46e2fa0a Mon Sep 17 00:00:00 2001 From: saurabhmaheshwari-obin <242053479+saurabhmaheshwari-obin@users.noreply.github.com> Date: Sat, 24 Oct 2026 12:01:12 -0400 Subject: [PATCH 07/17] fix(accounts): say which installations carry no registry principal, and pin a solution's The registry_principal_id comment said a solution's installation carries none. It does: InstallSolution and a module's install both resolve the declared binding's registry principal in prepareInstallation and refuse without one. The column is empty only on rows written before migration 32 added it (left NULL by design, no backfill infers a principal) or written by the store directly with none. The comment now says so. TestInstallationReadsCarryTheRegistryPrincipal now names its row as a solution's installation and checks that LiveModuleAuthority resolves that solution's principal to the same row: the seal a solution's module identity mints is a row carrying its principal, which the bearer-less arm admits. Regenerated with Codefly source candidate 5234ce24 (--path saas), go generate, contracts and the saas-sdk library. Co-Authored-By: Claude Opus 5.5 --- .../api/accounts/authority/contract.binpb | Bin 1201411 -> 1201678 bytes .../accounts/v1/installation_listing.proto | 9 ++++++--- .../api/accounts/connect/contract.binpb | Bin 1201411 -> 1201678 bytes .../accounts/v1/installation_listing.proto | 9 ++++++--- .../api/auth-gateway/grpc/contract.binpb | Bin 357587 -> 357854 bytes .../accounts/v1/installation_listing.proto | 9 ++++++--- module/contracts/api/catalog.codefly.json | 6 +++--- module/module.package.codefly.yaml | 6 +++--- .../accounts/v1/installation_listing.pb.go | 9 ++++++--- .../pkg/infra/installation_dynamic_test.go | 12 ++++++++++++ .../generated/openapi-raw/api.swagger.json | 2 +- .../accounts/openapi/api.swagger.json | 2 +- .../accounts/v1/installation_listing.proto | 9 ++++++--- .../accounts/v1/installation_listing.pb.go | 9 ++++++--- .../accounts/v1/installation_listing.proto | 9 ++++++--- .../generated/contract/catalog.codefly.json | 2 +- .../generated/contract/contract.binpb | Bin 1201411 -> 1201678 bytes .../saas-sdk/generated/library.codefly.yaml | 2 +- .../accounts/v1/installation_listing_pb.ts | 9 ++++++--- .../accounts/v1/installation_listing_pb.ts | 9 ++++++--- 20 files changed, 76 insertions(+), 37 deletions(-) diff --git a/module/contracts/api/accounts/authority/contract.binpb b/module/contracts/api/accounts/authority/contract.binpb index 828546096daf56301a66ca03fdb954fdf36f4432..97ec3eafb88bdfc60cf59cd9d2e7d9bebc4f5f7a 100644 GIT binary patch delta 1229 zcmYjP&u`pB7~L7qI^OXnyYXg|Y9rDKPzoX~DMBEj@as^a^wNsvQV<7cchXGjwYT;x z-Dn{x)KXH^M9HN-fjA@JMA=hs97-u5a^uH=h#Lna#6Q5ZcU^H9+i$-2-uLDk-~4=e z?~COdF^Oqx#4IkxW;_;KaVh3s+y8!Pd~=Q`za82O$(6?|i^<{xbAAwgW$&4#&zAU@ zQ!Eq?lA~YDQ-hc8+OJt=n>0QpA~{;AP7U_&*?;mGMJ@iV$r&rqvcnYhSmUlKR+4{K z#%qI5?%OxX)_XGC5Xz6V>v=-2he7x9x>z3Ki-FhGD)1M$a78<4bv=195D`9H!R6D0R~f!wuzM7D}V_ zqadkkDBYF}g~l5OGCQ&xC3_F-7cQvM953@?I&#A?)cg~Ce26UEz>}WAE8I$p)^=&u zF|M!|d6kz&8xzxxag{yG?@>+}af(slLo%5B-d-YGktH%)LJLL5(70F|_X?DPSMw49 zDphiX{@SVK$O4*rCF=$m+Na%geQ8uDrepNz6#t84zHsVM(fFN+w`8Ql#*26p{LLjNch*sl)noGx_xg`_5M1n#@U%^r!0CI0&T}M}K;gA&-S)P~2*_zak5(k^BhXS1H!!d7@!T}+;VS}Z?Zxd@iqZ+ip z9JWg+&ujrRbS5vN=Pt45atP@zIlhoXblD}(Esh|;?Do^yt>bJ41sCYC#@orYi{;mo lKYq4PRQE~gSk8ucf4hEYW0kSSu_mx8Sk*!OaPseu{sTdo{y6{u delta 984 zcmYjOO-vI}5Z-z3wd}sPwB^x4(*}4%Ziq@0P5eK2kQ0gFWV}>qrCR#mP{dlI3xp>q0$dnwN7aIHE2M*QKC z6w&!x#u?LyDOrl7S!|2h->!$E%?I0J2?BHZ@`B1`RYH|=nTq`>ZuU;7M0SR^lf~S; zN@q*uL?)A*QJ0gcTrsK2vq^OY*>hQytfoul8H3#9K4&YH zG&Q`Bcejy_B1Y1b2f6MP^;IJ-=~Z@=hq%#61o|YMWuyEx;e>%?dF>X&wurK)o{4C4 z;++_RdR|xVd?1=n(gGeS@J2)qIPH}HkrDI|3Mt5559x@)r`)yJhB}IcP>t<5Ec6h>O!(qF50d4VsESxiF<6ET^SteAcB&rq>FF1dx>OKvS{yp5S#U? z-s6$5r5cX7z5pTUOSNy2p&lL2AgG68rhk!k6uKFlyTJJW860whnVpdo>ov6Ek}DZY zH4=?Ysi}OTP{D^)iY5EYVe_25^D)FnYKCV8b(nqqeB1GxNWTTzDnRTNI{xdq=xx?_ z#Q~^WpvPUYglK-rmVpFtEn^5>q`V@2Y#D<8xOm1gpf%3_2|-)g3ZSp;R^%e`TET^A zYKM!hD^9D4y6RN=*Ff6~k-7%@uuG*xW0Ez1aCa<17ma&G+}Qvz=s|34z|ejV z;+6&+Jk){UlWZN(#96$J667Kph+VXAPMc@#`@5ld%u$CGw`4@UJ{+^Ys#*fW#OTHd OU<5HjP1Wl8dHXLIfu;!n diff --git a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installation_listing.proto b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installation_listing.proto index 7e50d1a27..6b3061c5f 100644 --- a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installation_listing.proto +++ b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installation_listing.proto @@ -97,9 +97,12 @@ message Installation { // install and never changed. A person's capability for a module audience is // sealed to the active installation whose registry principal is that // audience's, so a reader that must tell a person's seal from unattended work - // compares this, not the route alias. Empty on a solution's installation and - // on a row installed before the column existed; a reader treats empty as - // unknown, never as "not this principal". + // compares this, not the route alias. Every install through InstallSolution, + // a module's or a solution's, records one. Empty only on a row written before + // the column existed (it is deliberately not backfilled: no principal is + // inferred) or one the store wrote with no registry principal. A capability + // a module or solution identity mints is sealed only to a row carrying its + // principal. A reader treats empty as unknown, never as "not this principal". string registry_principal_id = 14 [ (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, (buf.validate.field).string.uuid = true diff --git a/module/contracts/api/accounts/connect/contract.binpb b/module/contracts/api/accounts/connect/contract.binpb index 828546096daf56301a66ca03fdb954fdf36f4432..97ec3eafb88bdfc60cf59cd9d2e7d9bebc4f5f7a 100644 GIT binary patch delta 1229 zcmYjP&u`pB7~L7qI^OXnyYXg|Y9rDKPzoX~DMBEj@as^a^wNsvQV<7cchXGjwYT;x z-Dn{x)KXH^M9HN-fjA@JMA=hs97-u5a^uH=h#Lna#6Q5ZcU^H9+i$-2-uLDk-~4=e z?~COdF^Oqx#4IkxW;_;KaVh3s+y8!Pd~=Q`za82O$(6?|i^<{xbAAwgW$&4#&zAU@ zQ!Eq?lA~YDQ-hc8+OJt=n>0QpA~{;AP7U_&*?;mGMJ@iV$r&rqvcnYhSmUlKR+4{K z#%qI5?%OxX)_XGC5Xz6V>v=-2he7x9x>z3Ki-FhGD)1M$a78<4bv=195D`9H!R6D0R~f!wuzM7D}V_ zqadkkDBYF}g~l5OGCQ&xC3_F-7cQvM953@?I&#A?)cg~Ce26UEz>}WAE8I$p)^=&u zF|M!|d6kz&8xzxxag{yG?@>+}af(slLo%5B-d-YGktH%)LJLL5(70F|_X?DPSMw49 zDphiX{@SVK$O4*rCF=$m+Na%geQ8uDrepNz6#t84zHsVM(fFN+w`8Ql#*26p{LLjNch*sl)noGx_xg`_5M1n#@U%^r!0CI0&T}M}K;gA&-S)P~2*_zak5(k^BhXS1H!!d7@!T}+;VS}Z?Zxd@iqZ+ip z9JWg+&ujrRbS5vN=Pt45atP@zIlhoXblD}(Esh|;?Do^yt>bJ41sCYC#@orYi{;mo lKYq4PRQE~gSk8ucf4hEYW0kSSu_mx8Sk*!OaPseu{sTdo{y6{u delta 984 zcmYjOO-vI}5Z-z3wd}sPwB^x4(*}4%Ziq@0P5eK2kQ0gFWV}>qrCR#mP{dlI3xp>q0$dnwN7aIHE2M*QKC z6w&!x#u?LyDOrl7S!|2h->!$E%?I0J2?BHZ@`B1`RYH|=nTq`>ZuU;7M0SR^lf~S; zN@q*uL?)A*QJ0gcTrsK2vq^OY*>hQytfoul8H3#9K4&YH zG&Q`Bcejy_B1Y1b2f6MP^;IJ-=~Z@=hq%#61o|YMWuyEx;e>%?dF>X&wurK)o{4C4 z;++_RdR|xVd?1=n(gGeS@J2)qIPH}HkrDI|3Mt5559x@)r`)yJhB}IcP>t<5Ec6h>O!(qF50d4VsESxiF<6ET^SteAcB&rq>FF1dx>OKvS{yp5S#U? z-s6$5r5cX7z5pTUOSNy2p&lL2AgG68rhk!k6uKFlyTJJW860whnVpdo>ov6Ek}DZY zH4=?Ysi}OTP{D^)iY5EYVe_25^D)FnYKCV8b(nqqeB1GxNWTTzDnRTNI{xdq=xx?_ z#Q~^WpvPUYglK-rmVpFtEn^5>q`V@2Y#D<8xOm1gpf%3_2|-)g3ZSp;R^%e`TET^A zYKM!hD^9D4y6RN=*Ff6~k-7%@uuG*xW0Ez1aCa<17ma&G+}Qvz=s|34z|ejV z;+6&+Jk){UlWZN(#96$J667Kph+VXAPMc@#`@5ld%u$CGw`4@UJ{+^Ys#*fW#OTHd OU<5HjP1Wl8dHXLIfu;!n diff --git a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installation_listing.proto b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installation_listing.proto index 7e50d1a27..6b3061c5f 100644 --- a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installation_listing.proto +++ b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installation_listing.proto @@ -97,9 +97,12 @@ message Installation { // install and never changed. A person's capability for a module audience is // sealed to the active installation whose registry principal is that // audience's, so a reader that must tell a person's seal from unattended work - // compares this, not the route alias. Empty on a solution's installation and - // on a row installed before the column existed; a reader treats empty as - // unknown, never as "not this principal". + // compares this, not the route alias. Every install through InstallSolution, + // a module's or a solution's, records one. Empty only on a row written before + // the column existed (it is deliberately not backfilled: no principal is + // inferred) or one the store wrote with no registry principal. A capability + // a module or solution identity mints is sealed only to a row carrying its + // principal. A reader treats empty as unknown, never as "not this principal". string registry_principal_id = 14 [ (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, (buf.validate.field).string.uuid = true diff --git a/module/contracts/api/auth-gateway/grpc/contract.binpb b/module/contracts/api/auth-gateway/grpc/contract.binpb index 066debe0eee09219955750cfcb91725d081cdae4..940d593db06b46dc9a84913717bea3949b7d1dbb 100644 GIT binary patch delta 1221 zcmY*XO>7fK7~OAn?5qbvU=t_E5n&LKfRfU3>7k-U9H1%})W1s+hmAc+rdjX0v*X5* zX$)$lB#5Bog4J6ORa>b-?V+tty&=S%L)EIP#IYdtUa1G(u6Kc6)_(JS-+S+SGmrkR z{q?kVV}mx>hWiu$ZJYgadHDM_uZ~r|Iw!{blH=?R|JmiCN3G%2FZq9BS~E}FFAL5c zR`t1NA+P^jmLCq@yU!%O>tDEz5UD_=7m?SRF_6LI$!G zH#?y^k;vEpC3)#YGB1s4#HNWtq|V5Tt+rjm-!T24(Zs8A)o5!~Bp1|TY?OeEk_`~F zBH346omkbBC$*J2kxdoq1!V%OKoG@NE(DF0MIDB!`6jSz8yz)tI|$J%;CoRSqnf8u zQ;^S?6AKY&#ui^~m(ww$mLP*opU7i1%7_k#r;YP%vbCan4 z1CxWjJGkZj-|{)?C!Wj%h&d(SwRjf$@G5{(@ODumpvriG;Kce91@eF<-^z3`1|zAi zt)0v3RQBC2n-otd;{{Wf$@-rp-&e?e@Z)#<@vGh1OwkSQ?(ESVF<9!cF>!Y^%Y5Er z>w`m2_}zZbJ5)4bq&Hd1f+5BbOmAwP!;gnG82q?6nO=8uhuH~uuXFM82}nNY)ttmF z`TPbUi))!@5C9#nUy)bZ!KWQO(avD*DZlhedKbi8O6!%0VmND`nidb!+?ikABTi-aleIDClzqNM>;Sgl956$&D1vf}#-I0sa*Gg4D##cj<=cczN2BNkDn==T z2sH1-=yp0_M72Rhgh@9A3) z-&_g4_b4^wJU=IgRKl{>{p}Al9ps+TB^w?Dw_ehoOc&9XpGXo}m{L^45>0GTP<}-( z;KqEdwV8u=+PW&*EQDTmc%a16LKvB(l9PDk<3@q1d9d!k?5=zv0#bMr%DTIrr1WeATAs$~> z2MpOv;}@b~Y~$(SuzGqXu0v9ZHjc|b5cTCwZPXJ#L0@;9t%e`GrtRr=I%m|}yPcnm z2|1F7Lvmx_MMWMi`iFLCqw3lFjfgGYcsjZUAo4Z5%p!3fo7X_jBlsn|DB^_C3-?_h z`Tq+yc|@tWrQrG!pW=2DnM+Il)Gpo0FF9sw5k6+J@=JJ7RLKrltHIT`bQzXAVCSMR zh*nZj7jT5z5-~=kO_Beh3#0$E9J33&dibk|d^TDEzT3dcS|($K2>EESmaQwHHN|0; zg-Z6oS_j~t6eL*M1A8ix3baU3A5cbZ86!GviafIq!+C>j?Ze~=gFK}VCr`x)2}Ntb hN1Ef^1hSUt@Wf63={s`A*1$PvgzQ@jXp4S){tGUAiQE7H diff --git a/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/installation_listing.proto b/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/installation_listing.proto index 7e50d1a27..6b3061c5f 100644 --- a/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/installation_listing.proto +++ b/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/installation_listing.proto @@ -97,9 +97,12 @@ message Installation { // install and never changed. A person's capability for a module audience is // sealed to the active installation whose registry principal is that // audience's, so a reader that must tell a person's seal from unattended work - // compares this, not the route alias. Empty on a solution's installation and - // on a row installed before the column existed; a reader treats empty as - // unknown, never as "not this principal". + // compares this, not the route alias. Every install through InstallSolution, + // a module's or a solution's, records one. Empty only on a row written before + // the column existed (it is deliberately not backfilled: no principal is + // inferred) or one the store wrote with no registry principal. A capability + // a module or solution identity mints is sealed only to a row carrying its + // principal. A reader treats empty as unknown, never as "not this principal". string registry_principal_id = 14 [ (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, (buf.validate.field).string.uuid = true diff --git a/module/contracts/api/catalog.codefly.json b/module/contracts/api/catalog.codefly.json index f9d9b1929..4a3ead6fc 100644 --- a/module/contracts/api/catalog.codefly.json +++ b/module/contracts/api/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/authority/contract.binpb", - "digest": "sha256:35eb88dcfeb5febada3d7f051607af72afe9b2f48cfcab6899366f3aaf490204", + "digest": "sha256:8e59a8dabb889185a44cb9e0d8c9cdc4fbef03b27e7c01f2cc581c69d2e8799a", "services": [ { "name": "ModuleCapabilitiesService", @@ -65,7 +65,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/connect/contract.binpb", - "digest": "sha256:35eb88dcfeb5febada3d7f051607af72afe9b2f48cfcab6899366f3aaf490204", + "digest": "sha256:8e59a8dabb889185a44cb9e0d8c9cdc4fbef03b27e7c01f2cc581c69d2e8799a", "services": [ { "name": "APIKeyService", @@ -562,7 +562,7 @@ "kind": "protobuf", "package": "saas.gateway.auth.v1", "path": "contracts/api/auth-gateway/grpc/contract.binpb", - "digest": "sha256:c0437ee2b2451fff02460abbef5b10946c35ab9728a04786b5fc22d134c1f6b0", + "digest": "sha256:17a79ecb7eeb86732edc38736f51050374f0aa05e5db57053ae657c9106ae876", "services": [ { "name": "AuthGatewayService", diff --git a/module/module.package.codefly.yaml b/module/module.package.codefly.yaml index cdab4a6fd..509b3adfe 100644 --- a/module/module.package.codefly.yaml +++ b/module/module.package.codefly.yaml @@ -18,12 +18,12 @@ services: kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/authority/contract.binpb - digest: sha256:35eb88dcfeb5febada3d7f051607af72afe9b2f48cfcab6899366f3aaf490204 + digest: sha256:8e59a8dabb889185a44cb9e0d8c9cdc4fbef03b27e7c01f2cc581c69d2e8799a - endpoint: connect kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/connect/contract.binpb - digest: sha256:35eb88dcfeb5febada3d7f051607af72afe9b2f48cfcab6899366f3aaf490204 + digest: sha256:8e59a8dabb889185a44cb9e0d8c9cdc4fbef03b27e7c01f2cc581c69d2e8799a - name: auth-gateway endpoints: [grpc, rest] api-contracts: @@ -31,7 +31,7 @@ services: kind: protobuf package: saas.gateway.auth.v1 path: contracts/api/auth-gateway/grpc/contract.binpb - digest: sha256:c0437ee2b2451fff02460abbef5b10946c35ab9728a04786b5fc22d134c1f6b0 + digest: sha256:17a79ecb7eeb86732edc38736f51050374f0aa05e5db57053ae657c9106ae876 - name: cache endpoints: [read, write] - name: frontend diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go index aa3a96850..f7c66938b 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go @@ -191,9 +191,12 @@ type Installation struct { // install and never changed. A person's capability for a module audience is // sealed to the active installation whose registry principal is that // audience's, so a reader that must tell a person's seal from unattended work - // compares this, not the route alias. Empty on a solution's installation and - // on a row installed before the column existed; a reader treats empty as - // unknown, never as "not this principal". + // compares this, not the route alias. Every install through InstallSolution, + // a module's or a solution's, records one. Empty only on a row written before + // the column existed (it is deliberately not backfilled: no principal is + // inferred) or one the store wrote with no registry principal. A capability + // a module or solution identity mints is sealed only to a row carrying its + // principal. A reader treats empty as unknown, never as "not this principal". RegistryPrincipalId string `protobuf:"bytes,14,opt,name=registry_principal_id,json=registryPrincipalId,proto3" json:"registry_principal_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache diff --git a/module/services/accounts/code/pkg/infra/installation_dynamic_test.go b/module/services/accounts/code/pkg/infra/installation_dynamic_test.go index affc32fdb..a90a34ad4 100644 --- a/module/services/accounts/code/pkg/infra/installation_dynamic_test.go +++ b/module/services/accounts/code/pkg/infra/installation_dynamic_test.go @@ -240,10 +240,22 @@ func TestDynamicInstallationDBTenancyChangeRevokesOnlyForbiddenOrganizations(t * // read of it: ListInstallations (which the gateway's bearer-less module arm // reads to tell a person's seal from unattended work), GetInstallation, and the // row InstallSolution returns. +// +// The row is a SOLUTION's installation, the bearer-less arm's main caller: a +// solution's module identity mints an operation context sealed to the row +// LiveModuleAuthority resolves for its principal, so that row carries the +// principal by construction (the read selects on registry_principal_id), and +// the arm, which refuses a seal row with none, admits it. func TestInstallationReadsCarryTheRegistryPrincipal(t *testing.T) { a := newDynamicInstall(t, "", "", "") require.NotEmpty(t, a.principal) require.Equal(t, a.principal, a.installation.GetRegistryPrincipalId()) + // newDynamicInstall installs with Kind "solution"; the insert trigger + // refuses a kind the registry does not declare. + live, err := testStore.LiveModuleAuthority(testCtx, a.principal, a.org, "") + require.NoError(t, err) + require.Equal(t, a.installation.Id, live.InstallationID, + "the seal a module identity of this solution mints is this row, which carries its principal") require.NoError(t, testStore.WithOrgTx(testCtx, a.org, func(ctx context.Context) error { got, _, err := testStore.GetInstallation(ctx, a.org, a.installation.Id) if err != nil { diff --git a/module/services/accounts/generated/openapi-raw/api.swagger.json b/module/services/accounts/generated/openapi-raw/api.swagger.json index 8e9e3a14c..c1c810efb 100644 --- a/module/services/accounts/generated/openapi-raw/api.swagger.json +++ b/module/services/accounts/generated/openapi-raw/api.swagger.json @@ -9000,7 +9000,7 @@ }, "registryPrincipalId": { "type": "string", - "description": "The module principal this installation was installed under: the registry\nprincipal whose delivered authority named the target's binding, written at\ninstall and never changed. A person's capability for a module audience is\nsealed to the active installation whose registry principal is that\naudience's, so a reader that must tell a person's seal from unattended work\ncompares this, not the route alias. Empty on a solution's installation and\non a row installed before the column existed; a reader treats empty as\nunknown, never as \"not this principal\"." + "description": "The module principal this installation was installed under: the registry\nprincipal whose delivered authority named the target's binding, written at\ninstall and never changed. A person's capability for a module audience is\nsealed to the active installation whose registry principal is that\naudience's, so a reader that must tell a person's seal from unattended work\ncompares this, not the route alias. Every install through InstallSolution,\na module's or a solution's, records one. Empty only on a row written before\nthe column existed (it is deliberately not backfilled: no principal is\ninferred) or one the store wrote with no registry principal. A capability\na module or solution identity mints is sealed only to a row carrying its\nprincipal. A reader treats empty as unknown, never as \"not this principal\"." } }, "description": "Installation is one solution's bound identity in an org. Server-assigned ids;\nthe browser cannot attest to tenancy, the agent principal, or the scope node." diff --git a/module/services/accounts/openapi/api.swagger.json b/module/services/accounts/openapi/api.swagger.json index 951a3faba..b9812b189 100644 --- a/module/services/accounts/openapi/api.swagger.json +++ b/module/services/accounts/openapi/api.swagger.json @@ -2494,7 +2494,7 @@ "type": "string" }, "registryPrincipalId": { - "description": "The module principal this installation was installed under: the registry\nprincipal whose delivered authority named the target's binding, written at\ninstall and never changed. A person's capability for a module audience is\nsealed to the active installation whose registry principal is that\naudience's, so a reader that must tell a person's seal from unattended work\ncompares this, not the route alias. Empty on a solution's installation and\non a row installed before the column existed; a reader treats empty as\nunknown, never as \"not this principal\".", + "description": "The module principal this installation was installed under: the registry\nprincipal whose delivered authority named the target's binding, written at\ninstall and never changed. A person's capability for a module audience is\nsealed to the active installation whose registry principal is that\naudience's, so a reader that must tell a person's seal from unattended work\ncompares this, not the route alias. Every install through InstallSolution,\na module's or a solution's, records one. Empty only on a row written before\nthe column existed (it is deliberately not backfilled: no principal is\ninferred) or one the store wrote with no registry principal. A capability\na module or solution identity mints is sealed only to a row carrying its\nprincipal. A reader treats empty as unknown, never as \"not this principal\".", "type": "string" }, "revokedAt": { diff --git a/module/services/accounts/proto/saas/accounts/v1/installation_listing.proto b/module/services/accounts/proto/saas/accounts/v1/installation_listing.proto index 7e50d1a27..6b3061c5f 100644 --- a/module/services/accounts/proto/saas/accounts/v1/installation_listing.proto +++ b/module/services/accounts/proto/saas/accounts/v1/installation_listing.proto @@ -97,9 +97,12 @@ message Installation { // install and never changed. A person's capability for a module audience is // sealed to the active installation whose registry principal is that // audience's, so a reader that must tell a person's seal from unattended work - // compares this, not the route alias. Empty on a solution's installation and - // on a row installed before the column existed; a reader treats empty as - // unknown, never as "not this principal". + // compares this, not the route alias. Every install through InstallSolution, + // a module's or a solution's, records one. Empty only on a row written before + // the column existed (it is deliberately not backfilled: no principal is + // inferred) or one the store wrote with no registry principal. A capability + // a module or solution identity mints is sealed only to a row carrying its + // principal. A reader treats empty as unknown, never as "not this principal". string registry_principal_id = 14 [ (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, (buf.validate.field).string.uuid = true diff --git a/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go b/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go index 843b79fc1..ddb6db120 100644 --- a/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go +++ b/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go @@ -191,9 +191,12 @@ type Installation struct { // install and never changed. A person's capability for a module audience is // sealed to the active installation whose registry principal is that // audience's, so a reader that must tell a person's seal from unattended work - // compares this, not the route alias. Empty on a solution's installation and - // on a row installed before the column existed; a reader treats empty as - // unknown, never as "not this principal". + // compares this, not the route alias. Every install through InstallSolution, + // a module's or a solution's, records one. Empty only on a row written before + // the column existed (it is deliberately not backfilled: no principal is + // inferred) or one the store wrote with no registry principal. A capability + // a module or solution identity mints is sealed only to a row carrying its + // principal. A reader treats empty as unknown, never as "not this principal". RegistryPrincipalId string `protobuf:"bytes,14,opt,name=registry_principal_id,json=registryPrincipalId,proto3" json:"registry_principal_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache diff --git a/module/services/auth-gateway/proto/saas/accounts/v1/installation_listing.proto b/module/services/auth-gateway/proto/saas/accounts/v1/installation_listing.proto index 7e50d1a27..6b3061c5f 100644 --- a/module/services/auth-gateway/proto/saas/accounts/v1/installation_listing.proto +++ b/module/services/auth-gateway/proto/saas/accounts/v1/installation_listing.proto @@ -97,9 +97,12 @@ message Installation { // install and never changed. A person's capability for a module audience is // sealed to the active installation whose registry principal is that // audience's, so a reader that must tell a person's seal from unattended work - // compares this, not the route alias. Empty on a solution's installation and - // on a row installed before the column existed; a reader treats empty as - // unknown, never as "not this principal". + // compares this, not the route alias. Every install through InstallSolution, + // a module's or a solution's, records one. Empty only on a row written before + // the column existed (it is deliberately not backfilled: no principal is + // inferred) or one the store wrote with no registry principal. A capability + // a module or solution identity mints is sealed only to a row carrying its + // principal. A reader treats empty as unknown, never as "not this principal". string registry_principal_id = 14 [ (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, (buf.validate.field).string.uuid = true diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json index ad36113c3..ea8874bfa 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json +++ b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contract.binpb", - "digest": "sha256:35eb88dcfeb5febada3d7f051607af72afe9b2f48cfcab6899366f3aaf490204", + "digest": "sha256:8e59a8dabb889185a44cb9e0d8c9cdc4fbef03b27e7c01f2cc581c69d2e8799a", "services": [ { "name": "APIKeyService", diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb b/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb index 828546096daf56301a66ca03fdb954fdf36f4432..97ec3eafb88bdfc60cf59cd9d2e7d9bebc4f5f7a 100644 GIT binary patch delta 1229 zcmYjP&u`pB7~L7qI^OXnyYXg|Y9rDKPzoX~DMBEj@as^a^wNsvQV<7cchXGjwYT;x z-Dn{x)KXH^M9HN-fjA@JMA=hs97-u5a^uH=h#Lna#6Q5ZcU^H9+i$-2-uLDk-~4=e z?~COdF^Oqx#4IkxW;_;KaVh3s+y8!Pd~=Q`za82O$(6?|i^<{xbAAwgW$&4#&zAU@ zQ!Eq?lA~YDQ-hc8+OJt=n>0QpA~{;AP7U_&*?;mGMJ@iV$r&rqvcnYhSmUlKR+4{K z#%qI5?%OxX)_XGC5Xz6V>v=-2he7x9x>z3Ki-FhGD)1M$a78<4bv=195D`9H!R6D0R~f!wuzM7D}V_ zqadkkDBYF}g~l5OGCQ&xC3_F-7cQvM953@?I&#A?)cg~Ce26UEz>}WAE8I$p)^=&u zF|M!|d6kz&8xzxxag{yG?@>+}af(slLo%5B-d-YGktH%)LJLL5(70F|_X?DPSMw49 zDphiX{@SVK$O4*rCF=$m+Na%geQ8uDrepNz6#t84zHsVM(fFN+w`8Ql#*26p{LLjNch*sl)noGx_xg`_5M1n#@U%^r!0CI0&T}M}K;gA&-S)P~2*_zak5(k^BhXS1H!!d7@!T}+;VS}Z?Zxd@iqZ+ip z9JWg+&ujrRbS5vN=Pt45atP@zIlhoXblD}(Esh|;?Do^yt>bJ41sCYC#@orYi{;mo lKYq4PRQE~gSk8ucf4hEYW0kSSu_mx8Sk*!OaPseu{sTdo{y6{u delta 984 zcmYjOO-vI}5Z-z3wd}sPwB^x4(*}4%Ziq@0P5eK2kQ0gFWV}>qrCR#mP{dlI3xp>q0$dnwN7aIHE2M*QKC z6w&!x#u?LyDOrl7S!|2h->!$E%?I0J2?BHZ@`B1`RYH|=nTq`>ZuU;7M0SR^lf~S; zN@q*uL?)A*QJ0gcTrsK2vq^OY*>hQytfoul8H3#9K4&YH zG&Q`Bcejy_B1Y1b2f6MP^;IJ-=~Z@=hq%#61o|YMWuyEx;e>%?dF>X&wurK)o{4C4 z;++_RdR|xVd?1=n(gGeS@J2)qIPH}HkrDI|3Mt5559x@)r`)yJhB}IcP>t<5Ec6h>O!(qF50d4VsESxiF<6ET^SteAcB&rq>FF1dx>OKvS{yp5S#U? z-s6$5r5cX7z5pTUOSNy2p&lL2AgG68rhk!k6uKFlyTJJW860whnVpdo>ov6Ek}DZY zH4=?Ysi}OTP{D^)iY5EYVe_25^D)FnYKCV8b(nqqeB1GxNWTTzDnRTNI{xdq=xx?_ z#Q~^WpvPUYglK-rmVpFtEn^5>q`V@2Y#D<8xOm1gpf%3_2|-)g3ZSp;R^%e`TET^A zYKM!hD^9D4y6RN=*Ff6~k-7%@uuG*xW0Ez1aCa<17ma&G+}Qvz=s|34z|ejV z;+6&+Jk){UlWZN(#96$J667Kph+VXAPMc@#`@5ld%u$CGw`4@UJ{+^Ys#*fW#OTHd OU<5HjP1Wl8dHXLIfu;!n diff --git a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml index 722553097..a5d3eb3f6 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml +++ b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml @@ -12,7 +12,7 @@ sources: module: saas-starter service: accounts endpoint: connect - contract-digest: sha256:35eb88dcfeb5febada3d7f051607af72afe9b2f48cfcab6899366f3aaf490204 + contract-digest: sha256:8e59a8dabb889185a44cb9e0d8c9cdc4fbef03b27e7c01f2cc581c69d2e8799a services: - APIKeyService - AccessibleScopeService diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installation_listing_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installation_listing_pb.ts index b0a743686..5ab18f66b 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installation_listing_pb.ts +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installation_listing_pb.ts @@ -111,9 +111,12 @@ export type Installation = Message<"saas.accounts.v1.Installation"> & { * install and never changed. A person's capability for a module audience is * sealed to the active installation whose registry principal is that * audience's, so a reader that must tell a person's seal from unattended work - * compares this, not the route alias. Empty on a solution's installation and - * on a row installed before the column existed; a reader treats empty as - * unknown, never as "not this principal". + * compares this, not the route alias. Every install through InstallSolution, + * a module's or a solution's, records one. Empty only on a row written before + * the column existed (it is deliberately not backfilled: no principal is + * inferred) or one the store wrote with no registry principal. A capability + * a module or solution identity mints is sealed only to a row carrying its + * principal. A reader treats empty as unknown, never as "not this principal". * * @generated from field: string registry_principal_id = 14; */ diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/installation_listing_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/installation_listing_pb.ts index dca1d31f7..b5a522fad 100644 --- a/module/services/frontend/code/src/gen/saas/accounts/v1/installation_listing_pb.ts +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/installation_listing_pb.ts @@ -111,9 +111,12 @@ export type Installation = Message<"saas.accounts.v1.Installation"> & { * install and never changed. A person's capability for a module audience is * sealed to the active installation whose registry principal is that * audience's, so a reader that must tell a person's seal from unattended work - * compares this, not the route alias. Empty on a solution's installation and - * on a row installed before the column existed; a reader treats empty as - * unknown, never as "not this principal". + * compares this, not the route alias. Every install through InstallSolution, + * a module's or a solution's, records one. Empty only on a row written before + * the column existed (it is deliberately not backfilled: no principal is + * inferred) or one the store wrote with no registry principal. A capability + * a module or solution identity mints is sealed only to a row carrying its + * principal. A reader treats empty as unknown, never as "not this principal". * * @generated from field: string registry_principal_id = 14; */ From 6cef5dec1a62b36faa182ff2cffd8c55a49b06d8 Mon Sep 17 00:00:00 2001 From: saurabhmaheshwari-obin <242053479+saurabhmaheshwari-obin@users.noreply.github.com> Date: Sat, 24 Oct 2026 12:19:34 -0400 Subject: [PATCH 08/17] chore(saas-sdk): 0.3.36, for the moved installation listing types The generated accounts types moved to installation_listing_pb, so the published package changes. saas-ui's peer floor stays 0.3.35: it uses none of them. Co-Authored-By: Claude Opus 5.5 --- module/services/frontend/code/package-lock.json | 4 ++-- module/services/frontend/code/package.json | 2 +- module/services/frontend/code/packages/saas-sdk/package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/module/services/frontend/code/package-lock.json b/module/services/frontend/code/package-lock.json index e6813680e..2786581f2 100644 --- a/module/services/frontend/code/package-lock.json +++ b/module/services/frontend/code/package-lock.json @@ -15,7 +15,7 @@ "@bufbuild/protobuf": "^2.11.0", "@codefly-dev/saas-plugin-contract": "2.4.4", "@codefly-dev/saas-plugin-react": "0.4.5", - "@codefly-dev/saas-sdk": "0.3.35", + "@codefly-dev/saas-sdk": "0.3.36", "@codefly-dev/saas-ui": "0.13.0", "@codefly-dev/ui": "0.13.0", "@codefly/saas-plugin-manifest": "0.1.2", @@ -15742,7 +15742,7 @@ }, "packages/saas-sdk": { "name": "@codefly-dev/saas-sdk", - "version": "0.3.35", + "version": "0.3.36", "license": "Apache-2.0", "dependencies": { "@bufbuild/protobuf": "^2.11.0", diff --git a/module/services/frontend/code/package.json b/module/services/frontend/code/package.json index 719e620b9..4fe01a883 100644 --- a/module/services/frontend/code/package.json +++ b/module/services/frontend/code/package.json @@ -39,7 +39,7 @@ "@bufbuild/protobuf": "^2.11.0", "@codefly-dev/saas-plugin-contract": "2.4.4", "@codefly-dev/saas-plugin-react": "0.4.5", - "@codefly-dev/saas-sdk": "0.3.35", + "@codefly-dev/saas-sdk": "0.3.36", "@codefly-dev/saas-ui": "0.13.0", "@codefly-dev/ui": "0.13.0", "@codefly/saas-plugin-manifest": "0.1.2", diff --git a/module/services/frontend/code/packages/saas-sdk/package.json b/module/services/frontend/code/packages/saas-sdk/package.json index bddb526ba..485344fa3 100644 --- a/module/services/frontend/code/packages/saas-sdk/package.json +++ b/module/services/frontend/code/packages/saas-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@codefly-dev/saas-sdk", - "version": "0.3.35", + "version": "0.3.36", "description": "Typed Connect client, gateway-bound facades, and audit data-graph tooling for the saas-starter public API.", "repository": { "type": "git", From c81b616c60bc699e323f43961e0c0be67bf10873 Mon Sep 17 00:00:00 2001 From: saurabhmaheshwari-obin <242053479+saurabhmaheshwari-obin@users.noreply.github.com> Date: Sat, 24 Oct 2026 16:28:21 -0400 Subject: [PATCH 09/17] feat(accounts): AdmitWorkloadOperation, the issuer's allow-list for the bearer-less module arm The gateway's bearer-less arm of /v1//* classified a capability by its seal: one sealed to an active installation other than the target's read as unattended work. Several person-owned capabilities carry exactly that seal by design: a source-operation context, an installation task or install step, and the installed binding children of ExchangeDelegatedOperationAudience (content_read and module_actor), which keep the exchanging module's seal while addressing another module. The arm would admit each of them without the person's bearer. Core's Inspected withholds the owner, its kind and the chain, so the gateway cannot tell; the issuer can. WorkContextService/AdmitWorkloadOperation{work_context, alias} is internal-tier (EXPOSURE_INTERNAL, the internal credential, the tier ListInstallations sits in). Accounts runs core's Inspector (signature under this issuer, window, audience equal to the alias), decodes its own claims, and admits only what StartModuleOperationTask signs: a service principal's capability with no actor chain, grant hop, operation binding, parent session, owner agent, workspace, project or attribution teams, with authority scopes, an idempotent replay policy and at most the operation-context window, whose owner is a declared module principal other than ModulePrincipalID(alias) and whose seal is that principal's ACTIVE installation in the tenant, read live through WorkContextSeal. A refusal is an answer carrying a WorkloadOperationRefusal; an error means it could not decide. It never reaches verifyFor, verifyIssued or the replay store, so it consumes nothing. The messages live in an accounts-owned, import-minimal workload_operation_admission.proto, which work_contexts.proto imports, so the gateway can vendor it byte for byte as it does installation_listing.proto. Tests (pkg/adapters/workload_operation_admission_test.go): - TestEveryMintHasAWorkloadOperationVerdict mints through every token-answering RPC (StartTask, StartRootSession, ExchangeAudience, RenewWorkContext, StartChildSession, StartInstallationTask, MintModuleWorkContext, MintModuleOperationContext, MintSourceOperationContext, and both delegated exchanges in every binding mode: plain, content_read, module_actor, source delegation reference and parent), the install step, the installer token route, and crafted variants of the operation-context shape (single-use, longer window, no scope, an actor hop, the alias's own module, another installation's seal, an undeclared service principal, agent, human and unnamed owner kinds). Exactly one row is admitted. - TestWorkloadOperationTableNamesEveryMintAndOwnerKind fails when a saas.accounts.v1 RPC answers a token, an adapters function calls Start, Child, mint or remint, or business declares a PrincipalKind constant, that the table has no row for. - TestAdmitWorkloadOperationConsumesNothing: a single-use capability shown three times still verifies once at its callee and is then replayed; an admitted operation context still verifies at its callee. - TestAdmitWorkloadOperationIsInternalAndFailsClosed. The seal rule's documentation (accounts AGENTS.md, the seal pin test, the child guard, the ListInstallations contract and handler comments) no longer says the arm depends on it, and names the installed binding child among the kinds sealed elsewhere. The child guard and the seal pin test are unchanged in behaviour and pass: exchangeInstalledBinding derives through core's Child directly, not StartChildSession, so the guard never sees it. Regenerated with Codefly source candidate 5234ce24: codefly generate proto --path saas for accounts (a no-change run was clean first), go generate for business, adapters and cataloggen, codefly generate contracts saas-starter, and the saas-sdk library through scripts/generate.mjs --force. The internal method count moves 54 to 55. buf lint and buf breaking against the base pass. Co-Authored-By: Claude Opus 5.5 --- .../api/accounts/authority/contract.binpb | Bin 1201678 -> 1206731 bytes .../saas/accounts/v1/installations.proto | 6 +- .../saas/accounts/v1/work_contexts.proto | 25 + .../v1/workload_operation_admission.proto | 71 +++ .../api/accounts/connect/contract.binpb | Bin 1201678 -> 1206731 bytes .../saas/accounts/v1/installations.proto | 6 +- .../saas/accounts/v1/work_contexts.proto | 25 + .../v1/workload_operation_admission.proto | 71 +++ module/contracts/api/catalog.codefly.json | 5 +- module/module.package.codefly.yaml | 4 +- module/services/accounts/AGENTS.md | 71 ++- module/services/accounts/AUTHZ_MATRIX.md | 5 +- .../adapters/connect_handlers_work_context.go | 7 + .../code/pkg/adapters/installation_rpcs.go | 5 +- .../code/pkg/adapters/work_context_rpcs.go | 8 +- .../adapters/work_context_seal_pin_test.go | 19 +- .../adapters/workload_operation_admission.go | 143 +++++ .../workload_operation_admission_test.go | 539 ++++++++++++++++++ .../code/pkg/business/introspection.go | 1 + .../pkg/business/rpc_policy_inventory_test.go | 1 + .../code/pkg/cataloggen/authz_methods_test.go | 9 +- .../testdata/mesh-policy.golden.yaml | 1 + .../installations.connect.go | 12 +- .../work_contexts.connect.go | 49 ++ .../saas/accounts/v1/installations_grpc.pb.go | 12 +- .../gen/saas/accounts/v1/work_contexts.pb.go | 48 +- .../saas/accounts/v1/work_contexts_grpc.pb.go | 58 ++ .../v1/workload_operation_admission.pb.go | 309 ++++++++++ .../generated/api-contract-surfaces.json | 1 + .../accounts/generated/authz-methods.json | 37 ++ .../accounts/generated/service-catalog.json | 44 ++ .../saas/accounts/v1/installations.proto | 6 +- .../saas/accounts/v1/work_contexts.proto | 25 + .../v1/workload_operation_admission.proto | 71 +++ .../auth-gateway/code/authz_catalog_gen.go | 1 + .../generated/contract/catalog.codefly.json | 5 +- .../generated/contract/contract.binpb | Bin 1201678 -> 1206731 bytes .../saas-sdk/generated/library.codefly.yaml | 6 +- .../gen/saas/accounts/v1/installations_pb.ts | 6 +- .../gen/saas/accounts/v1/work_contexts_pb.ts | 23 +- .../v1/workload_operation_admission_pb.ts | 146 +++++ .../gen/saas/accounts/v1/installations_pb.ts | 6 +- .../gen/saas/accounts/v1/work_contexts_pb.ts | 23 +- .../v1/workload_operation_admission_pb.ts | 147 +++++ 44 files changed, 1958 insertions(+), 99 deletions(-) create mode 100644 module/contracts/api/accounts/authority/proto/saas/accounts/v1/workload_operation_admission.proto create mode 100644 module/contracts/api/accounts/connect/proto/saas/accounts/v1/workload_operation_admission.proto create mode 100644 module/services/accounts/code/pkg/adapters/workload_operation_admission.go create mode 100644 module/services/accounts/code/pkg/adapters/workload_operation_admission_test.go create mode 100644 module/services/accounts/code/pkg/gen/saas/accounts/v1/workload_operation_admission.pb.go create mode 100644 module/services/accounts/proto/saas/accounts/v1/workload_operation_admission.proto create mode 100644 module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/workload_operation_admission_pb.ts create mode 100644 module/services/frontend/code/src/gen/saas/accounts/v1/workload_operation_admission_pb.ts diff --git a/module/contracts/api/accounts/authority/contract.binpb b/module/contracts/api/accounts/authority/contract.binpb index 97ec3eafb88bdfc60cf59cd9d2e7d9bebc4f5f7a..7f8edac8b9482f9e0df01754708965358210c69b 100644 GIT binary patch delta 15869 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 b{xw@>Wbm&S{`JPcKKOS;_m&ynzgPb^i@Xm{ delta 11319 zcmZ9SdAOEkoyX6;ywAIx7w|!jhXcYntZpgbjA@Rj2_fzj<~Uh%G37Nj(v>DFVk)vs{oAQ_9i?m%ub9R9tdLZ9czyd7e{$a9wf+m`&h1v+y4!Ty?oxNsUGA=QSG#K)>IXe#ez;LL=bgIOz-Hz4dTZ|~Pu2Sk z*Sp^9{o330!}cHeX;QOEi%!zyQLTTcO~>m=@6AJ6f2^Btk7*s&Ja1U*H^+3&IQJW! zv(GuZ*7^0B=gvO+!r7hQoYgt&>>1xW8}Up=XEJzxbLZyz-n|D6Z|%Ccnz+Pmz-)@7>qzCEqGtbXFU=74qOsm=2qYHbX-N!4_zCAF;}`Yps<^ExJXerlI$%)oWIefX~Ds4Lb0kfD{w^>9Jv= z1NjgW`yFs#n5Y@+pHF0{zm5~NASTwg+Zw04oYUg7PHmSS6MfeB+?D$*h3HC$#dT=u z>KYMz*7)2ta!mAjnilKW9%07^7oCJPBZG_B$~_#Fdl+QQ8;yr+3}q#ahqQdP_s_Pm zMrDUoM%HRIV^uXZ<8(vV!~db#xO%H|41k+`5Kahki)yJEuiO2i8jHr)_wkDyOD4b) zr%z}*K~IcB=$laO@6cqWTkvf{Ixr4aKzkqMVq}bn`n4e$cYIOtfPOMzbL(J-%nts+{G3`xx~_OP&C>NE)} zbCt6~$dj69)f)4&N!1~73MM8E8y=9M=#xXyl{TrJtdI9a2PKTv<2pn1lZU#7!?Hok zJwGpA;=qYBRO!TZA!e zP2GD?uw}6|we6&6%SuvEd_rhPri+9nCx&)Zx|C$8Iav=5MaA&Re&@iA5~fb>7#o}_ zvF>EQ;V_mk^AufXOiGrd_7puWx|p!;luE3A!n#x1_6aT~xOhtYwCG||={c-w&~>UE z6ZnCsDk(h8?iVbVDa3RNvC>zroZ9L7jONnT#>#AZXgsCM7@Zy(4}^m0q47W%oj&r6 zXtW&ia(bQ@Ct=B#^So5Bc_ucey7*XsrasGd9NBs5d9%Mc^V}KV>OAlKnctYxyrt3j z?AOlBov7f%nW@|6$w~!{XAbWQkyk{?h_j*-mEgp`RF+wL8+M) zni_z3qrSpDDnm4zvGK6(0BWPg>DZL zyuL6ryLQcfQD}BhYA@0kN6XUe7rAE7BnavDi$b@9uys+}#nD#m_Wurqk_o=szZ+Ps z>GsroFEl$yfZw$MnEGC5b`X}n7n)s5vu7rw+Z%KPFfC8y(_^p>YD)(&Fg1y zBHJaRTnb<~k&_I#ECi1L{YzBUUu=NrU!q2janS;ac8MChEJP~;h{&z!${1}Bv23ZL zHM~*yXqOieF(35#Fqp};V7aQs7^Fp)t8lCsX>VZru_2_;--reu6BG#RRumzTTu?KQ zNV2;^(Q4iox*ABUN&O6coXPyQW*(AswuE$55t0+J?&?S)qb&#HN=3u@B0(w8LBWPx z+EyxB&YPs(Kzh!@68>Jp(G^ONmC^*bDtDNEWt_GP(^-9ud5o6S3@CX}k>@#@RHvNwsWJs$;G0oP6Qe&zW)Vzn^`^%=7>AxS72lw>O5X?50RS z>j=0emJ66#kS8Dr@76>D0x`2D5)eqW)9utCruw(;m~=Jo1g!k-d)(ZG1~*_p1sIT`oK&o^?N0T$7~nN?emb+=V1u z5I*&V)W|`%u$J0iD6UFwAy*|3cO|#bQT~BQ`I1l`Tcd@xgEH-}V;#B=_kNWUOr-NTXd zCCB+kijYVJ=!6ij?75F9o<}}>A^anX_fddE^oZ)5>Uf2wN>3)2Dr8;9hbcntEs@+M zAvfr;JR~9amPl@fF}Fnx^(Pz~cS+b{ zw-#w-j{`lC`ylk*8k?Hg7~LA18i>)Ys_lu~Xt-tB$BV3nYnJ^L+mr9hl3cXxld+?K z4*8A(Ve-ki@dGjWWNfICT(<1C*igVU0X>!b%WaF{u;%%{^jt1lKHr4)A?r^+u z{W^A(%<&!d8`=1`Nkd`eIo0xZ@3*@ePpa%WRq-`W9OIvl^Z=$7s$JRb=!&=daoNchAN{)Jc>V48q-#!Vgp`d^5Q2crLl$ao-;zM#hL43Ub!6aEje zG(p6&9g*;fyJ5d1l)OS-li)M~zM^c$#o=++oH(j6FMBE8?U;feFR9dj^a7&rCAIG` z$4el7yrhP|lKT<&{g(?r;=cc51F@dI&UmzwZe?nG>aS7Re2ZkK;OauS$Ykmn=_^{+=x0#X0E%EB(6xLtmC zBqcCSKyL+7f?2&gk`l=3-I0_)oZGG1-^!hfyZjrGlQPG1@=eD{`BKTP-@dO}7Bqi3 zrtxISkVkv_l|Ivm)mhMqJTV+=T zy3z2RtL%2KkNHUC@A=@=0^7r6soKZ!Qp_N0KbE|ERV5~WA{3LetSp-l$iJ~b4u&@r z5KW(`bYxtLrccxue>W}bgzKmMw1|M+4HQ+$mh`Er#3 zwElbRK*aP4jekK(Z7~^K@7AqK2lE<`=5CA%Ohbr>twJnqR1@p=!Pk??6UT zHDB`%Tq~5O4|;UV<;|}h&{&i8=!(}-ag1KbM&b$uOf6`ViwlItg}UnL0z~6N-R_$K zNKO{&j^o@REYR<=3-u8Ha;nG&ce_Zum$w`+O+c6FfAv>s1SENp4mV67<`(Ip(KYFU zMLIm~7$t;7dhBJ|Kj=l^ZF{6$667)+9;^a=kjr#Mvi1y_= z++%=fU#`PFMxfWeT!(8+)wTYLLOZPwni}Gg)?c9mg_(=;6*^Ej1EYO~9(BAckied5 zLPb{S(Nlr~xg=c`3sjX$5@@CN^#N0#t8{&IoQ=7w^q7Mq#N1VS`~-IxF`4>Yr4L;h z>T|S6+$Sh|wGJsS)XZmyr=MLdKD?)4NtJCT0p8d<v60#Ub4j~rFoB?PRc$z8hT=UR0B%=;j{nsKO+BfRqJ{%z0H|nkv9AyN0?Hlz7&$*hr#B7p;$<7W;E$B-jE=k{} zSTP2%wMnNw>I{srO?s@~ynz_or1$?~Fy?M5T;ezB1EvLi!q48w&zkTPbWh+X7<0Xa zIXTXHHNV>VB+GHutNGbZ2vUSy}F}m)R^!Be!5hsMD_v)_ua~~p0?<;(Wm-?TH54(j{^)z`{xBMDf z{Q+7X^Fix~<{gKz#XYRUH@UiF)aF=pU}{00Q6QY!92o_~sm+m5K+3Q=GD@JY`R2%| zx?|L%qI{=Z7{O@*e7yP2XB#`tepH7qW5`kbs19F(fCT%fZht%kD+1q=kHuhvh-F)2 z!t3&p*KU)e_cd<~ifwBiaYSSHvD@O2%Ouupi$^Yy7`EvHr}2@!f`dY(!6G^6wAwRd zFlkAXr*+Gw<~g5hJU#GfUFoR3h;n|oXJEV5e`-!UvavJUt`pY?BFE}>-Q`X)ftcH_ zPyRRIl0aDSthO)fgkNqePH^{P*!irkzpP8b=O`od&*`MAuL#Z0>tu9a5fQ(@O=mdT zYH?+md4JIT+;`$ABg!2*`G>wsxn1nk$=}T4mhhrZ4(&Uup4vbD-NII$5;9AE+Co1y zt?knKJ^qBDC+=OR9@Y46wo51O6)tmdX_p>)xHOnRlCn!5_s=0Ioc779+P<47MOl}W zS9Sf}JSk2_QeG>P;zVeEy-11^k(AwilHz2NlsAf`I2lQKvxv=!NXlD9Qk;mSyj>*4 ziAc&{{%#>jc}HeFXI)Zwq1kyxU(x%kiH)!5Y@SKlLhQ2f%`^KP5h1CVXC|E)QXz-f zd}A*+kssQpV!o+gZX!RNj8rTzIX|2T&D|#FhZB*C@0)@jP9~|i#N_;NGE&iFa(*}w zsaR-oemD`SSY&d3I1#D1^zRmuipyjccQ zcNzPOyc*V5<1SPGMP3ajL+gfuolb=2jRiZMDBY7%gH3)_Y~}OGV*lbRzQovm%pDM85AY^6fr`*yWD!R3VHxCuvDwnp0%%syp zKDYxW4;%YTo{y5x$HS)nOr8%XL+j>(olb=2M@%O4bnJ98VdoZ;Y|Yu}q_FdG$mA@x)GIIBnNj?*Z=|tr2w?*!p zh}`|I$gmSZ)u#)pNVIeXvMCEHmZJdnS>@I5KMAYVu zSR4L0P+X=oZ@SWCuKAse+PqcN#)+uS+eK}hh}!(6sEs7#U1LAUYm@k zXdHelPd+wwiH*lx*%#+yQ(t1^G3R7x{Uj786SXoA)t?sCaUyA)znWx$joZ4DQk-s^ zH;xmz;#^`YM}*kC5RE-H-{YN(;w-ZHi72AXP@PL{zRA03ROd3AH;xlgoXc&#$vY9n zxx(fLrxQ_}#WvsLB_UT@yPSmhJ-!@@bET~>CnM4$6C`}81l3t8HhtrG8I7~dQk-jz zJ9W80ud;e$?}?{1u2z|Rby{WfD@cWKSJ{2sbDW&YN2pcyh!a9>_#=YkI&1I9Yg6&H zxz5(_$ZO+dXkBgdBiD)0e7(*0G$$&xw5Q1pHd&L`#)+xT+U9AeHulbLw)_C*V{s!w zom+}(I2qOWaZwE?q8L9Zis3{Q/* is a module operation + // context (StartModuleOperationTask) for that alias, and admits nothing else. + // The auth-gateway's bearer-less arm is its caller. Unlike the revision + // checks above, the signed capability itself crosses this boundary: the + // answer depends on its owner, owner kind and chain, which only the issuer + // reads. It inspects (signature under this issuer, window, audience) and + // reads its own claims; it never verifies, so nothing is consumed and the + // callee still verifies the capability at full strength. Internal-tier: the + // tenant it answers about is the one the capability names. + rpc AdmitWorkloadOperation(AdmitWorkloadOperationRequest) returns (AdmitWorkloadOperationResponse) { + option (saas.policy.v1.method_policy) = { + exposure: EXPOSURE_INTERNAL + tenant: TENANT_REQUIREMENT_NONE + mfa: MFA_REQUIREMENT_NONE + audit: {emission: AUDIT_EMISSION_NONE} + idempotency: IDEMPOTENCY_REQUIREMENT_FORBIDDEN + rate_limit: RATE_LIMIT_CLASS_INTERNAL + request_sensitivity: SENSITIVITY_CONFIDENTIAL + response_sensitivity: SENSITIVITY_CONFIDENTIAL + platform_role: PLATFORM_ROLE_REQUIREMENT_NONE + }; + } + rpc StartTask(StartTaskWorkContextRequest) returns (IssuedWorkContext) { option (saas.policy.v1.method_policy) = { exposure: EXPOSURE_AUTHENTICATED diff --git a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/workload_operation_admission.proto b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/workload_operation_admission.proto new file mode 100644 index 000000000..dda95d168 --- /dev/null +++ b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/workload_operation_admission.proto @@ -0,0 +1,71 @@ +syntax = "proto3"; + +package saas.accounts.v1; + +import "buf/validate/validate.proto"; + +// Workload operation admission: the issuer's answer to whether a capability +// presented with no bearer at a declared module route is a module operation +// context, the one shape that route admits without a person. +// +// A forwarding hop cannot answer it. Core's Inspector, which is all a hop runs, +// withholds the owner, the owner's kind, the actor chain and the operation +// binding, and those are exactly what tell a module's unattended work from a +// person's capability. Accounts signed the capability and can read its own +// claims, so the gateway asks it. WorkContextService/AdmitWorkloadOperation +// takes and answers these messages. +// +// They live in their own file, with deliberately minimal imports, so the +// auth-gateway can generate the same types from the same source proto, as it +// does for installation_listing.proto and solution_entitlements.proto. The +// gateway's copy is this file, byte for byte. + +// WorkloadOperationRefusal is why a capability was not admitted. The gateway +// logs it and answers every refusal alike. +enum WorkloadOperationRefusal { + WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED = 0; + // Not a capability this issuer signed, inside its window, addressed to the + // alias; or the alias is one no operation context is minted for. + WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED = 1; + // Owned by a principal that is not a service: a person, an agent, or a kind + // this issuer does not name. + WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE = 2; + // A service principal's capability that is not shaped as a module operation + // context: it carries an actor chain, a grant hop, an operation binding, a + // parent session or an owner agent, names no authority scope, or has another + // replay policy or a longer window. + WORKLOAD_OPERATION_REFUSAL_NOT_OPERATION_CONTEXT = 3; + // Owned by the module principal the alias names: a module does not reach + // itself through this route. + WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET = 4; + // The owner is not a declared module principal, or the seal is not an ACTIVE + // installation in the tenant installed under that principal. + WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS = 5; +} + +message AdmitWorkloadOperationRequest { + // The capability as presented, encoded. It is inspected and read, never + // verified: nothing it carries is consumed. + string work_context = 1 [ + (buf.validate.field).string.min_len = 1, + (buf.validate.field).string.max_len = 32768 + ]; + // The route alias the gateway resolved from the request path. The capability + // must be addressed to it. + string alias = 2 [ + (buf.validate.field).string.min_len = 1, + (buf.validate.field).string.max_len = 512 + ]; +} + +message AdmitWorkloadOperationResponse { + bool admitted = 1; + // Set when admitted is false. + WorkloadOperationRefusal refusal = 2; + // The signed facts the gateway logs and forwards, set only when admitted: + // the tenant, the installation the capability is sealed to (the CALLING + // module's), and the task. + string tenant_id = 3; + string installation_id = 4; + string task_id = 5; +} diff --git a/module/contracts/api/accounts/connect/contract.binpb b/module/contracts/api/accounts/connect/contract.binpb index 97ec3eafb88bdfc60cf59cd9d2e7d9bebc4f5f7a..7f8edac8b9482f9e0df01754708965358210c69b 100644 GIT binary patch delta 15869 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 b{xw@>Wbm&S{`JPcKKOS;_m&ynzgPb^i@Xm{ delta 11319 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/* is a module operation + // context (StartModuleOperationTask) for that alias, and admits nothing else. + // The auth-gateway's bearer-less arm is its caller. Unlike the revision + // checks above, the signed capability itself crosses this boundary: the + // answer depends on its owner, owner kind and chain, which only the issuer + // reads. It inspects (signature under this issuer, window, audience) and + // reads its own claims; it never verifies, so nothing is consumed and the + // callee still verifies the capability at full strength. Internal-tier: the + // tenant it answers about is the one the capability names. + rpc AdmitWorkloadOperation(AdmitWorkloadOperationRequest) returns (AdmitWorkloadOperationResponse) { + option (saas.policy.v1.method_policy) = { + exposure: EXPOSURE_INTERNAL + tenant: TENANT_REQUIREMENT_NONE + mfa: MFA_REQUIREMENT_NONE + audit: {emission: AUDIT_EMISSION_NONE} + idempotency: IDEMPOTENCY_REQUIREMENT_FORBIDDEN + rate_limit: RATE_LIMIT_CLASS_INTERNAL + request_sensitivity: SENSITIVITY_CONFIDENTIAL + response_sensitivity: SENSITIVITY_CONFIDENTIAL + platform_role: PLATFORM_ROLE_REQUIREMENT_NONE + }; + } + rpc StartTask(StartTaskWorkContextRequest) returns (IssuedWorkContext) { option (saas.policy.v1.method_policy) = { exposure: EXPOSURE_AUTHENTICATED diff --git a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/workload_operation_admission.proto b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/workload_operation_admission.proto new file mode 100644 index 000000000..dda95d168 --- /dev/null +++ b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/workload_operation_admission.proto @@ -0,0 +1,71 @@ +syntax = "proto3"; + +package saas.accounts.v1; + +import "buf/validate/validate.proto"; + +// Workload operation admission: the issuer's answer to whether a capability +// presented with no bearer at a declared module route is a module operation +// context, the one shape that route admits without a person. +// +// A forwarding hop cannot answer it. Core's Inspector, which is all a hop runs, +// withholds the owner, the owner's kind, the actor chain and the operation +// binding, and those are exactly what tell a module's unattended work from a +// person's capability. Accounts signed the capability and can read its own +// claims, so the gateway asks it. WorkContextService/AdmitWorkloadOperation +// takes and answers these messages. +// +// They live in their own file, with deliberately minimal imports, so the +// auth-gateway can generate the same types from the same source proto, as it +// does for installation_listing.proto and solution_entitlements.proto. The +// gateway's copy is this file, byte for byte. + +// WorkloadOperationRefusal is why a capability was not admitted. The gateway +// logs it and answers every refusal alike. +enum WorkloadOperationRefusal { + WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED = 0; + // Not a capability this issuer signed, inside its window, addressed to the + // alias; or the alias is one no operation context is minted for. + WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED = 1; + // Owned by a principal that is not a service: a person, an agent, or a kind + // this issuer does not name. + WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE = 2; + // A service principal's capability that is not shaped as a module operation + // context: it carries an actor chain, a grant hop, an operation binding, a + // parent session or an owner agent, names no authority scope, or has another + // replay policy or a longer window. + WORKLOAD_OPERATION_REFUSAL_NOT_OPERATION_CONTEXT = 3; + // Owned by the module principal the alias names: a module does not reach + // itself through this route. + WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET = 4; + // The owner is not a declared module principal, or the seal is not an ACTIVE + // installation in the tenant installed under that principal. + WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS = 5; +} + +message AdmitWorkloadOperationRequest { + // The capability as presented, encoded. It is inspected and read, never + // verified: nothing it carries is consumed. + string work_context = 1 [ + (buf.validate.field).string.min_len = 1, + (buf.validate.field).string.max_len = 32768 + ]; + // The route alias the gateway resolved from the request path. The capability + // must be addressed to it. + string alias = 2 [ + (buf.validate.field).string.min_len = 1, + (buf.validate.field).string.max_len = 512 + ]; +} + +message AdmitWorkloadOperationResponse { + bool admitted = 1; + // Set when admitted is false. + WorkloadOperationRefusal refusal = 2; + // The signed facts the gateway logs and forwards, set only when admitted: + // the tenant, the installation the capability is sealed to (the CALLING + // module's), and the task. + string tenant_id = 3; + string installation_id = 4; + string task_id = 5; +} diff --git a/module/contracts/api/catalog.codefly.json b/module/contracts/api/catalog.codefly.json index 4a3ead6fc..a5fcf2a98 100644 --- a/module/contracts/api/catalog.codefly.json +++ b/module/contracts/api/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/authority/contract.binpb", - "digest": "sha256:8e59a8dabb889185a44cb9e0d8c9cdc4fbef03b27e7c01f2cc581c69d2e8799a", + "digest": "sha256:6100316ada259d0f5b29458a686ca0dd430948b6770b3c0d69d2153716ce57d7", "services": [ { "name": "ModuleCapabilitiesService", @@ -65,7 +65,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/connect/contract.binpb", - "digest": "sha256:8e59a8dabb889185a44cb9e0d8c9cdc4fbef03b27e7c01f2cc581c69d2e8799a", + "digest": "sha256:6100316ada259d0f5b29458a686ca0dd430948b6770b3c0d69d2153716ce57d7", "services": [ { "name": "APIKeyService", @@ -542,6 +542,7 @@ "name": "WorkContextService", "fullName": "saas.accounts.v1.WorkContextService", "procedures": [ + "/saas.accounts.v1.WorkContextService/AdmitWorkloadOperation", "/saas.accounts.v1.WorkContextService/AuthorizeEvidenceRead", "/saas.accounts.v1.WorkContextService/CheckAuthorizationRevision", "/saas.accounts.v1.WorkContextService/ConsumeSingleUse", diff --git a/module/module.package.codefly.yaml b/module/module.package.codefly.yaml index 509b3adfe..b75b0bc86 100644 --- a/module/module.package.codefly.yaml +++ b/module/module.package.codefly.yaml @@ -18,12 +18,12 @@ services: kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/authority/contract.binpb - digest: sha256:8e59a8dabb889185a44cb9e0d8c9cdc4fbef03b27e7c01f2cc581c69d2e8799a + digest: sha256:6100316ada259d0f5b29458a686ca0dd430948b6770b3c0d69d2153716ce57d7 - endpoint: connect kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/connect/contract.binpb - digest: sha256:8e59a8dabb889185a44cb9e0d8c9cdc4fbef03b27e7c01f2cc581c69d2e8799a + digest: sha256:6100316ada259d0f5b29458a686ca0dd430948b6770b3c0d69d2153716ce57d7 - name: auth-gateway endpoints: [grpc, rest] api-contracts: diff --git a/module/services/accounts/AGENTS.md b/module/services/accounts/AGENTS.md index bb534f89b..b69aab2d2 100644 --- a/module/services/accounts/AGENTS.md +++ b/module/services/accounts/AGENTS.md @@ -523,33 +523,66 @@ the parent's seal) refuses a person's child for an audience sealed elsewhere. It holds a child of a service principal's capability as it is, and refuses a child of any other owner kind. -Two person-owned kinds are sealed elsewhere by design, each behind its own gate: +Several person-owned kinds are sealed elsewhere by design, each behind its own +gate: - a **source-operation context** (`startSourceOperation`): sealed to the installation of the module using the person's source delegation, and minted only by that module's identity exercising the delegation (`MintSourceOperationContext`, or the delegation arm of `ExchangeDelegatedOperationAudience`); -- an **installation task** (`StartInstallationTask`): owned by the - installation's owner of record, sealed to that installation, and reached only - with an `EXPOSURE_INTERNAL` credential. - -The gateway's bearer-less module arm depends on it: it admits a capability with -no bearer only when the seal names an active installation other than the -target's. That admits module operation contexts and the two kinds above, the -unattended-work family, and never a person's interactive capability. +- an **installation task** (`StartInstallationTask`, and an install step's + capability): owned by the installation's owner of record, sealed to that + installation, and reached only with an `EXPOSURE_INTERNAL` credential; +- an **installed binding's child** (`exchangeInstalledBinding`, the + `content_read` and `module_actor` modes of + `ExchangeDelegatedOperationAudience`): a core `Child` of the person's + capability, so it keeps the parent's seal, the exchanging module's + installation, while addressing another module. It does not pass through + `StartChildSession`, so the child guard does not apply to it. + `TestAPersonsCapabilityIsSealedToItsAudiencesInstallation` -(`pkg/adapters/work_context_seal_pin_test.go`) pins it over every person mint, -and `TestEveryWorkContextMintIsClassifiedBySeal` enumerates every mint RPC from -the generated descriptors and fails on one listed neither as person-sealed (with -its case in that table) nor as sealed elsewhere (with its reason). A mint that -copies a parent's seal across audiences reopens the arm to a person's credential -without its bearer. - -The arm reads the tenant's installations through `InstallationService/ListInstallations` -(`EXPOSURE_INTERNAL`). Its contract comment in `installations.proto` names both +(`pkg/adapters/work_context_seal_pin_test.go`) pins the rule over every person +mint, and `TestEveryWorkContextMintIsClassifiedBySeal` enumerates every mint RPC +from the generated descriptors and fails on one listed neither as person-sealed +(with its case in that table) nor as sealed elsewhere (with its reason). + +## The bearer-less module arm asks the issuer + +The gateway's bearer-less arm of `/v1//*` admits exactly one shape, a +module operation context (`StartModuleOperationTask`), and decides nothing from +the seal itself: a seal on an installation other than the target's is carried +by every kind above, all of them a person's. It asks +`WorkContextService/AdmitWorkloadOperation` (`EXPOSURE_INTERNAL`, the +gateway's internal credential, the tier `ListInstallations` sits in). Accounts, +as the issuer, runs core's `Inspector` (signature under this issuer, window, +audience equal to the alias), decodes its own claims and admits only a +capability owned by a service principal with no actor chain, grant hop, +operation binding, parent session or owner agent, with authority scopes, an +idempotent replay policy and at most the operation-context window, whose owner +is a declared module principal other than `ModulePrincipalID(alias)` and whose +seal is that principal's ACTIVE installation in the tenant, read live. It never +reaches `verifyFor`, `verifyIssued` or the replay store, so it consumes +nothing: the callee verifies at full strength. A refusal is an answer with a +`WorkloadOperationRefusal`; an error means it could not decide, and the gateway +answers 503. The messages live in `workload_operation_admission.proto`, which +the gateway vendors byte for byte. + +Two guards hold it (`pkg/adapters/workload_operation_admission_test.go`): +`TestEveryMintHasAWorkloadOperationVerdict` mints through every mint RPC, every +binding mode and every signing entrypoint, plus crafted variants of the +operation-context shape, and asserts each verdict, with exactly one admitted; +`TestWorkloadOperationTableNamesEveryMintAndOwnerKind` fails when a +`saas.accounts.v1` RPC answering a token, an adapters function that signs (calls +core's `Start` or `Child`, `mint` or `remint`) or a `PrincipalKind*` constant +has no row. `TestAdmitWorkloadOperationConsumesNothing` pins that a single-use +capability shown to it still verifies once at its callee. + +The arm also reads the tenant's installations through +`InstallationService/ListInstallations` (`EXPOSURE_INTERNAL`), to require the +target installed there. Its contract comment in `installations.proto` names both of the gateway's reads: the viewer projection, with the tenant projected from a -verified identity, and the arm, whose tenant is the one an edge-verified Work +verified identity, and the arm, whose tenant is the one an edge-inspected Work Context names, a signed statement of this issuer's and never a value the request chose. The messages the RPC takes and answers live in `installation_listing.proto`, which `installations.proto` imports and the diff --git a/module/services/accounts/AUTHZ_MATRIX.md b/module/services/accounts/AUTHZ_MATRIX.md index 9a6f9d90a..4dc7bf3eb 100644 --- a/module/services/accounts/AUTHZ_MATRIX.md +++ b/module/services/accounts/AUTHZ_MATRIX.md @@ -2,7 +2,7 @@ > Generated from protobuf service descriptors and `saas.policy.v1.method_policy`; only the prose description is joined from `pkg/business/introspection.go`. Do not edit by hand. Run `go generate ./pkg/business` from `module/services/accounts/code`. -Inventory: **270 RPCs** across **36 services**. Missing, invalid, or unclassified procedures are denied by both Connect and gRPC interceptors. +Inventory: **271 RPCs** across **36 services**. Missing, invalid, or unclassified procedures are denied by both Connect and gRPC interceptors. The compact tier is retained for compatibility. Guards, resource bindings, audit events, limiter class, and data sensitivity are descriptor-authoritative. Domain handlers may enforce stronger state-dependent rules but may not weaken this floor. @@ -269,6 +269,7 @@ The compact tier is retained for compatibility. Guards, resource bindings, audit | `/saas.accounts.v1.WebhookService/ReplayDelivery` | unary | `POST /v1/webhooks/deliveries/{id}:replay` | `org_admin` | exposure=AUTHENTICATED; tenant=ORG_ADMIN | perm=webhooks:write; scope=webhooks:write | id → OWNED_RESOURCE/RESOURCE_TO_ORGANIZATION | SUCCESS: saas.webhook.replayed | FORBIDDEN / STANDARD_WRITE | CONFIDENTIAL → CONFIDENTIAL | Create and audit a new attempt for a past delivery using its stable event ID. | | `/saas.accounts.v1.WebhookService/RotateSecret` | unary | `POST /v1/webhooks/{id}:rotateSecret` | `mfa` | exposure=AUTHENTICATED; tenant=ORG_ADMIN; mfa=RECENT_STEP_UP; impersonation=FORBIDDEN | perm=webhooks:write; scope=webhooks:write | id → OWNED_RESOURCE/RESOURCE_TO_ORGANIZATION | SUCCESS: saas.webhook.secret_rotated | FORBIDDEN / SENSITIVE | CONFIDENTIAL → SECRET | Rotate the reveal-once signing secret with bounded dual-signature overlap. Requires recent MFA. | | `/saas.accounts.v1.WebhookService/TestWebhook` | unary | `POST /v1/webhooks/{id}:test` | `org_admin` | exposure=AUTHENTICATED; tenant=ORG_ADMIN | perm=webhooks:write; scope=webhooks:write | id → OWNED_RESOURCE/RESOURCE_TO_ORGANIZATION | — | FORBIDDEN / STANDARD_WRITE | CONFIDENTIAL → CONFIDENTIAL | Send a test ping. | +| `/saas.accounts.v1.WorkContextService/AdmitWorkloadOperation` | unary | `—` | `internal` | exposure=INTERNAL; tenant=NONE | — | — | — | FORBIDDEN / INTERNAL | CONFIDENTIAL → CONFIDENTIAL | Answer whether a capability presented with no bearer at a module route is a module operation context for it, consuming nothing. | | `/saas.accounts.v1.WorkContextService/AuthorizeEvidenceRead` | unary | `—` | `internal` | exposure=INTERNAL; tenant=NONE | — | — | — | FORBIDDEN / INTERNAL | CONFIDENTIAL → CONFIDENTIAL | Authorize a filtered Evidence read from current tenant membership and RBAC facts. | | `/saas.accounts.v1.WorkContextService/CheckAuthorizationRevision` | unary | `—` | `internal` | exposure=INTERNAL; tenant=NONE | — | — | — | FORBIDDEN / INTERNAL | CONFIDENTIAL → CONFIDENTIAL | Revalidate every subject and scope in a signed Work Context against current authority. | | `/saas.accounts.v1.WorkContextService/ConsumeSingleUse` | unary | `—` | `internal` | exposure=INTERNAL; tenant=NONE | — | — | — | FORBIDDEN / INTERNAL | CONFIDENTIAL → CONFIDENTIAL | Claim a single-use Work Context exactly once; replays of the same context id fail closed. | @@ -282,7 +283,7 @@ The compact tier is retained for compatibility. Guards, resource bindings, audit ## Tier totals - `auth`: 43 -- `internal`: 54 +- `internal`: 55 - `mfa`: 5 - `org_admin`: 65 - `org_member`: 56 diff --git a/module/services/accounts/code/pkg/adapters/connect_handlers_work_context.go b/module/services/accounts/code/pkg/adapters/connect_handlers_work_context.go index 1531e6f41..d2eac299f 100644 --- a/module/services/accounts/code/pkg/adapters/connect_handlers_work_context.go +++ b/module/services/accounts/code/pkg/adapters/connect_handlers_work_context.go @@ -37,6 +37,13 @@ func (h *workContextConnectHandler) ConsumeSingleUse( return unary(ctx, req, h.inner.ConsumeSingleUse) } +func (h *workContextConnectHandler) AdmitWorkloadOperation( + ctx context.Context, + req *connect.Request[gen.AdmitWorkloadOperationRequest], +) (*connect.Response[gen.AdmitWorkloadOperationResponse], error) { + return unary(ctx, req, h.inner.AdmitWorkloadOperation) +} + func (h *workContextConnectHandler) StartTask( ctx context.Context, req *connect.Request[gen.StartTaskWorkContextRequest], diff --git a/module/services/accounts/code/pkg/adapters/installation_rpcs.go b/module/services/accounts/code/pkg/adapters/installation_rpcs.go index 89167b47c..ff6f07db6 100644 --- a/module/services/accounts/code/pkg/adapters/installation_rpcs.go +++ b/module/services/accounts/code/pkg/adapters/installation_rpcs.go @@ -136,8 +136,9 @@ func (s *InstallationServer) GetInstallation(ctx context.Context, req *gen.GetIn // like the scope listing it is read beside: the organization is a request field, // so a bare tenant JWT must never be able to ask this. The caller that may is // the auth-gateway, naming either the tenant it projected from a verified -// identity (its viewer projection) or the tenant an edge-verified Work Context -// names (its declared module route's bearer-less arm), never one a request chose. +// identity (its viewer projection) or the tenant an edge-inspected Work Context +// names (its declared module route's bearer-less arm, which asks whether the +// target is installed there), never one a request chose. func (s *InstallationServer) ListInstallations(ctx context.Context, req *gen.ListInstallationsRequest) (*gen.ListInstallationsResponse, error) { if err := Validate(req); err != nil { return nil, err diff --git a/module/services/accounts/code/pkg/adapters/work_context_rpcs.go b/module/services/accounts/code/pkg/adapters/work_context_rpcs.go index a339ac48f..ec53f1f83 100644 --- a/module/services/accounts/code/pkg/adapters/work_context_rpcs.go +++ b/module/services/accounts/code/pkg/adapters/work_context_rpcs.go @@ -1467,10 +1467,10 @@ func (s *WorkContextAuthorityServer) StartChildSession( // the seal from the audience (StartTask, and remint for StartRootSession, // ExchangeAudience and RenewWorkContext). Core's Child cannot: a child keeps its // parent's seal, so a child for another audience would be a person's capability -// sealed to an installation that is not its audience's. The gateway's bearer-less -// module arm reads exactly that difference as "unattended work" (a module or -// installation principal is sealed to its own installation), so such a child -// would be admitted there without the person's bearer. +// sealed to an installation that is not its audience's, which a verifier reading +// the seal as the audience's installation would misplace. (The gateway's +// bearer-less module arm no longer reads the seal that way: it admits only what +// AdmitWorkloadOperation, reading the owner, says is a module operation context.) // // A same-audience child keeps the seal its audience resolves to now, and is // minted. A service principal is sealed to its own installation, which the diff --git a/module/services/accounts/code/pkg/adapters/work_context_seal_pin_test.go b/module/services/accounts/code/pkg/adapters/work_context_seal_pin_test.go index 15cdb075d..efa962da0 100644 --- a/module/services/accounts/code/pkg/adapters/work_context_seal_pin_test.go +++ b/module/services/accounts/code/pkg/adapters/work_context_seal_pin_test.go @@ -22,12 +22,12 @@ import ( // // A person's capability is sealed to the organization's installation of the // audience it addresses; a module or installation principal's is sealed to its -// own installation. The gateway's bearer-less module arm depends on that -// difference: it admits a capability for module X without a bearer only when the -// seal names an active installation OTHER than the organization's installation -// of X, which is what unattended work carries and what a person's capability -// never does. A person mint that let the seal drift to another audience's -// installation would be admitted there without the person's bearer. +// own installation. The gateway's bearer-less module arm used to read that +// difference as "unattended work"; it no longer reads the seal at all, and asks +// AdmitWorkloadOperation instead, which admits only a module operation context +// (workload_operation_admission_test.go pins every mint's verdict). The rule +// still states where a person's capability is sealed, and a verifier that holds +// the seal to the audience's installation relies on it. // // Every case below either mints a capability sealed to its audience's // installation, or is refused. Each parent that is re-derived is sealed to a @@ -226,13 +226,14 @@ var personSealedMints = map[string]string{ // notAudienceSealedMints is every RPC (or arm of one) that issues a capability // NOT sealed to its audience's installation, each by design and with the gate -// that keeps it from a person's hands. The gateway's bearer-less module arm -// admits these: a seal other than the target's is what they carry. +// that keeps it from a person's hands. Of these, the gateway's bearer-less module +// arm admits only the module operation context: it asks the issuer for the +// owner rather than reading the seal. var notAudienceSealedMints = map[string]string{ "ModuleCapabilitiesService/MintModuleWorkContext": "a module's own identity (StartModuleTask): owned by the module principal and sealed to its own installation", "ModuleCapabilitiesService/MintModuleOperationContext": "a module operation context (StartModuleOperationTask): owned by the module principal and sealed to its own installation", "ModuleCapabilitiesService/MintSourceOperationContext": "a source-operation context (StartSourceOperationTask): owned by the person, sealed to the USING module's installation, minted only by that module's identity exercising the person's source delegation", - "ModuleCapabilitiesService/ExchangeDelegatedOperationAudience": "its delegation_id arm and a source-delegation parent re-mint a source-operation context (startSourceOperation), sealed and gated as MintSourceOperationContext is", + "ModuleCapabilitiesService/ExchangeDelegatedOperationAudience": "its delegation_id arm and a source-delegation parent re-mint a source-operation context (startSourceOperation), sealed and gated as MintSourceOperationContext is; its installed content_read and module_actor modes derive a core Child of the person's capability (exchangeInstalledBinding), which keeps the exchanging module's seal", "WorkContextService/StartInstallationTask": "an installation task: owned by the installation's owner of record, sealed to the installation it draws on, minted only with an EXPOSURE_INTERNAL credential", } diff --git a/module/services/accounts/code/pkg/adapters/workload_operation_admission.go b/module/services/accounts/code/pkg/adapters/workload_operation_admission.go new file mode 100644 index 000000000..852c59b8e --- /dev/null +++ b/module/services/accounts/code/pkg/adapters/workload_operation_admission.go @@ -0,0 +1,143 @@ +package adapters + +import ( + "context" + "crypto/ed25519" + "errors" + "time" + + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" + "github.com/codefly-dev/core/workcontext" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "accounts/pkg/business" + gen "accounts/pkg/gen/saas/accounts/v1" +) + +// The issuer's half of the gateway's bearer-less module arm. +// +// A request at /v1//* with no bearer is admitted only for a module +// operation context: the capability StartModuleOperationTask mints for a +// composed module acting as itself, with no person present. The gateway cannot +// tell that shape from a person's capability. Core's Inspector, all a hop runs, +// withholds the owner, its kind, the chain and the binding, and several +// person-owned capabilities are sealed to an installation that is not their +// audience's (a source-operation context, an installation task, an installed +// binding's child). So the gateway asks the issuer, which reads its own claims. +// +// This is an ALLOW-LIST of one shape. Every other capability this issuer mints +// is refused, and a new mint is refused until it is admitted here on purpose; +// TestEveryMintHasAWorkloadOperationVerdict holds every mint to a verdict and +// TestWorkloadOperationTableNamesEveryMintAndOwnerKind fails when a mint or an +// owner kind appears that the table does not name. +// +// It consumes nothing. It inspects (core's stateless prefix of Verify: +// signature under this issuer, window, audience, shape) and decodes; it never +// reaches verifyFor, verifyIssued or the replay store, so a single-use +// capability it was shown still verifies at its callee, which remains the one +// party that verifies at full strength. + +// AdmitWorkloadOperation answers whether a capability presented with no bearer +// at the declared module route for alias is a module operation context for it. +// A refusal is an answer, not an error: errors are reserved for "could not +// decide", which the gateway answers 503. +func (s *WorkContextAuthorityServer) AdmitWorkloadOperation( + ctx context.Context, req *gen.AdmitWorkloadOperationRequest, +) (*gen.AdmitWorkloadOperationResponse, error) { + if err := Validate(req); err != nil { + return nil, err + } + if err := requireInternalCredential(ctx); err != nil { + return nil, err + } + if !s.configured() || service == nil { + return nil, status.Error(codes.Unavailable, "Work Context authority is not configured") + } + claims, refusal, err := s.admitWorkloadOperation(ctx, req.GetWorkContext(), req.GetAlias()) + if err != nil { + return nil, status.Error(codes.Unavailable, "module installation authority unavailable") + } + if refusal != gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED { + return &gen.AdmitWorkloadOperationResponse{Refusal: refusal}, nil + } + return &gen.AdmitWorkloadOperationResponse{ + Admitted: true, + TenantId: claims.GetTenantId(), + InstallationId: claims.GetSeal().GetInstallationId(), + TaskId: claims.GetTaskId(), + }, nil +} + +// admitWorkloadOperation is the decision. It returns the claims when admitted, +// the refusal otherwise, and an error only when the live read could not answer. +func (s *WorkContextAuthorityServer) admitWorkloadOperation( + ctx context.Context, encoded, alias string, +) (*basev0.WorkContextV1, gen.WorkloadOperationRefusal, error) { + // No operation context is minted for the capability surface, and an empty + // alias is not a route. + if requireMintableAudience(alias) != nil { + return nil, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED, nil + } + inspector := &workcontext.Inspector{ + Issuer: s.issuer, + Keys: map[string]ed25519.PublicKey{s.keyID: s.publicKey}, + Now: s.now, + } + if _, err := inspector.Inspect(encoded, alias); err != nil { + return nil, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED, nil + } + // The signature is this issuer's, so the claims are its own statement. + decoded, err := workcontext.Decode(encoded) + if err != nil { + return nil, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED, nil + } + claims := decoded.Context() + if claims.GetOwnerPrincipalKind() != business.PrincipalKindService { + return nil, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE, nil + } + if !isModuleOperationContextShape(claims) { + return nil, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_OPERATION_CONTEXT, nil + } + owner := claims.GetOwnerPrincipalId() + if owner == business.ModulePrincipalID(alias) { + return nil, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET, nil + } + if !service.IsDeclaredModulePrincipal(owner) { + return nil, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS, nil + } + // The seal must be the ACTIVE installation the owner is installed under in + // the tenant, read live: the same answer a verifier holds the seal to. + seal := claims.GetSeal().GetInstallationId() + live, _, err := service.WorkContextSeal(ctx, claims.GetTenantId(), owner, seal) + switch { + case errors.Is(err, business.ErrModuleInstallationInactive), errors.Is(err, business.ErrWorkContextSealUnheld): + return nil, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS, nil + case err != nil: + return nil, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED, err + case live == "" || live != seal: + return nil, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS, nil + } + return claims, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED, nil +} + +// isModuleOperationContextShape reports whether a service principal's claims +// are exactly what StartModuleOperationTask signs: the module acting as itself +// (no actor chain, no grant hop, no operation binding, no owner agent), a root +// session of its own task, the binding's headless scopes, idempotent, and no +// longer than the operation-context window. Anything a re-mint, a child or +// another service mint would add is a difference, and a difference refuses. +func isModuleOperationContextShape(claims *basev0.WorkContextV1) bool { + window := time.Duration(claims.GetExpiresAtUnix()-claims.GetIssuedAtUnix()) * time.Second + return len(claims.GetActorChain()) == 0 && + claims.GrantHop == nil && + claims.OperationBinding == nil && + claims.OwnerAgentId == nil && + claims.ParentSessionId == nil && + claims.WorkspaceId == nil && + claims.ProjectId == nil && + len(claims.GetAttributionTeamIds()) == 0 && + len(claims.GetAuthorityScopes()) > 0 && + claims.GetReplayPolicy() == workcontext.ReplayIdempotent && + window > 0 && window <= business.ModuleOperationContextTTL +} diff --git a/module/services/accounts/code/pkg/adapters/workload_operation_admission_test.go b/module/services/accounts/code/pkg/adapters/workload_operation_admission_test.go new file mode 100644 index 000000000..d0d3d29c1 --- /dev/null +++ b/module/services/accounts/code/pkg/adapters/workload_operation_admission_test.go @@ -0,0 +1,539 @@ +package adapters + +import ( + "context" + "errors" + "go/ast" + "go/parser" + "go/token" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "testing" + "time" + + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" + "github.com/codefly-dev/core/workcontext" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/reflect/protoregistry" + + "accounts/pkg/auth" + "accounts/pkg/business" + "accounts/pkg/business/moduleregistrytest" + gen "accounts/pkg/gen/saas/accounts/v1" +) + +// THE BEARER-LESS ARM ADMITS ONE SHAPE, pinned over every mint. +// +// The gateway admits a capability at /v1//* with no bearer only when +// AdmitWorkloadOperation says it is a module operation context +// (StartModuleOperationTask). Every row below mints a capability through a real +// mint path and asserts the verdict; only the operation context is admitted. +// A new mint RPC, a new signing entrypoint or a new owner kind fails +// TestWorkloadOperationTableNamesEveryMintAndOwnerKind until it has a row here. + +const admissionTestToken = "workload-admission-test" + +func admissionContext(t *testing.T) context.Context { + t.Helper() + SetInternalToken(admissionTestToken) + t.Cleanup(func() { SetInternalToken("") }) + return metadata.NewIncomingContext(context.Background(), metadata.Pairs("x-codefly-internal-token", admissionTestToken)) +} + +func admitWorkload(t *testing.T, server *WorkContextAuthorityServer, token, alias string) *gen.AdmitWorkloadOperationResponse { + t.Helper() + answer, err := server.AdmitWorkloadOperation(admissionContext(t), &gen.AdmitWorkloadOperationRequest{WorkContext: token, Alias: alias}) + require.NoError(t, err) + return answer +} + +// admissionMint mints one capability and names the alias it is presented at. +type admissionMint func(t *testing.T) (server *WorkContextAuthorityServer, alias, token string) + +// admissionRow is one mint path and the verdict it must get. +type admissionRow struct { + name string + // rpc is the saas.accounts.v1 method the capability comes from, empty for + // a path no RPC answers (an install step's job, a crafted shape). + rpc string + // kind is the owner kind the minted capability must carry. + kind string + // want is the refusal; UNSPECIFIED means admitted. + want gen.WorkloadOperationRefusal + mint admissionMint +} + +func fromSealPin(name string) admissionMint { + return func(t *testing.T) (*WorkContextAuthorityServer, string, string) { + for _, tc := range sealPinCases() { + if tc.name != name { + continue + } + server, audience, token, err := tc.mint(t) + require.NoError(t, err) + return server, audience, token + } + t.Fatalf("no seal pin case %q", name) + return nil, "", "" + } +} + +func installedExchange(binding, alias string, lookup bool) admissionMint { + return func(t *testing.T) (*WorkContextAuthorityServer, string, string) { + c := moduleActorFixture(t, append(moduleActorScopes, &gen.WorkContextScope{ResourceKind: "documents", Actions: []string{"read"}})) + issued, err := c.exchange(binding, c.parent.Token, c.moduleToken, lookup) + require.NoError(t, err) + return c.server, alias, issued.GetToken() + } +} + +func moduleOperationFixture(t *testing.T) (business.ModuleOperationContextAuthority, string) { + t.Helper() + installModuleOperationContextService(t, headlessPrincipals) + installModuleWorkContextAuthority(t) + principal := business.ModulePrincipalID("documents") + installation, _, err := service.WorkContextSeal(context.Background(), moduleWorkContextTenant, principal, "") + require.NoError(t, err) + return business.ModuleOperationContextAuthority{ + ModuleWorkContextAuthority: business.ModuleWorkContextAuthority{PrincipalID: principal, Tenant: moduleWorkContextTenant, InstallationID: installation}, + BindingID: "model", + Audience: "modelservice", + Scopes: []business.ModuleOperationScope{{ResourceKind: "modelservice.profiles", Actions: []string{"invoke", "read"}}}, + Revision: 1, + }, installation +} + +// operationShape mints what StartModuleOperationTask signs, with change applied +// to the start input or the request, so a row can vary exactly one thing. +func operationShape(change func(*mintRequest)) admissionMint { + return func(t *testing.T) (*WorkContextAuthorityServer, string, string) { + authority, installation := moduleOperationFixture(t) + request := mintRequest{ + Start: workcontext.StartInput{ + TenantID: authority.Tenant, OwnerPrincipalID: authority.PrincipalID, + OwnerPrincipalKind: business.PrincipalKindService, OrganizationID: authority.Tenant, + TaskID: uuid.NewString(), Audience: "modelservice", InstallationID: installation, + AuthorityScopes: []*basev0.WorkScopeV1{{ResourceKind: "modelservice.profiles", Actions: []string{"read"}}}, + }, + Replay: workcontext.ReplayIdempotent, + TTL: business.ModuleOperationContextTTL, + } + change(&request) + token, _, err := workContextSingleton.mint(context.Background(), request) + require.NoError(t, err) + return workContextSingleton, "modelservice", token + } +} + +func workloadOperationRows() []admissionRow { + refused := func(r gen.WorkloadOperationRefusal) gen.WorkloadOperationRefusal { return r } + person := refused(gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE) + notInspected := refused(gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED) + notShape := refused(gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_OPERATION_CONTEXT) + admitted := gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED + human, svc := business.PrincipalKindHuman, business.PrincipalKindService + return []admissionRow{ + // A person's own session, and every re-mint of it. + {"StartTask", "WorkContextService/StartTask", human, person, fromSealPin("StartTask")}, + {"StartRootSession", "WorkContextService/StartRootSession", human, person, fromSealPin("StartRootSession")}, + {"ExchangeAudience", "WorkContextService/ExchangeAudience", human, person, fromSealPin("ExchangeAudience")}, + {"RenewWorkContext", "WorkContextService/RenewWorkContext", human, person, fromSealPin("RenewWorkContext")}, + {"StartChildSession", "WorkContextService/StartChildSession", human, person, fromSealPin("StartChildSession for the parent's audience")}, + // A module exchanging a person's capability, in every binding mode. + {"operation exchange, plain binding", "ModuleCapabilitiesService/ExchangeDelegatedOperationAudience", human, person, + fromSealPin("a module's exchange through its operation binding")}, + {"read exchange, plain binding", "ModuleCapabilitiesService/ExchangeDelegatedReadAudience", human, person, + fromSealPin("a module's read exchange through its read binding")}, + {"operation exchange, content_read, invoke", "ModuleCapabilitiesService/ExchangeDelegatedOperationAudience", human, person, + installedExchange("evidence", "documents", false)}, + {"operation exchange, content_read, lookup", "ModuleCapabilitiesService/ExchangeDelegatedOperationAudience", human, person, + installedExchange("evidence", "documents", true)}, + {"operation exchange, module_actor, invoke", "ModuleCapabilitiesService/ExchangeDelegatedOperationAudience", human, person, + installedExchange("act", "compute", false)}, + {"operation exchange, module_actor, lookup", "ModuleCapabilitiesService/ExchangeDelegatedOperationAudience", human, person, + installedExchange("act", "compute", true)}, + {"operation exchange, source delegation reference", "ModuleCapabilitiesService/ExchangeDelegatedOperationAudience", human, person, + func(t *testing.T) (*WorkContextAuthorityServer, string, string) { + installDelegationBindingService(t) + issued, err := exchangeByDelegation(presentedAs(t, identityOf(t, "ingestservice")), "ingest") + require.NoError(t, err) + return workContextSingleton, "reports", issued.GetToken() + }}, + {"operation exchange, source delegation parent", "ModuleCapabilitiesService/ExchangeDelegatedOperationAudience", human, person, + func(t *testing.T) (*WorkContextAuthorityServer, string, string) { + installDelegationBindingService(t) + // A binding no wider than the delegation's own hop, so the + // re-mint attenuates rather than widens. + registry, err := moduleregistrytest.Parse(delegationBindingPrincipals) + require.NoError(t, err) + target := registry[business.ModulePrincipalID("ingestservice")] + target.OperationAudiences["ingest-write"] = business.ModuleOperationAudience{Audience: "reports", + InvokeScopes: []business.ModuleOperationScope{{ResourceKind: "collections", Actions: []string{"write"}}}} + service.SetModuleCapabilities(nil, nil, registry) + parent, err := mintSourceContext("docstore", moduleToken(t, "docstore"), business.SourceDelegationRef{DelegationID: delegationID}) + require.NoError(t, err) + issued, err := ModuleCapabilitiesSingleton().ExchangeDelegatedOperationAudience(presentedAs(t, identityOf(t, "ingestservice")), + &gen.ModuleExchangeDelegatedOperationAudienceRequest{BindingId: "ingest-write", ParentWorkContextToken: parent.GetToken()}) + require.NoError(t, err) + return workContextSingleton, "reports", issued.GetToken() + }}, + // Person-owned unattended work: sealed elsewhere by design, still a person's. + {"source-operation context", "ModuleCapabilitiesService/MintSourceOperationContext", human, person, + func(t *testing.T) (*WorkContextAuthorityServer, string, string) { + installSourceDelegationService(t) + resp, err := mintSourceContext("docstore", moduleToken(t, "docstore"), business.SourceDelegationRef{DelegationID: delegationID}) + require.NoError(t, err) + return workContextSingleton, "ingestservice", resp.GetToken() + }}, + {"installation task", "WorkContextService/StartInstallationTask", human, person, + func(t *testing.T) (*WorkContextAuthorityServer, string, string) { + server := newInstallationMintServer(t, healthyInstallationFacts(), nil) + issued, err := server.StartInstallationTask(installInternalContext(), installTaskRequest()) + require.NoError(t, err) + return server, installTaskRequest().GetAudience(), issued.GetToken() + }}, + {"installation step", "", human, person, + func(t *testing.T) (*WorkContextAuthorityServer, string, string) { + server := newInstallationMintServer(t, healthyInstallationFacts(), nil) + issued, err := server.signInstallationStep(context.Background(), installTaskRequest(), healthyInstallationFacts(), nil) + require.NoError(t, err) + return server, installTaskRequest().GetAudience(), issued.GetToken() + }}, + // A module's own identity is addressed to the capability surface, which + // is never a route. + {"module identity", "ModuleCapabilitiesService/MintModuleWorkContext", svc, notInspected, + func(t *testing.T) (*WorkContextAuthorityServer, string, string) { + installModuleWorkContextService(t, documentsPrincipals) + installModuleWorkContextAuthority(t) + resp, err := mintModuleWorkContext(t, "documents", moduleToken(t, "documents")) + require.NoError(t, err) + return workContextSingleton, ModuleWorkContextAudience, resp.GetToken() + }}, + {"installer token route", "", svc, notInspected, + func(t *testing.T) (*WorkContextAuthorityServer, string, string) { + for _, entry := range mintEntrypoints() { + if entry.name != "installer token route" { + continue + } + prefix, mint := entry.install(t) + token, err := mint(moduleToken(t, prefix)) + require.NoError(t, err) + return workContextSingleton, ModuleWorkContextAudience, token + } + t.Fatal("no installer token route entrypoint") + return nil, "", "" + }}, + {"module identity at a module route", "", svc, notInspected, + func(t *testing.T) (*WorkContextAuthorityServer, string, string) { + installModuleWorkContextService(t, documentsPrincipals) + installModuleWorkContextAuthority(t) + resp, err := mintModuleWorkContext(t, "documents", moduleToken(t, "documents")) + require.NoError(t, err) + return workContextSingleton, "modelservice", resp.GetToken() + }}, + // THE ONE ADMITTED SHAPE. + {"module operation context", "ModuleCapabilitiesService/MintModuleOperationContext", svc, admitted, + func(t *testing.T) (*WorkContextAuthorityServer, string, string) { + installModuleOperationContextService(t, headlessPrincipals) + installModuleWorkContextAuthority(t) + resp, err := mintModuleOperationContext("documents", moduleToken(t, "documents"), "model") + require.NoError(t, err) + return workContextSingleton, "modelservice", resp.GetToken() + }}, + {"module operation context at another alias", "", svc, notInspected, + func(t *testing.T) (*WorkContextAuthorityServer, string, string) { + installModuleOperationContextService(t, headlessPrincipals) + installModuleWorkContextAuthority(t) + resp, err := mintModuleOperationContext("documents", moduleToken(t, "documents"), "model") + require.NoError(t, err) + return workContextSingleton, "reportservice", resp.GetToken() + }}, + {"module operation context for its own module", "", svc, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET, + func(t *testing.T) (*WorkContextAuthorityServer, string, string) { + authority, _ := moduleOperationFixture(t) + authority.Audience = "documents" + token, _, err := workContextSingleton.StartModuleOperationTask(context.Background(), authority) + require.NoError(t, err) + return workContextSingleton, "documents", token + }}, + {"module operation context sealed to another installation", "", svc, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS, + func(t *testing.T) (*WorkContextAuthorityServer, string, string) { + authority, _ := moduleOperationFixture(t) + authority.InstallationID = uuid.NewString() + token, _, err := workContextSingleton.StartModuleOperationTask(context.Background(), authority) + require.NoError(t, err) + return workContextSingleton, "modelservice", token + }}, + {"service principal no module declares", "", svc, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS, + operationShape(func(r *mintRequest) { r.Start.OwnerPrincipalID = uuid.NewString() })}, + {"operation context shape, single-use", "", svc, notShape, + operationShape(func(r *mintRequest) { r.Replay = workcontext.ReplaySingleUse })}, + {"operation context shape, longer window", "", svc, notShape, + operationShape(func(r *mintRequest) { r.TTL = 5 * time.Minute })}, + {"operation context shape, no authority scope", "", svc, notShape, + operationShape(func(r *mintRequest) { r.Start.AuthorityScopes = nil })}, + {"operation context shape, with an actor hop", "", svc, notShape, + operationShape(func(r *mintRequest) { + r.Hops = []delegationHop{{ + PrincipalID: r.Start.OwnerPrincipalID, PrincipalKind: business.PrincipalKindService, DelegationID: uuid.NewString(), + GrantedScopes: []*basev0.WorkScopeV1{{ResourceKind: "modelservice.profiles", Actions: []string{"read"}}}, + }} + })}, + {"agent owner", "", business.PrincipalKindAgent, person, + operationShape(func(r *mintRequest) { r.Start.OwnerPrincipalKind = business.PrincipalKindAgent })}, + {"human owner in the operation context shape", "", human, person, + operationShape(func(r *mintRequest) { r.Start.OwnerPrincipalKind = business.PrincipalKindHuman })}, + {"an owner kind this issuer does not name", "", "Service", person, + operationShape(func(r *mintRequest) { r.Start.OwnerPrincipalKind = "Service" })}, + } +} + +// Guard A: every mint path gets an asserted verdict, and only +// StartModuleOperationTask is admitted. +func TestEveryMintHasAWorkloadOperationVerdict(t *testing.T) { + admittedRows := 0 + for _, row := range workloadOperationRows() { + t.Run(row.name, func(t *testing.T) { + server, alias, token := row.mint(t) + require.NotEmpty(t, token) + decoded, err := workcontext.Decode(token) + require.NoError(t, err) + claims := decoded.Context() + require.Equal(t, row.kind, claims.GetOwnerPrincipalKind(), "the row mints the owner kind it claims to") + + answer := admitWorkload(t, server, token, alias) + if row.want == gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED { + admittedRows++ + require.True(t, answer.GetAdmitted(), "refused: %s", answer.GetRefusal()) + require.Equal(t, claims.GetTenantId(), answer.GetTenantId()) + require.Equal(t, claims.GetSeal().GetInstallationId(), answer.GetInstallationId()) + require.Equal(t, claims.GetTaskId(), answer.GetTaskId()) + return + } + require.False(t, answer.GetAdmitted()) + require.Equal(t, row.want, answer.GetRefusal()) + require.Empty(t, answer.GetTenantId()+answer.GetInstallationId()+answer.GetTaskId(), "a refusal names nothing") + }) + } + require.Equal(t, 1, admittedRows, "exactly one shape is admitted") +} + +// mintEntrypointRows maps every adapters function that signs a capability +// (calls core's Start or Child, mint or remint) to the rows that exercise it. +var mintEntrypointRows = map[string][]string{ + "mint": {"StartTask", "module operation context", "operation context shape, single-use"}, + "remint": {"StartRootSession", "ExchangeAudience", "RenewWorkContext", "operation exchange, plain binding", "read exchange, plain binding"}, + "StartTask": {"StartTask"}, + "StartRootSession": {"StartRootSession"}, + "exchangeVerifiedParent": {"ExchangeAudience", "operation exchange, plain binding", "read exchange, plain binding"}, + "RenewWorkContext": {"RenewWorkContext"}, + "StartChildSession": {"StartChildSession"}, + "exchangeInstalledBinding": {"operation exchange, content_read, invoke", "operation exchange, module_actor, invoke"}, + "startSourceOperation": {"source-operation context", "operation exchange, source delegation reference", "operation exchange, source delegation parent"}, + "mintInstallationTask": {"installation task", "installation step"}, + "StartModuleTask": {"module identity"}, + // The installer route's handler calls its mint field, StartModuleTask. + "token": {"installer token route"}, + "StartModuleOperationTask": {"module operation context", "module operation context for its own module"}, +} + +// signingFunctions walks the package's non-test sources and returns every +// function whose body calls a method named Start, Child, mint or remint. +func signingFunctions(t *testing.T) map[string]bool { + t.Helper() + files, err := filepath.Glob("*.go") + require.NoError(t, err) + found := map[string]bool{} + fset := token.NewFileSet() + for _, path := range files { + if strings.HasSuffix(path, "_test.go") { + continue + } + src, err := os.ReadFile(path) + require.NoError(t, err) + file, err := parser.ParseFile(fset, path, src, 0) + require.NoError(t, err) + for _, decl := range file.Decls { + fn, ok := decl.(*ast.FuncDecl) + if !ok || fn.Body == nil { + continue + } + ast.Inspect(fn.Body, func(node ast.Node) bool { + call, ok := node.(*ast.CallExpr) + if !ok { + return true + } + if sel, ok := call.Fun.(*ast.SelectorExpr); ok { + switch sel.Sel.Name { + case "Start", "Child", "mint", "remint": + found[fn.Name.Name] = true + } + } + return true + }) + } + } + return found +} + +// ownerKindConstants reads every PrincipalKind* constant business declares. +func ownerKindConstants(t *testing.T) map[string]bool { + t.Helper() + file, err := parser.ParseFile(token.NewFileSet(), filepath.Join("..", "business", "principals.go"), nil, 0) + require.NoError(t, err) + kinds := map[string]bool{} + for _, decl := range file.Decls { + gen, ok := decl.(*ast.GenDecl) + if !ok || gen.Tok != token.CONST { + continue + } + for _, spec := range gen.Specs { + value := spec.(*ast.ValueSpec) + for i, name := range value.Names { + if !strings.HasPrefix(name.Name, "PrincipalKind") || i >= len(value.Values) { + continue + } + lit, ok := value.Values[i].(*ast.BasicLit) + require.True(t, ok, "%s is not a literal", name.Name) + kind, err := strconv.Unquote(lit.Value) + require.NoError(t, err) + kinds[kind] = true + } + } + } + require.NotEmpty(t, kinds) + return kinds +} + +// Guard B: the table above is complete. It fails when a saas.accounts.v1 RPC +// answers a token no row mints through, when an adapters function starts +// signing capabilities without a row, and when business declares an owner kind +// no row mints. +func TestWorkloadOperationTableNamesEveryMintAndOwnerKind(t *testing.T) { + rows := map[string]admissionRow{} + rpcs := map[string]bool{} + kinds := map[string]bool{} + for _, row := range workloadOperationRows() { + require.NotContains(t, rows, row.name, "row names are unique") + rows[row.name] = row + if row.rpc != "" { + rpcs[row.rpc] = true + } + kinds[row.kind] = true + } + + var minting []string + protoregistry.GlobalFiles.RangeFilesByPackage("saas.accounts.v1", func(file protoreflect.FileDescriptor) bool { + for i := 0; i < file.Services().Len(); i++ { + service := file.Services().Get(i) + for j := 0; j < service.Methods().Len(); j++ { + method := service.Methods().Get(j) + if method.Output().Fields().ByName("token") != nil { + minting = append(minting, string(service.Name())+"/"+string(method.Name())) + } + } + } + return true + }) + require.NotEmpty(t, minting) + sort.Strings(minting) + for _, rpc := range minting { + require.True(t, rpcs[rpc], "%s answers a token and no row mints through it: add one to workloadOperationRows", rpc) + } + for rpc := range rpcs { + require.Contains(t, minting, rpc, "a row names %s, which answers no token", rpc) + } + + signing := signingFunctions(t) + for fn := range signing { + named, listed := mintEntrypointRows[fn] + require.True(t, listed, "%s signs a capability and mintEntrypointRows does not name it: add a row that mints through it", fn) + for _, name := range named { + require.Contains(t, rows, name, "%s names row %q, which does not exist", fn, name) + } + } + for fn := range mintEntrypointRows { + require.True(t, signing[fn], "mintEntrypointRows names %s, which no longer signs a capability", fn) + } + + for kind := range ownerKindConstants(t) { + require.True(t, kinds[kind], "owner kind %q has no row: say what the arm answers for it", kind) + } +} + +// AdmitWorkloadOperation consumes nothing. A single-use capability it was shown +// any number of times still verifies once at its callee, and only once; an +// admitted operation context still verifies at its callee afterwards. +func TestAdmitWorkloadOperationConsumesNothing(t *testing.T) { + t.Run("single-use", func(t *testing.T) { + exchangeInstallationService(t) + server := newCoreTestServer(t, &workContextAuthorityFake{facts: &business.WorkContextAuthorityFacts{OrganizationRevision: 12}}, newSealAnswersFake(12)) + token, _ := mintTestCapability(t, server, testCapability{ + Audience: "documents", Tenant: renewOrgID, Owner: renewActorID, Replay: workcontext.ReplaySingleUse, + Scopes: []*basev0.WorkScopeV1{{ResourceKind: "evidence", Actions: []string{"read"}}}, + }) + for range 3 { + answer := admitWorkload(t, server, token, "documents") + require.Equal(t, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE, answer.GetRefusal(), + "the shape check ran, so the capability was inspected and read") + } + _, err := server.verifyFor(context.Background(), token, "documents") + require.NoError(t, err, "the callee's one use is still there") + _, err = server.verifyFor(context.Background(), token, "documents") + require.ErrorIs(t, err, workcontext.ErrReplayed, "and it was single-use: the callee spent it, admission did not") + }) + t.Run("admitted operation context", func(t *testing.T) { + installModuleOperationContextService(t, headlessPrincipals) + installModuleWorkContextAuthority(t) + resp, err := mintModuleOperationContext("documents", moduleToken(t, "documents"), "model") + require.NoError(t, err) + for range 3 { + require.True(t, admitWorkload(t, workContextSingleton, resp.GetToken(), "modelservice").GetAdmitted()) + } + _, err = workContextSingleton.verifyFor(context.Background(), resp.GetToken(), "modelservice") + require.NoError(t, err) + }) +} + +// The RPC is internal-tier and fails closed: no internal credential is +// Unauthenticated, an unconfigured issuer or a live read that cannot answer is +// Unavailable, and neither is an admission. +func TestAdmitWorkloadOperationIsInternalAndFailsClosed(t *testing.T) { + installModuleOperationContextService(t, headlessPrincipals) + installModuleWorkContextAuthority(t) + resp, err := mintModuleOperationContext("documents", moduleToken(t, "documents"), "model") + require.NoError(t, err) + request := &gen.AdmitWorkloadOperationRequest{WorkContext: resp.GetToken(), Alias: "modelservice"} + + SetInternalToken(admissionTestToken) + t.Cleanup(func() { SetInternalToken("") }) + _, err = workContextSingleton.AdmitWorkloadOperation(context.Background(), request) + require.Equal(t, codes.Unauthenticated, status.Code(err)) + bearer := stampVerifiedIdentity(context.Background(), readOwner, readOrg, auth.Assurance{}) + _, err = workContextSingleton.AdmitWorkloadOperation(bearer, request) + require.Equal(t, codes.Unauthenticated, status.Code(err), "a person's identity is not the internal credential") + + unconfigured := &WorkContextAuthorityServer{} + _, err = unconfigured.AdmitWorkloadOperation(admissionContext(t), request) + require.Equal(t, codes.Unavailable, status.Code(err)) + + service.SetModuleAuthorityReads(unavailableModuleAuthority{currentModuleAuthority{service: service}}, nil) + answer, err := workContextSingleton.AdmitWorkloadOperation(admissionContext(t), request) + require.Equal(t, codes.Unavailable, status.Code(err)) + require.Nil(t, answer) +} + +// unavailableModuleAuthority is a live authority read that cannot answer. +type unavailableModuleAuthority struct{ currentModuleAuthority } + +func (unavailableModuleAuthority) LiveModuleAuthority(context.Context, string, string, string) (*business.LiveModuleAuthority, error) { + return nil, errors.New("authority read unavailable") +} diff --git a/module/services/accounts/code/pkg/business/introspection.go b/module/services/accounts/code/pkg/business/introspection.go index 86142d347..05ea48f06 100644 --- a/module/services/accounts/code/pkg/business/introspection.go +++ b/module/services/accounts/code/pkg/business/introspection.go @@ -318,6 +318,7 @@ var rpcDescriptions = withWorkContextConsumerDescriptions(map[string]string{ "UsageService/GetUsageHistory": "UTC hourly, daily, or monthly usage buckets for a bounded range.", "UsageService/GetUsage": "Current monthly meter total and effective limit.", "UsageService/ListUsageMeters": "Customer-visible meter catalog with current totals and limits.", + "WorkContextService/AdmitWorkloadOperation": "Answer whether a capability presented with no bearer at a module route is a module operation context for it, consuming nothing.", "WorkContextService/ExchangeAudience": "Reissue one Task and Session lineage for another audience with attenuated authority.", "WorkContextService/RenewWorkContext": "Let the current delegated actor extend its own Work Context past the TTL cap, attenuation-preserving.", "WorkContextService/StartChildSession": "Exchange a current Work Context for an attenuated child-agent Session.", diff --git a/module/services/accounts/code/pkg/business/rpc_policy_inventory_test.go b/module/services/accounts/code/pkg/business/rpc_policy_inventory_test.go index fb3bff57e..bb526ac80 100644 --- a/module/services/accounts/code/pkg/business/rpc_policy_inventory_test.go +++ b/module/services/accounts/code/pkg/business/rpc_policy_inventory_test.go @@ -84,6 +84,7 @@ func TestRPCPolicyInventoryIsCompleteAndClassified(t *testing.T) { "/saas.accounts.v1.SolutionRegistryService/ListSolutionHostBindings", "/saas.accounts.v1.SolutionRegistryService/ListSolutionRegistrations", "/saas.accounts.v1.UsageService/ConsumeUsage", + "/saas.accounts.v1.WorkContextService/AdmitWorkloadOperation", "/saas.accounts.v1.WorkContextService/AuthorizeEvidenceRead", "/saas.accounts.v1.WorkContextService/CheckAuthorizationRevision", "/saas.accounts.v1.WorkContextService/ConsumeSingleUse", diff --git a/module/services/accounts/code/pkg/cataloggen/authz_methods_test.go b/module/services/accounts/code/pkg/cataloggen/authz_methods_test.go index 8b939969a..a4e678b84 100644 --- a/module/services/accounts/code/pkg/cataloggen/authz_methods_test.go +++ b/module/services/accounts/code/pkg/cataloggen/authz_methods_test.go @@ -45,10 +45,11 @@ func TestAuthorizationCatalogCompilationAndPolicyProjection(t *testing.T) { // declared-presence RPC this branch adds. Base 49, main +4, this branch +1; // then -2 when MintModuleRegistration and MintSolutionRegistration were // deleted with their secrets (#1056), and +1 for the live-authority read a - // callee's verifier asks (#1107) — picking either side's number would be - // silently wrong, so this is read off the regenerated - // generated/authz-methods.json. - require.Equal(t, 54, internalCount) // includes the internal retained-frontend read + // callee's verifier asks (#1107), and +1 for the issuer's admission of a + // bearer-less module route's capability (AdmitWorkloadOperation). Picking + // either side's number would be silently wrong, so this is read off the + // regenerated generated/authz-methods.json. + require.Equal(t, 55, internalCount) // includes the internal retained-frontend read require.Equal(t, 17, failClosedCount) require.Equal(t, 2, factorAttemptCount) require.Equal( diff --git a/module/services/accounts/code/pkg/cataloggen/testdata/mesh-policy.golden.yaml b/module/services/accounts/code/pkg/cataloggen/testdata/mesh-policy.golden.yaml index 9941cbfa8..9c85361fc 100644 --- a/module/services/accounts/code/pkg/cataloggen/testdata/mesh-policy.golden.yaml +++ b/module/services/accounts/code/pkg/cataloggen/testdata/mesh-policy.golden.yaml @@ -78,6 +78,7 @@ spec: - /saas.accounts.v1.SolutionRegistryService/ListSolutionHostBindings - /saas.accounts.v1.SolutionRegistryService/ListSolutionRegistrations - /saas.accounts.v1.UsageService/ConsumeUsage + - /saas.accounts.v1.WorkContextService/AdmitWorkloadOperation - /saas.accounts.v1.WorkContextService/AuthorizeEvidenceRead - /saas.accounts.v1.WorkContextService/CheckAuthorizationRevision - /saas.accounts.v1.WorkContextService/ConsumeSingleUse diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/installations.connect.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/installations.connect.go index b9b0f2d38..dd97d47e2 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/installations.connect.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/installations.connect.go @@ -106,9 +106,9 @@ type InstallationServiceClient interface { // PermissionService.ListAccessibleScopes for that reason, the two read together // to answer what a viewer's organization installed and what that viewer was // granted. Its declared module route's bearer-less arm asks about the tenant an - // edge-verified Work Context names, a value this issuer signed, to admit a - // headless operation context only when the target is installed there and the - // context is sealed to another active installation. + // edge-inspected Work Context names, a value this issuer signed, to admit a + // headless operation context only when the target is installed there; whether + // the context is one is WorkContextService/AdmitWorkloadOperation's answer. ListInstallations(context.Context, *connect.Request[v1.ListInstallationsRequest]) (*connect.Response[v1.ListInstallationsResponse], error) // ListAvailableSolutions is the catalogue: the solution targets an // administrator may install right now. @@ -353,9 +353,9 @@ type InstallationServiceHandler interface { // PermissionService.ListAccessibleScopes for that reason, the two read together // to answer what a viewer's organization installed and what that viewer was // granted. Its declared module route's bearer-less arm asks about the tenant an - // edge-verified Work Context names, a value this issuer signed, to admit a - // headless operation context only when the target is installed there and the - // context is sealed to another active installation. + // edge-inspected Work Context names, a value this issuer signed, to admit a + // headless operation context only when the target is installed there; whether + // the context is one is WorkContextService/AdmitWorkloadOperation's answer. ListInstallations(context.Context, *connect.Request[v1.ListInstallationsRequest]) (*connect.Response[v1.ListInstallationsResponse], error) // ListAvailableSolutions is the catalogue: the solution targets an // administrator may install right now. diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/work_contexts.connect.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/work_contexts.connect.go index d22009b07..698c883d4 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/work_contexts.connect.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/work_contexts.connect.go @@ -44,6 +44,9 @@ const ( // WorkContextServiceConsumeSingleUseProcedure is the fully-qualified name of the // WorkContextService's ConsumeSingleUse RPC. WorkContextServiceConsumeSingleUseProcedure = "/saas.accounts.v1.WorkContextService/ConsumeSingleUse" + // WorkContextServiceAdmitWorkloadOperationProcedure is the fully-qualified name of the + // WorkContextService's AdmitWorkloadOperation RPC. + WorkContextServiceAdmitWorkloadOperationProcedure = "/saas.accounts.v1.WorkContextService/AdmitWorkloadOperation" // WorkContextServiceStartTaskProcedure is the fully-qualified name of the WorkContextService's // StartTask RPC. WorkContextServiceStartTaskProcedure = "/saas.accounts.v1.WorkContextService/StartTask" @@ -83,6 +86,17 @@ type WorkContextServiceClient interface { // a replay and returns ALREADY_EXISTS. A caller must treat that as the // capability being spent and fail the operation closed, not retry it. ConsumeSingleUse(context.Context, *connect.Request[v1.ConsumeSingleUseWorkContextRequest]) (*connect.Response[emptypb.Empty], error) + // AdmitWorkloadOperation answers whether a capability presented with no + // bearer at the declared module route /v1//* is a module operation + // context (StartModuleOperationTask) for that alias, and admits nothing else. + // The auth-gateway's bearer-less arm is its caller. Unlike the revision + // checks above, the signed capability itself crosses this boundary: the + // answer depends on its owner, owner kind and chain, which only the issuer + // reads. It inspects (signature under this issuer, window, audience) and + // reads its own claims; it never verifies, so nothing is consumed and the + // callee still verifies the capability at full strength. Internal-tier: the + // tenant it answers about is the one the capability names. + AdmitWorkloadOperation(context.Context, *connect.Request[v1.AdmitWorkloadOperationRequest]) (*connect.Response[v1.AdmitWorkloadOperationResponse], error) StartTask(context.Context, *connect.Request[v1.StartTaskWorkContextRequest]) (*connect.Response[v1.IssuedWorkContext], error) // StartInstallationTask is the headless mint. Unlike StartTask it opens with a // service credential rather than a bearer: it resolves the installation's owner @@ -136,6 +150,12 @@ func NewWorkContextServiceClient(httpClient connect.HTTPClient, baseURL string, connect.WithSchema(workContextServiceMethods.ByName("ConsumeSingleUse")), connect.WithClientOptions(opts...), ), + admitWorkloadOperation: connect.NewClient[v1.AdmitWorkloadOperationRequest, v1.AdmitWorkloadOperationResponse]( + httpClient, + baseURL+WorkContextServiceAdmitWorkloadOperationProcedure, + connect.WithSchema(workContextServiceMethods.ByName("AdmitWorkloadOperation")), + connect.WithClientOptions(opts...), + ), startTask: connect.NewClient[v1.StartTaskWorkContextRequest, v1.IssuedWorkContext]( httpClient, baseURL+WorkContextServiceStartTaskProcedure, @@ -180,6 +200,7 @@ type workContextServiceClient struct { checkAuthorizationRevision *connect.Client[v1.CheckAuthorizationRevisionRequest, emptypb.Empty] authorizeEvidenceRead *connect.Client[v1.AuthorizeEvidenceReadRequest, emptypb.Empty] consumeSingleUse *connect.Client[v1.ConsumeSingleUseWorkContextRequest, emptypb.Empty] + admitWorkloadOperation *connect.Client[v1.AdmitWorkloadOperationRequest, v1.AdmitWorkloadOperationResponse] startTask *connect.Client[v1.StartTaskWorkContextRequest, v1.IssuedWorkContext] startInstallationTask *connect.Client[v1.StartInstallationTaskRequest, v1.IssuedWorkContext] startRootSession *connect.Client[v1.StartRootSessionWorkContextRequest, v1.IssuedWorkContext] @@ -203,6 +224,11 @@ func (c *workContextServiceClient) ConsumeSingleUse(ctx context.Context, req *co return c.consumeSingleUse.CallUnary(ctx, req) } +// AdmitWorkloadOperation calls saas.accounts.v1.WorkContextService.AdmitWorkloadOperation. +func (c *workContextServiceClient) AdmitWorkloadOperation(ctx context.Context, req *connect.Request[v1.AdmitWorkloadOperationRequest]) (*connect.Response[v1.AdmitWorkloadOperationResponse], error) { + return c.admitWorkloadOperation.CallUnary(ctx, req) +} + // StartTask calls saas.accounts.v1.WorkContextService.StartTask. func (c *workContextServiceClient) StartTask(ctx context.Context, req *connect.Request[v1.StartTaskWorkContextRequest]) (*connect.Response[v1.IssuedWorkContext], error) { return c.startTask.CallUnary(ctx, req) @@ -253,6 +279,17 @@ type WorkContextServiceHandler interface { // a replay and returns ALREADY_EXISTS. A caller must treat that as the // capability being spent and fail the operation closed, not retry it. ConsumeSingleUse(context.Context, *connect.Request[v1.ConsumeSingleUseWorkContextRequest]) (*connect.Response[emptypb.Empty], error) + // AdmitWorkloadOperation answers whether a capability presented with no + // bearer at the declared module route /v1//* is a module operation + // context (StartModuleOperationTask) for that alias, and admits nothing else. + // The auth-gateway's bearer-less arm is its caller. Unlike the revision + // checks above, the signed capability itself crosses this boundary: the + // answer depends on its owner, owner kind and chain, which only the issuer + // reads. It inspects (signature under this issuer, window, audience) and + // reads its own claims; it never verifies, so nothing is consumed and the + // callee still verifies the capability at full strength. Internal-tier: the + // tenant it answers about is the one the capability names. + AdmitWorkloadOperation(context.Context, *connect.Request[v1.AdmitWorkloadOperationRequest]) (*connect.Response[v1.AdmitWorkloadOperationResponse], error) StartTask(context.Context, *connect.Request[v1.StartTaskWorkContextRequest]) (*connect.Response[v1.IssuedWorkContext], error) // StartInstallationTask is the headless mint. Unlike StartTask it opens with a // service credential rather than a bearer: it resolves the installation's owner @@ -302,6 +339,12 @@ func NewWorkContextServiceHandler(svc WorkContextServiceHandler, opts ...connect connect.WithSchema(workContextServiceMethods.ByName("ConsumeSingleUse")), connect.WithHandlerOptions(opts...), ) + workContextServiceAdmitWorkloadOperationHandler := connect.NewUnaryHandler( + WorkContextServiceAdmitWorkloadOperationProcedure, + svc.AdmitWorkloadOperation, + connect.WithSchema(workContextServiceMethods.ByName("AdmitWorkloadOperation")), + connect.WithHandlerOptions(opts...), + ) workContextServiceStartTaskHandler := connect.NewUnaryHandler( WorkContextServiceStartTaskProcedure, svc.StartTask, @@ -346,6 +389,8 @@ func NewWorkContextServiceHandler(svc WorkContextServiceHandler, opts ...connect workContextServiceAuthorizeEvidenceReadHandler.ServeHTTP(w, r) case WorkContextServiceConsumeSingleUseProcedure: workContextServiceConsumeSingleUseHandler.ServeHTTP(w, r) + case WorkContextServiceAdmitWorkloadOperationProcedure: + workContextServiceAdmitWorkloadOperationHandler.ServeHTTP(w, r) case WorkContextServiceStartTaskProcedure: workContextServiceStartTaskHandler.ServeHTTP(w, r) case WorkContextServiceStartInstallationTaskProcedure: @@ -379,6 +424,10 @@ func (UnimplementedWorkContextServiceHandler) ConsumeSingleUse(context.Context, return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.WorkContextService.ConsumeSingleUse is not implemented")) } +func (UnimplementedWorkContextServiceHandler) AdmitWorkloadOperation(context.Context, *connect.Request[v1.AdmitWorkloadOperationRequest]) (*connect.Response[v1.AdmitWorkloadOperationResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.WorkContextService.AdmitWorkloadOperation is not implemented")) +} + func (UnimplementedWorkContextServiceHandler) StartTask(context.Context, *connect.Request[v1.StartTaskWorkContextRequest]) (*connect.Response[v1.IssuedWorkContext], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.WorkContextService.StartTask is not implemented")) } diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations_grpc.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations_grpc.pb.go index 25f190592..9344b4e28 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations_grpc.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations_grpc.pb.go @@ -76,9 +76,9 @@ type InstallationServiceClient interface { // PermissionService.ListAccessibleScopes for that reason, the two read together // to answer what a viewer's organization installed and what that viewer was // granted. Its declared module route's bearer-less arm asks about the tenant an - // edge-verified Work Context names, a value this issuer signed, to admit a - // headless operation context only when the target is installed there and the - // context is sealed to another active installation. + // edge-inspected Work Context names, a value this issuer signed, to admit a + // headless operation context only when the target is installed there; whether + // the context is one is WorkContextService/AdmitWorkloadOperation's answer. ListInstallations(ctx context.Context, in *ListInstallationsRequest, opts ...grpc.CallOption) (*ListInstallationsResponse, error) // ListAvailableSolutions is the catalogue: the solution targets an // administrator may install right now. @@ -294,9 +294,9 @@ type InstallationServiceServer interface { // PermissionService.ListAccessibleScopes for that reason, the two read together // to answer what a viewer's organization installed and what that viewer was // granted. Its declared module route's bearer-less arm asks about the tenant an - // edge-verified Work Context names, a value this issuer signed, to admit a - // headless operation context only when the target is installed there and the - // context is sealed to another active installation. + // edge-inspected Work Context names, a value this issuer signed, to admit a + // headless operation context only when the target is installed there; whether + // the context is one is WorkContextService/AdmitWorkloadOperation's answer. ListInstallations(context.Context, *ListInstallationsRequest) (*ListInstallationsResponse, error) // ListAvailableSolutions is the catalogue: the solution targets an // administrator may install right now. diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/work_contexts.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/work_contexts.pb.go index 6de6b4e1a..14e6fd15c 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/work_contexts.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/work_contexts.pb.go @@ -1196,7 +1196,7 @@ var File_saas_accounts_v1_work_contexts_proto protoreflect.FileDescriptor const file_saas_accounts_v1_work_contexts_proto_rawDesc = "" + "\n" + - "$saas/accounts/v1/work_contexts.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1csaas/policy/v1/options.proto\"\xad\x01\n" + + "$saas/accounts/v1/work_contexts.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a3saas/accounts/v1/workload_operation_admission.proto\x1a\x1csaas/policy/v1/options.proto\"\xad\x01\n" + "\x10WorkContextScope\x12/\n" + "\rresource_kind\x18\x01 \x01(\tB\n" + "\xbaH\ar\x05\x10\x01\x18\x80\x01R\fresourceKind\x12/\n" + @@ -1346,11 +1346,12 @@ const file_saas_accounts_v1_work_contexts_proto_rawDesc = "" + "\x17WorkContextReplayPolicy\x12*\n" + "&WORK_CONTEXT_REPLAY_POLICY_UNSPECIFIED\x10\x00\x12)\n" + "%WORK_CONTEXT_REPLAY_POLICY_IDEMPOTENT\x10\x01\x12)\n" + - "%WORK_CONTEXT_REPLAY_POLICY_SINGLE_USE\x10\x022\xfb\f\n" + + "%WORK_CONTEXT_REPLAY_POLICY_SINGLE_USE\x10\x022\x93\x0e\n" + "\x12WorkContextService\x12\x83\x01\n" + "\x1aCheckAuthorizationRevision\x123.saas.accounts.v1.CheckAuthorizationRevisionRequest\x1a\x16.google.protobuf.Empty\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01\x12y\n" + "\x15AuthorizeEvidenceRead\x12..saas.accounts.v1.AuthorizeEvidenceReadRequest\x1a\x16.google.protobuf.Empty\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01\x12z\n" + - "\x10ConsumeSingleUse\x124.saas.accounts.v1.ConsumeSingleUseWorkContextRequest\x1a\x16.google.protobuf.Empty\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01\x12\xca\x01\n" + + "\x10ConsumeSingleUse\x124.saas.accounts.v1.ConsumeSingleUseWorkContextRequest\x1a\x16.google.protobuf.Empty\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01\x12\x95\x01\n" + + "\x16AdmitWorkloadOperation\x12/.saas.accounts.v1.AdmitWorkloadOperationRequest\x1a0.saas.accounts.v1.AdmitWorkloadOperationResponse\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01\x12\xca\x01\n" + "\tStartTask\x12-.saas.accounts.v1.StartTaskWorkContextRequest\x1a#.saas.accounts.v1.IssuedWorkContext\"i\xc2\xf3\x18D\b\x02\x10\x03*\f\n" + "\x06org_id\x10\x02\x18\x010\x01:\"\n" + "\x1esaas.work_context.task_started\x10\x02@\x01H\x05P\x03X\x04`\x01x\x02\x82\xd3\xe4\x93\x02\x1b:\x01*\"\x16/v1/work-contexts:task\x12\x86\x01\n" + @@ -1398,7 +1399,9 @@ var file_saas_accounts_v1_work_contexts_proto_goTypes = []any{ (*AuthorizeEvidenceReadRequest)(nil), // 11: saas.accounts.v1.AuthorizeEvidenceReadRequest (*ConsumeSingleUseWorkContextRequest)(nil), // 12: saas.accounts.v1.ConsumeSingleUseWorkContextRequest (*timestamppb.Timestamp)(nil), // 13: google.protobuf.Timestamp - (*emptypb.Empty)(nil), // 14: google.protobuf.Empty + (*AdmitWorkloadOperationRequest)(nil), // 14: saas.accounts.v1.AdmitWorkloadOperationRequest + (*emptypb.Empty)(nil), // 15: google.protobuf.Empty + (*AdmitWorkloadOperationResponse)(nil), // 16: saas.accounts.v1.AdmitWorkloadOperationResponse } var file_saas_accounts_v1_work_contexts_proto_depIdxs = []int32{ 1, // 0: saas.accounts.v1.StartTaskWorkContextRequest.authority_scopes:type_name -> saas.accounts.v1.WorkContextScope @@ -1419,23 +1422,25 @@ var file_saas_accounts_v1_work_contexts_proto_depIdxs = []int32{ 10, // 15: saas.accounts.v1.WorkContextService.CheckAuthorizationRevision:input_type -> saas.accounts.v1.CheckAuthorizationRevisionRequest 11, // 16: saas.accounts.v1.WorkContextService.AuthorizeEvidenceRead:input_type -> saas.accounts.v1.AuthorizeEvidenceReadRequest 12, // 17: saas.accounts.v1.WorkContextService.ConsumeSingleUse:input_type -> saas.accounts.v1.ConsumeSingleUseWorkContextRequest - 2, // 18: saas.accounts.v1.WorkContextService.StartTask:input_type -> saas.accounts.v1.StartTaskWorkContextRequest - 3, // 19: saas.accounts.v1.WorkContextService.StartInstallationTask:input_type -> saas.accounts.v1.StartInstallationTaskRequest - 4, // 20: saas.accounts.v1.WorkContextService.StartRootSession:input_type -> saas.accounts.v1.StartRootSessionWorkContextRequest - 5, // 21: saas.accounts.v1.WorkContextService.ExchangeAudience:input_type -> saas.accounts.v1.ExchangeWorkContextAudienceRequest - 6, // 22: saas.accounts.v1.WorkContextService.StartChildSession:input_type -> saas.accounts.v1.StartChildSessionWorkContextRequest - 7, // 23: saas.accounts.v1.WorkContextService.RenewWorkContext:input_type -> saas.accounts.v1.RenewWorkContextRequest - 14, // 24: saas.accounts.v1.WorkContextService.CheckAuthorizationRevision:output_type -> google.protobuf.Empty - 14, // 25: saas.accounts.v1.WorkContextService.AuthorizeEvidenceRead:output_type -> google.protobuf.Empty - 14, // 26: saas.accounts.v1.WorkContextService.ConsumeSingleUse:output_type -> google.protobuf.Empty - 8, // 27: saas.accounts.v1.WorkContextService.StartTask:output_type -> saas.accounts.v1.IssuedWorkContext - 8, // 28: saas.accounts.v1.WorkContextService.StartInstallationTask:output_type -> saas.accounts.v1.IssuedWorkContext - 8, // 29: saas.accounts.v1.WorkContextService.StartRootSession:output_type -> saas.accounts.v1.IssuedWorkContext - 8, // 30: saas.accounts.v1.WorkContextService.ExchangeAudience:output_type -> saas.accounts.v1.IssuedWorkContext - 8, // 31: saas.accounts.v1.WorkContextService.StartChildSession:output_type -> saas.accounts.v1.IssuedWorkContext - 8, // 32: saas.accounts.v1.WorkContextService.RenewWorkContext:output_type -> saas.accounts.v1.IssuedWorkContext - 24, // [24:33] is the sub-list for method output_type - 15, // [15:24] is the sub-list for method input_type + 14, // 18: saas.accounts.v1.WorkContextService.AdmitWorkloadOperation:input_type -> saas.accounts.v1.AdmitWorkloadOperationRequest + 2, // 19: saas.accounts.v1.WorkContextService.StartTask:input_type -> saas.accounts.v1.StartTaskWorkContextRequest + 3, // 20: saas.accounts.v1.WorkContextService.StartInstallationTask:input_type -> saas.accounts.v1.StartInstallationTaskRequest + 4, // 21: saas.accounts.v1.WorkContextService.StartRootSession:input_type -> saas.accounts.v1.StartRootSessionWorkContextRequest + 5, // 22: saas.accounts.v1.WorkContextService.ExchangeAudience:input_type -> saas.accounts.v1.ExchangeWorkContextAudienceRequest + 6, // 23: saas.accounts.v1.WorkContextService.StartChildSession:input_type -> saas.accounts.v1.StartChildSessionWorkContextRequest + 7, // 24: saas.accounts.v1.WorkContextService.RenewWorkContext:input_type -> saas.accounts.v1.RenewWorkContextRequest + 15, // 25: saas.accounts.v1.WorkContextService.CheckAuthorizationRevision:output_type -> google.protobuf.Empty + 15, // 26: saas.accounts.v1.WorkContextService.AuthorizeEvidenceRead:output_type -> google.protobuf.Empty + 15, // 27: saas.accounts.v1.WorkContextService.ConsumeSingleUse:output_type -> google.protobuf.Empty + 16, // 28: saas.accounts.v1.WorkContextService.AdmitWorkloadOperation:output_type -> saas.accounts.v1.AdmitWorkloadOperationResponse + 8, // 29: saas.accounts.v1.WorkContextService.StartTask:output_type -> saas.accounts.v1.IssuedWorkContext + 8, // 30: saas.accounts.v1.WorkContextService.StartInstallationTask:output_type -> saas.accounts.v1.IssuedWorkContext + 8, // 31: saas.accounts.v1.WorkContextService.StartRootSession:output_type -> saas.accounts.v1.IssuedWorkContext + 8, // 32: saas.accounts.v1.WorkContextService.ExchangeAudience:output_type -> saas.accounts.v1.IssuedWorkContext + 8, // 33: saas.accounts.v1.WorkContextService.StartChildSession:output_type -> saas.accounts.v1.IssuedWorkContext + 8, // 34: saas.accounts.v1.WorkContextService.RenewWorkContext:output_type -> saas.accounts.v1.IssuedWorkContext + 25, // [25:35] is the sub-list for method output_type + 15, // [15:25] is the sub-list for method input_type 15, // [15:15] is the sub-list for extension type_name 15, // [15:15] is the sub-list for extension extendee 0, // [0:15] is the sub-list for field type_name @@ -1446,6 +1451,7 @@ func file_saas_accounts_v1_work_contexts_proto_init() { if File_saas_accounts_v1_work_contexts_proto != nil { return } + file_saas_accounts_v1_workload_operation_admission_proto_init() file_saas_accounts_v1_work_contexts_proto_msgTypes[1].OneofWrappers = []any{} file_saas_accounts_v1_work_contexts_proto_msgTypes[2].OneofWrappers = []any{} file_saas_accounts_v1_work_contexts_proto_msgTypes[6].OneofWrappers = []any{} diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/work_contexts_grpc.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/work_contexts_grpc.pb.go index c9a02bcdc..4497085cc 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/work_contexts_grpc.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/work_contexts_grpc.pb.go @@ -24,6 +24,7 @@ const ( WorkContextService_CheckAuthorizationRevision_FullMethodName = "/saas.accounts.v1.WorkContextService/CheckAuthorizationRevision" WorkContextService_AuthorizeEvidenceRead_FullMethodName = "/saas.accounts.v1.WorkContextService/AuthorizeEvidenceRead" WorkContextService_ConsumeSingleUse_FullMethodName = "/saas.accounts.v1.WorkContextService/ConsumeSingleUse" + WorkContextService_AdmitWorkloadOperation_FullMethodName = "/saas.accounts.v1.WorkContextService/AdmitWorkloadOperation" WorkContextService_StartTask_FullMethodName = "/saas.accounts.v1.WorkContextService/StartTask" WorkContextService_StartInstallationTask_FullMethodName = "/saas.accounts.v1.WorkContextService/StartInstallationTask" WorkContextService_StartRootSession_FullMethodName = "/saas.accounts.v1.WorkContextService/StartRootSession" @@ -57,6 +58,17 @@ type WorkContextServiceClient interface { // a replay and returns ALREADY_EXISTS. A caller must treat that as the // capability being spent and fail the operation closed, not retry it. ConsumeSingleUse(ctx context.Context, in *ConsumeSingleUseWorkContextRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + // AdmitWorkloadOperation answers whether a capability presented with no + // bearer at the declared module route /v1//* is a module operation + // context (StartModuleOperationTask) for that alias, and admits nothing else. + // The auth-gateway's bearer-less arm is its caller. Unlike the revision + // checks above, the signed capability itself crosses this boundary: the + // answer depends on its owner, owner kind and chain, which only the issuer + // reads. It inspects (signature under this issuer, window, audience) and + // reads its own claims; it never verifies, so nothing is consumed and the + // callee still verifies the capability at full strength. Internal-tier: the + // tenant it answers about is the one the capability names. + AdmitWorkloadOperation(ctx context.Context, in *AdmitWorkloadOperationRequest, opts ...grpc.CallOption) (*AdmitWorkloadOperationResponse, error) StartTask(ctx context.Context, in *StartTaskWorkContextRequest, opts ...grpc.CallOption) (*IssuedWorkContext, error) // StartInstallationTask is the headless mint. Unlike StartTask it opens with a // service credential rather than a bearer: it resolves the installation's owner @@ -119,6 +131,16 @@ func (c *workContextServiceClient) ConsumeSingleUse(ctx context.Context, in *Con return out, nil } +func (c *workContextServiceClient) AdmitWorkloadOperation(ctx context.Context, in *AdmitWorkloadOperationRequest, opts ...grpc.CallOption) (*AdmitWorkloadOperationResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AdmitWorkloadOperationResponse) + err := c.cc.Invoke(ctx, WorkContextService_AdmitWorkloadOperation_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *workContextServiceClient) StartTask(ctx context.Context, in *StartTaskWorkContextRequest, opts ...grpc.CallOption) (*IssuedWorkContext, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(IssuedWorkContext) @@ -204,6 +226,17 @@ type WorkContextServiceServer interface { // a replay and returns ALREADY_EXISTS. A caller must treat that as the // capability being spent and fail the operation closed, not retry it. ConsumeSingleUse(context.Context, *ConsumeSingleUseWorkContextRequest) (*emptypb.Empty, error) + // AdmitWorkloadOperation answers whether a capability presented with no + // bearer at the declared module route /v1//* is a module operation + // context (StartModuleOperationTask) for that alias, and admits nothing else. + // The auth-gateway's bearer-less arm is its caller. Unlike the revision + // checks above, the signed capability itself crosses this boundary: the + // answer depends on its owner, owner kind and chain, which only the issuer + // reads. It inspects (signature under this issuer, window, audience) and + // reads its own claims; it never verifies, so nothing is consumed and the + // callee still verifies the capability at full strength. Internal-tier: the + // tenant it answers about is the one the capability names. + AdmitWorkloadOperation(context.Context, *AdmitWorkloadOperationRequest) (*AdmitWorkloadOperationResponse, error) StartTask(context.Context, *StartTaskWorkContextRequest) (*IssuedWorkContext, error) // StartInstallationTask is the headless mint. Unlike StartTask it opens with a // service credential rather than a bearer: it resolves the installation's owner @@ -245,6 +278,9 @@ func (UnimplementedWorkContextServiceServer) AuthorizeEvidenceRead(context.Conte func (UnimplementedWorkContextServiceServer) ConsumeSingleUse(context.Context, *ConsumeSingleUseWorkContextRequest) (*emptypb.Empty, error) { return nil, status.Error(codes.Unimplemented, "method ConsumeSingleUse not implemented") } +func (UnimplementedWorkContextServiceServer) AdmitWorkloadOperation(context.Context, *AdmitWorkloadOperationRequest) (*AdmitWorkloadOperationResponse, error) { + return nil, status.Error(codes.Unimplemented, "method AdmitWorkloadOperation not implemented") +} func (UnimplementedWorkContextServiceServer) StartTask(context.Context, *StartTaskWorkContextRequest) (*IssuedWorkContext, error) { return nil, status.Error(codes.Unimplemented, "method StartTask not implemented") } @@ -338,6 +374,24 @@ func _WorkContextService_ConsumeSingleUse_Handler(srv interface{}, ctx context.C return interceptor(ctx, in, info, handler) } +func _WorkContextService_AdmitWorkloadOperation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AdmitWorkloadOperationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WorkContextServiceServer).AdmitWorkloadOperation(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: WorkContextService_AdmitWorkloadOperation_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WorkContextServiceServer).AdmitWorkloadOperation(ctx, req.(*AdmitWorkloadOperationRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _WorkContextService_StartTask_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(StartTaskWorkContextRequest) if err := dec(in); err != nil { @@ -465,6 +519,10 @@ var WorkContextService_ServiceDesc = grpc.ServiceDesc{ MethodName: "ConsumeSingleUse", Handler: _WorkContextService_ConsumeSingleUse_Handler, }, + { + MethodName: "AdmitWorkloadOperation", + Handler: _WorkContextService_AdmitWorkloadOperation_Handler, + }, { MethodName: "StartTask", Handler: _WorkContextService_StartTask_Handler, diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/workload_operation_admission.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/workload_operation_admission.pb.go new file mode 100644 index 000000000..ac7e67d52 --- /dev/null +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/workload_operation_admission.pb.go @@ -0,0 +1,309 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: saas/accounts/v1/workload_operation_admission.proto + +package accountsv1 + +import ( + reflect "reflect" + sync "sync" + unsafe "unsafe" + + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// WorkloadOperationRefusal is why a capability was not admitted. The gateway +// logs it and answers every refusal alike. +type WorkloadOperationRefusal int32 + +const ( + WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED WorkloadOperationRefusal = 0 + // Not a capability this issuer signed, inside its window, addressed to the + // alias; or the alias is one no operation context is minted for. + WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED WorkloadOperationRefusal = 1 + // Owned by a principal that is not a service: a person, an agent, or a kind + // this issuer does not name. + WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE WorkloadOperationRefusal = 2 + // A service principal's capability that is not shaped as a module operation + // context: it carries an actor chain, a grant hop, an operation binding, a + // parent session or an owner agent, names no authority scope, or has another + // replay policy or a longer window. + WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_OPERATION_CONTEXT WorkloadOperationRefusal = 3 + // Owned by the module principal the alias names: a module does not reach + // itself through this route. + WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET WorkloadOperationRefusal = 4 + // The owner is not a declared module principal, or the seal is not an ACTIVE + // installation in the tenant installed under that principal. + WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS WorkloadOperationRefusal = 5 +) + +// Enum value maps for WorkloadOperationRefusal. +var ( + WorkloadOperationRefusal_name = map[int32]string{ + 0: "WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED", + 1: "WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED", + 2: "WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE", + 3: "WORKLOAD_OPERATION_REFUSAL_NOT_OPERATION_CONTEXT", + 4: "WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET", + 5: "WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS", + } + WorkloadOperationRefusal_value = map[string]int32{ + "WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED": 0, + "WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED": 1, + "WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE": 2, + "WORKLOAD_OPERATION_REFUSAL_NOT_OPERATION_CONTEXT": 3, + "WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET": 4, + "WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS": 5, + } +) + +func (x WorkloadOperationRefusal) Enum() *WorkloadOperationRefusal { + p := new(WorkloadOperationRefusal) + *p = x + return p +} + +func (x WorkloadOperationRefusal) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (WorkloadOperationRefusal) Descriptor() protoreflect.EnumDescriptor { + return file_saas_accounts_v1_workload_operation_admission_proto_enumTypes[0].Descriptor() +} + +func (WorkloadOperationRefusal) Type() protoreflect.EnumType { + return &file_saas_accounts_v1_workload_operation_admission_proto_enumTypes[0] +} + +func (x WorkloadOperationRefusal) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use WorkloadOperationRefusal.Descriptor instead. +func (WorkloadOperationRefusal) EnumDescriptor() ([]byte, []int) { + return file_saas_accounts_v1_workload_operation_admission_proto_rawDescGZIP(), []int{0} +} + +type AdmitWorkloadOperationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The capability as presented, encoded. It is inspected and read, never + // verified: nothing it carries is consumed. + WorkContext string `protobuf:"bytes,1,opt,name=work_context,json=workContext,proto3" json:"work_context,omitempty"` + // The route alias the gateway resolved from the request path. The capability + // must be addressed to it. + Alias string `protobuf:"bytes,2,opt,name=alias,proto3" json:"alias,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdmitWorkloadOperationRequest) Reset() { + *x = AdmitWorkloadOperationRequest{} + mi := &file_saas_accounts_v1_workload_operation_admission_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdmitWorkloadOperationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdmitWorkloadOperationRequest) ProtoMessage() {} + +func (x *AdmitWorkloadOperationRequest) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_workload_operation_admission_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AdmitWorkloadOperationRequest.ProtoReflect.Descriptor instead. +func (*AdmitWorkloadOperationRequest) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_workload_operation_admission_proto_rawDescGZIP(), []int{0} +} + +func (x *AdmitWorkloadOperationRequest) GetWorkContext() string { + if x != nil { + return x.WorkContext + } + return "" +} + +func (x *AdmitWorkloadOperationRequest) GetAlias() string { + if x != nil { + return x.Alias + } + return "" +} + +type AdmitWorkloadOperationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Admitted bool `protobuf:"varint,1,opt,name=admitted,proto3" json:"admitted,omitempty"` + // Set when admitted is false. + Refusal WorkloadOperationRefusal `protobuf:"varint,2,opt,name=refusal,proto3,enum=saas.accounts.v1.WorkloadOperationRefusal" json:"refusal,omitempty"` + // The signed facts the gateway logs and forwards, set only when admitted: + // the tenant, the installation the capability is sealed to (the CALLING + // module's), and the task. + TenantId string `protobuf:"bytes,3,opt,name=tenant_id,json=tenantId,proto3" json:"tenant_id,omitempty"` + InstallationId string `protobuf:"bytes,4,opt,name=installation_id,json=installationId,proto3" json:"installation_id,omitempty"` + TaskId string `protobuf:"bytes,5,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdmitWorkloadOperationResponse) Reset() { + *x = AdmitWorkloadOperationResponse{} + mi := &file_saas_accounts_v1_workload_operation_admission_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdmitWorkloadOperationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdmitWorkloadOperationResponse) ProtoMessage() {} + +func (x *AdmitWorkloadOperationResponse) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_workload_operation_admission_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AdmitWorkloadOperationResponse.ProtoReflect.Descriptor instead. +func (*AdmitWorkloadOperationResponse) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_workload_operation_admission_proto_rawDescGZIP(), []int{1} +} + +func (x *AdmitWorkloadOperationResponse) GetAdmitted() bool { + if x != nil { + return x.Admitted + } + return false +} + +func (x *AdmitWorkloadOperationResponse) GetRefusal() WorkloadOperationRefusal { + if x != nil { + return x.Refusal + } + return WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED +} + +func (x *AdmitWorkloadOperationResponse) GetTenantId() string { + if x != nil { + return x.TenantId + } + return "" +} + +func (x *AdmitWorkloadOperationResponse) GetInstallationId() string { + if x != nil { + return x.InstallationId + } + return "" +} + +func (x *AdmitWorkloadOperationResponse) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +var File_saas_accounts_v1_workload_operation_admission_proto protoreflect.FileDescriptor + +const file_saas_accounts_v1_workload_operation_admission_proto_rawDesc = "" + + "\n" + + "3saas/accounts/v1/workload_operation_admission.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\"q\n" + + "\x1dAdmitWorkloadOperationRequest\x12.\n" + + "\fwork_context\x18\x01 \x01(\tB\v\xbaH\br\x06\x10\x01\x18\x80\x80\x02R\vworkContext\x12 \n" + + "\x05alias\x18\x02 \x01(\tB\n" + + "\xbaH\ar\x05\x10\x01\x18\x80\x04R\x05alias\"\xe1\x01\n" + + "\x1eAdmitWorkloadOperationResponse\x12\x1a\n" + + "\badmitted\x18\x01 \x01(\bR\badmitted\x12D\n" + + "\arefusal\x18\x02 \x01(\x0e2*.saas.accounts.v1.WorkloadOperationRefusalR\arefusal\x12\x1b\n" + + "\ttenant_id\x18\x03 \x01(\tR\btenantId\x12'\n" + + "\x0finstallation_id\x18\x04 \x01(\tR\x0einstallationId\x12\x17\n" + + "\atask_id\x18\x05 \x01(\tR\x06taskId*\xbc\x02\n" + + "\x18WorkloadOperationRefusal\x12*\n" + + "&WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED\x10\x00\x12,\n" + + "(WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED\x10\x01\x120\n" + + ",WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE\x10\x02\x124\n" + + "0WORKLOAD_OPERATION_REFUSAL_NOT_OPERATION_CONTEXT\x10\x03\x12.\n" + + "*WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET\x10\x04\x12.\n" + + "*WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS\x10\x05B\xc7\x01\n" + + "\x14com.saas.accounts.v1B\x1fWorkloadOperationAdmissionProtoP\x01Z,accounts/pkg/gen/saas/accounts/v1;accountsv1\xa2\x02\x03SAX\xaa\x02\x10Saas.Accounts.V1\xca\x02\x10Saas\\Accounts\\V1\xe2\x02\x1cSaas\\Accounts\\V1\\GPBMetadata\xea\x02\x12Saas::Accounts::V1b\x06proto3" + +var ( + file_saas_accounts_v1_workload_operation_admission_proto_rawDescOnce sync.Once + file_saas_accounts_v1_workload_operation_admission_proto_rawDescData []byte +) + +func file_saas_accounts_v1_workload_operation_admission_proto_rawDescGZIP() []byte { + file_saas_accounts_v1_workload_operation_admission_proto_rawDescOnce.Do(func() { + file_saas_accounts_v1_workload_operation_admission_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_workload_operation_admission_proto_rawDesc), len(file_saas_accounts_v1_workload_operation_admission_proto_rawDesc))) + }) + return file_saas_accounts_v1_workload_operation_admission_proto_rawDescData +} + +var file_saas_accounts_v1_workload_operation_admission_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_saas_accounts_v1_workload_operation_admission_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_saas_accounts_v1_workload_operation_admission_proto_goTypes = []any{ + (WorkloadOperationRefusal)(0), // 0: saas.accounts.v1.WorkloadOperationRefusal + (*AdmitWorkloadOperationRequest)(nil), // 1: saas.accounts.v1.AdmitWorkloadOperationRequest + (*AdmitWorkloadOperationResponse)(nil), // 2: saas.accounts.v1.AdmitWorkloadOperationResponse +} +var file_saas_accounts_v1_workload_operation_admission_proto_depIdxs = []int32{ + 0, // 0: saas.accounts.v1.AdmitWorkloadOperationResponse.refusal:type_name -> saas.accounts.v1.WorkloadOperationRefusal + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_saas_accounts_v1_workload_operation_admission_proto_init() } +func file_saas_accounts_v1_workload_operation_admission_proto_init() { + if File_saas_accounts_v1_workload_operation_admission_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_workload_operation_admission_proto_rawDesc), len(file_saas_accounts_v1_workload_operation_admission_proto_rawDesc)), + NumEnums: 1, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_saas_accounts_v1_workload_operation_admission_proto_goTypes, + DependencyIndexes: file_saas_accounts_v1_workload_operation_admission_proto_depIdxs, + EnumInfos: file_saas_accounts_v1_workload_operation_admission_proto_enumTypes, + MessageInfos: file_saas_accounts_v1_workload_operation_admission_proto_msgTypes, + }.Build() + File_saas_accounts_v1_workload_operation_admission_proto = out.File + file_saas_accounts_v1_workload_operation_admission_proto_goTypes = nil + file_saas_accounts_v1_workload_operation_admission_proto_depIdxs = nil +} diff --git a/module/services/accounts/generated/api-contract-surfaces.json b/module/services/accounts/generated/api-contract-surfaces.json index 4b33a0d58..dd933f193 100644 --- a/module/services/accounts/generated/api-contract-surfaces.json +++ b/module/services/accounts/generated/api-contract-surfaces.json @@ -521,6 +521,7 @@ "name": "WorkContextService", "fullName": "saas.accounts.v1.WorkContextService", "procedures": [ + "/saas.accounts.v1.WorkContextService/AdmitWorkloadOperation", "/saas.accounts.v1.WorkContextService/AuthorizeEvidenceRead", "/saas.accounts.v1.WorkContextService/CheckAuthorizationRevision", "/saas.accounts.v1.WorkContextService/ConsumeSingleUse", diff --git a/module/services/accounts/generated/authz-methods.json b/module/services/accounts/generated/authz-methods.json index 2f0ea79fb..1f9a184b2 100644 --- a/module/services/accounts/generated/authz-methods.json +++ b/module/services/accounts/generated/authz-methods.json @@ -10780,6 +10780,43 @@ "policy_sha256": "19d51fded6d0d21c195a1527d70784808afabf8a04b4f234431586184d94b8cd", "rate_limit_backend_failure_mode": "RATE_LIMIT_BACKEND_FAILURE_MODE_FAIL_OPEN" }, + { + "procedure": "/saas.accounts.v1.WorkContextService/AdmitWorkloadOperation", + "full_service": "saas.accounts.v1.WorkContextService", + "method": "AdmitWorkloadOperation", + "input_type": "saas.accounts.v1.AdmitWorkloadOperationRequest", + "output_type": "saas.accounts.v1.AdmitWorkloadOperationResponse", + "client_streaming": false, + "server_streaming": false, + "policy": { + "exposure": "EXPOSURE_INTERNAL", + "tenant": "TENANT_REQUIREMENT_NONE", + "permissions": [], + "scopes": [], + "resource_bindings": [], + "mfa": "MFA_REQUIREMENT_NONE", + "audit": { + "events": [], + "emission": "AUDIT_EMISSION_NONE" + }, + "idempotency": "IDEMPOTENCY_REQUIREMENT_FORBIDDEN", + "rate_limit": "RATE_LIMIT_CLASS_INTERNAL", + "request_sensitivity": "SENSITIVITY_CONFIDENTIAL", + "response_sensitivity": "SENSITIVITY_CONFIDENTIAL", + "platform_role": "PLATFORM_ROLE_REQUIREMENT_NONE", + "authentication_factor_attempt": false, + "conditions": [], + "impersonation": "IMPERSONATION_REQUIREMENT_UNSPECIFIED" + }, + "policy_tier": "internal", + "owner": { + "module": "saas-starter", + "service": "accounts" + }, + "source_proto": "saas/accounts/v1/work_contexts.proto", + "policy_sha256": "45df71bfa7f313b565b0e8c314557847bab913990d43ba59b97d755412b4a4fd", + "rate_limit_backend_failure_mode": "RATE_LIMIT_BACKEND_FAILURE_MODE_FAIL_OPEN" + }, { "procedure": "/saas.accounts.v1.WorkContextService/AuthorizeEvidenceRead", "full_service": "saas.accounts.v1.WorkContextService", diff --git a/module/services/accounts/generated/service-catalog.json b/module/services/accounts/generated/service-catalog.json index 142c0b4c8..9300de8a8 100644 --- a/module/services/accounts/generated/service-catalog.json +++ b/module/services/accounts/generated/service-catalog.json @@ -482,6 +482,7 @@ "name": "WorkContextService", "full_name": "saas.accounts.v1.WorkContextService", "procedures": [ + "/saas.accounts.v1.WorkContextService/AdmitWorkloadOperation", "/saas.accounts.v1.WorkContextService/AuthorizeEvidenceRead", "/saas.accounts.v1.WorkContextService/CheckAuthorizationRevision", "/saas.accounts.v1.WorkContextService/ConsumeSingleUse", @@ -14178,6 +14179,49 @@ "service": "accounts" } }, + { + "procedure": "/saas.accounts.v1.WorkContextService/AdmitWorkloadOperation", + "service": "WorkContextService", + "full_service": "saas.accounts.v1.WorkContextService", + "method": "AdmitWorkloadOperation", + "input_type": "saas.accounts.v1.AdmitWorkloadOperationRequest", + "output_type": "saas.accounts.v1.AdmitWorkloadOperationResponse", + "client_streaming": false, + "server_streaming": false, + "deprecated": false, + "source_proto": "saas/accounts/v1/work_contexts.proto", + "protocols": [ + "PROTOCOL_GRPC", + "PROTOCOL_CONNECT" + ], + "http_bindings": [], + "policy": { + "exposure": "EXPOSURE_INTERNAL", + "tenant": "TENANT_REQUIREMENT_NONE", + "permissions": [], + "scopes": [], + "resource_bindings": [], + "mfa": "MFA_REQUIREMENT_NONE", + "audit": { + "events": [], + "emission": "AUDIT_EMISSION_NONE" + }, + "idempotency": "IDEMPOTENCY_REQUIREMENT_FORBIDDEN", + "rate_limit": "RATE_LIMIT_CLASS_INTERNAL", + "request_sensitivity": "SENSITIVITY_CONFIDENTIAL", + "response_sensitivity": "SENSITIVITY_CONFIDENTIAL", + "platform_role": "PLATFORM_ROLE_REQUIREMENT_NONE", + "authentication_factor_attempt": false, + "conditions": [], + "impersonation": "IMPERSONATION_REQUIREMENT_UNSPECIFIED" + }, + "policy_tier": "internal", + "description": "Answer whether a capability presented with no bearer at a module route is a module operation context for it, consuming nothing.", + "owner": { + "module": "saas-starter", + "service": "accounts" + } + }, { "procedure": "/saas.accounts.v1.WorkContextService/AuthorizeEvidenceRead", "service": "WorkContextService", diff --git a/module/services/accounts/proto/saas/accounts/v1/installations.proto b/module/services/accounts/proto/saas/accounts/v1/installations.proto index 1149a82cf..9fc95dd86 100644 --- a/module/services/accounts/proto/saas/accounts/v1/installations.proto +++ b/module/services/accounts/proto/saas/accounts/v1/installations.proto @@ -324,9 +324,9 @@ service InstallationService { // PermissionService.ListAccessibleScopes for that reason, the two read together // to answer what a viewer's organization installed and what that viewer was // granted. Its declared module route's bearer-less arm asks about the tenant an - // edge-verified Work Context names, a value this issuer signed, to admit a - // headless operation context only when the target is installed there and the - // context is sealed to another active installation. + // edge-inspected Work Context names, a value this issuer signed, to admit a + // headless operation context only when the target is installed there; whether + // the context is one is WorkContextService/AdmitWorkloadOperation's answer. rpc ListInstallations(ListInstallationsRequest) returns (ListInstallationsResponse) { option (saas.policy.v1.method_policy) = { exposure: EXPOSURE_INTERNAL diff --git a/module/services/accounts/proto/saas/accounts/v1/work_contexts.proto b/module/services/accounts/proto/saas/accounts/v1/work_contexts.proto index c5db6a13c..8a645eea6 100644 --- a/module/services/accounts/proto/saas/accounts/v1/work_contexts.proto +++ b/module/services/accounts/proto/saas/accounts/v1/work_contexts.proto @@ -6,6 +6,7 @@ import "buf/validate/validate.proto"; import "google/api/annotations.proto"; import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; +import "saas/accounts/v1/workload_operation_admission.proto"; import "saas/policy/v1/options.proto"; // WorkContextReplayPolicy controls how a consumer may accept a signed Work @@ -397,6 +398,30 @@ service WorkContextService { }; } + // AdmitWorkloadOperation answers whether a capability presented with no + // bearer at the declared module route /v1//* is a module operation + // context (StartModuleOperationTask) for that alias, and admits nothing else. + // The auth-gateway's bearer-less arm is its caller. Unlike the revision + // checks above, the signed capability itself crosses this boundary: the + // answer depends on its owner, owner kind and chain, which only the issuer + // reads. It inspects (signature under this issuer, window, audience) and + // reads its own claims; it never verifies, so nothing is consumed and the + // callee still verifies the capability at full strength. Internal-tier: the + // tenant it answers about is the one the capability names. + rpc AdmitWorkloadOperation(AdmitWorkloadOperationRequest) returns (AdmitWorkloadOperationResponse) { + option (saas.policy.v1.method_policy) = { + exposure: EXPOSURE_INTERNAL + tenant: TENANT_REQUIREMENT_NONE + mfa: MFA_REQUIREMENT_NONE + audit: {emission: AUDIT_EMISSION_NONE} + idempotency: IDEMPOTENCY_REQUIREMENT_FORBIDDEN + rate_limit: RATE_LIMIT_CLASS_INTERNAL + request_sensitivity: SENSITIVITY_CONFIDENTIAL + response_sensitivity: SENSITIVITY_CONFIDENTIAL + platform_role: PLATFORM_ROLE_REQUIREMENT_NONE + }; + } + rpc StartTask(StartTaskWorkContextRequest) returns (IssuedWorkContext) { option (saas.policy.v1.method_policy) = { exposure: EXPOSURE_AUTHENTICATED diff --git a/module/services/accounts/proto/saas/accounts/v1/workload_operation_admission.proto b/module/services/accounts/proto/saas/accounts/v1/workload_operation_admission.proto new file mode 100644 index 000000000..dda95d168 --- /dev/null +++ b/module/services/accounts/proto/saas/accounts/v1/workload_operation_admission.proto @@ -0,0 +1,71 @@ +syntax = "proto3"; + +package saas.accounts.v1; + +import "buf/validate/validate.proto"; + +// Workload operation admission: the issuer's answer to whether a capability +// presented with no bearer at a declared module route is a module operation +// context, the one shape that route admits without a person. +// +// A forwarding hop cannot answer it. Core's Inspector, which is all a hop runs, +// withholds the owner, the owner's kind, the actor chain and the operation +// binding, and those are exactly what tell a module's unattended work from a +// person's capability. Accounts signed the capability and can read its own +// claims, so the gateway asks it. WorkContextService/AdmitWorkloadOperation +// takes and answers these messages. +// +// They live in their own file, with deliberately minimal imports, so the +// auth-gateway can generate the same types from the same source proto, as it +// does for installation_listing.proto and solution_entitlements.proto. The +// gateway's copy is this file, byte for byte. + +// WorkloadOperationRefusal is why a capability was not admitted. The gateway +// logs it and answers every refusal alike. +enum WorkloadOperationRefusal { + WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED = 0; + // Not a capability this issuer signed, inside its window, addressed to the + // alias; or the alias is one no operation context is minted for. + WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED = 1; + // Owned by a principal that is not a service: a person, an agent, or a kind + // this issuer does not name. + WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE = 2; + // A service principal's capability that is not shaped as a module operation + // context: it carries an actor chain, a grant hop, an operation binding, a + // parent session or an owner agent, names no authority scope, or has another + // replay policy or a longer window. + WORKLOAD_OPERATION_REFUSAL_NOT_OPERATION_CONTEXT = 3; + // Owned by the module principal the alias names: a module does not reach + // itself through this route. + WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET = 4; + // The owner is not a declared module principal, or the seal is not an ACTIVE + // installation in the tenant installed under that principal. + WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS = 5; +} + +message AdmitWorkloadOperationRequest { + // The capability as presented, encoded. It is inspected and read, never + // verified: nothing it carries is consumed. + string work_context = 1 [ + (buf.validate.field).string.min_len = 1, + (buf.validate.field).string.max_len = 32768 + ]; + // The route alias the gateway resolved from the request path. The capability + // must be addressed to it. + string alias = 2 [ + (buf.validate.field).string.min_len = 1, + (buf.validate.field).string.max_len = 512 + ]; +} + +message AdmitWorkloadOperationResponse { + bool admitted = 1; + // Set when admitted is false. + WorkloadOperationRefusal refusal = 2; + // The signed facts the gateway logs and forwards, set only when admitted: + // the tenant, the installation the capability is sealed to (the CALLING + // module's), and the task. + string tenant_id = 3; + string installation_id = 4; + string task_id = 5; +} diff --git a/module/services/auth-gateway/code/authz_catalog_gen.go b/module/services/auth-gateway/code/authz_catalog_gen.go index 8ded00ad5..d2eaa4154 100644 --- a/module/services/auth-gateway/code/authz_catalog_gen.go +++ b/module/services/auth-gateway/code/authz_catalog_gen.go @@ -272,6 +272,7 @@ var generatedAuthorizationByProcedure = map[string]generatedAuthorizationMetadat "/saas.accounts.v1.WebhookService/ReplayDelivery": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassStandardWrite, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "6e8ef6cf8a1443cd9b313d1008c8c623516101a8a9ec04147ec1ca62805c67dd"}, "/saas.accounts.v1.WebhookService/RotateSecret": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassSensitive, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "73e7544e3f55a695533a1e6016b97a694d0c1fe3029dc8e35c78abbe8c129752"}, "/saas.accounts.v1.WebhookService/TestWebhook": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassStandardWrite, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "19d51fded6d0d21c195a1527d70784808afabf8a04b4f234431586184d94b8cd"}, + "/saas.accounts.v1.WorkContextService/AdmitWorkloadOperation": {exposure: edgeExposureInternal, rateLimitClass: edgeRateLimitClassInternal, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "45df71bfa7f313b565b0e8c314557847bab913990d43ba59b97d755412b4a4fd"}, "/saas.accounts.v1.WorkContextService/AuthorizeEvidenceRead": {exposure: edgeExposureInternal, rateLimitClass: edgeRateLimitClassInternal, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "45df71bfa7f313b565b0e8c314557847bab913990d43ba59b97d755412b4a4fd"}, "/saas.accounts.v1.WorkContextService/CheckAuthorizationRevision": {exposure: edgeExposureInternal, rateLimitClass: edgeRateLimitClassInternal, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "45df71bfa7f313b565b0e8c314557847bab913990d43ba59b97d755412b4a4fd"}, "/saas.accounts.v1.WorkContextService/ConsumeSingleUse": {exposure: edgeExposureInternal, rateLimitClass: edgeRateLimitClassInternal, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "45df71bfa7f313b565b0e8c314557847bab913990d43ba59b97d755412b4a4fd"}, diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json index ea8874bfa..1c252ed2d 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json +++ b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json @@ -1,7 +1,7 @@ { "schema": "codefly/module-api-contracts/v1", "package": "codefly/saas-starter", - "version": "0.1.0", + "version": "0.2.0", "endpoints": [ { "service": "accounts", @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contract.binpb", - "digest": "sha256:8e59a8dabb889185a44cb9e0d8c9cdc4fbef03b27e7c01f2cc581c69d2e8799a", + "digest": "sha256:6100316ada259d0f5b29458a686ca0dd430948b6770b3c0d69d2153716ce57d7", "services": [ { "name": "APIKeyService", @@ -487,6 +487,7 @@ "name": "WorkContextService", "fullName": "saas.accounts.v1.WorkContextService", "procedures": [ + "/saas.accounts.v1.WorkContextService/AdmitWorkloadOperation", "/saas.accounts.v1.WorkContextService/AuthorizeEvidenceRead", "/saas.accounts.v1.WorkContextService/CheckAuthorizationRevision", "/saas.accounts.v1.WorkContextService/ConsumeSingleUse", diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb b/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb index 97ec3eafb88bdfc60cf59cd9d2e7d9bebc4f5f7a..7f8edac8b9482f9e0df01754708965358210c69b 100644 GIT binary patch delta 15869 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 b{xw@>Wbm&S{`JPcKKOS;_m&ynzgPb^i@Xm{ delta 11319 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/* is a module operation + * context (StartModuleOperationTask) for that alias, and admits nothing else. + * The auth-gateway's bearer-less arm is its caller. Unlike the revision + * checks above, the signed capability itself crosses this boundary: the + * answer depends on its owner, owner kind and chain, which only the issuer + * reads. It inspects (signature under this issuer, window, audience) and + * reads its own claims; it never verifies, so nothing is consumed and the + * callee still verifies the capability at full strength. Internal-tier: the + * tenant it answers about is the one the capability names. + * + * @generated from rpc saas.accounts.v1.WorkContextService.AdmitWorkloadOperation + */ + admitWorkloadOperation: { + methodKind: "unary"; + input: typeof AdmitWorkloadOperationRequestSchema; + output: typeof AdmitWorkloadOperationResponseSchema; + }, /** * @generated from rpc saas.accounts.v1.WorkContextService.StartTask */ diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/workload_operation_admission_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/workload_operation_admission_pb.ts new file mode 100644 index 000000000..3d3dbf6a3 --- /dev/null +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/workload_operation_admission_pb.ts @@ -0,0 +1,146 @@ +// @generated by protoc-gen-es v2.11.0 with parameter "target=ts,import_extension=none" +// @generated from file saas/accounts/v1/workload_operation_admission.proto (package saas.accounts.v1, syntax proto3) +/* eslint-disable */ + +import type { GenEnum, GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import { enumDesc, fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; +import { file_buf_validate_validate } from "../../../buf/validate/validate_pb"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file saas/accounts/v1/workload_operation_admission.proto. + */ +export const file_saas_accounts_v1_workload_operation_admission: GenFile = /*@__PURE__*/ + fileDesc("CjNzYWFzL2FjY291bnRzL3YxL3dvcmtsb2FkX29wZXJhdGlvbl9hZG1pc3Npb24ucHJvdG8SEHNhYXMuYWNjb3VudHMudjEiXQodQWRtaXRXb3JrbG9hZE9wZXJhdGlvblJlcXVlc3QSIQoMd29ya19jb250ZXh0GAEgASgJQgu6SAhyBhABGICAAhIZCgVhbGlhcxgCIAEoCUIKukgHcgUQARiABCKsAQoeQWRtaXRXb3JrbG9hZE9wZXJhdGlvblJlc3BvbnNlEhAKCGFkbWl0dGVkGAEgASgIEjsKB3JlZnVzYWwYAiABKA4yKi5zYWFzLmFjY291bnRzLnYxLldvcmtsb2FkT3BlcmF0aW9uUmVmdXNhbBIRCgl0ZW5hbnRfaWQYAyABKAkSFwoPaW5zdGFsbGF0aW9uX2lkGAQgASgJEg8KB3Rhc2tfaWQYBSABKAkqvAIKGFdvcmtsb2FkT3BlcmF0aW9uUmVmdXNhbBIqCiZXT1JLTE9BRF9PUEVSQVRJT05fUkVGVVNBTF9VTlNQRUNJRklFRBAAEiwKKFdPUktMT0FEX09QRVJBVElPTl9SRUZVU0FMX05PVF9JTlNQRUNURUQQARIwCixXT1JLTE9BRF9PUEVSQVRJT05fUkVGVVNBTF9PV05FUl9OT1RfU0VSVklDRRACEjQKMFdPUktMT0FEX09QRVJBVElPTl9SRUZVU0FMX05PVF9PUEVSQVRJT05fQ09OVEVYVBADEi4KKldPUktMT0FEX09QRVJBVElPTl9SRUZVU0FMX09XTkVSX0lTX1RBUkdFVBAEEi4KKldPUktMT0FEX09QRVJBVElPTl9SRUZVU0FMX1NFQUxfTk9UX09XTkVSUxAFYgZwcm90bzM", [file_buf_validate_validate]); + +/** + * @generated from message saas.accounts.v1.AdmitWorkloadOperationRequest + */ +export type AdmitWorkloadOperationRequest = Message<"saas.accounts.v1.AdmitWorkloadOperationRequest"> & { + /** + * The capability as presented, encoded. It is inspected and read, never + * verified: nothing it carries is consumed. + * + * @generated from field: string work_context = 1; + */ + workContext: string; + + /** + * The route alias the gateway resolved from the request path. The capability + * must be addressed to it. + * + * @generated from field: string alias = 2; + */ + alias: string; +}; + +/** + * Describes the message saas.accounts.v1.AdmitWorkloadOperationRequest. + * Use `create(AdmitWorkloadOperationRequestSchema)` to create a new message. + */ +export const AdmitWorkloadOperationRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_workload_operation_admission, 0); + +/** + * @generated from message saas.accounts.v1.AdmitWorkloadOperationResponse + */ +export type AdmitWorkloadOperationResponse = Message<"saas.accounts.v1.AdmitWorkloadOperationResponse"> & { + /** + * @generated from field: bool admitted = 1; + */ + admitted: boolean; + + /** + * Set when admitted is false. + * + * @generated from field: saas.accounts.v1.WorkloadOperationRefusal refusal = 2; + */ + refusal: WorkloadOperationRefusal; + + /** + * The signed facts the gateway logs and forwards, set only when admitted: + * the tenant, the installation the capability is sealed to (the CALLING + * module's), and the task. + * + * @generated from field: string tenant_id = 3; + */ + tenantId: string; + + /** + * @generated from field: string installation_id = 4; + */ + installationId: string; + + /** + * @generated from field: string task_id = 5; + */ + taskId: string; +}; + +/** + * Describes the message saas.accounts.v1.AdmitWorkloadOperationResponse. + * Use `create(AdmitWorkloadOperationResponseSchema)` to create a new message. + */ +export const AdmitWorkloadOperationResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_workload_operation_admission, 1); + +/** + * WorkloadOperationRefusal is why a capability was not admitted. The gateway + * logs it and answers every refusal alike. + * + * @generated from enum saas.accounts.v1.WorkloadOperationRefusal + */ +export enum WorkloadOperationRefusal { + /** + * @generated from enum value: WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * Not a capability this issuer signed, inside its window, addressed to the + * alias; or the alias is one no operation context is minted for. + * + * @generated from enum value: WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED = 1; + */ + NOT_INSPECTED = 1, + + /** + * Owned by a principal that is not a service: a person, an agent, or a kind + * this issuer does not name. + * + * @generated from enum value: WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE = 2; + */ + OWNER_NOT_SERVICE = 2, + + /** + * A service principal's capability that is not shaped as a module operation + * context: it carries an actor chain, a grant hop, an operation binding, a + * parent session or an owner agent, names no authority scope, or has another + * replay policy or a longer window. + * + * @generated from enum value: WORKLOAD_OPERATION_REFUSAL_NOT_OPERATION_CONTEXT = 3; + */ + NOT_OPERATION_CONTEXT = 3, + + /** + * Owned by the module principal the alias names: a module does not reach + * itself through this route. + * + * @generated from enum value: WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET = 4; + */ + OWNER_IS_TARGET = 4, + + /** + * The owner is not a declared module principal, or the seal is not an ACTIVE + * installation in the tenant installed under that principal. + * + * @generated from enum value: WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS = 5; + */ + SEAL_NOT_OWNERS = 5, +} + +/** + * Describes the enum saas.accounts.v1.WorkloadOperationRefusal. + */ +export const WorkloadOperationRefusalSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_saas_accounts_v1_workload_operation_admission, 0); diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/installations_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/installations_pb.ts index beb3d2ec5..82f82346a 100644 --- a/module/services/frontend/code/src/gen/saas/accounts/v1/installations_pb.ts +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/installations_pb.ts @@ -1152,9 +1152,9 @@ export const InstallationService: GenService<{ * PermissionService.ListAccessibleScopes for that reason, the two read together * to answer what a viewer's organization installed and what that viewer was * granted. Its declared module route's bearer-less arm asks about the tenant an - * edge-verified Work Context names, a value this issuer signed, to admit a - * headless operation context only when the target is installed there and the - * context is sealed to another active installation. + * edge-inspected Work Context names, a value this issuer signed, to admit a + * headless operation context only when the target is installed there; whether + * the context is one is WorkContextService/AdmitWorkloadOperation's answer. * * @generated from rpc saas.accounts.v1.InstallationService.ListInstallations */ diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/work_contexts_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/work_contexts_pb.ts index 1ae3625b3..0097dfc2e 100644 --- a/module/services/frontend/code/src/gen/saas/accounts/v1/work_contexts_pb.ts +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/work_contexts_pb.ts @@ -8,6 +8,8 @@ import { file_buf_validate_validate } from "../../../buf/validate/validate_pb"; import { file_google_api_annotations } from "../../../google/api/annotations_pb"; import type { EmptySchema, Timestamp } from "@bufbuild/protobuf/wkt"; import { file_google_protobuf_empty, file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; +import type { AdmitWorkloadOperationRequestSchema, AdmitWorkloadOperationResponseSchema } from "./workload_operation_admission_pb"; +import { file_saas_accounts_v1_workload_operation_admission } from "./workload_operation_admission_pb"; import { file_saas_policy_v1_options } from "../../policy/v1/options_pb"; import type { Message } from "@bufbuild/protobuf"; @@ -15,7 +17,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/work_contexts.proto. */ export const file_saas_accounts_v1_work_contexts: GenFile = /*@__PURE__*/ - fileDesc("CiRzYWFzL2FjY291bnRzL3YxL3dvcmtfY29udGV4dHMucHJvdG8SEHNhYXMuYWNjb3VudHMudjEiiQEKEFdvcmtDb250ZXh0U2NvcGUSIQoNcmVzb3VyY2Vfa2luZBgBIAEoCUIKukgHcgUQARiAARImCgdhY3Rpb25zGAIgAygJQhW6SBKSAQ8IARBAGAEiB3IFEAEYgAESKgoMcmVzb3VyY2VfaWRzGAMgAygJQhS6SBGSAQ4QgAIYASIHcgUQARiABCK6AwobU3RhcnRUYXNrV29ya0NvbnRleHRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESHAoHdGFza19pZBgCIAEoCUILukgI2AEBcgOwAQESGgoSYWN0b3JfcHJpbmNpcGFsX2lkGAQgASgJEkgKEGF1dGhvcml0eV9zY29wZXMYBSADKAsyIi5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0U2NvcGVCCrpIB5IBBAgBEEASHAoIYXVkaWVuY2UYBiABKAlCCrpIB3IFEAEYgAQSQAoNcmVwbGF5X3BvbGljeRgHIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRSZXBsYXlQb2xpY3kSHwoLdHRsX3NlY29uZHMYCCABKAVCCrpIBxoFGIQHKAASJQoMd29ya3NwYWNlX2lkGAkgASgJQgq6SAdyBRABGIAESACIAQESIwoKcHJvamVjdF9pZBgKIAEoCUIKukgHcgUQARiABEgBiAEBQg8KDV93b3Jrc3BhY2VfaWRCDQoLX3Byb2plY3RfaWRKBAgDEARSCnNlc3Npb25faWQivwMKHFN0YXJ0SW5zdGFsbGF0aW9uVGFza1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhkKB3Rhc2tfaWQYAyABKAlCCLpIBXIDsAEBEkgKEGF1dGhvcml0eV9zY29wZXMYBSADKAsyIi5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0U2NvcGVCCrpIB5IBBAgBEEASHAoIYXVkaWVuY2UYBiABKAlCCrpIB3IFEAEYgAQSQAoNcmVwbGF5X3BvbGljeRgHIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRSZXBsYXlQb2xpY3kSHwoLdHRsX3NlY29uZHMYCCABKAVCCrpIBxoFGIQHKAASJQoMd29ya3NwYWNlX2lkGAkgASgJQgq6SAdyBRABGIAESACIAQESIwoKcHJvamVjdF9pZBgKIAEoCUIKukgHcgUQARiABEgBiAEBQg8KDV93b3Jrc3BhY2VfaWRCDQoLX3Byb2plY3RfaWRKBAgEEAVSCnNlc3Npb25faWQijQIKIlN0YXJ0Um9vdFNlc3Npb25Xb3JrQ29udGV4dFJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIuChlwYXJlbnRfd29ya19jb250ZXh0X3Rva2VuGAIgASgJQgu6SAhyBhABGICAAhIcCgpzZXNzaW9uX2lkGAMgASgJQgi6SAVyA7ABARIcCghhdWRpZW5jZRgEIAEoCUIKukgHcgUQARiABBJACg1yZXBsYXlfcG9saWN5GAUgASgOMikuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFJlcGxheVBvbGljeRIfCgt0dGxfc2Vjb25kcxgGIAEoBUIKukgHGgUYhAcoACK6AgoiRXhjaGFuZ2VXb3JrQ29udGV4dEF1ZGllbmNlUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEi4KGXBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4YAiABKAlCC7pICHIGEAEYgIACEhwKCGF1ZGllbmNlGAMgASgJQgq6SAdyBRABGIAEEkkKEWF0dGVudWF0ZWRfc2NvcGVzGAQgAygLMiIuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFNjb3BlQgq6SAeSAQQIARBAEkAKDXJlcGxheV9wb2xpY3kYBSABKA4yKS5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0UmVwbGF5UG9saWN5Eh8KC3R0bF9zZWNvbmRzGAYgASgFQgq6SAcaBRiEBygAIvACCiNTdGFydENoaWxkU2Vzc2lvbldvcmtDb250ZXh0UmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEi4KGXBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4YAiABKAlCC7pICHIGEAEYgIACEiQKEmFjdG9yX3ByaW5jaXBhbF9pZBgEIAEoCUIIukgFcgOwAQESRgoOZ3JhbnRlZF9zY29wZXMYBSADKAsyIi5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0U2NvcGVCCrpIB5IBBAgBEEASHAoIYXVkaWVuY2UYBiABKAlCCrpIB3IFEAEYgAQSQAoNcmVwbGF5X3BvbGljeRgHIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRSZXBsYXlQb2xpY3kSHwoLdHRsX3NlY29uZHMYCCABKAVCCrpIBxoFGIQHKABKBAgDEARSCnNlc3Npb25faWQivwIKF1JlbmV3V29ya0NvbnRleHRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESLgoZcGFyZW50X3dvcmtfY29udGV4dF90b2tlbhgCIAEoCUILukgIcgYQARiAgAISIQoIYXVkaWVuY2UYAyABKAlCCrpIB3IFEAEYgARIAIgBARJHChFhdHRlbnVhdGVkX3Njb3BlcxgEIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRTY29wZUIIukgFkgECEEASQAoNcmVwbGF5X3BvbGljeRgFIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRSZXBsYXlQb2xpY3kSHwoLdHRsX3NlY29uZHMYBiABKAVCCrpIBxoFGIQHKABCCwoJX2F1ZGllbmNlIvECChFJc3N1ZWRXb3JrQ29udGV4dBINCgV0b2tlbhgBIAEoCRIOCgZvcmdfaWQYAiABKAkSGgoSb3duZXJfcHJpbmNpcGFsX2lkGAMgASgJEg8KB3Rhc2tfaWQYBCABKAkSEgoKc2Vzc2lvbl9pZBgFIAEoCRIeChFwYXJlbnRfc2Vzc2lvbl9pZBgGIAEoCUgAiAEBEiIKGmN1cnJlbnRfYWN0b3JfcHJpbmNpcGFsX2lkGAcgASgJEh4KFmF1dGhvcml6YXRpb25fcmV2aXNpb24YCCABKAQSLgoKZXhwaXJlc19hdBgJIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoMd29ya3NwYWNlX2lkGAogASgJSAGIAQESFwoKcHJvamVjdF9pZBgLIAEoCUgCiAEBQhQKEl9wYXJlbnRfc2Vzc2lvbl9pZEIPCg1fd29ya3NwYWNlX2lkQg0KC19wcm9qZWN0X2lkInoKGldvcmtDb250ZXh0UmV2aXNpb25TdWJqZWN0Eh4KDHByaW5jaXBhbF9pZBgBIAEoCUIIukgFcgOwAQESPAoGc2NvcGVzGAIgAygLMiIuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFNjb3BlQgi6SAWSAQIQQCLUAgohQ2hlY2tBdXRob3JpemF0aW9uUmV2aXNpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESJAoSb3duZXJfcHJpbmNpcGFsX2lkGAIgASgJQgi6SAVyA7ABARInChZhdXRob3JpemF0aW9uX3JldmlzaW9uGAMgASgEQge6SAQyAiAAEkoKCHN1YmplY3RzGAQgAygLMiwuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFJldmlzaW9uU3ViamVjdEIKukgHkgEECAEQERItChRvcGVyYXRpb25fYmluZGluZ19pZBgFIAEoCUIKukgHcgUQARiABEgAiAEBEh0KEHNlYWxzX25vX2JpbmRpbmcYBiABKAhIAYgBAUIXChVfb3BlcmF0aW9uX2JpbmRpbmdfaWRCEwoRX3NlYWxzX25vX2JpbmRpbmcigwIKHEF1dGhvcml6ZUV2aWRlbmNlUmVhZFJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIlChNjYWxsZXJfcHJpbmNpcGFsX2lkGAIgASgJQgi6SAVyA7ABARIpChJvd25lcl9wcmluY2lwYWxfaWQYAyABKAlCCLpIBXIDsAEBSACIAQESIAoHdGFza19pZBgEIAEoCUIKukgHcgUQARiABEgBiAEBEiMKCnNlc3Npb25faWQYBSABKAlCCrpIB3IFEAEYgARIAogBAUIVChNfb3duZXJfcHJpbmNpcGFsX2lkQgoKCF90YXNrX2lkQg0KC19zZXNzaW9uX2lkIpYBCiJDb25zdW1lU2luZ2xlVXNlV29ya0NvbnRleHRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESHgoKY29udGV4dF9pZBgCIAEoCUIKukgHcgUQARiABBI2CgpleHBpcmVzX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEIGukgDyAEBKpsBChdXb3JrQ29udGV4dFJlcGxheVBvbGljeRIqCiZXT1JLX0NPTlRFWFRfUkVQTEFZX1BPTElDWV9VTlNQRUNJRklFRBAAEikKJVdPUktfQ09OVEVYVF9SRVBMQVlfUE9MSUNZX0lERU1QT1RFTlQQARIpCiVXT1JLX0NPTlRFWFRfUkVQTEFZX1BPTElDWV9TSU5HTEVfVVNFEAIy+wwKEldvcmtDb250ZXh0U2VydmljZRKDAQoaQ2hlY2tBdXRob3JpemF0aW9uUmV2aXNpb24SMy5zYWFzLmFjY291bnRzLnYxLkNoZWNrQXV0aG9yaXphdGlvblJldmlzaW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEnkKFUF1dGhvcml6ZUV2aWRlbmNlUmVhZBIuLnNhYXMuYWNjb3VudHMudjEuQXV0aG9yaXplRXZpZGVuY2VSZWFkUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEnoKEENvbnN1bWVTaW5nbGVVc2USNC5zYWFzLmFjY291bnRzLnYxLkNvbnN1bWVTaW5nbGVVc2VXb3JrQ29udGV4dFJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARLKAQoJU3RhcnRUYXNrEi0uc2Fhcy5hY2NvdW50cy52MS5TdGFydFRhc2tXb3JrQ29udGV4dFJlcXVlc3QaIy5zYWFzLmFjY291bnRzLnYxLklzc3VlZFdvcmtDb250ZXh0ImnC8xhECAIQAyoMCgZvcmdfaWQQAhgBMAE6Igoec2Fhcy53b3JrX2NvbnRleHQudGFza19zdGFydGVkEAJAAUgFUANYBGABeAKC0+STAhs6ASoiFi92MS93b3JrLWNvbnRleHRzOnRhc2sShgEKFVN0YXJ0SW5zdGFsbGF0aW9uVGFzaxIuLnNhYXMuYWNjb3VudHMudjEuU3RhcnRJbnN0YWxsYXRpb25UYXNrUmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuSXNzdWVkV29ya0NvbnRleHQiGMLzGBQIAxABMAE6AhABQAFIB1ADWARgARLoAQoQU3RhcnRSb290U2Vzc2lvbhI0LnNhYXMuYWNjb3VudHMudjEuU3RhcnRSb290U2Vzc2lvbldvcmtDb250ZXh0UmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuSXNzdWVkV29ya0NvbnRleHQiecLzGEwIAhADKgwKBm9yZ19pZBACGAEwAToqCiZzYWFzLndvcmtfY29udGV4dC5yb290X3Nlc3Npb25fc3RhcnRlZBACQAFIBVAEWARgAXgCgtPkkwIjOgEqIh4vdjEvd29yay1jb250ZXh0czpyb290LXNlc3Npb24S6wEKEEV4Y2hhbmdlQXVkaWVuY2USNC5zYWFzLmFjY291bnRzLnYxLkV4Y2hhbmdlV29ya0NvbnRleHRBdWRpZW5jZVJlcXVlc3QaIy5zYWFzLmFjY291bnRzLnYxLklzc3VlZFdvcmtDb250ZXh0InzC8xhKCAIQAyoMCgZvcmdfaWQQAhgBMAE6KAokc2Fhcy53b3JrX2NvbnRleHQuYXVkaWVuY2VfZXhjaGFuZ2VkEAJAAUgFUARYBGABeAKC0+STAig6ASoiIy92MS93b3JrLWNvbnRleHRzOmV4Y2hhbmdlLWF1ZGllbmNlEuwBChFTdGFydENoaWxkU2Vzc2lvbhI1LnNhYXMuYWNjb3VudHMudjEuU3RhcnRDaGlsZFNlc3Npb25Xb3JrQ29udGV4dFJlcXVlc3QaIy5zYWFzLmFjY291bnRzLnYxLklzc3VlZFdvcmtDb250ZXh0InvC8xhNCAIQAyoMCgZvcmdfaWQQAhgBMAE6Kwonc2Fhcy53b3JrX2NvbnRleHQuY2hpbGRfc2Vzc2lvbl9zdGFydGVkEAJAAUgFUARYBGABeAKC0+STAiQ6ASoiHy92MS93b3JrLWNvbnRleHRzOmNoaWxkLXNlc3Npb24SyQEKEFJlbmV3V29ya0NvbnRleHQSKS5zYWFzLmFjY291bnRzLnYxLlJlbmV3V29ya0NvbnRleHRSZXF1ZXN0GiMuc2Fhcy5hY2NvdW50cy52MS5Jc3N1ZWRXb3JrQ29udGV4dCJlwvMYPwgCEAMqDAoGb3JnX2lkEAIYATABOh0KGXNhYXMud29ya19jb250ZXh0LnJlbmV3ZWQQAkABSAVQBFgEYAF4AoLT5JMCHDoBKiIXL3YxL3dvcmstY29udGV4dHM6cmVuZXdCuQEKFGNvbS5zYWFzLmFjY291bnRzLnYxQhFXb3JrQ29udGV4dHNQcm90b1ABWixhY2NvdW50cy9wa2cvZ2VuL3NhYXMvYWNjb3VudHMvdjE7YWNjb3VudHN2MaICA1NBWKoCEFNhYXMuQWNjb3VudHMuVjHKAhBTYWFzXEFjY291bnRzXFYx4gIcU2Fhc1xBY2NvdW50c1xWMVxHUEJNZXRhZGF0YeoCElNhYXM6OkFjY291bnRzOjpWMWIGcHJvdG8z", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_policy_v1_options]); + fileDesc("CiRzYWFzL2FjY291bnRzL3YxL3dvcmtfY29udGV4dHMucHJvdG8SEHNhYXMuYWNjb3VudHMudjEiiQEKEFdvcmtDb250ZXh0U2NvcGUSIQoNcmVzb3VyY2Vfa2luZBgBIAEoCUIKukgHcgUQARiAARImCgdhY3Rpb25zGAIgAygJQhW6SBKSAQ8IARBAGAEiB3IFEAEYgAESKgoMcmVzb3VyY2VfaWRzGAMgAygJQhS6SBGSAQ4QgAIYASIHcgUQARiABCK6AwobU3RhcnRUYXNrV29ya0NvbnRleHRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESHAoHdGFza19pZBgCIAEoCUILukgI2AEBcgOwAQESGgoSYWN0b3JfcHJpbmNpcGFsX2lkGAQgASgJEkgKEGF1dGhvcml0eV9zY29wZXMYBSADKAsyIi5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0U2NvcGVCCrpIB5IBBAgBEEASHAoIYXVkaWVuY2UYBiABKAlCCrpIB3IFEAEYgAQSQAoNcmVwbGF5X3BvbGljeRgHIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRSZXBsYXlQb2xpY3kSHwoLdHRsX3NlY29uZHMYCCABKAVCCrpIBxoFGIQHKAASJQoMd29ya3NwYWNlX2lkGAkgASgJQgq6SAdyBRABGIAESACIAQESIwoKcHJvamVjdF9pZBgKIAEoCUIKukgHcgUQARiABEgBiAEBQg8KDV93b3Jrc3BhY2VfaWRCDQoLX3Byb2plY3RfaWRKBAgDEARSCnNlc3Npb25faWQivwMKHFN0YXJ0SW5zdGFsbGF0aW9uVGFza1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhkKB3Rhc2tfaWQYAyABKAlCCLpIBXIDsAEBEkgKEGF1dGhvcml0eV9zY29wZXMYBSADKAsyIi5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0U2NvcGVCCrpIB5IBBAgBEEASHAoIYXVkaWVuY2UYBiABKAlCCrpIB3IFEAEYgAQSQAoNcmVwbGF5X3BvbGljeRgHIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRSZXBsYXlQb2xpY3kSHwoLdHRsX3NlY29uZHMYCCABKAVCCrpIBxoFGIQHKAASJQoMd29ya3NwYWNlX2lkGAkgASgJQgq6SAdyBRABGIAESACIAQESIwoKcHJvamVjdF9pZBgKIAEoCUIKukgHcgUQARiABEgBiAEBQg8KDV93b3Jrc3BhY2VfaWRCDQoLX3Byb2plY3RfaWRKBAgEEAVSCnNlc3Npb25faWQijQIKIlN0YXJ0Um9vdFNlc3Npb25Xb3JrQ29udGV4dFJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIuChlwYXJlbnRfd29ya19jb250ZXh0X3Rva2VuGAIgASgJQgu6SAhyBhABGICAAhIcCgpzZXNzaW9uX2lkGAMgASgJQgi6SAVyA7ABARIcCghhdWRpZW5jZRgEIAEoCUIKukgHcgUQARiABBJACg1yZXBsYXlfcG9saWN5GAUgASgOMikuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFJlcGxheVBvbGljeRIfCgt0dGxfc2Vjb25kcxgGIAEoBUIKukgHGgUYhAcoACK6AgoiRXhjaGFuZ2VXb3JrQ29udGV4dEF1ZGllbmNlUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEi4KGXBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4YAiABKAlCC7pICHIGEAEYgIACEhwKCGF1ZGllbmNlGAMgASgJQgq6SAdyBRABGIAEEkkKEWF0dGVudWF0ZWRfc2NvcGVzGAQgAygLMiIuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFNjb3BlQgq6SAeSAQQIARBAEkAKDXJlcGxheV9wb2xpY3kYBSABKA4yKS5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0UmVwbGF5UG9saWN5Eh8KC3R0bF9zZWNvbmRzGAYgASgFQgq6SAcaBRiEBygAIvACCiNTdGFydENoaWxkU2Vzc2lvbldvcmtDb250ZXh0UmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEi4KGXBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4YAiABKAlCC7pICHIGEAEYgIACEiQKEmFjdG9yX3ByaW5jaXBhbF9pZBgEIAEoCUIIukgFcgOwAQESRgoOZ3JhbnRlZF9zY29wZXMYBSADKAsyIi5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0U2NvcGVCCrpIB5IBBAgBEEASHAoIYXVkaWVuY2UYBiABKAlCCrpIB3IFEAEYgAQSQAoNcmVwbGF5X3BvbGljeRgHIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRSZXBsYXlQb2xpY3kSHwoLdHRsX3NlY29uZHMYCCABKAVCCrpIBxoFGIQHKABKBAgDEARSCnNlc3Npb25faWQivwIKF1JlbmV3V29ya0NvbnRleHRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESLgoZcGFyZW50X3dvcmtfY29udGV4dF90b2tlbhgCIAEoCUILukgIcgYQARiAgAISIQoIYXVkaWVuY2UYAyABKAlCCrpIB3IFEAEYgARIAIgBARJHChFhdHRlbnVhdGVkX3Njb3BlcxgEIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRTY29wZUIIukgFkgECEEASQAoNcmVwbGF5X3BvbGljeRgFIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRSZXBsYXlQb2xpY3kSHwoLdHRsX3NlY29uZHMYBiABKAVCCrpIBxoFGIQHKABCCwoJX2F1ZGllbmNlIvECChFJc3N1ZWRXb3JrQ29udGV4dBINCgV0b2tlbhgBIAEoCRIOCgZvcmdfaWQYAiABKAkSGgoSb3duZXJfcHJpbmNpcGFsX2lkGAMgASgJEg8KB3Rhc2tfaWQYBCABKAkSEgoKc2Vzc2lvbl9pZBgFIAEoCRIeChFwYXJlbnRfc2Vzc2lvbl9pZBgGIAEoCUgAiAEBEiIKGmN1cnJlbnRfYWN0b3JfcHJpbmNpcGFsX2lkGAcgASgJEh4KFmF1dGhvcml6YXRpb25fcmV2aXNpb24YCCABKAQSLgoKZXhwaXJlc19hdBgJIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoMd29ya3NwYWNlX2lkGAogASgJSAGIAQESFwoKcHJvamVjdF9pZBgLIAEoCUgCiAEBQhQKEl9wYXJlbnRfc2Vzc2lvbl9pZEIPCg1fd29ya3NwYWNlX2lkQg0KC19wcm9qZWN0X2lkInoKGldvcmtDb250ZXh0UmV2aXNpb25TdWJqZWN0Eh4KDHByaW5jaXBhbF9pZBgBIAEoCUIIukgFcgOwAQESPAoGc2NvcGVzGAIgAygLMiIuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFNjb3BlQgi6SAWSAQIQQCLUAgohQ2hlY2tBdXRob3JpemF0aW9uUmV2aXNpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESJAoSb3duZXJfcHJpbmNpcGFsX2lkGAIgASgJQgi6SAVyA7ABARInChZhdXRob3JpemF0aW9uX3JldmlzaW9uGAMgASgEQge6SAQyAiAAEkoKCHN1YmplY3RzGAQgAygLMiwuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFJldmlzaW9uU3ViamVjdEIKukgHkgEECAEQERItChRvcGVyYXRpb25fYmluZGluZ19pZBgFIAEoCUIKukgHcgUQARiABEgAiAEBEh0KEHNlYWxzX25vX2JpbmRpbmcYBiABKAhIAYgBAUIXChVfb3BlcmF0aW9uX2JpbmRpbmdfaWRCEwoRX3NlYWxzX25vX2JpbmRpbmcigwIKHEF1dGhvcml6ZUV2aWRlbmNlUmVhZFJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIlChNjYWxsZXJfcHJpbmNpcGFsX2lkGAIgASgJQgi6SAVyA7ABARIpChJvd25lcl9wcmluY2lwYWxfaWQYAyABKAlCCLpIBXIDsAEBSACIAQESIAoHdGFza19pZBgEIAEoCUIKukgHcgUQARiABEgBiAEBEiMKCnNlc3Npb25faWQYBSABKAlCCrpIB3IFEAEYgARIAogBAUIVChNfb3duZXJfcHJpbmNpcGFsX2lkQgoKCF90YXNrX2lkQg0KC19zZXNzaW9uX2lkIpYBCiJDb25zdW1lU2luZ2xlVXNlV29ya0NvbnRleHRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESHgoKY29udGV4dF9pZBgCIAEoCUIKukgHcgUQARiABBI2CgpleHBpcmVzX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEIGukgDyAEBKpsBChdXb3JrQ29udGV4dFJlcGxheVBvbGljeRIqCiZXT1JLX0NPTlRFWFRfUkVQTEFZX1BPTElDWV9VTlNQRUNJRklFRBAAEikKJVdPUktfQ09OVEVYVF9SRVBMQVlfUE9MSUNZX0lERU1QT1RFTlQQARIpCiVXT1JLX0NPTlRFWFRfUkVQTEFZX1BPTElDWV9TSU5HTEVfVVNFEAIykw4KEldvcmtDb250ZXh0U2VydmljZRKDAQoaQ2hlY2tBdXRob3JpemF0aW9uUmV2aXNpb24SMy5zYWFzLmFjY291bnRzLnYxLkNoZWNrQXV0aG9yaXphdGlvblJldmlzaW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEnkKFUF1dGhvcml6ZUV2aWRlbmNlUmVhZBIuLnNhYXMuYWNjb3VudHMudjEuQXV0aG9yaXplRXZpZGVuY2VSZWFkUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEnoKEENvbnN1bWVTaW5nbGVVc2USNC5zYWFzLmFjY291bnRzLnYxLkNvbnN1bWVTaW5nbGVVc2VXb3JrQ29udGV4dFJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARKVAQoWQWRtaXRXb3JrbG9hZE9wZXJhdGlvbhIvLnNhYXMuYWNjb3VudHMudjEuQWRtaXRXb3JrbG9hZE9wZXJhdGlvblJlcXVlc3QaMC5zYWFzLmFjY291bnRzLnYxLkFkbWl0V29ya2xvYWRPcGVyYXRpb25SZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEsoBCglTdGFydFRhc2sSLS5zYWFzLmFjY291bnRzLnYxLlN0YXJ0VGFza1dvcmtDb250ZXh0UmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuSXNzdWVkV29ya0NvbnRleHQiacLzGEQIAhADKgwKBm9yZ19pZBACGAEwAToiCh5zYWFzLndvcmtfY29udGV4dC50YXNrX3N0YXJ0ZWQQAkABSAVQA1gEYAF4AoLT5JMCGzoBKiIWL3YxL3dvcmstY29udGV4dHM6dGFzaxKGAQoVU3RhcnRJbnN0YWxsYXRpb25UYXNrEi4uc2Fhcy5hY2NvdW50cy52MS5TdGFydEluc3RhbGxhdGlvblRhc2tSZXF1ZXN0GiMuc2Fhcy5hY2NvdW50cy52MS5Jc3N1ZWRXb3JrQ29udGV4dCIYwvMYFAgDEAEwAToCEAFAAUgHUANYBGABEugBChBTdGFydFJvb3RTZXNzaW9uEjQuc2Fhcy5hY2NvdW50cy52MS5TdGFydFJvb3RTZXNzaW9uV29ya0NvbnRleHRSZXF1ZXN0GiMuc2Fhcy5hY2NvdW50cy52MS5Jc3N1ZWRXb3JrQ29udGV4dCJ5wvMYTAgCEAMqDAoGb3JnX2lkEAIYATABOioKJnNhYXMud29ya19jb250ZXh0LnJvb3Rfc2Vzc2lvbl9zdGFydGVkEAJAAUgFUARYBGABeAKC0+STAiM6ASoiHi92MS93b3JrLWNvbnRleHRzOnJvb3Qtc2Vzc2lvbhLrAQoQRXhjaGFuZ2VBdWRpZW5jZRI0LnNhYXMuYWNjb3VudHMudjEuRXhjaGFuZ2VXb3JrQ29udGV4dEF1ZGllbmNlUmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuSXNzdWVkV29ya0NvbnRleHQifMLzGEoIAhADKgwKBm9yZ19pZBACGAEwATooCiRzYWFzLndvcmtfY29udGV4dC5hdWRpZW5jZV9leGNoYW5nZWQQAkABSAVQBFgEYAF4AoLT5JMCKDoBKiIjL3YxL3dvcmstY29udGV4dHM6ZXhjaGFuZ2UtYXVkaWVuY2US7AEKEVN0YXJ0Q2hpbGRTZXNzaW9uEjUuc2Fhcy5hY2NvdW50cy52MS5TdGFydENoaWxkU2Vzc2lvbldvcmtDb250ZXh0UmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuSXNzdWVkV29ya0NvbnRleHQie8LzGE0IAhADKgwKBm9yZ19pZBACGAEwATorCidzYWFzLndvcmtfY29udGV4dC5jaGlsZF9zZXNzaW9uX3N0YXJ0ZWQQAkABSAVQBFgEYAF4AoLT5JMCJDoBKiIfL3YxL3dvcmstY29udGV4dHM6Y2hpbGQtc2Vzc2lvbhLJAQoQUmVuZXdXb3JrQ29udGV4dBIpLnNhYXMuYWNjb3VudHMudjEuUmVuZXdXb3JrQ29udGV4dFJlcXVlc3QaIy5zYWFzLmFjY291bnRzLnYxLklzc3VlZFdvcmtDb250ZXh0ImXC8xg/CAIQAyoMCgZvcmdfaWQQAhgBMAE6HQoZc2Fhcy53b3JrX2NvbnRleHQucmVuZXdlZBACQAFIBVAEWARgAXgCgtPkkwIcOgEqIhcvdjEvd29yay1jb250ZXh0czpyZW5ld0K5AQoUY29tLnNhYXMuYWNjb3VudHMudjFCEVdvcmtDb250ZXh0c1Byb3RvUAFaLGFjY291bnRzL3BrZy9nZW4vc2Fhcy9hY2NvdW50cy92MTthY2NvdW50c3YxogIDU0FYqgIQU2Fhcy5BY2NvdW50cy5WMcoCEFNhYXNcQWNjb3VudHNcVjHiAhxTYWFzXEFjY291bnRzXFYxXEdQQk1ldGFkYXRh6gISU2Fhczo6QWNjb3VudHM6OlYxYgZwcm90bzM", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_workload_operation_admission, file_saas_policy_v1_options]); /** * WorkContextScope is one product-neutral authority slice. Product plugins @@ -723,6 +725,25 @@ export const WorkContextService: GenService<{ input: typeof ConsumeSingleUseWorkContextRequestSchema; output: typeof EmptySchema; }, + /** + * AdmitWorkloadOperation answers whether a capability presented with no + * bearer at the declared module route /v1//* is a module operation + * context (StartModuleOperationTask) for that alias, and admits nothing else. + * The auth-gateway's bearer-less arm is its caller. Unlike the revision + * checks above, the signed capability itself crosses this boundary: the + * answer depends on its owner, owner kind and chain, which only the issuer + * reads. It inspects (signature under this issuer, window, audience) and + * reads its own claims; it never verifies, so nothing is consumed and the + * callee still verifies the capability at full strength. Internal-tier: the + * tenant it answers about is the one the capability names. + * + * @generated from rpc saas.accounts.v1.WorkContextService.AdmitWorkloadOperation + */ + admitWorkloadOperation: { + methodKind: "unary"; + input: typeof AdmitWorkloadOperationRequestSchema; + output: typeof AdmitWorkloadOperationResponseSchema; + }, /** * @generated from rpc saas.accounts.v1.WorkContextService.StartTask */ diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/workload_operation_admission_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/workload_operation_admission_pb.ts new file mode 100644 index 000000000..61a7af504 --- /dev/null +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/workload_operation_admission_pb.ts @@ -0,0 +1,147 @@ +// @generated by protoc-gen-es v2.11.0 with parameter "target=ts,import_extension=none" +// @generated from file saas/accounts/v1/workload_operation_admission.proto (package saas.accounts.v1, syntax proto3) +/* eslint-disable */ + +import type { GenEnum, GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import { enumDesc, fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; +import { file_buf_validate_validate } from "../../../buf/validate/validate_pb"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file saas/accounts/v1/workload_operation_admission.proto. + */ +export const file_saas_accounts_v1_workload_operation_admission: GenFile = /*@__PURE__*/ + fileDesc("CjNzYWFzL2FjY291bnRzL3YxL3dvcmtsb2FkX29wZXJhdGlvbl9hZG1pc3Npb24ucHJvdG8SEHNhYXMuYWNjb3VudHMudjEiXQodQWRtaXRXb3JrbG9hZE9wZXJhdGlvblJlcXVlc3QSIQoMd29ya19jb250ZXh0GAEgASgJQgu6SAhyBhABGICAAhIZCgVhbGlhcxgCIAEoCUIKukgHcgUQARiABCKsAQoeQWRtaXRXb3JrbG9hZE9wZXJhdGlvblJlc3BvbnNlEhAKCGFkbWl0dGVkGAEgASgIEjsKB3JlZnVzYWwYAiABKA4yKi5zYWFzLmFjY291bnRzLnYxLldvcmtsb2FkT3BlcmF0aW9uUmVmdXNhbBIRCgl0ZW5hbnRfaWQYAyABKAkSFwoPaW5zdGFsbGF0aW9uX2lkGAQgASgJEg8KB3Rhc2tfaWQYBSABKAkqvAIKGFdvcmtsb2FkT3BlcmF0aW9uUmVmdXNhbBIqCiZXT1JLTE9BRF9PUEVSQVRJT05fUkVGVVNBTF9VTlNQRUNJRklFRBAAEiwKKFdPUktMT0FEX09QRVJBVElPTl9SRUZVU0FMX05PVF9JTlNQRUNURUQQARIwCixXT1JLTE9BRF9PUEVSQVRJT05fUkVGVVNBTF9PV05FUl9OT1RfU0VSVklDRRACEjQKMFdPUktMT0FEX09QRVJBVElPTl9SRUZVU0FMX05PVF9PUEVSQVRJT05fQ09OVEVYVBADEi4KKldPUktMT0FEX09QRVJBVElPTl9SRUZVU0FMX09XTkVSX0lTX1RBUkdFVBAEEi4KKldPUktMT0FEX09QRVJBVElPTl9SRUZVU0FMX1NFQUxfTk9UX09XTkVSUxAFQscBChRjb20uc2Fhcy5hY2NvdW50cy52MUIfV29ya2xvYWRPcGVyYXRpb25BZG1pc3Npb25Qcm90b1ABWixhY2NvdW50cy9wa2cvZ2VuL3NhYXMvYWNjb3VudHMvdjE7YWNjb3VudHN2MaICA1NBWKoCEFNhYXMuQWNjb3VudHMuVjHKAhBTYWFzXEFjY291bnRzXFYx4gIcU2Fhc1xBY2NvdW50c1xWMVxHUEJNZXRhZGF0YeoCElNhYXM6OkFjY291bnRzOjpWMWIGcHJvdG8z", [file_buf_validate_validate]); + +/** + * @generated from message saas.accounts.v1.AdmitWorkloadOperationRequest + */ +export type AdmitWorkloadOperationRequest = Message<"saas.accounts.v1.AdmitWorkloadOperationRequest"> & { + /** + * The capability as presented, encoded. It is inspected and read, never + * verified: nothing it carries is consumed. + * + * @generated from field: string work_context = 1; + */ + workContext: string; + + /** + * The route alias the gateway resolved from the request path. The capability + * must be addressed to it. + * + * @generated from field: string alias = 2; + */ + alias: string; +}; + +/** + * Describes the message saas.accounts.v1.AdmitWorkloadOperationRequest. + * Use `create(AdmitWorkloadOperationRequestSchema)` to create a new message. + */ +export const AdmitWorkloadOperationRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_workload_operation_admission, 0); + +/** + * @generated from message saas.accounts.v1.AdmitWorkloadOperationResponse + */ +export type AdmitWorkloadOperationResponse = Message<"saas.accounts.v1.AdmitWorkloadOperationResponse"> & { + /** + * @generated from field: bool admitted = 1; + */ + admitted: boolean; + + /** + * Set when admitted is false. + * + * @generated from field: saas.accounts.v1.WorkloadOperationRefusal refusal = 2; + */ + refusal: WorkloadOperationRefusal; + + /** + * The signed facts the gateway logs and forwards, set only when admitted: + * the tenant, the installation the capability is sealed to (the CALLING + * module's), and the task. + * + * @generated from field: string tenant_id = 3; + */ + tenantId: string; + + /** + * @generated from field: string installation_id = 4; + */ + installationId: string; + + /** + * @generated from field: string task_id = 5; + */ + taskId: string; +}; + +/** + * Describes the message saas.accounts.v1.AdmitWorkloadOperationResponse. + * Use `create(AdmitWorkloadOperationResponseSchema)` to create a new message. + */ +export const AdmitWorkloadOperationResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_workload_operation_admission, 1); + +/** + * WorkloadOperationRefusal is why a capability was not admitted. The gateway + * logs it and answers every refusal alike. + * + * @generated from enum saas.accounts.v1.WorkloadOperationRefusal + */ +export enum WorkloadOperationRefusal { + /** + * @generated from enum value: WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * Not a capability this issuer signed, inside its window, addressed to the + * alias; or the alias is one no operation context is minted for. + * + * @generated from enum value: WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED = 1; + */ + NOT_INSPECTED = 1, + + /** + * Owned by a principal that is not a service: a person, an agent, or a kind + * this issuer does not name. + * + * @generated from enum value: WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE = 2; + */ + OWNER_NOT_SERVICE = 2, + + /** + * A service principal's capability that is not shaped as a module operation + * context: it carries an actor chain, a grant hop, an operation binding, a + * parent session or an owner agent, names no authority scope, or has another + * replay policy or a longer window. + * + * @generated from enum value: WORKLOAD_OPERATION_REFUSAL_NOT_OPERATION_CONTEXT = 3; + */ + NOT_OPERATION_CONTEXT = 3, + + /** + * Owned by the module principal the alias names: a module does not reach + * itself through this route. + * + * @generated from enum value: WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET = 4; + */ + OWNER_IS_TARGET = 4, + + /** + * The owner is not a declared module principal, or the seal is not an ACTIVE + * installation in the tenant installed under that principal. + * + * @generated from enum value: WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS = 5; + */ + SEAL_NOT_OWNERS = 5, +} + +/** + * Describes the enum saas.accounts.v1.WorkloadOperationRefusal. + */ +export const WorkloadOperationRefusalSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_saas_accounts_v1_workload_operation_admission, 0); + From 7e0d0b0b470f0e8425d4fc7b8936457bfd8ffa3e Mon Sep 17 00:00:00 2001 From: saurabhmaheshwari-obin <242053479+saurabhmaheshwari-obin@users.noreply.github.com> Date: Sat, 24 Oct 2026 16:33:14 -0400 Subject: [PATCH 10/17] fix(auth-gateway): the bearer-less module arm admits what the issuer admits, not what the seal suggests The arm admitted a capability with no bearer when its seal named an active installation that was not the target's and was not installed under the alias's module principal. A source-operation context, an installation task or install step, and an installed binding's child (content_read, module_actor) are each a person's capability sealed exactly that way, so each was admitted without the person's bearer. The arm now asks accounts' WorkContextService/AdmitWorkloadOperation, the issuer's allow-list of the one shape it is for (StartModuleOperationTask), and decides nothing from the seal. Selection, the identity strip (with Authorization, Proxy-Authorization and X-Forwarded-Client-Cert) before the limiter, the fail-closed workload-operation: budget, the static 502 and the admit/refusal log are unchanged. Behind the limiter: - AdmitWorkloadOperation{work_context, alias}, by method name over the existing accounts connection with the internal credential, bounded at 2 seconds (the on-request lookup bound the client registry uses). A refusal is 401 "authentication required", logged with the issuer's reason; an error, a timeout, no client wired, or an answer naming another tenant or seal than the edge inspected is 503. Nothing else is asked after a refusal. - ListInstallations for the signed tenant, as before, for the target's rows only: not installed is 403, a seal on one of the target's rows is 401. x-codefly-installation-id is set once, to the seal accounts admitted. The gateway vendors workload_operation_admission.proto byte for byte (TestVendoredAccountsProtosAreAccounts covers it). modulePrincipalID and its pin are gone: the alias's own principal is refused by the issuer now. Tests: TestModuleRoute_TheBearerLessArmAdmitsOnlyWhatTheIssuerAdmits holds every WorkloadOperationRefusal to 401 with no installation read, and fails against the seal-based arm (each case answered 200 there). The refusal table gains an erroring issuer, no issuer wired and admissions naming another seal or tenant; TestAccountsWorkloadAdmissionAsksTheIssuerWithinItsBound runs the production client against a gRPC accounts double for the method and internal credential, and against one that never answers for a bounded 503. The limiter tests assert a limited or limiter-down request never reaches the issuer, and the admit test that the header carries one value, the admitted seal, with no bearer forwarded. The registry-principal cases leave with the check they tested. The gateway AGENTS.md, GATEWAY_ROUTES.md and WORK_CONTEXTS.md say the arm asks the issuer, name the guard tests, and change the release note: an older accounts does not serve AdmitWorkloadOperation, so the arm answers 503 and admits nothing until accounts is rolled. Regenerated with Codefly source candidate 5234ce24: codefly generate proto for the gateway and codefly generate contracts saas-starter. buf lint passes for the gateway's proto tree. Co-Authored-By: Claude Opus 5.5 --- module/GATEWAY_ROUTES.md | 23 +- module/WORK_CONTEXTS.md | 34 +- .../api/auth-gateway/grpc/contract.binpb | Bin 357854 -> 361671 bytes .../v1/workload_operation_admission.proto | 71 ++++ module/contracts/api/catalog.codefly.json | 2 +- module/module.package.codefly.yaml | 2 +- module/services/auth-gateway/AGENTS.md | 76 ++-- .../auth-gateway/code/attack_battery_test.go | 11 +- module/services/auth-gateway/code/gateway.go | 4 + .../code/gateway_module_confinement_test.go | 1 + .../code/gateway_module_routes_test.go | 346 +++++++++++++----- .../code/gateway_operation_context_test.go | 3 + .../code/gateway_workload_operation.go | 189 +++++----- module/services/auth-gateway/code/main.go | 3 + .../code/perimeter_workos_free_test.go | 4 +- .../v1/workload_operation_admission.pb.go | 309 ++++++++++++++++ .../v1/workload_operation_admission.proto | 71 ++++ 17 files changed, 900 insertions(+), 249 deletions(-) create mode 100644 module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/workload_operation_admission.proto create mode 100644 module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/workload_operation_admission.pb.go create mode 100644 module/services/auth-gateway/proto/saas/accounts/v1/workload_operation_admission.proto diff --git a/module/GATEWAY_ROUTES.md b/module/GATEWAY_ROUTES.md index d9991d0bd..b7b897294 100644 --- a/module/GATEWAY_ROUTES.md +++ b/module/GATEWAY_ROUTES.md @@ -129,19 +129,18 @@ there may name the alias or `module-capabilities`; anything else is 401 non-empty signed tenant and seal (else 401 `invalid work context`); every identity header stripped, `Authorization`, `Proxy-Authorization` and `X-Forwarded-Client-Cert` dropped; the budget keyed - `workload-operation:`, failing closed; and accounts' + `workload-operation:`, failing closed; then accounts' + `AdmitWorkloadOperation` must admit the capability as a module operation + context for the alias (a refusal is 401 `authentication required`, the answer + a person's capability has without its bearer), and accounts' `ListInstallations` for that tenant (ACTIVE, uncached) must show an - installation of the target (else 403) and the seal as an active installation - that is none of the target's and whose registry principal is known and is not - the alias's module principal (else 401 `authentication required`, the answer - a person's capability has without its bearer). The principal check is what - holds when a module's alias and principal differ: accounts seals a person's - capability by principal, the route resolves by alias. 503 when the authority cannot answer or - is not wired. Admitted requests carry no bearer and no identity header, and - `x-codefly-installation-id` is set to the seal, which names the CALLER's - installation, not consent on the target's side. An upstream failure is a - static 502. Every admit and refusal is logged with alias, tenant, task, seal - and presenter, never the token. + installation of the target (else 403) that is not the seal (else 401). 503 + when either cannot answer in time or is not wired, or the admission names + another tenant or seal. Admitted requests carry no bearer and no identity + header, and `x-codefly-installation-id` is set to the admitted seal, which + names the CALLER's installation, not consent on the target's side. An + upstream failure is a static 502. Every admit and refusal is logged with + alias, tenant, task, seal and presenter, never the token. Elsewhere the same request is a bearer-less request: 401 on `rest`, 401 with a `module-capabilities` capability, 401 for a capability sealed to the target's own diff --git a/module/WORK_CONTEXTS.md b/module/WORK_CONTEXTS.md index e0d63e0af..f52193d1a 100644 --- a/module/WORK_CONTEXTS.md +++ b/module/WORK_CONTEXTS.md @@ -349,7 +349,7 @@ answered by one set of functions for minting and verifying alike the audience it addresses. `StartChildSession` is the one derivation core cannot re-seal (a child keeps its parent's seal), so it refuses a person's child for an audience whose installation is not the parent's seal. - Two kinds of person-owned capability are sealed elsewhere, by design, and + Several kinds of person-owned capability are sealed elsewhere, by design, and each has its own mint gate: - **Source-operation contexts** (`startSourceOperation`, `pkg/adapters/source_delegation.go`): owned by the person, sealed to the @@ -359,12 +359,16 @@ answered by one set of functions for minting and verifying alike - **Installation tasks** (`mintInstallationTask`, `pkg/adapters/work_context_rpcs.go`): owned by the installation's owner of record, sealed to the installation the authority is drawn from. Only an - `EXPOSURE_INTERNAL` credential reaches `StartInstallationTask`. - - The gateway's bearer-less module arm admits a capability whose seal is an - active installation other than the target's, so it admits these two kinds as - well as module operation contexts: that is the unattended-work family it is - for, and no person's interactive session can produce one. + `EXPOSURE_INTERNAL` credential reaches `StartInstallationTask`, and an + install step is signed by the host itself. + - **Installed binding children** (`exchangeInstalledBinding`, + `pkg/adapters/installed_content_read.go`): a module's `content_read` or + `module_actor` exchange of a person's capability derives a core `Child`, + which keeps the parent's seal (the exchanging module's installation) while + addressing another module. + + So a seal on an installation other than the audience's does not mean + unattended work, and nothing decides that from the seal. `TestAPersonsCapabilityIsSealedToItsAudiencesInstallation` pins the rule over every person mint, and `TestEveryWorkContextMintIsClassifiedBySeal` fails on any mint RPC that names neither where it seals nor why. @@ -664,12 +668,16 @@ endpoint, with no bearer: the declared module route's bearer-less arm. A composed workload with no person present, such as a solution's release publisher, reaches the module it is bound to this way; modules keep calling their peers in-mesh. The edge checks what core's `Inspector` can (signature, -issuer, window, the route's audience, shape), then the signed tenant's ACTIVE -installations: the target must be installed there, and the seal must name a -different active installation, which is what unattended work carries and what a -person's capability never does (see the seal rule above). It strips every -identity header, meters on the tenant, forwards the capability as presented and -stamps only `x-codefly-installation-id` with the seal. The callee verifies the +issuer, window, the route's audience, shape), strips every identity header and +meters on the tenant. It then asks the issuer: accounts' +`AdmitWorkloadOperation` admits a capability only if it is exactly what +`StartModuleOperationTask` signs, owned by a declared module principal other +than the alias's and sealed to that principal's active installation, and +refuses every person-owned kind, including those sealed elsewhere (see the seal +rule above). It inspects and reads its own claims, and consumes nothing. The +target must also be installed in the signed tenant, and the seal must not be +one of its rows. The arm forwards the capability as presented and stamps only +`x-codefly-installation-id` with the admitted seal. The callee verifies the capability at full strength, as for any other carrier. The route's rules are in [GATEWAY_ROUTES.md](./GATEWAY_ROUTES.md#module-rest-routes). diff --git a/module/contracts/api/auth-gateway/grpc/contract.binpb b/module/contracts/api/auth-gateway/grpc/contract.binpb index 940d593db06b46dc9a84913717bea3949b7d1dbb..a650b8d950ad727f06df42c0e003348bf7bb8e32 100644 GIT binary patch delta 3583 zcmaJ@&u<&Y73MCNGBr*dbsgI>Z8cNFj$E0del<#?#xPWhj@cTbK{0VpB}Uv4xzKW# z-dT#)XSk>#v22s~i7ZVp~cGwI({9VCY9`?POzZ6okiKAe?qEkfDg& zz;6qu?<$2~Uw&r27vFlumUi(adYXDRGRLNTHk2x|E9?!5((VL)B%eo%1~=}`|NP?1 zN5yb%!B~9p!fak7hibBJb5;;ARa6fe(~KkHfLM8+{5vI{$Twm{!NjYg`90I z=I$A#6aIbLa#!(D@01JP5d(4PdTw;eM?!Hwhz^^dIcll8ejzs9VewX)x0mSpQU|J1mQ;B&s2qiexeKnAs zC`SFYS1LN7R=y`s$8UaXT^oP(xpljk=4IorzOnY~GV&rT z<4%A`I7&YL_or5A{Opg`Evwa&Z0vtwEscNwg>^k%EG)4%SfP+JbB3M2Zr)`VnMD>E ztJ#tX>s~pv3^?92SJE9#*-lk!jHEOSgu*Fo7rxsmI~PA~#o z$3f{UQd{fC&|$bVe#QZv^w$nbTU|)vW`%P$;2f0Qb#R+*z8S38e3s z6&7jOSbE3Yic4MIKoL6pkcPw{bp4Jy5FXN^kPuauJy*$@?M%7^m{FfybZePF3SGfC zyBWe-eA@1dK4wJ*g7R)`rdB=%rrYKw``px^@?^bVIy2FNY0fF8bA@^ipzdwd%WFr$ z4m8K%grTd;i|bB{lU4&A-VJftQP*m2Q;D;d(;b6egOkEx;mx3#<3BZVCI*ou)wmd9 zu7oshRR_VeOg%yfNig~t8ff5&gHt{TJhyY2sOSZL7qL(nsEdhad1!X~z3o&p3z99H zW%f)fW|6T`a;B#kjs35!t8rE}8W0^^IwL2?f(GvN7!{)QjvyzdVz-x^pdOHu^cK=D zudS!)3rN3$^j{{`Um$Cv^gBb}A&C@JHnHx7b2A$QdQx9n4k~RWQEXYA`Fyw9s((@g zs+s2o48X$d*3Xe_2_-wkNTUWprA`88sAIDL*eig|m}%jnT+Qtm_?@??&sU3gh>nFm z2Z437$XZ88M)Jx-%Ka zKljL$&JMLwk1mv%G6Zp4RBG5qEZrdl0r=MEuVnbp66=fC*_#R4BJh>A@@#?4lWXy} z^Orz#!@4*71zR>_Yb}jnfZ;Y6qV2K8u&r(L1FLkT38pt84I3?SHn+hv@J_%-Ulk;N zm^^^_Kt#Rdy)YGr@%~UnxU6ReS&T`B!Jm<$nYlfGDT4%r+dr^VQU*wEU;ZE?Wu&Cu zpODgDu;r)LsyzgX|NO&rpXXH!HH*RHd5cRWQrNi(iCzmPd% fTkZ1OhjW84h=MKlr{`?^#4TL?+wIlD%SZnM8I$2g delta 25 hcmX@!DR%F*XhREQ3sVbo3(FQ(Zr$x)tXVI6004*z3Dy7r diff --git a/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/workload_operation_admission.proto b/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/workload_operation_admission.proto new file mode 100644 index 000000000..dda95d168 --- /dev/null +++ b/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/workload_operation_admission.proto @@ -0,0 +1,71 @@ +syntax = "proto3"; + +package saas.accounts.v1; + +import "buf/validate/validate.proto"; + +// Workload operation admission: the issuer's answer to whether a capability +// presented with no bearer at a declared module route is a module operation +// context, the one shape that route admits without a person. +// +// A forwarding hop cannot answer it. Core's Inspector, which is all a hop runs, +// withholds the owner, the owner's kind, the actor chain and the operation +// binding, and those are exactly what tell a module's unattended work from a +// person's capability. Accounts signed the capability and can read its own +// claims, so the gateway asks it. WorkContextService/AdmitWorkloadOperation +// takes and answers these messages. +// +// They live in their own file, with deliberately minimal imports, so the +// auth-gateway can generate the same types from the same source proto, as it +// does for installation_listing.proto and solution_entitlements.proto. The +// gateway's copy is this file, byte for byte. + +// WorkloadOperationRefusal is why a capability was not admitted. The gateway +// logs it and answers every refusal alike. +enum WorkloadOperationRefusal { + WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED = 0; + // Not a capability this issuer signed, inside its window, addressed to the + // alias; or the alias is one no operation context is minted for. + WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED = 1; + // Owned by a principal that is not a service: a person, an agent, or a kind + // this issuer does not name. + WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE = 2; + // A service principal's capability that is not shaped as a module operation + // context: it carries an actor chain, a grant hop, an operation binding, a + // parent session or an owner agent, names no authority scope, or has another + // replay policy or a longer window. + WORKLOAD_OPERATION_REFUSAL_NOT_OPERATION_CONTEXT = 3; + // Owned by the module principal the alias names: a module does not reach + // itself through this route. + WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET = 4; + // The owner is not a declared module principal, or the seal is not an ACTIVE + // installation in the tenant installed under that principal. + WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS = 5; +} + +message AdmitWorkloadOperationRequest { + // The capability as presented, encoded. It is inspected and read, never + // verified: nothing it carries is consumed. + string work_context = 1 [ + (buf.validate.field).string.min_len = 1, + (buf.validate.field).string.max_len = 32768 + ]; + // The route alias the gateway resolved from the request path. The capability + // must be addressed to it. + string alias = 2 [ + (buf.validate.field).string.min_len = 1, + (buf.validate.field).string.max_len = 512 + ]; +} + +message AdmitWorkloadOperationResponse { + bool admitted = 1; + // Set when admitted is false. + WorkloadOperationRefusal refusal = 2; + // The signed facts the gateway logs and forwards, set only when admitted: + // the tenant, the installation the capability is sealed to (the CALLING + // module's), and the task. + string tenant_id = 3; + string installation_id = 4; + string task_id = 5; +} diff --git a/module/contracts/api/catalog.codefly.json b/module/contracts/api/catalog.codefly.json index a5fcf2a98..f51302ff4 100644 --- a/module/contracts/api/catalog.codefly.json +++ b/module/contracts/api/catalog.codefly.json @@ -563,7 +563,7 @@ "kind": "protobuf", "package": "saas.gateway.auth.v1", "path": "contracts/api/auth-gateway/grpc/contract.binpb", - "digest": "sha256:17a79ecb7eeb86732edc38736f51050374f0aa05e5db57053ae657c9106ae876", + "digest": "sha256:29781c23f5f8ab2533e4d568c74690ed1ae0ffc32c2424829dc5149a2581428e", "services": [ { "name": "AuthGatewayService", diff --git a/module/module.package.codefly.yaml b/module/module.package.codefly.yaml index b75b0bc86..f848e4144 100644 --- a/module/module.package.codefly.yaml +++ b/module/module.package.codefly.yaml @@ -31,7 +31,7 @@ services: kind: protobuf package: saas.gateway.auth.v1 path: contracts/api/auth-gateway/grpc/contract.binpb - digest: sha256:17a79ecb7eeb86732edc38736f51050374f0aa05e5db57053ae657c9106ae876 + digest: sha256:29781c23f5f8ab2533e4d568c74690ed1ae0ffc32c2424829dc5149a2581428e - name: cache endpoints: [read, write] - name: frontend diff --git a/module/services/auth-gateway/AGENTS.md b/module/services/auth-gateway/AGENTS.md index c97eb3d3b..e038f7fe1 100644 --- a/module/services/auth-gateway/AGENTS.md +++ b/module/services/auth-gateway/AGENTS.md @@ -105,30 +105,29 @@ cookie, or arriving on `rest`) takes the person path above. A request on the workload endpoint with no `Authorization` and no `Cookie`, presenting a capability addressed to the alias, takes the bearer-less arm (`gateway_workload_operation.go`): a composed workload's headless operation -context, minted at `/modules/_operation-context`. The arm reads no principal. It -requires exactly one capability header (401 `invalid work context` otherwise), -the signed tenant to have an ACTIVE installation of the target, and the seal to -name an ACTIVE installation in that tenant that is none of the target's and -whose `registry_principal_id` is set and is not `modulePrincipalID(alias)`, read -from accounts' `ListInstallations` per request and uncached; any other seal may -be a person's capability (accounts seals one by the audience's principal, which -need not be the route alias's target) and answers 401 as before. -`modulePrincipalID` restates accounts' `ModulePrincipalID`, pinned on both sides -by test. It strips identity (and `X-Forwarded-Client-Cert`) before its limiter, -meters on `workload-operation:` (fail closed), forwards no bearer, stamps -only `x-codefly-installation-id` (the seal, so the CALLER's installation on this -arm and the target's admitting installation on the person path; equal to the -presented seal whenever a capability is presented), answers an upstream -failure with a static 502, and logs every admit and refusal with alias, tenant, -task, seal and presenter. No installations client wired answers 503. It is the -gateway's half of a solution's release-declared headless operation; a module -calling a peer keeps its in-mesh seam instead. - -**Release note: deploy accounts before this gateway.** The arm relies on -accounts refusing a person's child for another audience's installation -(`StartChildSession`). An older accounts mints such a child, valid for up to 900 -seconds, which this arm would read as unattended work. Roll accounts first, and -roll this gateway only once no older accounts replica is serving. +context, minted at `/modules/_operation-context`. The arm reads no principal and +decides nothing from the seal. It requires exactly one capability header (401 +`invalid work context` otherwise), strips identity (and +`X-Forwarded-Client-Cert`) before its limiter, meters on +`workload-operation:` (fail closed), and only then asks accounts two +things, per request and uncached: `WorkContextService/AdmitWorkloadOperation`, +whether the capability is a module operation context for this alias (any +refusal is 401 `authentication required`, logged with the issuer's reason; an +error, a call past its 2 second bound, an answer naming another tenant or seal, +or no client wired is 503), and `ListInstallations`, whether the signed tenant +has an ACTIVE installation of the target (else 403) whose rows do not include +the seal (else 401). It forwards no bearer, stamps only +`x-codefly-installation-id` (the seal accounts admitted, so the CALLER's +installation on this arm and the target's admitting installation on the person +path; equal to the presented seal whenever a capability is presented), answers +an upstream failure with a static 502, and logs every admit and refusal with +alias, tenant, task, seal and presenter. It is the gateway's half of a +solution's release-declared headless operation; a module calling a peer keeps +its in-mesh seam instead. + +**Release note: deploy accounts before this gateway.** The arm calls +`AdmitWorkloadOperation`, which an older accounts does not serve: there every +bearer-less request answers 503 and nothing is admitted. Roll accounts first. The registry's `SolutionDeclaredBinding.kind` positively identifies each surface. An unspecified kind is refused by both. Neither presence nor a reusable alias @@ -136,19 +135,19 @@ constitutes organization consent. Module identity exchanges are described below; accounts owns their authority. -A person's interactive capability is sealed to the organization's installation -of the audience it addresses, a module principal's to its own. Two person-owned -kinds are sealed elsewhere by design: a source-operation context, sealed to the -installation of the module using the person's source delegation and minted only -by that module's identity, and an installation task, sealed to its installation -and minted only with an `EXPOSURE_INTERNAL` credential. The bearer-less arm -admits all three kinds (the unattended-work family) and never a person's -interactive capability. Accounts holds that as an invariant over every person -mint: `TestAPersonsCapabilityIsSealedToItsAudiencesInstallation` and -`TestEveryWorkContextMintIsClassifiedBySeal` in accounts -(`pkg/adapters/work_context_seal_pin_test.go`) pin it. The gateway reads no -owner (core's `Inspected` carries none), so any decision this gateway makes from -a seal rests on that test. +The gateway reads no owner (core's `Inspected` carries none), so it cannot tell +a module operation context from a person's capability by anything it holds: a +source-operation context, an installation task and an installed binding's child +are each a person's and each sealed to an installation that is not the target's. +That is why the arm asks the issuer rather than reading the seal. Accounts +admits exactly one shape, and two tests in accounts +(`pkg/adapters/workload_operation_admission_test.go`) hold it there: +`TestEveryMintHasAWorkloadOperationVerdict` asserts the verdict for every mint +RPC, binding mode and signing entrypoint, with only `StartModuleOperationTask` +admitted, and `TestWorkloadOperationTableNamesEveryMintAndOwnerKind` fails when a +mint RPC, a signing entrypoint or an owner kind appears that the table does not +name. On this side, `TestModuleRoute_TheBearerLessArmAdmitsOnlyWhatTheIssuerAdmits` +pins every refusal to 401 before anything else is read. ## What a runtime-registered upstream receives, and what it does not @@ -429,7 +428,8 @@ on this endpoint and nowhere else: on `rest` they are unexposed paths (`404`). The fourth workload-only surface is the bearer-less arm of `/v1//*` (above): on `rest` the same request is an unauthenticated one, `401`. Unlike the exchanges it proves no workload with a projected token: it admits a capability -on the strength of its signature, its seal and, on a cell, the presenter. A +on the strength of its signature, the issuer's admission of its shape and, on a +cell, the presenter. A deployed gateway requires that presenter again in the arm itself. The listener puts the authenticated presenter in private request context and removes the raw headers. `moduleMintContext` sends that principal to accounts diff --git a/module/services/auth-gateway/code/attack_battery_test.go b/module/services/auth-gateway/code/attack_battery_test.go index 50e7dcfe7..777824115 100644 --- a/module/services/auth-gateway/code/attack_battery_test.go +++ b/module/services/auth-gateway/code/attack_battery_test.go @@ -71,7 +71,8 @@ func TestAttack_AnyAuthorizationSchemeLeavesTheArm(t *testing.T) { require.Equal(t, http.StatusUnauthorized, w.Code) require.Zero(t, module.calls) - require.Empty(t, installations.calls, "the arm was not selected") + require.Empty(t, admissionOf(t, gw).calls, "the arm was not selected") + require.Empty(t, installations.calls) } // TestAttack_TheBearerLessArmForwardsNoProxyCredential: a Proxy-Authorization @@ -93,7 +94,7 @@ func TestAttack_TheBearerLessArmForwardsNoProxyCredential(t *testing.T) { // generic key would be the waypoint's address, one bucket for every tenant on a // cell. The arm's budget is the tenant the capability is signed for, in its own // namespace, read after the strip: a caller's X-Org-Id and its source address -// open no new bucket. Exhausting it refuses before the authority is asked. +// open no new bucket. Exhausting it refuses before accounts is asked anything. func TestAttack_TheBearerLessArmIsMeteredOnItsSignedTenant(t *testing.T) { gw, module, installations, kid, wcPriv := operationHarness(t) backend := &memoryBackend{} @@ -114,7 +115,8 @@ func TestAttack_TheBearerLessArmIsMeteredOnItsSignedTenant(t *testing.T) { } require.Equal(t, http.StatusTooManyRequests, serve(3)) require.Equal(t, 3, module.calls) - require.Len(t, installations.calls, 3, "a limited request never reaches the admission read") + require.Len(t, admissionOf(t, gw).calls, 3, "a limited request never reaches the issuer") + require.Len(t, installations.calls, 3, "nor the installation read") var keys []string backend.windows.Range(func(key, _ any) bool { @@ -128,7 +130,7 @@ func TestAttack_TheBearerLessArmIsMeteredOnItsSignedTenant(t *testing.T) { // TestAttack_TheBearerLessArmFailsClosedWithoutItsLimiter: the arm's budget is a // security budget. A limiter backend that cannot answer refuses the request; it -// does not wave it through to the authority read. +// does not wave it through to the issuer or the installation read. func TestAttack_TheBearerLessArmFailsClosedWithoutItsLimiter(t *testing.T) { gw, module, installations, kid, wcPriv := operationHarness(t) gw.rateLimiter = &RateLimiter{backend: failingRateLimitBackend{}, limit: 100, burst: 20, stop: make(chan struct{}), proxies: newProxyTrust("")} @@ -139,5 +141,6 @@ func TestAttack_TheBearerLessArmFailsClosedWithoutItsLimiter(t *testing.T) { require.Equal(t, http.StatusServiceUnavailable, w.Code) require.Contains(t, w.Body.String(), "rate limit service unavailable") require.Zero(t, module.calls) + require.Empty(t, admissionOf(t, gw).calls) require.Empty(t, installations.calls) } diff --git a/module/services/auth-gateway/code/gateway.go b/module/services/auth-gateway/code/gateway.go index fb42290ec..d2833b324 100644 --- a/module/services/auth-gateway/code/gateway.go +++ b/module/services/auth-gateway/code/gateway.go @@ -66,6 +66,10 @@ type Gateway struct { // bearer-less module arm (gateway_workload_operation.go). Set after // construction; nil answers 503 there and never admits. installations installationListingClient + // workloadAdmission asks accounts whether a capability on the bearer-less + // module arm is a module operation context. Set after construction; nil + // answers 503 there and never admits. + workloadAdmission workloadAdmissionClient // requireWorkloadPresenter makes the bearer-less arm require the waypoint // presenter the workload listener authenticated. Set outside native // development, where the listener already requires one. diff --git a/module/services/auth-gateway/code/gateway_module_confinement_test.go b/module/services/auth-gateway/code/gateway_module_confinement_test.go index 3b67bf708..698ce23bf 100644 --- a/module/services/auth-gateway/code/gateway_module_confinement_test.go +++ b/module/services/auth-gateway/code/gateway_module_confinement_test.go @@ -296,6 +296,7 @@ func TestModuleRoute_TheBearerLessArmTakesNoIdentityFromTheCaller(t *testing.T) func TestModuleRoute_ABearerOnTheWorkloadListenerTakesThePersonPath(t *testing.T) { gw, module, _, caller, kid, wcPriv, _ := confinedModuleHarness(t) gw.installations = &installationsFake{err: errors.New("the arm must not be consulted")} + gw.workloadAdmission = &admissionFake{err: errors.New("the arm must not be consulted")} capability := mintViewerCapability(t, kid, wcPriv, "documents", caller.subject, caller.org, caller.session) w := httptest.NewRecorder() diff --git a/module/services/auth-gateway/code/gateway_module_routes_test.go b/module/services/auth-gateway/code/gateway_module_routes_test.go index 80871840d..df736226c 100644 --- a/module/services/auth-gateway/code/gateway_module_routes_test.go +++ b/module/services/auth-gateway/code/gateway_module_routes_test.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "log" + "net" "net/http" "net/http/httptest" "strings" @@ -18,6 +19,9 @@ import ( "github.com/codefly-dev/core/workcontext" carrier "github.com/codefly-dev/sdk-go/workcontext" "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" ) // Routing a declared module (issue #952). @@ -364,6 +368,39 @@ func pageCursor(index int) string { return fmt.Sprintf("page-%d", index) } +// admissionFake stands in for accounts' AdmitWorkloadOperation. By default it +// admits, echoing the presented capability's own tenant, seal and task the way +// accounts answers them; refusal or err replace the answer, and answer, when +// set, is returned as it is. +type admissionFake struct { + refusal accountsv1.WorkloadOperationRefusal + err error + answer *accountsv1.AdmitWorkloadOperationResponse + calls []*accountsv1.AdmitWorkloadOperationRequest +} + +func (f *admissionFake) Admit( + _ context.Context, req *accountsv1.AdmitWorkloadOperationRequest, +) (*accountsv1.AdmitWorkloadOperationResponse, error) { + f.calls = append(f.calls, req) + switch { + case f.err != nil: + return nil, f.err + case f.answer != nil: + return f.answer, nil + case f.refusal != accountsv1.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED: + return &accountsv1.AdmitWorkloadOperationResponse{Refusal: f.refusal}, nil + } + decoded, err := workcontext.Decode(req.GetWorkContext()) + if err != nil { + return &accountsv1.AdmitWorkloadOperationResponse{Refusal: accountsv1.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED}, nil + } + claims := decoded.Context() + return &accountsv1.AdmitWorkloadOperationResponse{ + Admitted: true, TenantId: claims.GetTenantId(), InstallationId: claims.GetSeal().GetInstallationId(), TaskId: claims.GetTaskId(), + }, nil +} + // activeInstallation is one ACTIVE row of operationTenant. func activeInstallation(id, target string) *accountsv1.InstallationSummary { return &accountsv1.InstallationSummary{Installation: &accountsv1.Installation{ @@ -373,12 +410,9 @@ func activeInstallation(id, target string) *accountsv1.InstallationSummary { } // moduleInstallation is the ACTIVE row of operationTenant for the module whose -// alias and principal prefix are both name: its target, installed under its own -// module principal. +// alias is name. func moduleInstallation(id, name string) *accountsv1.InstallationSummary { - summary := activeInstallation(id, fakeSolutionTarget(name)) - summary.Installation.RegistryPrincipalId = modulePrincipalID(name) - return summary + return activeInstallation(id, fakeSolutionTarget(name)) } // tenantInstalled is a tenant that installed the target module (E) and the @@ -393,7 +427,8 @@ func tenantInstalled() *installationsFake { } // operationHarness is a gateway with the edge verifier, the module "documents" -// declared, and a tenant that installed it and the publishing workload. +// declared, a tenant that installed it and the publishing workload, and an +// issuer that admits. func operationHarness(t *testing.T) (gw *Gateway, module *fakeUpstream, installations *installationsFake, kid string, wcPriv ed25519.PrivateKey) { t.Helper() gw, _, _, _ = newGatewayHarness(t) @@ -404,10 +439,20 @@ func operationHarness(t *testing.T) (gw *Gateway, module *fakeUpstream, installa module = declareModule(t, gw, "documents") installations = tenantInstalled() gw.installations = installations + gw.workloadAdmission = &admissionFake{} return gw, module, installations, kid, wcPriv } -// operationCapability is a module operation context for audience, sealed to seal. +// admissionOf is the issuer fake the harness wired. +func admissionOf(t *testing.T, gw *Gateway) *admissionFake { + t.Helper() + fake, ok := gw.workloadAdmission.(*admissionFake) + require.True(t, ok, "the gateway is not wired with admissionFake") + return fake +} + +// operationCapability is a capability for audience, sealed to seal. The edge +// cannot tell what kind it is; the issuer fake answers that. func operationCapability(t *testing.T, kid string, priv ed25519.PrivateKey, audience, seal string) string { t.Helper() return mintCapability(t, kid, priv, edgeCapability{ @@ -434,13 +479,15 @@ func captureLog(t *testing.T) *bytes.Buffer { } // THE ARM ADMITS A HEADLESS OPERATION CONTEXT. No bearer, the workload endpoint, -// a capability for this alias sealed to the publisher's own installation (W) -// in a tenant that installed the target (E, not W). +// a capability for this alias that accounts admits as a module operation +// context, sealed to the publisher's own installation (W), in a tenant that +// installed the target (E, not W). // -// What the upstream holds is the capability and the one header stating its -// seal, and nothing that names a person. ext_authz is not consulted: it denies -// every request without a bearer, so a 200 here is a request it never saw, and -// it stamps x-user-id and x-credential-kind on every allow, which are absent. +// What the upstream holds is the capability and the one header stating the +// admitted seal, and nothing that names a person. ext_authz is not consulted: it +// denies every request without a bearer, so a 200 here is a request it never +// saw, and it stamps x-user-id and x-credential-kind on every allow, which are +// absent. func TestModuleRoute_AWorkloadOperationContextIsAdmittedWithoutABearer(t *testing.T) { gw, module, installations, kid, wcPriv := operationHarness(t) logs := captureLog(t) @@ -456,8 +503,8 @@ func TestModuleRoute_AWorkloadOperationContextIsAdmittedWithoutABearer(t *testin require.Equal(t, "/v1/documents/items:write", module.lastPath, "the path is forwarded unchanged") require.Equal(t, `{"bundle":"b"}`, module.lastBody) require.Equal(t, capability, module.lastHeaders.Get(carrier.HeaderName), "the capability is forwarded as presented") - require.Equal(t, publisherSeal, module.lastHeaders.Get(installationConfinementHeader), - "the one stamped header is the capability's own seal") + require.Equal(t, []string{publisherSeal}, module.lastHeaders.Values(installationConfinementHeader), + "the one stamped header is the seal accounts admitted, once") for _, header := range []string{ "Authorization", "Cookie", "Proxy-Authorization", "X-User-Id", "X-Org-Id", "X-Session-Id", "X-Scopes", "X-Credential-Kind", resourceConfinementHeader, "X-Codefly-Gateway-Token", "X-Forwarded-Client-Cert", @@ -465,6 +512,10 @@ func TestModuleRoute_AWorkloadOperationContextIsAdmittedWithoutABearer(t *testin require.Empty(t, module.lastHeaders.Values(header), "%s must not reach the upstream on the bearer-less arm", header) } + admission := admissionOf(t, gw) + require.Len(t, admission.calls, 1) + require.Equal(t, capability, admission.calls[0].GetWorkContext(), "the issuer is shown the capability as presented") + require.Equal(t, "documents", admission.calls[0].GetAlias(), "and the alias the route resolved") require.Len(t, installations.calls, 1) require.Equal(t, operationTenant, installations.calls[0].GetOrgId(), "the tenant is the capability's, signed") require.Equal(t, accountsv1.InstallationStatus_INSTALLATION_STATUS_ACTIVE, installations.calls[0].GetStatus()) @@ -477,13 +528,49 @@ func TestModuleRoute_AWorkloadOperationContextIsAdmittedWithoutABearer(t *testin require.NotContains(t, line, capability, "the token is never logged") } +// THE ISSUER DECIDES. A capability the edge inspects, addressed to this alias +// and sealed to an active installation that is not the target's, is exactly +// what a source-operation context, an installation task or an installed +// binding's child looks like to a hop: each is a person's. Before the arm asked +// accounts it admitted all of them on the seal; every refusal accounts gives is +// now a 401, the answer a person's capability without its bearer has always had. +func TestModuleRoute_TheBearerLessArmAdmitsOnlyWhatTheIssuerAdmits(t *testing.T) { + for _, refusal := range []accountsv1.WorkloadOperationRefusal{ + accountsv1.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED, + accountsv1.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE, + accountsv1.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_OPERATION_CONTEXT, + accountsv1.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET, + accountsv1.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS, + } { + t.Run(refusal.String(), func(t *testing.T) { + gw, module, installations, kid, wcPriv := operationHarness(t) + admissionOf(t, gw).refusal = refusal + logs := captureLog(t) + capability := operationCapability(t, kid, wcPriv, "documents", publisherSeal) + + w := httptest.NewRecorder() + gw.ServeHTTP(w, onWorkloadEndpoint(publishRequest(capability))) + + require.Equal(t, http.StatusUnauthorized, w.Code, w.Body.String()) + require.Contains(t, w.Body.String(), "authentication required") + require.Zero(t, module.calls) + require.Empty(t, installations.calls, "a refused capability asks nothing more") + require.Contains(t, logs.String(), "reason=workload_operation_"+refusalReason(refusal)) + require.NotContains(t, logs.String(), capability) + }) + } + // Every refusal the issuer names is one of the above. + require.Len(t, accountsv1.WorkloadOperationRefusal_name, 6) +} + // Every refusal, each with one fact broken and the rest held equal. func TestModuleRoute_TheBearerLessArmRefuses(t *testing.T) { for name, test := range map[string]struct { prepare func(t *testing.T, gw *Gateway, installations *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request code int body string - asked bool + // asked is whether the issuer was asked. + asked bool }{ // The arm is the workload endpoint's alone: the public listener never // selects it, so the request takes the person path and has no bearer. @@ -508,16 +595,16 @@ func TestModuleRoute_TheBearerLessArmRefuses(t *testing.T) { }, code: http.StatusUnauthorized, body: "invalid work context", }, - // A person's capability for this module is sealed to the target's own - // installation, and without its bearer it is answered as it always was. - "a person-sealed capability": { + // Sealed to the target's own installation: whatever the issuer says, + // the caller would be the module it addresses. + "a capability sealed to the target's installation": { prepare: func(t *testing.T, _ *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", targetInstallation))) }, code: http.StatusUnauthorized, body: "authentication required", asked: true, }, // The tenant may hold more than one ACTIVE row of the target. A seal - // equal to ANY of them is a person's, not only the last row read. + // equal to ANY of them is refused, not only the last row read. "a capability sealed to the first of two rows of the target": { prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { gw.installations = &installationsFake{pages: []*accountsv1.ListInstallationsResponse{{ @@ -529,49 +616,7 @@ func TestModuleRoute_TheBearerLessArmRefuses(t *testing.T) { }}} return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", targetInstallation))) }, - code: http.StatusUnauthorized, body: "authentication required", - }, - // The alias and the module principal are independent names. Module A is - // routed at "storage" but its principal is "documents"; module B is - // routed at "documents" under principal "docstore". Accounts seals a - // person's StartTask(audience="documents") to A's installation, which is - // not B's target row, so only the registry principal tells it apart. - "a person's capability sealed to the alias's principal on another target": { - prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { - moduleA := activeInstallation("77777777-7777-4777-8777-777777777777", fakeSolutionTarget("storage")) - moduleA.Installation.RegistryPrincipalId = modulePrincipalID("documents") - moduleB := activeInstallation(targetInstallation, fakeSolutionTarget("documents")) - moduleB.Installation.RegistryPrincipalId = modulePrincipalID("docstore") - gw.installations = &installationsFake{pages: []*accountsv1.ListInstallationsResponse{{ - Installations: []*accountsv1.InstallationSummary{moduleA, moduleB, moduleInstallation(publisherSeal, "publisher")}, - }}} - return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", moduleA.Installation.Id))) - }, - code: http.StatusUnauthorized, body: "authentication required", - }, - // A row a withdrawn binding left ACTIVE under the alias's principal is - // still where accounts may seal a person's capability for the alias. - "a person's capability sealed to a stale row of the alias's principal": { - prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { - stale := activeInstallation("88888888-8888-4888-8888-888888888888", fakeSolutionTarget("documents-withdrawn")) - stale.Installation.RegistryPrincipalId = modulePrincipalID("documents") - gw.installations = &installationsFake{pages: []*accountsv1.ListInstallationsResponse{{ - Installations: []*accountsv1.InstallationSummary{moduleInstallation(targetInstallation, "documents"), stale, moduleInstallation(publisherSeal, "publisher")}, - }}} - return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", stale.Installation.Id))) - }, - code: http.StatusUnauthorized, body: "authentication required", - }, - // A seal row with no registry principal is unknown, and unknown never - // admits. - "a seal row with no registry principal": { - prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { - gw.installations = &installationsFake{pages: []*accountsv1.ListInstallationsResponse{{ - Installations: []*accountsv1.InstallationSummary{moduleInstallation(targetInstallation, "documents"), activeInstallation(publisherSeal, fakeSolutionTarget("publisher"))}, - }}} - return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) - }, - code: http.StatusUnauthorized, body: "authentication required", + code: http.StatusUnauthorized, body: "authentication required", asked: true, }, // Core's Inspector verified the first capability header only; a second // is a value nothing verified, so the arm admits exactly one. @@ -583,12 +628,6 @@ func TestModuleRoute_TheBearerLessArmRefuses(t *testing.T) { }, code: http.StatusUnauthorized, body: "invalid work context", }, - "a seal that names no active installation in the tenant": { - prepare: func(t *testing.T, _ *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { - return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", "44444444-4444-4444-8444-444444444444"))) - }, - code: http.StatusUnauthorized, body: "authentication required", asked: true, - }, "a tenant with no active installation of the target": { prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { gw.installations = &installationsFake{pages: []*accountsv1.ListInstallationsResponse{{ @@ -596,7 +635,7 @@ func TestModuleRoute_TheBearerLessArmRefuses(t *testing.T) { }}} return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) }, - code: http.StatusForbidden, body: "module is not installed for this organization", + code: http.StatusForbidden, body: "module is not installed for this organization", asked: true, }, "a route that resolves no target": { prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { @@ -610,28 +649,62 @@ func TestModuleRoute_TheBearerLessArmRefuses(t *testing.T) { require.NoError(t, gw.solutions.refresh(context.Background())) return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) }, - code: http.StatusForbidden, body: "module is not installed for this organization", + code: http.StatusForbidden, body: "module is not installed for this organization", asked: true, }, - "an authority that errors": { + "an issuer that errors": { prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { - gw.installations = &installationsFake{err: errors.New("accounts down: dial tcp 10.0.0.7:9090")} + gw.workloadAdmission = &admissionFake{err: errors.New("accounts down: dial tcp 10.0.0.7:9090")} + return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) + }, + code: http.StatusServiceUnavailable, body: "module installation authority unavailable", + }, + "no issuer wired": { + prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + gw.workloadAdmission = nil return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) }, code: http.StatusServiceUnavailable, body: "module installation authority unavailable", }, + // The issuer's facts must be the ones the edge inspected: an answer for + // another seal or tenant is one nothing here can arbitrate. + "an admission naming another seal": { + prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + gw.workloadAdmission = &admissionFake{answer: &accountsv1.AdmitWorkloadOperationResponse{ + Admitted: true, TenantId: operationTenant, InstallationId: "44444444-4444-4444-8444-444444444444", TaskId: "task-1", + }} + return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) + }, + code: http.StatusServiceUnavailable, body: "module installation authority unavailable", + }, + "an admission naming another tenant": { + prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + gw.workloadAdmission = &admissionFake{answer: &accountsv1.AdmitWorkloadOperationResponse{ + Admitted: true, TenantId: "66666666-6666-4666-8666-666666666666", InstallationId: publisherSeal, TaskId: "task-1", + }} + return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) + }, + code: http.StatusServiceUnavailable, body: "module installation authority unavailable", + }, + "an installation listing that errors": { + prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + gw.installations = &installationsFake{err: errors.New("accounts down: dial tcp 10.0.0.7:9090")} + return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) + }, + code: http.StatusServiceUnavailable, body: "module installation authority unavailable", asked: true, + }, "a cursor that never terminates": { prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { gw.installations = &installationsFake{endless: true} return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) }, - code: http.StatusServiceUnavailable, body: "module installation authority unavailable", + code: http.StatusServiceUnavailable, body: "module installation authority unavailable", asked: true, }, - "no installation authority wired": { + "no installation listing wired": { prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { gw.installations = nil return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) }, - code: http.StatusServiceUnavailable, body: "module installation authority unavailable", + code: http.StatusServiceUnavailable, body: "module installation authority unavailable", asked: true, }, // No edge verifier means no inspected capability, so the arm is never // selected and the person path answers. @@ -663,6 +736,7 @@ func TestModuleRoute_TheBearerLessArmRefuses(t *testing.T) { } { t.Run(name, func(t *testing.T) { gw, module, installations, kid, wcPriv := operationHarness(t) + admission := admissionOf(t, gw) logs := captureLog(t) req := test.prepare(t, gw, installations, kid, wcPriv) @@ -673,7 +747,9 @@ func TestModuleRoute_TheBearerLessArmRefuses(t *testing.T) { require.Contains(t, w.Body.String(), test.body) require.Zero(t, module.calls, "nothing reaches the upstream") if test.asked { - require.Len(t, installations.calls, 1) + require.Len(t, admission.calls, 1) + } else { + require.Empty(t, admission.calls, "the default issuer was not the one asked") } require.NotContains(t, logs.String(), req.Header.Get(carrier.HeaderName), "no refusal logs the token") require.NotContains(t, w.Body.String(), "10.0.0.7", "no refusal carries the authority's error") @@ -686,8 +762,9 @@ func TestModuleRoute_TheBearerLessArmRefuses(t *testing.T) { // presenter, because the arm runs no ext_authz and this line is its only record. func TestModuleRoute_TheBearerLessArmLogsEachRefusal(t *testing.T) { gw, _, _, kid, wcPriv := operationHarness(t) + admissionOf(t, gw).refusal = accountsv1.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE logs := captureLog(t) - capability := operationCapability(t, kid, wcPriv, "documents", targetInstallation) + capability := operationCapability(t, kid, wcPriv, "documents", publisherSeal) req := onWorkloadEndpoint(publishRequest(capability)) req = req.WithContext(context.WithValue(req.Context(), workloadPresenterKey{}, "spiffe://cluster.local/ns/apps/sa/publisher")) @@ -696,8 +773,8 @@ func TestModuleRoute_TheBearerLessArmLogsEachRefusal(t *testing.T) { require.Equal(t, http.StatusUnauthorized, w.Code) line := logs.String() - require.Contains(t, line, "reason=workload_operation_person_sealed") - for _, fact := range []string{`alias="documents"`, `tenant="` + operationTenant + `"`, `task="task-1"`, `seal="` + targetInstallation + `"`, `presenter="spiffe://cluster.local/ns/apps/sa/publisher"`} { + require.Contains(t, line, "reason=workload_operation_owner_not_service") + for _, fact := range []string{`alias="documents"`, `tenant="` + operationTenant + `"`, `task="task-1"`, `seal="` + publisherSeal + `"`, `presenter="spiffe://cluster.local/ns/apps/sa/publisher"`} { require.Contains(t, line, fact) } require.NotContains(t, line, capability) @@ -755,6 +832,7 @@ func TestModuleRoute_TheBearerLessArmRefusesWhatTheEdgeRefuses(t *testing.T) { require.Equal(t, http.StatusUnauthorized, w.Code) require.Contains(t, w.Body.String(), "invalid work context") require.Zero(t, module.calls) + require.Empty(t, admissionOf(t, gw).calls) require.Empty(t, installations.calls) }) } @@ -775,19 +853,20 @@ func TestModuleRoute_TheBearerLessArmRefusesACapabilityWithNoTenantOrSeal(t *tes require.Equal(t, http.StatusUnauthorized, w.Code) require.Contains(t, w.Body.String(), "invalid work context") require.Zero(t, module.calls) + require.Empty(t, admissionOf(t, gw).calls) require.Empty(t, installations.calls) } -// The two facts may sit on different pages: the arm follows the cursor until it -// holds both, and a row the authority should not have returned (another tenant, -// not ACTIVE) counts for neither. +// The target's rows may sit on any page: the arm follows the cursor to the end, +// so a row of the target on a later page counts, and a row the authority should +// not have returned (another tenant) counts for nothing. func TestModuleRoute_TheBearerLessArmReadsEveryPageItNeeds(t *testing.T) { gw, module, _, kid, wcPriv := operationHarness(t) - foreign := moduleInstallation(publisherSeal, "publisher") + foreign := moduleInstallation(targetInstallation, "documents") foreign.Installation.OrgId = "66666666-6666-4666-8666-666666666666" installations := &installationsFake{pages: []*accountsv1.ListInstallationsResponse{ - {Installations: []*accountsv1.InstallationSummary{moduleInstallation(targetInstallation, "documents"), foreign}, NextPageToken: pageCursor(1)}, - {Installations: []*accountsv1.InstallationSummary{moduleInstallation(publisherSeal, "publisher")}}, + {Installations: []*accountsv1.InstallationSummary{foreign, moduleInstallation(publisherSeal, "publisher")}, NextPageToken: pageCursor(1)}, + {Installations: []*accountsv1.InstallationSummary{moduleInstallation(targetInstallation, "documents")}}, }} gw.installations = installations @@ -798,19 +877,98 @@ func TestModuleRoute_TheBearerLessArmReadsEveryPageItNeeds(t *testing.T) { require.Len(t, installations.calls, 2) require.Equal(t, pageCursor(1), installations.calls[1].GetPageToken()) - // The seal only on the foreign row: not an active installation of THIS tenant. + // A seal on the target's row that only the second page names is still the + // target's. + w = httptest.NewRecorder() + gw.ServeHTTP(w, onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", targetInstallation)))) + require.Equal(t, http.StatusUnauthorized, w.Code) + + // The target only on the foreign row: not installed in THIS tenant. installations.pages = installations.pages[:1] installations.pages[0].NextPageToken = "" w = httptest.NewRecorder() gw.ServeHTTP(w, onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal)))) - require.Equal(t, http.StatusUnauthorized, w.Code) + require.Equal(t, http.StatusForbidden, w.Code) require.Equal(t, 1, module.calls) } -// modulePrincipalID restates accounts' business.ModulePrincipalID. These values -// are accounts' own, and TestModulePrincipalIDIsPinnedForTheGateway in accounts -// holds accounts to the same ones, so a change on either side fails a test. -func TestModulePrincipalIDMatchesAccounts(t *testing.T) { - require.Equal(t, "8d4a32d7-50a7-5009-a69d-71bf529b22a2", modulePrincipalID("documents")) - require.Equal(t, "44943497-cee3-5b83-9ada-8ec1384050cf", modulePrincipalID("storage")) +// accountsAdmissionServer is accounts' AdmitWorkloadOperation on a real gRPC +// listener: it records the method, the internal credential and the request, +// and answers admit, or blocks until the caller gives up. +type accountsAdmissionServer struct { + block bool + token []string + requests []*accountsv1.AdmitWorkloadOperationRequest +} + +func startAccountsAdmission(t *testing.T, fake *accountsAdmissionServer) *grpc.ClientConn { + t.Helper() + server := grpc.NewServer() + server.RegisterService(&grpc.ServiceDesc{ + ServiceName: "saas.accounts.v1.WorkContextService", + HandlerType: (*any)(nil), + Methods: []grpc.MethodDesc{{ + MethodName: "AdmitWorkloadOperation", + Handler: func(_ any, ctx context.Context, dec func(any) error, _ grpc.UnaryServerInterceptor) (any, error) { + req := &accountsv1.AdmitWorkloadOperationRequest{} + if err := dec(req); err != nil { + return nil, err + } + md, _ := metadata.FromIncomingContext(ctx) + fake.token = md.Get("x-codefly-internal-token") + fake.requests = append(fake.requests, req) + if fake.block { + <-ctx.Done() + return nil, ctx.Err() + } + return &accountsv1.AdmitWorkloadOperationResponse{Admitted: true, TenantId: operationTenant, InstallationId: publisherSeal, TaskId: "task-1"}, nil + }, + }}, + Metadata: "saas/accounts/v1/work_contexts.proto", + }, fake) + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + go func() { _ = server.Serve(listener) }() + t.Cleanup(server.Stop) + conn, err := grpc.NewClient(listener.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + t.Cleanup(func() { _ = conn.Close() }) + return conn +} + +// The production client asks the method accounts serves, with the internal +// credential, and gives up at its bound: an issuer that does not answer is a +// 503 on the arm, never an admission. +func TestAccountsWorkloadAdmissionAsksTheIssuerWithinItsBound(t *testing.T) { + t.Run("asks with the internal credential", func(t *testing.T) { + fake := &accountsAdmissionServer{} + client := &accountsWorkloadAdmission{conn: startAccountsAdmission(t, fake), internalToken: "test-internal-token"} + + answer, err := client.Admit(context.Background(), &accountsv1.AdmitWorkloadOperationRequest{WorkContext: "wc", Alias: "documents"}) + require.NoError(t, err) + require.True(t, answer.GetAdmitted()) + require.Equal(t, []string{"test-internal-token"}, fake.token) + require.Len(t, fake.requests, 1) + require.Equal(t, "wc", fake.requests[0].GetWorkContext()) + require.Equal(t, "documents", fake.requests[0].GetAlias()) + }) + t.Run("an issuer that does not answer is a 503", func(t *testing.T) { + previous := workloadAdmissionCallTimeout + workloadAdmissionCallTimeout = 50 * time.Millisecond + t.Cleanup(func() { workloadAdmissionCallTimeout = previous }) + fake := &accountsAdmissionServer{block: true} + gw, module, installations, kid, wcPriv := operationHarness(t) + gw.workloadAdmission = &accountsWorkloadAdmission{conn: startAccountsAdmission(t, fake), internalToken: "test-internal-token"} + + started := time.Now() + w := httptest.NewRecorder() + gw.ServeHTTP(w, onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal)))) + + require.Equal(t, http.StatusServiceUnavailable, w.Code, w.Body.String()) + require.Contains(t, w.Body.String(), "module installation authority unavailable") + require.Less(t, time.Since(started), 5*time.Second, "bounded by the call timeout") + require.Len(t, fake.requests, 1) + require.Zero(t, module.calls) + require.Empty(t, installations.calls) + }) } diff --git a/module/services/auth-gateway/code/gateway_operation_context_test.go b/module/services/auth-gateway/code/gateway_operation_context_test.go index d80f6e132..c489cdb29 100644 --- a/module/services/auth-gateway/code/gateway_operation_context_test.go +++ b/module/services/auth-gateway/code/gateway_operation_context_test.go @@ -223,6 +223,8 @@ func TestGateway_AMintedOperationContextReachesItsModuleWithoutABearer(t *testin gw.workContext = newWorkContextVerifier(jwks.URL) module := declareModule(t, gw, "documents") gw.installations = tenantInstalled() + admission := &admissionFake{} + gw.workloadAdmission = admission mint.token = operationCapability(t, "key-1", wcPriv, "documents", publisherSeal) logs := captureLog(t) @@ -240,6 +242,7 @@ func TestGateway_AMintedOperationContextReachesItsModuleWithoutABearer(t *testin require.Equal(t, 1, module.calls) require.Equal(t, "/v1/documents/items:write", module.lastPath) require.Equal(t, capability, module.lastHeaders.Get(carrier.HeaderName)) + require.Len(t, admission.calls, 1, "accounts said what the context is") // The same context on the public listener is a bearer-less request and // nothing more. diff --git a/module/services/auth-gateway/code/gateway_workload_operation.go b/module/services/auth-gateway/code/gateway_workload_operation.go index a18ec33ba..e815524d5 100644 --- a/module/services/auth-gateway/code/gateway_workload_operation.go +++ b/module/services/auth-gateway/code/gateway_workload_operation.go @@ -14,53 +14,43 @@ package main // Selection is narrow and every other request takes the person path unchanged: // no Authorization and no Cookie header, the workload listener, and an // edge-inspected capability addressed to the alias this route resolved. Once -// selected, the arm decides from three signed facts and one authority read: +// selected, the arm decides from two authority reads, both behind the tenant's +// limiter: // -// - the tenant and the seal the capability names (core's Inspector checked the -// signature, issuer, window, audience and shape at the route); -// - the tenant's ACTIVE installations, read from accounts per request. +// - accounts' AdmitWorkloadOperation, the issuer's answer to whether the +// capability is a module operation context (StartModuleOperationTask) for +// this alias. The gateway cannot tell: core's Inspected withholds the owner, +// its kind and the chain, and a person's capability can be sealed to an +// installation that is not the target's (a source-operation context, an +// installation task, an installed binding's child). Accounts reads its own +// claims and admits that one shape; everything else is refused. It inspects +// and never verifies, so nothing is consumed on the way. +// - the tenant's ACTIVE installations, read from accounts per request: the +// target must be installed there, and the seal must not be one of the +// target's own rows (a module does not reach itself through this door). // -// It admits only when the tenant has an active installation of the TARGET, and -// the seal names an active installation in that tenant that is none of the -// target's. The second half -// is how a hop that reads no owner tells unattended work from a person: a -// person's interactive capability is sealed to the organization's installation -// of the audience it addresses, which here is the target's own, while a module -// operation context, a source-operation context and an installation task are -// each sealed to another installation (the unattended-work family, admitted by -// design; see WORK_CONTEXTS.md). Accounts holds -// that as an invariant (TestAPersonsCapabilityIsSealedToItsAudiencesInstallation -// in accounts); this arm is sound only while it holds. A misreading refuses, -// never widens, and the callee verifies the capability at full strength. +// A misreading refuses, never widens, and the callee verifies the capability at +// full strength. // // What the upstream receives: the capability as presented, the path unchanged, // no bearer, no cookie, no identity header, and x-codefly-installation-id set to -// the seal. Nothing names a principal, because nothing here read one. +// the seal accounts admitted. Nothing names a principal: the gateway reads none. import ( "context" "log" "net/http" + "strings" + "time" accountsv1 "auth-gateway/pkg/gen/saas/accounts/v1" "github.com/codefly-dev/core/workcontext" carrier "github.com/codefly-dev/sdk-go/workcontext" - "github.com/google/uuid" "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) -// modulePrincipalID is accounts' business.ModulePrincipalID: the registry -// principal id of the module whose principal prefix is prefix, a UUIDv5 under -// the URL namespace. Neither core nor the sdk exports it and the gateway cannot -// import accounts, so it is restated here and pinned to accounts' value by -// TestModulePrincipalIDMatchesAccounts on this side and -// TestModulePrincipalIDIsPinnedForTheGateway on accounts' side. -func modulePrincipalID(prefix string) string { - return uuid.NewSHA1(uuid.NameSpaceURL, []byte("https://codefly.dev/module-principal/"+prefix)).String() -} - const listInstallationsMethod = "/saas.accounts.v1.InstallationService/ListInstallations" // installationListingPageSize and installationListingPageLimit bound one @@ -96,6 +86,38 @@ func (c *accountsInstallations) List( return resp, c.conn.Invoke(ctx, listInstallationsMethod, req, resp) } +const admitWorkloadOperationMethod = "/saas.accounts.v1.WorkContextService/AdmitWorkloadOperation" + +// workloadAdmissionCallTimeout bounds the issuer's answer. A caller waits on it +// in the request path, as on the client registry's on-demand lookup, so it is +// that lookup's bound rather than a background reconcile's; past it the arm +// answers 503 and admits nothing. +var workloadAdmissionCallTimeout = 2 * time.Second + +// workloadAdmissionClient is the issuer's half of the arm. The interface exists +// so the arm can be exercised without an accounts process. +type workloadAdmissionClient interface { + Admit(ctx context.Context, req *accountsv1.AdmitWorkloadOperationRequest) (*accountsv1.AdmitWorkloadOperationResponse, error) +} + +// accountsWorkloadAdmission invokes AdmitWorkloadOperation by method name over +// the existing accounts connection, with the gateway's cluster-internal +// credential, the shape of accountsInstallations. +type accountsWorkloadAdmission struct { + conn *grpc.ClientConn + internalToken string +} + +func (c *accountsWorkloadAdmission) Admit( + ctx context.Context, req *accountsv1.AdmitWorkloadOperationRequest, +) (*accountsv1.AdmitWorkloadOperationResponse, error) { + ctx, cancel := context.WithTimeout(ctx, workloadAdmissionCallTimeout) + defer cancel() + ctx = metadata.AppendToOutgoingContext(ctx, "x-codefly-internal-token", c.internalToken) + resp := &accountsv1.AdmitWorkloadOperationResponse{} + return resp, c.conn.Invoke(ctx, admitWorkloadOperationMethod, req, resp) +} + // workloadOperationTenantKey carries the signed tenant from the arm to the rate // limiter. A private context key, set only by serveWorkloadOperation: no header // and no other route can select the budget it keys. @@ -120,35 +142,30 @@ func selectsWorkloadOperation(r *http.Request, capability *workcontext.Inspected capability.Audience() == alias } -// workloadOperationAdmission is the arm's authority verdict. Like the viewer -// admission it is not a boolean: "not installed", "not unattended work" and "I -// could not ask" are different answers. -type workloadOperationAdmission int +// workloadOperationInstallation is the arm's installation verdict. Like the +// viewer admission it is not a boolean: "not installed", "the target's own +// installation" and "I could not ask" are different answers. +type workloadOperationInstallation int const ( - workloadOperationAdmitted workloadOperationAdmission = iota + workloadOperationInstalled workloadOperationInstallation = iota // workloadOperationNotInstalled: the tenant has no ACTIVE installation of // the target, or the route resolved no target. workloadOperationNotInstalled - // workloadOperationPersonSealed: the seal is one of the target's own - // installations, or an installation of the alias's module principal (or of - // an unknown one), which is where a person's capability for this module is - // sealed. - workloadOperationPersonSealed - // workloadOperationSealInactive: the seal names no ACTIVE installation in - // the tenant (revoked, another tenant's, unknown). - workloadOperationSealInactive + // workloadOperationSealOnTarget: the seal is one of the target's own + // installations, so the caller would be the module it addresses. + workloadOperationSealOnTarget // workloadOperationUndecidable: no client wired, an RPC error, or a cursor // that did not terminate. Answered 503, never admitted. workloadOperationUndecidable ) -// admitWorkloadOperation reads the tenant's ACTIVE installations and decides. -// Uncached, for the reason the viewer admission is: an uninstall changes the -// answer with no write this replica would see. -func (g *Gateway) admitWorkloadOperation( - ctx context.Context, tenant, seal, alias string, routing *solutionRouting, -) workloadOperationAdmission { +// workloadOperationInstallations reads the tenant's ACTIVE installations and +// decides. Uncached, for the reason the viewer admission is: an uninstall +// changes the answer with no write this replica would see. +func (g *Gateway) workloadOperationInstallations( + ctx context.Context, tenant, seal string, routing *solutionRouting, +) workloadOperationInstallation { targetID := routing.GetTargetID() if targetID == "" { return workloadOperationNotInstalled @@ -156,17 +173,9 @@ func (g *Gateway) admitWorkloadOperation( if g.installations == nil { return workloadOperationUndecidable } - // Every ACTIVE row of the target counts: the seal of a person's capability - // for this module is one of them, and keeping only the last row seen would - // read the others as some other installation's. + // Every ACTIVE row of the target counts: keeping only the last row seen + // would read the others as some other installation's. targetInstallations := map[string]bool{} - sealActive := false - // The registry principal of the seal's row. A person's capability for this - // alias is sealed to the active installation installed under the alias's - // module principal, which need not be the route's target: the route - // resolves the alias to a target, accounts resolves the audience to a - // principal, and the two are independent names. - sealPrincipal := "" token := "" for page := 0; page < installationListingPageLimit; page++ { resp, err := g.installations.List(ctx, &accountsv1.ListInstallationsRequest{ @@ -189,13 +198,7 @@ func (g *Gateway) admitWorkloadOperation( if installation.GetTargetId() == targetID { targetInstallations[installation.GetId()] = true } - if installation.GetId() == seal { - sealActive = true - sealPrincipal = installation.GetRegistryPrincipalId() - } } - // No early exit: whether the seal is ANOTHER row of the target is only - // conclusive after the whole set, so every page is read. token = resp.GetNextPageToken() if token == "" { break @@ -208,16 +211,15 @@ func (g *Gateway) admitWorkloadOperation( case len(targetInstallations) == 0: return workloadOperationNotInstalled case targetInstallations[seal]: - return workloadOperationPersonSealed - case sealActive && (sealPrincipal == "" || sealPrincipal == modulePrincipalID(alias)): - // Sealed to the installation of the alias's own module principal, which - // is where accounts seals a person's capability for this audience. An - // empty principal is unknown, and unknown never admits. - return workloadOperationPersonSealed - case !sealActive: - return workloadOperationSealInactive + return workloadOperationSealOnTarget } - return workloadOperationAdmitted + return workloadOperationInstalled +} + +// refusalReason is the log reason for an issuer's refusal: +// WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE logs as owner_not_service. +func refusalReason(refusal accountsv1.WorkloadOperationRefusal) string { + return strings.ToLower(strings.TrimPrefix(refusal.String(), "WORKLOAD_OPERATION_REFUSAL_")) } // workloadOperationRecord is what every admit and refusal on the arm logs. The @@ -299,26 +301,45 @@ func (g *Gateway) serveWorkloadOperation( reached := false g.rateLimitThenServe(w, r, entry, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { reached = true - switch g.admitWorkloadOperation(r.Context(), rec.tenant, rec.seal, alias, routing) { + // The issuer decides what the capability is. Asked only behind the + // limiter, so an exhausted budget never reaches accounts. + if g.workloadAdmission == nil { + rec.refuse(w, http.StatusServiceUnavailable, "module installation authority unavailable", "authority_unavailable") + return + } + answer, err := g.workloadAdmission.Admit(r.Context(), &accountsv1.AdmitWorkloadOperationRequest{ + WorkContext: r.Header.Get(carrier.HeaderName), Alias: alias, + }) + switch { + case err != nil: + rec.refuse(w, http.StatusServiceUnavailable, "module installation authority unavailable", "authority_unavailable") + return + case !answer.GetAdmitted(): + // The answer a person's capability without its bearer has always had. + rec.refuse(w, http.StatusUnauthorized, "authentication required", refusalReason(answer.GetRefusal())) + return + case answer.GetTenantId() != rec.tenant || answer.GetInstallationId() != rec.seal: + // The issuer read other claims than the edge did: nothing here can + // say which is right, so nothing is admitted. + rec.refuse(w, http.StatusServiceUnavailable, "module installation authority unavailable", "authority_mismatch") + return + } + switch g.workloadOperationInstallations(r.Context(), rec.tenant, rec.seal, routing) { case workloadOperationUndecidable: rec.refuse(w, http.StatusServiceUnavailable, "module installation authority unavailable", "authority_unavailable") return case workloadOperationNotInstalled: rec.refuse(w, http.StatusForbidden, "module is not installed for this organization", "not_installed") return - case workloadOperationPersonSealed: - // The answer a person's capability without its bearer has always had. - rec.refuse(w, http.StatusUnauthorized, "authentication required", "person_sealed") - return - case workloadOperationSealInactive: - rec.refuse(w, http.StatusUnauthorized, "authentication required", "seal_inactive") + case workloadOperationSealOnTarget: + rec.refuse(w, http.StatusUnauthorized, "authentication required", "seal_on_target") return } - // The one header this arm stamps: the seal of the capability it admitted. - // An sdk consumer reading the carrier requires this header to equal the - // seal, and on the person path it equals the seal too. Here it names the - // CALLER's installation, not consent on the target's side. - r.Header.Set(installationConfinementHeader, rec.seal) + // The one header this arm stamps: the seal of the capability accounts + // admitted. An sdk consumer reading the carrier requires this header to + // equal the seal, and on the person path it equals the seal too. Here + // it names the CALLER's installation, not consent on the target's side. + r.Header.Set(installationConfinementHeader, answer.GetInstallationId()) rec.log("INFO: admitted", "admit") g.proxyTo(w, r, routing.Upstream, entry) })) diff --git a/module/services/auth-gateway/code/main.go b/module/services/auth-gateway/code/main.go index 2eaf8dc31..64c37da87 100644 --- a/module/services/auth-gateway/code/main.go +++ b/module/services/auth-gateway/code/main.go @@ -258,6 +258,9 @@ func main() { // The organization-level installation read the bearer-less module arm // admits on, over the same connection and uncached for the same reason. gateway.installations = &accountsInstallations{conn: internalAPIConn, internalToken: authz.internalToken} + // The issuer's answer to what a bearer-less capability is, over the + // same connection, asked per request. + gateway.workloadAdmission = &accountsWorkloadAdmission{conn: internalAPIConn, internalToken: authz.internalToken} gateway.requireWorkloadPresenter = !codefly.IsLocal() go gateway.solutions.reconcile(ctx) go gateway.clients.reconcile(ctx) diff --git a/module/services/auth-gateway/code/perimeter_workos_free_test.go b/module/services/auth-gateway/code/perimeter_workos_free_test.go index 9da2d865c..c4f5d7692 100644 --- a/module/services/auth-gateway/code/perimeter_workos_free_test.go +++ b/module/services/auth-gateway/code/perimeter_workos_free_test.go @@ -190,8 +190,8 @@ var httpAllowedPerimeterFiles = map[string]bool{ // gateway_workload_operation.go is the bearer-less arm of a declared module // route. It verifies no token itself (core's Inspector already ran at the // route, with local keys), and its only network calls are the internal gRPC - // hop to accounts for the tenant's installations and the proxy hop to the - // declared upstream. It reaches no external IdP. + // hops to accounts (the issuer's admission and the tenant's installations) + // and the proxy hop to the declared upstream. It reaches no external IdP. "gateway_workload_operation.go": true, // gateway_cors.go answers cross-origin access for registered clients. It // decides from the registry snapshot and from the client id the ext_authz diff --git a/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/workload_operation_admission.pb.go b/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/workload_operation_admission.pb.go new file mode 100644 index 000000000..27cc9e025 --- /dev/null +++ b/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/workload_operation_admission.pb.go @@ -0,0 +1,309 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: saas/accounts/v1/workload_operation_admission.proto + +package accountsv1 + +import ( + reflect "reflect" + sync "sync" + unsafe "unsafe" + + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// WorkloadOperationRefusal is why a capability was not admitted. The gateway +// logs it and answers every refusal alike. +type WorkloadOperationRefusal int32 + +const ( + WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED WorkloadOperationRefusal = 0 + // Not a capability this issuer signed, inside its window, addressed to the + // alias; or the alias is one no operation context is minted for. + WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED WorkloadOperationRefusal = 1 + // Owned by a principal that is not a service: a person, an agent, or a kind + // this issuer does not name. + WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE WorkloadOperationRefusal = 2 + // A service principal's capability that is not shaped as a module operation + // context: it carries an actor chain, a grant hop, an operation binding, a + // parent session or an owner agent, names no authority scope, or has another + // replay policy or a longer window. + WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_OPERATION_CONTEXT WorkloadOperationRefusal = 3 + // Owned by the module principal the alias names: a module does not reach + // itself through this route. + WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET WorkloadOperationRefusal = 4 + // The owner is not a declared module principal, or the seal is not an ACTIVE + // installation in the tenant installed under that principal. + WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS WorkloadOperationRefusal = 5 +) + +// Enum value maps for WorkloadOperationRefusal. +var ( + WorkloadOperationRefusal_name = map[int32]string{ + 0: "WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED", + 1: "WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED", + 2: "WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE", + 3: "WORKLOAD_OPERATION_REFUSAL_NOT_OPERATION_CONTEXT", + 4: "WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET", + 5: "WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS", + } + WorkloadOperationRefusal_value = map[string]int32{ + "WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED": 0, + "WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED": 1, + "WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE": 2, + "WORKLOAD_OPERATION_REFUSAL_NOT_OPERATION_CONTEXT": 3, + "WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET": 4, + "WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS": 5, + } +) + +func (x WorkloadOperationRefusal) Enum() *WorkloadOperationRefusal { + p := new(WorkloadOperationRefusal) + *p = x + return p +} + +func (x WorkloadOperationRefusal) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (WorkloadOperationRefusal) Descriptor() protoreflect.EnumDescriptor { + return file_saas_accounts_v1_workload_operation_admission_proto_enumTypes[0].Descriptor() +} + +func (WorkloadOperationRefusal) Type() protoreflect.EnumType { + return &file_saas_accounts_v1_workload_operation_admission_proto_enumTypes[0] +} + +func (x WorkloadOperationRefusal) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use WorkloadOperationRefusal.Descriptor instead. +func (WorkloadOperationRefusal) EnumDescriptor() ([]byte, []int) { + return file_saas_accounts_v1_workload_operation_admission_proto_rawDescGZIP(), []int{0} +} + +type AdmitWorkloadOperationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The capability as presented, encoded. It is inspected and read, never + // verified: nothing it carries is consumed. + WorkContext string `protobuf:"bytes,1,opt,name=work_context,json=workContext,proto3" json:"work_context,omitempty"` + // The route alias the gateway resolved from the request path. The capability + // must be addressed to it. + Alias string `protobuf:"bytes,2,opt,name=alias,proto3" json:"alias,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdmitWorkloadOperationRequest) Reset() { + *x = AdmitWorkloadOperationRequest{} + mi := &file_saas_accounts_v1_workload_operation_admission_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdmitWorkloadOperationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdmitWorkloadOperationRequest) ProtoMessage() {} + +func (x *AdmitWorkloadOperationRequest) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_workload_operation_admission_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AdmitWorkloadOperationRequest.ProtoReflect.Descriptor instead. +func (*AdmitWorkloadOperationRequest) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_workload_operation_admission_proto_rawDescGZIP(), []int{0} +} + +func (x *AdmitWorkloadOperationRequest) GetWorkContext() string { + if x != nil { + return x.WorkContext + } + return "" +} + +func (x *AdmitWorkloadOperationRequest) GetAlias() string { + if x != nil { + return x.Alias + } + return "" +} + +type AdmitWorkloadOperationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Admitted bool `protobuf:"varint,1,opt,name=admitted,proto3" json:"admitted,omitempty"` + // Set when admitted is false. + Refusal WorkloadOperationRefusal `protobuf:"varint,2,opt,name=refusal,proto3,enum=saas.accounts.v1.WorkloadOperationRefusal" json:"refusal,omitempty"` + // The signed facts the gateway logs and forwards, set only when admitted: + // the tenant, the installation the capability is sealed to (the CALLING + // module's), and the task. + TenantId string `protobuf:"bytes,3,opt,name=tenant_id,json=tenantId,proto3" json:"tenant_id,omitempty"` + InstallationId string `protobuf:"bytes,4,opt,name=installation_id,json=installationId,proto3" json:"installation_id,omitempty"` + TaskId string `protobuf:"bytes,5,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdmitWorkloadOperationResponse) Reset() { + *x = AdmitWorkloadOperationResponse{} + mi := &file_saas_accounts_v1_workload_operation_admission_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdmitWorkloadOperationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdmitWorkloadOperationResponse) ProtoMessage() {} + +func (x *AdmitWorkloadOperationResponse) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_workload_operation_admission_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AdmitWorkloadOperationResponse.ProtoReflect.Descriptor instead. +func (*AdmitWorkloadOperationResponse) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_workload_operation_admission_proto_rawDescGZIP(), []int{1} +} + +func (x *AdmitWorkloadOperationResponse) GetAdmitted() bool { + if x != nil { + return x.Admitted + } + return false +} + +func (x *AdmitWorkloadOperationResponse) GetRefusal() WorkloadOperationRefusal { + if x != nil { + return x.Refusal + } + return WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED +} + +func (x *AdmitWorkloadOperationResponse) GetTenantId() string { + if x != nil { + return x.TenantId + } + return "" +} + +func (x *AdmitWorkloadOperationResponse) GetInstallationId() string { + if x != nil { + return x.InstallationId + } + return "" +} + +func (x *AdmitWorkloadOperationResponse) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +var File_saas_accounts_v1_workload_operation_admission_proto protoreflect.FileDescriptor + +const file_saas_accounts_v1_workload_operation_admission_proto_rawDesc = "" + + "\n" + + "3saas/accounts/v1/workload_operation_admission.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\"q\n" + + "\x1dAdmitWorkloadOperationRequest\x12.\n" + + "\fwork_context\x18\x01 \x01(\tB\v\xbaH\br\x06\x10\x01\x18\x80\x80\x02R\vworkContext\x12 \n" + + "\x05alias\x18\x02 \x01(\tB\n" + + "\xbaH\ar\x05\x10\x01\x18\x80\x04R\x05alias\"\xe1\x01\n" + + "\x1eAdmitWorkloadOperationResponse\x12\x1a\n" + + "\badmitted\x18\x01 \x01(\bR\badmitted\x12D\n" + + "\arefusal\x18\x02 \x01(\x0e2*.saas.accounts.v1.WorkloadOperationRefusalR\arefusal\x12\x1b\n" + + "\ttenant_id\x18\x03 \x01(\tR\btenantId\x12'\n" + + "\x0finstallation_id\x18\x04 \x01(\tR\x0einstallationId\x12\x17\n" + + "\atask_id\x18\x05 \x01(\tR\x06taskId*\xbc\x02\n" + + "\x18WorkloadOperationRefusal\x12*\n" + + "&WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED\x10\x00\x12,\n" + + "(WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED\x10\x01\x120\n" + + ",WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE\x10\x02\x124\n" + + "0WORKLOAD_OPERATION_REFUSAL_NOT_OPERATION_CONTEXT\x10\x03\x12.\n" + + "*WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET\x10\x04\x12.\n" + + "*WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS\x10\x05B\xcb\x01\n" + + "\x14com.saas.accounts.v1B\x1fWorkloadOperationAdmissionProtoP\x01Z0auth-gateway/pkg/gen/saas/accounts/v1;accountsv1\xa2\x02\x03SAX\xaa\x02\x10Saas.Accounts.V1\xca\x02\x10Saas\\Accounts\\V1\xe2\x02\x1cSaas\\Accounts\\V1\\GPBMetadata\xea\x02\x12Saas::Accounts::V1b\x06proto3" + +var ( + file_saas_accounts_v1_workload_operation_admission_proto_rawDescOnce sync.Once + file_saas_accounts_v1_workload_operation_admission_proto_rawDescData []byte +) + +func file_saas_accounts_v1_workload_operation_admission_proto_rawDescGZIP() []byte { + file_saas_accounts_v1_workload_operation_admission_proto_rawDescOnce.Do(func() { + file_saas_accounts_v1_workload_operation_admission_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_workload_operation_admission_proto_rawDesc), len(file_saas_accounts_v1_workload_operation_admission_proto_rawDesc))) + }) + return file_saas_accounts_v1_workload_operation_admission_proto_rawDescData +} + +var file_saas_accounts_v1_workload_operation_admission_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_saas_accounts_v1_workload_operation_admission_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_saas_accounts_v1_workload_operation_admission_proto_goTypes = []any{ + (WorkloadOperationRefusal)(0), // 0: saas.accounts.v1.WorkloadOperationRefusal + (*AdmitWorkloadOperationRequest)(nil), // 1: saas.accounts.v1.AdmitWorkloadOperationRequest + (*AdmitWorkloadOperationResponse)(nil), // 2: saas.accounts.v1.AdmitWorkloadOperationResponse +} +var file_saas_accounts_v1_workload_operation_admission_proto_depIdxs = []int32{ + 0, // 0: saas.accounts.v1.AdmitWorkloadOperationResponse.refusal:type_name -> saas.accounts.v1.WorkloadOperationRefusal + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_saas_accounts_v1_workload_operation_admission_proto_init() } +func file_saas_accounts_v1_workload_operation_admission_proto_init() { + if File_saas_accounts_v1_workload_operation_admission_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_workload_operation_admission_proto_rawDesc), len(file_saas_accounts_v1_workload_operation_admission_proto_rawDesc)), + NumEnums: 1, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_saas_accounts_v1_workload_operation_admission_proto_goTypes, + DependencyIndexes: file_saas_accounts_v1_workload_operation_admission_proto_depIdxs, + EnumInfos: file_saas_accounts_v1_workload_operation_admission_proto_enumTypes, + MessageInfos: file_saas_accounts_v1_workload_operation_admission_proto_msgTypes, + }.Build() + File_saas_accounts_v1_workload_operation_admission_proto = out.File + file_saas_accounts_v1_workload_operation_admission_proto_goTypes = nil + file_saas_accounts_v1_workload_operation_admission_proto_depIdxs = nil +} diff --git a/module/services/auth-gateway/proto/saas/accounts/v1/workload_operation_admission.proto b/module/services/auth-gateway/proto/saas/accounts/v1/workload_operation_admission.proto new file mode 100644 index 000000000..dda95d168 --- /dev/null +++ b/module/services/auth-gateway/proto/saas/accounts/v1/workload_operation_admission.proto @@ -0,0 +1,71 @@ +syntax = "proto3"; + +package saas.accounts.v1; + +import "buf/validate/validate.proto"; + +// Workload operation admission: the issuer's answer to whether a capability +// presented with no bearer at a declared module route is a module operation +// context, the one shape that route admits without a person. +// +// A forwarding hop cannot answer it. Core's Inspector, which is all a hop runs, +// withholds the owner, the owner's kind, the actor chain and the operation +// binding, and those are exactly what tell a module's unattended work from a +// person's capability. Accounts signed the capability and can read its own +// claims, so the gateway asks it. WorkContextService/AdmitWorkloadOperation +// takes and answers these messages. +// +// They live in their own file, with deliberately minimal imports, so the +// auth-gateway can generate the same types from the same source proto, as it +// does for installation_listing.proto and solution_entitlements.proto. The +// gateway's copy is this file, byte for byte. + +// WorkloadOperationRefusal is why a capability was not admitted. The gateway +// logs it and answers every refusal alike. +enum WorkloadOperationRefusal { + WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED = 0; + // Not a capability this issuer signed, inside its window, addressed to the + // alias; or the alias is one no operation context is minted for. + WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED = 1; + // Owned by a principal that is not a service: a person, an agent, or a kind + // this issuer does not name. + WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE = 2; + // A service principal's capability that is not shaped as a module operation + // context: it carries an actor chain, a grant hop, an operation binding, a + // parent session or an owner agent, names no authority scope, or has another + // replay policy or a longer window. + WORKLOAD_OPERATION_REFUSAL_NOT_OPERATION_CONTEXT = 3; + // Owned by the module principal the alias names: a module does not reach + // itself through this route. + WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET = 4; + // The owner is not a declared module principal, or the seal is not an ACTIVE + // installation in the tenant installed under that principal. + WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS = 5; +} + +message AdmitWorkloadOperationRequest { + // The capability as presented, encoded. It is inspected and read, never + // verified: nothing it carries is consumed. + string work_context = 1 [ + (buf.validate.field).string.min_len = 1, + (buf.validate.field).string.max_len = 32768 + ]; + // The route alias the gateway resolved from the request path. The capability + // must be addressed to it. + string alias = 2 [ + (buf.validate.field).string.min_len = 1, + (buf.validate.field).string.max_len = 512 + ]; +} + +message AdmitWorkloadOperationResponse { + bool admitted = 1; + // Set when admitted is false. + WorkloadOperationRefusal refusal = 2; + // The signed facts the gateway logs and forwards, set only when admitted: + // the tenant, the installation the capability is sealed to (the CALLING + // module's), and the task. + string tenant_id = 3; + string installation_id = 4; + string task_id = 5; +} From c1e2be2a556680e897c6a5f91774959f5c9c78f7 Mon Sep 17 00:00:00 2001 From: saurabhmaheshwari-obin <242053479+saurabhmaheshwari-obin@users.noreply.github.com> Date: Sat, 24 Oct 2026 16:40:27 -0400 Subject: [PATCH 11/17] fix(accounts): drop Installation.registry_principal_id, which nothing reads now registry_principal_id = 14 was added to the accounts-owned installation_listing.proto so the gateway's bearer-less arm could refuse a seal on the alias's module principal. The arm now asks AdmitWorkloadOperation, which reads the owner itself, and no longer reads the field; nothing else does. This restores, byte for byte, the state before the two commits that added it: installation_listing.proto (accounts' file and the gateway's vendored copy), the installation read columns in postgres_installations.go, and their tests (TestInstallationReadsCarryTheRegistryPrincipal and TestModulePrincipalIDIsPinnedForTheGateway). The column itself predates this branch (migration 32) and is untouched. The field never shipped in a release, so buf breaking against the base sees no change to Installation. Regenerated with Codefly source candidate 5234ce24 (proto for accounts and the gateway, go generate, contracts, the saas-sdk library); every generated file the field touched is identical to its state before it was added. Co-Authored-By: Claude Opus 5.5 --- .../api/accounts/authority/contract.binpb | Bin 1206731 -> 1205712 bytes .../accounts/v1/installation_listing.proto | 15 ------- .../api/accounts/connect/contract.binpb | Bin 1206731 -> 1205712 bytes .../accounts/v1/installation_listing.proto | 15 ------- .../api/auth-gateway/grpc/contract.binpb | Bin 361671 -> 360652 bytes .../accounts/v1/installation_listing.proto | 15 ------- module/contracts/api/catalog.codefly.json | 6 +-- module/module.package.codefly.yaml | 6 +-- .../pkg/business/module_capabilities_test.go | 16 -------- .../accounts/v1/installation_listing.pb.go | 28 ++----------- .../pkg/infra/installation_dynamic_test.go | 37 ------------------ .../code/pkg/infra/postgres_installations.go | 10 ++--- .../generated/openapi-raw/api.swagger.json | 4 -- .../accounts/openapi/api.swagger.json | 4 -- .../accounts/v1/installation_listing.proto | 15 ------- .../accounts/v1/installation_listing.pb.go | 28 ++----------- .../accounts/v1/installation_listing.proto | 15 ------- .../generated/contract/catalog.codefly.json | 2 +- .../generated/contract/contract.binpb | Bin 1206731 -> 1205712 bytes .../saas-sdk/generated/library.codefly.yaml | 2 +- .../accounts/v1/installation_listing_pb.ts | 19 +-------- .../accounts/v1/installation_listing_pb.ts | 19 +-------- 22 files changed, 21 insertions(+), 235 deletions(-) diff --git a/module/contracts/api/accounts/authority/contract.binpb b/module/contracts/api/accounts/authority/contract.binpb index 7f8edac8b9482f9e0df01754708965358210c69b..9931b6eab4fece02bc19ff8f656858c59edd986c 100644 GIT binary patch delta 814 zcmYk3OKVd>6vywJnVZ}@ljd=oq#O&?pdDg@t5MyWBTSY_n-YpE&30unX+I05L-w*FnOY~+UeF@x8`r(&s+-LJrNQD zxhE#n{(hRr9n&y&{K?nYQgHT_xM-1E!TP@V&C_pz10xPwHrk~46r^9>6NTW+M^Pe! zWlQn|HyHuLbZ8wcE;v)9;DT-uBs-;vA~~~~ETrstp60faS-W;Jk0-fvQJ=&d+JG$o zNy5FjsR6S1jmWF@PPgrkKZ(gwBX>kk-lAG>m#0LAyTY@G#Fqxm`hUImVBH;j9v_!LQgHfKLuhsY)CM> y6|{vI($V5|d9B`+>nnBt#TRiiJtTHP3-Eh}yK*eB5?C`>NvsrBI*{YcqtRc|`hz3@ delta 1743 zcmY*ZQD_`R7`~a^yX?(g@6t^!4GAQZYH6iuOwm>-wWWe$d}*n^O5NN}a?|Y1p1X5i zLTawUK~iZTeaXJ|y{HkY2SVR0)V3Hw1O-9!s^EiyPa^&^*}2q*P59>j|L_0)@1MEv zp3FV@e(q)$b#d3~+TDD&&>iSH-C~#AcmMjt`tmK3{=Vx@r?0$JnN4RO7pBk;J^87- zgfDp)Ni}ARJdyFrwPwskjW_-98V^Rx6rCu(=6?02^CLpBwS&;rA(MHa&T|oPu{cfJF_)4F>Pup{sV@R5*as}8wYo1BSujH{(I$(NNI+@LZ~F5* z6fY54iyBQoW{R5wpXfwTuFQ&Bl2Je8 zelkN}hbdS3gUdPu7wdFx-{iNWuqAmUra@UYCCsm9PO^|v8napy2M`bp2pdf>CK93g zqb7*jna7iY&NFa~5iO$z3=KiqGF&#$e1fO{o+*!X=Kb2z0#^$@M@95MlhlG1SRAw9 z1tkL7X`(~|C-jJ5sy3H(Rc0>4LA_ex!R8JIQcPwT%7YFQQf;=ug@ntNm__P=V4M2` z)%-YKf$KxLgkAS9sp?@i2LFM8X*b~7w#1T%+5*;~?-iUJ6?z}gqF;Y>hP+3z*MZ=5 z6ekR_08Kqd4(-1j2j0r_p+zdhQ7^|?FOGZGb^9zCB*pBYJmOhP_DS*p6Ko?dpX5G5 zXzVjg)AHx;Xz$7+cM^3Hhw26h=io6)c#JN*ML-H%Fd!{j%ld4Xve)3Kisy~^MhCjH|Zckk44b=+hF_wvMKhA>E4#RFt} zzZQG;Dqc(fdF*a?R-MBp2})LntJ)ZZF&MKtvSx!G2gbml$AXh1YgVREp8)R-n>>30 zBp=$PjKs=&_Duv{ysI?>0ie;b%k*;7f4{|Ot(8`ux^J9RyFl1NDqb8i#X5cD2zjJ@ z$6jF_s}w+Ld8*&q)yk5 Y?Yi)l;5!6g8NLd9gS}(Bhwp9w2Lvv!A^-pY diff --git a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installation_listing.proto b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installation_listing.proto index 6b3061c5f..57150f92b 100644 --- a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installation_listing.proto +++ b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installation_listing.proto @@ -92,21 +92,6 @@ message Installation { // Why the install step failed: the solution's bounded refusal reason, or a // fixed reason when it never answered. Empty unless status is FAILED. string failure_reason = 13 [(buf.validate.field).string.max_len = 512]; - // The module principal this installation was installed under: the registry - // principal whose delivered authority named the target's binding, written at - // install and never changed. A person's capability for a module audience is - // sealed to the active installation whose registry principal is that - // audience's, so a reader that must tell a person's seal from unattended work - // compares this, not the route alias. Every install through InstallSolution, - // a module's or a solution's, records one. Empty only on a row written before - // the column existed (it is deliberately not backfilled: no principal is - // inferred) or one the store wrote with no registry principal. A capability - // a module or solution identity mints is sealed only to a row carrying its - // principal. A reader treats empty as unknown, never as "not this principal". - string registry_principal_id = 14 [ - (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, - (buf.validate.field).string.uuid = true - ]; } // ListInstallationsRequest asks what one organization has installed. Every other diff --git a/module/contracts/api/accounts/connect/contract.binpb b/module/contracts/api/accounts/connect/contract.binpb index 7f8edac8b9482f9e0df01754708965358210c69b..9931b6eab4fece02bc19ff8f656858c59edd986c 100644 GIT binary patch delta 814 zcmYk3OKVd>6vywJnVZ}@ljd=oq#O&?pdDg@t5MyWBTSY_n-YpE&30unX+I05L-w*FnOY~+UeF@x8`r(&s+-LJrNQD zxhE#n{(hRr9n&y&{K?nYQgHT_xM-1E!TP@V&C_pz10xPwHrk~46r^9>6NTW+M^Pe! zWlQn|HyHuLbZ8wcE;v)9;DT-uBs-;vA~~~~ETrstp60faS-W;Jk0-fvQJ=&d+JG$o zNy5FjsR6S1jmWF@PPgrkKZ(gwBX>kk-lAG>m#0LAyTY@G#Fqxm`hUImVBH;j9v_!LQgHfKLuhsY)CM> y6|{vI($V5|d9B`+>nnBt#TRiiJtTHP3-Eh}yK*eB5?C`>NvsrBI*{YcqtRc|`hz3@ delta 1743 zcmY*ZQD_`R7`~a^yX?(g@6t^!4GAQZYH6iuOwm>-wWWe$d}*n^O5NN}a?|Y1p1X5i zLTawUK~iZTeaXJ|y{HkY2SVR0)V3Hw1O-9!s^EiyPa^&^*}2q*P59>j|L_0)@1MEv zp3FV@e(q)$b#d3~+TDD&&>iSH-C~#AcmMjt`tmK3{=Vx@r?0$JnN4RO7pBk;J^87- zgfDp)Ni}ARJdyFrwPwskjW_-98V^Rx6rCu(=6?02^CLpBwS&;rA(MHa&T|oPu{cfJF_)4F>Pup{sV@R5*as}8wYo1BSujH{(I$(NNI+@LZ~F5* z6fY54iyBQoW{R5wpXfwTuFQ&Bl2Je8 zelkN}hbdS3gUdPu7wdFx-{iNWuqAmUra@UYCCsm9PO^|v8napy2M`bp2pdf>CK93g zqb7*jna7iY&NFa~5iO$z3=KiqGF&#$e1fO{o+*!X=Kb2z0#^$@M@95MlhlG1SRAw9 z1tkL7X`(~|C-jJ5sy3H(Rc0>4LA_ex!R8JIQcPwT%7YFQQf;=ug@ntNm__P=V4M2` z)%-YKf$KxLgkAS9sp?@i2LFM8X*b~7w#1T%+5*;~?-iUJ6?z}gqF;Y>hP+3z*MZ=5 z6ekR_08Kqd4(-1j2j0r_p+zdhQ7^|?FOGZGb^9zCB*pBYJmOhP_DS*p6Ko?dpX5G5 zXzVjg)AHx;Xz$7+cM^3Hhw26h=io6)c#JN*ML-H%Fd!{j%ld4Xve)3Kisy~^MhCjH|Zckk44b=+hF_wvMKhA>E4#RFt} zzZQG;Dqc(fdF*a?R-MBp2})LntJ)ZZF&MKtvSx!G2gbml$AXh1YgVREp8)R-n>>30 zBp=$PjKs=&_Duv{ysI?>0ie;b%k*;7f4{|Ot(8`ux^J9RyFl1NDqb8i#X5cD2zjJ@ z$6jF_s}w+Ld8*&q)yk5 Y?Yi)l;5!6g8NLd9gS}(Bhwp9w2Lvv!A^-pY diff --git a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installation_listing.proto b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installation_listing.proto index 6b3061c5f..57150f92b 100644 --- a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installation_listing.proto +++ b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installation_listing.proto @@ -92,21 +92,6 @@ message Installation { // Why the install step failed: the solution's bounded refusal reason, or a // fixed reason when it never answered. Empty unless status is FAILED. string failure_reason = 13 [(buf.validate.field).string.max_len = 512]; - // The module principal this installation was installed under: the registry - // principal whose delivered authority named the target's binding, written at - // install and never changed. A person's capability for a module audience is - // sealed to the active installation whose registry principal is that - // audience's, so a reader that must tell a person's seal from unattended work - // compares this, not the route alias. Every install through InstallSolution, - // a module's or a solution's, records one. Empty only on a row written before - // the column existed (it is deliberately not backfilled: no principal is - // inferred) or one the store wrote with no registry principal. A capability - // a module or solution identity mints is sealed only to a row carrying its - // principal. A reader treats empty as unknown, never as "not this principal". - string registry_principal_id = 14 [ - (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, - (buf.validate.field).string.uuid = true - ]; } // ListInstallationsRequest asks what one organization has installed. Every other diff --git a/module/contracts/api/auth-gateway/grpc/contract.binpb b/module/contracts/api/auth-gateway/grpc/contract.binpb index a650b8d950ad727f06df42c0e003348bf7bb8e32..279996772b17d7e454b60801e8c1b4bdb5852726 100644 GIT binary patch delta 707 zcmYk3y=xRv6vgM=cgMWB8^!mjy?f7@bI*Jn#GiWcqrU1>U%%!r1A2QU z`#RuMJ;`qz@?FaLh=)4*zAR%l41)b^_=cDA*~fg@sI~moQ~oWLcO=Bh@_MkD|9QqM zs=ICy8JCb11) zp$cC(H7J?+rjl#*t?kV$f6l|DMs3z_!@DuRP!b_hlg8wDq$SHuYGr4C@^PzaPWdHB zn$?<|EUqAfrMSh&V~~fCN8@F>rOSlH3w-af{Qm-z`#fG+YRucH;&u@kqjt9Vi`VAb zk-u8I$9P&kxI&R@CwQ1&eZi}$E25cj<&GKML=>Qb4owurf*g!G2Vk)?`EL=Xo$`1h zSKKa&bN>V6z{`}QvNMbz+l8!xD7tIIyh_RL~&m8O8LPDu(wkb{eUgm0yl zc~6j~3+Ft+K0SY-=vR{pE%y|<*+48{L31-Vlk4^E#X%=jXc%Y}`AX=Ni&ROg z*~EEY=^Lmj8%U$$sL)$Z^2i`MMUB;C2{X&MkS>ZeMyEtcc#wxhtCJdGR2brXsLi$& zD@ojfTha~|KtwlA*0}KFR$C^@l56lqF{-VVamPS=sAak&Uct~CrA&mAIP0u7#f8BR z-ipJHmA(jR#n7-Lw;(M-R%xRAI0+Dl6c}4=_!LK>{jn3o-J$2p3jQgc%kV+aiiBFn zrGw;C+Q3F^GYj>yN+jmQ9wB*I`fDrN-iI^5|8dPOa7889{gYM{+ULxvK@;?hF=khr zrY-xw>vqEr7yEF)%um{_oRTy4T+oRkd z{z0H}cVvoftwnJ+!dmishm(_o1!d?lBsjUmdGU4#-cEh4L=MpW$QXN{faKT=K^@v3R%Iw3RPQ1J%*FV>x3)BfUTY=LB{ zEAn21R`6;KUhN1TEszE0$`C}E`gn;T%6Jnca)Bma%q5@TvGT7^?v!BJ$Pz;YPQS<_xjw40#QiXq(}IzK`r&zCf(}I|H*D;o9?kP z2_>78Q@KZkQh2o4*kaJ*VhjpB4o;45IfcUf1iT+G{^$uvK4hZ>iBt91H3Bc*%{7An zXnN+NxY(9gJ4*PS-t&L4S5Mou5b6*cuTPZ4d3xhGziU@Oz1j|`+#vn;zF_A_Ho_f7 zk-tOS**u@pO3h=tgpOex#sG69${=!g$@nlA+`9z+*v^!~Tee4F?w}O8KxHY(A)GC8 zQM#Ae+EK`IyxKmg+$03+c9@-DpSbgRszWQxV+VxVE6EsOj-D-p=y^a!=1Pdt19D=q qgy?cWo_?l)1V$cOBbzxBLG-Zq(U6vywJnVZ}@ljd=oq#O&?pdDg@t5MyWBTSY_n-YpE&30unX+I05L-w*FnOY~+UeF@x8`r(&s+-LJrNQD zxhE#n{(hRr9n&y&{K?nYQgHT_xM-1E!TP@V&C_pz10xPwHrk~46r^9>6NTW+M^Pe! zWlQn|HyHuLbZ8wcE;v)9;DT-uBs-;vA~~~~ETrstp60faS-W;Jk0-fvQJ=&d+JG$o zNy5FjsR6S1jmWF@PPgrkKZ(gwBX>kk-lAG>m#0LAyTY@G#Fqxm`hUImVBH;j9v_!LQgHfKLuhsY)CM> y6|{vI($V5|d9B`+>nnBt#TRiiJtTHP3-Eh}yK*eB5?C`>NvsrBI*{YcqtRc|`hz3@ delta 1743 zcmY*ZQD_`R7`~a^yX?(g@6t^!4GAQZYH6iuOwm>-wWWe$d}*n^O5NN}a?|Y1p1X5i zLTawUK~iZTeaXJ|y{HkY2SVR0)V3Hw1O-9!s^EiyPa^&^*}2q*P59>j|L_0)@1MEv zp3FV@e(q)$b#d3~+TDD&&>iSH-C~#AcmMjt`tmK3{=Vx@r?0$JnN4RO7pBk;J^87- zgfDp)Ni}ARJdyFrwPwskjW_-98V^Rx6rCu(=6?02^CLpBwS&;rA(MHa&T|oPu{cfJF_)4F>Pup{sV@R5*as}8wYo1BSujH{(I$(NNI+@LZ~F5* z6fY54iyBQoW{R5wpXfwTuFQ&Bl2Je8 zelkN}hbdS3gUdPu7wdFx-{iNWuqAmUra@UYCCsm9PO^|v8napy2M`bp2pdf>CK93g zqb7*jna7iY&NFa~5iO$z3=KiqGF&#$e1fO{o+*!X=Kb2z0#^$@M@95MlhlG1SRAw9 z1tkL7X`(~|C-jJ5sy3H(Rc0>4LA_ex!R8JIQcPwT%7YFQQf;=ug@ntNm__P=V4M2` z)%-YKf$KxLgkAS9sp?@i2LFM8X*b~7w#1T%+5*;~?-iUJ6?z}gqF;Y>hP+3z*MZ=5 z6ekR_08Kqd4(-1j2j0r_p+zdhQ7^|?FOGZGb^9zCB*pBYJmOhP_DS*p6Ko?dpX5G5 zXzVjg)AHx;Xz$7+cM^3Hhw26h=io6)c#JN*ML-H%Fd!{j%ld4Xve)3Kisy~^MhCjH|Zckk44b=+hF_wvMKhA>E4#RFt} zzZQG;Dqc(fdF*a?R-MBp2})LntJ)ZZF&MKtvSx!G2gbml$AXh1YgVREp8)R-n>>30 zBp=$PjKs=&_Duv{ysI?>0ie;b%k*;7f4{|Ot(8`ux^J9RyFl1NDqb8i#X5cD2zjJ@ z$6jF_s}w+Ld8*&q)yk5 Y?Yi)l;5!6g8NLd9gS}(Bhwp9w2Lvv!A^-pY diff --git a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml index a4156106f..a67cb66b0 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml +++ b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml @@ -12,7 +12,7 @@ sources: module: saas-starter service: accounts endpoint: connect - contract-digest: sha256:6100316ada259d0f5b29458a686ca0dd430948b6770b3c0d69d2153716ce57d7 + contract-digest: sha256:d0d8f6519a5a1eb4394265f3094faa19fee5a24afb3feeff4b33ea4ad5b07e5c services: - APIKeyService - AccessibleScopeService diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installation_listing_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installation_listing_pb.ts index 5ab18f66b..934ddc7e4 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installation_listing_pb.ts +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installation_listing_pb.ts @@ -13,7 +13,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/installation_listing.proto. */ export const file_saas_accounts_v1_installation_listing: GenFile = /*@__PURE__*/ - fileDesc("CitzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbl9saXN0aW5nLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIqUECgxJbnN0YWxsYXRpb24SFAoCaWQYASABKAlCCLpIBXIDsAEBEhgKBm9yZ19pZBgCIAEoCUIIukgFcgOwAQESJAoSYWdlbnRfcHJpbmNpcGFsX2lkGAMgASgJQgi6SAVyA7ABARIkChJvd25lcl9wcmluY2lwYWxfaWQYBSABKAlCCLpIBXIDsAEBEh4KFmNvX293bmVyX3ByaW5jaXBhbF9pZHMYBiADKAkSJAoScm9vdF9zY29wZV9ub2RlX2lkGAcgASgJQgi6SAVyA7ABARI0CgZzdGF0dXMYCCABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN0YXR1cxIuCgpjcmVhdGVkX2F0GAkgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIzCgpyZXZva2VkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgAiAEBEh4KCXRhcmdldF9pZBgLIAEoCUILukgI2AEBcgOwAQESIAoOcmV2b2tlZF9yZWFzb24YDCABKAlCCLpIBXIDGIAEEiAKDmZhaWx1cmVfcmVhc29uGA0gASgJQgi6SAVyAxiABBIqChVyZWdpc3RyeV9wcmluY2lwYWxfaWQYDiABKAlCC7pICNgBAXIDsAEBQg0KC19yZXZva2VkX2F0SgQIBBAFUhNzb2x1dGlvbl9pZGVudGlmaWVyIp0BChhMaXN0SW5zdGFsbGF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARI0CgZzdGF0dXMYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN0YXR1cxIdCglwYWdlX3NpemUYAyABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgEIAEoCSKBAQoTSW5zdGFsbGF0aW9uU3VtbWFyeRI0CgxpbnN0YWxsYXRpb24YASABKAsyHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbhI0CgZoZWFsdGgYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbkhlYWx0aCJyChlMaXN0SW5zdGFsbGF0aW9uc1Jlc3BvbnNlEjwKDWluc3RhbGxhdGlvbnMYASADKAsyJS5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN1bW1hcnkSFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJKrsBChJJbnN0YWxsYXRpb25TdGF0dXMSIwofSU5TVEFMTEFUSU9OX1NUQVRVU19VTlNQRUNJRklFRBAAEh4KGklOU1RBTExBVElPTl9TVEFUVVNfQUNUSVZFEAESHwobSU5TVEFMTEFUSU9OX1NUQVRVU19SRVZPS0VEEAISHwobSU5TVEFMTEFUSU9OX1NUQVRVU19QRU5ESU5HEAMSHgoaSU5TVEFMTEFUSU9OX1NUQVRVU19GQUlMRUQQBCqEAgoSSW5zdGFsbGF0aW9uSGVhbHRoEiMKH0lOU1RBTExBVElPTl9IRUFMVEhfVU5TUEVDSUZJRUQQABIfChtJTlNUQUxMQVRJT05fSEVBTFRIX0hFQUxUSFkQARIpCiVJTlNUQUxMQVRJT05fSEVBTFRIX05PX0VMSUdJQkxFX09XTkVSEAISJQohSU5TVEFMTEFUSU9OX0hFQUxUSF9BR0VOVF9SRVZPS0VEEAMSJgoiSU5TVEFMTEFUSU9OX0hFQUxUSF9BR0VOVF9ESVNBQkxFRBAEEi4KKklOU1RBTExBVElPTl9IRUFMVEhfU1RBTkRJTkdfR1JBTlRfTUlTU0lORxAFYgZwcm90bzM", [file_buf_validate_validate, file_google_protobuf_timestamp]); + fileDesc("CitzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbl9saXN0aW5nLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIvkDCgxJbnN0YWxsYXRpb24SFAoCaWQYASABKAlCCLpIBXIDsAEBEhgKBm9yZ19pZBgCIAEoCUIIukgFcgOwAQESJAoSYWdlbnRfcHJpbmNpcGFsX2lkGAMgASgJQgi6SAVyA7ABARIkChJvd25lcl9wcmluY2lwYWxfaWQYBSABKAlCCLpIBXIDsAEBEh4KFmNvX293bmVyX3ByaW5jaXBhbF9pZHMYBiADKAkSJAoScm9vdF9zY29wZV9ub2RlX2lkGAcgASgJQgi6SAVyA7ABARI0CgZzdGF0dXMYCCABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN0YXR1cxIuCgpjcmVhdGVkX2F0GAkgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIzCgpyZXZva2VkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgAiAEBEh4KCXRhcmdldF9pZBgLIAEoCUILukgI2AEBcgOwAQESIAoOcmV2b2tlZF9yZWFzb24YDCABKAlCCLpIBXIDGIAEEiAKDmZhaWx1cmVfcmVhc29uGA0gASgJQgi6SAVyAxiABEINCgtfcmV2b2tlZF9hdEoECAQQBVITc29sdXRpb25faWRlbnRpZmllciKdAQoYTGlzdEluc3RhbGxhdGlvbnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoGc3RhdHVzGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdGF0dXMSHQoJcGFnZV9zaXplGAMgASgFQgq6SAcaBRj0AygAEhIKCnBhZ2VfdG9rZW4YBCABKAkigQEKE0luc3RhbGxhdGlvblN1bW1hcnkSNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SNAoGaGVhbHRoGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25IZWFsdGgicgoZTGlzdEluc3RhbGxhdGlvbnNSZXNwb25zZRI8Cg1pbnN0YWxsYXRpb25zGAEgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdW1tYXJ5EhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSq7AQoSSW5zdGFsbGF0aW9uU3RhdHVzEiMKH0lOU1RBTExBVElPTl9TVEFUVVNfVU5TUEVDSUZJRUQQABIeChpJTlNUQUxMQVRJT05fU1RBVFVTX0FDVElWRRABEh8KG0lOU1RBTExBVElPTl9TVEFUVVNfUkVWT0tFRBACEh8KG0lOU1RBTExBVElPTl9TVEFUVVNfUEVORElORxADEh4KGklOU1RBTExBVElPTl9TVEFUVVNfRkFJTEVEEAQqhAIKEkluc3RhbGxhdGlvbkhlYWx0aBIjCh9JTlNUQUxMQVRJT05fSEVBTFRIX1VOU1BFQ0lGSUVEEAASHwobSU5TVEFMTEFUSU9OX0hFQUxUSF9IRUFMVEhZEAESKQolSU5TVEFMTEFUSU9OX0hFQUxUSF9OT19FTElHSUJMRV9PV05FUhACEiUKIUlOU1RBTExBVElPTl9IRUFMVEhfQUdFTlRfUkVWT0tFRBADEiYKIklOU1RBTExBVElPTl9IRUFMVEhfQUdFTlRfRElTQUJMRUQQBBIuCipJTlNUQUxMQVRJT05fSEVBTFRIX1NUQU5ESU5HX0dSQU5UX01JU1NJTkcQBWIGcHJvdG8z", [file_buf_validate_validate, file_google_protobuf_timestamp]); /** * Installation is one solution's bound identity in an org. Server-assigned ids; @@ -104,23 +104,6 @@ export type Installation = Message<"saas.accounts.v1.Installation"> & { * @generated from field: string failure_reason = 13; */ failureReason: string; - - /** - * The module principal this installation was installed under: the registry - * principal whose delivered authority named the target's binding, written at - * install and never changed. A person's capability for a module audience is - * sealed to the active installation whose registry principal is that - * audience's, so a reader that must tell a person's seal from unattended work - * compares this, not the route alias. Every install through InstallSolution, - * a module's or a solution's, records one. Empty only on a row written before - * the column existed (it is deliberately not backfilled: no principal is - * inferred) or one the store wrote with no registry principal. A capability - * a module or solution identity mints is sealed only to a row carrying its - * principal. A reader treats empty as unknown, never as "not this principal". - * - * @generated from field: string registry_principal_id = 14; - */ - registryPrincipalId: string; }; /** diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/installation_listing_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/installation_listing_pb.ts index b5a522fad..c425a987b 100644 --- a/module/services/frontend/code/src/gen/saas/accounts/v1/installation_listing_pb.ts +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/installation_listing_pb.ts @@ -13,7 +13,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/installation_listing.proto. */ export const file_saas_accounts_v1_installation_listing: GenFile = /*@__PURE__*/ - fileDesc("CitzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbl9saXN0aW5nLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIqUECgxJbnN0YWxsYXRpb24SFAoCaWQYASABKAlCCLpIBXIDsAEBEhgKBm9yZ19pZBgCIAEoCUIIukgFcgOwAQESJAoSYWdlbnRfcHJpbmNpcGFsX2lkGAMgASgJQgi6SAVyA7ABARIkChJvd25lcl9wcmluY2lwYWxfaWQYBSABKAlCCLpIBXIDsAEBEh4KFmNvX293bmVyX3ByaW5jaXBhbF9pZHMYBiADKAkSJAoScm9vdF9zY29wZV9ub2RlX2lkGAcgASgJQgi6SAVyA7ABARI0CgZzdGF0dXMYCCABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN0YXR1cxIuCgpjcmVhdGVkX2F0GAkgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIzCgpyZXZva2VkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgAiAEBEh4KCXRhcmdldF9pZBgLIAEoCUILukgI2AEBcgOwAQESIAoOcmV2b2tlZF9yZWFzb24YDCABKAlCCLpIBXIDGIAEEiAKDmZhaWx1cmVfcmVhc29uGA0gASgJQgi6SAVyAxiABBIqChVyZWdpc3RyeV9wcmluY2lwYWxfaWQYDiABKAlCC7pICNgBAXIDsAEBQg0KC19yZXZva2VkX2F0SgQIBBAFUhNzb2x1dGlvbl9pZGVudGlmaWVyIp0BChhMaXN0SW5zdGFsbGF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARI0CgZzdGF0dXMYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN0YXR1cxIdCglwYWdlX3NpemUYAyABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgEIAEoCSKBAQoTSW5zdGFsbGF0aW9uU3VtbWFyeRI0CgxpbnN0YWxsYXRpb24YASABKAsyHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbhI0CgZoZWFsdGgYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbkhlYWx0aCJyChlMaXN0SW5zdGFsbGF0aW9uc1Jlc3BvbnNlEjwKDWluc3RhbGxhdGlvbnMYASADKAsyJS5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN1bW1hcnkSFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJKrsBChJJbnN0YWxsYXRpb25TdGF0dXMSIwofSU5TVEFMTEFUSU9OX1NUQVRVU19VTlNQRUNJRklFRBAAEh4KGklOU1RBTExBVElPTl9TVEFUVVNfQUNUSVZFEAESHwobSU5TVEFMTEFUSU9OX1NUQVRVU19SRVZPS0VEEAISHwobSU5TVEFMTEFUSU9OX1NUQVRVU19QRU5ESU5HEAMSHgoaSU5TVEFMTEFUSU9OX1NUQVRVU19GQUlMRUQQBCqEAgoSSW5zdGFsbGF0aW9uSGVhbHRoEiMKH0lOU1RBTExBVElPTl9IRUFMVEhfVU5TUEVDSUZJRUQQABIfChtJTlNUQUxMQVRJT05fSEVBTFRIX0hFQUxUSFkQARIpCiVJTlNUQUxMQVRJT05fSEVBTFRIX05PX0VMSUdJQkxFX09XTkVSEAISJQohSU5TVEFMTEFUSU9OX0hFQUxUSF9BR0VOVF9SRVZPS0VEEAMSJgoiSU5TVEFMTEFUSU9OX0hFQUxUSF9BR0VOVF9ESVNBQkxFRBAEEi4KKklOU1RBTExBVElPTl9IRUFMVEhfU1RBTkRJTkdfR1JBTlRfTUlTU0lORxAFQsABChRjb20uc2Fhcy5hY2NvdW50cy52MUIYSW5zdGFsbGF0aW9uTGlzdGluZ1Byb3RvUAFaLGFjY291bnRzL3BrZy9nZW4vc2Fhcy9hY2NvdW50cy92MTthY2NvdW50c3YxogIDU0FYqgIQU2Fhcy5BY2NvdW50cy5WMcoCEFNhYXNcQWNjb3VudHNcVjHiAhxTYWFzXEFjY291bnRzXFYxXEdQQk1ldGFkYXRh6gISU2Fhczo6QWNjb3VudHM6OlYxYgZwcm90bzM", [file_buf_validate_validate, file_google_protobuf_timestamp]); + fileDesc("CitzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbl9saXN0aW5nLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIvkDCgxJbnN0YWxsYXRpb24SFAoCaWQYASABKAlCCLpIBXIDsAEBEhgKBm9yZ19pZBgCIAEoCUIIukgFcgOwAQESJAoSYWdlbnRfcHJpbmNpcGFsX2lkGAMgASgJQgi6SAVyA7ABARIkChJvd25lcl9wcmluY2lwYWxfaWQYBSABKAlCCLpIBXIDsAEBEh4KFmNvX293bmVyX3ByaW5jaXBhbF9pZHMYBiADKAkSJAoScm9vdF9zY29wZV9ub2RlX2lkGAcgASgJQgi6SAVyA7ABARI0CgZzdGF0dXMYCCABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN0YXR1cxIuCgpjcmVhdGVkX2F0GAkgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIzCgpyZXZva2VkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgAiAEBEh4KCXRhcmdldF9pZBgLIAEoCUILukgI2AEBcgOwAQESIAoOcmV2b2tlZF9yZWFzb24YDCABKAlCCLpIBXIDGIAEEiAKDmZhaWx1cmVfcmVhc29uGA0gASgJQgi6SAVyAxiABEINCgtfcmV2b2tlZF9hdEoECAQQBVITc29sdXRpb25faWRlbnRpZmllciKdAQoYTGlzdEluc3RhbGxhdGlvbnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoGc3RhdHVzGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdGF0dXMSHQoJcGFnZV9zaXplGAMgASgFQgq6SAcaBRj0AygAEhIKCnBhZ2VfdG9rZW4YBCABKAkigQEKE0luc3RhbGxhdGlvblN1bW1hcnkSNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SNAoGaGVhbHRoGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25IZWFsdGgicgoZTGlzdEluc3RhbGxhdGlvbnNSZXNwb25zZRI8Cg1pbnN0YWxsYXRpb25zGAEgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdW1tYXJ5EhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSq7AQoSSW5zdGFsbGF0aW9uU3RhdHVzEiMKH0lOU1RBTExBVElPTl9TVEFUVVNfVU5TUEVDSUZJRUQQABIeChpJTlNUQUxMQVRJT05fU1RBVFVTX0FDVElWRRABEh8KG0lOU1RBTExBVElPTl9TVEFUVVNfUkVWT0tFRBACEh8KG0lOU1RBTExBVElPTl9TVEFUVVNfUEVORElORxADEh4KGklOU1RBTExBVElPTl9TVEFUVVNfRkFJTEVEEAQqhAIKEkluc3RhbGxhdGlvbkhlYWx0aBIjCh9JTlNUQUxMQVRJT05fSEVBTFRIX1VOU1BFQ0lGSUVEEAASHwobSU5TVEFMTEFUSU9OX0hFQUxUSF9IRUFMVEhZEAESKQolSU5TVEFMTEFUSU9OX0hFQUxUSF9OT19FTElHSUJMRV9PV05FUhACEiUKIUlOU1RBTExBVElPTl9IRUFMVEhfQUdFTlRfUkVWT0tFRBADEiYKIklOU1RBTExBVElPTl9IRUFMVEhfQUdFTlRfRElTQUJMRUQQBBIuCipJTlNUQUxMQVRJT05fSEVBTFRIX1NUQU5ESU5HX0dSQU5UX01JU1NJTkcQBULAAQoUY29tLnNhYXMuYWNjb3VudHMudjFCGEluc3RhbGxhdGlvbkxpc3RpbmdQcm90b1ABWixhY2NvdW50cy9wa2cvZ2VuL3NhYXMvYWNjb3VudHMvdjE7YWNjb3VudHN2MaICA1NBWKoCEFNhYXMuQWNjb3VudHMuVjHKAhBTYWFzXEFjY291bnRzXFYx4gIcU2Fhc1xBY2NvdW50c1xWMVxHUEJNZXRhZGF0YeoCElNhYXM6OkFjY291bnRzOjpWMWIGcHJvdG8z", [file_buf_validate_validate, file_google_protobuf_timestamp]); /** * Installation is one solution's bound identity in an org. Server-assigned ids; @@ -104,23 +104,6 @@ export type Installation = Message<"saas.accounts.v1.Installation"> & { * @generated from field: string failure_reason = 13; */ failureReason: string; - - /** - * The module principal this installation was installed under: the registry - * principal whose delivered authority named the target's binding, written at - * install and never changed. A person's capability for a module audience is - * sealed to the active installation whose registry principal is that - * audience's, so a reader that must tell a person's seal from unattended work - * compares this, not the route alias. Every install through InstallSolution, - * a module's or a solution's, records one. Empty only on a row written before - * the column existed (it is deliberately not backfilled: no principal is - * inferred) or one the store wrote with no registry principal. A capability - * a module or solution identity mints is sealed only to a row carrying its - * principal. A reader treats empty as unknown, never as "not this principal". - * - * @generated from field: string registry_principal_id = 14; - */ - registryPrincipalId: string; }; /** From bcec7e3961f9862ec42767fc71db00c83cf4a521 Mon Sep 17 00:00:00 2001 From: saurabhmaheshwari-obin <242053479+saurabhmaheshwari-obin@users.noreply.github.com> Date: Sat, 24 Oct 2026 16:40:27 -0400 Subject: [PATCH 12/17] fix(tools): the naming gate reads text files up to 1 MiB The accounts service catalog (generated/service-catalog.json) is generated text, and each RPC adds about 1.7 KB to it. With AdmitWorkloadOperation it grew to 524,580 bytes, past the gate's 512 KiB cap, so the gate reported it as too large to scan and failed. The gate's own remedies were to split a generated file or to allowlist it, and an allowlisted catalog would stop being read at all while every RPC description keeps landing in it. The cap is raised to 1 MiB instead, above the largest generated text in the tree, so the file is scanned rather than exempted. A file over the cap is still reported, never skipped; the test that pins that builds a file over the new cap. Co-Authored-By: Claude Opus 5.5 --- module/tools/naming-gate.mjs | 7 ++++++- module/tools/naming-gate.test.mjs | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/module/tools/naming-gate.mjs b/module/tools/naming-gate.mjs index 3fee8be25..6a0f312b6 100644 --- a/module/tools/naming-gate.mjs +++ b/module/tools/naming-gate.mjs @@ -83,7 +83,12 @@ const SKIP_FILE = (rel) => rel.endsWith(".tsbuildinfo") || rel.endsWith(".DS_Store"); -const MAX_BYTES = 512 * 1024; +// The cap bounds what one text file costs to read, not what the gate covers: a file over it is +// reported, never skipped. It sits above the largest generated text the tree carries (the +// accounts service catalog, which grows by about 1.7 KB per RPC and crossed 512 KiB with +// AdmitWorkloadOperation), so a generated file outgrowing the cap is a red gate to raise it +// again, not a reason to allowlist a file every RPC description reaches. +const MAX_BYTES = 1024 * 1024; // A slug is an identifier-ish run: a hyphenated repository name, a dotted hostname, an email // address, an UPPER_SNAKE constant, a camelCase identifier. Splitting it on separators yields diff --git a/module/tools/naming-gate.test.mjs b/module/tools/naming-gate.test.mjs index bc655d6ed..152e9b1cf 100644 --- a/module/tools/naming-gate.test.mjs +++ b/module/tools/naming-gate.test.mjs @@ -206,7 +206,7 @@ test("canonicalScanRoot widens to the repository root only in canonical", () => }); test("a file too large to scan is reported, never skipped silently", () => { - const big = "clean line\n".repeat(60000); // comfortably over the 512 KiB cap + const big = "clean line\n".repeat(120000); // comfortably over the 1 MiB cap assert.match(joined({ "big.md": big }), /big\.md: too large to scan/); // The allowlist is the escape hatch the message names, so it must actually silence it. assert.deepEqual( From da1d4743017fb24f4f649b3c9de3da947f036ae1 Mon Sep 17 00:00:00 2001 From: saurabhmaheshwari-obin <242053479+saurabhmaheshwari-obin@users.noreply.github.com> Date: Sat, 24 Oct 2026 17:04:35 -0400 Subject: [PATCH 13/17] fix(accounts): AdmitWorkloadOperation reads the capability back and answers the target's installation too Review follow-up on the bearer-less arm's issuer answer. - The capability is read back, not inspected and decoded: readBackFor, beside readBack, is core's Authority.ReadBack with the alias as the audience and noApprovalGrants as the grant source, so the answer also holds the live revision, seal and epochs, and still never touches the replay store. Read-back failures map to refusals: an invalid or foreign token is NOT_INSPECTED; a superseded revision, a seal the owner no longer holds (core reports it as revoked), an uninstalled module or a module whose grant now names another tenant is the new NOT_CURRENT; a binding or a grant hop is NOT_OPERATION_CONTEXT. Any other failure is a live read that could not answer, Unavailable. The shape and owner checks run on the read-back claims. The explicit WorkContextSeal call is gone: the read-back's seal check is the same answer. - One call answers the gateway: the request gains target_id (uuid), and accounts refuses TARGET_NOT_INSTALLED when the tenant has no ACTIVE installation of it and SEAL_ON_TARGET when the seal is one of its rows. business.ActiveTargetInstallations pages ListInstallations for those rows; TestActiveTargetInstallationsAnswersEveryActiveRowOfTheTarget (Postgres) pins it against a revoked row, another target and another organization. - The internal credential is checked before the request is validated. Guards: - signingFunctions type-checks the package (export data from go list) and flags any declaration referencing a method of core's *workcontext.Authority other than ReadBack, or this server's own mint or remint, by type: calls, method values, method expressions, and package-level variables. Entries are keyed by receiver type and name, so another type's mint is its own entry. Throwaway files calling Authority.Grant, binding Authority.Start to a variable, declaring a package-level method expression of it, and declaring a signing mint on another type each failed TestWorkloadOperationTableNamesEveryMintAndOwnerKind; all were deleted. - The RPC guards read the response and request messages transitively: an RPC answers a capability when IssuedWorkContext, or an accounts string field named token or work_context, appears at any depth, and re-mints when it also takes parent_work_context_token at any depth. TestCapabilityDetectionIsTransitive holds that against synthetic nested messages (a walker limited to the top level fails it). - TestEveryRemintOfAServicePrincipalsParentHasAVerdict presents a service principal's capability to every re-mint, and Guard B fails on a re-mint RPC without such a row. Every re-mint refuses it at the mint: the owner a re-mint resolves current authority for needs an organization membership and an authorization revision, which a declared module principal does not have. TestADelegatedExchangeRefusesAModuleOperationContextAsItsParent (pkg/infra) runs both delegated exchanges against PostgreSQL with a module operation context minted through StartModuleOperationTask, and Guard B requires that file to cover every module re-mint. workContextAuthorityFake now refuses a declared module owner as the store does; no other test's verdict changed. - New verdict rows: target not installed, seal on a target row, a module reinstalled since the mint, a module uninstalled since the mint, a module whose tenant changed, and a person's capability whose revision moved (all NOT_CURRENT but the first two). Regenerated with Codefly source candidate 5234ce24 (accounts and gateway proto, go generate, contracts, saas-sdk). The gateway's vendored copy follows the accounts file; the gateway does not send target_id until the next commit. Co-Authored-By: Claude Opus 5.5 --- .../api/accounts/authority/contract.binpb | Bin 1205712 -> 1206896 bytes .../saas/accounts/v1/work_contexts.proto | 12 +- .../v1/workload_operation_admission.proto | 22 +- .../api/accounts/connect/contract.binpb | Bin 1205712 -> 1206896 bytes .../saas/accounts/v1/work_contexts.proto | 12 +- .../v1/workload_operation_admission.proto | 22 +- .../api/auth-gateway/grpc/contract.binpb | Bin 360652 -> 361718 bytes .../v1/workload_operation_admission.proto | 22 +- module/contracts/api/catalog.codefly.json | 6 +- module/module.package.codefly.yaml | 6 +- module/services/accounts/AGENTS.md | 74 +-- .../code/pkg/adapters/work_context_core.go | 18 + .../pkg/adapters/work_context_rpcs_test.go | 14 +- .../adapters/workload_operation_admission.go | 101 ++-- .../workload_operation_admission_test.go | 563 ++++++++++++++++-- .../code/pkg/business/installations.go | 37 ++ .../work_contexts.connect.go | 24 +- .../saas/accounts/v1/work_contexts_grpc.pb.go | 24 +- .../v1/workload_operation_admission.pb.go | 44 +- .../infra/active_target_installations_test.go | 40 ++ .../pkg/infra/service_parent_exchange_test.go | 103 ++++ .../saas/accounts/v1/work_contexts.proto | 12 +- .../v1/workload_operation_admission.proto | 22 +- .../v1/workload_operation_admission.pb.go | 44 +- .../v1/workload_operation_admission.proto | 22 +- .../generated/contract/catalog.codefly.json | 2 +- .../generated/contract/contract.binpb | Bin 1205712 -> 1206896 bytes .../saas-sdk/generated/library.codefly.yaml | 2 +- .../gen/saas/accounts/v1/work_contexts_pb.ts | 12 +- .../v1/workload_operation_admission_pb.ts | 39 +- .../gen/saas/accounts/v1/work_contexts_pb.ts | 12 +- .../v1/workload_operation_admission_pb.ts | 39 +- 32 files changed, 1132 insertions(+), 218 deletions(-) create mode 100644 module/services/accounts/code/pkg/infra/active_target_installations_test.go create mode 100644 module/services/accounts/code/pkg/infra/service_parent_exchange_test.go diff --git a/module/contracts/api/accounts/authority/contract.binpb b/module/contracts/api/accounts/authority/contract.binpb index 9931b6eab4fece02bc19ff8f656858c59edd986c..99f9f12a78efb6ba8a5f898f5af449dec14b52d4 100644 GIT binary patch delta 4874 zcmaKvYmgMx700*x-s$P->0y`VwG3=*C|1xNc2`~nY(!STm3LT{N~J1Qq<5#;opPBW zGqX#PFOyWJVjjLt(FReJC@T2K!#D^=1@kZh3IQ|<0u;yRJ68i|?6p zyd9U~ZLz8MDMpjme#&h03ZIxU@9mRjcfx8m5@n~@?|P$qt#RI>BMk|!<&^n8Kgx_o z@6bteOl|sUbGpK<*+!e!e%hRr?&<9ATGq31&Dx%g%h#;$>0GsH+0q8hnrU1^-y7C1 z>s+;Q?V62#TtnQ;oHnnTAi|ey=X7ReZ9`Ejt8m82r)eSXPXYq-3 z^OW?kP5XNHziIZVZ^fT`%e-61{u?O72tYJI-PA*?6LoV5ct$BMe zH|*M_vQu_5cBx>O2VA=!gRRas+bP{5re|y?pOu3=1(cl4uHEMh4jT5>0T)w>Bo_-q zWp{eXCU45NnZs;sg*Z1`oAS1qRc@LXYcfnD7OPY16pN+R8Aj6JVk8DLgHI|(Eg3md z5ZI(%Yk-7^6eI7*JLZnbO*30%h!AbH)hJU0X4~Gk&X~hZ_I0yl1Vco#I?K36r4WWu zZR}ZdtTMTI?mQX85i>*07+}$rM4IavLP-8?de`htK>R^woIh3iPrL21G=asNt?DVEu`CnK z3qylhyB8J=xQ6W~AS$!sWV1!LRB}s{j>smCY-Y8#$!v6FGZ$qdE!V|pX8OCnpSe?sEEnj&4pn%Yj1H-=AxeaoGngoZZKEgwvfu+87dDHieyoxkT>k2JDe*Cw{W!5 zkTBEDy4ehzg#Wg?rO-DZa_w`9#hhEB9h~g2lkao0Vb(gbp4*Z!3`6C(l=JIuR`74e zMVZgno5g83${Z!rq3XuHs17l-bl#yZ;_0g5?Kx-8oGorF-B!DI8>Q->FcWYD%-p6N z&V~xu0^i(x+5706+1A^U7#D1T+U{taXr%lNb&GeT7cry3NQe#fTN9&@=@)s+NCi`_ z=j1qI6y(^69C4Y%$l{&qQl8%AU3AQG-j4I;SNl2>qvU1^W_FIYm0K**i4a| z-57303Z^XKbk(uEip{#P*+ae|NUcllOy8q;gNKt-Hbj@h8O-6xIB{W6pe+|N_NHQC zvtLf~w#XtB_bHd}_b-yXahr!w3Ih&0os$>$uCbCU`{+atK-876N8Xnk#6O%vPqZn~3R#$g| zizjVmP5I2cWr1>{ws~Ia+4$JsYX;ZqkiNE)sgE<3KBU>=DIV{wT$K9N@($4*v;Xe_ z8u|X9H+`-bh=+>$u~&aX>ixz3V|it}@UP;GzCuwvYn^`EC~s@+#x=bWZ|Xw3ltN@xTVb z-mb-|=E=bmf_4|@CnLqABvs!uZQYMC2JSotY& zg0U4kJZboGrajHHpRxGJNZ9mqrd?%>F_LhJ0^xhcw&}zjHzdFkq>g)``V@CxZ0s5 zsdlw;jA@f3lNe5Yj7^>&41wqvThb#liUHBUZgnnqgALm)}gU}DsuxCF*8M#yS&hq* z{Rh)#%KD@*c=yl9JCfkt#mHfjP_mbzk`;!0$oR)$$!fA>KV->|!;U*p z64HR>dpvByBmv9!dDuxvLPh_82UIF5X~1$D4?77-z;Zhedo@YGat9APi7@0Y&hLdG zekX}bEO+tby>P_uB)$kNcXRE!D>P8KhgX2b@AJB)ZRhGUJZSUyKLX#*wF&Z);HNl- zjUB-daJKWCzAW#Ij$Ay>_l9tEACAY9dqX&+Vb%M1*yi<$h{Mu9;-b$J9SRe4c5v;% z5Duw;vnzr_62WkH)Bd=V?|j^L06IFCedNCKS7qY)j_Kxa=#M3Z}Qc+0* zmscY$B!SECA}%C>%WDxAVaQR=-w3%RB$uN+`9{da7lGv%$Imezi?0F8>-^UKwJH3K S!e2f98t^x|*1y)ujsG7)Dw3E0 delta 3812 zcmZ9OZE#f88OQhD^X%EXXYVHACWaW2W{JEtZzNz-f-pEuLe)-ZIu0E_I!oBnjA;Vd zMaF(vaJEC=q}7RDCSbKH8kEl15!fi8IJK>kAnkyq(m;zN(&;O-_CwTm`ajQoTm7&M z&;LH>x##?U|9jy2`8$6zbLXy-Q8G)SM5TC1mJ%halq{)I>WF>!Iq^ZWavQJNx4M%z z5-X{7yJ~iKe{46q7th&u$6K!$>T|koaJQYe-=q_!YH)AAU^kREy<@i;>aHsbm2@Z; z+vxVp*DOx$wv-uGZEByvow{-_|G}PYYU=FrTTF;8W|t~(2UfSZ6La?c%`H8v z{0@PRCFUyih{-JiReAX(yUu7@@`YZ%htRX!>`>deg;2QnT($9dUHDq2n=c zD{)lrwafOtwQXoo$|M-p8Cz@M?}GFJJFTU9DdB)nD z50B=z<%SBmEt%}-7IwEIGn%_UH?A`HWB41xZqIx6%6q$$HwEKxg6@W!RoWYFVO-;C zp=wl854ZLttKdQpwO1-wvWiyv0}Sk^7yE%_SYbHSYxcRXy=NDSy~(Pe2P=9PIQ~lv zyrj3bFL;UJzvPSIOIX2@HPp#wVCcHoM$Zflah%O#9CBKlZkVv7-@WCk z{YtSv>G%swEa<l(6Eu))2paXXMNNK1ST-0gLvjX950qi* zPz@$)f<`PGtgTCJmfOb13S+C)OBc&qKd?s|Q^yY)D&fRqv9qKm6y~!+G|t2``fILtcYG=RhWlWD`f9BoqI_mWet7F>wyohrq#(jO#$wf;wY8)`3Ue$aAx*BVS`mf@?;j*R4TnS5b*WF&mZANWvqTXysC z$Z+8Scf(+MX_Lwna--SNf}WKf&F{>O=QG*OV>=+^-Ccv}w&J9aO*%&U2&O}mqIqpl z3SABkscP>jIdZ?CgF-4tS-PF3_u|z1Me3l4s|6|oe2FP&yPbQbt3eGHB5b8$YuWC zSi8f_lU zf5q^1CLB30%tzeLucr4G=OZjSB=ZrL1hC9USQ6O9M}i)r;9^a9SUwV|hbXvMSp$}j zBP=Xo@+XXicg2#X@?TMuyD7LxSO%5{DZEHnVk{-HdVMUOh>_o+@FHOiSoTnO2eSk$ z-=**(VF`);9tBj2CTqa*eF`rUmVo646kgRV0m~05yhyYmdx;)~A>Kuj@UiTr)WdMZ zyGT3{SbjvZ=TjP}{FsWs;$8EWBPYoG83nhz1sf+wF7!VV3+J7nhTDP?a3*NuJ^nXi zVK0voO@(kQ56+{MnhN2thN+KHc*|Qw-G|XG>1&?paGR#HkK}<64y%Ckcm#(fhVukj zeg4@z5fh(`;IIZbPepK80-WO05gpb*=b4a>$r9ikjNq^YIL}6KSOT2qA~@QRLqvx| zIF=9R5Ty=>a4hQ5H#+D%uixnr^EyH21p=J?g1?!hBaf2#cKQ0((vwElGw&EqPIdWNsr4hO1%|wVGUf$5f_#)c{;+vlE(5fLUS_Y!ZNtL z8gXHXxtt=a-9MElV&v-)7uLY#7ZDehz~zky3roQA%MeS^WDQ(?6>(t+Tz(yKVF_H` zjJRk+P7|F8xg>oqrzv$N3IGDG;^n1{vFfMu5UBsQh-tHQ4uzZ(1&loOkr;rjmr D^tM)| diff --git a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/work_contexts.proto b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/work_contexts.proto index 8a645eea6..a6226ee7e 100644 --- a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/work_contexts.proto +++ b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/work_contexts.proto @@ -400,14 +400,16 @@ service WorkContextService { // AdmitWorkloadOperation answers whether a capability presented with no // bearer at the declared module route /v1//* is a module operation - // context (StartModuleOperationTask) for that alias, and admits nothing else. + // context (StartModuleOperationTask) for that alias, and admits nothing else; + // and whether the route's target is installed in the capability's tenant. // The auth-gateway's bearer-less arm is its caller. Unlike the revision // checks above, the signed capability itself crosses this boundary: the // answer depends on its owner, owner kind and chain, which only the issuer - // reads. It inspects (signature under this issuer, window, audience) and - // reads its own claims; it never verifies, so nothing is consumed and the - // callee still verifies the capability at full strength. Internal-tier: the - // tenant it answers about is the one the capability names. + // reads. It reads the capability back (core's Authority.ReadBack: signature + // under this issuer, window, audience, revision, seal, epochs) and never + // verifies it as presented, so nothing is consumed and the callee still + // verifies at full strength. Internal-tier: the tenant it answers about is + // the one the capability names. rpc AdmitWorkloadOperation(AdmitWorkloadOperationRequest) returns (AdmitWorkloadOperationResponse) { option (saas.policy.v1.method_policy) = { exposure: EXPOSURE_INTERNAL diff --git a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/workload_operation_admission.proto b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/workload_operation_admission.proto index dda95d168..3bc644315 100644 --- a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/workload_operation_admission.proto +++ b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/workload_operation_admission.proto @@ -11,8 +11,9 @@ import "buf/validate/validate.proto"; // A forwarding hop cannot answer it. Core's Inspector, which is all a hop runs, // withholds the owner, the owner's kind, the actor chain and the operation // binding, and those are exactly what tell a module's unattended work from a -// person's capability. Accounts signed the capability and can read its own -// claims, so the gateway asks it. WorkContextService/AdmitWorkloadOperation +// person's capability. Accounts signed the capability and reads it back, held +// against its own live state, so the gateway asks it, and asks in the same call +// whether the route's target is installed. WorkContextService/AdmitWorkloadOperation // takes and answers these messages. // // They live in their own file, with deliberately minimal imports, so the @@ -41,11 +42,21 @@ enum WorkloadOperationRefusal { // The owner is not a declared module principal, or the seal is not an ACTIVE // installation in the tenant installed under that principal. WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS = 5; + // The tenant has no ACTIVE installation of the route's target. The gateway + // answers this one 403, as it answers a person whose organization never + // installed the module. + WORKLOAD_OPERATION_REFUSAL_TARGET_NOT_INSTALLED = 6; + // The seal is one of the target's own installations: the caller would be the + // module it addresses. + WORKLOAD_OPERATION_REFUSAL_SEAL_ON_TARGET = 7; + // The issuer's live state has moved past the capability: its authorization + // revision is superseded, or an epoch it carries has advanced. + WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT = 8; } message AdmitWorkloadOperationRequest { - // The capability as presented, encoded. It is inspected and read, never - // verified: nothing it carries is consumed. + // The capability as presented, encoded. It is read back by its issuer, never + // verified as presented: nothing it carries is consumed. string work_context = 1 [ (buf.validate.field).string.min_len = 1, (buf.validate.field).string.max_len = 32768 @@ -56,6 +67,9 @@ message AdmitWorkloadOperationRequest { (buf.validate.field).string.min_len = 1, (buf.validate.field).string.max_len = 512 ]; + // The immutable solution target the route's alias resolved to, from the + // gateway's own routing. The tenant must have an ACTIVE installation of it. + string target_id = 3 [(buf.validate.field).string.uuid = true]; } message AdmitWorkloadOperationResponse { diff --git a/module/contracts/api/accounts/connect/contract.binpb b/module/contracts/api/accounts/connect/contract.binpb index 9931b6eab4fece02bc19ff8f656858c59edd986c..99f9f12a78efb6ba8a5f898f5af449dec14b52d4 100644 GIT binary patch delta 4874 zcmaKvYmgMx700*x-s$P->0y`VwG3=*C|1xNc2`~nY(!STm3LT{N~J1Qq<5#;opPBW zGqX#PFOyWJVjjLt(FReJC@T2K!#D^=1@kZh3IQ|<0u;yRJ68i|?6p zyd9U~ZLz8MDMpjme#&h03ZIxU@9mRjcfx8m5@n~@?|P$qt#RI>BMk|!<&^n8Kgx_o z@6bteOl|sUbGpK<*+!e!e%hRr?&<9ATGq31&Dx%g%h#;$>0GsH+0q8hnrU1^-y7C1 z>s+;Q?V62#TtnQ;oHnnTAi|ey=X7ReZ9`Ejt8m82r)eSXPXYq-3 z^OW?kP5XNHziIZVZ^fT`%e-61{u?O72tYJI-PA*?6LoV5ct$BMe zH|*M_vQu_5cBx>O2VA=!gRRas+bP{5re|y?pOu3=1(cl4uHEMh4jT5>0T)w>Bo_-q zWp{eXCU45NnZs;sg*Z1`oAS1qRc@LXYcfnD7OPY16pN+R8Aj6JVk8DLgHI|(Eg3md z5ZI(%Yk-7^6eI7*JLZnbO*30%h!AbH)hJU0X4~Gk&X~hZ_I0yl1Vco#I?K36r4WWu zZR}ZdtTMTI?mQX85i>*07+}$rM4IavLP-8?de`htK>R^woIh3iPrL21G=asNt?DVEu`CnK z3qylhyB8J=xQ6W~AS$!sWV1!LRB}s{j>smCY-Y8#$!v6FGZ$qdE!V|pX8OCnpSe?sEEnj&4pn%Yj1H-=AxeaoGngoZZKEgwvfu+87dDHieyoxkT>k2JDe*Cw{W!5 zkTBEDy4ehzg#Wg?rO-DZa_w`9#hhEB9h~g2lkao0Vb(gbp4*Z!3`6C(l=JIuR`74e zMVZgno5g83${Z!rq3XuHs17l-bl#yZ;_0g5?Kx-8oGorF-B!DI8>Q->FcWYD%-p6N z&V~xu0^i(x+5706+1A^U7#D1T+U{taXr%lNb&GeT7cry3NQe#fTN9&@=@)s+NCi`_ z=j1qI6y(^69C4Y%$l{&qQl8%AU3AQG-j4I;SNl2>qvU1^W_FIYm0K**i4a| z-57303Z^XKbk(uEip{#P*+ae|NUcllOy8q;gNKt-Hbj@h8O-6xIB{W6pe+|N_NHQC zvtLf~w#XtB_bHd}_b-yXahr!w3Ih&0os$>$uCbCU`{+atK-876N8Xnk#6O%vPqZn~3R#$g| zizjVmP5I2cWr1>{ws~Ia+4$JsYX;ZqkiNE)sgE<3KBU>=DIV{wT$K9N@($4*v;Xe_ z8u|X9H+`-bh=+>$u~&aX>ixz3V|it}@UP;GzCuwvYn^`EC~s@+#x=bWZ|Xw3ltN@xTVb z-mb-|=E=bmf_4|@CnLqABvs!uZQYMC2JSotY& zg0U4kJZboGrajHHpRxGJNZ9mqrd?%>F_LhJ0^xhcw&}zjHzdFkq>g)``V@CxZ0s5 zsdlw;jA@f3lNe5Yj7^>&41wqvThb#liUHBUZgnnqgALm)}gU}DsuxCF*8M#yS&hq* z{Rh)#%KD@*c=yl9JCfkt#mHfjP_mbzk`;!0$oR)$$!fA>KV->|!;U*p z64HR>dpvByBmv9!dDuxvLPh_82UIF5X~1$D4?77-z;Zhedo@YGat9APi7@0Y&hLdG zekX}bEO+tby>P_uB)$kNcXRE!D>P8KhgX2b@AJB)ZRhGUJZSUyKLX#*wF&Z);HNl- zjUB-daJKWCzAW#Ij$Ay>_l9tEACAY9dqX&+Vb%M1*yi<$h{Mu9;-b$J9SRe4c5v;% z5Duw;vnzr_62WkH)Bd=V?|j^L06IFCedNCKS7qY)j_Kxa=#M3Z}Qc+0* zmscY$B!SECA}%C>%WDxAVaQR=-w3%RB$uN+`9{da7lGv%$Imezi?0F8>-^UKwJH3K S!e2f98t^x|*1y)ujsG7)Dw3E0 delta 3812 zcmZ9OZE#f88OQhD^X%EXXYVHACWaW2W{JEtZzNz-f-pEuLe)-ZIu0E_I!oBnjA;Vd zMaF(vaJEC=q}7RDCSbKH8kEl15!fi8IJK>kAnkyq(m;zN(&;O-_CwTm`ajQoTm7&M z&;LH>x##?U|9jy2`8$6zbLXy-Q8G)SM5TC1mJ%halq{)I>WF>!Iq^ZWavQJNx4M%z z5-X{7yJ~iKe{46q7th&u$6K!$>T|koaJQYe-=q_!YH)AAU^kREy<@i;>aHsbm2@Z; z+vxVp*DOx$wv-uGZEByvow{-_|G}PYYU=FrTTF;8W|t~(2UfSZ6La?c%`H8v z{0@PRCFUyih{-JiReAX(yUu7@@`YZ%htRX!>`>deg;2QnT($9dUHDq2n=c zD{)lrwafOtwQXoo$|M-p8Cz@M?}GFJJFTU9DdB)nD z50B=z<%SBmEt%}-7IwEIGn%_UH?A`HWB41xZqIx6%6q$$HwEKxg6@W!RoWYFVO-;C zp=wl854ZLttKdQpwO1-wvWiyv0}Sk^7yE%_SYbHSYxcRXy=NDSy~(Pe2P=9PIQ~lv zyrj3bFL;UJzvPSIOIX2@HPp#wVCcHoM$Zflah%O#9CBKlZkVv7-@WCk z{YtSv>G%swEa<l(6Eu))2paXXMNNK1ST-0gLvjX950qi* zPz@$)f<`PGtgTCJmfOb13S+C)OBc&qKd?s|Q^yY)D&fRqv9qKm6y~!+G|t2``fILtcYG=RhWlWD`f9BoqI_mWet7F>wyohrq#(jO#$wf;wY8)`3Ue$aAx*BVS`mf@?;j*R4TnS5b*WF&mZANWvqTXysC z$Z+8Scf(+MX_Lwna--SNf}WKf&F{>O=QG*OV>=+^-Ccv}w&J9aO*%&U2&O}mqIqpl z3SABkscP>jIdZ?CgF-4tS-PF3_u|z1Me3l4s|6|oe2FP&yPbQbt3eGHB5b8$YuWC zSi8f_lU zf5q^1CLB30%tzeLucr4G=OZjSB=ZrL1hC9USQ6O9M}i)r;9^a9SUwV|hbXvMSp$}j zBP=Xo@+XXicg2#X@?TMuyD7LxSO%5{DZEHnVk{-HdVMUOh>_o+@FHOiSoTnO2eSk$ z-=**(VF`);9tBj2CTqa*eF`rUmVo646kgRV0m~05yhyYmdx;)~A>Kuj@UiTr)WdMZ zyGT3{SbjvZ=TjP}{FsWs;$8EWBPYoG83nhz1sf+wF7!VV3+J7nhTDP?a3*NuJ^nXi zVK0voO@(kQ56+{MnhN2thN+KHc*|Qw-G|XG>1&?paGR#HkK}<64y%Ckcm#(fhVukj zeg4@z5fh(`;IIZbPepK80-WO05gpb*=b4a>$r9ikjNq^YIL}6KSOT2qA~@QRLqvx| zIF=9R5Ty=>a4hQ5H#+D%uixnr^EyH21p=J?g1?!hBaf2#cKQ0((vwElGw&EqPIdWNsr4hO1%|wVGUf$5f_#)c{;+vlE(5fLUS_Y!ZNtL z8gXHXxtt=a-9MElV&v-)7uLY#7ZDehz~zky3roQA%MeS^WDQ(?6>(t+Tz(yKVF_H` zjJRk+P7|F8xg>oqrzv$N3IGDG;^n1{vFfMu5UBsQh-tHQ4uzZ(1&loOkr;rjmr D^tM)| diff --git a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/work_contexts.proto b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/work_contexts.proto index 8a645eea6..a6226ee7e 100644 --- a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/work_contexts.proto +++ b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/work_contexts.proto @@ -400,14 +400,16 @@ service WorkContextService { // AdmitWorkloadOperation answers whether a capability presented with no // bearer at the declared module route /v1//* is a module operation - // context (StartModuleOperationTask) for that alias, and admits nothing else. + // context (StartModuleOperationTask) for that alias, and admits nothing else; + // and whether the route's target is installed in the capability's tenant. // The auth-gateway's bearer-less arm is its caller. Unlike the revision // checks above, the signed capability itself crosses this boundary: the // answer depends on its owner, owner kind and chain, which only the issuer - // reads. It inspects (signature under this issuer, window, audience) and - // reads its own claims; it never verifies, so nothing is consumed and the - // callee still verifies the capability at full strength. Internal-tier: the - // tenant it answers about is the one the capability names. + // reads. It reads the capability back (core's Authority.ReadBack: signature + // under this issuer, window, audience, revision, seal, epochs) and never + // verifies it as presented, so nothing is consumed and the callee still + // verifies at full strength. Internal-tier: the tenant it answers about is + // the one the capability names. rpc AdmitWorkloadOperation(AdmitWorkloadOperationRequest) returns (AdmitWorkloadOperationResponse) { option (saas.policy.v1.method_policy) = { exposure: EXPOSURE_INTERNAL diff --git a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/workload_operation_admission.proto b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/workload_operation_admission.proto index dda95d168..3bc644315 100644 --- a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/workload_operation_admission.proto +++ b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/workload_operation_admission.proto @@ -11,8 +11,9 @@ import "buf/validate/validate.proto"; // A forwarding hop cannot answer it. Core's Inspector, which is all a hop runs, // withholds the owner, the owner's kind, the actor chain and the operation // binding, and those are exactly what tell a module's unattended work from a -// person's capability. Accounts signed the capability and can read its own -// claims, so the gateway asks it. WorkContextService/AdmitWorkloadOperation +// person's capability. Accounts signed the capability and reads it back, held +// against its own live state, so the gateway asks it, and asks in the same call +// whether the route's target is installed. WorkContextService/AdmitWorkloadOperation // takes and answers these messages. // // They live in their own file, with deliberately minimal imports, so the @@ -41,11 +42,21 @@ enum WorkloadOperationRefusal { // The owner is not a declared module principal, or the seal is not an ACTIVE // installation in the tenant installed under that principal. WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS = 5; + // The tenant has no ACTIVE installation of the route's target. The gateway + // answers this one 403, as it answers a person whose organization never + // installed the module. + WORKLOAD_OPERATION_REFUSAL_TARGET_NOT_INSTALLED = 6; + // The seal is one of the target's own installations: the caller would be the + // module it addresses. + WORKLOAD_OPERATION_REFUSAL_SEAL_ON_TARGET = 7; + // The issuer's live state has moved past the capability: its authorization + // revision is superseded, or an epoch it carries has advanced. + WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT = 8; } message AdmitWorkloadOperationRequest { - // The capability as presented, encoded. It is inspected and read, never - // verified: nothing it carries is consumed. + // The capability as presented, encoded. It is read back by its issuer, never + // verified as presented: nothing it carries is consumed. string work_context = 1 [ (buf.validate.field).string.min_len = 1, (buf.validate.field).string.max_len = 32768 @@ -56,6 +67,9 @@ message AdmitWorkloadOperationRequest { (buf.validate.field).string.min_len = 1, (buf.validate.field).string.max_len = 512 ]; + // The immutable solution target the route's alias resolved to, from the + // gateway's own routing. The tenant must have an ACTIVE installation of it. + string target_id = 3 [(buf.validate.field).string.uuid = true]; } message AdmitWorkloadOperationResponse { diff --git a/module/contracts/api/auth-gateway/grpc/contract.binpb b/module/contracts/api/auth-gateway/grpc/contract.binpb index 279996772b17d7e454b60801e8c1b4bdb5852726..3f8e8923f50defc74bb30819f87d6edfafc7fb1f 100644 GIT binary patch delta 1872 zcmY*Z&u<$=81;^K{9`DM+5D*E#Au3?G>#oRzp0wi)+E7g(xk*Oa6t_d?<8K;*)_Xs zN8w_SB0+B@NNmNS;>Zahgb3omU(f>rBrcUeNF2BzxNzp1*>MUtzuEWsd*A!s%;Wcl z{<%N&?y+`^j-$u=S2&eJ55@x{hq?YxBiZ2398L!(Is8VSF-DR;bN9KwW0~n_F#408 z4*tmD-_hgf>GzZPs-_!rq!fIS$0M0qxmsDR?QGtu?cCbju9a`!UR_DWj46@}e$8Xr zcV}}aq@?1;BpH`Q7X*9xwQ6;BvzAJ%{rEhI8G0mA3qF6KU+jC5AP7eceK@kHWjZw2 zm;_;W3-~VjIez+G0spOaUCvC8T0Y%n^?L={;7yZIw$Civ^WoT|&Y?}4R)b%)3@A96!w;A8XOtLFN@#|B9a3TfEoMH`Xbp_@xy@{!Hke0k zhnAOXw_aZjTEFRo-Tzxxlx(#ISefk|auHPE@EJd!i_l?mOP#hZ;;&_}o z?mn}vcbIR%km z%oa;ZO#;*`KvtCR34x|EAB3h_p4a9sG=G|8>F|Nm;U;Y{$hru1oweAm)wKMhD>D16 z?Kd1(S=1%e_Mj_hbiVc9>n~=9%gQs9wky z2q8#^t9dzcQ-j}8U1a@oVoXe(T4b(8Ns1&z!^Hc^ z^Pu#qp8xL&l)R)%$AP1P#xiJx8zqpTm(faaV*&g1a`L=tqCrLZIYTwkph>xZrPoAL zON9savALCrT`PKfFNdx*fD30u2Vw94t~@x(^3%e>dEl5CZ$DBn>qe0+E%$p&o*? zRW966P9ZVY5S*m1%PoU5Ymr*GW${q0CB|i)CbztfV&Z+%0U6PCG|>A5#PxXJr5=QS z>qfsq8ax>rC_^S?9WXb7w-#|BzcKPcj{*FRv0fix;ue_rjWAI|VB%JAx`@B+ZmEIb z3<|c?KpKidK@|!@VeqMn)2dHYd>a{6NQbiRvod%cw^5ij4T*qDB+2ohw$)g_YK~L}T z4>*#6Z)s^}&|^FDAcH${G>d2aGl+!zYh$8s=5UUQo2OSq%;cKZl2?!rFS^wjcVo$NPm(+YC6t&X?n?Fx%N?K^GxeGT)s3&uoLjwUE1ro^Q|@i>)B zuIOS!7^-;usWD6`Ec4v|D`YA1d{}_dc}jF1ii{l0_|>2=3&N;EOx-;vW$INJkG32ls~0Xu zlIm5>u~4y?sn;MWvVo7lntWbHJ6D^YiwNRt3sE1SA?q~cM-@5)ili=2%lPTAZkV9~ z$WWh{43$oXO)?A)Mq8T@yjL=-7qn?4B2ev3r$WWa);PD(%W1&iq8O-0)i6dPkZg^y L@qV&`UvK;amGpT) diff --git a/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/workload_operation_admission.proto b/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/workload_operation_admission.proto index dda95d168..3bc644315 100644 --- a/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/workload_operation_admission.proto +++ b/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/workload_operation_admission.proto @@ -11,8 +11,9 @@ import "buf/validate/validate.proto"; // A forwarding hop cannot answer it. Core's Inspector, which is all a hop runs, // withholds the owner, the owner's kind, the actor chain and the operation // binding, and those are exactly what tell a module's unattended work from a -// person's capability. Accounts signed the capability and can read its own -// claims, so the gateway asks it. WorkContextService/AdmitWorkloadOperation +// person's capability. Accounts signed the capability and reads it back, held +// against its own live state, so the gateway asks it, and asks in the same call +// whether the route's target is installed. WorkContextService/AdmitWorkloadOperation // takes and answers these messages. // // They live in their own file, with deliberately minimal imports, so the @@ -41,11 +42,21 @@ enum WorkloadOperationRefusal { // The owner is not a declared module principal, or the seal is not an ACTIVE // installation in the tenant installed under that principal. WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS = 5; + // The tenant has no ACTIVE installation of the route's target. The gateway + // answers this one 403, as it answers a person whose organization never + // installed the module. + WORKLOAD_OPERATION_REFUSAL_TARGET_NOT_INSTALLED = 6; + // The seal is one of the target's own installations: the caller would be the + // module it addresses. + WORKLOAD_OPERATION_REFUSAL_SEAL_ON_TARGET = 7; + // The issuer's live state has moved past the capability: its authorization + // revision is superseded, or an epoch it carries has advanced. + WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT = 8; } message AdmitWorkloadOperationRequest { - // The capability as presented, encoded. It is inspected and read, never - // verified: nothing it carries is consumed. + // The capability as presented, encoded. It is read back by its issuer, never + // verified as presented: nothing it carries is consumed. string work_context = 1 [ (buf.validate.field).string.min_len = 1, (buf.validate.field).string.max_len = 32768 @@ -56,6 +67,9 @@ message AdmitWorkloadOperationRequest { (buf.validate.field).string.min_len = 1, (buf.validate.field).string.max_len = 512 ]; + // The immutable solution target the route's alias resolved to, from the + // gateway's own routing. The tenant must have an ACTIVE installation of it. + string target_id = 3 [(buf.validate.field).string.uuid = true]; } message AdmitWorkloadOperationResponse { diff --git a/module/contracts/api/catalog.codefly.json b/module/contracts/api/catalog.codefly.json index 9d6cef650..d440ad33a 100644 --- a/module/contracts/api/catalog.codefly.json +++ b/module/contracts/api/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/authority/contract.binpb", - "digest": "sha256:d0d8f6519a5a1eb4394265f3094faa19fee5a24afb3feeff4b33ea4ad5b07e5c", + "digest": "sha256:c9df546fead8be738783f6fba95b7a35c8037576c2e6362077f7e44406ca3e32", "services": [ { "name": "ModuleCapabilitiesService", @@ -65,7 +65,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/connect/contract.binpb", - "digest": "sha256:d0d8f6519a5a1eb4394265f3094faa19fee5a24afb3feeff4b33ea4ad5b07e5c", + "digest": "sha256:c9df546fead8be738783f6fba95b7a35c8037576c2e6362077f7e44406ca3e32", "services": [ { "name": "APIKeyService", @@ -563,7 +563,7 @@ "kind": "protobuf", "package": "saas.gateway.auth.v1", "path": "contracts/api/auth-gateway/grpc/contract.binpb", - "digest": "sha256:e243d131218567bb766e7fc5ed309b35dcc99b2ba71a7cda947561c876a347f7", + "digest": "sha256:d290f52b25016e8100c42d07278e1ee1504fafaaa9cef1fe1fe7a607cfc17617", "services": [ { "name": "AuthGatewayService", diff --git a/module/module.package.codefly.yaml b/module/module.package.codefly.yaml index 03fa7aa57..53dff4323 100644 --- a/module/module.package.codefly.yaml +++ b/module/module.package.codefly.yaml @@ -18,12 +18,12 @@ services: kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/authority/contract.binpb - digest: sha256:d0d8f6519a5a1eb4394265f3094faa19fee5a24afb3feeff4b33ea4ad5b07e5c + digest: sha256:c9df546fead8be738783f6fba95b7a35c8037576c2e6362077f7e44406ca3e32 - endpoint: connect kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/connect/contract.binpb - digest: sha256:d0d8f6519a5a1eb4394265f3094faa19fee5a24afb3feeff4b33ea4ad5b07e5c + digest: sha256:c9df546fead8be738783f6fba95b7a35c8037576c2e6362077f7e44406ca3e32 - name: auth-gateway endpoints: [grpc, rest] api-contracts: @@ -31,7 +31,7 @@ services: kind: protobuf package: saas.gateway.auth.v1 path: contracts/api/auth-gateway/grpc/contract.binpb - digest: sha256:e243d131218567bb766e7fc5ed309b35dcc99b2ba71a7cda947561c876a347f7 + digest: sha256:d290f52b25016e8100c42d07278e1ee1504fafaaa9cef1fe1fe7a607cfc17617 - name: cache endpoints: [read, write] - name: frontend diff --git a/module/services/accounts/AGENTS.md b/module/services/accounts/AGENTS.md index b69aab2d2..ff0ec422b 100644 --- a/module/services/accounts/AGENTS.md +++ b/module/services/accounts/AGENTS.md @@ -491,9 +491,10 @@ so matching on a bare reason across the whole service is wrong. ## Reading back a capability this host issued A capability presented with a QUESTION is read back, never verified: -`CheckWorkContextRecordAccess`, `ListReadableSourceCollections` and the -module-parent reads (`GetCurrentInstallation`, the executable-artifact reads) go -through `WorkContextAuthorityServer.readBack`, which is core's +`CheckWorkContextRecordAccess`, `ListReadableSourceCollections`, the +module-parent reads (`GetCurrentInstallation`, the executable-artifact reads) +and `AdmitWorkloadOperation` (through `readBackFor`, which takes the audience +from the question) go through `WorkContextAuthorityServer.readBack`, which is core's `Authority.ReadBack`. It makes every check a verifier makes against live state — signature, issuer, audience, window, revision, seal, epochs, binding — and never touches the replay store, so the consumer that verified and consumed a @@ -553,41 +554,46 @@ The gateway's bearer-less arm of `/v1//*` admits exactly one shape, a module operation context (`StartModuleOperationTask`), and decides nothing from the seal itself: a seal on an installation other than the target's is carried by every kind above, all of them a person's. It asks -`WorkContextService/AdmitWorkloadOperation` (`EXPOSURE_INTERNAL`, the -gateway's internal credential, the tier `ListInstallations` sits in). Accounts, -as the issuer, runs core's `Inspector` (signature under this issuer, window, -audience equal to the alias), decodes its own claims and admits only a -capability owned by a service principal with no actor chain, grant hop, -operation binding, parent session or owner agent, with authority scopes, an -idempotent replay policy and at most the operation-context window, whose owner -is a declared module principal other than `ModulePrincipalID(alias)` and whose -seal is that principal's ACTIVE installation in the tenant, read live. It never -reaches `verifyFor`, `verifyIssued` or the replay store, so it consumes -nothing: the callee verifies at full strength. A refusal is an answer with a -`WorkloadOperationRefusal`; an error means it could not decide, and the gateway -answers 503. The messages live in `workload_operation_admission.proto`, which -the gateway vendors byte for byte. - -Two guards hold it (`pkg/adapters/workload_operation_admission_test.go`): +`WorkContextService/AdmitWorkloadOperation{work_context, alias, target_id}` +(`EXPOSURE_INTERNAL`, the gateway's internal credential, checked before the +request is validated), one call per request. Accounts, as the issuer, reads the +capability back (`readBackFor`, core's `Authority.ReadBack` with the alias as +audience: signature, window, live revision, seal and epochs, never the replay +store), and admits only a capability owned by a service principal with no actor +chain, grant hop, operation binding, parent session or owner agent, with +authority scopes, an idempotent replay policy and at most the +operation-context window, whose owner is a declared module principal other +than `ModulePrincipalID(alias)`, and whose tenant has an ACTIVE installation of +`target_id` (`ActiveTargetInstallations`) that is not the seal. A refusal is an +answer with a `WorkloadOperationRefusal`; an error means a live read could not +answer, and the gateway answers 503. The messages live in +`workload_operation_admission.proto`, which the gateway vendors byte for byte. + +Three tests hold it (`pkg/adapters/workload_operation_admission_test.go`): `TestEveryMintHasAWorkloadOperationVerdict` mints through every mint RPC, every binding mode and every signing entrypoint, plus crafted variants of the operation-context shape, and asserts each verdict, with exactly one admitted; +`TestEveryRemintOfAServicePrincipalsParentHasAVerdict` presents a service +principal's capability to every re-mint; and `TestWorkloadOperationTableNamesEveryMintAndOwnerKind` fails when a -`saas.accounts.v1` RPC answering a token, an adapters function that signs (calls -core's `Start` or `Child`, `mint` or `remint`) or a `PrincipalKind*` constant -has no row. `TestAdmitWorkloadOperationConsumesNothing` pins that a single-use -capability shown to it still verifies once at its callee. - -The arm also reads the tenant's installations through -`InstallationService/ListInstallations` (`EXPOSURE_INTERNAL`), to require the -target installed there. Its contract comment in `installations.proto` names both -of the gateway's reads: the viewer projection, with the tenant projected from a -verified identity, and the arm, whose tenant is the one an edge-inspected Work -Context names, a signed statement of this issuer's and never a value the request -chose. The messages the RPC takes and answers live in -`installation_listing.proto`, which `installations.proto` imports and the -gateway vendors byte for byte, as it does `module_registration.proto` and -`solution_entitlements.proto`. +`saas.accounts.v1` RPC whose response carries a capability at any depth +(`IssuedWorkContext`, or an accounts field named `token` or `work_context`), a +re-mint RPC (one that also takes `parent_work_context_token` at any depth), an +adapters declaration referencing a method of core's `*workcontext.Authority` +other than `ReadBack` (or this server's `mint` or `remint`), found by type and +keyed by receiver and name, whether a function, a method, a method value or a +package-level variable, or a `PrincipalKind*` constant has no row +(`TestCapabilityDetectionIsTransitive` holds the depth). `TestAdmitWorkloadOperationConsumesNothing` +pins that a single-use capability shown to it still verifies once at its callee. + +Every re-mint refuses a service principal's capability as its parent: the +owner a re-mint resolves current authority for needs an organization membership +and an authorization revision, which a declared module principal does not have. +`TestEveryRemintOfAServicePrincipalsParentHasAVerdict` pins it per re-mint, and +`TestADelegatedExchangeRefusesAModuleOperationContextAsItsParent` (`pkg/infra`) +runs both delegated exchanges against PostgreSQL with a real module operation +context. The adapters' `workContextAuthorityFake` refuses a declared module +owner as the store does. ## Minting a module Work Context diff --git a/module/services/accounts/code/pkg/adapters/work_context_core.go b/module/services/accounts/code/pkg/adapters/work_context_core.go index b1471fe69..64ad110f7 100644 --- a/module/services/accounts/code/pkg/adapters/work_context_core.go +++ b/module/services/accounts/code/pkg/adapters/work_context_core.go @@ -277,6 +277,24 @@ func (s *WorkContextAuthorityServer) readBack(ctx context.Context, encoded strin workcontext.ReadBackCheck{Audience: claims.GetAudience(), Grants: noApprovalGrants{}}) } +// readBackFor is readBack for a question whose subject names the audience the +// capability must have been minted for, rather than taking it from the +// capability: the gateway's bearer-less arm asks about the alias its route +// resolved, and a capability addressed anywhere else is not the one it asks +// about. Every other check, and the untouched replay store, are readBack's. +func (s *WorkContextAuthorityServer) readBackFor(ctx context.Context, encoded, audience string) (*workcontext.ReadBackCapability, error) { + if !s.configured() { + return nil, ErrWorkContextAuthorityUnconfigured + } + decoded, err := workcontext.Decode(encoded) + if err != nil { + return nil, err + } + claims := decoded.Context() + return s.coreAuthority(claims.GetTenantId(), claims.GetOwnerPrincipalId()).ReadBack(ctx, encoded, + workcontext.ReadBackCheck{Audience: audience, Grants: noApprovalGrants{}}) +} + // delegationHop is one actor hop a mint appends after the owner's session. type delegationHop struct { PrincipalID string diff --git a/module/services/accounts/code/pkg/adapters/work_context_rpcs_test.go b/module/services/accounts/code/pkg/adapters/work_context_rpcs_test.go index b14ca7e70..ecce0a43a 100644 --- a/module/services/accounts/code/pkg/adapters/work_context_rpcs_test.go +++ b/module/services/accounts/code/pkg/adapters/work_context_rpcs_test.go @@ -29,13 +29,25 @@ type workContextAuthorityFake struct { installationErr error } +// ResolveWorkContextAuthority answers the configured facts, except for an owner +// the store holds no membership row for: a declared module principal, which the +// deployment declares rather than registers in any organization. The store +// refuses those (ErrTypeNotFound, "current organization membership and +// authorization revision required"), and so does this fake, so no test can +// read success for a module owner the store would never give. func (f *workContextAuthorityFake) ResolveWorkContextAuthority( _ context.Context, _ string, - _ string, + owner string, _ string, _ []business.WorkContextPermission, ) (*business.WorkContextAuthorityFacts, error) { + if service != nil && service.IsDeclaredModulePrincipal(owner) { + return nil, business.NewStoreError( + errors.New("current organization membership and authorization revision required"), + business.ErrTypeNotFound, + ) + } return f.facts, f.err } diff --git a/module/services/accounts/code/pkg/adapters/workload_operation_admission.go b/module/services/accounts/code/pkg/adapters/workload_operation_admission.go index 852c59b8e..035614e0f 100644 --- a/module/services/accounts/code/pkg/adapters/workload_operation_admission.go +++ b/module/services/accounts/code/pkg/adapters/workload_operation_admission.go @@ -2,8 +2,8 @@ package adapters import ( "context" - "crypto/ed25519" "errors" + "slices" "time" basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" @@ -32,29 +32,44 @@ import ( // TestWorkloadOperationTableNamesEveryMintAndOwnerKind fails when a mint or an // owner kind appears that the table does not name. // -// It consumes nothing. It inspects (core's stateless prefix of Verify: -// signature under this issuer, window, audience, shape) and decodes; it never -// reaches verifyFor, verifyIssued or the replay store, so a single-use -// capability it was shown still verifies at its callee, which remains the one -// party that verifies at full strength. +// It consumes nothing. The capability is READ BACK (core's Authority.ReadBack +// through readBackFor): signature under this issuer, window, the alias as +// audience, and the live revision, seal and epochs, exactly as a verifier holds +// them, without the replay store. The callee remains the one party that +// verifies the capability as presented and spends it. + +// workloadTargetInstallations answers the ids of a tenant's ACTIVE +// installations of a target. Nil means the business service's +// ActiveTargetInstallations; a test sets it to answer without a database. +var workloadTargetInstallations func(ctx context.Context, orgID, targetID string) ([]string, error) + +func activeTargetInstallations(ctx context.Context, orgID, targetID string) ([]string, error) { + if workloadTargetInstallations != nil { + return workloadTargetInstallations(ctx, orgID, targetID) + } + return service.ActiveTargetInstallations(ctx, orgID, targetID) +} // AdmitWorkloadOperation answers whether a capability presented with no bearer -// at the declared module route for alias is a module operation context for it. -// A refusal is an answer, not an error: errors are reserved for "could not -// decide", which the gateway answers 503. +// at the declared module route for alias is a module operation context for it, +// and whether the route's target is installed in its tenant. A refusal is an +// answer, not an error: errors are reserved for "could not decide", which the +// gateway answers 503. func (s *WorkContextAuthorityServer) AdmitWorkloadOperation( ctx context.Context, req *gen.AdmitWorkloadOperationRequest, ) (*gen.AdmitWorkloadOperationResponse, error) { - if err := Validate(req); err != nil { + // The credential first: a caller without it learns nothing, not even + // which of its fields would have been refused. + if err := requireInternalCredential(ctx); err != nil { return nil, err } - if err := requireInternalCredential(ctx); err != nil { + if err := Validate(req); err != nil { return nil, err } if !s.configured() || service == nil { return nil, status.Error(codes.Unavailable, "Work Context authority is not configured") } - claims, refusal, err := s.admitWorkloadOperation(ctx, req.GetWorkContext(), req.GetAlias()) + claims, refusal, err := s.admitWorkloadOperation(ctx, req.GetWorkContext(), req.GetAlias(), req.GetTargetId()) if err != nil { return nil, status.Error(codes.Unavailable, "module installation authority unavailable") } @@ -70,29 +85,24 @@ func (s *WorkContextAuthorityServer) AdmitWorkloadOperation( } // admitWorkloadOperation is the decision. It returns the claims when admitted, -// the refusal otherwise, and an error only when the live read could not answer. +// the refusal otherwise, and an error only when a live read could not answer. func (s *WorkContextAuthorityServer) admitWorkloadOperation( - ctx context.Context, encoded, alias string, + ctx context.Context, encoded, alias, targetID string, ) (*basev0.WorkContextV1, gen.WorkloadOperationRefusal, error) { // No operation context is minted for the capability surface, and an empty // alias is not a route. if requireMintableAudience(alias) != nil { return nil, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED, nil } - inspector := &workcontext.Inspector{ - Issuer: s.issuer, - Keys: map[string]ed25519.PublicKey{s.keyID: s.publicKey}, - Now: s.now, - } - if _, err := inspector.Inspect(encoded, alias); err != nil { - return nil, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED, nil - } - // The signature is this issuer's, so the claims are its own statement. - decoded, err := workcontext.Decode(encoded) + read, err := s.readBackFor(ctx, encoded, alias) if err != nil { - return nil, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED, nil + refusal, refused := readBackRefusal(err) + if !refused { + return nil, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED, err + } + return nil, refusal, nil } - claims := decoded.Context() + claims := read.Context() if claims.GetOwnerPrincipalKind() != business.PrincipalKindService { return nil, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE, nil } @@ -103,24 +113,47 @@ func (s *WorkContextAuthorityServer) admitWorkloadOperation( if owner == business.ModulePrincipalID(alias) { return nil, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET, nil } + // The read-back held the seal to the owner's live installation; this says + // the owner is a module the deployment declares, the only service principal + // StartModuleOperationTask signs for. if !service.IsDeclaredModulePrincipal(owner) { return nil, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS, nil } - // The seal must be the ACTIVE installation the owner is installed under in - // the tenant, read live: the same answer a verifier holds the seal to. - seal := claims.GetSeal().GetInstallationId() - live, _, err := service.WorkContextSeal(ctx, claims.GetTenantId(), owner, seal) + installed, err := activeTargetInstallations(ctx, claims.GetTenantId(), targetID) switch { - case errors.Is(err, business.ErrModuleInstallationInactive), errors.Is(err, business.ErrWorkContextSealUnheld): - return nil, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS, nil case err != nil: return nil, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED, err - case live == "" || live != seal: - return nil, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS, nil + case len(installed) == 0: + return nil, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_TARGET_NOT_INSTALLED, nil + case slices.Contains(installed, claims.GetSeal().GetInstallationId()): + return nil, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_SEAL_ON_TARGET, nil } return claims, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED, nil } +// readBackRefusal names the refusal a failed read-back is, and reports false for +// a failure that is not about the capability (a live read that could not +// answer), which the caller answers as undecidable. +func readBackRefusal(err error) (gen.WorkloadOperationRefusal, bool) { + switch { + case errors.Is(err, workcontext.ErrInvalid), errors.Is(err, workcontext.ErrNotACoreToken): + return gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED, true + case errors.Is(err, workcontext.ErrRevoked), + // The live module authority no longer stands behind the owner: its + // installation is gone, or its grant now names another tenant. The + // capability is stale, not undecidable. + errors.Is(err, business.ErrModuleInstallationInactive), + errors.Is(err, business.ErrModuleTenantNotCurrent): + // Core reports a seal the owner no longer holds as ErrRevoked too + // (checkSealAgainst), so that case is this one. + return gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT, true + case errors.Is(err, business.ErrNoOperationBinding), errors.Is(err, ErrNoApprovalGrants): + // A binding or a grant hop: neither is in an operation context. + return gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_OPERATION_CONTEXT, true + } + return gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED, false +} + // isModuleOperationContextShape reports whether a service principal's claims // are exactly what StartModuleOperationTask signs: the module acting as itself // (no actor chain, no grant hop, no operation binding, no owner agent), a root diff --git a/module/services/accounts/code/pkg/adapters/workload_operation_admission_test.go b/module/services/accounts/code/pkg/adapters/workload_operation_admission_test.go index d0d3d29c1..c79f60e5a 100644 --- a/module/services/accounts/code/pkg/adapters/workload_operation_admission_test.go +++ b/module/services/accounts/code/pkg/adapters/workload_operation_admission_test.go @@ -3,10 +3,15 @@ package adapters import ( "context" "errors" + "fmt" "go/ast" + "go/importer" "go/parser" "go/token" + "go/types" + "io" "os" + "os/exec" "path/filepath" "sort" "strconv" @@ -14,6 +19,7 @@ import ( "testing" "time" + "connectrpc.com/connect" basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" "github.com/codefly-dev/core/workcontext" "github.com/google/uuid" @@ -21,8 +27,11 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protodesc" "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/reflect/protoregistry" + "google.golang.org/protobuf/types/descriptorpb" "accounts/pkg/auth" "accounts/pkg/business" @@ -48,9 +57,37 @@ func admissionContext(t *testing.T) context.Context { return metadata.NewIncomingContext(context.Background(), metadata.Pairs("x-codefly-internal-token", admissionTestToken)) } +// admissionTarget is the route's target every row asks about, and +// admissionTargetRow the tenant's ACTIVE installation of it: a row that is +// none of the minted capabilities' seals. +const ( + admissionTarget = "019f6bf7-7777-7777-8777-777777777777" + admissionTargetRow = "019f6bf7-8888-7888-8888-888888888888" +) + +// installTargetRows answers the tenant's ACTIVE installations of +// admissionTarget with rows, for the rest of the test. +func installTargetRows(t *testing.T, rows ...string) { + t.Helper() + previous := workloadTargetInstallations + t.Cleanup(func() { workloadTargetInstallations = previous }) + workloadTargetInstallations = func(_ context.Context, _, target string) ([]string, error) { + if target != admissionTarget { + return nil, nil + } + return rows, nil + } +} + +// admitWorkload asks the issuer about token at alias, for admissionTarget. A +// test that has not said which rows the target has gets admissionTargetRow. func admitWorkload(t *testing.T, server *WorkContextAuthorityServer, token, alias string) *gen.AdmitWorkloadOperationResponse { t.Helper() - answer, err := server.AdmitWorkloadOperation(admissionContext(t), &gen.AdmitWorkloadOperationRequest{WorkContext: token, Alias: alias}) + if workloadTargetInstallations == nil { + installTargetRows(t, admissionTargetRow) + } + answer, err := server.AdmitWorkloadOperation(admissionContext(t), + &gen.AdmitWorkloadOperationRequest{WorkContext: token, Alias: alias, TargetId: admissionTarget}) require.NoError(t, err) return answer } @@ -264,14 +301,77 @@ func workloadOperationRows() []admissionRow { require.NoError(t, err) return workContextSingleton, "documents", token }}, - {"module operation context sealed to another installation", "", svc, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS, + {"module operation context after its module was reinstalled", "", svc, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT, func(t *testing.T) (*WorkContextAuthorityServer, string, string) { authority, _ := moduleOperationFixture(t) - authority.InstallationID = uuid.NewString() + // Minted and read back against the business service's live + // module authority, which holds the principal to its one + // installation as the store does; between the two the module + // is reinstalled, so the capability no longer describes live + // state (the read-back finds its revision superseded). + live := &sealedModuleAuthority{currentModuleAuthority: currentModuleAuthority{service: service}, installation: "installation-current"} + service.SetModuleAuthorityReads(live, nil) + workContextSingleton.seals = service + authority.InstallationID = "installation-current" token, _, err := workContextSingleton.StartModuleOperationTask(context.Background(), authority) require.NoError(t, err) + live.installation = "installation-replaced" return workContextSingleton, "modelservice", token }}, + {"module operation context after its module was uninstalled", "", svc, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT, + func(t *testing.T) (*WorkContextAuthorityServer, string, string) { + authority, _ := moduleOperationFixture(t) + token, _, err := workContextSingleton.StartModuleOperationTask(context.Background(), authority) + require.NoError(t, err) + // Read back against the business service, whose live module + // authority now holds no installation for the principal. + service.SetModuleAuthorityReads(uninstalledModuleAuthority{currentModuleAuthority{service: service}}, nil) + workContextSingleton.seals = service + return workContextSingleton, "modelservice", token + }}, + {"module operation context after its module's tenant changed", "", svc, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT, + func(t *testing.T) (*WorkContextAuthorityServer, string, string) { + authority, _ := moduleOperationFixture(t) + token, _, err := workContextSingleton.StartModuleOperationTask(context.Background(), authority) + require.NoError(t, err) + moved, err := moduleregistrytest.Parse(strings.Replace(headlessPrincipals, moduleWorkContextTenant, "66666666-6666-4666-8666-666666666666", 1)) + require.NoError(t, err) + service.SetModuleCapabilities(nil, nil, moved) + workContextSingleton.seals = service + return workContextSingleton, "modelservice", token + }}, + {"module operation context, target not installed", "", svc, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_TARGET_NOT_INSTALLED, + func(t *testing.T) (*WorkContextAuthorityServer, string, string) { + installTargetRows(t) + installModuleOperationContextService(t, headlessPrincipals) + installModuleWorkContextAuthority(t) + resp, err := mintModuleOperationContext("documents", moduleToken(t, "documents"), "model") + require.NoError(t, err) + return workContextSingleton, "modelservice", resp.GetToken() + }}, + {"module operation context sealed to one of the target's rows", "", svc, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_SEAL_ON_TARGET, + func(t *testing.T) (*WorkContextAuthorityServer, string, string) { + installModuleOperationContextService(t, headlessPrincipals) + installModuleWorkContextAuthority(t) + resp, err := mintModuleOperationContext("documents", moduleToken(t, "documents"), "model") + require.NoError(t, err) + decoded, err := workcontext.Decode(resp.GetToken()) + require.NoError(t, err) + installTargetRows(t, admissionTargetRow, decoded.Seal().GetInstallationId()) + return workContextSingleton, "modelservice", resp.GetToken() + }}, + {"a capability whose revision moved since it was minted", "", human, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT, + func(t *testing.T) (*WorkContextAuthorityServer, string, string) { + exchangeInstallationService(t) + seals := newSealAnswersFake(12) + server := newCoreTestServer(t, &workContextAuthorityFake{facts: &business.WorkContextAuthorityFacts{OrganizationRevision: 12}}, seals) + token, _ := mintTestCapability(t, server, testCapability{ + Audience: "documents", Tenant: renewOrgID, Owner: renewActorID, + Scopes: []*basev0.WorkScopeV1{{ResourceKind: "evidence", Actions: []string{"read"}}}, + }) + seals.revision = 13 + return server, "documents", token + }}, {"service principal no module declares", "", svc, gen.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS, operationShape(func(r *mintRequest) { r.Start.OwnerPrincipalID = uuid.NewString() })}, {"operation context shape, single-use", "", svc, notShape, @@ -326,64 +426,362 @@ func TestEveryMintHasAWorkloadOperationVerdict(t *testing.T) { require.Equal(t, 1, admittedRows, "exactly one shape is admitted") } -// mintEntrypointRows maps every adapters function that signs a capability -// (calls core's Start or Child, mint or remint) to the rows that exercise it. +// mintEntrypointRows maps every adapters declaration that signs a capability to +// the rows that exercise it, keyed by receiver type and name ("var name" for a +// package-level variable). A declaration signs when it references a method of +// core's *workcontext.Authority other than ReadBack (Start, Child, Grant, as a +// call, a method value or a method expression), or this server's own mint or +// remint. var mintEntrypointRows = map[string][]string{ - "mint": {"StartTask", "module operation context", "operation context shape, single-use"}, - "remint": {"StartRootSession", "ExchangeAudience", "RenewWorkContext", "operation exchange, plain binding", "read exchange, plain binding"}, - "StartTask": {"StartTask"}, - "StartRootSession": {"StartRootSession"}, - "exchangeVerifiedParent": {"ExchangeAudience", "operation exchange, plain binding", "read exchange, plain binding"}, - "RenewWorkContext": {"RenewWorkContext"}, - "StartChildSession": {"StartChildSession"}, - "exchangeInstalledBinding": {"operation exchange, content_read, invoke", "operation exchange, module_actor, invoke"}, - "startSourceOperation": {"source-operation context", "operation exchange, source delegation reference", "operation exchange, source delegation parent"}, - "mintInstallationTask": {"installation task", "installation step"}, - "StartModuleTask": {"module identity"}, - // The installer route's handler calls its mint field, StartModuleTask. - "token": {"installer token route"}, - "StartModuleOperationTask": {"module operation context", "module operation context for its own module"}, + "(*WorkContextAuthorityServer).mint": {"StartTask", "module operation context", "operation context shape, single-use"}, + "(*WorkContextAuthorityServer).remint": {"StartRootSession", "ExchangeAudience", "RenewWorkContext", "operation exchange, plain binding", "read exchange, plain binding"}, + "(*WorkContextAuthorityServer).StartTask": {"StartTask"}, + "(*WorkContextAuthorityServer).StartRootSession": {"StartRootSession"}, + "(*WorkContextAuthorityServer).exchangeVerifiedParent": {"ExchangeAudience", "operation exchange, plain binding", "read exchange, plain binding"}, + "(*WorkContextAuthorityServer).RenewWorkContext": {"RenewWorkContext"}, + "(*WorkContextAuthorityServer).StartChildSession": {"StartChildSession"}, + "(*WorkContextAuthorityServer).exchangeInstalledBinding": {"operation exchange, content_read, invoke", "operation exchange, module_actor, invoke"}, + "(*WorkContextAuthorityServer).startSourceOperation": {"source-operation context", "operation exchange, source delegation reference", "operation exchange, source delegation parent"}, + "(*WorkContextAuthorityServer).mintInstallationTask": {"installation task", "installation step"}, + "(*WorkContextAuthorityServer).StartModuleTask": {"module identity", "installer token route"}, + "(*WorkContextAuthorityServer).StartModuleOperationTask": {"module operation context", "module operation context for its own module"}, } -// signingFunctions walks the package's non-test sources and returns every -// function whose body calls a method named Start, Child, mint or remint. +// isSigningMethod reports the methods a signing function references: every method +// of core's Authority except the read-back, and this server's own two signing +// helpers. +func isSigningMethod(fn *types.Func) bool { + recv := fn.Type().(*types.Signature).Recv() + if recv == nil { + return false + } + target := types.Unalias(recv.Type()) + if pointer, ok := target.(*types.Pointer); ok { + target = types.Unalias(pointer.Elem()) + } + typeName, ok := target.(*types.Named) + if !ok { + return false + } + object := typeName.Obj() + switch { + case object.Pkg() != nil && object.Pkg().Path() == "github.com/codefly-dev/core/workcontext" && object.Name() == "Authority": + return fn.Name() != "ReadBack" + case object.Pkg() != nil && object.Pkg().Path() == "accounts/pkg/adapters" && object.Name() == "WorkContextAuthorityServer": + return fn.Name() == "mint" || fn.Name() == "remint" + } + return false +} + +// signingFunctions type-checks the package's non-test sources and returns +// every function that references a signing method, by the type the method is +// declared on rather than by its name: a call, a method value bound to a +// variable and a method expression all count. func signingFunctions(t *testing.T) map[string]bool { t.Helper() - files, err := filepath.Glob("*.go") - require.NoError(t, err) - found := map[string]bool{} + // Export data for every dependency, from the go command, so the check + // resolves imports exactly as the build does. + cmd := exec.Command("go", "list", "-export", "-deps", "-f", "{{.ImportPath}}\t{{.Export}}", ".") + cmd.Env = append(os.Environ(), "GOWORK=off") + listed, err := cmd.Output() + require.NoError(t, err, "go list -export") + exports := map[string]string{} + for _, line := range strings.Split(strings.TrimSpace(string(listed)), "\n") { + path, export, _ := strings.Cut(line, "\t") + exports[path] = export + } fset := token.NewFileSet() - for _, path := range files { + paths, err := filepath.Glob("*.go") + require.NoError(t, err) + var files []*ast.File + for _, path := range paths { if strings.HasSuffix(path, "_test.go") { continue } - src, err := os.ReadFile(path) - require.NoError(t, err) - file, err := parser.ParseFile(fset, path, src, 0) + file, err := parser.ParseFile(fset, path, nil, 0) require.NoError(t, err) - for _, decl := range file.Decls { - fn, ok := decl.(*ast.FuncDecl) - if !ok || fn.Body == nil { - continue + files = append(files, file) + } + info := &types.Info{Selections: map[*ast.SelectorExpr]*types.Selection{}, Uses: map[*ast.Ident]types.Object{}} + config := types.Config{Importer: importer.ForCompiler(fset, "gc", func(path string) (io.ReadCloser, error) { + export, ok := exports[path] + if !ok || export == "" { + return nil, fmt.Errorf("no export data for %s", path) + } + return os.Open(export) + })} + _, err = config.Check("accounts/pkg/adapters", fset, files, info) + require.NoError(t, err) + + found := map[string]bool{} + signs := func(node ast.Node) bool { + signing := false + ast.Inspect(node, func(node ast.Node) bool { + sel, ok := node.(*ast.SelectorExpr) + if !ok { + return true + } + var method *types.Func + if selection, ok := info.Selections[sel]; ok { + method, _ = selection.Obj().(*types.Func) + } else { + method, _ = info.Uses[sel.Sel].(*types.Func) } - ast.Inspect(fn.Body, func(node ast.Node) bool { - call, ok := node.(*ast.CallExpr) - if !ok { - return true + if method != nil && isSigningMethod(method) { + signing = true + } + return true + }) + return signing + } + for _, file := range files { + for _, decl := range file.Decls { + switch decl := decl.(type) { + case *ast.FuncDecl: + if decl.Body == nil || !signs(decl.Body) { + continue + } + name := decl.Name.Name + if decl.Recv != nil && len(decl.Recv.List) == 1 { + name = "(" + types.ExprString(decl.Recv.List[0].Type) + ")." + name } - if sel, ok := call.Fun.(*ast.SelectorExpr); ok { - switch sel.Sel.Name { - case "Start", "Child", "mint", "remint": - found[fn.Name.Name] = true + found[name] = true + case *ast.GenDecl: + for _, spec := range decl.Specs { + value, ok := spec.(*ast.ValueSpec) + if !ok || !signs(value) { + continue + } + for _, name := range value.Names { + found["var "+name.Name] = true } } - return true - }) + } } } return found } +// serviceParentRow presents a service principal's capability, shaped like a +// module operation context, as the parent of a re-mint, and pins the verdict: +// every re-mint refuses it at the mint. The owner a re-mint resolves current +// authority for must hold an organization membership and authorization +// revision, which a declared module principal does not. The two delegated +// exchanges are also run against PostgreSQL +// (pkg/infra/service_parent_exchange_test.go), the store this fixture's fake +// answers for. +type serviceParentRow struct { + rpc string + code codes.Code + mint func(t *testing.T) (*WorkContextAuthorityServer, string, error) +} + +// serviceParent is a service principal's capability for audience, sealed to +// installation: the module operation context's shape, owned by module. +func serviceParent(t *testing.T, server *WorkContextAuthorityServer, tenant, module, audience, installation string, scopes ...*basev0.WorkScopeV1) string { + t.Helper() + if len(scopes) == 0 { + scopes = []*basev0.WorkScopeV1{{ResourceKind: "evidence", Actions: []string{"read"}}} + } + token, _ := mintTestCapability(t, server, testCapability{ + Audience: audience, Tenant: tenant, Owner: business.ModulePrincipalID(module), OwnerKind: business.PrincipalKindService, + Scopes: scopes, + Installation: installation, TTL: business.ModuleOperationContextTTL, + }) + return token +} + +func serviceParentRows() []serviceParentRow { + // The caller is the module principal itself: the most a re-mint could be + // asked by the owner of the parent. + owner := func() context.Context { + return stampVerifiedIdentity(context.Background(), business.ModulePrincipalID("tool"), renewOrgID, auth.Assurance{}) + } + renewServer := func(t *testing.T) (*WorkContextAuthorityServer, string) { + exchangeInstallationService(t) + server := newRenewTestServer(t) + return server, serviceParent(t, server, renewOrgID, "tool", "tool.test", testInstallation("tool.test")) + } + return []serviceParentRow{ + {rpc: "WorkContextService/StartRootSession", code: codes.PermissionDenied, mint: func(t *testing.T) (*WorkContextAuthorityServer, string, error) { + server, parent := renewServer(t) + token, err := sealPinIssued(server.StartRootSession(owner(), &gen.StartRootSessionWorkContextRequest{ + OrgId: renewOrgID, ParentWorkContextToken: parent, SessionId: renewSession, Audience: "documents", TtlSeconds: 60, + })) + return server, token, err + }}, + {rpc: "WorkContextService/ExchangeAudience", code: codes.PermissionDenied, mint: func(t *testing.T) (*WorkContextAuthorityServer, string, error) { + server, parent := renewServer(t) + token, err := sealPinIssued(server.ExchangeAudience(owner(), &gen.ExchangeWorkContextAudienceRequest{ + OrgId: renewOrgID, ParentWorkContextToken: parent, Audience: "documents", TtlSeconds: 60, + AttenuatedScopes: []*gen.WorkContextScope{{ResourceKind: "evidence", Actions: []string{"read"}}}, + })) + return server, token, err + }}, + {rpc: "WorkContextService/StartChildSession", code: codes.PermissionDenied, mint: func(t *testing.T) (*WorkContextAuthorityServer, string, error) { + server, parent := renewServer(t) + token, err := sealPinIssued(server.StartChildSession(owner(), sealPinChildRequest(parent, "tool.test"))) + return server, token, err + }}, + {rpc: "WorkContextService/RenewWorkContext", code: codes.PermissionDenied, mint: func(t *testing.T) (*WorkContextAuthorityServer, string, error) { + server, parent := renewServer(t) + token, err := sealPinIssued(server.RenewWorkContext(owner(), &gen.RenewWorkContextRequest{ + OrgId: renewOrgID, ParentWorkContextToken: parent, TtlSeconds: 60, + })) + return server, token, err + }}, + {rpc: "ModuleCapabilitiesService/ExchangeDelegatedOperationAudience", code: codes.FailedPrecondition, mint: func(t *testing.T) (*WorkContextAuthorityServer, string, error) { + _, _, client, _ := sourceReadFixture(t) + service.SetModuleAuthorityReads(currentModuleAuthority{service: service}, nil) + service.SetModuleCapabilities(nil, nil, installedOperationRegistry(true)) + // A declared module's own capability, addressed to the exchanging one. + parent := serviceParent(t, workContextSingleton, readOrg, "example", "example", "installation-example", + &basev0.WorkScopeV1{ResourceKind: "results", Actions: []string{"read", "write"}}) + req := connect.NewRequest(&gen.ModuleExchangeDelegatedOperationAudienceRequest{BindingId: "generate", ParentWorkContextToken: parent}) + req.Header().Set(workContextHeader, mintModuleIdentity(t, business.ModulePrincipalID("example"), readOrg)) + req.Header().Set("x-codefly-internal-token", "source-read-test-perimeter") + out, err := client.ExchangeDelegatedOperationAudience(context.Background(), req) + if err != nil { + return workContextSingleton, "", err + } + return workContextSingleton, out.Msg.GetToken(), nil + }}, + {rpc: "ModuleCapabilitiesService/ExchangeDelegatedReadAudience", code: codes.FailedPrecondition, mint: func(t *testing.T) (*WorkContextAuthorityServer, string, error) { + _, _, client, _ := sourceReadFixture(t) + installReadBinding() + // A declared module's own capability, addressed to the exchanging one. + parent := serviceParent(t, workContextSingleton, readOrg, "example", "example", "installation-example", + &basev0.WorkScopeV1{ResourceKind: "results", Actions: []string{"read", "write"}}) + out, err := client.ExchangeDelegatedReadAudience(context.Background(), readExchangeRequest(t, parent)) + if err != nil { + return workContextSingleton, "", err + } + return workContextSingleton, out.Msg.GetToken(), nil + }}, + } +} + +// A module operation context presented to every re-mint is refused at the mint. +func TestEveryRemintOfAServicePrincipalsParentHasAVerdict(t *testing.T) { + for _, row := range serviceParentRows() { + t.Run(row.rpc, func(t *testing.T) { + _, token, err := row.mint(t) + require.Error(t, err) + code := status.Code(err) + if code == codes.Unknown { + // The module exchanges answer through the Connect client. + code = codes.Code(connect.CodeOf(err)) + } + require.Equal(t, row.code, code, "%v", err) + require.Empty(t, token) + }) + } +} + +// accountsMethods lists "Service/Method" for every saas.accounts.v1 method +// matching keep, sorted. +func accountsMethods(keep func(protoreflect.MethodDescriptor) bool) []string { + var out []string + protoregistry.GlobalFiles.RangeFilesByPackage("saas.accounts.v1", func(file protoreflect.FileDescriptor) bool { + for i := 0; i < file.Services().Len(); i++ { + service := file.Services().Get(i) + for j := 0; j < service.Methods().Len(); j++ { + if method := service.Methods().Get(j); keep(method) { + out = append(out, string(service.Name())+"/"+string(method.Name())) + } + } + } + return true + }) + sort.Strings(out) + return out +} + +// capabilityFields are the field names a signed capability travels under. +var capabilityFields = map[protoreflect.Name]bool{"token": true, "work_context": true, "work_context_token": true} + +// answersCapability reports whether a message carries a signed capability +// anywhere inside it: IssuedWorkContext, or a string field of an accounts +// message named as one, at any depth. Another package's field of the same name +// is not this issuer's capability (saas.jobs.v1.Job's token is a lease id). +func answersCapability(message protoreflect.MessageDescriptor) bool { + return containsField(message, map[protoreflect.FullName]bool{}, func(field protoreflect.FieldDescriptor) bool { + return field.Kind() == protoreflect.StringKind && capabilityFields[field.Name()] && + field.ContainingMessage().ParentFile().Package() == "saas.accounts.v1" + }, "saas.accounts.v1.IssuedWorkContext") +} + +// takesParent reports whether a message carries a parent capability at any depth. +func takesParent(message protoreflect.MessageDescriptor) bool { + return containsField(message, map[protoreflect.FullName]bool{}, func(field protoreflect.FieldDescriptor) bool { + return field.Name() == "parent_work_context_token" + }, "") +} + +func containsField(message protoreflect.MessageDescriptor, seen map[protoreflect.FullName]bool, + match func(protoreflect.FieldDescriptor) bool, wanted protoreflect.FullName, +) bool { + if message.FullName() == wanted { + return true + } + if seen[message.FullName()] { + return false + } + seen[message.FullName()] = true + fields := message.Fields() + for i := 0; i < fields.Len(); i++ { + field := fields.Get(i) + if match(field) { + return true + } + if field.Message() != nil && containsField(field.Message(), seen, match, wanted) { + return true + } + } + return false +} + +// The capability detection reaches through nesting: a response that wraps +// IssuedWorkContext, or a capability field, in another message is found. +func TestCapabilityDetectionIsTransitive(t *testing.T) { + issued, err := protoregistry.GlobalFiles.FindDescriptorByName("saas.accounts.v1.IssuedWorkContext") + require.NoError(t, err) + file, err := protodesc.NewFile(&descriptorpb.FileDescriptorProto{ + Name: proto.String("saas/accounts/v1/zz_capability_detection.proto"), + Package: proto.String("saas.accounts.v1"), + Syntax: proto.String("proto3"), + Dependency: []string{issued.ParentFile().Path()}, + MessageType: []*descriptorpb.DescriptorProto{ + {Name: proto.String("ZzDetectionWrapped"), Field: []*descriptorpb.FieldDescriptorProto{{ + Name: proto.String("issued"), Number: proto.Int32(1), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), + Type: descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum(), TypeName: proto.String(".saas.accounts.v1.IssuedWorkContext"), + }}}, + {Name: proto.String("ZzDetectionInner"), Field: []*descriptorpb.FieldDescriptorProto{{ + Name: proto.String("work_context"), Number: proto.Int32(1), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), + Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum(), + }, { + Name: proto.String("parent_work_context_token"), Number: proto.Int32(2), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), + Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum(), + }}}, + {Name: proto.String("ZzDetectionOuter"), Field: []*descriptorpb.FieldDescriptorProto{{ + Name: proto.String("inner"), Number: proto.Int32(1), Label: descriptorpb.FieldDescriptorProto_LABEL_REPEATED.Enum(), + Type: descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum(), TypeName: proto.String(".saas.accounts.v1.ZzDetectionInner"), + }}}, + {Name: proto.String("ZzDetectionPlain"), Field: []*descriptorpb.FieldDescriptorProto{{ + Name: proto.String("next_page_token"), Number: proto.Int32(1), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), + Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum(), + }}}, + }, + }, protoregistry.GlobalFiles) + require.NoError(t, err) + messages := file.Messages() + require.True(t, answersCapability(messages.ByName("ZzDetectionWrapped")), "IssuedWorkContext one level down") + require.True(t, answersCapability(messages.ByName("ZzDetectionOuter")), "a capability field two levels down") + require.True(t, takesParent(messages.ByName("ZzDetectionOuter")), "a parent field two levels down") + require.False(t, answersCapability(messages.ByName("ZzDetectionPlain"))) + require.False(t, takesParent(messages.ByName("ZzDetectionPlain"))) +} + // ownerKindConstants reads every PrincipalKind* constant business declares. func ownerKindConstants(t *testing.T) map[string]bool { t.Helper() @@ -430,18 +828,8 @@ func TestWorkloadOperationTableNamesEveryMintAndOwnerKind(t *testing.T) { kinds[row.kind] = true } - var minting []string - protoregistry.GlobalFiles.RangeFilesByPackage("saas.accounts.v1", func(file protoreflect.FileDescriptor) bool { - for i := 0; i < file.Services().Len(); i++ { - service := file.Services().Get(i) - for j := 0; j < service.Methods().Len(); j++ { - method := service.Methods().Get(j) - if method.Output().Fields().ByName("token") != nil { - minting = append(minting, string(service.Name())+"/"+string(method.Name())) - } - } - } - return true + minting := accountsMethods(func(method protoreflect.MethodDescriptor) bool { + return answersCapability(method.Output()) }) require.NotEmpty(t, minting) sort.Strings(minting) @@ -464,6 +852,27 @@ func TestWorkloadOperationTableNamesEveryMintAndOwnerKind(t *testing.T) { require.True(t, signing[fn], "mintEntrypointRows names %s, which no longer signs a capability", fn) } + // Every re-mint (an RPC that answers a capability and takes a parent) has a + // service-parent row, and a module's re-mint is also run against + // PostgreSQL, whose refusal the fake only mirrors. + parents := map[string]bool{} + for _, row := range serviceParentRows() { + parents[row.rpc] = true + } + postgres, err := os.ReadFile(filepath.Join("..", "infra", "service_parent_exchange_test.go")) + require.NoError(t, err) + remints := accountsMethods(func(method protoreflect.MethodDescriptor) bool { + return answersCapability(method.Output()) && takesParent(method.Input()) + }) + require.NotEmpty(t, remints) + for _, rpc := range remints { + require.True(t, parents[rpc], "%s re-mints a parent and serviceParentRows has no row for it", rpc) + service, method, _ := strings.Cut(rpc, "/") + if service == "ModuleCapabilitiesService" { + require.Contains(t, string(postgres), "."+method+"(", "%s has no PostgreSQL-backed row in pkg/infra/service_parent_exchange_test.go", rpc) + } + } + for kind := range ownerKindConstants(t) { require.True(t, kinds[kind], "owner kind %q has no row: say what the arm answers for it", kind) } @@ -511,12 +920,15 @@ func TestAdmitWorkloadOperationIsInternalAndFailsClosed(t *testing.T) { installModuleWorkContextAuthority(t) resp, err := mintModuleOperationContext("documents", moduleToken(t, "documents"), "model") require.NoError(t, err) - request := &gen.AdmitWorkloadOperationRequest{WorkContext: resp.GetToken(), Alias: "modelservice"} + request := &gen.AdmitWorkloadOperationRequest{WorkContext: resp.GetToken(), Alias: "modelservice", TargetId: admissionTarget} + installTargetRows(t, admissionTargetRow) SetInternalToken(admissionTestToken) t.Cleanup(func() { SetInternalToken("") }) _, err = workContextSingleton.AdmitWorkloadOperation(context.Background(), request) require.Equal(t, codes.Unauthenticated, status.Code(err)) + _, err = workContextSingleton.AdmitWorkloadOperation(context.Background(), &gen.AdmitWorkloadOperationRequest{}) + require.Equal(t, codes.Unauthenticated, status.Code(err), "the credential is checked before the request is validated") bearer := stampVerifiedIdentity(context.Background(), readOwner, readOrg, auth.Assurance{}) _, err = workContextSingleton.AdmitWorkloadOperation(bearer, request) require.Equal(t, codes.Unauthenticated, status.Code(err), "a person's identity is not the internal credential") @@ -525,10 +937,22 @@ func TestAdmitWorkloadOperationIsInternalAndFailsClosed(t *testing.T) { _, err = unconfigured.AdmitWorkloadOperation(admissionContext(t), request) require.Equal(t, codes.Unavailable, status.Code(err)) - service.SetModuleAuthorityReads(unavailableModuleAuthority{currentModuleAuthority{service: service}}, nil) + // The target's installations cannot be read. + workloadTargetInstallations = func(context.Context, string, string) ([]string, error) { + return nil, errors.New("installations unavailable") + } answer, err := workContextSingleton.AdmitWorkloadOperation(admissionContext(t), request) require.Equal(t, codes.Unavailable, status.Code(err)) require.Nil(t, answer) + + // The read-back's live seal cannot be read: not a refusal of the + // capability, so not an answer either. + installTargetRows(t, admissionTargetRow) + service.SetModuleAuthorityReads(unavailableModuleAuthority{currentModuleAuthority{service: service}}, nil) + workContextSingleton.seals = service + answer, err = workContextSingleton.AdmitWorkloadOperation(admissionContext(t), request) + require.Equal(t, codes.Unavailable, status.Code(err)) + require.Nil(t, answer) } // unavailableModuleAuthority is a live authority read that cannot answer. @@ -537,3 +961,30 @@ type unavailableModuleAuthority struct{ currentModuleAuthority } func (unavailableModuleAuthority) LiveModuleAuthority(context.Context, string, string, string) (*business.LiveModuleAuthority, error) { return nil, errors.New("authority read unavailable") } + +// sealedModuleAuthority answers a module principal's one installation and, as +// the store's read does, nothing for any other installation id. +type sealedModuleAuthority struct { + currentModuleAuthority + installation string +} + +func (f *sealedModuleAuthority) LiveModuleAuthority(ctx context.Context, principal, org, installation string) (*business.LiveModuleAuthority, error) { + live, err := f.currentModuleAuthority.LiveModuleAuthority(ctx, principal, org, installation) + if err != nil { + return nil, err + } + live.InstallationID = f.installation + if installation != "" && installation != live.InstallationID { + return nil, business.ErrModuleInstallationInactive + } + return live, nil +} + +// uninstalledModuleAuthority is a live authority read that finds no ACTIVE +// installation for the principal: the module was uninstalled. +type uninstalledModuleAuthority struct{ currentModuleAuthority } + +func (uninstalledModuleAuthority) LiveModuleAuthority(context.Context, string, string, string) (*business.LiveModuleAuthority, error) { + return nil, business.ErrModuleInstallationInactive +} diff --git a/module/services/accounts/code/pkg/business/installations.go b/module/services/accounts/code/pkg/business/installations.go index a69e0065b..ed7a39fe2 100644 --- a/module/services/accounts/code/pkg/business/installations.go +++ b/module/services/accounts/code/pkg/business/installations.go @@ -261,6 +261,43 @@ func (s *Service) ListInstallations(ctx context.Context, req *gen.ListInstallati return &gen.ListInstallationsResponse{Installations: summaries, NextPageToken: nextToken}, nil } +// activeTargetInstallationPageLimit bounds ActiveTargetInstallations: a cursor +// that has not ended after this many full pages is an error, never a short +// answer. +const activeTargetInstallationPageLimit = 20 + +// ActiveTargetInstallations answers the ids of orgID's ACTIVE installations of +// targetID, read through ListInstallations page by page. It is the admission +// read of the gateway's bearer-less module arm, asked inside +// AdmitWorkloadOperation so the gateway makes one call; the arm needs every +// row, because a seal on ANY of them is the target's own. +func (s *Service) ActiveTargetInstallations(ctx context.Context, orgID, targetID string) ([]string, error) { + var ids []string + token := "" + for page := 0; page < activeTargetInstallationPageLimit; page++ { + resp, err := s.ListInstallations(ctx, &gen.ListInstallationsRequest{ + OrgId: orgID, + Status: gen.InstallationStatus_INSTALLATION_STATUS_ACTIVE, + PageSize: listInstallationsMaxPageSize, + PageToken: token, + }) + if err != nil { + return nil, err + } + for _, summary := range resp.GetInstallations() { + installation := summary.GetInstallation() + if installation.GetOrgId() == orgID && installation.GetTargetId() == targetID && + installation.GetStatus() == gen.InstallationStatus_INSTALLATION_STATUS_ACTIVE { + ids = append(ids, installation.GetId()) + } + } + if token = resp.GetNextPageToken(); token == "" { + return ids, nil + } + } + return nil, fmt.Errorf("installations of organization %s did not end within %d pages", orgID, activeTargetInstallationPageLimit) +} + // TransferInstallationOwnership reassigns the owner of record and replaces the // co-owner succession set. The new owner must be a current org admin; the store // fails closed otherwise. diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/work_contexts.connect.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/work_contexts.connect.go index 698c883d4..0966e298b 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/work_contexts.connect.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/work_contexts.connect.go @@ -88,14 +88,16 @@ type WorkContextServiceClient interface { ConsumeSingleUse(context.Context, *connect.Request[v1.ConsumeSingleUseWorkContextRequest]) (*connect.Response[emptypb.Empty], error) // AdmitWorkloadOperation answers whether a capability presented with no // bearer at the declared module route /v1//* is a module operation - // context (StartModuleOperationTask) for that alias, and admits nothing else. + // context (StartModuleOperationTask) for that alias, and admits nothing else; + // and whether the route's target is installed in the capability's tenant. // The auth-gateway's bearer-less arm is its caller. Unlike the revision // checks above, the signed capability itself crosses this boundary: the // answer depends on its owner, owner kind and chain, which only the issuer - // reads. It inspects (signature under this issuer, window, audience) and - // reads its own claims; it never verifies, so nothing is consumed and the - // callee still verifies the capability at full strength. Internal-tier: the - // tenant it answers about is the one the capability names. + // reads. It reads the capability back (core's Authority.ReadBack: signature + // under this issuer, window, audience, revision, seal, epochs) and never + // verifies it as presented, so nothing is consumed and the callee still + // verifies at full strength. Internal-tier: the tenant it answers about is + // the one the capability names. AdmitWorkloadOperation(context.Context, *connect.Request[v1.AdmitWorkloadOperationRequest]) (*connect.Response[v1.AdmitWorkloadOperationResponse], error) StartTask(context.Context, *connect.Request[v1.StartTaskWorkContextRequest]) (*connect.Response[v1.IssuedWorkContext], error) // StartInstallationTask is the headless mint. Unlike StartTask it opens with a @@ -281,14 +283,16 @@ type WorkContextServiceHandler interface { ConsumeSingleUse(context.Context, *connect.Request[v1.ConsumeSingleUseWorkContextRequest]) (*connect.Response[emptypb.Empty], error) // AdmitWorkloadOperation answers whether a capability presented with no // bearer at the declared module route /v1//* is a module operation - // context (StartModuleOperationTask) for that alias, and admits nothing else. + // context (StartModuleOperationTask) for that alias, and admits nothing else; + // and whether the route's target is installed in the capability's tenant. // The auth-gateway's bearer-less arm is its caller. Unlike the revision // checks above, the signed capability itself crosses this boundary: the // answer depends on its owner, owner kind and chain, which only the issuer - // reads. It inspects (signature under this issuer, window, audience) and - // reads its own claims; it never verifies, so nothing is consumed and the - // callee still verifies the capability at full strength. Internal-tier: the - // tenant it answers about is the one the capability names. + // reads. It reads the capability back (core's Authority.ReadBack: signature + // under this issuer, window, audience, revision, seal, epochs) and never + // verifies it as presented, so nothing is consumed and the callee still + // verifies at full strength. Internal-tier: the tenant it answers about is + // the one the capability names. AdmitWorkloadOperation(context.Context, *connect.Request[v1.AdmitWorkloadOperationRequest]) (*connect.Response[v1.AdmitWorkloadOperationResponse], error) StartTask(context.Context, *connect.Request[v1.StartTaskWorkContextRequest]) (*connect.Response[v1.IssuedWorkContext], error) // StartInstallationTask is the headless mint. Unlike StartTask it opens with a diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/work_contexts_grpc.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/work_contexts_grpc.pb.go index 4497085cc..e0209403a 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/work_contexts_grpc.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/work_contexts_grpc.pb.go @@ -60,14 +60,16 @@ type WorkContextServiceClient interface { ConsumeSingleUse(ctx context.Context, in *ConsumeSingleUseWorkContextRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) // AdmitWorkloadOperation answers whether a capability presented with no // bearer at the declared module route /v1//* is a module operation - // context (StartModuleOperationTask) for that alias, and admits nothing else. + // context (StartModuleOperationTask) for that alias, and admits nothing else; + // and whether the route's target is installed in the capability's tenant. // The auth-gateway's bearer-less arm is its caller. Unlike the revision // checks above, the signed capability itself crosses this boundary: the // answer depends on its owner, owner kind and chain, which only the issuer - // reads. It inspects (signature under this issuer, window, audience) and - // reads its own claims; it never verifies, so nothing is consumed and the - // callee still verifies the capability at full strength. Internal-tier: the - // tenant it answers about is the one the capability names. + // reads. It reads the capability back (core's Authority.ReadBack: signature + // under this issuer, window, audience, revision, seal, epochs) and never + // verifies it as presented, so nothing is consumed and the callee still + // verifies at full strength. Internal-tier: the tenant it answers about is + // the one the capability names. AdmitWorkloadOperation(ctx context.Context, in *AdmitWorkloadOperationRequest, opts ...grpc.CallOption) (*AdmitWorkloadOperationResponse, error) StartTask(ctx context.Context, in *StartTaskWorkContextRequest, opts ...grpc.CallOption) (*IssuedWorkContext, error) // StartInstallationTask is the headless mint. Unlike StartTask it opens with a @@ -228,14 +230,16 @@ type WorkContextServiceServer interface { ConsumeSingleUse(context.Context, *ConsumeSingleUseWorkContextRequest) (*emptypb.Empty, error) // AdmitWorkloadOperation answers whether a capability presented with no // bearer at the declared module route /v1//* is a module operation - // context (StartModuleOperationTask) for that alias, and admits nothing else. + // context (StartModuleOperationTask) for that alias, and admits nothing else; + // and whether the route's target is installed in the capability's tenant. // The auth-gateway's bearer-less arm is its caller. Unlike the revision // checks above, the signed capability itself crosses this boundary: the // answer depends on its owner, owner kind and chain, which only the issuer - // reads. It inspects (signature under this issuer, window, audience) and - // reads its own claims; it never verifies, so nothing is consumed and the - // callee still verifies the capability at full strength. Internal-tier: the - // tenant it answers about is the one the capability names. + // reads. It reads the capability back (core's Authority.ReadBack: signature + // under this issuer, window, audience, revision, seal, epochs) and never + // verifies it as presented, so nothing is consumed and the callee still + // verifies at full strength. Internal-tier: the tenant it answers about is + // the one the capability names. AdmitWorkloadOperation(context.Context, *AdmitWorkloadOperationRequest) (*AdmitWorkloadOperationResponse, error) StartTask(context.Context, *StartTaskWorkContextRequest) (*IssuedWorkContext, error) // StartInstallationTask is the headless mint. Unlike StartTask it opens with a diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/workload_operation_admission.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/workload_operation_admission.pb.go index ac7e67d52..dde00c76c 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/workload_operation_admission.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/workload_operation_admission.pb.go @@ -46,6 +46,16 @@ const ( // The owner is not a declared module principal, or the seal is not an ACTIVE // installation in the tenant installed under that principal. WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS WorkloadOperationRefusal = 5 + // The tenant has no ACTIVE installation of the route's target. The gateway + // answers this one 403, as it answers a person whose organization never + // installed the module. + WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_TARGET_NOT_INSTALLED WorkloadOperationRefusal = 6 + // The seal is one of the target's own installations: the caller would be the + // module it addresses. + WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_SEAL_ON_TARGET WorkloadOperationRefusal = 7 + // The issuer's live state has moved past the capability: its authorization + // revision is superseded, or an epoch it carries has advanced. + WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT WorkloadOperationRefusal = 8 ) // Enum value maps for WorkloadOperationRefusal. @@ -57,6 +67,9 @@ var ( 3: "WORKLOAD_OPERATION_REFUSAL_NOT_OPERATION_CONTEXT", 4: "WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET", 5: "WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS", + 6: "WORKLOAD_OPERATION_REFUSAL_TARGET_NOT_INSTALLED", + 7: "WORKLOAD_OPERATION_REFUSAL_SEAL_ON_TARGET", + 8: "WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT", } WorkloadOperationRefusal_value = map[string]int32{ "WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED": 0, @@ -65,6 +78,9 @@ var ( "WORKLOAD_OPERATION_REFUSAL_NOT_OPERATION_CONTEXT": 3, "WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET": 4, "WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS": 5, + "WORKLOAD_OPERATION_REFUSAL_TARGET_NOT_INSTALLED": 6, + "WORKLOAD_OPERATION_REFUSAL_SEAL_ON_TARGET": 7, + "WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT": 8, } ) @@ -97,12 +113,15 @@ func (WorkloadOperationRefusal) EnumDescriptor() ([]byte, []int) { type AdmitWorkloadOperationRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // The capability as presented, encoded. It is inspected and read, never - // verified: nothing it carries is consumed. + // The capability as presented, encoded. It is read back by its issuer, never + // verified as presented: nothing it carries is consumed. WorkContext string `protobuf:"bytes,1,opt,name=work_context,json=workContext,proto3" json:"work_context,omitempty"` // The route alias the gateway resolved from the request path. The capability // must be addressed to it. - Alias string `protobuf:"bytes,2,opt,name=alias,proto3" json:"alias,omitempty"` + Alias string `protobuf:"bytes,2,opt,name=alias,proto3" json:"alias,omitempty"` + // The immutable solution target the route's alias resolved to, from the + // gateway's own routing. The tenant must have an ACTIVE installation of it. + TargetId string `protobuf:"bytes,3,opt,name=target_id,json=targetId,proto3" json:"target_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -151,6 +170,13 @@ func (x *AdmitWorkloadOperationRequest) GetAlias() string { return "" } +func (x *AdmitWorkloadOperationRequest) GetTargetId() string { + if x != nil { + return x.TargetId + } + return "" +} + type AdmitWorkloadOperationResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Admitted bool `protobuf:"varint,1,opt,name=admitted,proto3" json:"admitted,omitempty"` @@ -235,24 +261,28 @@ var File_saas_accounts_v1_workload_operation_admission_proto protoreflect.FileDe const file_saas_accounts_v1_workload_operation_admission_proto_rawDesc = "" + "\n" + - "3saas/accounts/v1/workload_operation_admission.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\"q\n" + + "3saas/accounts/v1/workload_operation_admission.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\"\x98\x01\n" + "\x1dAdmitWorkloadOperationRequest\x12.\n" + "\fwork_context\x18\x01 \x01(\tB\v\xbaH\br\x06\x10\x01\x18\x80\x80\x02R\vworkContext\x12 \n" + "\x05alias\x18\x02 \x01(\tB\n" + - "\xbaH\ar\x05\x10\x01\x18\x80\x04R\x05alias\"\xe1\x01\n" + + "\xbaH\ar\x05\x10\x01\x18\x80\x04R\x05alias\x12%\n" + + "\ttarget_id\x18\x03 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\btargetId\"\xe1\x01\n" + "\x1eAdmitWorkloadOperationResponse\x12\x1a\n" + "\badmitted\x18\x01 \x01(\bR\badmitted\x12D\n" + "\arefusal\x18\x02 \x01(\x0e2*.saas.accounts.v1.WorkloadOperationRefusalR\arefusal\x12\x1b\n" + "\ttenant_id\x18\x03 \x01(\tR\btenantId\x12'\n" + "\x0finstallation_id\x18\x04 \x01(\tR\x0einstallationId\x12\x17\n" + - "\atask_id\x18\x05 \x01(\tR\x06taskId*\xbc\x02\n" + + "\atask_id\x18\x05 \x01(\tR\x06taskId*\xcc\x03\n" + "\x18WorkloadOperationRefusal\x12*\n" + "&WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED\x10\x00\x12,\n" + "(WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED\x10\x01\x120\n" + ",WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE\x10\x02\x124\n" + "0WORKLOAD_OPERATION_REFUSAL_NOT_OPERATION_CONTEXT\x10\x03\x12.\n" + "*WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET\x10\x04\x12.\n" + - "*WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS\x10\x05B\xc7\x01\n" + + "*WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS\x10\x05\x123\n" + + "/WORKLOAD_OPERATION_REFUSAL_TARGET_NOT_INSTALLED\x10\x06\x12-\n" + + ")WORKLOAD_OPERATION_REFUSAL_SEAL_ON_TARGET\x10\a\x12*\n" + + "&WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT\x10\bB\xc7\x01\n" + "\x14com.saas.accounts.v1B\x1fWorkloadOperationAdmissionProtoP\x01Z,accounts/pkg/gen/saas/accounts/v1;accountsv1\xa2\x02\x03SAX\xaa\x02\x10Saas.Accounts.V1\xca\x02\x10Saas\\Accounts\\V1\xe2\x02\x1cSaas\\Accounts\\V1\\GPBMetadata\xea\x02\x12Saas::Accounts::V1b\x06proto3" var ( diff --git a/module/services/accounts/code/pkg/infra/active_target_installations_test.go b/module/services/accounts/code/pkg/infra/active_target_installations_test.go new file mode 100644 index 000000000..274c2a727 --- /dev/null +++ b/module/services/accounts/code/pkg/infra/active_target_installations_test.go @@ -0,0 +1,40 @@ +package infra_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "accounts/pkg/business" +) + +// ActiveTargetInstallations is the admission read of the gateway's bearer-less +// module arm: every ACTIVE installation of one target in one organization, and +// nothing else. A revoked row, another target's row and another organization's +// row of the same target are none of them. +func TestActiveTargetInstallationsAnswersEveryActiveRowOfTheTarget(t *testing.T) { + svc, err := business.NewService(testStore) + require.NoError(t, err) + orgA, ownerA, roleA := entitlementOrg(t) + orgB, ownerB, roleB := entitlementOrg(t) + alias := uniqueAlias("ledger") + target := declarePresence(t, "acme.test."+alias, alias) + + revoked := installSolutionTarget(t, orgA, ownerA, roleA, "ledger", target) + require.NoError(t, testStore.WithOrgTx(testCtx, orgA, func(ctx context.Context) error { + _, _, err := testStore.UninstallSolution(ctx, orgA, revoked.GetId()) + return err + })) + current := installSolutionTarget(t, orgA, ownerA, roleA, "ledger", target) + installSolution(t, orgA, ownerA, roleA, "audit") + installSolutionTarget(t, orgB, ownerB, roleB, "ledger-other-org", target) + + ids, err := svc.ActiveTargetInstallations(testCtx, orgA, target) + require.NoError(t, err) + require.Equal(t, []string{current.GetId()}, ids) + + ids, err = svc.ActiveTargetInstallations(testCtx, orgA, business.NewIDString()) + require.NoError(t, err) + require.Empty(t, ids, "a target the organization never installed has no rows") +} diff --git a/module/services/accounts/code/pkg/infra/service_parent_exchange_test.go b/module/services/accounts/code/pkg/infra/service_parent_exchange_test.go new file mode 100644 index 000000000..a3c11413b --- /dev/null +++ b/module/services/accounts/code/pkg/infra/service_parent_exchange_test.go @@ -0,0 +1,103 @@ +//go:build !pure + +package infra_test + +import ( + "crypto/ed25519" + "crypto/rand" + "encoding/json" + "testing" + + "accounts/pkg/adapters" + "accounts/pkg/business" + "accounts/pkg/business/executiontest" + gen "accounts/pkg/gen/saas/accounts/v1" + + "github.com/codefly-dev/core/solutionhost" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +// A module's delegated exchanges take a parent whose owner the host resolves +// current authority for: an organization membership and an authorization +// revision, both rows a declared module principal never has. A module +// operation context minted through StartModuleOperationTask, presented as the +// parent to either exchange by the module it is addressed to, is therefore +// refused before any binding is resolved. Run against the migrated store, not a +// fake, because the refusal is the store's. +func TestADelegatedExchangeRefusesAModuleOperationContextAsItsParent(t *testing.T) { + s := newRegistryScenario(t, "opsource") + target := newRegistryBinding(t, "optarget") + svc := s.delivery.service + manifests := map[string]*solutionhost.ModuleManifest{} + for _, b := range []*registryBinding{s.primary, target} { + m := productionPathManifest(t, b.solutionID, 1) + m.Kind = solutionhost.KindModule + m.Authority = &solutionhost.Authority{Principal: b.solutionID, ResourceKinds: []string{"documents"}} + manifests[b.solutionID] = m + } + read := []business.ModuleOperationScope{{ResourceKind: "documents", Actions: []string{"read"}}} + // The operation context's audience is the target; its scopes are read. + grants := map[string]business.ModuleEnvelopePrincipal{ + s.prefix: {Grant: business.ModulePrincipalGrant{Tenant: s.tenant, Resources: []string{"documents"}}}, + target.solutionID: {Grant: business.ModulePrincipalGrant{Tenant: s.tenant, Resources: []string{"documents"}}}, + } + rev := s.currentRevision(t) + wire := map[string]any{} + for prefix, entry := range grants { + wire[prefix] = map[string]any{"grant": map[string]any{"tenant": entry.Grant.Tenant, "resources": entry.Grant.Resources}} + } + raw, err := json.Marshal(map[string]any{"revision": rev + 1, "principals": wire}) + require.NoError(t, err) + _, err = svc.PutModuleEnvelope(testCtx, s.admin, rev, raw) + require.NoError(t, err) + for _, b := range []*registryBinding{s.primary, target} { + requireDelivered(t, s.delivery.deliver(t, manifests[b.solutionID])) + } + s.pass(t) + originInstall := s.install(t, s.tenant) + targetScenario := *s + targetScenario.prefix, targetScenario.primary = target.solutionID, target + targetInstall := targetScenario.install(t, s.tenant) + svc.SetWorkContextSealStore(testStore) + svc.SetExecutionBinding(executiontest.New()) + _, key, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + previous := *adapters.WorkContextSingleton() + t.Cleanup(func() { + *adapters.WorkContextSingleton() = previous + adapters.WithService(nil) + adapters.SetInternalToken("") + }) + issuer := adapters.WorkContextSingleton() + issuer.Configure(adapters.WorkContextAuthorityConfiguration{Issuer: "accounts.test", KeyID: "service-parent-test", PrivateKey: key, Authority: testStore, Seals: svc}) + adapters.WithService(svc) + adapters.SetInternalToken(delegationExchangeInternalToken) + + parent, _, err := issuer.StartModuleOperationTask(testCtx, business.ModuleOperationContextAuthority{ + ModuleWorkContextAuthority: business.ModuleWorkContextAuthority{PrincipalID: s.principalID(), Tenant: s.tenant, InstallationID: originInstall.Id}, + BindingID: "publish", Audience: target.solutionID, Scopes: read, Revision: 1, + }) + require.NoError(t, err) + moduleToken, _, err := issuer.StartModuleTask(testCtx, business.ModuleWorkContextAuthority{PrincipalID: business.ModulePrincipalID(target.solutionID), Tenant: s.tenant, InstallationID: targetInstall.Id}) + require.NoError(t, err) + callee := metadata.NewIncomingContext(testCtx, metadata.Pairs("x-codefly-internal-token", delegationExchangeInternalToken, "x-codefly-work-context", moduleToken)) + refused := func(t *testing.T, out *gen.IssuedWorkContext, err error) { + t.Helper() + require.Equal(t, codes.FailedPrecondition, status.Code(err), "%v", err) + require.ErrorContains(t, err, "current organization membership and authorization revision required") + require.Nil(t, out) + } + t.Run("ExchangeDelegatedOperationAudience", func(t *testing.T) { + out, err := adapters.ModuleCapabilitiesSingleton().ExchangeDelegatedOperationAudience(callee, + &gen.ModuleExchangeDelegatedOperationAudienceRequest{BindingId: "back", ParentWorkContextToken: parent}) + refused(t, out, err) + }) + t.Run("ExchangeDelegatedReadAudience", func(t *testing.T) { + out, err := adapters.ModuleCapabilitiesSingleton().ExchangeDelegatedReadAudience(callee, + &gen.ModuleExchangeDelegatedReadAudienceRequest{BindingId: "back", ParentWorkContextToken: parent}) + refused(t, out, err) + }) +} diff --git a/module/services/accounts/proto/saas/accounts/v1/work_contexts.proto b/module/services/accounts/proto/saas/accounts/v1/work_contexts.proto index 8a645eea6..a6226ee7e 100644 --- a/module/services/accounts/proto/saas/accounts/v1/work_contexts.proto +++ b/module/services/accounts/proto/saas/accounts/v1/work_contexts.proto @@ -400,14 +400,16 @@ service WorkContextService { // AdmitWorkloadOperation answers whether a capability presented with no // bearer at the declared module route /v1//* is a module operation - // context (StartModuleOperationTask) for that alias, and admits nothing else. + // context (StartModuleOperationTask) for that alias, and admits nothing else; + // and whether the route's target is installed in the capability's tenant. // The auth-gateway's bearer-less arm is its caller. Unlike the revision // checks above, the signed capability itself crosses this boundary: the // answer depends on its owner, owner kind and chain, which only the issuer - // reads. It inspects (signature under this issuer, window, audience) and - // reads its own claims; it never verifies, so nothing is consumed and the - // callee still verifies the capability at full strength. Internal-tier: the - // tenant it answers about is the one the capability names. + // reads. It reads the capability back (core's Authority.ReadBack: signature + // under this issuer, window, audience, revision, seal, epochs) and never + // verifies it as presented, so nothing is consumed and the callee still + // verifies at full strength. Internal-tier: the tenant it answers about is + // the one the capability names. rpc AdmitWorkloadOperation(AdmitWorkloadOperationRequest) returns (AdmitWorkloadOperationResponse) { option (saas.policy.v1.method_policy) = { exposure: EXPOSURE_INTERNAL diff --git a/module/services/accounts/proto/saas/accounts/v1/workload_operation_admission.proto b/module/services/accounts/proto/saas/accounts/v1/workload_operation_admission.proto index dda95d168..3bc644315 100644 --- a/module/services/accounts/proto/saas/accounts/v1/workload_operation_admission.proto +++ b/module/services/accounts/proto/saas/accounts/v1/workload_operation_admission.proto @@ -11,8 +11,9 @@ import "buf/validate/validate.proto"; // A forwarding hop cannot answer it. Core's Inspector, which is all a hop runs, // withholds the owner, the owner's kind, the actor chain and the operation // binding, and those are exactly what tell a module's unattended work from a -// person's capability. Accounts signed the capability and can read its own -// claims, so the gateway asks it. WorkContextService/AdmitWorkloadOperation +// person's capability. Accounts signed the capability and reads it back, held +// against its own live state, so the gateway asks it, and asks in the same call +// whether the route's target is installed. WorkContextService/AdmitWorkloadOperation // takes and answers these messages. // // They live in their own file, with deliberately minimal imports, so the @@ -41,11 +42,21 @@ enum WorkloadOperationRefusal { // The owner is not a declared module principal, or the seal is not an ACTIVE // installation in the tenant installed under that principal. WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS = 5; + // The tenant has no ACTIVE installation of the route's target. The gateway + // answers this one 403, as it answers a person whose organization never + // installed the module. + WORKLOAD_OPERATION_REFUSAL_TARGET_NOT_INSTALLED = 6; + // The seal is one of the target's own installations: the caller would be the + // module it addresses. + WORKLOAD_OPERATION_REFUSAL_SEAL_ON_TARGET = 7; + // The issuer's live state has moved past the capability: its authorization + // revision is superseded, or an epoch it carries has advanced. + WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT = 8; } message AdmitWorkloadOperationRequest { - // The capability as presented, encoded. It is inspected and read, never - // verified: nothing it carries is consumed. + // The capability as presented, encoded. It is read back by its issuer, never + // verified as presented: nothing it carries is consumed. string work_context = 1 [ (buf.validate.field).string.min_len = 1, (buf.validate.field).string.max_len = 32768 @@ -56,6 +67,9 @@ message AdmitWorkloadOperationRequest { (buf.validate.field).string.min_len = 1, (buf.validate.field).string.max_len = 512 ]; + // The immutable solution target the route's alias resolved to, from the + // gateway's own routing. The tenant must have an ACTIVE installation of it. + string target_id = 3 [(buf.validate.field).string.uuid = true]; } message AdmitWorkloadOperationResponse { diff --git a/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/workload_operation_admission.pb.go b/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/workload_operation_admission.pb.go index 27cc9e025..9726c09e7 100644 --- a/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/workload_operation_admission.pb.go +++ b/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/workload_operation_admission.pb.go @@ -46,6 +46,16 @@ const ( // The owner is not a declared module principal, or the seal is not an ACTIVE // installation in the tenant installed under that principal. WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS WorkloadOperationRefusal = 5 + // The tenant has no ACTIVE installation of the route's target. The gateway + // answers this one 403, as it answers a person whose organization never + // installed the module. + WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_TARGET_NOT_INSTALLED WorkloadOperationRefusal = 6 + // The seal is one of the target's own installations: the caller would be the + // module it addresses. + WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_SEAL_ON_TARGET WorkloadOperationRefusal = 7 + // The issuer's live state has moved past the capability: its authorization + // revision is superseded, or an epoch it carries has advanced. + WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT WorkloadOperationRefusal = 8 ) // Enum value maps for WorkloadOperationRefusal. @@ -57,6 +67,9 @@ var ( 3: "WORKLOAD_OPERATION_REFUSAL_NOT_OPERATION_CONTEXT", 4: "WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET", 5: "WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS", + 6: "WORKLOAD_OPERATION_REFUSAL_TARGET_NOT_INSTALLED", + 7: "WORKLOAD_OPERATION_REFUSAL_SEAL_ON_TARGET", + 8: "WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT", } WorkloadOperationRefusal_value = map[string]int32{ "WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED": 0, @@ -65,6 +78,9 @@ var ( "WORKLOAD_OPERATION_REFUSAL_NOT_OPERATION_CONTEXT": 3, "WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET": 4, "WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS": 5, + "WORKLOAD_OPERATION_REFUSAL_TARGET_NOT_INSTALLED": 6, + "WORKLOAD_OPERATION_REFUSAL_SEAL_ON_TARGET": 7, + "WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT": 8, } ) @@ -97,12 +113,15 @@ func (WorkloadOperationRefusal) EnumDescriptor() ([]byte, []int) { type AdmitWorkloadOperationRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // The capability as presented, encoded. It is inspected and read, never - // verified: nothing it carries is consumed. + // The capability as presented, encoded. It is read back by its issuer, never + // verified as presented: nothing it carries is consumed. WorkContext string `protobuf:"bytes,1,opt,name=work_context,json=workContext,proto3" json:"work_context,omitempty"` // The route alias the gateway resolved from the request path. The capability // must be addressed to it. - Alias string `protobuf:"bytes,2,opt,name=alias,proto3" json:"alias,omitempty"` + Alias string `protobuf:"bytes,2,opt,name=alias,proto3" json:"alias,omitempty"` + // The immutable solution target the route's alias resolved to, from the + // gateway's own routing. The tenant must have an ACTIVE installation of it. + TargetId string `protobuf:"bytes,3,opt,name=target_id,json=targetId,proto3" json:"target_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -151,6 +170,13 @@ func (x *AdmitWorkloadOperationRequest) GetAlias() string { return "" } +func (x *AdmitWorkloadOperationRequest) GetTargetId() string { + if x != nil { + return x.TargetId + } + return "" +} + type AdmitWorkloadOperationResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Admitted bool `protobuf:"varint,1,opt,name=admitted,proto3" json:"admitted,omitempty"` @@ -235,24 +261,28 @@ var File_saas_accounts_v1_workload_operation_admission_proto protoreflect.FileDe const file_saas_accounts_v1_workload_operation_admission_proto_rawDesc = "" + "\n" + - "3saas/accounts/v1/workload_operation_admission.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\"q\n" + + "3saas/accounts/v1/workload_operation_admission.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\"\x98\x01\n" + "\x1dAdmitWorkloadOperationRequest\x12.\n" + "\fwork_context\x18\x01 \x01(\tB\v\xbaH\br\x06\x10\x01\x18\x80\x80\x02R\vworkContext\x12 \n" + "\x05alias\x18\x02 \x01(\tB\n" + - "\xbaH\ar\x05\x10\x01\x18\x80\x04R\x05alias\"\xe1\x01\n" + + "\xbaH\ar\x05\x10\x01\x18\x80\x04R\x05alias\x12%\n" + + "\ttarget_id\x18\x03 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\btargetId\"\xe1\x01\n" + "\x1eAdmitWorkloadOperationResponse\x12\x1a\n" + "\badmitted\x18\x01 \x01(\bR\badmitted\x12D\n" + "\arefusal\x18\x02 \x01(\x0e2*.saas.accounts.v1.WorkloadOperationRefusalR\arefusal\x12\x1b\n" + "\ttenant_id\x18\x03 \x01(\tR\btenantId\x12'\n" + "\x0finstallation_id\x18\x04 \x01(\tR\x0einstallationId\x12\x17\n" + - "\atask_id\x18\x05 \x01(\tR\x06taskId*\xbc\x02\n" + + "\atask_id\x18\x05 \x01(\tR\x06taskId*\xcc\x03\n" + "\x18WorkloadOperationRefusal\x12*\n" + "&WORKLOAD_OPERATION_REFUSAL_UNSPECIFIED\x10\x00\x12,\n" + "(WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED\x10\x01\x120\n" + ",WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE\x10\x02\x124\n" + "0WORKLOAD_OPERATION_REFUSAL_NOT_OPERATION_CONTEXT\x10\x03\x12.\n" + "*WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET\x10\x04\x12.\n" + - "*WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS\x10\x05B\xcb\x01\n" + + "*WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS\x10\x05\x123\n" + + "/WORKLOAD_OPERATION_REFUSAL_TARGET_NOT_INSTALLED\x10\x06\x12-\n" + + ")WORKLOAD_OPERATION_REFUSAL_SEAL_ON_TARGET\x10\a\x12*\n" + + "&WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT\x10\bB\xcb\x01\n" + "\x14com.saas.accounts.v1B\x1fWorkloadOperationAdmissionProtoP\x01Z0auth-gateway/pkg/gen/saas/accounts/v1;accountsv1\xa2\x02\x03SAX\xaa\x02\x10Saas.Accounts.V1\xca\x02\x10Saas\\Accounts\\V1\xe2\x02\x1cSaas\\Accounts\\V1\\GPBMetadata\xea\x02\x12Saas::Accounts::V1b\x06proto3" var ( diff --git a/module/services/auth-gateway/proto/saas/accounts/v1/workload_operation_admission.proto b/module/services/auth-gateway/proto/saas/accounts/v1/workload_operation_admission.proto index dda95d168..3bc644315 100644 --- a/module/services/auth-gateway/proto/saas/accounts/v1/workload_operation_admission.proto +++ b/module/services/auth-gateway/proto/saas/accounts/v1/workload_operation_admission.proto @@ -11,8 +11,9 @@ import "buf/validate/validate.proto"; // A forwarding hop cannot answer it. Core's Inspector, which is all a hop runs, // withholds the owner, the owner's kind, the actor chain and the operation // binding, and those are exactly what tell a module's unattended work from a -// person's capability. Accounts signed the capability and can read its own -// claims, so the gateway asks it. WorkContextService/AdmitWorkloadOperation +// person's capability. Accounts signed the capability and reads it back, held +// against its own live state, so the gateway asks it, and asks in the same call +// whether the route's target is installed. WorkContextService/AdmitWorkloadOperation // takes and answers these messages. // // They live in their own file, with deliberately minimal imports, so the @@ -41,11 +42,21 @@ enum WorkloadOperationRefusal { // The owner is not a declared module principal, or the seal is not an ACTIVE // installation in the tenant installed under that principal. WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS = 5; + // The tenant has no ACTIVE installation of the route's target. The gateway + // answers this one 403, as it answers a person whose organization never + // installed the module. + WORKLOAD_OPERATION_REFUSAL_TARGET_NOT_INSTALLED = 6; + // The seal is one of the target's own installations: the caller would be the + // module it addresses. + WORKLOAD_OPERATION_REFUSAL_SEAL_ON_TARGET = 7; + // The issuer's live state has moved past the capability: its authorization + // revision is superseded, or an epoch it carries has advanced. + WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT = 8; } message AdmitWorkloadOperationRequest { - // The capability as presented, encoded. It is inspected and read, never - // verified: nothing it carries is consumed. + // The capability as presented, encoded. It is read back by its issuer, never + // verified as presented: nothing it carries is consumed. string work_context = 1 [ (buf.validate.field).string.min_len = 1, (buf.validate.field).string.max_len = 32768 @@ -56,6 +67,9 @@ message AdmitWorkloadOperationRequest { (buf.validate.field).string.min_len = 1, (buf.validate.field).string.max_len = 512 ]; + // The immutable solution target the route's alias resolved to, from the + // gateway's own routing. The tenant must have an ACTIVE installation of it. + string target_id = 3 [(buf.validate.field).string.uuid = true]; } message AdmitWorkloadOperationResponse { diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json index ccc81274a..379b3573d 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json +++ b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contract.binpb", - "digest": "sha256:d0d8f6519a5a1eb4394265f3094faa19fee5a24afb3feeff4b33ea4ad5b07e5c", + "digest": "sha256:c9df546fead8be738783f6fba95b7a35c8037576c2e6362077f7e44406ca3e32", "services": [ { "name": "APIKeyService", diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb b/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb index 9931b6eab4fece02bc19ff8f656858c59edd986c..99f9f12a78efb6ba8a5f898f5af449dec14b52d4 100644 GIT binary patch delta 4874 zcmaKvYmgMx700*x-s$P->0y`VwG3=*C|1xNc2`~nY(!STm3LT{N~J1Qq<5#;opPBW zGqX#PFOyWJVjjLt(FReJC@T2K!#D^=1@kZh3IQ|<0u;yRJ68i|?6p zyd9U~ZLz8MDMpjme#&h03ZIxU@9mRjcfx8m5@n~@?|P$qt#RI>BMk|!<&^n8Kgx_o z@6bteOl|sUbGpK<*+!e!e%hRr?&<9ATGq31&Dx%g%h#;$>0GsH+0q8hnrU1^-y7C1 z>s+;Q?V62#TtnQ;oHnnTAi|ey=X7ReZ9`Ejt8m82r)eSXPXYq-3 z^OW?kP5XNHziIZVZ^fT`%e-61{u?O72tYJI-PA*?6LoV5ct$BMe zH|*M_vQu_5cBx>O2VA=!gRRas+bP{5re|y?pOu3=1(cl4uHEMh4jT5>0T)w>Bo_-q zWp{eXCU45NnZs;sg*Z1`oAS1qRc@LXYcfnD7OPY16pN+R8Aj6JVk8DLgHI|(Eg3md z5ZI(%Yk-7^6eI7*JLZnbO*30%h!AbH)hJU0X4~Gk&X~hZ_I0yl1Vco#I?K36r4WWu zZR}ZdtTMTI?mQX85i>*07+}$rM4IavLP-8?de`htK>R^woIh3iPrL21G=asNt?DVEu`CnK z3qylhyB8J=xQ6W~AS$!sWV1!LRB}s{j>smCY-Y8#$!v6FGZ$qdE!V|pX8OCnpSe?sEEnj&4pn%Yj1H-=AxeaoGngoZZKEgwvfu+87dDHieyoxkT>k2JDe*Cw{W!5 zkTBEDy4ehzg#Wg?rO-DZa_w`9#hhEB9h~g2lkao0Vb(gbp4*Z!3`6C(l=JIuR`74e zMVZgno5g83${Z!rq3XuHs17l-bl#yZ;_0g5?Kx-8oGorF-B!DI8>Q->FcWYD%-p6N z&V~xu0^i(x+5706+1A^U7#D1T+U{taXr%lNb&GeT7cry3NQe#fTN9&@=@)s+NCi`_ z=j1qI6y(^69C4Y%$l{&qQl8%AU3AQG-j4I;SNl2>qvU1^W_FIYm0K**i4a| z-57303Z^XKbk(uEip{#P*+ae|NUcllOy8q;gNKt-Hbj@h8O-6xIB{W6pe+|N_NHQC zvtLf~w#XtB_bHd}_b-yXahr!w3Ih&0os$>$uCbCU`{+atK-876N8Xnk#6O%vPqZn~3R#$g| zizjVmP5I2cWr1>{ws~Ia+4$JsYX;ZqkiNE)sgE<3KBU>=DIV{wT$K9N@($4*v;Xe_ z8u|X9H+`-bh=+>$u~&aX>ixz3V|it}@UP;GzCuwvYn^`EC~s@+#x=bWZ|Xw3ltN@xTVb z-mb-|=E=bmf_4|@CnLqABvs!uZQYMC2JSotY& zg0U4kJZboGrajHHpRxGJNZ9mqrd?%>F_LhJ0^xhcw&}zjHzdFkq>g)``V@CxZ0s5 zsdlw;jA@f3lNe5Yj7^>&41wqvThb#liUHBUZgnnqgALm)}gU}DsuxCF*8M#yS&hq* z{Rh)#%KD@*c=yl9JCfkt#mHfjP_mbzk`;!0$oR)$$!fA>KV->|!;U*p z64HR>dpvByBmv9!dDuxvLPh_82UIF5X~1$D4?77-z;Zhedo@YGat9APi7@0Y&hLdG zekX}bEO+tby>P_uB)$kNcXRE!D>P8KhgX2b@AJB)ZRhGUJZSUyKLX#*wF&Z);HNl- zjUB-daJKWCzAW#Ij$Ay>_l9tEACAY9dqX&+Vb%M1*yi<$h{Mu9;-b$J9SRe4c5v;% z5Duw;vnzr_62WkH)Bd=V?|j^L06IFCedNCKS7qY)j_Kxa=#M3Z}Qc+0* zmscY$B!SECA}%C>%WDxAVaQR=-w3%RB$uN+`9{da7lGv%$Imezi?0F8>-^UKwJH3K S!e2f98t^x|*1y)ujsG7)Dw3E0 delta 3812 zcmZ9OZE#f88OQhD^X%EXXYVHACWaW2W{JEtZzNz-f-pEuLe)-ZIu0E_I!oBnjA;Vd zMaF(vaJEC=q}7RDCSbKH8kEl15!fi8IJK>kAnkyq(m;zN(&;O-_CwTm`ajQoTm7&M z&;LH>x##?U|9jy2`8$6zbLXy-Q8G)SM5TC1mJ%halq{)I>WF>!Iq^ZWavQJNx4M%z z5-X{7yJ~iKe{46q7th&u$6K!$>T|koaJQYe-=q_!YH)AAU^kREy<@i;>aHsbm2@Z; z+vxVp*DOx$wv-uGZEByvow{-_|G}PYYU=FrTTF;8W|t~(2UfSZ6La?c%`H8v z{0@PRCFUyih{-JiReAX(yUu7@@`YZ%htRX!>`>deg;2QnT($9dUHDq2n=c zD{)lrwafOtwQXoo$|M-p8Cz@M?}GFJJFTU9DdB)nD z50B=z<%SBmEt%}-7IwEIGn%_UH?A`HWB41xZqIx6%6q$$HwEKxg6@W!RoWYFVO-;C zp=wl854ZLttKdQpwO1-wvWiyv0}Sk^7yE%_SYbHSYxcRXy=NDSy~(Pe2P=9PIQ~lv zyrj3bFL;UJzvPSIOIX2@HPp#wVCcHoM$Zflah%O#9CBKlZkVv7-@WCk z{YtSv>G%swEa<l(6Eu))2paXXMNNK1ST-0gLvjX950qi* zPz@$)f<`PGtgTCJmfOb13S+C)OBc&qKd?s|Q^yY)D&fRqv9qKm6y~!+G|t2``fILtcYG=RhWlWD`f9BoqI_mWet7F>wyohrq#(jO#$wf;wY8)`3Ue$aAx*BVS`mf@?;j*R4TnS5b*WF&mZANWvqTXysC z$Z+8Scf(+MX_Lwna--SNf}WKf&F{>O=QG*OV>=+^-Ccv}w&J9aO*%&U2&O}mqIqpl z3SABkscP>jIdZ?CgF-4tS-PF3_u|z1Me3l4s|6|oe2FP&yPbQbt3eGHB5b8$YuWC zSi8f_lU zf5q^1CLB30%tzeLucr4G=OZjSB=ZrL1hC9USQ6O9M}i)r;9^a9SUwV|hbXvMSp$}j zBP=Xo@+XXicg2#X@?TMuyD7LxSO%5{DZEHnVk{-HdVMUOh>_o+@FHOiSoTnO2eSk$ z-=**(VF`);9tBj2CTqa*eF`rUmVo646kgRV0m~05yhyYmdx;)~A>Kuj@UiTr)WdMZ zyGT3{SbjvZ=TjP}{FsWs;$8EWBPYoG83nhz1sf+wF7!VV3+J7nhTDP?a3*NuJ^nXi zVK0voO@(kQ56+{MnhN2thN+KHc*|Qw-G|XG>1&?paGR#HkK}<64y%Ckcm#(fhVukj zeg4@z5fh(`;IIZbPepK80-WO05gpb*=b4a>$r9ikjNq^YIL}6KSOT2qA~@QRLqvx| zIF=9R5Ty=>a4hQ5H#+D%uixnr^EyH21p=J?g1?!hBaf2#cKQ0((vwElGw&EqPIdWNsr4hO1%|wVGUf$5f_#)c{;+vlE(5fLUS_Y!ZNtL z8gXHXxtt=a-9MElV&v-)7uLY#7ZDehz~zky3roQA%MeS^WDQ(?6>(t+Tz(yKVF_H` zjJRk+P7|F8xg>oqrzv$N3IGDG;^n1{vFfMu5UBsQh-tHQ4uzZ(1&loOkr;rjmr D^tM)| diff --git a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml index a67cb66b0..8e1c70998 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml +++ b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml @@ -12,7 +12,7 @@ sources: module: saas-starter service: accounts endpoint: connect - contract-digest: sha256:d0d8f6519a5a1eb4394265f3094faa19fee5a24afb3feeff4b33ea4ad5b07e5c + contract-digest: sha256:c9df546fead8be738783f6fba95b7a35c8037576c2e6362077f7e44406ca3e32 services: - APIKeyService - AccessibleScopeService diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/work_contexts_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/work_contexts_pb.ts index a653c0af7..becb23b62 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/work_contexts_pb.ts +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/work_contexts_pb.ts @@ -728,14 +728,16 @@ export const WorkContextService: GenService<{ /** * AdmitWorkloadOperation answers whether a capability presented with no * bearer at the declared module route /v1//* is a module operation - * context (StartModuleOperationTask) for that alias, and admits nothing else. + * context (StartModuleOperationTask) for that alias, and admits nothing else; + * and whether the route's target is installed in the capability's tenant. * The auth-gateway's bearer-less arm is its caller. Unlike the revision * checks above, the signed capability itself crosses this boundary: the * answer depends on its owner, owner kind and chain, which only the issuer - * reads. It inspects (signature under this issuer, window, audience) and - * reads its own claims; it never verifies, so nothing is consumed and the - * callee still verifies the capability at full strength. Internal-tier: the - * tenant it answers about is the one the capability names. + * reads. It reads the capability back (core's Authority.ReadBack: signature + * under this issuer, window, audience, revision, seal, epochs) and never + * verifies it as presented, so nothing is consumed and the callee still + * verifies at full strength. Internal-tier: the tenant it answers about is + * the one the capability names. * * @generated from rpc saas.accounts.v1.WorkContextService.AdmitWorkloadOperation */ diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/workload_operation_admission_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/workload_operation_admission_pb.ts index 3d3dbf6a3..8bdd5b995 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/workload_operation_admission_pb.ts +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/workload_operation_admission_pb.ts @@ -11,15 +11,15 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/workload_operation_admission.proto. */ export const file_saas_accounts_v1_workload_operation_admission: GenFile = /*@__PURE__*/ - fileDesc("CjNzYWFzL2FjY291bnRzL3YxL3dvcmtsb2FkX29wZXJhdGlvbl9hZG1pc3Npb24ucHJvdG8SEHNhYXMuYWNjb3VudHMudjEiXQodQWRtaXRXb3JrbG9hZE9wZXJhdGlvblJlcXVlc3QSIQoMd29ya19jb250ZXh0GAEgASgJQgu6SAhyBhABGICAAhIZCgVhbGlhcxgCIAEoCUIKukgHcgUQARiABCKsAQoeQWRtaXRXb3JrbG9hZE9wZXJhdGlvblJlc3BvbnNlEhAKCGFkbWl0dGVkGAEgASgIEjsKB3JlZnVzYWwYAiABKA4yKi5zYWFzLmFjY291bnRzLnYxLldvcmtsb2FkT3BlcmF0aW9uUmVmdXNhbBIRCgl0ZW5hbnRfaWQYAyABKAkSFwoPaW5zdGFsbGF0aW9uX2lkGAQgASgJEg8KB3Rhc2tfaWQYBSABKAkqvAIKGFdvcmtsb2FkT3BlcmF0aW9uUmVmdXNhbBIqCiZXT1JLTE9BRF9PUEVSQVRJT05fUkVGVVNBTF9VTlNQRUNJRklFRBAAEiwKKFdPUktMT0FEX09QRVJBVElPTl9SRUZVU0FMX05PVF9JTlNQRUNURUQQARIwCixXT1JLTE9BRF9PUEVSQVRJT05fUkVGVVNBTF9PV05FUl9OT1RfU0VSVklDRRACEjQKMFdPUktMT0FEX09QRVJBVElPTl9SRUZVU0FMX05PVF9PUEVSQVRJT05fQ09OVEVYVBADEi4KKldPUktMT0FEX09QRVJBVElPTl9SRUZVU0FMX09XTkVSX0lTX1RBUkdFVBAEEi4KKldPUktMT0FEX09QRVJBVElPTl9SRUZVU0FMX1NFQUxfTk9UX09XTkVSUxAFYgZwcm90bzM", [file_buf_validate_validate]); + fileDesc("CjNzYWFzL2FjY291bnRzL3YxL3dvcmtsb2FkX29wZXJhdGlvbl9hZG1pc3Npb24ucHJvdG8SEHNhYXMuYWNjb3VudHMudjEiegodQWRtaXRXb3JrbG9hZE9wZXJhdGlvblJlcXVlc3QSIQoMd29ya19jb250ZXh0GAEgASgJQgu6SAhyBhABGICAAhIZCgVhbGlhcxgCIAEoCUIKukgHcgUQARiABBIbCgl0YXJnZXRfaWQYAyABKAlCCLpIBXIDsAEBIqwBCh5BZG1pdFdvcmtsb2FkT3BlcmF0aW9uUmVzcG9uc2USEAoIYWRtaXR0ZWQYASABKAgSOwoHcmVmdXNhbBgCIAEoDjIqLnNhYXMuYWNjb3VudHMudjEuV29ya2xvYWRPcGVyYXRpb25SZWZ1c2FsEhEKCXRlbmFudF9pZBgDIAEoCRIXCg9pbnN0YWxsYXRpb25faWQYBCABKAkSDwoHdGFza19pZBgFIAEoCSrMAwoYV29ya2xvYWRPcGVyYXRpb25SZWZ1c2FsEioKJldPUktMT0FEX09QRVJBVElPTl9SRUZVU0FMX1VOU1BFQ0lGSUVEEAASLAooV09SS0xPQURfT1BFUkFUSU9OX1JFRlVTQUxfTk9UX0lOU1BFQ1RFRBABEjAKLFdPUktMT0FEX09QRVJBVElPTl9SRUZVU0FMX09XTkVSX05PVF9TRVJWSUNFEAISNAowV09SS0xPQURfT1BFUkFUSU9OX1JFRlVTQUxfTk9UX09QRVJBVElPTl9DT05URVhUEAMSLgoqV09SS0xPQURfT1BFUkFUSU9OX1JFRlVTQUxfT1dORVJfSVNfVEFSR0VUEAQSLgoqV09SS0xPQURfT1BFUkFUSU9OX1JFRlVTQUxfU0VBTF9OT1RfT1dORVJTEAUSMwovV09SS0xPQURfT1BFUkFUSU9OX1JFRlVTQUxfVEFSR0VUX05PVF9JTlNUQUxMRUQQBhItCilXT1JLTE9BRF9PUEVSQVRJT05fUkVGVVNBTF9TRUFMX09OX1RBUkdFVBAHEioKJldPUktMT0FEX09QRVJBVElPTl9SRUZVU0FMX05PVF9DVVJSRU5UEAhiBnByb3RvMw", [file_buf_validate_validate]); /** * @generated from message saas.accounts.v1.AdmitWorkloadOperationRequest */ export type AdmitWorkloadOperationRequest = Message<"saas.accounts.v1.AdmitWorkloadOperationRequest"> & { /** - * The capability as presented, encoded. It is inspected and read, never - * verified: nothing it carries is consumed. + * The capability as presented, encoded. It is read back by its issuer, never + * verified as presented: nothing it carries is consumed. * * @generated from field: string work_context = 1; */ @@ -32,6 +32,14 @@ export type AdmitWorkloadOperationRequest = Message<"saas.accounts.v1.AdmitWorkl * @generated from field: string alias = 2; */ alias: string; + + /** + * The immutable solution target the route's alias resolved to, from the + * gateway's own routing. The tenant must have an ACTIVE installation of it. + * + * @generated from field: string target_id = 3; + */ + targetId: string; }; /** @@ -137,6 +145,31 @@ export enum WorkloadOperationRefusal { * @generated from enum value: WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS = 5; */ SEAL_NOT_OWNERS = 5, + + /** + * The tenant has no ACTIVE installation of the route's target. The gateway + * answers this one 403, as it answers a person whose organization never + * installed the module. + * + * @generated from enum value: WORKLOAD_OPERATION_REFUSAL_TARGET_NOT_INSTALLED = 6; + */ + TARGET_NOT_INSTALLED = 6, + + /** + * The seal is one of the target's own installations: the caller would be the + * module it addresses. + * + * @generated from enum value: WORKLOAD_OPERATION_REFUSAL_SEAL_ON_TARGET = 7; + */ + SEAL_ON_TARGET = 7, + + /** + * The issuer's live state has moved past the capability: its authorization + * revision is superseded, or an epoch it carries has advanced. + * + * @generated from enum value: WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT = 8; + */ + NOT_CURRENT = 8, } /** diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/work_contexts_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/work_contexts_pb.ts index 0097dfc2e..a43b829f0 100644 --- a/module/services/frontend/code/src/gen/saas/accounts/v1/work_contexts_pb.ts +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/work_contexts_pb.ts @@ -728,14 +728,16 @@ export const WorkContextService: GenService<{ /** * AdmitWorkloadOperation answers whether a capability presented with no * bearer at the declared module route /v1//* is a module operation - * context (StartModuleOperationTask) for that alias, and admits nothing else. + * context (StartModuleOperationTask) for that alias, and admits nothing else; + * and whether the route's target is installed in the capability's tenant. * The auth-gateway's bearer-less arm is its caller. Unlike the revision * checks above, the signed capability itself crosses this boundary: the * answer depends on its owner, owner kind and chain, which only the issuer - * reads. It inspects (signature under this issuer, window, audience) and - * reads its own claims; it never verifies, so nothing is consumed and the - * callee still verifies the capability at full strength. Internal-tier: the - * tenant it answers about is the one the capability names. + * reads. It reads the capability back (core's Authority.ReadBack: signature + * under this issuer, window, audience, revision, seal, epochs) and never + * verifies it as presented, so nothing is consumed and the callee still + * verifies at full strength. Internal-tier: the tenant it answers about is + * the one the capability names. * * @generated from rpc saas.accounts.v1.WorkContextService.AdmitWorkloadOperation */ diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/workload_operation_admission_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/workload_operation_admission_pb.ts index 61a7af504..c7ad596cf 100644 --- a/module/services/frontend/code/src/gen/saas/accounts/v1/workload_operation_admission_pb.ts +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/workload_operation_admission_pb.ts @@ -11,15 +11,15 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/workload_operation_admission.proto. */ export const file_saas_accounts_v1_workload_operation_admission: GenFile = /*@__PURE__*/ - fileDesc("CjNzYWFzL2FjY291bnRzL3YxL3dvcmtsb2FkX29wZXJhdGlvbl9hZG1pc3Npb24ucHJvdG8SEHNhYXMuYWNjb3VudHMudjEiXQodQWRtaXRXb3JrbG9hZE9wZXJhdGlvblJlcXVlc3QSIQoMd29ya19jb250ZXh0GAEgASgJQgu6SAhyBhABGICAAhIZCgVhbGlhcxgCIAEoCUIKukgHcgUQARiABCKsAQoeQWRtaXRXb3JrbG9hZE9wZXJhdGlvblJlc3BvbnNlEhAKCGFkbWl0dGVkGAEgASgIEjsKB3JlZnVzYWwYAiABKA4yKi5zYWFzLmFjY291bnRzLnYxLldvcmtsb2FkT3BlcmF0aW9uUmVmdXNhbBIRCgl0ZW5hbnRfaWQYAyABKAkSFwoPaW5zdGFsbGF0aW9uX2lkGAQgASgJEg8KB3Rhc2tfaWQYBSABKAkqvAIKGFdvcmtsb2FkT3BlcmF0aW9uUmVmdXNhbBIqCiZXT1JLTE9BRF9PUEVSQVRJT05fUkVGVVNBTF9VTlNQRUNJRklFRBAAEiwKKFdPUktMT0FEX09QRVJBVElPTl9SRUZVU0FMX05PVF9JTlNQRUNURUQQARIwCixXT1JLTE9BRF9PUEVSQVRJT05fUkVGVVNBTF9PV05FUl9OT1RfU0VSVklDRRACEjQKMFdPUktMT0FEX09QRVJBVElPTl9SRUZVU0FMX05PVF9PUEVSQVRJT05fQ09OVEVYVBADEi4KKldPUktMT0FEX09QRVJBVElPTl9SRUZVU0FMX09XTkVSX0lTX1RBUkdFVBAEEi4KKldPUktMT0FEX09QRVJBVElPTl9SRUZVU0FMX1NFQUxfTk9UX09XTkVSUxAFQscBChRjb20uc2Fhcy5hY2NvdW50cy52MUIfV29ya2xvYWRPcGVyYXRpb25BZG1pc3Npb25Qcm90b1ABWixhY2NvdW50cy9wa2cvZ2VuL3NhYXMvYWNjb3VudHMvdjE7YWNjb3VudHN2MaICA1NBWKoCEFNhYXMuQWNjb3VudHMuVjHKAhBTYWFzXEFjY291bnRzXFYx4gIcU2Fhc1xBY2NvdW50c1xWMVxHUEJNZXRhZGF0YeoCElNhYXM6OkFjY291bnRzOjpWMWIGcHJvdG8z", [file_buf_validate_validate]); + fileDesc("CjNzYWFzL2FjY291bnRzL3YxL3dvcmtsb2FkX29wZXJhdGlvbl9hZG1pc3Npb24ucHJvdG8SEHNhYXMuYWNjb3VudHMudjEiegodQWRtaXRXb3JrbG9hZE9wZXJhdGlvblJlcXVlc3QSIQoMd29ya19jb250ZXh0GAEgASgJQgu6SAhyBhABGICAAhIZCgVhbGlhcxgCIAEoCUIKukgHcgUQARiABBIbCgl0YXJnZXRfaWQYAyABKAlCCLpIBXIDsAEBIqwBCh5BZG1pdFdvcmtsb2FkT3BlcmF0aW9uUmVzcG9uc2USEAoIYWRtaXR0ZWQYASABKAgSOwoHcmVmdXNhbBgCIAEoDjIqLnNhYXMuYWNjb3VudHMudjEuV29ya2xvYWRPcGVyYXRpb25SZWZ1c2FsEhEKCXRlbmFudF9pZBgDIAEoCRIXCg9pbnN0YWxsYXRpb25faWQYBCABKAkSDwoHdGFza19pZBgFIAEoCSrMAwoYV29ya2xvYWRPcGVyYXRpb25SZWZ1c2FsEioKJldPUktMT0FEX09QRVJBVElPTl9SRUZVU0FMX1VOU1BFQ0lGSUVEEAASLAooV09SS0xPQURfT1BFUkFUSU9OX1JFRlVTQUxfTk9UX0lOU1BFQ1RFRBABEjAKLFdPUktMT0FEX09QRVJBVElPTl9SRUZVU0FMX09XTkVSX05PVF9TRVJWSUNFEAISNAowV09SS0xPQURfT1BFUkFUSU9OX1JFRlVTQUxfTk9UX09QRVJBVElPTl9DT05URVhUEAMSLgoqV09SS0xPQURfT1BFUkFUSU9OX1JFRlVTQUxfT1dORVJfSVNfVEFSR0VUEAQSLgoqV09SS0xPQURfT1BFUkFUSU9OX1JFRlVTQUxfU0VBTF9OT1RfT1dORVJTEAUSMwovV09SS0xPQURfT1BFUkFUSU9OX1JFRlVTQUxfVEFSR0VUX05PVF9JTlNUQUxMRUQQBhItCilXT1JLTE9BRF9PUEVSQVRJT05fUkVGVVNBTF9TRUFMX09OX1RBUkdFVBAHEioKJldPUktMT0FEX09QRVJBVElPTl9SRUZVU0FMX05PVF9DVVJSRU5UEAhCxwEKFGNvbS5zYWFzLmFjY291bnRzLnYxQh9Xb3JrbG9hZE9wZXJhdGlvbkFkbWlzc2lvblByb3RvUAFaLGFjY291bnRzL3BrZy9nZW4vc2Fhcy9hY2NvdW50cy92MTthY2NvdW50c3YxogIDU0FYqgIQU2Fhcy5BY2NvdW50cy5WMcoCEFNhYXNcQWNjb3VudHNcVjHiAhxTYWFzXEFjY291bnRzXFYxXEdQQk1ldGFkYXRh6gISU2Fhczo6QWNjb3VudHM6OlYxYgZwcm90bzM", [file_buf_validate_validate]); /** * @generated from message saas.accounts.v1.AdmitWorkloadOperationRequest */ export type AdmitWorkloadOperationRequest = Message<"saas.accounts.v1.AdmitWorkloadOperationRequest"> & { /** - * The capability as presented, encoded. It is inspected and read, never - * verified: nothing it carries is consumed. + * The capability as presented, encoded. It is read back by its issuer, never + * verified as presented: nothing it carries is consumed. * * @generated from field: string work_context = 1; */ @@ -32,6 +32,14 @@ export type AdmitWorkloadOperationRequest = Message<"saas.accounts.v1.AdmitWorkl * @generated from field: string alias = 2; */ alias: string; + + /** + * The immutable solution target the route's alias resolved to, from the + * gateway's own routing. The tenant must have an ACTIVE installation of it. + * + * @generated from field: string target_id = 3; + */ + targetId: string; }; /** @@ -137,6 +145,31 @@ export enum WorkloadOperationRefusal { * @generated from enum value: WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS = 5; */ SEAL_NOT_OWNERS = 5, + + /** + * The tenant has no ACTIVE installation of the route's target. The gateway + * answers this one 403, as it answers a person whose organization never + * installed the module. + * + * @generated from enum value: WORKLOAD_OPERATION_REFUSAL_TARGET_NOT_INSTALLED = 6; + */ + TARGET_NOT_INSTALLED = 6, + + /** + * The seal is one of the target's own installations: the caller would be the + * module it addresses. + * + * @generated from enum value: WORKLOAD_OPERATION_REFUSAL_SEAL_ON_TARGET = 7; + */ + SEAL_ON_TARGET = 7, + + /** + * The issuer's live state has moved past the capability: its authorization + * revision is superseded, or an epoch it carries has advanced. + * + * @generated from enum value: WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT = 8; + */ + NOT_CURRENT = 8, } /** From 1b4c1dfab91229e769e04f75839846c2d4789786 Mon Sep 17 00:00:00 2001 From: saurabhmaheshwari-obin <242053479+saurabhmaheshwari-obin@users.noreply.github.com> Date: Sat, 24 Oct 2026 17:08:40 -0400 Subject: [PATCH 14/17] fix(auth-gateway): one issuer call per bearer-less request, no stamped installation header Review follow-up on the bearer-less module arm. - One authority call: the arm sends the route's target_id with the capability and alias to AdmitWorkloadOperation, which now answers the installation question too. TARGET_NOT_INSTALLED (and a route that resolves no target, decided before any call) is 403; every other refusal is 401. The ListInstallations loop, its client, its page bounds and the gateway's installations field are gone from the arm. - An InvalidArgument answer is 401 "authentication required" (the request's shape is the capability's or the alias's), not 503. - A Connection option may not name the capability header: the arm answers 401 "invalid work context" before anything is stripped or asked. - The arm stamps no x-codefly-installation-id. No consumer of a module route reads it (the only reader in this repository is accounts' own interceptor, for requests carrying the gateway token on accounts' routes; neither sdk-go nor saas-sdk-go reads it), a caller's value is stripped as before, and core says a hop stamps nothing a callee would read in place of the token. The header's comment and the docs say so. Tests: the refusal table and harness lose the installation fakes; the issuer fake records target_id; every WorkloadOperationRefusal value is mapped (403 for TARGET_NOT_INSTALLED, 401 otherwise); the admit test asserts one call carrying the capability, alias and target and no installation header upstream. Docs: the gateway AGENTS.md, GATEWAY_ROUTES.md and WORK_CONTEXTS.md describe the single call, the Connection rule and the absent header. Co-Authored-By: Claude Opus 5.5 --- module/GATEWAY_ROUTES.md | 25 +- module/WORK_CONTEXTS.md | 8 +- module/services/auth-gateway/AGENTS.md | 31 +- .../auth-gateway/code/attack_battery_test.go | 17 +- module/services/auth-gateway/code/gateway.go | 16 +- .../code/gateway_module_confinement_test.go | 9 +- .../code/gateway_module_routes_test.go | 297 +++++------------- .../code/gateway_operation_context_test.go | 5 +- .../code/gateway_workload_operation.go | 197 ++++-------- module/services/auth-gateway/code/main.go | 3 - 10 files changed, 179 insertions(+), 429 deletions(-) diff --git a/module/GATEWAY_ROUTES.md b/module/GATEWAY_ROUTES.md index b7b897294..00564b930 100644 --- a/module/GATEWAY_ROUTES.md +++ b/module/GATEWAY_ROUTES.md @@ -129,18 +129,19 @@ there may name the alias or `module-capabilities`; anything else is 401 non-empty signed tenant and seal (else 401 `invalid work context`); every identity header stripped, `Authorization`, `Proxy-Authorization` and `X-Forwarded-Client-Cert` dropped; the budget keyed - `workload-operation:`, failing closed; then accounts' - `AdmitWorkloadOperation` must admit the capability as a module operation - context for the alias (a refusal is 401 `authentication required`, the answer - a person's capability has without its bearer), and accounts' - `ListInstallations` for that tenant (ACTIVE, uncached) must show an - installation of the target (else 403) that is not the seal (else 401). 503 - when either cannot answer in time or is not wired, or the admission names - another tenant or seal. Admitted requests carry no bearer and no identity - header, and `x-codefly-installation-id` is set to the admitted seal, which - names the CALLER's installation, not consent on the target's side. An - upstream failure is a static 502. Every admit and refusal is logged with - alias, tenant, task, seal and presenter, never the token. + `workload-operation:`, failing closed; then one call to accounts' + `AdmitWorkloadOperation` with the capability, the alias and the route's + target: it must admit the capability as a module operation context for the + alias with the target installed (ACTIVE) in its tenant and the seal none of + the target's rows. Not installed is 403; any other refusal is 401 + `authentication required`, the answer a person's capability has without its + bearer. 503 when it cannot answer in time or is not wired, or names another + tenant or seal. A `Connection` option naming the capability is 401 `invalid + work context`. Admitted requests carry the capability and nothing the + gateway asserts: no bearer, no identity header, no + `x-codefly-installation-id`. An upstream failure is a static 502. Every + admit and refusal is logged with alias, tenant, task, seal and presenter, + never the token. Elsewhere the same request is a bearer-less request: 401 on `rest`, 401 with a `module-capabilities` capability, 401 for a capability sealed to the target's own diff --git a/module/WORK_CONTEXTS.md b/module/WORK_CONTEXTS.md index f52193d1a..45886cc01 100644 --- a/module/WORK_CONTEXTS.md +++ b/module/WORK_CONTEXTS.md @@ -674,10 +674,10 @@ meters on the tenant. It then asks the issuer: accounts' `StartModuleOperationTask` signs, owned by a declared module principal other than the alias's and sealed to that principal's active installation, and refuses every person-owned kind, including those sealed elsewhere (see the seal -rule above). It inspects and reads its own claims, and consumes nothing. The -target must also be installed in the signed tenant, and the seal must not be -one of its rows. The arm forwards the capability as presented and stamps only -`x-codefly-installation-id` with the admitted seal. The callee verifies the +rule above). It reads the capability back against its own live state and +consumes nothing. In the same call it requires the route's target installed in +the signed tenant, with the seal none of its rows. The arm forwards the +capability as presented and stamps nothing beside it. The callee verifies the capability at full strength, as for any other carrier. The route's rules are in [GATEWAY_ROUTES.md](./GATEWAY_ROUTES.md#module-rest-routes). diff --git a/module/services/auth-gateway/AGENTS.md b/module/services/auth-gateway/AGENTS.md index e038f7fe1..a409ca3bf 100644 --- a/module/services/auth-gateway/AGENTS.md +++ b/module/services/auth-gateway/AGENTS.md @@ -106,22 +106,23 @@ workload endpoint with no `Authorization` and no `Cookie`, presenting a capability addressed to the alias, takes the bearer-less arm (`gateway_workload_operation.go`): a composed workload's headless operation context, minted at `/modules/_operation-context`. The arm reads no principal and -decides nothing from the seal. It requires exactly one capability header (401 -`invalid work context` otherwise), strips identity (and +decides nothing from the seal. It requires exactly one capability header and +no `Connection` option naming it (401 `invalid work context` otherwise), strips +identity (and `X-Forwarded-Client-Cert`) before its limiter, meters on -`workload-operation:` (fail closed), and only then asks accounts two -things, per request and uncached: `WorkContextService/AdmitWorkloadOperation`, -whether the capability is a module operation context for this alias (any -refusal is 401 `authentication required`, logged with the issuer's reason; an -error, a call past its 2 second bound, an answer naming another tenant or seal, -or no client wired is 503), and `ListInstallations`, whether the signed tenant -has an ACTIVE installation of the target (else 403) whose rows do not include -the seal (else 401). It forwards no bearer, stamps only -`x-codefly-installation-id` (the seal accounts admitted, so the CALLER's -installation on this arm and the target's admitting installation on the person -path; equal to the presented seal whenever a capability is presented), answers -an upstream failure with a static 502, and logs every admit and refusal with -alias, tenant, task, seal and presenter. It is the gateway's half of a +`workload-operation:` (fail closed), and only then makes one call, per +request and uncached: `WorkContextService/AdmitWorkloadOperation` with the +capability, the alias and the route's target, which answers whether the +capability is a module operation context for this alias and whether the target +is installed in its tenant. Not installed (or no target resolved) is 403; any +other refusal, or the request refused as invalid, is 401 `authentication +required`, logged with the issuer's reason; an error, a call past its 2 second +bound, an answer naming another tenant or seal, or no client wired is 503. It +forwards no bearer and stamps nothing: no identity header and no +`x-codefly-installation-id` (a caller's is stripped), because the callee +verifies the capability and no module-route consumer reads that header. It +answers an upstream failure with a static 502, and logs every admit and refusal +with alias, tenant, task, seal and presenter. It is the gateway's half of a solution's release-declared headless operation; a module calling a peer keeps its in-mesh seam instead. diff --git a/module/services/auth-gateway/code/attack_battery_test.go b/module/services/auth-gateway/code/attack_battery_test.go index 777824115..04fa932ba 100644 --- a/module/services/auth-gateway/code/attack_battery_test.go +++ b/module/services/auth-gateway/code/attack_battery_test.go @@ -62,7 +62,7 @@ func TestAttack_TheBearerLessArmStripsGrpcMetadataIdentityHeaders(t *testing.T) // Authorization header at all. A Basic credential is not "no bearer"; it takes // the person path, which accepts no such scheme. func TestAttack_AnyAuthorizationSchemeLeavesTheArm(t *testing.T) { - gw, module, installations, kid, wcPriv := operationHarness(t) + gw, module, admission, kid, wcPriv := operationHarness(t) req := onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) req.Header.Set("Authorization", "Basic dXNlcjpwYXNz") @@ -71,8 +71,7 @@ func TestAttack_AnyAuthorizationSchemeLeavesTheArm(t *testing.T) { require.Equal(t, http.StatusUnauthorized, w.Code) require.Zero(t, module.calls) - require.Empty(t, admissionOf(t, gw).calls, "the arm was not selected") - require.Empty(t, installations.calls) + require.Empty(t, admission.calls, "the arm was not selected") } // TestAttack_TheBearerLessArmForwardsNoProxyCredential: a Proxy-Authorization @@ -96,7 +95,7 @@ func TestAttack_TheBearerLessArmForwardsNoProxyCredential(t *testing.T) { // namespace, read after the strip: a caller's X-Org-Id and its source address // open no new bucket. Exhausting it refuses before accounts is asked anything. func TestAttack_TheBearerLessArmIsMeteredOnItsSignedTenant(t *testing.T) { - gw, module, installations, kid, wcPriv := operationHarness(t) + gw, module, admission, kid, wcPriv := operationHarness(t) backend := &memoryBackend{} gw.rateLimiter = &RateLimiter{backend: backend, limit: 2, burst: 1, stop: make(chan struct{}), proxies: newProxyTrust("")} capability := operationCapability(t, kid, wcPriv, "documents", publisherSeal) @@ -115,8 +114,7 @@ func TestAttack_TheBearerLessArmIsMeteredOnItsSignedTenant(t *testing.T) { } require.Equal(t, http.StatusTooManyRequests, serve(3)) require.Equal(t, 3, module.calls) - require.Len(t, admissionOf(t, gw).calls, 3, "a limited request never reaches the issuer") - require.Len(t, installations.calls, 3, "nor the installation read") + require.Len(t, admission.calls, 3, "a limited request never reaches the issuer") var keys []string backend.windows.Range(func(key, _ any) bool { @@ -130,9 +128,9 @@ func TestAttack_TheBearerLessArmIsMeteredOnItsSignedTenant(t *testing.T) { // TestAttack_TheBearerLessArmFailsClosedWithoutItsLimiter: the arm's budget is a // security budget. A limiter backend that cannot answer refuses the request; it -// does not wave it through to the issuer or the installation read. +// does not wave it through to the issuer. func TestAttack_TheBearerLessArmFailsClosedWithoutItsLimiter(t *testing.T) { - gw, module, installations, kid, wcPriv := operationHarness(t) + gw, module, admission, kid, wcPriv := operationHarness(t) gw.rateLimiter = &RateLimiter{backend: failingRateLimitBackend{}, limit: 100, burst: 20, stop: make(chan struct{}), proxies: newProxyTrust("")} w := httptest.NewRecorder() @@ -141,6 +139,5 @@ func TestAttack_TheBearerLessArmFailsClosedWithoutItsLimiter(t *testing.T) { require.Equal(t, http.StatusServiceUnavailable, w.Code) require.Contains(t, w.Body.String(), "rate limit service unavailable") require.Zero(t, module.calls) - require.Empty(t, admissionOf(t, gw).calls) - require.Empty(t, installations.calls) + require.Empty(t, admission.calls) } diff --git a/module/services/auth-gateway/code/gateway.go b/module/services/auth-gateway/code/gateway.go index d2833b324..69e47e656 100644 --- a/module/services/auth-gateway/code/gateway.go +++ b/module/services/auth-gateway/code/gateway.go @@ -62,10 +62,6 @@ type Gateway struct { // solution a viewer is currently using. solutionEntitlements solutionEntitlementClient workContext *workContextVerifier - // installations answers an organization's ACTIVE installations for the - // bearer-less module arm (gateway_workload_operation.go). Set after - // construction; nil answers 503 there and never admits. - installations installationListingClient // workloadAdmission asks accounts whether a capability on the bearer-less // module arm is a module operation context. Set after construction; nil // answers 503 there and never admits. @@ -922,14 +918,10 @@ const resourceConfinementHeader = "x-codefly-resource" // entitlement authority and an empty value here would be a claim this process // cannot make. // -// WHAT IT MEANS differs by arm, and one statement holds on both: when a -// capability is presented with the request, it equals that capability's seal. -// On a person's path it is -// the TARGET's admitting installation, and a person's capability for that target -// is sealed to it. On a declared module route's bearer-less arm it is the seal of -// the headless operation context, which names the CALLER's own installation. -// There it is not consent on the target's side, and an upstream must not read it -// as such; the capability itself is what the upstream verifies. +// A declared module route's bearer-less arm stamps NO value: a caller's is +// stripped like any other, and the upstream verifies the operation context +// itself. That arm admits on the caller's installation, which is not consent on +// the target's side, so no header stating it could be read correctly here. const installationConfinementHeader = "x-codefly-installation-id" // solutionIdentityHeader and solutionPublisherHeader are the headers accounts diff --git a/module/services/auth-gateway/code/gateway_module_confinement_test.go b/module/services/auth-gateway/code/gateway_module_confinement_test.go index 698ce23bf..0c2f8768f 100644 --- a/module/services/auth-gateway/code/gateway_module_confinement_test.go +++ b/module/services/auth-gateway/code/gateway_module_confinement_test.go @@ -268,8 +268,8 @@ func TestModuleRoute_TheSessionIsTheBindingAndTheOwnerIsNotRead(t *testing.T) { } // ON THE BEARER-LESS ARM a caller can assert no identity and no installation: -// everything it sent under a gateway-owned name is gone, and the installation -// header is the seal the capability is signed with. +// everything it sent under a gateway-owned name is gone, and the arm stamps no +// installation of its own in its place. func TestModuleRoute_TheBearerLessArmTakesNoIdentityFromTheCaller(t *testing.T) { gw, module, _, kid, wcPriv := operationHarness(t) req := onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) @@ -283,9 +283,7 @@ func TestModuleRoute_TheBearerLessArmTakesNoIdentityFromTheCaller(t *testing.T) gw.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code, w.Body.String()) - require.Equal(t, publisherSeal, module.lastHeaders.Get(installationConfinementHeader), - "the installation is the signed seal, not the one the caller named") - for _, header := range []string{resourceConfinementHeader, "X-Org-Id", "X-User-Id", workloadPresenterHeader} { + for _, header := range []string{installationConfinementHeader, resourceConfinementHeader, "X-Org-Id", "X-User-Id", workloadPresenterHeader} { require.Empty(t, module.lastHeaders.Values(header), "%s is the gateway's, never the caller's", header) } } @@ -295,7 +293,6 @@ func TestModuleRoute_TheBearerLessArmTakesNoIdentityFromTheCaller(t *testing.T) // person, and the viewer admission names the target's installation. func TestModuleRoute_ABearerOnTheWorkloadListenerTakesThePersonPath(t *testing.T) { gw, module, _, caller, kid, wcPriv, _ := confinedModuleHarness(t) - gw.installations = &installationsFake{err: errors.New("the arm must not be consulted")} gw.workloadAdmission = &admissionFake{err: errors.New("the arm must not be consulted")} capability := mintViewerCapability(t, kid, wcPriv, "documents", caller.subject, caller.org, caller.session) diff --git a/module/services/auth-gateway/code/gateway_module_routes_test.go b/module/services/auth-gateway/code/gateway_module_routes_test.go index df736226c..9572e4763 100644 --- a/module/services/auth-gateway/code/gateway_module_routes_test.go +++ b/module/services/auth-gateway/code/gateway_module_routes_test.go @@ -5,7 +5,6 @@ import ( "context" "crypto/ed25519" "errors" - "fmt" "log" "net" "net/http" @@ -20,8 +19,10 @@ import ( carrier "github.com/codefly-dev/sdk-go/workcontext" "github.com/stretchr/testify/require" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/metadata" + grpcstatus "google.golang.org/grpc/status" ) // Routing a declared module (issue #952). @@ -333,45 +334,11 @@ const ( targetInstallation = "33333333-3333-4333-8333-333333333333" ) -// installationsFake answers ListInstallations from pages, page N answering the -// cursor "page-N" (page 0 the empty cursor), and records every request. -type installationsFake struct { - pages []*accountsv1.ListInstallationsResponse - err error - // endless answers every page with a further cursor. - endless bool - calls []*accountsv1.ListInstallationsRequest -} - -func (f *installationsFake) List( - _ context.Context, req *accountsv1.ListInstallationsRequest, -) (*accountsv1.ListInstallationsResponse, error) { - f.calls = append(f.calls, req) - if f.err != nil { - return nil, f.err - } - if f.endless { - return &accountsv1.ListInstallationsResponse{NextPageToken: "more"}, nil - } - for index, page := range f.pages { - if req.GetPageToken() == pageCursor(index) { - return page, nil - } - } - return &accountsv1.ListInstallationsResponse{}, nil -} - -func pageCursor(index int) string { - if index == 0 { - return "" - } - return fmt.Sprintf("page-%d", index) -} - // admissionFake stands in for accounts' AdmitWorkloadOperation. By default it // admits, echoing the presented capability's own tenant, seal and task the way // accounts answers them; refusal or err replace the answer, and answer, when -// set, is returned as it is. +// set, is returned as it is. Whether the target is installed is accounts' +// answer too, so a refusal of TARGET_NOT_INSTALLED is how a test says it is not. type admissionFake struct { refusal accountsv1.WorkloadOperationRefusal err error @@ -401,35 +368,9 @@ func (f *admissionFake) Admit( }, nil } -// activeInstallation is one ACTIVE row of operationTenant. -func activeInstallation(id, target string) *accountsv1.InstallationSummary { - return &accountsv1.InstallationSummary{Installation: &accountsv1.Installation{ - Id: id, OrgId: operationTenant, TargetId: target, - Status: accountsv1.InstallationStatus_INSTALLATION_STATUS_ACTIVE, - }} -} - -// moduleInstallation is the ACTIVE row of operationTenant for the module whose -// alias is name. -func moduleInstallation(id, name string) *accountsv1.InstallationSummary { - return activeInstallation(id, fakeSolutionTarget(name)) -} - -// tenantInstalled is a tenant that installed the target module (E) and the -// publishing workload (W). -func tenantInstalled() *installationsFake { - return &installationsFake{pages: []*accountsv1.ListInstallationsResponse{{ - Installations: []*accountsv1.InstallationSummary{ - moduleInstallation(targetInstallation, "documents"), - moduleInstallation(publisherSeal, "publisher"), - }, - }}} -} - // operationHarness is a gateway with the edge verifier, the module "documents" -// declared, a tenant that installed it and the publishing workload, and an -// issuer that admits. -func operationHarness(t *testing.T) (gw *Gateway, module *fakeUpstream, installations *installationsFake, kid string, wcPriv ed25519.PrivateKey) { +// declared, and an issuer that admits. +func operationHarness(t *testing.T) (gw *Gateway, module *fakeUpstream, admission *admissionFake, kid string, wcPriv ed25519.PrivateKey) { t.Helper() gw, _, _, _ = newGatewayHarness(t) pub, wcPriv := mustEd25519(t) @@ -437,10 +378,9 @@ func operationHarness(t *testing.T) (gw *Gateway, module *fakeUpstream, installa jwks := jwksServer(t, jwksDocument(map[string]ed25519.PublicKey{kid: pub}), nil) gw.workContext = newWorkContextVerifier(jwks.URL) module = declareModule(t, gw, "documents") - installations = tenantInstalled() - gw.installations = installations - gw.workloadAdmission = &admissionFake{} - return gw, module, installations, kid, wcPriv + admission = &admissionFake{} + gw.workloadAdmission = admission + return gw, module, admission, kid, wcPriv } // admissionOf is the issuer fake the harness wired. @@ -480,16 +420,15 @@ func captureLog(t *testing.T) *bytes.Buffer { // THE ARM ADMITS A HEADLESS OPERATION CONTEXT. No bearer, the workload endpoint, // a capability for this alias that accounts admits as a module operation -// context, sealed to the publisher's own installation (W), in a tenant that -// installed the target (E, not W). +// context, sealed to the publisher's own installation, for a target accounts +// says is installed in its tenant. // -// What the upstream holds is the capability and the one header stating the -// admitted seal, and nothing that names a person. ext_authz is not consulted: it -// denies every request without a bearer, so a 200 here is a request it never -// saw, and it stamps x-user-id and x-credential-kind on every allow, which are -// absent. +// What the upstream holds is the capability and nothing this gateway asserts: +// no person, and no installation header. ext_authz is not consulted: it denies +// every request without a bearer, so a 200 here is a request it never saw, and +// it stamps x-user-id and x-credential-kind on every allow, which are absent. func TestModuleRoute_AWorkloadOperationContextIsAdmittedWithoutABearer(t *testing.T) { - gw, module, installations, kid, wcPriv := operationHarness(t) + gw, module, admission, kid, wcPriv := operationHarness(t) logs := captureLog(t) capability := operationCapability(t, kid, wcPriv, "documents", publisherSeal) req := onWorkloadEndpoint(publishRequest(capability)) @@ -503,22 +442,17 @@ func TestModuleRoute_AWorkloadOperationContextIsAdmittedWithoutABearer(t *testin require.Equal(t, "/v1/documents/items:write", module.lastPath, "the path is forwarded unchanged") require.Equal(t, `{"bundle":"b"}`, module.lastBody) require.Equal(t, capability, module.lastHeaders.Get(carrier.HeaderName), "the capability is forwarded as presented") - require.Equal(t, []string{publisherSeal}, module.lastHeaders.Values(installationConfinementHeader), - "the one stamped header is the seal accounts admitted, once") for _, header := range []string{ "Authorization", "Cookie", "Proxy-Authorization", "X-User-Id", "X-Org-Id", "X-Session-Id", "X-Scopes", - "X-Credential-Kind", resourceConfinementHeader, "X-Codefly-Gateway-Token", "X-Forwarded-Client-Cert", + "X-Credential-Kind", resourceConfinementHeader, installationConfinementHeader, "X-Codefly-Gateway-Token", "X-Forwarded-Client-Cert", } { require.Empty(t, module.lastHeaders.Values(header), "%s must not reach the upstream on the bearer-less arm", header) } - admission := admissionOf(t, gw) - require.Len(t, admission.calls, 1) + require.Len(t, admission.calls, 1, "one authority call") require.Equal(t, capability, admission.calls[0].GetWorkContext(), "the issuer is shown the capability as presented") require.Equal(t, "documents", admission.calls[0].GetAlias(), "and the alias the route resolved") - require.Len(t, installations.calls, 1) - require.Equal(t, operationTenant, installations.calls[0].GetOrgId(), "the tenant is the capability's, signed") - require.Equal(t, accountsv1.InstallationStatus_INSTALLATION_STATUS_ACTIVE, installations.calls[0].GetStatus()) + require.Equal(t, fakeSolutionTarget("documents"), admission.calls[0].GetTargetId(), "and the target the route resolved") line := logs.String() require.Contains(t, line, "reason=workload_operation_admit") @@ -533,40 +467,41 @@ func TestModuleRoute_AWorkloadOperationContextIsAdmittedWithoutABearer(t *testin // what a source-operation context, an installation task or an installed // binding's child looks like to a hop: each is a person's. Before the arm asked // accounts it admitted all of them on the seal; every refusal accounts gives is -// now a 401, the answer a person's capability without its bearer has always had. +// now 401, the answer a person's capability without its bearer has always had, +// except that the target is not installed, which is 403 as for a person. func TestModuleRoute_TheBearerLessArmAdmitsOnlyWhatTheIssuerAdmits(t *testing.T) { - for _, refusal := range []accountsv1.WorkloadOperationRefusal{ - accountsv1.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_INSPECTED, - accountsv1.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE, - accountsv1.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_OPERATION_CONTEXT, - accountsv1.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET, - accountsv1.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS, - } { - t.Run(refusal.String(), func(t *testing.T) { - gw, module, installations, kid, wcPriv := operationHarness(t) - admissionOf(t, gw).refusal = refusal + for refusal, name := range accountsv1.WorkloadOperationRefusal_name { + if refusal == 0 { + continue + } + t.Run(name, func(t *testing.T) { + gw, module, admission, kid, wcPriv := operationHarness(t) + admission.refusal = accountsv1.WorkloadOperationRefusal(refusal) logs := captureLog(t) capability := operationCapability(t, kid, wcPriv, "documents", publisherSeal) w := httptest.NewRecorder() gw.ServeHTTP(w, onWorkloadEndpoint(publishRequest(capability))) - require.Equal(t, http.StatusUnauthorized, w.Code, w.Body.String()) - require.Contains(t, w.Body.String(), "authentication required") + if admission.refusal == accountsv1.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_TARGET_NOT_INSTALLED { + require.Equal(t, http.StatusForbidden, w.Code, w.Body.String()) + require.Contains(t, w.Body.String(), "module is not installed for this organization") + } else { + require.Equal(t, http.StatusUnauthorized, w.Code, w.Body.String()) + require.Contains(t, w.Body.String(), "authentication required") + } require.Zero(t, module.calls) - require.Empty(t, installations.calls, "a refused capability asks nothing more") - require.Contains(t, logs.String(), "reason=workload_operation_"+refusalReason(refusal)) + require.Len(t, admission.calls, 1) + require.Contains(t, logs.String(), "reason=workload_operation_"+refusalReason(admission.refusal)) require.NotContains(t, logs.String(), capability) }) } - // Every refusal the issuer names is one of the above. - require.Len(t, accountsv1.WorkloadOperationRefusal_name, 6) } // Every refusal, each with one fact broken and the rest held equal. func TestModuleRoute_TheBearerLessArmRefuses(t *testing.T) { for name, test := range map[string]struct { - prepare func(t *testing.T, gw *Gateway, installations *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request + prepare func(t *testing.T, gw *Gateway, kid string, wcPriv ed25519.PrivateKey) *http.Request code int body string // asked is whether the issuer was asked. @@ -575,7 +510,7 @@ func TestModuleRoute_TheBearerLessArmRefuses(t *testing.T) { // The arm is the workload endpoint's alone: the public listener never // selects it, so the request takes the person path and has no bearer. "the same request on the public listener": { - prepare: func(t *testing.T, _ *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + prepare: func(t *testing.T, _ *Gateway, kid string, wcPriv ed25519.PrivateKey) *http.Request { return publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal)) }, code: http.StatusUnauthorized, body: "authentication required", @@ -583,62 +518,39 @@ func TestModuleRoute_TheBearerLessArmRefuses(t *testing.T) { // A module's own identity verifies at this route, and is never a // headless operation context. "a module identity capability": { - prepare: func(t *testing.T, _ *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + prepare: func(t *testing.T, _ *Gateway, kid string, wcPriv ed25519.PrivateKey) *http.Request { return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, moduleCapabilitiesAudience, publisherSeal))) }, code: http.StatusUnauthorized, body: "authentication required", }, "a capability for another module": { - prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + prepare: func(t *testing.T, gw *Gateway, kid string, wcPriv ed25519.PrivateKey) *http.Request { declareModule(t, gw, "ledgers") return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "ledgers", publisherSeal))) }, code: http.StatusUnauthorized, body: "invalid work context", }, - // Sealed to the target's own installation: whatever the issuer says, - // the caller would be the module it addresses. - "a capability sealed to the target's installation": { - prepare: func(t *testing.T, _ *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { - return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", targetInstallation))) - }, - code: http.StatusUnauthorized, body: "authentication required", asked: true, - }, - // The tenant may hold more than one ACTIVE row of the target. A seal - // equal to ANY of them is refused, not only the last row read. - "a capability sealed to the first of two rows of the target": { - prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { - gw.installations = &installationsFake{pages: []*accountsv1.ListInstallationsResponse{{ - Installations: []*accountsv1.InstallationSummary{ - moduleInstallation(targetInstallation, "documents"), - moduleInstallation("55555555-5555-4555-8555-555555555555", "documents"), - moduleInstallation(publisherSeal, "publisher"), - }, - }}} - return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", targetInstallation))) - }, - code: http.StatusUnauthorized, body: "authentication required", asked: true, - }, // Core's Inspector verified the first capability header only; a second // is a value nothing verified, so the arm admits exactly one. "two capability headers": { - prepare: func(t *testing.T, _ *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + prepare: func(t *testing.T, _ *Gateway, kid string, wcPriv ed25519.PrivateKey) *http.Request { req := onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) req.Header.Add(carrier.HeaderName, operationCapability(t, kid, wcPriv, "documents", targetInstallation)) return req }, code: http.StatusUnauthorized, body: "invalid work context", }, - "a tenant with no active installation of the target": { - prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { - gw.installations = &installationsFake{pages: []*accountsv1.ListInstallationsResponse{{ - Installations: []*accountsv1.InstallationSummary{moduleInstallation(publisherSeal, "publisher")}, - }}} - return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) + // A Connection option may not name the capability header. + "a Connection option naming the capability": { + prepare: func(t *testing.T, _ *Gateway, kid string, wcPriv ed25519.PrivateKey) *http.Request { + req := onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) + req.Header.Set("Connection", "keep-alive, "+strings.ToLower(carrier.HeaderName)) + return req }, - code: http.StatusForbidden, body: "module is not installed for this organization", asked: true, + code: http.StatusUnauthorized, body: "invalid work context", }, "a route that resolves no target": { - prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + prepare: func(t *testing.T, gw *Gateway, kid string, wcPriv ed25519.PrivateKey) *http.Request { registry := solutionRegistryFake(t, gw) registry.mu.Lock() registry.revision++ @@ -649,17 +561,26 @@ func TestModuleRoute_TheBearerLessArmRefuses(t *testing.T) { require.NoError(t, gw.solutions.refresh(context.Background())) return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) }, - code: http.StatusForbidden, body: "module is not installed for this organization", asked: true, + code: http.StatusForbidden, body: "module is not installed for this organization", }, "an issuer that errors": { - prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + prepare: func(t *testing.T, gw *Gateway, kid string, wcPriv ed25519.PrivateKey) *http.Request { gw.workloadAdmission = &admissionFake{err: errors.New("accounts down: dial tcp 10.0.0.7:9090")} return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) }, code: http.StatusServiceUnavailable, body: "module installation authority unavailable", }, + // A request accounts refuses as malformed is about the capability or + // the alias, never an outage. + "an issuer that refuses the request as invalid": { + prepare: func(t *testing.T, gw *Gateway, kid string, wcPriv ed25519.PrivateKey) *http.Request { + gw.workloadAdmission = &admissionFake{err: grpcstatus.Error(codes.InvalidArgument, "work_context: value length must be at most 32768 characters")} + return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) + }, + code: http.StatusUnauthorized, body: "authentication required", + }, "no issuer wired": { - prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + prepare: func(t *testing.T, gw *Gateway, kid string, wcPriv ed25519.PrivateKey) *http.Request { gw.workloadAdmission = nil return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) }, @@ -668,7 +589,7 @@ func TestModuleRoute_TheBearerLessArmRefuses(t *testing.T) { // The issuer's facts must be the ones the edge inspected: an answer for // another seal or tenant is one nothing here can arbitrate. "an admission naming another seal": { - prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + prepare: func(t *testing.T, gw *Gateway, kid string, wcPriv ed25519.PrivateKey) *http.Request { gw.workloadAdmission = &admissionFake{answer: &accountsv1.AdmitWorkloadOperationResponse{ Admitted: true, TenantId: operationTenant, InstallationId: "44444444-4444-4444-8444-444444444444", TaskId: "task-1", }} @@ -677,7 +598,7 @@ func TestModuleRoute_TheBearerLessArmRefuses(t *testing.T) { code: http.StatusServiceUnavailable, body: "module installation authority unavailable", }, "an admission naming another tenant": { - prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + prepare: func(t *testing.T, gw *Gateway, kid string, wcPriv ed25519.PrivateKey) *http.Request { gw.workloadAdmission = &admissionFake{answer: &accountsv1.AdmitWorkloadOperationResponse{ Admitted: true, TenantId: "66666666-6666-4666-8666-666666666666", InstallationId: publisherSeal, TaskId: "task-1", }} @@ -685,31 +606,10 @@ func TestModuleRoute_TheBearerLessArmRefuses(t *testing.T) { }, code: http.StatusServiceUnavailable, body: "module installation authority unavailable", }, - "an installation listing that errors": { - prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { - gw.installations = &installationsFake{err: errors.New("accounts down: dial tcp 10.0.0.7:9090")} - return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) - }, - code: http.StatusServiceUnavailable, body: "module installation authority unavailable", asked: true, - }, - "a cursor that never terminates": { - prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { - gw.installations = &installationsFake{endless: true} - return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) - }, - code: http.StatusServiceUnavailable, body: "module installation authority unavailable", asked: true, - }, - "no installation listing wired": { - prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { - gw.installations = nil - return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) - }, - code: http.StatusServiceUnavailable, body: "module installation authority unavailable", asked: true, - }, // No edge verifier means no inspected capability, so the arm is never // selected and the person path answers. "no edge verifier": { - prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + prepare: func(t *testing.T, gw *Gateway, kid string, wcPriv ed25519.PrivateKey) *http.Request { capability := operationCapability(t, kid, wcPriv, "documents", publisherSeal) gw.workContext = nil return onWorkloadEndpoint(publishRequest(capability)) @@ -717,7 +617,7 @@ func TestModuleRoute_TheBearerLessArmRefuses(t *testing.T) { code: http.StatusUnauthorized, body: "authentication required", }, "a cookie and no bearer": { - prepare: func(t *testing.T, _ *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + prepare: func(t *testing.T, _ *Gateway, kid string, wcPriv ed25519.PrivateKey) *http.Request { req := onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) req.Header.Set("Cookie", "session=abc") return req @@ -727,7 +627,7 @@ func TestModuleRoute_TheBearerLessArmRefuses(t *testing.T) { // On a cell the listener requires the waypoint presenter; the arm // requires it again, so a listener wired without it admits nothing. "a cell gateway with no presenter": { - prepare: func(t *testing.T, gw *Gateway, _ *installationsFake, kid string, wcPriv ed25519.PrivateKey) *http.Request { + prepare: func(t *testing.T, gw *Gateway, kid string, wcPriv ed25519.PrivateKey) *http.Request { gw.requireWorkloadPresenter = true return onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal))) }, @@ -735,10 +635,9 @@ func TestModuleRoute_TheBearerLessArmRefuses(t *testing.T) { }, } { t.Run(name, func(t *testing.T) { - gw, module, installations, kid, wcPriv := operationHarness(t) - admission := admissionOf(t, gw) + gw, module, admission, kid, wcPriv := operationHarness(t) logs := captureLog(t) - req := test.prepare(t, gw, installations, kid, wcPriv) + req := test.prepare(t, gw, kid, wcPriv) w := httptest.NewRecorder() gw.ServeHTTP(w, req) @@ -746,11 +645,7 @@ func TestModuleRoute_TheBearerLessArmRefuses(t *testing.T) { require.Equal(t, test.code, w.Code, w.Body.String()) require.Contains(t, w.Body.String(), test.body) require.Zero(t, module.calls, "nothing reaches the upstream") - if test.asked { - require.Len(t, admission.calls, 1) - } else { - require.Empty(t, admission.calls, "the default issuer was not the one asked") - } + require.Empty(t, admission.calls, "the default issuer was not the one asked") require.NotContains(t, logs.String(), req.Header.Get(carrier.HeaderName), "no refusal logs the token") require.NotContains(t, w.Body.String(), "10.0.0.7", "no refusal carries the authority's error") require.NotContains(t, logs.String(), "10.0.0.7", "nor logs it") @@ -761,8 +656,8 @@ func TestModuleRoute_TheBearerLessArmRefuses(t *testing.T) { // Every refusal ON THE ARM is a log line naming the signed facts and the // presenter, because the arm runs no ext_authz and this line is its only record. func TestModuleRoute_TheBearerLessArmLogsEachRefusal(t *testing.T) { - gw, _, _, kid, wcPriv := operationHarness(t) - admissionOf(t, gw).refusal = accountsv1.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE + gw, _, admission, kid, wcPriv := operationHarness(t) + admission.refusal = accountsv1.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE logs := captureLog(t) capability := operationCapability(t, kid, wcPriv, "documents", publisherSeal) req := onWorkloadEndpoint(publishRequest(capability)) @@ -824,7 +719,7 @@ func TestModuleRoute_TheBearerLessArmRefusesWhatTheEdgeRefuses(t *testing.T) { }, } { t.Run(name, func(t *testing.T) { - gw, module, installations, kid, wcPriv := operationHarness(t) + gw, module, admission, kid, wcPriv := operationHarness(t) w := httptest.NewRecorder() gw.ServeHTTP(w, onWorkloadEndpoint(publishRequest(mint(t, kid, wcPriv)))) @@ -832,8 +727,7 @@ func TestModuleRoute_TheBearerLessArmRefusesWhatTheEdgeRefuses(t *testing.T) { require.Equal(t, http.StatusUnauthorized, w.Code) require.Contains(t, w.Body.String(), "invalid work context") require.Zero(t, module.calls) - require.Empty(t, admissionOf(t, gw).calls) - require.Empty(t, installations.calls) + require.Empty(t, admission.calls) }) } } @@ -843,7 +737,7 @@ func TestModuleRoute_TheBearerLessArmRefusesWhatTheEdgeRefuses(t *testing.T) { // arm checks both anyway, before anything is stripped or asked, and this holds // that check to its answer with the only empty capability that can be built. func TestModuleRoute_TheBearerLessArmRefusesACapabilityWithNoTenantOrSeal(t *testing.T) { - gw, module, installations, _, _ := operationHarness(t) + gw, module, admission, _, _ := operationHarness(t) routing, resolution := gw.solutions.resolveModuleRouting(context.Background(), "documents") require.Equal(t, solutionRoutable, resolution) @@ -853,43 +747,7 @@ func TestModuleRoute_TheBearerLessArmRefusesACapabilityWithNoTenantOrSeal(t *tes require.Equal(t, http.StatusUnauthorized, w.Code) require.Contains(t, w.Body.String(), "invalid work context") require.Zero(t, module.calls) - require.Empty(t, admissionOf(t, gw).calls) - require.Empty(t, installations.calls) -} - -// The target's rows may sit on any page: the arm follows the cursor to the end, -// so a row of the target on a later page counts, and a row the authority should -// not have returned (another tenant) counts for nothing. -func TestModuleRoute_TheBearerLessArmReadsEveryPageItNeeds(t *testing.T) { - gw, module, _, kid, wcPriv := operationHarness(t) - foreign := moduleInstallation(targetInstallation, "documents") - foreign.Installation.OrgId = "66666666-6666-4666-8666-666666666666" - installations := &installationsFake{pages: []*accountsv1.ListInstallationsResponse{ - {Installations: []*accountsv1.InstallationSummary{foreign, moduleInstallation(publisherSeal, "publisher")}, NextPageToken: pageCursor(1)}, - {Installations: []*accountsv1.InstallationSummary{moduleInstallation(targetInstallation, "documents")}}, - }} - gw.installations = installations - - w := httptest.NewRecorder() - gw.ServeHTTP(w, onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal)))) - require.Equal(t, http.StatusOK, w.Code, w.Body.String()) - require.Equal(t, 1, module.calls) - require.Len(t, installations.calls, 2) - require.Equal(t, pageCursor(1), installations.calls[1].GetPageToken()) - - // A seal on the target's row that only the second page names is still the - // target's. - w = httptest.NewRecorder() - gw.ServeHTTP(w, onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", targetInstallation)))) - require.Equal(t, http.StatusUnauthorized, w.Code) - - // The target only on the foreign row: not installed in THIS tenant. - installations.pages = installations.pages[:1] - installations.pages[0].NextPageToken = "" - w = httptest.NewRecorder() - gw.ServeHTTP(w, onWorkloadEndpoint(publishRequest(operationCapability(t, kid, wcPriv, "documents", publisherSeal)))) - require.Equal(t, http.StatusForbidden, w.Code) - require.Equal(t, 1, module.calls) + require.Empty(t, admission.calls) } // accountsAdmissionServer is accounts' AdmitWorkloadOperation on a real gRPC @@ -957,7 +815,7 @@ func TestAccountsWorkloadAdmissionAsksTheIssuerWithinItsBound(t *testing.T) { workloadAdmissionCallTimeout = 50 * time.Millisecond t.Cleanup(func() { workloadAdmissionCallTimeout = previous }) fake := &accountsAdmissionServer{block: true} - gw, module, installations, kid, wcPriv := operationHarness(t) + gw, module, _, kid, wcPriv := operationHarness(t) gw.workloadAdmission = &accountsWorkloadAdmission{conn: startAccountsAdmission(t, fake), internalToken: "test-internal-token"} started := time.Now() @@ -969,6 +827,5 @@ func TestAccountsWorkloadAdmissionAsksTheIssuerWithinItsBound(t *testing.T) { require.Less(t, time.Since(started), 5*time.Second, "bounded by the call timeout") require.Len(t, fake.requests, 1) require.Zero(t, module.calls) - require.Empty(t, installations.calls) }) } diff --git a/module/services/auth-gateway/code/gateway_operation_context_test.go b/module/services/auth-gateway/code/gateway_operation_context_test.go index c489cdb29..ea35dfaf3 100644 --- a/module/services/auth-gateway/code/gateway_operation_context_test.go +++ b/module/services/auth-gateway/code/gateway_operation_context_test.go @@ -222,7 +222,6 @@ func TestGateway_AMintedOperationContextReachesItsModuleWithoutABearer(t *testin jwks := jwksServer(t, jwksDocument(map[string]ed25519.PublicKey{"key-1": pub}), nil) gw.workContext = newWorkContextVerifier(jwks.URL) module := declareModule(t, gw, "documents") - gw.installations = tenantInstalled() admission := &admissionFake{} gw.workloadAdmission = admission mint.token = operationCapability(t, "key-1", wcPriv, "documents", publisherSeal) @@ -252,10 +251,10 @@ func TestGateway_AMintedOperationContextReachesItsModuleWithoutABearer(t *testin require.Equal(t, 1, module.calls) // And a refusal on the arm logs the facts, never the token. - gw.installations = &installationsFake{} + admission.refusal = accountsv1.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_TARGET_NOT_INSTALLED w = httptest.NewRecorder() gw.ServeHTTP(w, onWorkloadEndpoint(publishRequest(capability))) require.Equal(t, http.StatusForbidden, w.Code) - require.Contains(t, logs.String(), "reason=workload_operation_not_installed") + require.Contains(t, logs.String(), "reason=workload_operation_target_not_installed") require.NotContains(t, logs.String(), capability) } diff --git a/module/services/auth-gateway/code/gateway_workload_operation.go b/module/services/auth-gateway/code/gateway_workload_operation.go index e815524d5..177b92426 100644 --- a/module/services/auth-gateway/code/gateway_workload_operation.go +++ b/module/services/auth-gateway/code/gateway_workload_operation.go @@ -14,32 +14,31 @@ package main // Selection is narrow and every other request takes the person path unchanged: // no Authorization and no Cookie header, the workload listener, and an // edge-inspected capability addressed to the alias this route resolved. Once -// selected, the arm decides from two authority reads, both behind the tenant's -// limiter: -// -// - accounts' AdmitWorkloadOperation, the issuer's answer to whether the -// capability is a module operation context (StartModuleOperationTask) for -// this alias. The gateway cannot tell: core's Inspected withholds the owner, -// its kind and the chain, and a person's capability can be sealed to an -// installation that is not the target's (a source-operation context, an -// installation task, an installed binding's child). Accounts reads its own -// claims and admits that one shape; everything else is refused. It inspects -// and never verifies, so nothing is consumed on the way. -// - the tenant's ACTIVE installations, read from accounts per request: the -// target must be installed there, and the seal must not be one of the -// target's own rows (a module does not reach itself through this door). +// selected, the arm decides from one authority read, behind the tenant's +// limiter: accounts' AdmitWorkloadOperation, the issuer's answer to whether the +// capability is a module operation context (StartModuleOperationTask) for this +// alias, and whether the route's target is installed in its tenant. The gateway +// cannot tell the first: core's Inspected withholds the owner, its kind and the +// chain, and a person's capability can be sealed to an installation that is not +// the target's (a source-operation context, an installation task, an installed +// binding's child). Accounts reads the capability back against its own live +// state and admits that one shape; everything else is refused. Nothing is +// consumed on the way. // // A misreading refuses, never widens, and the callee verifies the capability at // full strength. // // What the upstream receives: the capability as presented, the path unchanged, -// no bearer, no cookie, no identity header, and x-codefly-installation-id set to -// the seal accounts admitted. Nothing names a principal: the gateway reads none. +// and nothing else this gateway asserts. No bearer, no cookie, no identity +// header, and no x-codefly-installation-id: a hop stamps nothing a callee would +// read in place of verifying the token (core's Inspected, by design), and no +// consumer of a module route reads that header. import ( "context" "log" "net/http" + "net/textproto" "strings" "time" @@ -48,44 +47,11 @@ import ( "github.com/codefly-dev/core/workcontext" carrier "github.com/codefly-dev/sdk-go/workcontext" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" ) -const listInstallationsMethod = "/saas.accounts.v1.InstallationService/ListInstallations" - -// installationListingPageSize and installationListingPageLimit bound one -// admission read the way the entitlement read beside it is bounded: a cursor -// that never terminates is undecidable, never a short answer. -const ( - installationListingPageSize = 200 - installationListingPageLimit = solutionEntitlementPageLimit -) - -// installationListingClient is the authority surface the arm consumes. The -// interface exists so the arm can be exercised without an accounts process. -type installationListingClient interface { - List(ctx context.Context, req *accountsv1.ListInstallationsRequest) (*accountsv1.ListInstallationsResponse, error) -} - -// accountsInstallations invokes the organization's installation listing by -// method name over the existing accounts connection, with the gateway's -// cluster-internal credential: the shape of accountsSolutionEntitlements, and for -// the same reason (only the vendored message types, no client stub). -type accountsInstallations struct { - conn *grpc.ClientConn - internalToken string -} - -func (c *accountsInstallations) List( - ctx context.Context, req *accountsv1.ListInstallationsRequest, -) (*accountsv1.ListInstallationsResponse, error) { - ctx, cancel := context.WithTimeout(ctx, solutionEntitlementCallTimeout) - defer cancel() - ctx = metadata.AppendToOutgoingContext(ctx, "x-codefly-internal-token", c.internalToken) - resp := &accountsv1.ListInstallationsResponse{} - return resp, c.conn.Invoke(ctx, listInstallationsMethod, req, resp) -} - const admitWorkloadOperationMethod = "/saas.accounts.v1.WorkContextService/AdmitWorkloadOperation" // workloadAdmissionCallTimeout bounds the issuer's answer. A caller waits on it @@ -142,80 +108,6 @@ func selectsWorkloadOperation(r *http.Request, capability *workcontext.Inspected capability.Audience() == alias } -// workloadOperationInstallation is the arm's installation verdict. Like the -// viewer admission it is not a boolean: "not installed", "the target's own -// installation" and "I could not ask" are different answers. -type workloadOperationInstallation int - -const ( - workloadOperationInstalled workloadOperationInstallation = iota - // workloadOperationNotInstalled: the tenant has no ACTIVE installation of - // the target, or the route resolved no target. - workloadOperationNotInstalled - // workloadOperationSealOnTarget: the seal is one of the target's own - // installations, so the caller would be the module it addresses. - workloadOperationSealOnTarget - // workloadOperationUndecidable: no client wired, an RPC error, or a cursor - // that did not terminate. Answered 503, never admitted. - workloadOperationUndecidable -) - -// workloadOperationInstallations reads the tenant's ACTIVE installations and -// decides. Uncached, for the reason the viewer admission is: an uninstall -// changes the answer with no write this replica would see. -func (g *Gateway) workloadOperationInstallations( - ctx context.Context, tenant, seal string, routing *solutionRouting, -) workloadOperationInstallation { - targetID := routing.GetTargetID() - if targetID == "" { - return workloadOperationNotInstalled - } - if g.installations == nil { - return workloadOperationUndecidable - } - // Every ACTIVE row of the target counts: keeping only the last row seen - // would read the others as some other installation's. - targetInstallations := map[string]bool{} - token := "" - for page := 0; page < installationListingPageLimit; page++ { - resp, err := g.installations.List(ctx, &accountsv1.ListInstallationsRequest{ - OrgId: tenant, - Status: accountsv1.InstallationStatus_INSTALLATION_STATUS_ACTIVE, - PageSize: installationListingPageSize, - PageToken: token, - }) - if err != nil { - return workloadOperationUndecidable - } - for _, summary := range resp.GetInstallations() { - installation := summary.GetInstallation() - // The request asked for ACTIVE rows of this tenant; a row that is - // neither is not counted, whatever the authority returned. - if installation.GetId() == "" || installation.GetOrgId() != tenant || - installation.GetStatus() != accountsv1.InstallationStatus_INSTALLATION_STATUS_ACTIVE { - continue - } - if installation.GetTargetId() == targetID { - targetInstallations[installation.GetId()] = true - } - } - token = resp.GetNextPageToken() - if token == "" { - break - } - if page == installationListingPageLimit-1 { - return workloadOperationUndecidable - } - } - switch { - case len(targetInstallations) == 0: - return workloadOperationNotInstalled - case targetInstallations[seal]: - return workloadOperationSealOnTarget - } - return workloadOperationInstalled -} - // refusalReason is the log reason for an issuer's refusal: // WORKLOAD_OPERATION_REFUSAL_OWNER_NOT_SERVICE logs as owner_not_service. func refusalReason(refusal accountsv1.WorkloadOperationRefusal) string { @@ -275,6 +167,12 @@ func (g *Gateway) serveWorkloadOperation( rec.refuse(w, http.StatusUnauthorized, "invalid work context", "no_seal") return } + // The capability is the one thing this arm forwards for the callee to + // verify, and it travels end to end: a Connection option may not name it. + if connectionNames(r.Header, carrier.HeaderName) { + rec.refuse(w, http.StatusUnauthorized, "invalid work context", "connection_names_capability") + return + } // Strip BEFORE the limiter, as the person path does: a caller's X-Org-Id // must never reach the key, and nothing below may read a header the caller @@ -301,19 +199,34 @@ func (g *Gateway) serveWorkloadOperation( reached := false g.rateLimitThenServe(w, r, entry, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { reached = true - // The issuer decides what the capability is. Asked only behind the - // limiter, so an exhausted budget never reaches accounts. + targetID := routing.GetTargetID() + if targetID == "" { + rec.refuse(w, http.StatusForbidden, "module is not installed for this organization", "not_installed") + return + } + // The issuer decides, in one call, what the capability is and whether + // the target is installed. Asked only behind the limiter, so an + // exhausted budget never reaches accounts. if g.workloadAdmission == nil { rec.refuse(w, http.StatusServiceUnavailable, "module installation authority unavailable", "authority_unavailable") return } answer, err := g.workloadAdmission.Admit(r.Context(), &accountsv1.AdmitWorkloadOperationRequest{ - WorkContext: r.Header.Get(carrier.HeaderName), Alias: alias, + WorkContext: r.Header.Get(carrier.HeaderName), Alias: alias, TargetId: targetID, }) switch { + case status.Code(err) == codes.InvalidArgument: + // Accounts refused the request's shape, which is the capability's + // or the alias's: a refusal, not an outage. + rec.refuse(w, http.StatusUnauthorized, "authentication required", "invalid_request") + return case err != nil: rec.refuse(w, http.StatusServiceUnavailable, "module installation authority unavailable", "authority_unavailable") return + case answer.GetRefusal() == accountsv1.WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_TARGET_NOT_INSTALLED: + // The answer a person gets whose organization never installed it. + rec.refuse(w, http.StatusForbidden, "module is not installed for this organization", refusalReason(answer.GetRefusal())) + return case !answer.GetAdmitted(): // The answer a person's capability without its bearer has always had. rec.refuse(w, http.StatusUnauthorized, "authentication required", refusalReason(answer.GetRefusal())) @@ -324,22 +237,6 @@ func (g *Gateway) serveWorkloadOperation( rec.refuse(w, http.StatusServiceUnavailable, "module installation authority unavailable", "authority_mismatch") return } - switch g.workloadOperationInstallations(r.Context(), rec.tenant, rec.seal, routing) { - case workloadOperationUndecidable: - rec.refuse(w, http.StatusServiceUnavailable, "module installation authority unavailable", "authority_unavailable") - return - case workloadOperationNotInstalled: - rec.refuse(w, http.StatusForbidden, "module is not installed for this organization", "not_installed") - return - case workloadOperationSealOnTarget: - rec.refuse(w, http.StatusUnauthorized, "authentication required", "seal_on_target") - return - } - // The one header this arm stamps: the seal of the capability accounts - // admitted. An sdk consumer reading the carrier requires this header to - // equal the seal, and on the person path it equals the seal too. Here - // it names the CALLER's installation, not consent on the target's side. - r.Header.Set(installationConfinementHeader, answer.GetInstallationId()) rec.log("INFO: admitted", "admit") g.proxyTo(w, r, routing.Upstream, entry) })) @@ -350,3 +247,15 @@ func (g *Gateway) serveWorkloadOperation( rec.log("WARN: blocked", "limited_or_origin") } } + +// connectionNames reports whether any Connection option on h names header. +func connectionNames(h http.Header, header string) bool { + for _, value := range h.Values("Connection") { + for _, option := range strings.Split(value, ",") { + if strings.EqualFold(textproto.TrimString(option), header) { + return true + } + } + } + return false +} diff --git a/module/services/auth-gateway/code/main.go b/module/services/auth-gateway/code/main.go index 64c37da87..2d83fdd21 100644 --- a/module/services/auth-gateway/code/main.go +++ b/module/services/auth-gateway/code/main.go @@ -255,9 +255,6 @@ func main() { // connection. It is not a cache: the answer is per viewer and must not // outlive a revoke, so every request asks. gateway.solutionEntitlements = &accountsSolutionEntitlements{conn: internalAPIConn, internalToken: authz.internalToken} - // The organization-level installation read the bearer-less module arm - // admits on, over the same connection and uncached for the same reason. - gateway.installations = &accountsInstallations{conn: internalAPIConn, internalToken: authz.internalToken} // The issuer's answer to what a bearer-less capability is, over the // same connection, asked per request. gateway.workloadAdmission = &accountsWorkloadAdmission{conn: internalAPIConn, internalToken: authz.internalToken} From 31f50c93cdbdb5f716aa6fd4e1df57fd188be110 Mon Sep 17 00:00:00 2001 From: saurabhmaheshwari-obin <242053479+saurabhmaheshwari-obin@users.noreply.github.com> Date: Sat, 24 Oct 2026 17:12:23 -0400 Subject: [PATCH 15/17] revert(contracts): installation_listing.proto goes back into installations.proto The bearer-less arm no longer calls ListInstallations (accounts answers the target's installation inside AdmitWorkloadOperation), and nothing else in the gateway did on the base either, so the gateway needs neither the ListInstallations messages nor a vendored copy of them. This undoes the extraction e94688afd made for it, back to the base byte for byte: - accounts: installations.proto, platform_admin.proto and proto/buf.yaml (with its MESSAGE_NO_DELETE and ENUM_NO_DELETE waivers for installations.proto) return to the base; installation_listing.proto is deleted. The ListInstallations contract comment and handler comment return to the base wording, which never named the arm. - gateway: its vendored installation_listing.proto and generated Go are gone. TestVendoredAccountsProtosAreAccounts stays and now guards workload_operation_admission.proto with the vendored files the base had. - frontend: the five solution-installations imports and ui-inventory.json return to installations_pb. Every regenerated installation file (Go, TypeScript, raw and public OpenAPI, contract protos and buf.yaml) is identical to the base. The saas-sdk keeps 0.3.36: its content still differs from the last release (the admission messages), and kit-version check requires the bump. Regenerated with Codefly source candidate 5234ce24 (accounts and gateway proto, go generate, contracts). The saas-sdk library was regenerated with released CLI 0.1.191, the version the base's last regeneration of it records: its output is byte-identical to the candidate's except the generated-by stamp, which now reads 0.1.191 again. Co-Authored-By: Claude Opus 5.5 --- .../api/accounts/authority/contract.binpb | Bin 1206896 -> 1205762 bytes .../api/accounts/authority/proto/buf.yaml | 11 - .../accounts/v1/installation_listing.proto | 131 --- .../saas/accounts/v1/installations.proto | 132 ++- .../saas/accounts/v1/platform_admin.proto | 2 +- .../v1/workload_operation_admission.proto | 2 +- .../api/accounts/connect/contract.binpb | Bin 1206896 -> 1205762 bytes .../api/accounts/connect/proto/buf.yaml | 11 - .../accounts/v1/installation_listing.proto | 131 --- .../saas/accounts/v1/installations.proto | 132 ++- .../saas/accounts/v1/platform_admin.proto | 2 +- .../v1/workload_operation_admission.proto | 2 +- .../api/auth-gateway/grpc/contract.binpb | Bin 361718 -> 353207 bytes .../accounts/v1/installation_listing.proto | 131 --- .../v1/workload_operation_admission.proto | 2 +- module/contracts/api/catalog.codefly.json | 6 +- module/module.package.codefly.yaml | 6 +- .../code/pkg/adapters/installation_rpcs.go | 7 +- .../installations.connect.go | 34 +- .../accounts/v1/installation_listing.pb.go | 607 ------------- .../gen/saas/accounts/v1/installations.pb.go | 826 ++++++++++++++---- .../saas/accounts/v1/installations_grpc.pb.go | 34 +- .../gen/saas/accounts/v1/platform_admin.pb.go | 4 +- .../generated/openapi-raw/api.swagger.json | 2 +- .../accounts/openapi/api.swagger.json | 2 +- module/services/accounts/proto/buf.yaml | 11 - .../accounts/v1/installation_listing.proto | 131 --- .../saas/accounts/v1/installations.proto | 132 ++- .../saas/accounts/v1/platform_admin.proto | 2 +- .../v1/workload_operation_admission.proto | 2 +- .../accounts/v1/installation_listing.pb.go | 607 ------------- .../code/vendored_accounts_protos_test.go | 2 +- .../accounts/v1/installation_listing.proto | 131 --- .../v1/workload_operation_admission.proto | 2 +- .../generated/contract/catalog.codefly.json | 2 +- .../generated/contract/contract.binpb | Bin 1206896 -> 1205762 bytes .../saas-sdk/generated/library.codefly.yaml | 4 +- .../accounts/v1/installation_listing_pb.ts | 306 ------- .../gen/saas/accounts/v1/installations_pb.ts | 361 +++++++- .../gen/saas/accounts/v1/platform_admin_pb.ts | 6 +- .../model/installations.test.ts | 4 +- .../model/installations.ts | 8 +- .../solution-installations/service/queries.ts | 8 +- .../ui/installation-setup-page.tsx | 2 +- .../ui/solution-installations-page.tsx | 6 +- .../accounts/v1/installation_listing_pb.ts | 307 ------- .../gen/saas/accounts/v1/installations_pb.ts | 361 +++++++- .../gen/saas/accounts/v1/platform_admin_pb.ts | 6 +- .../services/frontend/code/ui-inventory.json | 2 - 49 files changed, 1745 insertions(+), 2875 deletions(-) delete mode 100644 module/contracts/api/accounts/authority/proto/saas/accounts/v1/installation_listing.proto delete mode 100644 module/contracts/api/accounts/connect/proto/saas/accounts/v1/installation_listing.proto delete mode 100644 module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/installation_listing.proto delete mode 100644 module/services/accounts/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go delete mode 100644 module/services/accounts/proto/saas/accounts/v1/installation_listing.proto delete mode 100644 module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go delete mode 100644 module/services/auth-gateway/proto/saas/accounts/v1/installation_listing.proto delete mode 100644 module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installation_listing_pb.ts delete mode 100644 module/services/frontend/code/src/gen/saas/accounts/v1/installation_listing_pb.ts diff --git a/module/contracts/api/accounts/authority/contract.binpb b/module/contracts/api/accounts/authority/contract.binpb index 99f9f12a78efb6ba8a5f898f5af449dec14b52d4..7ef3f1903569272098d3729e4fc7b7204c1c9ece 100644 GIT binary patch delta 15161 zcmZ8odz=-;u|D0?(|gX$z{0T0!UD@dd8oVvQ9wZi1T=~PRE$OfxCEln1Tf}a^ZOki ztKuUPym&`Z5Uxh#p@0fU5KuuLA`t}y0g;D_qNu1S@-p|U>Yka6f9wyw>h9{Q`l_pY zdgdJ7I_R#AgYKviHL}L3Q8jLjt|_U>*5qoum;LXbcc*?My_V6TO@6az=q7)B^v&m` z1ER)mNmX&e3w|Vv-QM+2xs`^$^(eeKtCqJ`_TZlQ@=;RT-SW@JW$PE*;! z^F2j~K7vl3C1#CG7X=i(zSMubv?bz81^I#|DeLZ^fE3jkJhuzm?ies#3w0ueCg0-$3@Yl@K6)=9Quzkmp|V=Dm&oz>PW zkkLYtU!CRYwkO=`EYGk#9rWznx{XET-Py9G?Fi@2mS@II=y-OUco@8Nx?nnDNl2GK zXWP+5$1WW^dGmxPOBDD%FEcvow!)uRI4@g9eH3WLi0(2=!wC`S5jR0|_naRi7~8#W zX^gT!^}2iYyc`Y7-j}P}5R&@m*>(Z7JrcE0)I;`)n?zV}dgNkF0(yId4dO=V?NN4t z)hn#t9_8oTc5<+1q8I-4l$XR!n5Ac~NsOSjXI1kUL2b|cAgfkZTD?764D^1Io})6k z(CH)VRDLO=!B5rA7aPCkUnvTG{J@OQ(Nd^|FfSpGMtY%hkx3@}AMv}$=WIt*9N5}7 zu@(08O>Ct!=$qK;P+MZF16%u&tp$#)V{PpVTMcCj+0ajVmNEwI<0eem N%+fF9 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 diff --git a/module/contracts/api/accounts/authority/proto/buf.yaml b/module/contracts/api/accounts/authority/proto/buf.yaml index 1a1cfc5ca..509da1b1f 100644 --- a/module/contracts/api/accounts/authority/proto/buf.yaml +++ b/module/contracts/api/accounts/authority/proto/buf.yaml @@ -94,23 +94,12 @@ breaking: # SolutionBackendBinding (#1118): the reported upstream half, deleted # with the report — a solution's address is resolved, never recorded. - saas/accounts/v1/solution_registry.proto - # Installation, ListInstallationsRequest, InstallationSummary and - # ListInstallationsResponse moved to installation_listing.proto so the - # auth-gateway can generate the ListInstallations messages from the same - # source file without vendoring the policy options and google.api this - # file imports. Same package, same fully-qualified names, same wire - # format; every RPC stays where it was. - - saas/accounts/v1/installations.proto # ModuleMintRegistrationRequest/Response and # SolutionMintRegistrationRequest/Response (#1056): the registration # credentials and the secrets that minted them are deleted. Presence is # delivered, so nothing registers at runtime and nothing verifies either # credential. - saas/accounts/v1/module_registration.proto - # InstallationStatus and InstallationHealth moved to - # installation_listing.proto with the messages above, for the same reason. - ENUM_NO_DELETE: - - saas/accounts/v1/installations.proto # ListMyAccessibleScopes moved from PermissionService to the new # AccessibleScopeService in accessible_scopes.proto (issue #577), which changes # its Connect/gRPC procedure path. Its REST route (GET /v1/accessible-scopes) is diff --git a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installation_listing.proto b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installation_listing.proto deleted file mode 100644 index 57150f92b..000000000 --- a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installation_listing.proto +++ /dev/null @@ -1,131 +0,0 @@ -syntax = "proto3"; - -package saas.accounts.v1; - -import "buf/validate/validate.proto"; -import "google/protobuf/timestamp.proto"; - -// The installation listing: an installation, its lifecycle and health, and the -// messages InstallationService/ListInstallations takes and answers. -// -// These definitions live in their own file, with deliberately minimal imports, -// so the auth-gateway can generate the same types from the same source proto -// and call ListInstallations over the internal listener, as it does for -// module_registration.proto and solution_entitlements.proto. installations.proto -// imports this file; the service and every other installation message stay -// there. Sharing the generated definition rather than restating the wire shape -// is what keeps the two sides from drifting: the gateway's copy is this file, -// byte for byte. - -// InstallationStatus is the lifecycle of an installation row. An uninstall is a -// soft delete (status=revoked, revoked_at set) so the agent, grants, and audit -// trail remain resolvable. -enum InstallationStatus { - INSTALLATION_STATUS_UNSPECIFIED = 0; - INSTALLATION_STATUS_ACTIVE = 1; - INSTALLATION_STATUS_REVOKED = 2; - // Installed, and waiting for the solution to acknowledge its standard install - // step. Nothing can use a pending installation. - INSTALLATION_STATUS_PENDING = 3; - // The solution refused its install step, or never answered within the - // attempt budget. Nothing can use it; an administrator may retry it. - INSTALLATION_STATUS_FAILED = 4; -} - -// InstallationHealth is resolved live from current authority facts, never -// stored. It is the observable form of the fail-closed mint rule: a -// StartInstallationTask fails exactly when health is not HEALTHY, and transfer -// or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record -// nor any co-owner is currently an org admin (the Zapier-class "connection owner -// left the org" failure), surfaced rather than silently reassigned. -enum InstallationHealth { - INSTALLATION_HEALTH_UNSPECIFIED = 0; - INSTALLATION_HEALTH_HEALTHY = 1; - INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER = 2; - INSTALLATION_HEALTH_AGENT_REVOKED = 3; - INSTALLATION_HEALTH_AGENT_DISABLED = 4; - INSTALLATION_HEALTH_STANDING_GRANT_MISSING = 5; -} - -// Installation is one solution's bound identity in an org. Server-assigned ids; -// the browser cannot attest to tenancy, the agent principal, or the scope node. -message Installation { - string id = 1 [(buf.validate.field).string.uuid = true]; - string org_id = 2 [(buf.validate.field).string.uuid = true]; - string agent_principal_id = 3 [(buf.validate.field).string.uuid = true]; - // Was `solution_identifier`: the free-text route alias. It was the join key - // every reader used, and because an alias is deliberately reusable by a later - // binding, an installation that named one inherited whatever took it next. - // Replaced by target_id below. Reserved rather than reused so no consumer can - // read an alias out of a field that now carries an identity. - reserved 4; - reserved "solution_identifier"; - string owner_principal_id = 5 [(buf.validate.field).string.uuid = true]; - repeated string co_owner_principal_ids = 6; - string root_scope_node_id = 7 [(buf.validate.field).string.uuid = true]; - InstallationStatus status = 8; - google.protobuf.Timestamp created_at = 9; - optional google.protobuf.Timestamp revoked_at = 10; - // The immutable solution target this installation was consented to: one - // continuous period of one binding's presence on this host. It is never - // reused, so a replacement binding — even one claiming the same route alias — - // is a different target and inherits nothing. - // - // Deliberately NOT accompanied by the route alias. The alias is a property of - // the target that a later generation may move, and resolving it here would - // mean joining the host's presence state into a request-path listing — which - // would require granting request traffic read over every organisation's - // presence to answer about one. A consumer that needs the alias asks - // ListAvailableSolutions, which serves accepted applied state, and joins on - // this id. - // - // Empty only on a revoked historical row whose target could not be - // established at the cutover; an active installation always names one. - string target_id = 11 [ - (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, - (buf.validate.field).string.uuid = true - ]; - // Why a revoked installation was revoked, when it was not an administrator - // uninstalling: the installation-identity cutover, or a tenancy narrowing that - // withdrew this organisation. Empty on an active row. - string revoked_reason = 12 [(buf.validate.field).string.max_len = 512]; - // Why the install step failed: the solution's bounded refusal reason, or a - // fixed reason when it never answered. Empty unless status is FAILED. - string failure_reason = 13 [(buf.validate.field).string.max_len = 512]; -} - -// ListInstallationsRequest asks what one organization has installed. Every other -// RPC on this service names a single installation, so until this existed nothing -// could answer "what has this org installed?" — which is the authority read a -// per-organization solution projection is built on (issue #949). -message ListInstallationsRequest { - string org_id = 1 [(buf.validate.field).string.uuid = true]; - // Restrict to one lifecycle status. UNSPECIFIED returns every status, so a - // caller that must see only live installs asks for ACTIVE explicitly rather - // than leaning on a default that would silently widen if the enum grew. - InstallationStatus status = 2; - // Page bound. 0 -> server default; capped so an organization with many - // installations can never return an unbounded result in one call. - int32 page_size = 3 [(buf.validate.field).int32 = { - gte: 0 - lte: 500 - }]; - // Opaque cursor from a previous response's next_page_token; empty for page one. - string page_token = 4; -} - -// InstallationSummary is one row of the listing: the installation, plus the health -// resolved live beside it exactly as GetInstallation resolves it for one. Health -// travels with the row because a consumer that renders an installed solution as -// unavailable would otherwise need one GetInstallation per installation. -message InstallationSummary { - Installation installation = 1; - InstallationHealth health = 2; -} - -message ListInstallationsResponse { - repeated InstallationSummary installations = 1; - // Set when more installations remain; pass it back as page_token. Empty on the - // last page. - string next_page_token = 2; -} diff --git a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installations.proto b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installations.proto index 9fc95dd86..82739f9de 100644 --- a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installations.proto +++ b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installations.proto @@ -5,8 +5,8 @@ package saas.accounts.v1; import "buf/validate/validate.proto"; import "google/api/annotations.proto"; import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; import "saas/accounts/v1/authorization.proto"; -import "saas/accounts/v1/installation_listing.proto"; import "saas/policy/v1/options.proto"; // ============================================================================ @@ -22,6 +22,83 @@ import "saas/policy/v1/options.proto"; // (StartInstallationTask) beside the other mints. // ============================================================================ +// InstallationStatus is the lifecycle of an installation row. An uninstall is a +// soft delete (status=revoked, revoked_at set) so the agent, grants, and audit +// trail remain resolvable. +enum InstallationStatus { + INSTALLATION_STATUS_UNSPECIFIED = 0; + INSTALLATION_STATUS_ACTIVE = 1; + INSTALLATION_STATUS_REVOKED = 2; + // Installed, and waiting for the solution to acknowledge its standard install + // step. Nothing can use a pending installation. + INSTALLATION_STATUS_PENDING = 3; + // The solution refused its install step, or never answered within the + // attempt budget. Nothing can use it; an administrator may retry it. + INSTALLATION_STATUS_FAILED = 4; +} + +// InstallationHealth is resolved live from current authority facts, never +// stored. It is the observable form of the fail-closed mint rule: a +// StartInstallationTask fails exactly when health is not HEALTHY, and transfer +// or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record +// nor any co-owner is currently an org admin (the Zapier-class "connection owner +// left the org" failure), surfaced rather than silently reassigned. +enum InstallationHealth { + INSTALLATION_HEALTH_UNSPECIFIED = 0; + INSTALLATION_HEALTH_HEALTHY = 1; + INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER = 2; + INSTALLATION_HEALTH_AGENT_REVOKED = 3; + INSTALLATION_HEALTH_AGENT_DISABLED = 4; + INSTALLATION_HEALTH_STANDING_GRANT_MISSING = 5; +} + +// Installation is one solution's bound identity in an org. Server-assigned ids; +// the browser cannot attest to tenancy, the agent principal, or the scope node. +message Installation { + string id = 1 [(buf.validate.field).string.uuid = true]; + string org_id = 2 [(buf.validate.field).string.uuid = true]; + string agent_principal_id = 3 [(buf.validate.field).string.uuid = true]; + // Was `solution_identifier`: the free-text route alias. It was the join key + // every reader used, and because an alias is deliberately reusable by a later + // binding, an installation that named one inherited whatever took it next. + // Replaced by target_id below. Reserved rather than reused so no consumer can + // read an alias out of a field that now carries an identity. + reserved 4; + reserved "solution_identifier"; + string owner_principal_id = 5 [(buf.validate.field).string.uuid = true]; + repeated string co_owner_principal_ids = 6; + string root_scope_node_id = 7 [(buf.validate.field).string.uuid = true]; + InstallationStatus status = 8; + google.protobuf.Timestamp created_at = 9; + optional google.protobuf.Timestamp revoked_at = 10; + // The immutable solution target this installation was consented to: one + // continuous period of one binding's presence on this host. It is never + // reused, so a replacement binding — even one claiming the same route alias — + // is a different target and inherits nothing. + // + // Deliberately NOT accompanied by the route alias. The alias is a property of + // the target that a later generation may move, and resolving it here would + // mean joining the host's presence state into a request-path listing — which + // would require granting request traffic read over every organisation's + // presence to answer about one. A consumer that needs the alias asks + // ListAvailableSolutions, which serves accepted applied state, and joins on + // this id. + // + // Empty only on a revoked historical row whose target could not be + // established at the cutover; an active installation always names one. + string target_id = 11 [ + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + (buf.validate.field).string.uuid = true + ]; + // Why a revoked installation was revoked, when it was not an administrator + // uninstalling: the installation-identity cutover, or a tenancy narrowing that + // withdrew this organisation. Empty on an active row. + string revoked_reason = 12 [(buf.validate.field).string.max_len = 512]; + // Why the install step failed: the solution's bounded refusal reason, or a + // fixed reason when it never answered. Empty unless status is FAILED. + string failure_reason = 13 [(buf.validate.field).string.max_len = 512]; +} + message InstallSolutionRequest { string org_id = 1 [(buf.validate.field).string.uuid = true]; // Canonical "publisher/name:version" of the solution's agent. The agent @@ -142,6 +219,42 @@ message GetInstallationResponse { InstallationHealth health = 2; } +// ListInstallationsRequest asks what one organization has installed. Every other +// RPC on this service names a single installation, so until this existed nothing +// could answer "what has this org installed?" — which is the authority read a +// per-organization solution projection is built on (issue #949). +message ListInstallationsRequest { + string org_id = 1 [(buf.validate.field).string.uuid = true]; + // Restrict to one lifecycle status. UNSPECIFIED returns every status, so a + // caller that must see only live installs asks for ACTIVE explicitly rather + // than leaning on a default that would silently widen if the enum grew. + InstallationStatus status = 2; + // Page bound. 0 -> server default; capped so an organization with many + // installations can never return an unbounded result in one call. + int32 page_size = 3 [(buf.validate.field).int32 = { + gte: 0 + lte: 500 + }]; + // Opaque cursor from a previous response's next_page_token; empty for page one. + string page_token = 4; +} + +// InstallationSummary is one row of the listing: the installation, plus the health +// resolved live beside it exactly as GetInstallation resolves it for one. Health +// travels with the row because a consumer that renders an installed solution as +// unavailable would otherwise need one GetInstallation per installation. +message InstallationSummary { + Installation installation = 1; + InstallationHealth health = 2; +} + +message ListInstallationsResponse { + repeated InstallationSummary installations = 1; + // Set when more installations remain; pass it back as page_token. Empty on the + // last page. + string next_page_token = 2; +} + // InstallationService composes and administers installations. The headless mint // that turns an installation into a short-lived Work Context is // WorkContextService.StartInstallationTask. @@ -318,15 +431,11 @@ service InstallationService { // ListInstallations enumerates one organization's installations with their live // health. It is internal-tier on purpose: it takes the organization as a request // field, so only a caller holding the cluster-internal credential may ask, and - // the tenant it asks about is never one a browser supplied. The auth-gateway is - // its caller, for two reads. Its viewer projection asks about the tenant it - // projected from a verified identity; it sits beside - // PermissionService.ListAccessibleScopes for that reason, the two read together - // to answer what a viewer's organization installed and what that viewer was - // granted. Its declared module route's bearer-less arm asks about the tenant an - // edge-inspected Work Context names, a value this issuer signed, to admit a - // headless operation context only when the target is installed there; whether - // the context is one is WorkContextService/AdmitWorkloadOperation's answer. + // the tenant it asks about is the one the auth-gateway projected from a verified + // identity — never one a browser supplied. It sits beside + // PermissionService.ListAccessibleScopes for that reason: the two are read + // together to answer what a viewer's organization installed and what that viewer + // was granted. rpc ListInstallations(ListInstallationsRequest) returns (ListInstallationsResponse) { option (saas.policy.v1.method_policy) = { exposure: EXPOSURE_INTERNAL @@ -387,8 +496,7 @@ service InstallationService { // It is the org-admin companion to the internal ListInstallations, and it is a // separate method rather than a relaxed policy on that one: ListInstallations // is read by the gateway for an organisation it projected from a verified - // identity or that an edge-verified Work Context names, and a bare tenant - // credential must never reach it. This one binds + // identity, and a bare tenant credential must never reach it. This one binds // the organisation to the caller's own admin membership, exactly as // ListAvailableSolutions does, so no organisation's administrator can read // another's. The one exception is inherited, not added here: a platform diff --git a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto index dd6ffc5f5..64acd9ec3 100644 --- a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto +++ b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto @@ -8,7 +8,7 @@ import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; import "saas/accounts/v1/authorization.proto"; import "saas/accounts/v1/common.proto"; -import "saas/accounts/v1/installation_listing.proto"; +import "saas/accounts/v1/installations.proto"; import "saas/accounts/v1/solution_registry.proto"; import "saas/events/v1/operations.proto"; import "saas/jobs/v1/jobs.proto"; diff --git a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/workload_operation_admission.proto b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/workload_operation_admission.proto index 3bc644315..006f844b3 100644 --- a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/workload_operation_admission.proto +++ b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/workload_operation_admission.proto @@ -18,7 +18,7 @@ import "buf/validate/validate.proto"; // // They live in their own file, with deliberately minimal imports, so the // auth-gateway can generate the same types from the same source proto, as it -// does for installation_listing.proto and solution_entitlements.proto. The +// does for module_registration.proto and solution_entitlements.proto. The // gateway's copy is this file, byte for byte. // WorkloadOperationRefusal is why a capability was not admitted. The gateway diff --git a/module/contracts/api/accounts/connect/contract.binpb b/module/contracts/api/accounts/connect/contract.binpb index 99f9f12a78efb6ba8a5f898f5af449dec14b52d4..7ef3f1903569272098d3729e4fc7b7204c1c9ece 100644 GIT binary patch delta 15161 zcmZ8odz=-;u|D0?(|gX$z{0T0!UD@dd8oVvQ9wZi1T=~PRE$OfxCEln1Tf}a^ZOki ztKuUPym&`Z5Uxh#p@0fU5KuuLA`t}y0g;D_qNu1S@-p|U>Yka6f9wyw>h9{Q`l_pY zdgdJ7I_R#AgYKviHL}L3Q8jLjt|_U>*5qoum;LXbcc*?My_V6TO@6az=q7)B^v&m` z1ER)mNmX&e3w|Vv-QM+2xs`^$^(eeKtCqJ`_TZlQ@=;RT-SW@JW$PE*;! z^F2j~K7vl3C1#CG7X=i(zSMubv?bz81^I#|DeLZ^fE3jkJhuzm?ies#3w0ueCg0-$3@Yl@K6)=9Quzkmp|V=Dm&oz>PW zkkLYtU!CRYwkO=`EYGk#9rWznx{XET-Py9G?Fi@2mS@II=y-OUco@8Nx?nnDNl2GK zXWP+5$1WW^dGmxPOBDD%FEcvow!)uRI4@g9eH3WLi0(2=!wC`S5jR0|_naRi7~8#W zX^gT!^}2iYyc`Y7-j}P}5R&@m*>(Z7JrcE0)I;`)n?zV}dgNkF0(yId4dO=V?NN4t z)hn#t9_8oTc5<+1q8I-4l$XR!n5Ac~NsOSjXI1kUL2b|cAgfkZTD?764D^1Io})6k z(CH)VRDLO=!B5rA7aPCkUnvTG{J@OQ(Nd^|FfSpGMtY%hkx3@}AMv}$=WIt*9N5}7 zu@(08O>Ct!=$qK;P+MZF16%u&tp$#)V{PpVTMcCj+0ajVmNEwI<0eem N%+fF9 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 diff --git a/module/contracts/api/accounts/connect/proto/buf.yaml b/module/contracts/api/accounts/connect/proto/buf.yaml index 1a1cfc5ca..509da1b1f 100644 --- a/module/contracts/api/accounts/connect/proto/buf.yaml +++ b/module/contracts/api/accounts/connect/proto/buf.yaml @@ -94,23 +94,12 @@ breaking: # SolutionBackendBinding (#1118): the reported upstream half, deleted # with the report — a solution's address is resolved, never recorded. - saas/accounts/v1/solution_registry.proto - # Installation, ListInstallationsRequest, InstallationSummary and - # ListInstallationsResponse moved to installation_listing.proto so the - # auth-gateway can generate the ListInstallations messages from the same - # source file without vendoring the policy options and google.api this - # file imports. Same package, same fully-qualified names, same wire - # format; every RPC stays where it was. - - saas/accounts/v1/installations.proto # ModuleMintRegistrationRequest/Response and # SolutionMintRegistrationRequest/Response (#1056): the registration # credentials and the secrets that minted them are deleted. Presence is # delivered, so nothing registers at runtime and nothing verifies either # credential. - saas/accounts/v1/module_registration.proto - # InstallationStatus and InstallationHealth moved to - # installation_listing.proto with the messages above, for the same reason. - ENUM_NO_DELETE: - - saas/accounts/v1/installations.proto # ListMyAccessibleScopes moved from PermissionService to the new # AccessibleScopeService in accessible_scopes.proto (issue #577), which changes # its Connect/gRPC procedure path. Its REST route (GET /v1/accessible-scopes) is diff --git a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installation_listing.proto b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installation_listing.proto deleted file mode 100644 index 57150f92b..000000000 --- a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installation_listing.proto +++ /dev/null @@ -1,131 +0,0 @@ -syntax = "proto3"; - -package saas.accounts.v1; - -import "buf/validate/validate.proto"; -import "google/protobuf/timestamp.proto"; - -// The installation listing: an installation, its lifecycle and health, and the -// messages InstallationService/ListInstallations takes and answers. -// -// These definitions live in their own file, with deliberately minimal imports, -// so the auth-gateway can generate the same types from the same source proto -// and call ListInstallations over the internal listener, as it does for -// module_registration.proto and solution_entitlements.proto. installations.proto -// imports this file; the service and every other installation message stay -// there. Sharing the generated definition rather than restating the wire shape -// is what keeps the two sides from drifting: the gateway's copy is this file, -// byte for byte. - -// InstallationStatus is the lifecycle of an installation row. An uninstall is a -// soft delete (status=revoked, revoked_at set) so the agent, grants, and audit -// trail remain resolvable. -enum InstallationStatus { - INSTALLATION_STATUS_UNSPECIFIED = 0; - INSTALLATION_STATUS_ACTIVE = 1; - INSTALLATION_STATUS_REVOKED = 2; - // Installed, and waiting for the solution to acknowledge its standard install - // step. Nothing can use a pending installation. - INSTALLATION_STATUS_PENDING = 3; - // The solution refused its install step, or never answered within the - // attempt budget. Nothing can use it; an administrator may retry it. - INSTALLATION_STATUS_FAILED = 4; -} - -// InstallationHealth is resolved live from current authority facts, never -// stored. It is the observable form of the fail-closed mint rule: a -// StartInstallationTask fails exactly when health is not HEALTHY, and transfer -// or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record -// nor any co-owner is currently an org admin (the Zapier-class "connection owner -// left the org" failure), surfaced rather than silently reassigned. -enum InstallationHealth { - INSTALLATION_HEALTH_UNSPECIFIED = 0; - INSTALLATION_HEALTH_HEALTHY = 1; - INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER = 2; - INSTALLATION_HEALTH_AGENT_REVOKED = 3; - INSTALLATION_HEALTH_AGENT_DISABLED = 4; - INSTALLATION_HEALTH_STANDING_GRANT_MISSING = 5; -} - -// Installation is one solution's bound identity in an org. Server-assigned ids; -// the browser cannot attest to tenancy, the agent principal, or the scope node. -message Installation { - string id = 1 [(buf.validate.field).string.uuid = true]; - string org_id = 2 [(buf.validate.field).string.uuid = true]; - string agent_principal_id = 3 [(buf.validate.field).string.uuid = true]; - // Was `solution_identifier`: the free-text route alias. It was the join key - // every reader used, and because an alias is deliberately reusable by a later - // binding, an installation that named one inherited whatever took it next. - // Replaced by target_id below. Reserved rather than reused so no consumer can - // read an alias out of a field that now carries an identity. - reserved 4; - reserved "solution_identifier"; - string owner_principal_id = 5 [(buf.validate.field).string.uuid = true]; - repeated string co_owner_principal_ids = 6; - string root_scope_node_id = 7 [(buf.validate.field).string.uuid = true]; - InstallationStatus status = 8; - google.protobuf.Timestamp created_at = 9; - optional google.protobuf.Timestamp revoked_at = 10; - // The immutable solution target this installation was consented to: one - // continuous period of one binding's presence on this host. It is never - // reused, so a replacement binding — even one claiming the same route alias — - // is a different target and inherits nothing. - // - // Deliberately NOT accompanied by the route alias. The alias is a property of - // the target that a later generation may move, and resolving it here would - // mean joining the host's presence state into a request-path listing — which - // would require granting request traffic read over every organisation's - // presence to answer about one. A consumer that needs the alias asks - // ListAvailableSolutions, which serves accepted applied state, and joins on - // this id. - // - // Empty only on a revoked historical row whose target could not be - // established at the cutover; an active installation always names one. - string target_id = 11 [ - (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, - (buf.validate.field).string.uuid = true - ]; - // Why a revoked installation was revoked, when it was not an administrator - // uninstalling: the installation-identity cutover, or a tenancy narrowing that - // withdrew this organisation. Empty on an active row. - string revoked_reason = 12 [(buf.validate.field).string.max_len = 512]; - // Why the install step failed: the solution's bounded refusal reason, or a - // fixed reason when it never answered. Empty unless status is FAILED. - string failure_reason = 13 [(buf.validate.field).string.max_len = 512]; -} - -// ListInstallationsRequest asks what one organization has installed. Every other -// RPC on this service names a single installation, so until this existed nothing -// could answer "what has this org installed?" — which is the authority read a -// per-organization solution projection is built on (issue #949). -message ListInstallationsRequest { - string org_id = 1 [(buf.validate.field).string.uuid = true]; - // Restrict to one lifecycle status. UNSPECIFIED returns every status, so a - // caller that must see only live installs asks for ACTIVE explicitly rather - // than leaning on a default that would silently widen if the enum grew. - InstallationStatus status = 2; - // Page bound. 0 -> server default; capped so an organization with many - // installations can never return an unbounded result in one call. - int32 page_size = 3 [(buf.validate.field).int32 = { - gte: 0 - lte: 500 - }]; - // Opaque cursor from a previous response's next_page_token; empty for page one. - string page_token = 4; -} - -// InstallationSummary is one row of the listing: the installation, plus the health -// resolved live beside it exactly as GetInstallation resolves it for one. Health -// travels with the row because a consumer that renders an installed solution as -// unavailable would otherwise need one GetInstallation per installation. -message InstallationSummary { - Installation installation = 1; - InstallationHealth health = 2; -} - -message ListInstallationsResponse { - repeated InstallationSummary installations = 1; - // Set when more installations remain; pass it back as page_token. Empty on the - // last page. - string next_page_token = 2; -} diff --git a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installations.proto b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installations.proto index 9fc95dd86..82739f9de 100644 --- a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installations.proto +++ b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installations.proto @@ -5,8 +5,8 @@ package saas.accounts.v1; import "buf/validate/validate.proto"; import "google/api/annotations.proto"; import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; import "saas/accounts/v1/authorization.proto"; -import "saas/accounts/v1/installation_listing.proto"; import "saas/policy/v1/options.proto"; // ============================================================================ @@ -22,6 +22,83 @@ import "saas/policy/v1/options.proto"; // (StartInstallationTask) beside the other mints. // ============================================================================ +// InstallationStatus is the lifecycle of an installation row. An uninstall is a +// soft delete (status=revoked, revoked_at set) so the agent, grants, and audit +// trail remain resolvable. +enum InstallationStatus { + INSTALLATION_STATUS_UNSPECIFIED = 0; + INSTALLATION_STATUS_ACTIVE = 1; + INSTALLATION_STATUS_REVOKED = 2; + // Installed, and waiting for the solution to acknowledge its standard install + // step. Nothing can use a pending installation. + INSTALLATION_STATUS_PENDING = 3; + // The solution refused its install step, or never answered within the + // attempt budget. Nothing can use it; an administrator may retry it. + INSTALLATION_STATUS_FAILED = 4; +} + +// InstallationHealth is resolved live from current authority facts, never +// stored. It is the observable form of the fail-closed mint rule: a +// StartInstallationTask fails exactly when health is not HEALTHY, and transfer +// or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record +// nor any co-owner is currently an org admin (the Zapier-class "connection owner +// left the org" failure), surfaced rather than silently reassigned. +enum InstallationHealth { + INSTALLATION_HEALTH_UNSPECIFIED = 0; + INSTALLATION_HEALTH_HEALTHY = 1; + INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER = 2; + INSTALLATION_HEALTH_AGENT_REVOKED = 3; + INSTALLATION_HEALTH_AGENT_DISABLED = 4; + INSTALLATION_HEALTH_STANDING_GRANT_MISSING = 5; +} + +// Installation is one solution's bound identity in an org. Server-assigned ids; +// the browser cannot attest to tenancy, the agent principal, or the scope node. +message Installation { + string id = 1 [(buf.validate.field).string.uuid = true]; + string org_id = 2 [(buf.validate.field).string.uuid = true]; + string agent_principal_id = 3 [(buf.validate.field).string.uuid = true]; + // Was `solution_identifier`: the free-text route alias. It was the join key + // every reader used, and because an alias is deliberately reusable by a later + // binding, an installation that named one inherited whatever took it next. + // Replaced by target_id below. Reserved rather than reused so no consumer can + // read an alias out of a field that now carries an identity. + reserved 4; + reserved "solution_identifier"; + string owner_principal_id = 5 [(buf.validate.field).string.uuid = true]; + repeated string co_owner_principal_ids = 6; + string root_scope_node_id = 7 [(buf.validate.field).string.uuid = true]; + InstallationStatus status = 8; + google.protobuf.Timestamp created_at = 9; + optional google.protobuf.Timestamp revoked_at = 10; + // The immutable solution target this installation was consented to: one + // continuous period of one binding's presence on this host. It is never + // reused, so a replacement binding — even one claiming the same route alias — + // is a different target and inherits nothing. + // + // Deliberately NOT accompanied by the route alias. The alias is a property of + // the target that a later generation may move, and resolving it here would + // mean joining the host's presence state into a request-path listing — which + // would require granting request traffic read over every organisation's + // presence to answer about one. A consumer that needs the alias asks + // ListAvailableSolutions, which serves accepted applied state, and joins on + // this id. + // + // Empty only on a revoked historical row whose target could not be + // established at the cutover; an active installation always names one. + string target_id = 11 [ + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + (buf.validate.field).string.uuid = true + ]; + // Why a revoked installation was revoked, when it was not an administrator + // uninstalling: the installation-identity cutover, or a tenancy narrowing that + // withdrew this organisation. Empty on an active row. + string revoked_reason = 12 [(buf.validate.field).string.max_len = 512]; + // Why the install step failed: the solution's bounded refusal reason, or a + // fixed reason when it never answered. Empty unless status is FAILED. + string failure_reason = 13 [(buf.validate.field).string.max_len = 512]; +} + message InstallSolutionRequest { string org_id = 1 [(buf.validate.field).string.uuid = true]; // Canonical "publisher/name:version" of the solution's agent. The agent @@ -142,6 +219,42 @@ message GetInstallationResponse { InstallationHealth health = 2; } +// ListInstallationsRequest asks what one organization has installed. Every other +// RPC on this service names a single installation, so until this existed nothing +// could answer "what has this org installed?" — which is the authority read a +// per-organization solution projection is built on (issue #949). +message ListInstallationsRequest { + string org_id = 1 [(buf.validate.field).string.uuid = true]; + // Restrict to one lifecycle status. UNSPECIFIED returns every status, so a + // caller that must see only live installs asks for ACTIVE explicitly rather + // than leaning on a default that would silently widen if the enum grew. + InstallationStatus status = 2; + // Page bound. 0 -> server default; capped so an organization with many + // installations can never return an unbounded result in one call. + int32 page_size = 3 [(buf.validate.field).int32 = { + gte: 0 + lte: 500 + }]; + // Opaque cursor from a previous response's next_page_token; empty for page one. + string page_token = 4; +} + +// InstallationSummary is one row of the listing: the installation, plus the health +// resolved live beside it exactly as GetInstallation resolves it for one. Health +// travels with the row because a consumer that renders an installed solution as +// unavailable would otherwise need one GetInstallation per installation. +message InstallationSummary { + Installation installation = 1; + InstallationHealth health = 2; +} + +message ListInstallationsResponse { + repeated InstallationSummary installations = 1; + // Set when more installations remain; pass it back as page_token. Empty on the + // last page. + string next_page_token = 2; +} + // InstallationService composes and administers installations. The headless mint // that turns an installation into a short-lived Work Context is // WorkContextService.StartInstallationTask. @@ -318,15 +431,11 @@ service InstallationService { // ListInstallations enumerates one organization's installations with their live // health. It is internal-tier on purpose: it takes the organization as a request // field, so only a caller holding the cluster-internal credential may ask, and - // the tenant it asks about is never one a browser supplied. The auth-gateway is - // its caller, for two reads. Its viewer projection asks about the tenant it - // projected from a verified identity; it sits beside - // PermissionService.ListAccessibleScopes for that reason, the two read together - // to answer what a viewer's organization installed and what that viewer was - // granted. Its declared module route's bearer-less arm asks about the tenant an - // edge-inspected Work Context names, a value this issuer signed, to admit a - // headless operation context only when the target is installed there; whether - // the context is one is WorkContextService/AdmitWorkloadOperation's answer. + // the tenant it asks about is the one the auth-gateway projected from a verified + // identity — never one a browser supplied. It sits beside + // PermissionService.ListAccessibleScopes for that reason: the two are read + // together to answer what a viewer's organization installed and what that viewer + // was granted. rpc ListInstallations(ListInstallationsRequest) returns (ListInstallationsResponse) { option (saas.policy.v1.method_policy) = { exposure: EXPOSURE_INTERNAL @@ -387,8 +496,7 @@ service InstallationService { // It is the org-admin companion to the internal ListInstallations, and it is a // separate method rather than a relaxed policy on that one: ListInstallations // is read by the gateway for an organisation it projected from a verified - // identity or that an edge-verified Work Context names, and a bare tenant - // credential must never reach it. This one binds + // identity, and a bare tenant credential must never reach it. This one binds // the organisation to the caller's own admin membership, exactly as // ListAvailableSolutions does, so no organisation's administrator can read // another's. The one exception is inherited, not added here: a platform diff --git a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto index dd6ffc5f5..64acd9ec3 100644 --- a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto +++ b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto @@ -8,7 +8,7 @@ import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; import "saas/accounts/v1/authorization.proto"; import "saas/accounts/v1/common.proto"; -import "saas/accounts/v1/installation_listing.proto"; +import "saas/accounts/v1/installations.proto"; import "saas/accounts/v1/solution_registry.proto"; import "saas/events/v1/operations.proto"; import "saas/jobs/v1/jobs.proto"; diff --git a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/workload_operation_admission.proto b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/workload_operation_admission.proto index 3bc644315..006f844b3 100644 --- a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/workload_operation_admission.proto +++ b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/workload_operation_admission.proto @@ -18,7 +18,7 @@ import "buf/validate/validate.proto"; // // They live in their own file, with deliberately minimal imports, so the // auth-gateway can generate the same types from the same source proto, as it -// does for installation_listing.proto and solution_entitlements.proto. The +// does for module_registration.proto and solution_entitlements.proto. The // gateway's copy is this file, byte for byte. // WorkloadOperationRefusal is why a capability was not admitted. The gateway diff --git a/module/contracts/api/auth-gateway/grpc/contract.binpb b/module/contracts/api/auth-gateway/grpc/contract.binpb index 3f8e8923f50defc74bb30819f87d6edfafc7fb1f..4ca65c037a32d0b2aa549abb1320121238cb6c3f 100644 GIT binary patch delta 72 zcmV-O0Js14i59oV6@Y{RgaU*Ev;^0Qw-u`d`+WhAw-G4?v;zs!9ts8$1ONb3m#&Zm eApzT$@GJ#{0oJ#5Ed^@>mlu-+JGUG$1=>^8wHaXm delta 7580 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�MV4EfcZ&0jxS|fwE(dqWv5a=;V>FbN%T5tAs)!#`wNhmg zNCcJ$rRcjtcF(1BtHh+-M*l>$@dT#Fdqqi=Lm^74{3?S1ynzQzLn8|SeT2iNbRPz8 zl%isa3~)buSS*SvMVwu&y5tL~ud*5yb86#ca5%vs;KN6Ogy97beuSCHB=Q}JV+Vkq zgF%c&T-*YnnM4I%BViV5;Ijy67!V>kVLVUQDkxU0MKr4^91XN4M?dV-50{99MSBSm zg;12h0+OiWlSTSXW%CJz^qX40k*${fJ)1$obzVrZc9ubgXHoGs=Tp|{6ek! zaVx0_3>kX~8T$by2^>KUzGN16W-CzSDX`4tiV<&(ICJ10(6|FV~eHv6sfml>NY7V5KhOcDDQdXts2W`99WH8>P ztQ<8-Ss3q9*A{I`hMIS2>zGCjjJgG_Z#%2kbFkE9Sn4~;4@%H^*#bj?B6%MD#~TGz|a^(_+^7lT@B~XPgBCkOd`;Mw-FuAHf9WO4%1q57z_~n zz#BduapVzkjUFy!5>ZX1#y~lxi$jmRd0v7r=uQJs(p^O9M9;kvMV2Aw+?ys9qCv^Q z$fTm@x~iawn^27Wr}YGd)3Y_lB_(;U6NWXxA3Sv6P#?@ppoK=8Qbx7q7;_IoL}(}= zMOv|XEMF;a@tfxpSo@g;H`V1wlteJS`vTLxSWUqQgW6WHn{O69E*@aiD zppA$mXQ3?&=V+`03Wjqu*1=E+d%I>EdxZ>>0iCy8)35*e=hiE+n^E8mUvb7=w_esr zP^gja2H!%SXJAa44pR8C(ND?D*I&T6VzK5gVB%Q{#u^fkUWKEdln%ktRkO3}96wjJ zksLIuhxJe1u_ofHSvBk61Z~=|k-bCt1v#&^e9h>3-AtaAWCZF*bP8F*B-B;(QwBZa z!I<|K&jDU_k^U6n>XniZJGFjfj_aV|p#g<3Fx8RdVFO8XRxJAmM{t{iNG%(Ey5`wQm)7_}J|iwV=${@s zyHtLqgvi!qGkj^*IQR6hL-9?T6{2xts>CL^Ectd!Y;QQ}!(>6j)UkVMJH z1R`+Pv$<`user<{XSb_S0uPbSKV#gw`(9i6 qwUqH*{l#3m&-k!@JD0v;ynpwLtI}6e>zeCz@~_pqKWa~#)Bgn@yY58* diff --git a/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/installation_listing.proto b/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/installation_listing.proto deleted file mode 100644 index 57150f92b..000000000 --- a/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/installation_listing.proto +++ /dev/null @@ -1,131 +0,0 @@ -syntax = "proto3"; - -package saas.accounts.v1; - -import "buf/validate/validate.proto"; -import "google/protobuf/timestamp.proto"; - -// The installation listing: an installation, its lifecycle and health, and the -// messages InstallationService/ListInstallations takes and answers. -// -// These definitions live in their own file, with deliberately minimal imports, -// so the auth-gateway can generate the same types from the same source proto -// and call ListInstallations over the internal listener, as it does for -// module_registration.proto and solution_entitlements.proto. installations.proto -// imports this file; the service and every other installation message stay -// there. Sharing the generated definition rather than restating the wire shape -// is what keeps the two sides from drifting: the gateway's copy is this file, -// byte for byte. - -// InstallationStatus is the lifecycle of an installation row. An uninstall is a -// soft delete (status=revoked, revoked_at set) so the agent, grants, and audit -// trail remain resolvable. -enum InstallationStatus { - INSTALLATION_STATUS_UNSPECIFIED = 0; - INSTALLATION_STATUS_ACTIVE = 1; - INSTALLATION_STATUS_REVOKED = 2; - // Installed, and waiting for the solution to acknowledge its standard install - // step. Nothing can use a pending installation. - INSTALLATION_STATUS_PENDING = 3; - // The solution refused its install step, or never answered within the - // attempt budget. Nothing can use it; an administrator may retry it. - INSTALLATION_STATUS_FAILED = 4; -} - -// InstallationHealth is resolved live from current authority facts, never -// stored. It is the observable form of the fail-closed mint rule: a -// StartInstallationTask fails exactly when health is not HEALTHY, and transfer -// or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record -// nor any co-owner is currently an org admin (the Zapier-class "connection owner -// left the org" failure), surfaced rather than silently reassigned. -enum InstallationHealth { - INSTALLATION_HEALTH_UNSPECIFIED = 0; - INSTALLATION_HEALTH_HEALTHY = 1; - INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER = 2; - INSTALLATION_HEALTH_AGENT_REVOKED = 3; - INSTALLATION_HEALTH_AGENT_DISABLED = 4; - INSTALLATION_HEALTH_STANDING_GRANT_MISSING = 5; -} - -// Installation is one solution's bound identity in an org. Server-assigned ids; -// the browser cannot attest to tenancy, the agent principal, or the scope node. -message Installation { - string id = 1 [(buf.validate.field).string.uuid = true]; - string org_id = 2 [(buf.validate.field).string.uuid = true]; - string agent_principal_id = 3 [(buf.validate.field).string.uuid = true]; - // Was `solution_identifier`: the free-text route alias. It was the join key - // every reader used, and because an alias is deliberately reusable by a later - // binding, an installation that named one inherited whatever took it next. - // Replaced by target_id below. Reserved rather than reused so no consumer can - // read an alias out of a field that now carries an identity. - reserved 4; - reserved "solution_identifier"; - string owner_principal_id = 5 [(buf.validate.field).string.uuid = true]; - repeated string co_owner_principal_ids = 6; - string root_scope_node_id = 7 [(buf.validate.field).string.uuid = true]; - InstallationStatus status = 8; - google.protobuf.Timestamp created_at = 9; - optional google.protobuf.Timestamp revoked_at = 10; - // The immutable solution target this installation was consented to: one - // continuous period of one binding's presence on this host. It is never - // reused, so a replacement binding — even one claiming the same route alias — - // is a different target and inherits nothing. - // - // Deliberately NOT accompanied by the route alias. The alias is a property of - // the target that a later generation may move, and resolving it here would - // mean joining the host's presence state into a request-path listing — which - // would require granting request traffic read over every organisation's - // presence to answer about one. A consumer that needs the alias asks - // ListAvailableSolutions, which serves accepted applied state, and joins on - // this id. - // - // Empty only on a revoked historical row whose target could not be - // established at the cutover; an active installation always names one. - string target_id = 11 [ - (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, - (buf.validate.field).string.uuid = true - ]; - // Why a revoked installation was revoked, when it was not an administrator - // uninstalling: the installation-identity cutover, or a tenancy narrowing that - // withdrew this organisation. Empty on an active row. - string revoked_reason = 12 [(buf.validate.field).string.max_len = 512]; - // Why the install step failed: the solution's bounded refusal reason, or a - // fixed reason when it never answered. Empty unless status is FAILED. - string failure_reason = 13 [(buf.validate.field).string.max_len = 512]; -} - -// ListInstallationsRequest asks what one organization has installed. Every other -// RPC on this service names a single installation, so until this existed nothing -// could answer "what has this org installed?" — which is the authority read a -// per-organization solution projection is built on (issue #949). -message ListInstallationsRequest { - string org_id = 1 [(buf.validate.field).string.uuid = true]; - // Restrict to one lifecycle status. UNSPECIFIED returns every status, so a - // caller that must see only live installs asks for ACTIVE explicitly rather - // than leaning on a default that would silently widen if the enum grew. - InstallationStatus status = 2; - // Page bound. 0 -> server default; capped so an organization with many - // installations can never return an unbounded result in one call. - int32 page_size = 3 [(buf.validate.field).int32 = { - gte: 0 - lte: 500 - }]; - // Opaque cursor from a previous response's next_page_token; empty for page one. - string page_token = 4; -} - -// InstallationSummary is one row of the listing: the installation, plus the health -// resolved live beside it exactly as GetInstallation resolves it for one. Health -// travels with the row because a consumer that renders an installed solution as -// unavailable would otherwise need one GetInstallation per installation. -message InstallationSummary { - Installation installation = 1; - InstallationHealth health = 2; -} - -message ListInstallationsResponse { - repeated InstallationSummary installations = 1; - // Set when more installations remain; pass it back as page_token. Empty on the - // last page. - string next_page_token = 2; -} diff --git a/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/workload_operation_admission.proto b/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/workload_operation_admission.proto index 3bc644315..006f844b3 100644 --- a/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/workload_operation_admission.proto +++ b/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/workload_operation_admission.proto @@ -18,7 +18,7 @@ import "buf/validate/validate.proto"; // // They live in their own file, with deliberately minimal imports, so the // auth-gateway can generate the same types from the same source proto, as it -// does for installation_listing.proto and solution_entitlements.proto. The +// does for module_registration.proto and solution_entitlements.proto. The // gateway's copy is this file, byte for byte. // WorkloadOperationRefusal is why a capability was not admitted. The gateway diff --git a/module/contracts/api/catalog.codefly.json b/module/contracts/api/catalog.codefly.json index d440ad33a..378e7c0b6 100644 --- a/module/contracts/api/catalog.codefly.json +++ b/module/contracts/api/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/authority/contract.binpb", - "digest": "sha256:c9df546fead8be738783f6fba95b7a35c8037576c2e6362077f7e44406ca3e32", + "digest": "sha256:978af58084c99638d6b0d979ea5991d78439f6b4d5deca0d34ff4d547e675d0c", "services": [ { "name": "ModuleCapabilitiesService", @@ -65,7 +65,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/connect/contract.binpb", - "digest": "sha256:c9df546fead8be738783f6fba95b7a35c8037576c2e6362077f7e44406ca3e32", + "digest": "sha256:978af58084c99638d6b0d979ea5991d78439f6b4d5deca0d34ff4d547e675d0c", "services": [ { "name": "APIKeyService", @@ -563,7 +563,7 @@ "kind": "protobuf", "package": "saas.gateway.auth.v1", "path": "contracts/api/auth-gateway/grpc/contract.binpb", - "digest": "sha256:d290f52b25016e8100c42d07278e1ee1504fafaaa9cef1fe1fe7a607cfc17617", + "digest": "sha256:9d11dfc0427a0d24d14071347ccb12db8c8f1a94cfcb51713c5afad9fbea8ba9", "services": [ { "name": "AuthGatewayService", diff --git a/module/module.package.codefly.yaml b/module/module.package.codefly.yaml index 53dff4323..72ab953db 100644 --- a/module/module.package.codefly.yaml +++ b/module/module.package.codefly.yaml @@ -18,12 +18,12 @@ services: kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/authority/contract.binpb - digest: sha256:c9df546fead8be738783f6fba95b7a35c8037576c2e6362077f7e44406ca3e32 + digest: sha256:978af58084c99638d6b0d979ea5991d78439f6b4d5deca0d34ff4d547e675d0c - endpoint: connect kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/connect/contract.binpb - digest: sha256:c9df546fead8be738783f6fba95b7a35c8037576c2e6362077f7e44406ca3e32 + digest: sha256:978af58084c99638d6b0d979ea5991d78439f6b4d5deca0d34ff4d547e675d0c - name: auth-gateway endpoints: [grpc, rest] api-contracts: @@ -31,7 +31,7 @@ services: kind: protobuf package: saas.gateway.auth.v1 path: contracts/api/auth-gateway/grpc/contract.binpb - digest: sha256:d290f52b25016e8100c42d07278e1ee1504fafaaa9cef1fe1fe7a607cfc17617 + digest: sha256:9d11dfc0427a0d24d14071347ccb12db8c8f1a94cfcb51713c5afad9fbea8ba9 - name: cache endpoints: [read, write] - name: frontend diff --git a/module/services/accounts/code/pkg/adapters/installation_rpcs.go b/module/services/accounts/code/pkg/adapters/installation_rpcs.go index ff6f07db6..44a87309e 100644 --- a/module/services/accounts/code/pkg/adapters/installation_rpcs.go +++ b/module/services/accounts/code/pkg/adapters/installation_rpcs.go @@ -134,11 +134,8 @@ func (s *InstallationServer) GetInstallation(ctx context.Context, req *gen.GetIn // ListInstallations enumerates one organization's installations. Internal-tier, // like the scope listing it is read beside: the organization is a request field, -// so a bare tenant JWT must never be able to ask this. The caller that may is -// the auth-gateway, naming either the tenant it projected from a verified -// identity (its viewer projection) or the tenant an edge-inspected Work Context -// names (its declared module route's bearer-less arm, which asks whether the -// target is installed there), never one a request chose. +// so a bare tenant JWT must never be able to ask this — the caller that may is +// the auth-gateway, naming the tenant it projected from a verified identity. func (s *InstallationServer) ListInstallations(ctx context.Context, req *gen.ListInstallationsRequest) (*gen.ListInstallationsResponse, error) { if err := Validate(req); err != nil { return nil, err diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/installations.connect.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/installations.connect.go index dd97d47e2..7e6285a5f 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/installations.connect.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/installations.connect.go @@ -100,15 +100,11 @@ type InstallationServiceClient interface { // ListInstallations enumerates one organization's installations with their live // health. It is internal-tier on purpose: it takes the organization as a request // field, so only a caller holding the cluster-internal credential may ask, and - // the tenant it asks about is never one a browser supplied. The auth-gateway is - // its caller, for two reads. Its viewer projection asks about the tenant it - // projected from a verified identity; it sits beside - // PermissionService.ListAccessibleScopes for that reason, the two read together - // to answer what a viewer's organization installed and what that viewer was - // granted. Its declared module route's bearer-less arm asks about the tenant an - // edge-inspected Work Context names, a value this issuer signed, to admit a - // headless operation context only when the target is installed there; whether - // the context is one is WorkContextService/AdmitWorkloadOperation's answer. + // the tenant it asks about is the one the auth-gateway projected from a verified + // identity — never one a browser supplied. It sits beside + // PermissionService.ListAccessibleScopes for that reason: the two are read + // together to answer what a viewer's organization installed and what that viewer + // was granted. ListInstallations(context.Context, *connect.Request[v1.ListInstallationsRequest]) (*connect.Response[v1.ListInstallationsResponse], error) // ListAvailableSolutions is the catalogue: the solution targets an // administrator may install right now. @@ -133,8 +129,7 @@ type InstallationServiceClient interface { // It is the org-admin companion to the internal ListInstallations, and it is a // separate method rather than a relaxed policy on that one: ListInstallations // is read by the gateway for an organisation it projected from a verified - // identity or that an edge-verified Work Context names, and a bare tenant - // credential must never reach it. This one binds + // identity, and a bare tenant credential must never reach it. This one binds // the organisation to the caller's own admin membership, exactly as // ListAvailableSolutions does, so no organisation's administrator can read // another's. The one exception is inherited, not added here: a platform @@ -347,15 +342,11 @@ type InstallationServiceHandler interface { // ListInstallations enumerates one organization's installations with their live // health. It is internal-tier on purpose: it takes the organization as a request // field, so only a caller holding the cluster-internal credential may ask, and - // the tenant it asks about is never one a browser supplied. The auth-gateway is - // its caller, for two reads. Its viewer projection asks about the tenant it - // projected from a verified identity; it sits beside - // PermissionService.ListAccessibleScopes for that reason, the two read together - // to answer what a viewer's organization installed and what that viewer was - // granted. Its declared module route's bearer-less arm asks about the tenant an - // edge-inspected Work Context names, a value this issuer signed, to admit a - // headless operation context only when the target is installed there; whether - // the context is one is WorkContextService/AdmitWorkloadOperation's answer. + // the tenant it asks about is the one the auth-gateway projected from a verified + // identity — never one a browser supplied. It sits beside + // PermissionService.ListAccessibleScopes for that reason: the two are read + // together to answer what a viewer's organization installed and what that viewer + // was granted. ListInstallations(context.Context, *connect.Request[v1.ListInstallationsRequest]) (*connect.Response[v1.ListInstallationsResponse], error) // ListAvailableSolutions is the catalogue: the solution targets an // administrator may install right now. @@ -380,8 +371,7 @@ type InstallationServiceHandler interface { // It is the org-admin companion to the internal ListInstallations, and it is a // separate method rather than a relaxed policy on that one: ListInstallations // is read by the gateway for an organisation it projected from a verified - // identity or that an edge-verified Work Context names, and a bare tenant - // credential must never reach it. This one binds + // identity, and a bare tenant credential must never reach it. This one binds // the organisation to the caller's own admin membership, exactly as // ListAvailableSolutions does, so no organisation's administrator can read // another's. The one exception is inherited, not added here: a platform diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go deleted file mode 100644 index 10897223b..000000000 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go +++ /dev/null @@ -1,607 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc (unknown) -// source: saas/accounts/v1/installation_listing.proto - -package accountsv1 - -import ( - reflect "reflect" - sync "sync" - unsafe "unsafe" - - _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - timestamppb "google.golang.org/protobuf/types/known/timestamppb" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -// InstallationStatus is the lifecycle of an installation row. An uninstall is a -// soft delete (status=revoked, revoked_at set) so the agent, grants, and audit -// trail remain resolvable. -type InstallationStatus int32 - -const ( - InstallationStatus_INSTALLATION_STATUS_UNSPECIFIED InstallationStatus = 0 - InstallationStatus_INSTALLATION_STATUS_ACTIVE InstallationStatus = 1 - InstallationStatus_INSTALLATION_STATUS_REVOKED InstallationStatus = 2 - // Installed, and waiting for the solution to acknowledge its standard install - // step. Nothing can use a pending installation. - InstallationStatus_INSTALLATION_STATUS_PENDING InstallationStatus = 3 - // The solution refused its install step, or never answered within the - // attempt budget. Nothing can use it; an administrator may retry it. - InstallationStatus_INSTALLATION_STATUS_FAILED InstallationStatus = 4 -) - -// Enum value maps for InstallationStatus. -var ( - InstallationStatus_name = map[int32]string{ - 0: "INSTALLATION_STATUS_UNSPECIFIED", - 1: "INSTALLATION_STATUS_ACTIVE", - 2: "INSTALLATION_STATUS_REVOKED", - 3: "INSTALLATION_STATUS_PENDING", - 4: "INSTALLATION_STATUS_FAILED", - } - InstallationStatus_value = map[string]int32{ - "INSTALLATION_STATUS_UNSPECIFIED": 0, - "INSTALLATION_STATUS_ACTIVE": 1, - "INSTALLATION_STATUS_REVOKED": 2, - "INSTALLATION_STATUS_PENDING": 3, - "INSTALLATION_STATUS_FAILED": 4, - } -) - -func (x InstallationStatus) Enum() *InstallationStatus { - p := new(InstallationStatus) - *p = x - return p -} - -func (x InstallationStatus) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (InstallationStatus) Descriptor() protoreflect.EnumDescriptor { - return file_saas_accounts_v1_installation_listing_proto_enumTypes[0].Descriptor() -} - -func (InstallationStatus) Type() protoreflect.EnumType { - return &file_saas_accounts_v1_installation_listing_proto_enumTypes[0] -} - -func (x InstallationStatus) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use InstallationStatus.Descriptor instead. -func (InstallationStatus) EnumDescriptor() ([]byte, []int) { - return file_saas_accounts_v1_installation_listing_proto_rawDescGZIP(), []int{0} -} - -// InstallationHealth is resolved live from current authority facts, never -// stored. It is the observable form of the fail-closed mint rule: a -// StartInstallationTask fails exactly when health is not HEALTHY, and transfer -// or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record -// nor any co-owner is currently an org admin (the Zapier-class "connection owner -// left the org" failure), surfaced rather than silently reassigned. -type InstallationHealth int32 - -const ( - InstallationHealth_INSTALLATION_HEALTH_UNSPECIFIED InstallationHealth = 0 - InstallationHealth_INSTALLATION_HEALTH_HEALTHY InstallationHealth = 1 - InstallationHealth_INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER InstallationHealth = 2 - InstallationHealth_INSTALLATION_HEALTH_AGENT_REVOKED InstallationHealth = 3 - InstallationHealth_INSTALLATION_HEALTH_AGENT_DISABLED InstallationHealth = 4 - InstallationHealth_INSTALLATION_HEALTH_STANDING_GRANT_MISSING InstallationHealth = 5 -) - -// Enum value maps for InstallationHealth. -var ( - InstallationHealth_name = map[int32]string{ - 0: "INSTALLATION_HEALTH_UNSPECIFIED", - 1: "INSTALLATION_HEALTH_HEALTHY", - 2: "INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER", - 3: "INSTALLATION_HEALTH_AGENT_REVOKED", - 4: "INSTALLATION_HEALTH_AGENT_DISABLED", - 5: "INSTALLATION_HEALTH_STANDING_GRANT_MISSING", - } - InstallationHealth_value = map[string]int32{ - "INSTALLATION_HEALTH_UNSPECIFIED": 0, - "INSTALLATION_HEALTH_HEALTHY": 1, - "INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER": 2, - "INSTALLATION_HEALTH_AGENT_REVOKED": 3, - "INSTALLATION_HEALTH_AGENT_DISABLED": 4, - "INSTALLATION_HEALTH_STANDING_GRANT_MISSING": 5, - } -) - -func (x InstallationHealth) Enum() *InstallationHealth { - p := new(InstallationHealth) - *p = x - return p -} - -func (x InstallationHealth) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (InstallationHealth) Descriptor() protoreflect.EnumDescriptor { - return file_saas_accounts_v1_installation_listing_proto_enumTypes[1].Descriptor() -} - -func (InstallationHealth) Type() protoreflect.EnumType { - return &file_saas_accounts_v1_installation_listing_proto_enumTypes[1] -} - -func (x InstallationHealth) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use InstallationHealth.Descriptor instead. -func (InstallationHealth) EnumDescriptor() ([]byte, []int) { - return file_saas_accounts_v1_installation_listing_proto_rawDescGZIP(), []int{1} -} - -// Installation is one solution's bound identity in an org. Server-assigned ids; -// the browser cannot attest to tenancy, the agent principal, or the scope node. -type Installation struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - OrgId string `protobuf:"bytes,2,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` - AgentPrincipalId string `protobuf:"bytes,3,opt,name=agent_principal_id,json=agentPrincipalId,proto3" json:"agent_principal_id,omitempty"` - OwnerPrincipalId string `protobuf:"bytes,5,opt,name=owner_principal_id,json=ownerPrincipalId,proto3" json:"owner_principal_id,omitempty"` - CoOwnerPrincipalIds []string `protobuf:"bytes,6,rep,name=co_owner_principal_ids,json=coOwnerPrincipalIds,proto3" json:"co_owner_principal_ids,omitempty"` - RootScopeNodeId string `protobuf:"bytes,7,opt,name=root_scope_node_id,json=rootScopeNodeId,proto3" json:"root_scope_node_id,omitempty"` - Status InstallationStatus `protobuf:"varint,8,opt,name=status,proto3,enum=saas.accounts.v1.InstallationStatus" json:"status,omitempty"` - CreatedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` - RevokedAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=revoked_at,json=revokedAt,proto3,oneof" json:"revoked_at,omitempty"` - // The immutable solution target this installation was consented to: one - // continuous period of one binding's presence on this host. It is never - // reused, so a replacement binding — even one claiming the same route alias — - // is a different target and inherits nothing. - // - // Deliberately NOT accompanied by the route alias. The alias is a property of - // the target that a later generation may move, and resolving it here would - // mean joining the host's presence state into a request-path listing — which - // would require granting request traffic read over every organisation's - // presence to answer about one. A consumer that needs the alias asks - // ListAvailableSolutions, which serves accepted applied state, and joins on - // this id. - // - // Empty only on a revoked historical row whose target could not be - // established at the cutover; an active installation always names one. - TargetId string `protobuf:"bytes,11,opt,name=target_id,json=targetId,proto3" json:"target_id,omitempty"` - // Why a revoked installation was revoked, when it was not an administrator - // uninstalling: the installation-identity cutover, or a tenancy narrowing that - // withdrew this organisation. Empty on an active row. - RevokedReason string `protobuf:"bytes,12,opt,name=revoked_reason,json=revokedReason,proto3" json:"revoked_reason,omitempty"` - // Why the install step failed: the solution's bounded refusal reason, or a - // fixed reason when it never answered. Empty unless status is FAILED. - FailureReason string `protobuf:"bytes,13,opt,name=failure_reason,json=failureReason,proto3" json:"failure_reason,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Installation) Reset() { - *x = Installation{} - mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Installation) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Installation) ProtoMessage() {} - -func (x *Installation) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Installation.ProtoReflect.Descriptor instead. -func (*Installation) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installation_listing_proto_rawDescGZIP(), []int{0} -} - -func (x *Installation) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *Installation) GetOrgId() string { - if x != nil { - return x.OrgId - } - return "" -} - -func (x *Installation) GetAgentPrincipalId() string { - if x != nil { - return x.AgentPrincipalId - } - return "" -} - -func (x *Installation) GetOwnerPrincipalId() string { - if x != nil { - return x.OwnerPrincipalId - } - return "" -} - -func (x *Installation) GetCoOwnerPrincipalIds() []string { - if x != nil { - return x.CoOwnerPrincipalIds - } - return nil -} - -func (x *Installation) GetRootScopeNodeId() string { - if x != nil { - return x.RootScopeNodeId - } - return "" -} - -func (x *Installation) GetStatus() InstallationStatus { - if x != nil { - return x.Status - } - return InstallationStatus_INSTALLATION_STATUS_UNSPECIFIED -} - -func (x *Installation) GetCreatedAt() *timestamppb.Timestamp { - if x != nil { - return x.CreatedAt - } - return nil -} - -func (x *Installation) GetRevokedAt() *timestamppb.Timestamp { - if x != nil { - return x.RevokedAt - } - return nil -} - -func (x *Installation) GetTargetId() string { - if x != nil { - return x.TargetId - } - return "" -} - -func (x *Installation) GetRevokedReason() string { - if x != nil { - return x.RevokedReason - } - return "" -} - -func (x *Installation) GetFailureReason() string { - if x != nil { - return x.FailureReason - } - return "" -} - -// ListInstallationsRequest asks what one organization has installed. Every other -// RPC on this service names a single installation, so until this existed nothing -// could answer "what has this org installed?" — which is the authority read a -// per-organization solution projection is built on (issue #949). -type ListInstallationsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` - // Restrict to one lifecycle status. UNSPECIFIED returns every status, so a - // caller that must see only live installs asks for ACTIVE explicitly rather - // than leaning on a default that would silently widen if the enum grew. - Status InstallationStatus `protobuf:"varint,2,opt,name=status,proto3,enum=saas.accounts.v1.InstallationStatus" json:"status,omitempty"` - // Page bound. 0 -> server default; capped so an organization with many - // installations can never return an unbounded result in one call. - PageSize int32 `protobuf:"varint,3,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` - // Opaque cursor from a previous response's next_page_token; empty for page one. - PageToken string `protobuf:"bytes,4,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListInstallationsRequest) Reset() { - *x = ListInstallationsRequest{} - mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListInstallationsRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListInstallationsRequest) ProtoMessage() {} - -func (x *ListInstallationsRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[1] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ListInstallationsRequest.ProtoReflect.Descriptor instead. -func (*ListInstallationsRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installation_listing_proto_rawDescGZIP(), []int{1} -} - -func (x *ListInstallationsRequest) GetOrgId() string { - if x != nil { - return x.OrgId - } - return "" -} - -func (x *ListInstallationsRequest) GetStatus() InstallationStatus { - if x != nil { - return x.Status - } - return InstallationStatus_INSTALLATION_STATUS_UNSPECIFIED -} - -func (x *ListInstallationsRequest) GetPageSize() int32 { - if x != nil { - return x.PageSize - } - return 0 -} - -func (x *ListInstallationsRequest) GetPageToken() string { - if x != nil { - return x.PageToken - } - return "" -} - -// InstallationSummary is one row of the listing: the installation, plus the health -// resolved live beside it exactly as GetInstallation resolves it for one. Health -// travels with the row because a consumer that renders an installed solution as -// unavailable would otherwise need one GetInstallation per installation. -type InstallationSummary struct { - state protoimpl.MessageState `protogen:"open.v1"` - Installation *Installation `protobuf:"bytes,1,opt,name=installation,proto3" json:"installation,omitempty"` - Health InstallationHealth `protobuf:"varint,2,opt,name=health,proto3,enum=saas.accounts.v1.InstallationHealth" json:"health,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *InstallationSummary) Reset() { - *x = InstallationSummary{} - mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *InstallationSummary) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*InstallationSummary) ProtoMessage() {} - -func (x *InstallationSummary) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[2] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use InstallationSummary.ProtoReflect.Descriptor instead. -func (*InstallationSummary) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installation_listing_proto_rawDescGZIP(), []int{2} -} - -func (x *InstallationSummary) GetInstallation() *Installation { - if x != nil { - return x.Installation - } - return nil -} - -func (x *InstallationSummary) GetHealth() InstallationHealth { - if x != nil { - return x.Health - } - return InstallationHealth_INSTALLATION_HEALTH_UNSPECIFIED -} - -type ListInstallationsResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Installations []*InstallationSummary `protobuf:"bytes,1,rep,name=installations,proto3" json:"installations,omitempty"` - // Set when more installations remain; pass it back as page_token. Empty on the - // last page. - NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListInstallationsResponse) Reset() { - *x = ListInstallationsResponse{} - mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListInstallationsResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListInstallationsResponse) ProtoMessage() {} - -func (x *ListInstallationsResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[3] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ListInstallationsResponse.ProtoReflect.Descriptor instead. -func (*ListInstallationsResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installation_listing_proto_rawDescGZIP(), []int{3} -} - -func (x *ListInstallationsResponse) GetInstallations() []*InstallationSummary { - if x != nil { - return x.Installations - } - return nil -} - -func (x *ListInstallationsResponse) GetNextPageToken() string { - if x != nil { - return x.NextPageToken - } - return "" -} - -var File_saas_accounts_v1_installation_listing_proto protoreflect.FileDescriptor - -const file_saas_accounts_v1_installation_listing_proto_rawDesc = "" + - "\n" + - "+saas/accounts/v1/installation_listing.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x94\x05\n" + - "\fInstallation\x12\x18\n" + - "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id\x12\x1f\n" + - "\x06org_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x126\n" + - "\x12agent_principal_id\x18\x03 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x10agentPrincipalId\x126\n" + - "\x12owner_principal_id\x18\x05 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x10ownerPrincipalId\x123\n" + - "\x16co_owner_principal_ids\x18\x06 \x03(\tR\x13coOwnerPrincipalIds\x125\n" + - "\x12root_scope_node_id\x18\a \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0frootScopeNodeId\x12<\n" + - "\x06status\x18\b \x01(\x0e2$.saas.accounts.v1.InstallationStatusR\x06status\x129\n" + - "\n" + - "created_at\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12>\n" + - "\n" + - "revoked_at\x18\n" + - " \x01(\v2\x1a.google.protobuf.TimestampH\x00R\trevokedAt\x88\x01\x01\x12(\n" + - "\ttarget_id\x18\v \x01(\tB\v\xbaH\b\xd8\x01\x01r\x03\xb0\x01\x01R\btargetId\x12/\n" + - "\x0erevoked_reason\x18\f \x01(\tB\b\xbaH\x05r\x03\x18\x80\x04R\rrevokedReason\x12/\n" + - "\x0efailure_reason\x18\r \x01(\tB\b\xbaH\x05r\x03\x18\x80\x04R\rfailureReasonB\r\n" + - "\v_revoked_atJ\x04\b\x04\x10\x05R\x13solution_identifier\"\xc1\x01\n" + - "\x18ListInstallationsRequest\x12\x1f\n" + - "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x12<\n" + - "\x06status\x18\x02 \x01(\x0e2$.saas.accounts.v1.InstallationStatusR\x06status\x12'\n" + - "\tpage_size\x18\x03 \x01(\x05B\n" + - "\xbaH\a\x1a\x05\x18\xf4\x03(\x00R\bpageSize\x12\x1d\n" + - "\n" + - "page_token\x18\x04 \x01(\tR\tpageToken\"\x97\x01\n" + - "\x13InstallationSummary\x12B\n" + - "\finstallation\x18\x01 \x01(\v2\x1e.saas.accounts.v1.InstallationR\finstallation\x12<\n" + - "\x06health\x18\x02 \x01(\x0e2$.saas.accounts.v1.InstallationHealthR\x06health\"\x90\x01\n" + - "\x19ListInstallationsResponse\x12K\n" + - "\rinstallations\x18\x01 \x03(\v2%.saas.accounts.v1.InstallationSummaryR\rinstallations\x12&\n" + - "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken*\xbb\x01\n" + - "\x12InstallationStatus\x12#\n" + - "\x1fINSTALLATION_STATUS_UNSPECIFIED\x10\x00\x12\x1e\n" + - "\x1aINSTALLATION_STATUS_ACTIVE\x10\x01\x12\x1f\n" + - "\x1bINSTALLATION_STATUS_REVOKED\x10\x02\x12\x1f\n" + - "\x1bINSTALLATION_STATUS_PENDING\x10\x03\x12\x1e\n" + - "\x1aINSTALLATION_STATUS_FAILED\x10\x04*\x84\x02\n" + - "\x12InstallationHealth\x12#\n" + - "\x1fINSTALLATION_HEALTH_UNSPECIFIED\x10\x00\x12\x1f\n" + - "\x1bINSTALLATION_HEALTH_HEALTHY\x10\x01\x12)\n" + - "%INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER\x10\x02\x12%\n" + - "!INSTALLATION_HEALTH_AGENT_REVOKED\x10\x03\x12&\n" + - "\"INSTALLATION_HEALTH_AGENT_DISABLED\x10\x04\x12.\n" + - "*INSTALLATION_HEALTH_STANDING_GRANT_MISSING\x10\x05B\xc0\x01\n" + - "\x14com.saas.accounts.v1B\x18InstallationListingProtoP\x01Z,accounts/pkg/gen/saas/accounts/v1;accountsv1\xa2\x02\x03SAX\xaa\x02\x10Saas.Accounts.V1\xca\x02\x10Saas\\Accounts\\V1\xe2\x02\x1cSaas\\Accounts\\V1\\GPBMetadata\xea\x02\x12Saas::Accounts::V1b\x06proto3" - -var ( - file_saas_accounts_v1_installation_listing_proto_rawDescOnce sync.Once - file_saas_accounts_v1_installation_listing_proto_rawDescData []byte -) - -func file_saas_accounts_v1_installation_listing_proto_rawDescGZIP() []byte { - file_saas_accounts_v1_installation_listing_proto_rawDescOnce.Do(func() { - file_saas_accounts_v1_installation_listing_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_installation_listing_proto_rawDesc), len(file_saas_accounts_v1_installation_listing_proto_rawDesc))) - }) - return file_saas_accounts_v1_installation_listing_proto_rawDescData -} - -var file_saas_accounts_v1_installation_listing_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_saas_accounts_v1_installation_listing_proto_msgTypes = make([]protoimpl.MessageInfo, 4) -var file_saas_accounts_v1_installation_listing_proto_goTypes = []any{ - (InstallationStatus)(0), // 0: saas.accounts.v1.InstallationStatus - (InstallationHealth)(0), // 1: saas.accounts.v1.InstallationHealth - (*Installation)(nil), // 2: saas.accounts.v1.Installation - (*ListInstallationsRequest)(nil), // 3: saas.accounts.v1.ListInstallationsRequest - (*InstallationSummary)(nil), // 4: saas.accounts.v1.InstallationSummary - (*ListInstallationsResponse)(nil), // 5: saas.accounts.v1.ListInstallationsResponse - (*timestamppb.Timestamp)(nil), // 6: google.protobuf.Timestamp -} -var file_saas_accounts_v1_installation_listing_proto_depIdxs = []int32{ - 0, // 0: saas.accounts.v1.Installation.status:type_name -> saas.accounts.v1.InstallationStatus - 6, // 1: saas.accounts.v1.Installation.created_at:type_name -> google.protobuf.Timestamp - 6, // 2: saas.accounts.v1.Installation.revoked_at:type_name -> google.protobuf.Timestamp - 0, // 3: saas.accounts.v1.ListInstallationsRequest.status:type_name -> saas.accounts.v1.InstallationStatus - 2, // 4: saas.accounts.v1.InstallationSummary.installation:type_name -> saas.accounts.v1.Installation - 1, // 5: saas.accounts.v1.InstallationSummary.health:type_name -> saas.accounts.v1.InstallationHealth - 4, // 6: saas.accounts.v1.ListInstallationsResponse.installations:type_name -> saas.accounts.v1.InstallationSummary - 7, // [7:7] is the sub-list for method output_type - 7, // [7:7] is the sub-list for method input_type - 7, // [7:7] is the sub-list for extension type_name - 7, // [7:7] is the sub-list for extension extendee - 0, // [0:7] is the sub-list for field type_name -} - -func init() { file_saas_accounts_v1_installation_listing_proto_init() } -func file_saas_accounts_v1_installation_listing_proto_init() { - if File_saas_accounts_v1_installation_listing_proto != nil { - return - } - file_saas_accounts_v1_installation_listing_proto_msgTypes[0].OneofWrappers = []any{} - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_installation_listing_proto_rawDesc), len(file_saas_accounts_v1_installation_listing_proto_rawDesc)), - NumEnums: 2, - NumMessages: 4, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_saas_accounts_v1_installation_listing_proto_goTypes, - DependencyIndexes: file_saas_accounts_v1_installation_listing_proto_depIdxs, - EnumInfos: file_saas_accounts_v1_installation_listing_proto_enumTypes, - MessageInfos: file_saas_accounts_v1_installation_listing_proto_msgTypes, - }.Build() - File_saas_accounts_v1_installation_listing_proto = out.File - file_saas_accounts_v1_installation_listing_proto_goTypes = nil - file_saas_accounts_v1_installation_listing_proto_depIdxs = nil -} diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations.pb.go index 9503ce6cc..051a29951 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations.pb.go @@ -17,6 +17,7 @@ import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" emptypb "google.golang.org/protobuf/types/known/emptypb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" ) const ( @@ -26,6 +27,132 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +// InstallationStatus is the lifecycle of an installation row. An uninstall is a +// soft delete (status=revoked, revoked_at set) so the agent, grants, and audit +// trail remain resolvable. +type InstallationStatus int32 + +const ( + InstallationStatus_INSTALLATION_STATUS_UNSPECIFIED InstallationStatus = 0 + InstallationStatus_INSTALLATION_STATUS_ACTIVE InstallationStatus = 1 + InstallationStatus_INSTALLATION_STATUS_REVOKED InstallationStatus = 2 + // Installed, and waiting for the solution to acknowledge its standard install + // step. Nothing can use a pending installation. + InstallationStatus_INSTALLATION_STATUS_PENDING InstallationStatus = 3 + // The solution refused its install step, or never answered within the + // attempt budget. Nothing can use it; an administrator may retry it. + InstallationStatus_INSTALLATION_STATUS_FAILED InstallationStatus = 4 +) + +// Enum value maps for InstallationStatus. +var ( + InstallationStatus_name = map[int32]string{ + 0: "INSTALLATION_STATUS_UNSPECIFIED", + 1: "INSTALLATION_STATUS_ACTIVE", + 2: "INSTALLATION_STATUS_REVOKED", + 3: "INSTALLATION_STATUS_PENDING", + 4: "INSTALLATION_STATUS_FAILED", + } + InstallationStatus_value = map[string]int32{ + "INSTALLATION_STATUS_UNSPECIFIED": 0, + "INSTALLATION_STATUS_ACTIVE": 1, + "INSTALLATION_STATUS_REVOKED": 2, + "INSTALLATION_STATUS_PENDING": 3, + "INSTALLATION_STATUS_FAILED": 4, + } +) + +func (x InstallationStatus) Enum() *InstallationStatus { + p := new(InstallationStatus) + *p = x + return p +} + +func (x InstallationStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (InstallationStatus) Descriptor() protoreflect.EnumDescriptor { + return file_saas_accounts_v1_installations_proto_enumTypes[0].Descriptor() +} + +func (InstallationStatus) Type() protoreflect.EnumType { + return &file_saas_accounts_v1_installations_proto_enumTypes[0] +} + +func (x InstallationStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use InstallationStatus.Descriptor instead. +func (InstallationStatus) EnumDescriptor() ([]byte, []int) { + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{0} +} + +// InstallationHealth is resolved live from current authority facts, never +// stored. It is the observable form of the fail-closed mint rule: a +// StartInstallationTask fails exactly when health is not HEALTHY, and transfer +// or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record +// nor any co-owner is currently an org admin (the Zapier-class "connection owner +// left the org" failure), surfaced rather than silently reassigned. +type InstallationHealth int32 + +const ( + InstallationHealth_INSTALLATION_HEALTH_UNSPECIFIED InstallationHealth = 0 + InstallationHealth_INSTALLATION_HEALTH_HEALTHY InstallationHealth = 1 + InstallationHealth_INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER InstallationHealth = 2 + InstallationHealth_INSTALLATION_HEALTH_AGENT_REVOKED InstallationHealth = 3 + InstallationHealth_INSTALLATION_HEALTH_AGENT_DISABLED InstallationHealth = 4 + InstallationHealth_INSTALLATION_HEALTH_STANDING_GRANT_MISSING InstallationHealth = 5 +) + +// Enum value maps for InstallationHealth. +var ( + InstallationHealth_name = map[int32]string{ + 0: "INSTALLATION_HEALTH_UNSPECIFIED", + 1: "INSTALLATION_HEALTH_HEALTHY", + 2: "INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER", + 3: "INSTALLATION_HEALTH_AGENT_REVOKED", + 4: "INSTALLATION_HEALTH_AGENT_DISABLED", + 5: "INSTALLATION_HEALTH_STANDING_GRANT_MISSING", + } + InstallationHealth_value = map[string]int32{ + "INSTALLATION_HEALTH_UNSPECIFIED": 0, + "INSTALLATION_HEALTH_HEALTHY": 1, + "INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER": 2, + "INSTALLATION_HEALTH_AGENT_REVOKED": 3, + "INSTALLATION_HEALTH_AGENT_DISABLED": 4, + "INSTALLATION_HEALTH_STANDING_GRANT_MISSING": 5, + } +) + +func (x InstallationHealth) Enum() *InstallationHealth { + p := new(InstallationHealth) + *p = x + return p +} + +func (x InstallationHealth) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (InstallationHealth) Descriptor() protoreflect.EnumDescriptor { + return file_saas_accounts_v1_installations_proto_enumTypes[1].Descriptor() +} + +func (InstallationHealth) Type() protoreflect.EnumType { + return &file_saas_accounts_v1_installations_proto_enumTypes[1] +} + +func (x InstallationHealth) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use InstallationHealth.Descriptor instead. +func (InstallationHealth) EnumDescriptor() ([]byte, []int) { + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{1} +} + // ReadinessConditionKind is what kind of thing is unmet. A reader acts on the // words in `detail`; the kind is for the surface to choose an action and for a // consumer to count one kind without parsing prose. @@ -100,11 +227,11 @@ func (x ReadinessConditionKind) String() string { } func (ReadinessConditionKind) Descriptor() protoreflect.EnumDescriptor { - return file_saas_accounts_v1_installations_proto_enumTypes[0].Descriptor() + return file_saas_accounts_v1_installations_proto_enumTypes[2].Descriptor() } func (ReadinessConditionKind) Type() protoreflect.EnumType { - return &file_saas_accounts_v1_installations_proto_enumTypes[0] + return &file_saas_accounts_v1_installations_proto_enumTypes[2] } func (x ReadinessConditionKind) Number() protoreflect.EnumNumber { @@ -113,7 +240,7 @@ func (x ReadinessConditionKind) Number() protoreflect.EnumNumber { // Deprecated: Use ReadinessConditionKind.Descriptor instead. func (ReadinessConditionKind) EnumDescriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{0} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{2} } // ReadinessClearedBy is who can clear a condition. It is stated rather than @@ -161,11 +288,11 @@ func (x ReadinessClearedBy) String() string { } func (ReadinessClearedBy) Descriptor() protoreflect.EnumDescriptor { - return file_saas_accounts_v1_installations_proto_enumTypes[1].Descriptor() + return file_saas_accounts_v1_installations_proto_enumTypes[3].Descriptor() } func (ReadinessClearedBy) Type() protoreflect.EnumType { - return &file_saas_accounts_v1_installations_proto_enumTypes[1] + return &file_saas_accounts_v1_installations_proto_enumTypes[3] } func (x ReadinessClearedBy) Number() protoreflect.EnumNumber { @@ -174,7 +301,7 @@ func (x ReadinessClearedBy) Number() protoreflect.EnumNumber { // Deprecated: Use ReadinessClearedBy.Descriptor instead. func (ReadinessClearedBy) EnumDescriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{1} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{3} } // ReadinessAction is what clears the condition, named as the act rather than as @@ -226,11 +353,11 @@ func (x ReadinessAction) String() string { } func (ReadinessAction) Descriptor() protoreflect.EnumDescriptor { - return file_saas_accounts_v1_installations_proto_enumTypes[2].Descriptor() + return file_saas_accounts_v1_installations_proto_enumTypes[4].Descriptor() } func (ReadinessAction) Type() protoreflect.EnumType { - return &file_saas_accounts_v1_installations_proto_enumTypes[2] + return &file_saas_accounts_v1_installations_proto_enumTypes[4] } func (x ReadinessAction) Number() protoreflect.EnumNumber { @@ -239,7 +366,161 @@ func (x ReadinessAction) Number() protoreflect.EnumNumber { // Deprecated: Use ReadinessAction.Descriptor instead. func (ReadinessAction) EnumDescriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{2} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{4} +} + +// Installation is one solution's bound identity in an org. Server-assigned ids; +// the browser cannot attest to tenancy, the agent principal, or the scope node. +type Installation struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + OrgId string `protobuf:"bytes,2,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` + AgentPrincipalId string `protobuf:"bytes,3,opt,name=agent_principal_id,json=agentPrincipalId,proto3" json:"agent_principal_id,omitempty"` + OwnerPrincipalId string `protobuf:"bytes,5,opt,name=owner_principal_id,json=ownerPrincipalId,proto3" json:"owner_principal_id,omitempty"` + CoOwnerPrincipalIds []string `protobuf:"bytes,6,rep,name=co_owner_principal_ids,json=coOwnerPrincipalIds,proto3" json:"co_owner_principal_ids,omitempty"` + RootScopeNodeId string `protobuf:"bytes,7,opt,name=root_scope_node_id,json=rootScopeNodeId,proto3" json:"root_scope_node_id,omitempty"` + Status InstallationStatus `protobuf:"varint,8,opt,name=status,proto3,enum=saas.accounts.v1.InstallationStatus" json:"status,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + RevokedAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=revoked_at,json=revokedAt,proto3,oneof" json:"revoked_at,omitempty"` + // The immutable solution target this installation was consented to: one + // continuous period of one binding's presence on this host. It is never + // reused, so a replacement binding — even one claiming the same route alias — + // is a different target and inherits nothing. + // + // Deliberately NOT accompanied by the route alias. The alias is a property of + // the target that a later generation may move, and resolving it here would + // mean joining the host's presence state into a request-path listing — which + // would require granting request traffic read over every organisation's + // presence to answer about one. A consumer that needs the alias asks + // ListAvailableSolutions, which serves accepted applied state, and joins on + // this id. + // + // Empty only on a revoked historical row whose target could not be + // established at the cutover; an active installation always names one. + TargetId string `protobuf:"bytes,11,opt,name=target_id,json=targetId,proto3" json:"target_id,omitempty"` + // Why a revoked installation was revoked, when it was not an administrator + // uninstalling: the installation-identity cutover, or a tenancy narrowing that + // withdrew this organisation. Empty on an active row. + RevokedReason string `protobuf:"bytes,12,opt,name=revoked_reason,json=revokedReason,proto3" json:"revoked_reason,omitempty"` + // Why the install step failed: the solution's bounded refusal reason, or a + // fixed reason when it never answered. Empty unless status is FAILED. + FailureReason string `protobuf:"bytes,13,opt,name=failure_reason,json=failureReason,proto3" json:"failure_reason,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Installation) Reset() { + *x = Installation{} + mi := &file_saas_accounts_v1_installations_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Installation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Installation) ProtoMessage() {} + +func (x *Installation) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_installations_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Installation.ProtoReflect.Descriptor instead. +func (*Installation) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{0} +} + +func (x *Installation) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Installation) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + +func (x *Installation) GetAgentPrincipalId() string { + if x != nil { + return x.AgentPrincipalId + } + return "" +} + +func (x *Installation) GetOwnerPrincipalId() string { + if x != nil { + return x.OwnerPrincipalId + } + return "" +} + +func (x *Installation) GetCoOwnerPrincipalIds() []string { + if x != nil { + return x.CoOwnerPrincipalIds + } + return nil +} + +func (x *Installation) GetRootScopeNodeId() string { + if x != nil { + return x.RootScopeNodeId + } + return "" +} + +func (x *Installation) GetStatus() InstallationStatus { + if x != nil { + return x.Status + } + return InstallationStatus_INSTALLATION_STATUS_UNSPECIFIED +} + +func (x *Installation) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *Installation) GetRevokedAt() *timestamppb.Timestamp { + if x != nil { + return x.RevokedAt + } + return nil +} + +func (x *Installation) GetTargetId() string { + if x != nil { + return x.TargetId + } + return "" +} + +func (x *Installation) GetRevokedReason() string { + if x != nil { + return x.RevokedReason + } + return "" +} + +func (x *Installation) GetFailureReason() string { + if x != nil { + return x.FailureReason + } + return "" } type InstallSolutionRequest struct { @@ -299,7 +580,7 @@ type InstallSolutionRequest struct { func (x *InstallSolutionRequest) Reset() { *x = InstallSolutionRequest{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[0] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -311,7 +592,7 @@ func (x *InstallSolutionRequest) String() string { func (*InstallSolutionRequest) ProtoMessage() {} func (x *InstallSolutionRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[0] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -324,7 +605,7 @@ func (x *InstallSolutionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use InstallSolutionRequest.ProtoReflect.Descriptor instead. func (*InstallSolutionRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{0} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{1} } func (x *InstallSolutionRequest) GetOrgId() string { @@ -418,7 +699,7 @@ type NarrowInstallationRequest struct { func (x *NarrowInstallationRequest) Reset() { *x = NarrowInstallationRequest{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[1] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -430,7 +711,7 @@ func (x *NarrowInstallationRequest) String() string { func (*NarrowInstallationRequest) ProtoMessage() {} func (x *NarrowInstallationRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[1] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -443,7 +724,7 @@ func (x *NarrowInstallationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use NarrowInstallationRequest.ProtoReflect.Descriptor instead. func (*NarrowInstallationRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{1} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{2} } func (x *NarrowInstallationRequest) GetOrgId() string { @@ -477,7 +758,7 @@ type RetryInstallationRequest struct { func (x *RetryInstallationRequest) Reset() { *x = RetryInstallationRequest{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[2] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -489,7 +770,7 @@ func (x *RetryInstallationRequest) String() string { func (*RetryInstallationRequest) ProtoMessage() {} func (x *RetryInstallationRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[2] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -502,7 +783,7 @@ func (x *RetryInstallationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RetryInstallationRequest.ProtoReflect.Descriptor instead. func (*RetryInstallationRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{2} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{3} } func (x *RetryInstallationRequest) GetOrgId() string { @@ -529,7 +810,7 @@ type UninstallSolutionRequest struct { func (x *UninstallSolutionRequest) Reset() { *x = UninstallSolutionRequest{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[3] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -541,7 +822,7 @@ func (x *UninstallSolutionRequest) String() string { func (*UninstallSolutionRequest) ProtoMessage() {} func (x *UninstallSolutionRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[3] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -554,7 +835,7 @@ func (x *UninstallSolutionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UninstallSolutionRequest.ProtoReflect.Descriptor instead. func (*UninstallSolutionRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{3} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{4} } func (x *UninstallSolutionRequest) GetOrgId() string { @@ -585,7 +866,7 @@ type TransferInstallationOwnershipRequest struct { func (x *TransferInstallationOwnershipRequest) Reset() { *x = TransferInstallationOwnershipRequest{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[4] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -597,7 +878,7 @@ func (x *TransferInstallationOwnershipRequest) String() string { func (*TransferInstallationOwnershipRequest) ProtoMessage() {} func (x *TransferInstallationOwnershipRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[4] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -610,7 +891,7 @@ func (x *TransferInstallationOwnershipRequest) ProtoReflect() protoreflect.Messa // Deprecated: Use TransferInstallationOwnershipRequest.ProtoReflect.Descriptor instead. func (*TransferInstallationOwnershipRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{4} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{5} } func (x *TransferInstallationOwnershipRequest) GetOrgId() string { @@ -651,7 +932,7 @@ type GetInstallationRequest struct { func (x *GetInstallationRequest) Reset() { *x = GetInstallationRequest{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[5] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -663,7 +944,7 @@ func (x *GetInstallationRequest) String() string { func (*GetInstallationRequest) ProtoMessage() {} func (x *GetInstallationRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[5] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -676,7 +957,7 @@ func (x *GetInstallationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetInstallationRequest.ProtoReflect.Descriptor instead. func (*GetInstallationRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{5} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{6} } func (x *GetInstallationRequest) GetOrgId() string { @@ -704,7 +985,7 @@ type GetInstallationResponse struct { func (x *GetInstallationResponse) Reset() { *x = GetInstallationResponse{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[6] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -716,7 +997,7 @@ func (x *GetInstallationResponse) String() string { func (*GetInstallationResponse) ProtoMessage() {} func (x *GetInstallationResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[6] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -729,7 +1010,7 @@ func (x *GetInstallationResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetInstallationResponse.ProtoReflect.Descriptor instead. func (*GetInstallationResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{6} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{7} } func (x *GetInstallationResponse) GetInstallation() *Installation { @@ -746,6 +1027,194 @@ func (x *GetInstallationResponse) GetHealth() InstallationHealth { return InstallationHealth_INSTALLATION_HEALTH_UNSPECIFIED } +// ListInstallationsRequest asks what one organization has installed. Every other +// RPC on this service names a single installation, so until this existed nothing +// could answer "what has this org installed?" — which is the authority read a +// per-organization solution projection is built on (issue #949). +type ListInstallationsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` + // Restrict to one lifecycle status. UNSPECIFIED returns every status, so a + // caller that must see only live installs asks for ACTIVE explicitly rather + // than leaning on a default that would silently widen if the enum grew. + Status InstallationStatus `protobuf:"varint,2,opt,name=status,proto3,enum=saas.accounts.v1.InstallationStatus" json:"status,omitempty"` + // Page bound. 0 -> server default; capped so an organization with many + // installations can never return an unbounded result in one call. + PageSize int32 `protobuf:"varint,3,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Opaque cursor from a previous response's next_page_token; empty for page one. + PageToken string `protobuf:"bytes,4,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListInstallationsRequest) Reset() { + *x = ListInstallationsRequest{} + mi := &file_saas_accounts_v1_installations_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListInstallationsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListInstallationsRequest) ProtoMessage() {} + +func (x *ListInstallationsRequest) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_installations_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListInstallationsRequest.ProtoReflect.Descriptor instead. +func (*ListInstallationsRequest) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{8} +} + +func (x *ListInstallationsRequest) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + +func (x *ListInstallationsRequest) GetStatus() InstallationStatus { + if x != nil { + return x.Status + } + return InstallationStatus_INSTALLATION_STATUS_UNSPECIFIED +} + +func (x *ListInstallationsRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListInstallationsRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +// InstallationSummary is one row of the listing: the installation, plus the health +// resolved live beside it exactly as GetInstallation resolves it for one. Health +// travels with the row because a consumer that renders an installed solution as +// unavailable would otherwise need one GetInstallation per installation. +type InstallationSummary struct { + state protoimpl.MessageState `protogen:"open.v1"` + Installation *Installation `protobuf:"bytes,1,opt,name=installation,proto3" json:"installation,omitempty"` + Health InstallationHealth `protobuf:"varint,2,opt,name=health,proto3,enum=saas.accounts.v1.InstallationHealth" json:"health,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InstallationSummary) Reset() { + *x = InstallationSummary{} + mi := &file_saas_accounts_v1_installations_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InstallationSummary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InstallationSummary) ProtoMessage() {} + +func (x *InstallationSummary) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_installations_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InstallationSummary.ProtoReflect.Descriptor instead. +func (*InstallationSummary) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{9} +} + +func (x *InstallationSummary) GetInstallation() *Installation { + if x != nil { + return x.Installation + } + return nil +} + +func (x *InstallationSummary) GetHealth() InstallationHealth { + if x != nil { + return x.Health + } + return InstallationHealth_INSTALLATION_HEALTH_UNSPECIFIED +} + +type ListInstallationsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Installations []*InstallationSummary `protobuf:"bytes,1,rep,name=installations,proto3" json:"installations,omitempty"` + // Set when more installations remain; pass it back as page_token. Empty on the + // last page. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListInstallationsResponse) Reset() { + *x = ListInstallationsResponse{} + mi := &file_saas_accounts_v1_installations_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListInstallationsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListInstallationsResponse) ProtoMessage() {} + +func (x *ListInstallationsResponse) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_installations_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListInstallationsResponse.ProtoReflect.Descriptor instead. +func (*ListInstallationsResponse) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{10} +} + +func (x *ListInstallationsResponse) GetInstallations() []*InstallationSummary { + if x != nil { + return x.Installations + } + return nil +} + +func (x *ListInstallationsResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + type ListOrganizationInstallationsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` @@ -759,7 +1228,7 @@ type ListOrganizationInstallationsRequest struct { func (x *ListOrganizationInstallationsRequest) Reset() { *x = ListOrganizationInstallationsRequest{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[7] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -771,7 +1240,7 @@ func (x *ListOrganizationInstallationsRequest) String() string { func (*ListOrganizationInstallationsRequest) ProtoMessage() {} func (x *ListOrganizationInstallationsRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[7] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -784,7 +1253,7 @@ func (x *ListOrganizationInstallationsRequest) ProtoReflect() protoreflect.Messa // Deprecated: Use ListOrganizationInstallationsRequest.ProtoReflect.Descriptor instead. func (*ListOrganizationInstallationsRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{7} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{11} } func (x *ListOrganizationInstallationsRequest) GetOrgId() string { @@ -844,7 +1313,7 @@ type OrganizationInstallation struct { func (x *OrganizationInstallation) Reset() { *x = OrganizationInstallation{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[8] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -856,7 +1325,7 @@ func (x *OrganizationInstallation) String() string { func (*OrganizationInstallation) ProtoMessage() {} func (x *OrganizationInstallation) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[8] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -869,7 +1338,7 @@ func (x *OrganizationInstallation) ProtoReflect() protoreflect.Message { // Deprecated: Use OrganizationInstallation.ProtoReflect.Descriptor instead. func (*OrganizationInstallation) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{8} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{12} } func (x *OrganizationInstallation) GetInstallation() *Installation { @@ -949,7 +1418,7 @@ type ReadinessCondition struct { func (x *ReadinessCondition) Reset() { *x = ReadinessCondition{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[9] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -961,7 +1430,7 @@ func (x *ReadinessCondition) String() string { func (*ReadinessCondition) ProtoMessage() {} func (x *ReadinessCondition) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[9] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -974,7 +1443,7 @@ func (x *ReadinessCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadinessCondition.ProtoReflect.Descriptor instead. func (*ReadinessCondition) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{9} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{13} } func (x *ReadinessCondition) GetKind() ReadinessConditionKind { @@ -1036,7 +1505,7 @@ type InstallationReadiness struct { func (x *InstallationReadiness) Reset() { *x = InstallationReadiness{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[10] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1048,7 +1517,7 @@ func (x *InstallationReadiness) String() string { func (*InstallationReadiness) ProtoMessage() {} func (x *InstallationReadiness) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[10] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1061,7 +1530,7 @@ func (x *InstallationReadiness) ProtoReflect() protoreflect.Message { // Deprecated: Use InstallationReadiness.ProtoReflect.Descriptor instead. func (*InstallationReadiness) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{10} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{14} } func (x *InstallationReadiness) GetReady() bool { @@ -1107,7 +1576,7 @@ type ReachedModule struct { func (x *ReachedModule) Reset() { *x = ReachedModule{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[11] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1119,7 +1588,7 @@ func (x *ReachedModule) String() string { func (*ReachedModule) ProtoMessage() {} func (x *ReachedModule) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[11] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1132,7 +1601,7 @@ func (x *ReachedModule) ProtoReflect() protoreflect.Message { // Deprecated: Use ReachedModule.ProtoReflect.Descriptor instead. func (*ReachedModule) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{11} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{15} } func (x *ReachedModule) GetAudience() string { @@ -1167,7 +1636,7 @@ type ListOrganizationInstallationsResponse struct { func (x *ListOrganizationInstallationsResponse) Reset() { *x = ListOrganizationInstallationsResponse{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[12] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1179,7 +1648,7 @@ func (x *ListOrganizationInstallationsResponse) String() string { func (*ListOrganizationInstallationsResponse) ProtoMessage() {} func (x *ListOrganizationInstallationsResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[12] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1192,7 +1661,7 @@ func (x *ListOrganizationInstallationsResponse) ProtoReflect() protoreflect.Mess // Deprecated: Use ListOrganizationInstallationsResponse.ProtoReflect.Descriptor instead. func (*ListOrganizationInstallationsResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{12} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{16} } func (x *ListOrganizationInstallationsResponse) GetInstallations() []*OrganizationInstallation { @@ -1225,7 +1694,7 @@ type GrantInstallationToTeamRequest struct { func (x *GrantInstallationToTeamRequest) Reset() { *x = GrantInstallationToTeamRequest{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[13] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1237,7 +1706,7 @@ func (x *GrantInstallationToTeamRequest) String() string { func (*GrantInstallationToTeamRequest) ProtoMessage() {} func (x *GrantInstallationToTeamRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[13] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1250,7 +1719,7 @@ func (x *GrantInstallationToTeamRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GrantInstallationToTeamRequest.ProtoReflect.Descriptor instead. func (*GrantInstallationToTeamRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{13} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{17} } func (x *GrantInstallationToTeamRequest) GetOrgId() string { @@ -1314,7 +1783,7 @@ type CarriedModuleAccess struct { func (x *CarriedModuleAccess) Reset() { *x = CarriedModuleAccess{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[14] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1326,7 +1795,7 @@ func (x *CarriedModuleAccess) String() string { func (*CarriedModuleAccess) ProtoMessage() {} func (x *CarriedModuleAccess) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[14] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1339,7 +1808,7 @@ func (x *CarriedModuleAccess) ProtoReflect() protoreflect.Message { // Deprecated: Use CarriedModuleAccess.ProtoReflect.Descriptor instead. func (*CarriedModuleAccess) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{14} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{18} } func (x *CarriedModuleAccess) GetAudience() string { @@ -1397,7 +1866,7 @@ type GrantInstallationToTeamResponse struct { func (x *GrantInstallationToTeamResponse) Reset() { *x = GrantInstallationToTeamResponse{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[15] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1409,7 +1878,7 @@ func (x *GrantInstallationToTeamResponse) String() string { func (*GrantInstallationToTeamResponse) ProtoMessage() {} func (x *GrantInstallationToTeamResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[15] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1422,7 +1891,7 @@ func (x *GrantInstallationToTeamResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GrantInstallationToTeamResponse.ProtoReflect.Descriptor instead. func (*GrantInstallationToTeamResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{15} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{19} } func (x *GrantInstallationToTeamResponse) GetGrant() *ScopeGrant { @@ -1460,7 +1929,7 @@ type RevokeInstallationFromTeamRequest struct { func (x *RevokeInstallationFromTeamRequest) Reset() { *x = RevokeInstallationFromTeamRequest{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[16] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1472,7 +1941,7 @@ func (x *RevokeInstallationFromTeamRequest) String() string { func (*RevokeInstallationFromTeamRequest) ProtoMessage() {} func (x *RevokeInstallationFromTeamRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[16] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1485,7 +1954,7 @@ func (x *RevokeInstallationFromTeamRequest) ProtoReflect() protoreflect.Message // Deprecated: Use RevokeInstallationFromTeamRequest.ProtoReflect.Descriptor instead. func (*RevokeInstallationFromTeamRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{16} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{20} } func (x *RevokeInstallationFromTeamRequest) GetOrgId() string { @@ -1554,7 +2023,7 @@ type AvailableSolution struct { func (x *AvailableSolution) Reset() { *x = AvailableSolution{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[17] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1566,7 +2035,7 @@ func (x *AvailableSolution) String() string { func (*AvailableSolution) ProtoMessage() {} func (x *AvailableSolution) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[17] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1579,7 +2048,7 @@ func (x *AvailableSolution) ProtoReflect() protoreflect.Message { // Deprecated: Use AvailableSolution.ProtoReflect.Descriptor instead. func (*AvailableSolution) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{17} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{21} } func (x *AvailableSolution) GetTargetId() string { @@ -1665,7 +2134,7 @@ type ListAvailableSolutionsRequest struct { func (x *ListAvailableSolutionsRequest) Reset() { *x = ListAvailableSolutionsRequest{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[18] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1677,7 +2146,7 @@ func (x *ListAvailableSolutionsRequest) String() string { func (*ListAvailableSolutionsRequest) ProtoMessage() {} func (x *ListAvailableSolutionsRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[18] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1690,7 +2159,7 @@ func (x *ListAvailableSolutionsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAvailableSolutionsRequest.ProtoReflect.Descriptor instead. func (*ListAvailableSolutionsRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{18} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{22} } func (x *ListAvailableSolutionsRequest) GetOrgId() string { @@ -1724,7 +2193,7 @@ type ListAvailableSolutionsResponse struct { func (x *ListAvailableSolutionsResponse) Reset() { *x = ListAvailableSolutionsResponse{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[19] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1736,7 +2205,7 @@ func (x *ListAvailableSolutionsResponse) String() string { func (*ListAvailableSolutionsResponse) ProtoMessage() {} func (x *ListAvailableSolutionsResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[19] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1749,7 +2218,7 @@ func (x *ListAvailableSolutionsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAvailableSolutionsResponse.ProtoReflect.Descriptor instead. func (*ListAvailableSolutionsResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{19} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{23} } func (x *ListAvailableSolutionsResponse) GetSolutions() []*AvailableSolution { @@ -1770,7 +2239,24 @@ var File_saas_accounts_v1_installations_proto protoreflect.FileDescriptor const file_saas_accounts_v1_installations_proto_rawDesc = "" + "\n" + - "$saas/accounts/v1/installations.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a$saas/accounts/v1/authorization.proto\x1a+saas/accounts/v1/installation_listing.proto\x1a\x1csaas/policy/v1/options.proto\"\xf6\x04\n" + + "$saas/accounts/v1/installations.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$saas/accounts/v1/authorization.proto\x1a\x1csaas/policy/v1/options.proto\"\x94\x05\n" + + "\fInstallation\x12\x18\n" + + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id\x12\x1f\n" + + "\x06org_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x126\n" + + "\x12agent_principal_id\x18\x03 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x10agentPrincipalId\x126\n" + + "\x12owner_principal_id\x18\x05 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x10ownerPrincipalId\x123\n" + + "\x16co_owner_principal_ids\x18\x06 \x03(\tR\x13coOwnerPrincipalIds\x125\n" + + "\x12root_scope_node_id\x18\a \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0frootScopeNodeId\x12<\n" + + "\x06status\x18\b \x01(\x0e2$.saas.accounts.v1.InstallationStatusR\x06status\x129\n" + + "\n" + + "created_at\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12>\n" + + "\n" + + "revoked_at\x18\n" + + " \x01(\v2\x1a.google.protobuf.TimestampH\x00R\trevokedAt\x88\x01\x01\x12(\n" + + "\ttarget_id\x18\v \x01(\tB\v\xbaH\b\xd8\x01\x01r\x03\xb0\x01\x01R\btargetId\x12/\n" + + "\x0erevoked_reason\x18\f \x01(\tB\b\xbaH\x05r\x03\x18\x80\x04R\rrevokedReason\x12/\n" + + "\x0efailure_reason\x18\r \x01(\tB\b\xbaH\x05r\x03\x18\x80\x04R\rfailureReasonB\r\n" + + "\v_revoked_atJ\x04\b\x04\x10\x05R\x13solution_identifier\"\xf6\x04\n" + "\x16InstallSolutionRequest\x12\x1f\n" + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x125\n" + "\x10agent_identifier\x18\x02 \x01(\tB\n" + @@ -1806,7 +2292,20 @@ const file_saas_accounts_v1_installations_proto_rawDesc = "" + "\x0finstallation_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0einstallationId\"\x9b\x01\n" + "\x17GetInstallationResponse\x12B\n" + "\finstallation\x18\x01 \x01(\v2\x1e.saas.accounts.v1.InstallationR\finstallation\x12<\n" + - "\x06health\x18\x02 \x01(\x0e2$.saas.accounts.v1.InstallationHealthR\x06health\"\x8f\x01\n" + + "\x06health\x18\x02 \x01(\x0e2$.saas.accounts.v1.InstallationHealthR\x06health\"\xc1\x01\n" + + "\x18ListInstallationsRequest\x12\x1f\n" + + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x12<\n" + + "\x06status\x18\x02 \x01(\x0e2$.saas.accounts.v1.InstallationStatusR\x06status\x12'\n" + + "\tpage_size\x18\x03 \x01(\x05B\n" + + "\xbaH\a\x1a\x05\x18\xf4\x03(\x00R\bpageSize\x12\x1d\n" + + "\n" + + "page_token\x18\x04 \x01(\tR\tpageToken\"\x97\x01\n" + + "\x13InstallationSummary\x12B\n" + + "\finstallation\x18\x01 \x01(\v2\x1e.saas.accounts.v1.InstallationR\finstallation\x12<\n" + + "\x06health\x18\x02 \x01(\x0e2$.saas.accounts.v1.InstallationHealthR\x06health\"\x90\x01\n" + + "\x19ListInstallationsResponse\x12K\n" + + "\rinstallations\x18\x01 \x03(\v2%.saas.accounts.v1.InstallationSummaryR\rinstallations\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\x8f\x01\n" + "$ListOrganizationInstallationsRequest\x12\x1f\n" + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x12'\n" + "\tpage_size\x18\x02 \x01(\x05B\n" + @@ -1884,7 +2383,20 @@ const file_saas_accounts_v1_installations_proto_rawDesc = "" + "page_token\x18\x03 \x01(\tR\tpageToken\"\x8b\x01\n" + "\x1eListAvailableSolutionsResponse\x12A\n" + "\tsolutions\x18\x01 \x03(\v2#.saas.accounts.v1.AvailableSolutionR\tsolutions\x12&\n" + - "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken*\xf3\x03\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken*\xbb\x01\n" + + "\x12InstallationStatus\x12#\n" + + "\x1fINSTALLATION_STATUS_UNSPECIFIED\x10\x00\x12\x1e\n" + + "\x1aINSTALLATION_STATUS_ACTIVE\x10\x01\x12\x1f\n" + + "\x1bINSTALLATION_STATUS_REVOKED\x10\x02\x12\x1f\n" + + "\x1bINSTALLATION_STATUS_PENDING\x10\x03\x12\x1e\n" + + "\x1aINSTALLATION_STATUS_FAILED\x10\x04*\x84\x02\n" + + "\x12InstallationHealth\x12#\n" + + "\x1fINSTALLATION_HEALTH_UNSPECIFIED\x10\x00\x12\x1f\n" + + "\x1bINSTALLATION_HEALTH_HEALTHY\x10\x01\x12)\n" + + "%INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER\x10\x02\x12%\n" + + "!INSTALLATION_HEALTH_AGENT_REVOKED\x10\x03\x12&\n" + + "\"INSTALLATION_HEALTH_AGENT_DISABLED\x10\x04\x12.\n" + + "*INSTALLATION_HEALTH_STANDING_GRANT_MISSING\x10\x05*\xf3\x03\n" + "\x16ReadinessConditionKind\x12(\n" + "$READINESS_CONDITION_KIND_UNSPECIFIED\x10\x00\x121\n" + "-READINESS_CONDITION_KIND_INSTALL_STEP_PENDING\x10\x01\x120\n" + @@ -1952,86 +2464,96 @@ func file_saas_accounts_v1_installations_proto_rawDescGZIP() []byte { return file_saas_accounts_v1_installations_proto_rawDescData } -var file_saas_accounts_v1_installations_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_saas_accounts_v1_installations_proto_msgTypes = make([]protoimpl.MessageInfo, 20) +var file_saas_accounts_v1_installations_proto_enumTypes = make([]protoimpl.EnumInfo, 5) +var file_saas_accounts_v1_installations_proto_msgTypes = make([]protoimpl.MessageInfo, 24) var file_saas_accounts_v1_installations_proto_goTypes = []any{ - (ReadinessConditionKind)(0), // 0: saas.accounts.v1.ReadinessConditionKind - (ReadinessClearedBy)(0), // 1: saas.accounts.v1.ReadinessClearedBy - (ReadinessAction)(0), // 2: saas.accounts.v1.ReadinessAction - (*InstallSolutionRequest)(nil), // 3: saas.accounts.v1.InstallSolutionRequest - (*NarrowInstallationRequest)(nil), // 4: saas.accounts.v1.NarrowInstallationRequest - (*RetryInstallationRequest)(nil), // 5: saas.accounts.v1.RetryInstallationRequest - (*UninstallSolutionRequest)(nil), // 6: saas.accounts.v1.UninstallSolutionRequest - (*TransferInstallationOwnershipRequest)(nil), // 7: saas.accounts.v1.TransferInstallationOwnershipRequest - (*GetInstallationRequest)(nil), // 8: saas.accounts.v1.GetInstallationRequest - (*GetInstallationResponse)(nil), // 9: saas.accounts.v1.GetInstallationResponse - (*ListOrganizationInstallationsRequest)(nil), // 10: saas.accounts.v1.ListOrganizationInstallationsRequest - (*OrganizationInstallation)(nil), // 11: saas.accounts.v1.OrganizationInstallation - (*ReadinessCondition)(nil), // 12: saas.accounts.v1.ReadinessCondition - (*InstallationReadiness)(nil), // 13: saas.accounts.v1.InstallationReadiness - (*ReachedModule)(nil), // 14: saas.accounts.v1.ReachedModule - (*ListOrganizationInstallationsResponse)(nil), // 15: saas.accounts.v1.ListOrganizationInstallationsResponse - (*GrantInstallationToTeamRequest)(nil), // 16: saas.accounts.v1.GrantInstallationToTeamRequest - (*CarriedModuleAccess)(nil), // 17: saas.accounts.v1.CarriedModuleAccess - (*GrantInstallationToTeamResponse)(nil), // 18: saas.accounts.v1.GrantInstallationToTeamResponse - (*RevokeInstallationFromTeamRequest)(nil), // 19: saas.accounts.v1.RevokeInstallationFromTeamRequest - (*AvailableSolution)(nil), // 20: saas.accounts.v1.AvailableSolution - (*ListAvailableSolutionsRequest)(nil), // 21: saas.accounts.v1.ListAvailableSolutionsRequest - (*ListAvailableSolutionsResponse)(nil), // 22: saas.accounts.v1.ListAvailableSolutionsResponse - (*Installation)(nil), // 23: saas.accounts.v1.Installation - (InstallationHealth)(0), // 24: saas.accounts.v1.InstallationHealth - (*CollectionReadGrant)(nil), // 25: saas.accounts.v1.CollectionReadGrant - (*ScopeGrant)(nil), // 26: saas.accounts.v1.ScopeGrant - (*ListInstallationsRequest)(nil), // 27: saas.accounts.v1.ListInstallationsRequest - (*emptypb.Empty)(nil), // 28: google.protobuf.Empty - (*ListInstallationsResponse)(nil), // 29: saas.accounts.v1.ListInstallationsResponse + (InstallationStatus)(0), // 0: saas.accounts.v1.InstallationStatus + (InstallationHealth)(0), // 1: saas.accounts.v1.InstallationHealth + (ReadinessConditionKind)(0), // 2: saas.accounts.v1.ReadinessConditionKind + (ReadinessClearedBy)(0), // 3: saas.accounts.v1.ReadinessClearedBy + (ReadinessAction)(0), // 4: saas.accounts.v1.ReadinessAction + (*Installation)(nil), // 5: saas.accounts.v1.Installation + (*InstallSolutionRequest)(nil), // 6: saas.accounts.v1.InstallSolutionRequest + (*NarrowInstallationRequest)(nil), // 7: saas.accounts.v1.NarrowInstallationRequest + (*RetryInstallationRequest)(nil), // 8: saas.accounts.v1.RetryInstallationRequest + (*UninstallSolutionRequest)(nil), // 9: saas.accounts.v1.UninstallSolutionRequest + (*TransferInstallationOwnershipRequest)(nil), // 10: saas.accounts.v1.TransferInstallationOwnershipRequest + (*GetInstallationRequest)(nil), // 11: saas.accounts.v1.GetInstallationRequest + (*GetInstallationResponse)(nil), // 12: saas.accounts.v1.GetInstallationResponse + (*ListInstallationsRequest)(nil), // 13: saas.accounts.v1.ListInstallationsRequest + (*InstallationSummary)(nil), // 14: saas.accounts.v1.InstallationSummary + (*ListInstallationsResponse)(nil), // 15: saas.accounts.v1.ListInstallationsResponse + (*ListOrganizationInstallationsRequest)(nil), // 16: saas.accounts.v1.ListOrganizationInstallationsRequest + (*OrganizationInstallation)(nil), // 17: saas.accounts.v1.OrganizationInstallation + (*ReadinessCondition)(nil), // 18: saas.accounts.v1.ReadinessCondition + (*InstallationReadiness)(nil), // 19: saas.accounts.v1.InstallationReadiness + (*ReachedModule)(nil), // 20: saas.accounts.v1.ReachedModule + (*ListOrganizationInstallationsResponse)(nil), // 21: saas.accounts.v1.ListOrganizationInstallationsResponse + (*GrantInstallationToTeamRequest)(nil), // 22: saas.accounts.v1.GrantInstallationToTeamRequest + (*CarriedModuleAccess)(nil), // 23: saas.accounts.v1.CarriedModuleAccess + (*GrantInstallationToTeamResponse)(nil), // 24: saas.accounts.v1.GrantInstallationToTeamResponse + (*RevokeInstallationFromTeamRequest)(nil), // 25: saas.accounts.v1.RevokeInstallationFromTeamRequest + (*AvailableSolution)(nil), // 26: saas.accounts.v1.AvailableSolution + (*ListAvailableSolutionsRequest)(nil), // 27: saas.accounts.v1.ListAvailableSolutionsRequest + (*ListAvailableSolutionsResponse)(nil), // 28: saas.accounts.v1.ListAvailableSolutionsResponse + (*timestamppb.Timestamp)(nil), // 29: google.protobuf.Timestamp + (*CollectionReadGrant)(nil), // 30: saas.accounts.v1.CollectionReadGrant + (*ScopeGrant)(nil), // 31: saas.accounts.v1.ScopeGrant + (*emptypb.Empty)(nil), // 32: google.protobuf.Empty } var file_saas_accounts_v1_installations_proto_depIdxs = []int32{ - 23, // 0: saas.accounts.v1.GetInstallationResponse.installation:type_name -> saas.accounts.v1.Installation - 24, // 1: saas.accounts.v1.GetInstallationResponse.health:type_name -> saas.accounts.v1.InstallationHealth - 23, // 2: saas.accounts.v1.OrganizationInstallation.installation:type_name -> saas.accounts.v1.Installation - 24, // 3: saas.accounts.v1.OrganizationInstallation.health:type_name -> saas.accounts.v1.InstallationHealth - 20, // 4: saas.accounts.v1.OrganizationInstallation.target:type_name -> saas.accounts.v1.AvailableSolution - 25, // 5: saas.accounts.v1.OrganizationInstallation.granted_teams:type_name -> saas.accounts.v1.CollectionReadGrant - 25, // 6: saas.accounts.v1.OrganizationInstallation.inherited_teams:type_name -> saas.accounts.v1.CollectionReadGrant - 13, // 7: saas.accounts.v1.OrganizationInstallation.readiness:type_name -> saas.accounts.v1.InstallationReadiness - 14, // 8: saas.accounts.v1.OrganizationInstallation.reached_modules:type_name -> saas.accounts.v1.ReachedModule - 0, // 9: saas.accounts.v1.ReadinessCondition.kind:type_name -> saas.accounts.v1.ReadinessConditionKind - 1, // 10: saas.accounts.v1.ReadinessCondition.cleared_by:type_name -> saas.accounts.v1.ReadinessClearedBy - 2, // 11: saas.accounts.v1.ReadinessCondition.action:type_name -> saas.accounts.v1.ReadinessAction - 12, // 12: saas.accounts.v1.InstallationReadiness.unmet:type_name -> saas.accounts.v1.ReadinessCondition - 11, // 13: saas.accounts.v1.ListOrganizationInstallationsResponse.installations:type_name -> saas.accounts.v1.OrganizationInstallation - 26, // 14: saas.accounts.v1.GrantInstallationToTeamResponse.grant:type_name -> saas.accounts.v1.ScopeGrant - 17, // 15: saas.accounts.v1.GrantInstallationToTeamResponse.carried:type_name -> saas.accounts.v1.CarriedModuleAccess - 12, // 16: saas.accounts.v1.GrantInstallationToTeamResponse.unmet:type_name -> saas.accounts.v1.ReadinessCondition - 20, // 17: saas.accounts.v1.ListAvailableSolutionsResponse.solutions:type_name -> saas.accounts.v1.AvailableSolution - 3, // 18: saas.accounts.v1.InstallationService.InstallSolution:input_type -> saas.accounts.v1.InstallSolutionRequest - 6, // 19: saas.accounts.v1.InstallationService.UninstallSolution:input_type -> saas.accounts.v1.UninstallSolutionRequest - 4, // 20: saas.accounts.v1.InstallationService.NarrowInstallation:input_type -> saas.accounts.v1.NarrowInstallationRequest - 5, // 21: saas.accounts.v1.InstallationService.RetryInstallation:input_type -> saas.accounts.v1.RetryInstallationRequest - 7, // 22: saas.accounts.v1.InstallationService.TransferInstallationOwnership:input_type -> saas.accounts.v1.TransferInstallationOwnershipRequest - 8, // 23: saas.accounts.v1.InstallationService.GetInstallation:input_type -> saas.accounts.v1.GetInstallationRequest - 27, // 24: saas.accounts.v1.InstallationService.ListInstallations:input_type -> saas.accounts.v1.ListInstallationsRequest - 21, // 25: saas.accounts.v1.InstallationService.ListAvailableSolutions:input_type -> saas.accounts.v1.ListAvailableSolutionsRequest - 10, // 26: saas.accounts.v1.InstallationService.ListOrganizationInstallations:input_type -> saas.accounts.v1.ListOrganizationInstallationsRequest - 16, // 27: saas.accounts.v1.InstallationService.GrantInstallationToTeam:input_type -> saas.accounts.v1.GrantInstallationToTeamRequest - 19, // 28: saas.accounts.v1.InstallationService.RevokeInstallationFromTeam:input_type -> saas.accounts.v1.RevokeInstallationFromTeamRequest - 23, // 29: saas.accounts.v1.InstallationService.InstallSolution:output_type -> saas.accounts.v1.Installation - 28, // 30: saas.accounts.v1.InstallationService.UninstallSolution:output_type -> google.protobuf.Empty - 28, // 31: saas.accounts.v1.InstallationService.NarrowInstallation:output_type -> google.protobuf.Empty - 23, // 32: saas.accounts.v1.InstallationService.RetryInstallation:output_type -> saas.accounts.v1.Installation - 23, // 33: saas.accounts.v1.InstallationService.TransferInstallationOwnership:output_type -> saas.accounts.v1.Installation - 9, // 34: saas.accounts.v1.InstallationService.GetInstallation:output_type -> saas.accounts.v1.GetInstallationResponse - 29, // 35: saas.accounts.v1.InstallationService.ListInstallations:output_type -> saas.accounts.v1.ListInstallationsResponse - 22, // 36: saas.accounts.v1.InstallationService.ListAvailableSolutions:output_type -> saas.accounts.v1.ListAvailableSolutionsResponse - 15, // 37: saas.accounts.v1.InstallationService.ListOrganizationInstallations:output_type -> saas.accounts.v1.ListOrganizationInstallationsResponse - 18, // 38: saas.accounts.v1.InstallationService.GrantInstallationToTeam:output_type -> saas.accounts.v1.GrantInstallationToTeamResponse - 28, // 39: saas.accounts.v1.InstallationService.RevokeInstallationFromTeam:output_type -> google.protobuf.Empty - 29, // [29:40] is the sub-list for method output_type - 18, // [18:29] is the sub-list for method input_type - 18, // [18:18] is the sub-list for extension type_name - 18, // [18:18] is the sub-list for extension extendee - 0, // [0:18] is the sub-list for field type_name + 0, // 0: saas.accounts.v1.Installation.status:type_name -> saas.accounts.v1.InstallationStatus + 29, // 1: saas.accounts.v1.Installation.created_at:type_name -> google.protobuf.Timestamp + 29, // 2: saas.accounts.v1.Installation.revoked_at:type_name -> google.protobuf.Timestamp + 5, // 3: saas.accounts.v1.GetInstallationResponse.installation:type_name -> saas.accounts.v1.Installation + 1, // 4: saas.accounts.v1.GetInstallationResponse.health:type_name -> saas.accounts.v1.InstallationHealth + 0, // 5: saas.accounts.v1.ListInstallationsRequest.status:type_name -> saas.accounts.v1.InstallationStatus + 5, // 6: saas.accounts.v1.InstallationSummary.installation:type_name -> saas.accounts.v1.Installation + 1, // 7: saas.accounts.v1.InstallationSummary.health:type_name -> saas.accounts.v1.InstallationHealth + 14, // 8: saas.accounts.v1.ListInstallationsResponse.installations:type_name -> saas.accounts.v1.InstallationSummary + 5, // 9: saas.accounts.v1.OrganizationInstallation.installation:type_name -> saas.accounts.v1.Installation + 1, // 10: saas.accounts.v1.OrganizationInstallation.health:type_name -> saas.accounts.v1.InstallationHealth + 26, // 11: saas.accounts.v1.OrganizationInstallation.target:type_name -> saas.accounts.v1.AvailableSolution + 30, // 12: saas.accounts.v1.OrganizationInstallation.granted_teams:type_name -> saas.accounts.v1.CollectionReadGrant + 30, // 13: saas.accounts.v1.OrganizationInstallation.inherited_teams:type_name -> saas.accounts.v1.CollectionReadGrant + 19, // 14: saas.accounts.v1.OrganizationInstallation.readiness:type_name -> saas.accounts.v1.InstallationReadiness + 20, // 15: saas.accounts.v1.OrganizationInstallation.reached_modules:type_name -> saas.accounts.v1.ReachedModule + 2, // 16: saas.accounts.v1.ReadinessCondition.kind:type_name -> saas.accounts.v1.ReadinessConditionKind + 3, // 17: saas.accounts.v1.ReadinessCondition.cleared_by:type_name -> saas.accounts.v1.ReadinessClearedBy + 4, // 18: saas.accounts.v1.ReadinessCondition.action:type_name -> saas.accounts.v1.ReadinessAction + 18, // 19: saas.accounts.v1.InstallationReadiness.unmet:type_name -> saas.accounts.v1.ReadinessCondition + 17, // 20: saas.accounts.v1.ListOrganizationInstallationsResponse.installations:type_name -> saas.accounts.v1.OrganizationInstallation + 31, // 21: saas.accounts.v1.GrantInstallationToTeamResponse.grant:type_name -> saas.accounts.v1.ScopeGrant + 23, // 22: saas.accounts.v1.GrantInstallationToTeamResponse.carried:type_name -> saas.accounts.v1.CarriedModuleAccess + 18, // 23: saas.accounts.v1.GrantInstallationToTeamResponse.unmet:type_name -> saas.accounts.v1.ReadinessCondition + 26, // 24: saas.accounts.v1.ListAvailableSolutionsResponse.solutions:type_name -> saas.accounts.v1.AvailableSolution + 6, // 25: saas.accounts.v1.InstallationService.InstallSolution:input_type -> saas.accounts.v1.InstallSolutionRequest + 9, // 26: saas.accounts.v1.InstallationService.UninstallSolution:input_type -> saas.accounts.v1.UninstallSolutionRequest + 7, // 27: saas.accounts.v1.InstallationService.NarrowInstallation:input_type -> saas.accounts.v1.NarrowInstallationRequest + 8, // 28: saas.accounts.v1.InstallationService.RetryInstallation:input_type -> saas.accounts.v1.RetryInstallationRequest + 10, // 29: saas.accounts.v1.InstallationService.TransferInstallationOwnership:input_type -> saas.accounts.v1.TransferInstallationOwnershipRequest + 11, // 30: saas.accounts.v1.InstallationService.GetInstallation:input_type -> saas.accounts.v1.GetInstallationRequest + 13, // 31: saas.accounts.v1.InstallationService.ListInstallations:input_type -> saas.accounts.v1.ListInstallationsRequest + 27, // 32: saas.accounts.v1.InstallationService.ListAvailableSolutions:input_type -> saas.accounts.v1.ListAvailableSolutionsRequest + 16, // 33: saas.accounts.v1.InstallationService.ListOrganizationInstallations:input_type -> saas.accounts.v1.ListOrganizationInstallationsRequest + 22, // 34: saas.accounts.v1.InstallationService.GrantInstallationToTeam:input_type -> saas.accounts.v1.GrantInstallationToTeamRequest + 25, // 35: saas.accounts.v1.InstallationService.RevokeInstallationFromTeam:input_type -> saas.accounts.v1.RevokeInstallationFromTeamRequest + 5, // 36: saas.accounts.v1.InstallationService.InstallSolution:output_type -> saas.accounts.v1.Installation + 32, // 37: saas.accounts.v1.InstallationService.UninstallSolution:output_type -> google.protobuf.Empty + 32, // 38: saas.accounts.v1.InstallationService.NarrowInstallation:output_type -> google.protobuf.Empty + 5, // 39: saas.accounts.v1.InstallationService.RetryInstallation:output_type -> saas.accounts.v1.Installation + 5, // 40: saas.accounts.v1.InstallationService.TransferInstallationOwnership:output_type -> saas.accounts.v1.Installation + 12, // 41: saas.accounts.v1.InstallationService.GetInstallation:output_type -> saas.accounts.v1.GetInstallationResponse + 15, // 42: saas.accounts.v1.InstallationService.ListInstallations:output_type -> saas.accounts.v1.ListInstallationsResponse + 28, // 43: saas.accounts.v1.InstallationService.ListAvailableSolutions:output_type -> saas.accounts.v1.ListAvailableSolutionsResponse + 21, // 44: saas.accounts.v1.InstallationService.ListOrganizationInstallations:output_type -> saas.accounts.v1.ListOrganizationInstallationsResponse + 24, // 45: saas.accounts.v1.InstallationService.GrantInstallationToTeam:output_type -> saas.accounts.v1.GrantInstallationToTeamResponse + 32, // 46: saas.accounts.v1.InstallationService.RevokeInstallationFromTeam:output_type -> google.protobuf.Empty + 36, // [36:47] is the sub-list for method output_type + 25, // [25:36] is the sub-list for method input_type + 25, // [25:25] is the sub-list for extension type_name + 25, // [25:25] is the sub-list for extension extendee + 0, // [0:25] is the sub-list for field type_name } func init() { file_saas_accounts_v1_installations_proto_init() } @@ -2040,15 +2562,15 @@ func file_saas_accounts_v1_installations_proto_init() { return } file_saas_accounts_v1_authorization_proto_init() - file_saas_accounts_v1_installation_listing_proto_init() file_saas_accounts_v1_installations_proto_msgTypes[0].OneofWrappers = []any{} + file_saas_accounts_v1_installations_proto_msgTypes[1].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_installations_proto_rawDesc), len(file_saas_accounts_v1_installations_proto_rawDesc)), - NumEnums: 3, - NumMessages: 20, + NumEnums: 5, + NumMessages: 24, NumExtensions: 0, NumServices: 1, }, diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations_grpc.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations_grpc.pb.go index 9344b4e28..cee20fabe 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations_grpc.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations_grpc.pb.go @@ -70,15 +70,11 @@ type InstallationServiceClient interface { // ListInstallations enumerates one organization's installations with their live // health. It is internal-tier on purpose: it takes the organization as a request // field, so only a caller holding the cluster-internal credential may ask, and - // the tenant it asks about is never one a browser supplied. The auth-gateway is - // its caller, for two reads. Its viewer projection asks about the tenant it - // projected from a verified identity; it sits beside - // PermissionService.ListAccessibleScopes for that reason, the two read together - // to answer what a viewer's organization installed and what that viewer was - // granted. Its declared module route's bearer-less arm asks about the tenant an - // edge-inspected Work Context names, a value this issuer signed, to admit a - // headless operation context only when the target is installed there; whether - // the context is one is WorkContextService/AdmitWorkloadOperation's answer. + // the tenant it asks about is the one the auth-gateway projected from a verified + // identity — never one a browser supplied. It sits beside + // PermissionService.ListAccessibleScopes for that reason: the two are read + // together to answer what a viewer's organization installed and what that viewer + // was granted. ListInstallations(ctx context.Context, in *ListInstallationsRequest, opts ...grpc.CallOption) (*ListInstallationsResponse, error) // ListAvailableSolutions is the catalogue: the solution targets an // administrator may install right now. @@ -103,8 +99,7 @@ type InstallationServiceClient interface { // It is the org-admin companion to the internal ListInstallations, and it is a // separate method rather than a relaxed policy on that one: ListInstallations // is read by the gateway for an organisation it projected from a verified - // identity or that an edge-verified Work Context names, and a bare tenant - // credential must never reach it. This one binds + // identity, and a bare tenant credential must never reach it. This one binds // the organisation to the caller's own admin membership, exactly as // ListAvailableSolutions does, so no organisation's administrator can read // another's. The one exception is inherited, not added here: a platform @@ -288,15 +283,11 @@ type InstallationServiceServer interface { // ListInstallations enumerates one organization's installations with their live // health. It is internal-tier on purpose: it takes the organization as a request // field, so only a caller holding the cluster-internal credential may ask, and - // the tenant it asks about is never one a browser supplied. The auth-gateway is - // its caller, for two reads. Its viewer projection asks about the tenant it - // projected from a verified identity; it sits beside - // PermissionService.ListAccessibleScopes for that reason, the two read together - // to answer what a viewer's organization installed and what that viewer was - // granted. Its declared module route's bearer-less arm asks about the tenant an - // edge-inspected Work Context names, a value this issuer signed, to admit a - // headless operation context only when the target is installed there; whether - // the context is one is WorkContextService/AdmitWorkloadOperation's answer. + // the tenant it asks about is the one the auth-gateway projected from a verified + // identity — never one a browser supplied. It sits beside + // PermissionService.ListAccessibleScopes for that reason: the two are read + // together to answer what a viewer's organization installed and what that viewer + // was granted. ListInstallations(context.Context, *ListInstallationsRequest) (*ListInstallationsResponse, error) // ListAvailableSolutions is the catalogue: the solution targets an // administrator may install right now. @@ -321,8 +312,7 @@ type InstallationServiceServer interface { // It is the org-admin companion to the internal ListInstallations, and it is a // separate method rather than a relaxed policy on that one: ListInstallations // is read by the gateway for an organisation it projected from a verified - // identity or that an edge-verified Work Context names, and a bare tenant - // credential must never reach it. This one binds + // identity, and a bare tenant credential must never reach it. This one binds // the organisation to the caller's own admin membership, exactly as // ListAvailableSolutions does, so no organisation's administrator can read // another's. The one exception is inherited, not added here: a platform diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go index f0caf2fe3..2eff4d354 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go @@ -3861,7 +3861,7 @@ var File_saas_accounts_v1_platform_admin_proto protoreflect.FileDescriptor const file_saas_accounts_v1_platform_admin_proto_rawDesc = "" + "\n" + - "%saas/accounts/v1/platform_admin.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$saas/accounts/v1/authorization.proto\x1a\x1dsaas/accounts/v1/common.proto\x1a+saas/accounts/v1/installation_listing.proto\x1a(saas/accounts/v1/solution_registry.proto\x1a\x1fsaas/events/v1/operations.proto\x1a\x17saas/jobs/v1/jobs.proto\x1a\x1csaas/policy/v1/options.proto\"q\n" + + "%saas/accounts/v1/platform_admin.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$saas/accounts/v1/authorization.proto\x1a\x1dsaas/accounts/v1/common.proto\x1a$saas/accounts/v1/installations.proto\x1a(saas/accounts/v1/solution_registry.proto\x1a\x1fsaas/events/v1/operations.proto\x1a\x17saas/jobs/v1/jobs.proto\x1a\x1csaas/policy/v1/options.proto\"q\n" + "\x12SearchUsersRequest\x12\x14\n" + "\x05query\x18\x01 \x01(\tR\x05query\x12&\n" + "\tpage_size\x18\x02 \x01(\x05B\t\xbaH\x06\x1a\x04\x18d \x00R\bpageSize\x12\x1d\n" + @@ -4380,7 +4380,7 @@ func file_saas_accounts_v1_platform_admin_proto_init() { } file_saas_accounts_v1_authorization_proto_init() file_saas_accounts_v1_common_proto_init() - file_saas_accounts_v1_installation_listing_proto_init() + file_saas_accounts_v1_installations_proto_init() file_saas_accounts_v1_solution_registry_proto_init() file_saas_accounts_v1_platform_admin_proto_msgTypes[34].OneofWrappers = []any{ (*CatalogueDesired_DeclaredRevision)(nil), diff --git a/module/services/accounts/generated/openapi-raw/api.swagger.json b/module/services/accounts/generated/openapi-raw/api.swagger.json index 595d4790d..92a8f0b83 100644 --- a/module/services/accounts/generated/openapi-raw/api.swagger.json +++ b/module/services/accounts/generated/openapi-raw/api.swagger.json @@ -1733,7 +1733,7 @@ "/v1/installations": { "get": { "summary": "ListOrganizationInstallations is an organisation administrator's view of\nwhat that organisation has installed: each active installation with its live\nhealth, the catalogue row of the target it installed, the scope path of its\nauthority root, and the teams a grant reaches there.", - "description": "It is the org-admin companion to the internal ListInstallations, and it is a\nseparate method rather than a relaxed policy on that one: ListInstallations\nis read by the gateway for an organisation it projected from a verified\nidentity or that an edge-verified Work Context names, and a bare tenant\ncredential must never reach it. This one binds\nthe organisation to the caller's own admin membership, exactly as\nListAvailableSolutions does, so no organisation's administrator can read\nanother's. The one exception is inherited, not added here: a platform\nsuper administrator (not impersonating) passes the organisation-admin check\nfor every organisation, as on every other organisation-admin method, and so\nmay list any organisation's installations.", + "description": "It is the org-admin companion to the internal ListInstallations, and it is a\nseparate method rather than a relaxed policy on that one: ListInstallations\nis read by the gateway for an organisation it projected from a verified\nidentity, and a bare tenant credential must never reach it. This one binds\nthe organisation to the caller's own admin membership, exactly as\nListAvailableSolutions does, so no organisation's administrator can read\nanother's. The one exception is inherited, not added here: a platform\nsuper administrator (not impersonating) passes the organisation-admin check\nfor every organisation, as on every other organisation-admin method, and so\nmay list any organisation's installations.", "operationId": "InstallationService_ListOrganizationInstallations", "responses": { "200": { diff --git a/module/services/accounts/openapi/api.swagger.json b/module/services/accounts/openapi/api.swagger.json index a24754824..9fb8507fd 100644 --- a/module/services/accounts/openapi/api.swagger.json +++ b/module/services/accounts/openapi/api.swagger.json @@ -7756,7 +7756,7 @@ }, "/v1/installations": { "get": { - "description": "It is the org-admin companion to the internal ListInstallations, and it is a\nseparate method rather than a relaxed policy on that one: ListInstallations\nis read by the gateway for an organisation it projected from a verified\nidentity or that an edge-verified Work Context names, and a bare tenant\ncredential must never reach it. This one binds\nthe organisation to the caller's own admin membership, exactly as\nListAvailableSolutions does, so no organisation's administrator can read\nanother's. The one exception is inherited, not added here: a platform\nsuper administrator (not impersonating) passes the organisation-admin check\nfor every organisation, as on every other organisation-admin method, and so\nmay list any organisation's installations.", + "description": "It is the org-admin companion to the internal ListInstallations, and it is a\nseparate method rather than a relaxed policy on that one: ListInstallations\nis read by the gateway for an organisation it projected from a verified\nidentity, and a bare tenant credential must never reach it. This one binds\nthe organisation to the caller's own admin membership, exactly as\nListAvailableSolutions does, so no organisation's administrator can read\nanother's. The one exception is inherited, not added here: a platform\nsuper administrator (not impersonating) passes the organisation-admin check\nfor every organisation, as on every other organisation-admin method, and so\nmay list any organisation's installations.", "operationId": "InstallationService_ListOrganizationInstallations", "parameters": [ { diff --git a/module/services/accounts/proto/buf.yaml b/module/services/accounts/proto/buf.yaml index 1a1cfc5ca..509da1b1f 100644 --- a/module/services/accounts/proto/buf.yaml +++ b/module/services/accounts/proto/buf.yaml @@ -94,23 +94,12 @@ breaking: # SolutionBackendBinding (#1118): the reported upstream half, deleted # with the report — a solution's address is resolved, never recorded. - saas/accounts/v1/solution_registry.proto - # Installation, ListInstallationsRequest, InstallationSummary and - # ListInstallationsResponse moved to installation_listing.proto so the - # auth-gateway can generate the ListInstallations messages from the same - # source file without vendoring the policy options and google.api this - # file imports. Same package, same fully-qualified names, same wire - # format; every RPC stays where it was. - - saas/accounts/v1/installations.proto # ModuleMintRegistrationRequest/Response and # SolutionMintRegistrationRequest/Response (#1056): the registration # credentials and the secrets that minted them are deleted. Presence is # delivered, so nothing registers at runtime and nothing verifies either # credential. - saas/accounts/v1/module_registration.proto - # InstallationStatus and InstallationHealth moved to - # installation_listing.proto with the messages above, for the same reason. - ENUM_NO_DELETE: - - saas/accounts/v1/installations.proto # ListMyAccessibleScopes moved from PermissionService to the new # AccessibleScopeService in accessible_scopes.proto (issue #577), which changes # its Connect/gRPC procedure path. Its REST route (GET /v1/accessible-scopes) is diff --git a/module/services/accounts/proto/saas/accounts/v1/installation_listing.proto b/module/services/accounts/proto/saas/accounts/v1/installation_listing.proto deleted file mode 100644 index 57150f92b..000000000 --- a/module/services/accounts/proto/saas/accounts/v1/installation_listing.proto +++ /dev/null @@ -1,131 +0,0 @@ -syntax = "proto3"; - -package saas.accounts.v1; - -import "buf/validate/validate.proto"; -import "google/protobuf/timestamp.proto"; - -// The installation listing: an installation, its lifecycle and health, and the -// messages InstallationService/ListInstallations takes and answers. -// -// These definitions live in their own file, with deliberately minimal imports, -// so the auth-gateway can generate the same types from the same source proto -// and call ListInstallations over the internal listener, as it does for -// module_registration.proto and solution_entitlements.proto. installations.proto -// imports this file; the service and every other installation message stay -// there. Sharing the generated definition rather than restating the wire shape -// is what keeps the two sides from drifting: the gateway's copy is this file, -// byte for byte. - -// InstallationStatus is the lifecycle of an installation row. An uninstall is a -// soft delete (status=revoked, revoked_at set) so the agent, grants, and audit -// trail remain resolvable. -enum InstallationStatus { - INSTALLATION_STATUS_UNSPECIFIED = 0; - INSTALLATION_STATUS_ACTIVE = 1; - INSTALLATION_STATUS_REVOKED = 2; - // Installed, and waiting for the solution to acknowledge its standard install - // step. Nothing can use a pending installation. - INSTALLATION_STATUS_PENDING = 3; - // The solution refused its install step, or never answered within the - // attempt budget. Nothing can use it; an administrator may retry it. - INSTALLATION_STATUS_FAILED = 4; -} - -// InstallationHealth is resolved live from current authority facts, never -// stored. It is the observable form of the fail-closed mint rule: a -// StartInstallationTask fails exactly when health is not HEALTHY, and transfer -// or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record -// nor any co-owner is currently an org admin (the Zapier-class "connection owner -// left the org" failure), surfaced rather than silently reassigned. -enum InstallationHealth { - INSTALLATION_HEALTH_UNSPECIFIED = 0; - INSTALLATION_HEALTH_HEALTHY = 1; - INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER = 2; - INSTALLATION_HEALTH_AGENT_REVOKED = 3; - INSTALLATION_HEALTH_AGENT_DISABLED = 4; - INSTALLATION_HEALTH_STANDING_GRANT_MISSING = 5; -} - -// Installation is one solution's bound identity in an org. Server-assigned ids; -// the browser cannot attest to tenancy, the agent principal, or the scope node. -message Installation { - string id = 1 [(buf.validate.field).string.uuid = true]; - string org_id = 2 [(buf.validate.field).string.uuid = true]; - string agent_principal_id = 3 [(buf.validate.field).string.uuid = true]; - // Was `solution_identifier`: the free-text route alias. It was the join key - // every reader used, and because an alias is deliberately reusable by a later - // binding, an installation that named one inherited whatever took it next. - // Replaced by target_id below. Reserved rather than reused so no consumer can - // read an alias out of a field that now carries an identity. - reserved 4; - reserved "solution_identifier"; - string owner_principal_id = 5 [(buf.validate.field).string.uuid = true]; - repeated string co_owner_principal_ids = 6; - string root_scope_node_id = 7 [(buf.validate.field).string.uuid = true]; - InstallationStatus status = 8; - google.protobuf.Timestamp created_at = 9; - optional google.protobuf.Timestamp revoked_at = 10; - // The immutable solution target this installation was consented to: one - // continuous period of one binding's presence on this host. It is never - // reused, so a replacement binding — even one claiming the same route alias — - // is a different target and inherits nothing. - // - // Deliberately NOT accompanied by the route alias. The alias is a property of - // the target that a later generation may move, and resolving it here would - // mean joining the host's presence state into a request-path listing — which - // would require granting request traffic read over every organisation's - // presence to answer about one. A consumer that needs the alias asks - // ListAvailableSolutions, which serves accepted applied state, and joins on - // this id. - // - // Empty only on a revoked historical row whose target could not be - // established at the cutover; an active installation always names one. - string target_id = 11 [ - (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, - (buf.validate.field).string.uuid = true - ]; - // Why a revoked installation was revoked, when it was not an administrator - // uninstalling: the installation-identity cutover, or a tenancy narrowing that - // withdrew this organisation. Empty on an active row. - string revoked_reason = 12 [(buf.validate.field).string.max_len = 512]; - // Why the install step failed: the solution's bounded refusal reason, or a - // fixed reason when it never answered. Empty unless status is FAILED. - string failure_reason = 13 [(buf.validate.field).string.max_len = 512]; -} - -// ListInstallationsRequest asks what one organization has installed. Every other -// RPC on this service names a single installation, so until this existed nothing -// could answer "what has this org installed?" — which is the authority read a -// per-organization solution projection is built on (issue #949). -message ListInstallationsRequest { - string org_id = 1 [(buf.validate.field).string.uuid = true]; - // Restrict to one lifecycle status. UNSPECIFIED returns every status, so a - // caller that must see only live installs asks for ACTIVE explicitly rather - // than leaning on a default that would silently widen if the enum grew. - InstallationStatus status = 2; - // Page bound. 0 -> server default; capped so an organization with many - // installations can never return an unbounded result in one call. - int32 page_size = 3 [(buf.validate.field).int32 = { - gte: 0 - lte: 500 - }]; - // Opaque cursor from a previous response's next_page_token; empty for page one. - string page_token = 4; -} - -// InstallationSummary is one row of the listing: the installation, plus the health -// resolved live beside it exactly as GetInstallation resolves it for one. Health -// travels with the row because a consumer that renders an installed solution as -// unavailable would otherwise need one GetInstallation per installation. -message InstallationSummary { - Installation installation = 1; - InstallationHealth health = 2; -} - -message ListInstallationsResponse { - repeated InstallationSummary installations = 1; - // Set when more installations remain; pass it back as page_token. Empty on the - // last page. - string next_page_token = 2; -} diff --git a/module/services/accounts/proto/saas/accounts/v1/installations.proto b/module/services/accounts/proto/saas/accounts/v1/installations.proto index 9fc95dd86..82739f9de 100644 --- a/module/services/accounts/proto/saas/accounts/v1/installations.proto +++ b/module/services/accounts/proto/saas/accounts/v1/installations.proto @@ -5,8 +5,8 @@ package saas.accounts.v1; import "buf/validate/validate.proto"; import "google/api/annotations.proto"; import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; import "saas/accounts/v1/authorization.proto"; -import "saas/accounts/v1/installation_listing.proto"; import "saas/policy/v1/options.proto"; // ============================================================================ @@ -22,6 +22,83 @@ import "saas/policy/v1/options.proto"; // (StartInstallationTask) beside the other mints. // ============================================================================ +// InstallationStatus is the lifecycle of an installation row. An uninstall is a +// soft delete (status=revoked, revoked_at set) so the agent, grants, and audit +// trail remain resolvable. +enum InstallationStatus { + INSTALLATION_STATUS_UNSPECIFIED = 0; + INSTALLATION_STATUS_ACTIVE = 1; + INSTALLATION_STATUS_REVOKED = 2; + // Installed, and waiting for the solution to acknowledge its standard install + // step. Nothing can use a pending installation. + INSTALLATION_STATUS_PENDING = 3; + // The solution refused its install step, or never answered within the + // attempt budget. Nothing can use it; an administrator may retry it. + INSTALLATION_STATUS_FAILED = 4; +} + +// InstallationHealth is resolved live from current authority facts, never +// stored. It is the observable form of the fail-closed mint rule: a +// StartInstallationTask fails exactly when health is not HEALTHY, and transfer +// or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record +// nor any co-owner is currently an org admin (the Zapier-class "connection owner +// left the org" failure), surfaced rather than silently reassigned. +enum InstallationHealth { + INSTALLATION_HEALTH_UNSPECIFIED = 0; + INSTALLATION_HEALTH_HEALTHY = 1; + INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER = 2; + INSTALLATION_HEALTH_AGENT_REVOKED = 3; + INSTALLATION_HEALTH_AGENT_DISABLED = 4; + INSTALLATION_HEALTH_STANDING_GRANT_MISSING = 5; +} + +// Installation is one solution's bound identity in an org. Server-assigned ids; +// the browser cannot attest to tenancy, the agent principal, or the scope node. +message Installation { + string id = 1 [(buf.validate.field).string.uuid = true]; + string org_id = 2 [(buf.validate.field).string.uuid = true]; + string agent_principal_id = 3 [(buf.validate.field).string.uuid = true]; + // Was `solution_identifier`: the free-text route alias. It was the join key + // every reader used, and because an alias is deliberately reusable by a later + // binding, an installation that named one inherited whatever took it next. + // Replaced by target_id below. Reserved rather than reused so no consumer can + // read an alias out of a field that now carries an identity. + reserved 4; + reserved "solution_identifier"; + string owner_principal_id = 5 [(buf.validate.field).string.uuid = true]; + repeated string co_owner_principal_ids = 6; + string root_scope_node_id = 7 [(buf.validate.field).string.uuid = true]; + InstallationStatus status = 8; + google.protobuf.Timestamp created_at = 9; + optional google.protobuf.Timestamp revoked_at = 10; + // The immutable solution target this installation was consented to: one + // continuous period of one binding's presence on this host. It is never + // reused, so a replacement binding — even one claiming the same route alias — + // is a different target and inherits nothing. + // + // Deliberately NOT accompanied by the route alias. The alias is a property of + // the target that a later generation may move, and resolving it here would + // mean joining the host's presence state into a request-path listing — which + // would require granting request traffic read over every organisation's + // presence to answer about one. A consumer that needs the alias asks + // ListAvailableSolutions, which serves accepted applied state, and joins on + // this id. + // + // Empty only on a revoked historical row whose target could not be + // established at the cutover; an active installation always names one. + string target_id = 11 [ + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + (buf.validate.field).string.uuid = true + ]; + // Why a revoked installation was revoked, when it was not an administrator + // uninstalling: the installation-identity cutover, or a tenancy narrowing that + // withdrew this organisation. Empty on an active row. + string revoked_reason = 12 [(buf.validate.field).string.max_len = 512]; + // Why the install step failed: the solution's bounded refusal reason, or a + // fixed reason when it never answered. Empty unless status is FAILED. + string failure_reason = 13 [(buf.validate.field).string.max_len = 512]; +} + message InstallSolutionRequest { string org_id = 1 [(buf.validate.field).string.uuid = true]; // Canonical "publisher/name:version" of the solution's agent. The agent @@ -142,6 +219,42 @@ message GetInstallationResponse { InstallationHealth health = 2; } +// ListInstallationsRequest asks what one organization has installed. Every other +// RPC on this service names a single installation, so until this existed nothing +// could answer "what has this org installed?" — which is the authority read a +// per-organization solution projection is built on (issue #949). +message ListInstallationsRequest { + string org_id = 1 [(buf.validate.field).string.uuid = true]; + // Restrict to one lifecycle status. UNSPECIFIED returns every status, so a + // caller that must see only live installs asks for ACTIVE explicitly rather + // than leaning on a default that would silently widen if the enum grew. + InstallationStatus status = 2; + // Page bound. 0 -> server default; capped so an organization with many + // installations can never return an unbounded result in one call. + int32 page_size = 3 [(buf.validate.field).int32 = { + gte: 0 + lte: 500 + }]; + // Opaque cursor from a previous response's next_page_token; empty for page one. + string page_token = 4; +} + +// InstallationSummary is one row of the listing: the installation, plus the health +// resolved live beside it exactly as GetInstallation resolves it for one. Health +// travels with the row because a consumer that renders an installed solution as +// unavailable would otherwise need one GetInstallation per installation. +message InstallationSummary { + Installation installation = 1; + InstallationHealth health = 2; +} + +message ListInstallationsResponse { + repeated InstallationSummary installations = 1; + // Set when more installations remain; pass it back as page_token. Empty on the + // last page. + string next_page_token = 2; +} + // InstallationService composes and administers installations. The headless mint // that turns an installation into a short-lived Work Context is // WorkContextService.StartInstallationTask. @@ -318,15 +431,11 @@ service InstallationService { // ListInstallations enumerates one organization's installations with their live // health. It is internal-tier on purpose: it takes the organization as a request // field, so only a caller holding the cluster-internal credential may ask, and - // the tenant it asks about is never one a browser supplied. The auth-gateway is - // its caller, for two reads. Its viewer projection asks about the tenant it - // projected from a verified identity; it sits beside - // PermissionService.ListAccessibleScopes for that reason, the two read together - // to answer what a viewer's organization installed and what that viewer was - // granted. Its declared module route's bearer-less arm asks about the tenant an - // edge-inspected Work Context names, a value this issuer signed, to admit a - // headless operation context only when the target is installed there; whether - // the context is one is WorkContextService/AdmitWorkloadOperation's answer. + // the tenant it asks about is the one the auth-gateway projected from a verified + // identity — never one a browser supplied. It sits beside + // PermissionService.ListAccessibleScopes for that reason: the two are read + // together to answer what a viewer's organization installed and what that viewer + // was granted. rpc ListInstallations(ListInstallationsRequest) returns (ListInstallationsResponse) { option (saas.policy.v1.method_policy) = { exposure: EXPOSURE_INTERNAL @@ -387,8 +496,7 @@ service InstallationService { // It is the org-admin companion to the internal ListInstallations, and it is a // separate method rather than a relaxed policy on that one: ListInstallations // is read by the gateway for an organisation it projected from a verified - // identity or that an edge-verified Work Context names, and a bare tenant - // credential must never reach it. This one binds + // identity, and a bare tenant credential must never reach it. This one binds // the organisation to the caller's own admin membership, exactly as // ListAvailableSolutions does, so no organisation's administrator can read // another's. The one exception is inherited, not added here: a platform diff --git a/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto b/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto index dd6ffc5f5..64acd9ec3 100644 --- a/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto +++ b/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto @@ -8,7 +8,7 @@ import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; import "saas/accounts/v1/authorization.proto"; import "saas/accounts/v1/common.proto"; -import "saas/accounts/v1/installation_listing.proto"; +import "saas/accounts/v1/installations.proto"; import "saas/accounts/v1/solution_registry.proto"; import "saas/events/v1/operations.proto"; import "saas/jobs/v1/jobs.proto"; diff --git a/module/services/accounts/proto/saas/accounts/v1/workload_operation_admission.proto b/module/services/accounts/proto/saas/accounts/v1/workload_operation_admission.proto index 3bc644315..006f844b3 100644 --- a/module/services/accounts/proto/saas/accounts/v1/workload_operation_admission.proto +++ b/module/services/accounts/proto/saas/accounts/v1/workload_operation_admission.proto @@ -18,7 +18,7 @@ import "buf/validate/validate.proto"; // // They live in their own file, with deliberately minimal imports, so the // auth-gateway can generate the same types from the same source proto, as it -// does for installation_listing.proto and solution_entitlements.proto. The +// does for module_registration.proto and solution_entitlements.proto. The // gateway's copy is this file, byte for byte. // WorkloadOperationRefusal is why a capability was not admitted. The gateway diff --git a/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go b/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go deleted file mode 100644 index 0ea283153..000000000 --- a/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/installation_listing.pb.go +++ /dev/null @@ -1,607 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc (unknown) -// source: saas/accounts/v1/installation_listing.proto - -package accountsv1 - -import ( - reflect "reflect" - sync "sync" - unsafe "unsafe" - - _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - timestamppb "google.golang.org/protobuf/types/known/timestamppb" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -// InstallationStatus is the lifecycle of an installation row. An uninstall is a -// soft delete (status=revoked, revoked_at set) so the agent, grants, and audit -// trail remain resolvable. -type InstallationStatus int32 - -const ( - InstallationStatus_INSTALLATION_STATUS_UNSPECIFIED InstallationStatus = 0 - InstallationStatus_INSTALLATION_STATUS_ACTIVE InstallationStatus = 1 - InstallationStatus_INSTALLATION_STATUS_REVOKED InstallationStatus = 2 - // Installed, and waiting for the solution to acknowledge its standard install - // step. Nothing can use a pending installation. - InstallationStatus_INSTALLATION_STATUS_PENDING InstallationStatus = 3 - // The solution refused its install step, or never answered within the - // attempt budget. Nothing can use it; an administrator may retry it. - InstallationStatus_INSTALLATION_STATUS_FAILED InstallationStatus = 4 -) - -// Enum value maps for InstallationStatus. -var ( - InstallationStatus_name = map[int32]string{ - 0: "INSTALLATION_STATUS_UNSPECIFIED", - 1: "INSTALLATION_STATUS_ACTIVE", - 2: "INSTALLATION_STATUS_REVOKED", - 3: "INSTALLATION_STATUS_PENDING", - 4: "INSTALLATION_STATUS_FAILED", - } - InstallationStatus_value = map[string]int32{ - "INSTALLATION_STATUS_UNSPECIFIED": 0, - "INSTALLATION_STATUS_ACTIVE": 1, - "INSTALLATION_STATUS_REVOKED": 2, - "INSTALLATION_STATUS_PENDING": 3, - "INSTALLATION_STATUS_FAILED": 4, - } -) - -func (x InstallationStatus) Enum() *InstallationStatus { - p := new(InstallationStatus) - *p = x - return p -} - -func (x InstallationStatus) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (InstallationStatus) Descriptor() protoreflect.EnumDescriptor { - return file_saas_accounts_v1_installation_listing_proto_enumTypes[0].Descriptor() -} - -func (InstallationStatus) Type() protoreflect.EnumType { - return &file_saas_accounts_v1_installation_listing_proto_enumTypes[0] -} - -func (x InstallationStatus) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use InstallationStatus.Descriptor instead. -func (InstallationStatus) EnumDescriptor() ([]byte, []int) { - return file_saas_accounts_v1_installation_listing_proto_rawDescGZIP(), []int{0} -} - -// InstallationHealth is resolved live from current authority facts, never -// stored. It is the observable form of the fail-closed mint rule: a -// StartInstallationTask fails exactly when health is not HEALTHY, and transfer -// or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record -// nor any co-owner is currently an org admin (the Zapier-class "connection owner -// left the org" failure), surfaced rather than silently reassigned. -type InstallationHealth int32 - -const ( - InstallationHealth_INSTALLATION_HEALTH_UNSPECIFIED InstallationHealth = 0 - InstallationHealth_INSTALLATION_HEALTH_HEALTHY InstallationHealth = 1 - InstallationHealth_INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER InstallationHealth = 2 - InstallationHealth_INSTALLATION_HEALTH_AGENT_REVOKED InstallationHealth = 3 - InstallationHealth_INSTALLATION_HEALTH_AGENT_DISABLED InstallationHealth = 4 - InstallationHealth_INSTALLATION_HEALTH_STANDING_GRANT_MISSING InstallationHealth = 5 -) - -// Enum value maps for InstallationHealth. -var ( - InstallationHealth_name = map[int32]string{ - 0: "INSTALLATION_HEALTH_UNSPECIFIED", - 1: "INSTALLATION_HEALTH_HEALTHY", - 2: "INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER", - 3: "INSTALLATION_HEALTH_AGENT_REVOKED", - 4: "INSTALLATION_HEALTH_AGENT_DISABLED", - 5: "INSTALLATION_HEALTH_STANDING_GRANT_MISSING", - } - InstallationHealth_value = map[string]int32{ - "INSTALLATION_HEALTH_UNSPECIFIED": 0, - "INSTALLATION_HEALTH_HEALTHY": 1, - "INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER": 2, - "INSTALLATION_HEALTH_AGENT_REVOKED": 3, - "INSTALLATION_HEALTH_AGENT_DISABLED": 4, - "INSTALLATION_HEALTH_STANDING_GRANT_MISSING": 5, - } -) - -func (x InstallationHealth) Enum() *InstallationHealth { - p := new(InstallationHealth) - *p = x - return p -} - -func (x InstallationHealth) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (InstallationHealth) Descriptor() protoreflect.EnumDescriptor { - return file_saas_accounts_v1_installation_listing_proto_enumTypes[1].Descriptor() -} - -func (InstallationHealth) Type() protoreflect.EnumType { - return &file_saas_accounts_v1_installation_listing_proto_enumTypes[1] -} - -func (x InstallationHealth) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use InstallationHealth.Descriptor instead. -func (InstallationHealth) EnumDescriptor() ([]byte, []int) { - return file_saas_accounts_v1_installation_listing_proto_rawDescGZIP(), []int{1} -} - -// Installation is one solution's bound identity in an org. Server-assigned ids; -// the browser cannot attest to tenancy, the agent principal, or the scope node. -type Installation struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - OrgId string `protobuf:"bytes,2,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` - AgentPrincipalId string `protobuf:"bytes,3,opt,name=agent_principal_id,json=agentPrincipalId,proto3" json:"agent_principal_id,omitempty"` - OwnerPrincipalId string `protobuf:"bytes,5,opt,name=owner_principal_id,json=ownerPrincipalId,proto3" json:"owner_principal_id,omitempty"` - CoOwnerPrincipalIds []string `protobuf:"bytes,6,rep,name=co_owner_principal_ids,json=coOwnerPrincipalIds,proto3" json:"co_owner_principal_ids,omitempty"` - RootScopeNodeId string `protobuf:"bytes,7,opt,name=root_scope_node_id,json=rootScopeNodeId,proto3" json:"root_scope_node_id,omitempty"` - Status InstallationStatus `protobuf:"varint,8,opt,name=status,proto3,enum=saas.accounts.v1.InstallationStatus" json:"status,omitempty"` - CreatedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` - RevokedAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=revoked_at,json=revokedAt,proto3,oneof" json:"revoked_at,omitempty"` - // The immutable solution target this installation was consented to: one - // continuous period of one binding's presence on this host. It is never - // reused, so a replacement binding — even one claiming the same route alias — - // is a different target and inherits nothing. - // - // Deliberately NOT accompanied by the route alias. The alias is a property of - // the target that a later generation may move, and resolving it here would - // mean joining the host's presence state into a request-path listing — which - // would require granting request traffic read over every organisation's - // presence to answer about one. A consumer that needs the alias asks - // ListAvailableSolutions, which serves accepted applied state, and joins on - // this id. - // - // Empty only on a revoked historical row whose target could not be - // established at the cutover; an active installation always names one. - TargetId string `protobuf:"bytes,11,opt,name=target_id,json=targetId,proto3" json:"target_id,omitempty"` - // Why a revoked installation was revoked, when it was not an administrator - // uninstalling: the installation-identity cutover, or a tenancy narrowing that - // withdrew this organisation. Empty on an active row. - RevokedReason string `protobuf:"bytes,12,opt,name=revoked_reason,json=revokedReason,proto3" json:"revoked_reason,omitempty"` - // Why the install step failed: the solution's bounded refusal reason, or a - // fixed reason when it never answered. Empty unless status is FAILED. - FailureReason string `protobuf:"bytes,13,opt,name=failure_reason,json=failureReason,proto3" json:"failure_reason,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Installation) Reset() { - *x = Installation{} - mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Installation) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Installation) ProtoMessage() {} - -func (x *Installation) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Installation.ProtoReflect.Descriptor instead. -func (*Installation) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installation_listing_proto_rawDescGZIP(), []int{0} -} - -func (x *Installation) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *Installation) GetOrgId() string { - if x != nil { - return x.OrgId - } - return "" -} - -func (x *Installation) GetAgentPrincipalId() string { - if x != nil { - return x.AgentPrincipalId - } - return "" -} - -func (x *Installation) GetOwnerPrincipalId() string { - if x != nil { - return x.OwnerPrincipalId - } - return "" -} - -func (x *Installation) GetCoOwnerPrincipalIds() []string { - if x != nil { - return x.CoOwnerPrincipalIds - } - return nil -} - -func (x *Installation) GetRootScopeNodeId() string { - if x != nil { - return x.RootScopeNodeId - } - return "" -} - -func (x *Installation) GetStatus() InstallationStatus { - if x != nil { - return x.Status - } - return InstallationStatus_INSTALLATION_STATUS_UNSPECIFIED -} - -func (x *Installation) GetCreatedAt() *timestamppb.Timestamp { - if x != nil { - return x.CreatedAt - } - return nil -} - -func (x *Installation) GetRevokedAt() *timestamppb.Timestamp { - if x != nil { - return x.RevokedAt - } - return nil -} - -func (x *Installation) GetTargetId() string { - if x != nil { - return x.TargetId - } - return "" -} - -func (x *Installation) GetRevokedReason() string { - if x != nil { - return x.RevokedReason - } - return "" -} - -func (x *Installation) GetFailureReason() string { - if x != nil { - return x.FailureReason - } - return "" -} - -// ListInstallationsRequest asks what one organization has installed. Every other -// RPC on this service names a single installation, so until this existed nothing -// could answer "what has this org installed?" — which is the authority read a -// per-organization solution projection is built on (issue #949). -type ListInstallationsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` - // Restrict to one lifecycle status. UNSPECIFIED returns every status, so a - // caller that must see only live installs asks for ACTIVE explicitly rather - // than leaning on a default that would silently widen if the enum grew. - Status InstallationStatus `protobuf:"varint,2,opt,name=status,proto3,enum=saas.accounts.v1.InstallationStatus" json:"status,omitempty"` - // Page bound. 0 -> server default; capped so an organization with many - // installations can never return an unbounded result in one call. - PageSize int32 `protobuf:"varint,3,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` - // Opaque cursor from a previous response's next_page_token; empty for page one. - PageToken string `protobuf:"bytes,4,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListInstallationsRequest) Reset() { - *x = ListInstallationsRequest{} - mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListInstallationsRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListInstallationsRequest) ProtoMessage() {} - -func (x *ListInstallationsRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[1] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ListInstallationsRequest.ProtoReflect.Descriptor instead. -func (*ListInstallationsRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installation_listing_proto_rawDescGZIP(), []int{1} -} - -func (x *ListInstallationsRequest) GetOrgId() string { - if x != nil { - return x.OrgId - } - return "" -} - -func (x *ListInstallationsRequest) GetStatus() InstallationStatus { - if x != nil { - return x.Status - } - return InstallationStatus_INSTALLATION_STATUS_UNSPECIFIED -} - -func (x *ListInstallationsRequest) GetPageSize() int32 { - if x != nil { - return x.PageSize - } - return 0 -} - -func (x *ListInstallationsRequest) GetPageToken() string { - if x != nil { - return x.PageToken - } - return "" -} - -// InstallationSummary is one row of the listing: the installation, plus the health -// resolved live beside it exactly as GetInstallation resolves it for one. Health -// travels with the row because a consumer that renders an installed solution as -// unavailable would otherwise need one GetInstallation per installation. -type InstallationSummary struct { - state protoimpl.MessageState `protogen:"open.v1"` - Installation *Installation `protobuf:"bytes,1,opt,name=installation,proto3" json:"installation,omitempty"` - Health InstallationHealth `protobuf:"varint,2,opt,name=health,proto3,enum=saas.accounts.v1.InstallationHealth" json:"health,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *InstallationSummary) Reset() { - *x = InstallationSummary{} - mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *InstallationSummary) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*InstallationSummary) ProtoMessage() {} - -func (x *InstallationSummary) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[2] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use InstallationSummary.ProtoReflect.Descriptor instead. -func (*InstallationSummary) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installation_listing_proto_rawDescGZIP(), []int{2} -} - -func (x *InstallationSummary) GetInstallation() *Installation { - if x != nil { - return x.Installation - } - return nil -} - -func (x *InstallationSummary) GetHealth() InstallationHealth { - if x != nil { - return x.Health - } - return InstallationHealth_INSTALLATION_HEALTH_UNSPECIFIED -} - -type ListInstallationsResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Installations []*InstallationSummary `protobuf:"bytes,1,rep,name=installations,proto3" json:"installations,omitempty"` - // Set when more installations remain; pass it back as page_token. Empty on the - // last page. - NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListInstallationsResponse) Reset() { - *x = ListInstallationsResponse{} - mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListInstallationsResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListInstallationsResponse) ProtoMessage() {} - -func (x *ListInstallationsResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installation_listing_proto_msgTypes[3] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ListInstallationsResponse.ProtoReflect.Descriptor instead. -func (*ListInstallationsResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installation_listing_proto_rawDescGZIP(), []int{3} -} - -func (x *ListInstallationsResponse) GetInstallations() []*InstallationSummary { - if x != nil { - return x.Installations - } - return nil -} - -func (x *ListInstallationsResponse) GetNextPageToken() string { - if x != nil { - return x.NextPageToken - } - return "" -} - -var File_saas_accounts_v1_installation_listing_proto protoreflect.FileDescriptor - -const file_saas_accounts_v1_installation_listing_proto_rawDesc = "" + - "\n" + - "+saas/accounts/v1/installation_listing.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x94\x05\n" + - "\fInstallation\x12\x18\n" + - "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id\x12\x1f\n" + - "\x06org_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x126\n" + - "\x12agent_principal_id\x18\x03 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x10agentPrincipalId\x126\n" + - "\x12owner_principal_id\x18\x05 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x10ownerPrincipalId\x123\n" + - "\x16co_owner_principal_ids\x18\x06 \x03(\tR\x13coOwnerPrincipalIds\x125\n" + - "\x12root_scope_node_id\x18\a \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0frootScopeNodeId\x12<\n" + - "\x06status\x18\b \x01(\x0e2$.saas.accounts.v1.InstallationStatusR\x06status\x129\n" + - "\n" + - "created_at\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12>\n" + - "\n" + - "revoked_at\x18\n" + - " \x01(\v2\x1a.google.protobuf.TimestampH\x00R\trevokedAt\x88\x01\x01\x12(\n" + - "\ttarget_id\x18\v \x01(\tB\v\xbaH\b\xd8\x01\x01r\x03\xb0\x01\x01R\btargetId\x12/\n" + - "\x0erevoked_reason\x18\f \x01(\tB\b\xbaH\x05r\x03\x18\x80\x04R\rrevokedReason\x12/\n" + - "\x0efailure_reason\x18\r \x01(\tB\b\xbaH\x05r\x03\x18\x80\x04R\rfailureReasonB\r\n" + - "\v_revoked_atJ\x04\b\x04\x10\x05R\x13solution_identifier\"\xc1\x01\n" + - "\x18ListInstallationsRequest\x12\x1f\n" + - "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x12<\n" + - "\x06status\x18\x02 \x01(\x0e2$.saas.accounts.v1.InstallationStatusR\x06status\x12'\n" + - "\tpage_size\x18\x03 \x01(\x05B\n" + - "\xbaH\a\x1a\x05\x18\xf4\x03(\x00R\bpageSize\x12\x1d\n" + - "\n" + - "page_token\x18\x04 \x01(\tR\tpageToken\"\x97\x01\n" + - "\x13InstallationSummary\x12B\n" + - "\finstallation\x18\x01 \x01(\v2\x1e.saas.accounts.v1.InstallationR\finstallation\x12<\n" + - "\x06health\x18\x02 \x01(\x0e2$.saas.accounts.v1.InstallationHealthR\x06health\"\x90\x01\n" + - "\x19ListInstallationsResponse\x12K\n" + - "\rinstallations\x18\x01 \x03(\v2%.saas.accounts.v1.InstallationSummaryR\rinstallations\x12&\n" + - "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken*\xbb\x01\n" + - "\x12InstallationStatus\x12#\n" + - "\x1fINSTALLATION_STATUS_UNSPECIFIED\x10\x00\x12\x1e\n" + - "\x1aINSTALLATION_STATUS_ACTIVE\x10\x01\x12\x1f\n" + - "\x1bINSTALLATION_STATUS_REVOKED\x10\x02\x12\x1f\n" + - "\x1bINSTALLATION_STATUS_PENDING\x10\x03\x12\x1e\n" + - "\x1aINSTALLATION_STATUS_FAILED\x10\x04*\x84\x02\n" + - "\x12InstallationHealth\x12#\n" + - "\x1fINSTALLATION_HEALTH_UNSPECIFIED\x10\x00\x12\x1f\n" + - "\x1bINSTALLATION_HEALTH_HEALTHY\x10\x01\x12)\n" + - "%INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER\x10\x02\x12%\n" + - "!INSTALLATION_HEALTH_AGENT_REVOKED\x10\x03\x12&\n" + - "\"INSTALLATION_HEALTH_AGENT_DISABLED\x10\x04\x12.\n" + - "*INSTALLATION_HEALTH_STANDING_GRANT_MISSING\x10\x05B\xc4\x01\n" + - "\x14com.saas.accounts.v1B\x18InstallationListingProtoP\x01Z0auth-gateway/pkg/gen/saas/accounts/v1;accountsv1\xa2\x02\x03SAX\xaa\x02\x10Saas.Accounts.V1\xca\x02\x10Saas\\Accounts\\V1\xe2\x02\x1cSaas\\Accounts\\V1\\GPBMetadata\xea\x02\x12Saas::Accounts::V1b\x06proto3" - -var ( - file_saas_accounts_v1_installation_listing_proto_rawDescOnce sync.Once - file_saas_accounts_v1_installation_listing_proto_rawDescData []byte -) - -func file_saas_accounts_v1_installation_listing_proto_rawDescGZIP() []byte { - file_saas_accounts_v1_installation_listing_proto_rawDescOnce.Do(func() { - file_saas_accounts_v1_installation_listing_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_installation_listing_proto_rawDesc), len(file_saas_accounts_v1_installation_listing_proto_rawDesc))) - }) - return file_saas_accounts_v1_installation_listing_proto_rawDescData -} - -var file_saas_accounts_v1_installation_listing_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_saas_accounts_v1_installation_listing_proto_msgTypes = make([]protoimpl.MessageInfo, 4) -var file_saas_accounts_v1_installation_listing_proto_goTypes = []any{ - (InstallationStatus)(0), // 0: saas.accounts.v1.InstallationStatus - (InstallationHealth)(0), // 1: saas.accounts.v1.InstallationHealth - (*Installation)(nil), // 2: saas.accounts.v1.Installation - (*ListInstallationsRequest)(nil), // 3: saas.accounts.v1.ListInstallationsRequest - (*InstallationSummary)(nil), // 4: saas.accounts.v1.InstallationSummary - (*ListInstallationsResponse)(nil), // 5: saas.accounts.v1.ListInstallationsResponse - (*timestamppb.Timestamp)(nil), // 6: google.protobuf.Timestamp -} -var file_saas_accounts_v1_installation_listing_proto_depIdxs = []int32{ - 0, // 0: saas.accounts.v1.Installation.status:type_name -> saas.accounts.v1.InstallationStatus - 6, // 1: saas.accounts.v1.Installation.created_at:type_name -> google.protobuf.Timestamp - 6, // 2: saas.accounts.v1.Installation.revoked_at:type_name -> google.protobuf.Timestamp - 0, // 3: saas.accounts.v1.ListInstallationsRequest.status:type_name -> saas.accounts.v1.InstallationStatus - 2, // 4: saas.accounts.v1.InstallationSummary.installation:type_name -> saas.accounts.v1.Installation - 1, // 5: saas.accounts.v1.InstallationSummary.health:type_name -> saas.accounts.v1.InstallationHealth - 4, // 6: saas.accounts.v1.ListInstallationsResponse.installations:type_name -> saas.accounts.v1.InstallationSummary - 7, // [7:7] is the sub-list for method output_type - 7, // [7:7] is the sub-list for method input_type - 7, // [7:7] is the sub-list for extension type_name - 7, // [7:7] is the sub-list for extension extendee - 0, // [0:7] is the sub-list for field type_name -} - -func init() { file_saas_accounts_v1_installation_listing_proto_init() } -func file_saas_accounts_v1_installation_listing_proto_init() { - if File_saas_accounts_v1_installation_listing_proto != nil { - return - } - file_saas_accounts_v1_installation_listing_proto_msgTypes[0].OneofWrappers = []any{} - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_installation_listing_proto_rawDesc), len(file_saas_accounts_v1_installation_listing_proto_rawDesc)), - NumEnums: 2, - NumMessages: 4, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_saas_accounts_v1_installation_listing_proto_goTypes, - DependencyIndexes: file_saas_accounts_v1_installation_listing_proto_depIdxs, - EnumInfos: file_saas_accounts_v1_installation_listing_proto_enumTypes, - MessageInfos: file_saas_accounts_v1_installation_listing_proto_msgTypes, - }.Build() - File_saas_accounts_v1_installation_listing_proto = out.File - file_saas_accounts_v1_installation_listing_proto_goTypes = nil - file_saas_accounts_v1_installation_listing_proto_depIdxs = nil -} diff --git a/module/services/auth-gateway/code/vendored_accounts_protos_test.go b/module/services/auth-gateway/code/vendored_accounts_protos_test.go index 24cd85ff5..8f540123b 100644 --- a/module/services/auth-gateway/code/vendored_accounts_protos_test.go +++ b/module/services/auth-gateway/code/vendored_accounts_protos_test.go @@ -11,7 +11,7 @@ import ( // TestVendoredAccountsProtosAreAccounts holds every accounts proto this gateway // generates from byte-identical to the file accounts owns. The gateway carries -// only the files whose imports it can resolve (installation_listing.proto, +// only the files whose imports it can resolve (workload_operation_admission.proto, // module_registration.proto, solution_entitlements.proto and their peers), and // accounts keeps those files import-minimal for that reason. A copy that // drifted would decode a different wire shape from the one accounts writes, diff --git a/module/services/auth-gateway/proto/saas/accounts/v1/installation_listing.proto b/module/services/auth-gateway/proto/saas/accounts/v1/installation_listing.proto deleted file mode 100644 index 57150f92b..000000000 --- a/module/services/auth-gateway/proto/saas/accounts/v1/installation_listing.proto +++ /dev/null @@ -1,131 +0,0 @@ -syntax = "proto3"; - -package saas.accounts.v1; - -import "buf/validate/validate.proto"; -import "google/protobuf/timestamp.proto"; - -// The installation listing: an installation, its lifecycle and health, and the -// messages InstallationService/ListInstallations takes and answers. -// -// These definitions live in their own file, with deliberately minimal imports, -// so the auth-gateway can generate the same types from the same source proto -// and call ListInstallations over the internal listener, as it does for -// module_registration.proto and solution_entitlements.proto. installations.proto -// imports this file; the service and every other installation message stay -// there. Sharing the generated definition rather than restating the wire shape -// is what keeps the two sides from drifting: the gateway's copy is this file, -// byte for byte. - -// InstallationStatus is the lifecycle of an installation row. An uninstall is a -// soft delete (status=revoked, revoked_at set) so the agent, grants, and audit -// trail remain resolvable. -enum InstallationStatus { - INSTALLATION_STATUS_UNSPECIFIED = 0; - INSTALLATION_STATUS_ACTIVE = 1; - INSTALLATION_STATUS_REVOKED = 2; - // Installed, and waiting for the solution to acknowledge its standard install - // step. Nothing can use a pending installation. - INSTALLATION_STATUS_PENDING = 3; - // The solution refused its install step, or never answered within the - // attempt budget. Nothing can use it; an administrator may retry it. - INSTALLATION_STATUS_FAILED = 4; -} - -// InstallationHealth is resolved live from current authority facts, never -// stored. It is the observable form of the fail-closed mint rule: a -// StartInstallationTask fails exactly when health is not HEALTHY, and transfer -// or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record -// nor any co-owner is currently an org admin (the Zapier-class "connection owner -// left the org" failure), surfaced rather than silently reassigned. -enum InstallationHealth { - INSTALLATION_HEALTH_UNSPECIFIED = 0; - INSTALLATION_HEALTH_HEALTHY = 1; - INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER = 2; - INSTALLATION_HEALTH_AGENT_REVOKED = 3; - INSTALLATION_HEALTH_AGENT_DISABLED = 4; - INSTALLATION_HEALTH_STANDING_GRANT_MISSING = 5; -} - -// Installation is one solution's bound identity in an org. Server-assigned ids; -// the browser cannot attest to tenancy, the agent principal, or the scope node. -message Installation { - string id = 1 [(buf.validate.field).string.uuid = true]; - string org_id = 2 [(buf.validate.field).string.uuid = true]; - string agent_principal_id = 3 [(buf.validate.field).string.uuid = true]; - // Was `solution_identifier`: the free-text route alias. It was the join key - // every reader used, and because an alias is deliberately reusable by a later - // binding, an installation that named one inherited whatever took it next. - // Replaced by target_id below. Reserved rather than reused so no consumer can - // read an alias out of a field that now carries an identity. - reserved 4; - reserved "solution_identifier"; - string owner_principal_id = 5 [(buf.validate.field).string.uuid = true]; - repeated string co_owner_principal_ids = 6; - string root_scope_node_id = 7 [(buf.validate.field).string.uuid = true]; - InstallationStatus status = 8; - google.protobuf.Timestamp created_at = 9; - optional google.protobuf.Timestamp revoked_at = 10; - // The immutable solution target this installation was consented to: one - // continuous period of one binding's presence on this host. It is never - // reused, so a replacement binding — even one claiming the same route alias — - // is a different target and inherits nothing. - // - // Deliberately NOT accompanied by the route alias. The alias is a property of - // the target that a later generation may move, and resolving it here would - // mean joining the host's presence state into a request-path listing — which - // would require granting request traffic read over every organisation's - // presence to answer about one. A consumer that needs the alias asks - // ListAvailableSolutions, which serves accepted applied state, and joins on - // this id. - // - // Empty only on a revoked historical row whose target could not be - // established at the cutover; an active installation always names one. - string target_id = 11 [ - (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, - (buf.validate.field).string.uuid = true - ]; - // Why a revoked installation was revoked, when it was not an administrator - // uninstalling: the installation-identity cutover, or a tenancy narrowing that - // withdrew this organisation. Empty on an active row. - string revoked_reason = 12 [(buf.validate.field).string.max_len = 512]; - // Why the install step failed: the solution's bounded refusal reason, or a - // fixed reason when it never answered. Empty unless status is FAILED. - string failure_reason = 13 [(buf.validate.field).string.max_len = 512]; -} - -// ListInstallationsRequest asks what one organization has installed. Every other -// RPC on this service names a single installation, so until this existed nothing -// could answer "what has this org installed?" — which is the authority read a -// per-organization solution projection is built on (issue #949). -message ListInstallationsRequest { - string org_id = 1 [(buf.validate.field).string.uuid = true]; - // Restrict to one lifecycle status. UNSPECIFIED returns every status, so a - // caller that must see only live installs asks for ACTIVE explicitly rather - // than leaning on a default that would silently widen if the enum grew. - InstallationStatus status = 2; - // Page bound. 0 -> server default; capped so an organization with many - // installations can never return an unbounded result in one call. - int32 page_size = 3 [(buf.validate.field).int32 = { - gte: 0 - lte: 500 - }]; - // Opaque cursor from a previous response's next_page_token; empty for page one. - string page_token = 4; -} - -// InstallationSummary is one row of the listing: the installation, plus the health -// resolved live beside it exactly as GetInstallation resolves it for one. Health -// travels with the row because a consumer that renders an installed solution as -// unavailable would otherwise need one GetInstallation per installation. -message InstallationSummary { - Installation installation = 1; - InstallationHealth health = 2; -} - -message ListInstallationsResponse { - repeated InstallationSummary installations = 1; - // Set when more installations remain; pass it back as page_token. Empty on the - // last page. - string next_page_token = 2; -} diff --git a/module/services/auth-gateway/proto/saas/accounts/v1/workload_operation_admission.proto b/module/services/auth-gateway/proto/saas/accounts/v1/workload_operation_admission.proto index 3bc644315..006f844b3 100644 --- a/module/services/auth-gateway/proto/saas/accounts/v1/workload_operation_admission.proto +++ b/module/services/auth-gateway/proto/saas/accounts/v1/workload_operation_admission.proto @@ -18,7 +18,7 @@ import "buf/validate/validate.proto"; // // They live in their own file, with deliberately minimal imports, so the // auth-gateway can generate the same types from the same source proto, as it -// does for installation_listing.proto and solution_entitlements.proto. The +// does for module_registration.proto and solution_entitlements.proto. The // gateway's copy is this file, byte for byte. // WorkloadOperationRefusal is why a capability was not admitted. The gateway diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json index 379b3573d..e85a11ebd 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json +++ b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contract.binpb", - "digest": "sha256:c9df546fead8be738783f6fba95b7a35c8037576c2e6362077f7e44406ca3e32", + "digest": "sha256:978af58084c99638d6b0d979ea5991d78439f6b4d5deca0d34ff4d547e675d0c", "services": [ { "name": "APIKeyService", diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb b/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb index 99f9f12a78efb6ba8a5f898f5af449dec14b52d4..7ef3f1903569272098d3729e4fc7b7204c1c9ece 100644 GIT binary patch delta 15161 zcmZ8odz=-;u|D0?(|gX$z{0T0!UD@dd8oVvQ9wZi1T=~PRE$OfxCEln1Tf}a^ZOki ztKuUPym&`Z5Uxh#p@0fU5KuuLA`t}y0g;D_qNu1S@-p|U>Yka6f9wyw>h9{Q`l_pY zdgdJ7I_R#AgYKviHL}L3Q8jLjt|_U>*5qoum;LXbcc*?My_V6TO@6az=q7)B^v&m` z1ER)mNmX&e3w|Vv-QM+2xs`^$^(eeKtCqJ`_TZlQ@=;RT-SW@JW$PE*;! z^F2j~K7vl3C1#CG7X=i(zSMubv?bz81^I#|DeLZ^fE3jkJhuzm?ies#3w0ueCg0-$3@Yl@K6)=9Quzkmp|V=Dm&oz>PW zkkLYtU!CRYwkO=`EYGk#9rWznx{XET-Py9G?Fi@2mS@II=y-OUco@8Nx?nnDNl2GK zXWP+5$1WW^dGmxPOBDD%FEcvow!)uRI4@g9eH3WLi0(2=!wC`S5jR0|_naRi7~8#W zX^gT!^}2iYyc`Y7-j}P}5R&@m*>(Z7JrcE0)I;`)n?zV}dgNkF0(yId4dO=V?NN4t z)hn#t9_8oTc5<+1q8I-4l$XR!n5Ac~NsOSjXI1kUL2b|cAgfkZTD?764D^1Io})6k z(CH)VRDLO=!B5rA7aPCkUnvTG{J@OQ(Nd^|FfSpGMtY%hkx3@}AMv}$=WIt*9N5}7 zu@(08O>Ct!=$qK;P+MZF16%u&tp$#)V{PpVTMcCj+0ajVmNEwI<0eem N%+fF9 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 diff --git a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml index 8e1c70998..c86d9b367 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml +++ b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml @@ -12,7 +12,7 @@ sources: module: saas-starter service: accounts endpoint: connect - contract-digest: sha256:c9df546fead8be738783f6fba95b7a35c8037576c2e6362077f7e44406ca3e32 + contract-digest: sha256:978af58084c99638d6b0d979ea5991d78439f6b4d5deca0d34ff4d547e675d0c services: - APIKeyService - AccessibleScopeService @@ -52,6 +52,6 @@ sources: - WorkContextService as: accounts generated-by: - codefly: source-5234ce248b092edf1138a16ec4140dfbc8745522 + codefly: 0.1.191 companion: ghcr.io/codefly-dev/proto:0.0.16 facade: true diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installation_listing_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installation_listing_pb.ts deleted file mode 100644 index 934ddc7e4..000000000 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installation_listing_pb.ts +++ /dev/null @@ -1,306 +0,0 @@ -// @generated by protoc-gen-es v2.11.0 with parameter "target=ts,import_extension=none" -// @generated from file saas/accounts/v1/installation_listing.proto (package saas.accounts.v1, syntax proto3) -/* eslint-disable */ - -import type { GenEnum, GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; -import { enumDesc, fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; -import { file_buf_validate_validate } from "../../../buf/validate/validate_pb"; -import type { Timestamp } from "@bufbuild/protobuf/wkt"; -import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; -import type { Message } from "@bufbuild/protobuf"; - -/** - * Describes the file saas/accounts/v1/installation_listing.proto. - */ -export const file_saas_accounts_v1_installation_listing: GenFile = /*@__PURE__*/ - fileDesc("CitzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbl9saXN0aW5nLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIvkDCgxJbnN0YWxsYXRpb24SFAoCaWQYASABKAlCCLpIBXIDsAEBEhgKBm9yZ19pZBgCIAEoCUIIukgFcgOwAQESJAoSYWdlbnRfcHJpbmNpcGFsX2lkGAMgASgJQgi6SAVyA7ABARIkChJvd25lcl9wcmluY2lwYWxfaWQYBSABKAlCCLpIBXIDsAEBEh4KFmNvX293bmVyX3ByaW5jaXBhbF9pZHMYBiADKAkSJAoScm9vdF9zY29wZV9ub2RlX2lkGAcgASgJQgi6SAVyA7ABARI0CgZzdGF0dXMYCCABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN0YXR1cxIuCgpjcmVhdGVkX2F0GAkgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIzCgpyZXZva2VkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgAiAEBEh4KCXRhcmdldF9pZBgLIAEoCUILukgI2AEBcgOwAQESIAoOcmV2b2tlZF9yZWFzb24YDCABKAlCCLpIBXIDGIAEEiAKDmZhaWx1cmVfcmVhc29uGA0gASgJQgi6SAVyAxiABEINCgtfcmV2b2tlZF9hdEoECAQQBVITc29sdXRpb25faWRlbnRpZmllciKdAQoYTGlzdEluc3RhbGxhdGlvbnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoGc3RhdHVzGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdGF0dXMSHQoJcGFnZV9zaXplGAMgASgFQgq6SAcaBRj0AygAEhIKCnBhZ2VfdG9rZW4YBCABKAkigQEKE0luc3RhbGxhdGlvblN1bW1hcnkSNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SNAoGaGVhbHRoGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25IZWFsdGgicgoZTGlzdEluc3RhbGxhdGlvbnNSZXNwb25zZRI8Cg1pbnN0YWxsYXRpb25zGAEgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdW1tYXJ5EhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSq7AQoSSW5zdGFsbGF0aW9uU3RhdHVzEiMKH0lOU1RBTExBVElPTl9TVEFUVVNfVU5TUEVDSUZJRUQQABIeChpJTlNUQUxMQVRJT05fU1RBVFVTX0FDVElWRRABEh8KG0lOU1RBTExBVElPTl9TVEFUVVNfUkVWT0tFRBACEh8KG0lOU1RBTExBVElPTl9TVEFUVVNfUEVORElORxADEh4KGklOU1RBTExBVElPTl9TVEFUVVNfRkFJTEVEEAQqhAIKEkluc3RhbGxhdGlvbkhlYWx0aBIjCh9JTlNUQUxMQVRJT05fSEVBTFRIX1VOU1BFQ0lGSUVEEAASHwobSU5TVEFMTEFUSU9OX0hFQUxUSF9IRUFMVEhZEAESKQolSU5TVEFMTEFUSU9OX0hFQUxUSF9OT19FTElHSUJMRV9PV05FUhACEiUKIUlOU1RBTExBVElPTl9IRUFMVEhfQUdFTlRfUkVWT0tFRBADEiYKIklOU1RBTExBVElPTl9IRUFMVEhfQUdFTlRfRElTQUJMRUQQBBIuCipJTlNUQUxMQVRJT05fSEVBTFRIX1NUQU5ESU5HX0dSQU5UX01JU1NJTkcQBWIGcHJvdG8z", [file_buf_validate_validate, file_google_protobuf_timestamp]); - -/** - * Installation is one solution's bound identity in an org. Server-assigned ids; - * the browser cannot attest to tenancy, the agent principal, or the scope node. - * - * @generated from message saas.accounts.v1.Installation - */ -export type Installation = Message<"saas.accounts.v1.Installation"> & { - /** - * @generated from field: string id = 1; - */ - id: string; - - /** - * @generated from field: string org_id = 2; - */ - orgId: string; - - /** - * @generated from field: string agent_principal_id = 3; - */ - agentPrincipalId: string; - - /** - * @generated from field: string owner_principal_id = 5; - */ - ownerPrincipalId: string; - - /** - * @generated from field: repeated string co_owner_principal_ids = 6; - */ - coOwnerPrincipalIds: string[]; - - /** - * @generated from field: string root_scope_node_id = 7; - */ - rootScopeNodeId: string; - - /** - * @generated from field: saas.accounts.v1.InstallationStatus status = 8; - */ - status: InstallationStatus; - - /** - * @generated from field: google.protobuf.Timestamp created_at = 9; - */ - createdAt?: Timestamp; - - /** - * @generated from field: optional google.protobuf.Timestamp revoked_at = 10; - */ - revokedAt?: Timestamp; - - /** - * The immutable solution target this installation was consented to: one - * continuous period of one binding's presence on this host. It is never - * reused, so a replacement binding — even one claiming the same route alias — - * is a different target and inherits nothing. - * - * Deliberately NOT accompanied by the route alias. The alias is a property of - * the target that a later generation may move, and resolving it here would - * mean joining the host's presence state into a request-path listing — which - * would require granting request traffic read over every organisation's - * presence to answer about one. A consumer that needs the alias asks - * ListAvailableSolutions, which serves accepted applied state, and joins on - * this id. - * - * Empty only on a revoked historical row whose target could not be - * established at the cutover; an active installation always names one. - * - * @generated from field: string target_id = 11; - */ - targetId: string; - - /** - * Why a revoked installation was revoked, when it was not an administrator - * uninstalling: the installation-identity cutover, or a tenancy narrowing that - * withdrew this organisation. Empty on an active row. - * - * @generated from field: string revoked_reason = 12; - */ - revokedReason: string; - - /** - * Why the install step failed: the solution's bounded refusal reason, or a - * fixed reason when it never answered. Empty unless status is FAILED. - * - * @generated from field: string failure_reason = 13; - */ - failureReason: string; -}; - -/** - * Describes the message saas.accounts.v1.Installation. - * Use `create(InstallationSchema)` to create a new message. - */ -export const InstallationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installation_listing, 0); - -/** - * ListInstallationsRequest asks what one organization has installed. Every other - * RPC on this service names a single installation, so until this existed nothing - * could answer "what has this org installed?" — which is the authority read a - * per-organization solution projection is built on (issue #949). - * - * @generated from message saas.accounts.v1.ListInstallationsRequest - */ -export type ListInstallationsRequest = Message<"saas.accounts.v1.ListInstallationsRequest"> & { - /** - * @generated from field: string org_id = 1; - */ - orgId: string; - - /** - * Restrict to one lifecycle status. UNSPECIFIED returns every status, so a - * caller that must see only live installs asks for ACTIVE explicitly rather - * than leaning on a default that would silently widen if the enum grew. - * - * @generated from field: saas.accounts.v1.InstallationStatus status = 2; - */ - status: InstallationStatus; - - /** - * Page bound. 0 -> server default; capped so an organization with many - * installations can never return an unbounded result in one call. - * - * @generated from field: int32 page_size = 3; - */ - pageSize: number; - - /** - * Opaque cursor from a previous response's next_page_token; empty for page one. - * - * @generated from field: string page_token = 4; - */ - pageToken: string; -}; - -/** - * Describes the message saas.accounts.v1.ListInstallationsRequest. - * Use `create(ListInstallationsRequestSchema)` to create a new message. - */ -export const ListInstallationsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installation_listing, 1); - -/** - * InstallationSummary is one row of the listing: the installation, plus the health - * resolved live beside it exactly as GetInstallation resolves it for one. Health - * travels with the row because a consumer that renders an installed solution as - * unavailable would otherwise need one GetInstallation per installation. - * - * @generated from message saas.accounts.v1.InstallationSummary - */ -export type InstallationSummary = Message<"saas.accounts.v1.InstallationSummary"> & { - /** - * @generated from field: saas.accounts.v1.Installation installation = 1; - */ - installation?: Installation; - - /** - * @generated from field: saas.accounts.v1.InstallationHealth health = 2; - */ - health: InstallationHealth; -}; - -/** - * Describes the message saas.accounts.v1.InstallationSummary. - * Use `create(InstallationSummarySchema)` to create a new message. - */ -export const InstallationSummarySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installation_listing, 2); - -/** - * @generated from message saas.accounts.v1.ListInstallationsResponse - */ -export type ListInstallationsResponse = Message<"saas.accounts.v1.ListInstallationsResponse"> & { - /** - * @generated from field: repeated saas.accounts.v1.InstallationSummary installations = 1; - */ - installations: InstallationSummary[]; - - /** - * Set when more installations remain; pass it back as page_token. Empty on the - * last page. - * - * @generated from field: string next_page_token = 2; - */ - nextPageToken: string; -}; - -/** - * Describes the message saas.accounts.v1.ListInstallationsResponse. - * Use `create(ListInstallationsResponseSchema)` to create a new message. - */ -export const ListInstallationsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installation_listing, 3); - -/** - * InstallationStatus is the lifecycle of an installation row. An uninstall is a - * soft delete (status=revoked, revoked_at set) so the agent, grants, and audit - * trail remain resolvable. - * - * @generated from enum saas.accounts.v1.InstallationStatus - */ -export enum InstallationStatus { - /** - * @generated from enum value: INSTALLATION_STATUS_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - - /** - * @generated from enum value: INSTALLATION_STATUS_ACTIVE = 1; - */ - ACTIVE = 1, - - /** - * @generated from enum value: INSTALLATION_STATUS_REVOKED = 2; - */ - REVOKED = 2, - - /** - * Installed, and waiting for the solution to acknowledge its standard install - * step. Nothing can use a pending installation. - * - * @generated from enum value: INSTALLATION_STATUS_PENDING = 3; - */ - PENDING = 3, - - /** - * The solution refused its install step, or never answered within the - * attempt budget. Nothing can use it; an administrator may retry it. - * - * @generated from enum value: INSTALLATION_STATUS_FAILED = 4; - */ - FAILED = 4, -} - -/** - * Describes the enum saas.accounts.v1.InstallationStatus. - */ -export const InstallationStatusSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_saas_accounts_v1_installation_listing, 0); - -/** - * InstallationHealth is resolved live from current authority facts, never - * stored. It is the observable form of the fail-closed mint rule: a - * StartInstallationTask fails exactly when health is not HEALTHY, and transfer - * or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record - * nor any co-owner is currently an org admin (the Zapier-class "connection owner - * left the org" failure), surfaced rather than silently reassigned. - * - * @generated from enum saas.accounts.v1.InstallationHealth - */ -export enum InstallationHealth { - /** - * @generated from enum value: INSTALLATION_HEALTH_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - - /** - * @generated from enum value: INSTALLATION_HEALTH_HEALTHY = 1; - */ - HEALTHY = 1, - - /** - * @generated from enum value: INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER = 2; - */ - NO_ELIGIBLE_OWNER = 2, - - /** - * @generated from enum value: INSTALLATION_HEALTH_AGENT_REVOKED = 3; - */ - AGENT_REVOKED = 3, - - /** - * @generated from enum value: INSTALLATION_HEALTH_AGENT_DISABLED = 4; - */ - AGENT_DISABLED = 4, - - /** - * @generated from enum value: INSTALLATION_HEALTH_STANDING_GRANT_MISSING = 5; - */ - STANDING_GRANT_MISSING = 5, -} - -/** - * Describes the enum saas.accounts.v1.InstallationHealth. - */ -export const InstallationHealthSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_saas_accounts_v1_installation_listing, 1); diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installations_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installations_pb.ts index 9f5ed73c1..eb139cd43 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installations_pb.ts +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installations_pb.ts @@ -6,12 +6,10 @@ import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobu import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; import { file_buf_validate_validate } from "../../../buf/validate/validate_pb"; import { file_google_api_annotations } from "../../../google/api/annotations_pb"; -import type { EmptySchema } from "@bufbuild/protobuf/wkt"; -import { file_google_protobuf_empty } from "@bufbuild/protobuf/wkt"; +import type { EmptySchema, Timestamp } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_empty, file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; import type { CollectionReadGrant, ScopeGrant } from "./authorization_pb"; import { file_saas_accounts_v1_authorization } from "./authorization_pb"; -import type { Installation, InstallationHealth, InstallationSchema, ListInstallationsRequestSchema, ListInstallationsResponseSchema } from "./installation_listing_pb"; -import { file_saas_accounts_v1_installation_listing } from "./installation_listing_pb"; import { file_saas_policy_v1_options } from "../../policy/v1/options_pb"; import type { Message } from "@bufbuild/protobuf"; @@ -19,7 +17,105 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/installations.proto. */ export const file_saas_accounts_v1_installations: GenFile = /*@__PURE__*/ - fileDesc("CiRzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbnMucHJvdG8SEHNhYXMuYWNjb3VudHMudjEi4AMKFkluc3RhbGxTb2x1dGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIkChBhZ2VudF9pZGVudGlmaWVyGAIgASgJQgq6SAdyBRABGIAEEh4KDGRpc3BsYXlfbmFtZRgEIAEoCUIIukgFcgMYgAQSIgoQcm9vdF9zY29wZV9sYWJlbBgGIAEoCUIIukgFcgMYgAQSGQoHcm9sZV9pZBgHIAEoCUIIukgFcgOwAQESJwoSb3duZXJfcHJpbmNpcGFsX2lkGAggASgJQgu6SAjYAQFyA7ABARIvChZjb19vd25lcl9wcmluY2lwYWxfaWRzGAkgAygJQg+6SAySAQkQICIFcgOwAQESKgoRYWxsb3dlZF9hdWRpZW5jZXMYCiADKAlCD7pIDJIBCRBAIgVyAxiABBInCg5hbGxvd2VkX3Njb3BlcxgLIAMoCUIPukgMkgEJEEAiBXIDGIABEhsKCXRhcmdldF9pZBgMIAEoCUIIukgFcgOwAQESHwoFc2NvcGUYDSABKAlCC7pICHIGEAIogIBASACIAQFCCAoGX3Njb3BlSgQIAxAESgQIBRAGUhNzb2x1dGlvbl9pZGVudGlmaWVyUg9yb290X3Njb3BlX3BhdGgidAoZTmFycm93SW5zdGFsbGF0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESGgoFc2NvcGUYAyABKAlCC7pICHIGEAIogIBAIlcKGFJldHJ5SW5zdGFsbGF0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEiVwoYVW5pbnN0YWxsU29sdXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABASK+AQokVHJhbnNmZXJJbnN0YWxsYXRpb25Pd25lcnNoaXBSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABARIoChZuZXdfb3duZXJfcHJpbmNpcGFsX2lkGAMgASgJQgi6SAVyA7ABARIvChZjb19vd25lcl9wcmluY2lwYWxfaWRzGAQgAygJQg+6SAySAQkQICIFcgOwAQEiVQoWR2V0SW5zdGFsbGF0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEihQEKF0dldEluc3RhbGxhdGlvblJlc3BvbnNlEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEjQKBmhlYWx0aBgCIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uSGVhbHRoInMKJExpc3RPcmdhbml6YXRpb25JbnN0YWxsYXRpb25zUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEh0KCXBhZ2Vfc2l6ZRgCIAEoBUIKukgHGgUY9AMoABISCgpwYWdlX3Rva2VuGAMgASgJIsgDChhPcmdhbml6YXRpb25JbnN0YWxsYXRpb24SNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SNAoGaGVhbHRoGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25IZWFsdGgSMwoGdGFyZ2V0GAMgASgLMiMuc2Fhcy5hY2NvdW50cy52MS5BdmFpbGFibGVTb2x1dGlvbhIXCg9yb290X3Njb3BlX3BhdGgYBCABKAkSPAoNZ3JhbnRlZF90ZWFtcxgFIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ29sbGVjdGlvblJlYWRHcmFudBI+Cg9pbmhlcml0ZWRfdGVhbXMYBiADKAsyJS5zYWFzLmFjY291bnRzLnYxLkNvbGxlY3Rpb25SZWFkR3JhbnQSOgoJcmVhZGluZXNzGAcgASgLMicuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25SZWFkaW5lc3MSOAoPcmVhY2hlZF9tb2R1bGVzGAggAygLMh8uc2Fhcy5hY2NvdW50cy52MS5SZWFjaGVkTW9kdWxlIu0BChJSZWFkaW5lc3NDb25kaXRpb24SNgoEa2luZBgBIAEoDjIoLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQ29uZGl0aW9uS2luZBIPCgdzdWJqZWN0GAIgASgJEg4KBmRldGFpbBgDIAEoCRI4CgpjbGVhcmVkX2J5GAQgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5SZWFkaW5lc3NDbGVhcmVkQnkSMQoGYWN0aW9uGAUgASgOMiEuc2Fhcy5hY2NvdW50cy52MS5SZWFkaW5lc3NBY3Rpb24SEQoJc291cmNlX2lkGAYgASgJIlsKFUluc3RhbGxhdGlvblJlYWRpbmVzcxINCgVyZWFkeRgBIAEoCBIzCgV1bm1ldBgCIAMoCzIkLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQ29uZGl0aW9uIk8KDVJlYWNoZWRNb2R1bGUSEAoIYXVkaWVuY2UYASABKAkSFwoPaW5zdGFsbGF0aW9uX2lkGAIgASgJEhMKC3JvdXRlX2FsaWFzGAMgASgJIoMBCiVMaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1Jlc3BvbnNlEkEKDWluc3RhbGxhdGlvbnMYASADKAsyKi5zYWFzLmFjY291bnRzLnYxLk9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkikwEKHkdyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESGQoHdGVhbV9pZBgDIAEoCUIIukgFcgOwAQESGQoHcm9sZV9pZBgEIAEoCUIIukgFcgOwAQEidQoTQ2FycmllZE1vZHVsZUFjY2VzcxIQCghhdWRpZW5jZRgBIAEoCRIXCg9pbnN0YWxsYXRpb25faWQYAiABKAkSEgoKc2NvcGVfcGF0aBgDIAEoCRIPCgdyb2xlX2lkGAQgASgJEg4KBnNjb3BlcxgFIAMoCSK7AQofR3JhbnRJbnN0YWxsYXRpb25Ub1RlYW1SZXNwb25zZRIrCgVncmFudBgBIAEoCzIcLnNhYXMuYWNjb3VudHMudjEuU2NvcGVHcmFudBI2CgdjYXJyaWVkGAIgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5DYXJyaWVkTW9kdWxlQWNjZXNzEjMKBXVubWV0GAMgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5SZWFkaW5lc3NDb25kaXRpb24ilgEKIVJldm9rZUluc3RhbGxhdGlvbkZyb21UZWFtUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESGQoHdGVhbV9pZBgDIAEoCUIIukgFcgOwAQESGQoHcm9sZV9pZBgEIAEoCUIIukgFcgOwAQEihAIKEUF2YWlsYWJsZVNvbHV0aW9uEhsKCXRhcmdldF9pZBgBIAEoCUIIukgFcgOwAQESEgoKYmluZGluZ19pZBgCIAEoCRITCgtyb3V0ZV9hbGlhcxgDIAEoCRIZChFyZWxlYXNlX3B1Ymxpc2hlchgEIAEoCRIUCgxyZWxlYXNlX25hbWUYBSABKAkSFwoPcmVsZWFzZV92ZXJzaW9uGAYgASgJEhkKEW9wZW5lZF9nZW5lcmF0aW9uGAcgASgEEhoKEmFwcGxpZWRfZ2VuZXJhdGlvbhgIIAEoBBIRCglpbnN0YWxsZWQYCSABKAgSFQoEa2luZBgKIAEoCUIHukgEcgIYICJsCh1MaXN0QXZhaWxhYmxlU29sdXRpb25zUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEh0KCXBhZ2Vfc2l6ZRgCIAEoBUIKukgHGgUY9AMoABISCgpwYWdlX3Rva2VuGAMgASgJInEKHkxpc3RBdmFpbGFibGVTb2x1dGlvbnNSZXNwb25zZRI2Cglzb2x1dGlvbnMYASADKAsyIy5zYWFzLmFjY291bnRzLnYxLkF2YWlsYWJsZVNvbHV0aW9uEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSrzAwoWUmVhZGluZXNzQ29uZGl0aW9uS2luZBIoCiRSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfVU5TUEVDSUZJRUQQABIxCi1SRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfSU5TVEFMTF9TVEVQX1BFTkRJTkcQARIwCixSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfSU5TVEFMTF9TVEVQX0ZBSUxFRBACEi0KKVJFQURJTkVTU19DT05ESVRJT05fS0lORF9SRUNPUkRfVU5IRUFMVEhZEAMSNQoxUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX01PRFVMRV9QUklOQ0lQQUxfT01JVFRFRBAEEjYKMlJFQURJTkVTU19DT05ESVRJT05fS0lORF9PUEVSQVRJT05fQklORElOR19PTUlUVEVEEAUSMQotUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX1NPVVJDRV9OT1RfREVMRUdBVEVEEAYSOQo1UkVBRElORVNTX0NPTkRJVElPTl9LSU5EX1JFQUNIRURfTU9EVUxFX05PVF9JTlNUQUxMRUQQBxI+CjpSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfQ0FSUklFRF9NT0RVTEVfQUNDRVNTX05PVF9ERVJJVkVEEAgq1QEKElJlYWRpbmVzc0NsZWFyZWRCeRIkCiBSRUFESU5FU1NfQ0xFQVJFRF9CWV9VTlNQRUNJRklFRBAAEh8KG1JFQURJTkVTU19DTEVBUkVEX0JZX1ZJRVdFUhABEisKJ1JFQURJTkVTU19DTEVBUkVEX0JZX09SR0FOSVpBVElPTl9BRE1JThACEioKJlJFQURJTkVTU19DTEVBUkVEX0JZX1BMQVRGT1JNX09QRVJBVE9SEAMSHwobUkVBRElORVNTX0NMRUFSRURfQllfTk9CT0RZEAQqnwIKD1JlYWRpbmVzc0FjdGlvbhIgChxSRUFESU5FU1NfQUNUSU9OX1VOU1BFQ0lGSUVEEAASKQolUkVBRElORVNTX0FDVElPTl9OT05FX0FUX1RISVNfU1VSRkFDRRABEicKI1JFQURJTkVTU19BQ1RJT05fUkVUUllfSU5TVEFMTF9TVEVQEAISJQohUkVBRElORVNTX0FDVElPTl9SRUNPTk5FQ1RfU09VUkNFEAMSJwojUkVBRElORVNTX0FDVElPTl9UUkFOU0ZFUl9PV05FUlNISVAQBBIrCidSRUFESU5FU1NfQUNUSU9OX0lOU1RBTExfUkVBQ0hFRF9NT0RVTEUQBRIZChVSRUFESU5FU1NfQUNUSU9OX1dBSVQQBjKqEgoTSW5zdGFsbGF0aW9uU2VydmljZRK6AQoPSW5zdGFsbFNvbHV0aW9uEiguc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsU29sdXRpb25SZXF1ZXN0Gh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24iXcLzGD0IAhAEKgwKBm9yZ19pZBACGAEwATodChlzYWFzLmluc3RhbGxhdGlvbi5jcmVhdGVkEAJAAUgEUANYA2ABgtPkkwIWOgEqIhEvdjEvaW5zdGFsbGF0aW9ucxLSAQoRVW5pbnN0YWxsU29sdXRpb24SKi5zYWFzLmFjY291bnRzLnYxLlVuaW5zdGFsbFNvbHV0aW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJ5wvMYPQgCEAQqDAoGb3JnX2lkEAIYATABOh0KGXNhYXMuaW5zdGFsbGF0aW9uLnJldm9rZWQQAkABSARQA1gDYAGC0+STAjI6ASoiLS92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9OnVuaW5zdGFsbBLSAQoSTmFycm93SW5zdGFsbGF0aW9uEisuc2Fhcy5hY2NvdW50cy52MS5OYXJyb3dJbnN0YWxsYXRpb25SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5InfC8xg+CAIQBCoMCgZvcmdfaWQQAhgBMAE6Hgoac2Fhcy5pbnN0YWxsYXRpb24ubmFycm93ZWQQAkABSARQA1gDYAGC0+STAi86ASoiKi92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9Om5hcnJvdxLWAQoRUmV0cnlJbnN0YWxsYXRpb24SKi5zYWFzLmFjY291bnRzLnYxLlJldHJ5SW5zdGFsbGF0aW9uUmVxdWVzdBoeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uInXC8xg9CAIQBCoMCgZvcmdfaWQQAhgBMAE6HQoZc2Fhcy5pbnN0YWxsYXRpb24ucmV0cmllZBACQAFIBFADWANgAYLT5JMCLjoBKiIpL3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH06cmV0cnkSiQIKHVRyYW5zZmVySW5zdGFsbGF0aW9uT3duZXJzaGlwEjYuc2Fhcy5hY2NvdW50cy52MS5UcmFuc2Zlckluc3RhbGxhdGlvbk93bmVyc2hpcFJlcXVlc3QaHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbiKPAcLzGEsIAhAEKgwKBm9yZ19pZBACGAEwATorCidzYWFzLmluc3RhbGxhdGlvbi5vd25lcnNoaXBfdHJhbnNmZXJyZWQQAkABSARQA1gDYAGC0+STAjo6ASoiNS92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9OnRyYW5zZmVyT3duZXJzaGlwErkBCg9HZXRJbnN0YWxsYXRpb24SKC5zYWFzLmFjY291bnRzLnYxLkdldEluc3RhbGxhdGlvblJlcXVlc3QaKS5zYWFzLmFjY291bnRzLnYxLkdldEluc3RhbGxhdGlvblJlc3BvbnNlIlHC8xgiCAIQAyoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCJRIjL3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH0ShgEKEUxpc3RJbnN0YWxsYXRpb25zEiouc2Fhcy5hY2NvdW50cy52MS5MaXN0SW5zdGFsbGF0aW9uc1JlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLkxpc3RJbnN0YWxsYXRpb25zUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARLKAQoWTGlzdEF2YWlsYWJsZVNvbHV0aW9ucxIvLnNhYXMuYWNjb3VudHMudjEuTGlzdEF2YWlsYWJsZVNvbHV0aW9uc1JlcXVlc3QaMC5zYWFzLmFjY291bnRzLnYxLkxpc3RBdmFpbGFibGVTb2x1dGlvbnNSZXNwb25zZSJNwvMYIggCEAQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAiESHy92MS9pbnN0YWxsYXRpb25zOmxpc3RBdmFpbGFibGUS0QEKHUxpc3RPcmdhbml6YXRpb25JbnN0YWxsYXRpb25zEjYuc2Fhcy5hY2NvdW50cy52MS5MaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1JlcXVlc3QaNy5zYWFzLmFjY291bnRzLnYxLkxpc3RPcmdhbml6YXRpb25JbnN0YWxsYXRpb25zUmVzcG9uc2UiP8LzGCIIAhAEKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwITEhEvdjEvaW5zdGFsbGF0aW9ucxLwAQoXR3JhbnRJbnN0YWxsYXRpb25Ub1RlYW0SMC5zYWFzLmFjY291bnRzLnYxLkdyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVxdWVzdBoxLnNhYXMuYWNjb3VudHMudjEuR3JhbnRJbnN0YWxsYXRpb25Ub1RlYW1SZXNwb25zZSJwwvMYOAgCEAQqDAoGb3JnX2lkEAIYATABOhYKEnNhYXMuc2NvcGUuZ3JhbnRlZBACQAFIBFADWANgAXgCgtPkkwIuOgEqIikvdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfS90ZWFtcxLtAQoaUmV2b2tlSW5zdGFsbGF0aW9uRnJvbVRlYW0SMy5zYWFzLmFjY291bnRzLnYxLlJldm9rZUluc3RhbGxhdGlvbkZyb21UZWFtUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSKBAcLzGDgIAhAEKgwKBm9yZ19pZBACGAEwAToWChJzYWFzLnNjb3BlLnJldm9rZWQQAkABSARQA1gDYAF4AoLT5JMCPzoBKiI6L3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH0vdGVhbXMve3RlYW1faWR9OnJldm9rZWIGcHJvdG8z", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_saas_accounts_v1_authorization, file_saas_accounts_v1_installation_listing, file_saas_policy_v1_options]); + fileDesc("CiRzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbnMucHJvdG8SEHNhYXMuYWNjb3VudHMudjEi+QMKDEluc3RhbGxhdGlvbhIUCgJpZBgBIAEoCUIIukgFcgOwAQESGAoGb3JnX2lkGAIgASgJQgi6SAVyA7ABARIkChJhZ2VudF9wcmluY2lwYWxfaWQYAyABKAlCCLpIBXIDsAEBEiQKEm93bmVyX3ByaW5jaXBhbF9pZBgFIAEoCUIIukgFcgOwAQESHgoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgGIAMoCRIkChJyb290X3Njb3BlX25vZGVfaWQYByABKAlCCLpIBXIDsAEBEjQKBnN0YXR1cxgIIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uU3RhdHVzEi4KCmNyZWF0ZWRfYXQYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKCnJldm9rZWRfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSACIAQESHgoJdGFyZ2V0X2lkGAsgASgJQgu6SAjYAQFyA7ABARIgCg5yZXZva2VkX3JlYXNvbhgMIAEoCUIIukgFcgMYgAQSIAoOZmFpbHVyZV9yZWFzb24YDSABKAlCCLpIBXIDGIAEQg0KC19yZXZva2VkX2F0SgQIBBAFUhNzb2x1dGlvbl9pZGVudGlmaWVyIuADChZJbnN0YWxsU29sdXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESJAoQYWdlbnRfaWRlbnRpZmllchgCIAEoCUIKukgHcgUQARiABBIeCgxkaXNwbGF5X25hbWUYBCABKAlCCLpIBXIDGIAEEiIKEHJvb3Rfc2NvcGVfbGFiZWwYBiABKAlCCLpIBXIDGIAEEhkKB3JvbGVfaWQYByABKAlCCLpIBXIDsAEBEicKEm93bmVyX3ByaW5jaXBhbF9pZBgIIAEoCUILukgI2AEBcgOwAQESLwoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgJIAMoCUIPukgMkgEJECAiBXIDsAEBEioKEWFsbG93ZWRfYXVkaWVuY2VzGAogAygJQg+6SAySAQkQQCIFcgMYgAQSJwoOYWxsb3dlZF9zY29wZXMYCyADKAlCD7pIDJIBCRBAIgVyAxiAARIbCgl0YXJnZXRfaWQYDCABKAlCCLpIBXIDsAEBEh8KBXNjb3BlGA0gASgJQgu6SAhyBhACKICAQEgAiAEBQggKBl9zY29wZUoECAMQBEoECAUQBlITc29sdXRpb25faWRlbnRpZmllclIPcm9vdF9zY29wZV9wYXRoInQKGU5hcnJvd0luc3RhbGxhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhoKBXNjb3BlGAMgASgJQgu6SAhyBhACKICAQCJXChhSZXRyeUluc3RhbGxhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBIlcKGFVuaW5zdGFsbFNvbHV0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEivgEKJFRyYW5zZmVySW5zdGFsbGF0aW9uT3duZXJzaGlwUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESKAoWbmV3X293bmVyX3ByaW5jaXBhbF9pZBgDIAEoCUIIukgFcgOwAQESLwoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgEIAMoCUIPukgMkgEJECAiBXIDsAEBIlUKFkdldEluc3RhbGxhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBIoUBChdHZXRJbnN0YWxsYXRpb25SZXNwb25zZRI0CgxpbnN0YWxsYXRpb24YASABKAsyHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbhI0CgZoZWFsdGgYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbkhlYWx0aCKdAQoYTGlzdEluc3RhbGxhdGlvbnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoGc3RhdHVzGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdGF0dXMSHQoJcGFnZV9zaXplGAMgASgFQgq6SAcaBRj0AygAEhIKCnBhZ2VfdG9rZW4YBCABKAkigQEKE0luc3RhbGxhdGlvblN1bW1hcnkSNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SNAoGaGVhbHRoGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25IZWFsdGgicgoZTGlzdEluc3RhbGxhdGlvbnNSZXNwb25zZRI8Cg1pbnN0YWxsYXRpb25zGAEgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdW1tYXJ5EhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJzCiRMaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIdCglwYWdlX3NpemUYAiABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgDIAEoCSLIAwoYT3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEjQKBmhlYWx0aBgCIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uSGVhbHRoEjMKBnRhcmdldBgDIAEoCzIjLnNhYXMuYWNjb3VudHMudjEuQXZhaWxhYmxlU29sdXRpb24SFwoPcm9vdF9zY29wZV9wYXRoGAQgASgJEjwKDWdyYW50ZWRfdGVhbXMYBSADKAsyJS5zYWFzLmFjY291bnRzLnYxLkNvbGxlY3Rpb25SZWFkR3JhbnQSPgoPaW5oZXJpdGVkX3RlYW1zGAYgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50EjoKCXJlYWRpbmVzcxgHIAEoCzInLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uUmVhZGluZXNzEjgKD3JlYWNoZWRfbW9kdWxlcxgIIAMoCzIfLnNhYXMuYWNjb3VudHMudjEuUmVhY2hlZE1vZHVsZSLtAQoSUmVhZGluZXNzQ29uZGl0aW9uEjYKBGtpbmQYASABKA4yKC5zYWFzLmFjY291bnRzLnYxLlJlYWRpbmVzc0NvbmRpdGlvbktpbmQSDwoHc3ViamVjdBgCIAEoCRIOCgZkZXRhaWwYAyABKAkSOAoKY2xlYXJlZF9ieRgEIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQ2xlYXJlZEJ5EjEKBmFjdGlvbhgFIAEoDjIhLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQWN0aW9uEhEKCXNvdXJjZV9pZBgGIAEoCSJbChVJbnN0YWxsYXRpb25SZWFkaW5lc3MSDQoFcmVhZHkYASABKAgSMwoFdW5tZXQYAiADKAsyJC5zYWFzLmFjY291bnRzLnYxLlJlYWRpbmVzc0NvbmRpdGlvbiJPCg1SZWFjaGVkTW9kdWxlEhAKCGF1ZGllbmNlGAEgASgJEhcKD2luc3RhbGxhdGlvbl9pZBgCIAEoCRITCgtyb3V0ZV9hbGlhcxgDIAEoCSKDAQolTGlzdE9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbnNSZXNwb25zZRJBCg1pbnN0YWxsYXRpb25zGAEgAygLMiouc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb25JbnN0YWxsYXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIpMBCh5HcmFudEluc3RhbGxhdGlvblRvVGVhbVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhkKB3RlYW1faWQYAyABKAlCCLpIBXIDsAEBEhkKB3JvbGVfaWQYBCABKAlCCLpIBXIDsAEBInUKE0NhcnJpZWRNb2R1bGVBY2Nlc3MSEAoIYXVkaWVuY2UYASABKAkSFwoPaW5zdGFsbGF0aW9uX2lkGAIgASgJEhIKCnNjb3BlX3BhdGgYAyABKAkSDwoHcm9sZV9pZBgEIAEoCRIOCgZzY29wZXMYBSADKAkiuwEKH0dyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVzcG9uc2USKwoFZ3JhbnQYASABKAsyHC5zYWFzLmFjY291bnRzLnYxLlNjb3BlR3JhbnQSNgoHY2FycmllZBgCIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ2FycmllZE1vZHVsZUFjY2VzcxIzCgV1bm1ldBgDIAMoCzIkLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQ29uZGl0aW9uIpYBCiFSZXZva2VJbnN0YWxsYXRpb25Gcm9tVGVhbVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhkKB3RlYW1faWQYAyABKAlCCLpIBXIDsAEBEhkKB3JvbGVfaWQYBCABKAlCCLpIBXIDsAEBIoQCChFBdmFpbGFibGVTb2x1dGlvbhIbCgl0YXJnZXRfaWQYASABKAlCCLpIBXIDsAEBEhIKCmJpbmRpbmdfaWQYAiABKAkSEwoLcm91dGVfYWxpYXMYAyABKAkSGQoRcmVsZWFzZV9wdWJsaXNoZXIYBCABKAkSFAoMcmVsZWFzZV9uYW1lGAUgASgJEhcKD3JlbGVhc2VfdmVyc2lvbhgGIAEoCRIZChFvcGVuZWRfZ2VuZXJhdGlvbhgHIAEoBBIaChJhcHBsaWVkX2dlbmVyYXRpb24YCCABKAQSEQoJaW5zdGFsbGVkGAkgASgIEhUKBGtpbmQYCiABKAlCB7pIBHICGCAibAodTGlzdEF2YWlsYWJsZVNvbHV0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIdCglwYWdlX3NpemUYAiABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgDIAEoCSJxCh5MaXN0QXZhaWxhYmxlU29sdXRpb25zUmVzcG9uc2USNgoJc29sdXRpb25zGAEgAygLMiMuc2Fhcy5hY2NvdW50cy52MS5BdmFpbGFibGVTb2x1dGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkquwEKEkluc3RhbGxhdGlvblN0YXR1cxIjCh9JTlNUQUxMQVRJT05fU1RBVFVTX1VOU1BFQ0lGSUVEEAASHgoaSU5TVEFMTEFUSU9OX1NUQVRVU19BQ1RJVkUQARIfChtJTlNUQUxMQVRJT05fU1RBVFVTX1JFVk9LRUQQAhIfChtJTlNUQUxMQVRJT05fU1RBVFVTX1BFTkRJTkcQAxIeChpJTlNUQUxMQVRJT05fU1RBVFVTX0ZBSUxFRBAEKoQCChJJbnN0YWxsYXRpb25IZWFsdGgSIwofSU5TVEFMTEFUSU9OX0hFQUxUSF9VTlNQRUNJRklFRBAAEh8KG0lOU1RBTExBVElPTl9IRUFMVEhfSEVBTFRIWRABEikKJUlOU1RBTExBVElPTl9IRUFMVEhfTk9fRUxJR0lCTEVfT1dORVIQAhIlCiFJTlNUQUxMQVRJT05fSEVBTFRIX0FHRU5UX1JFVk9LRUQQAxImCiJJTlNUQUxMQVRJT05fSEVBTFRIX0FHRU5UX0RJU0FCTEVEEAQSLgoqSU5TVEFMTEFUSU9OX0hFQUxUSF9TVEFORElOR19HUkFOVF9NSVNTSU5HEAUq8wMKFlJlYWRpbmVzc0NvbmRpdGlvbktpbmQSKAokUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX1VOU1BFQ0lGSUVEEAASMQotUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX0lOU1RBTExfU1RFUF9QRU5ESU5HEAESMAosUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX0lOU1RBTExfU1RFUF9GQUlMRUQQAhItCilSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfUkVDT1JEX1VOSEVBTFRIWRADEjUKMVJFQURJTkVTU19DT05ESVRJT05fS0lORF9NT0RVTEVfUFJJTkNJUEFMX09NSVRURUQQBBI2CjJSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfT1BFUkFUSU9OX0JJTkRJTkdfT01JVFRFRBAFEjEKLVJFQURJTkVTU19DT05ESVRJT05fS0lORF9TT1VSQ0VfTk9UX0RFTEVHQVRFRBAGEjkKNVJFQURJTkVTU19DT05ESVRJT05fS0lORF9SRUFDSEVEX01PRFVMRV9OT1RfSU5TVEFMTEVEEAcSPgo6UkVBRElORVNTX0NPTkRJVElPTl9LSU5EX0NBUlJJRURfTU9EVUxFX0FDQ0VTU19OT1RfREVSSVZFRBAIKtUBChJSZWFkaW5lc3NDbGVhcmVkQnkSJAogUkVBRElORVNTX0NMRUFSRURfQllfVU5TUEVDSUZJRUQQABIfChtSRUFESU5FU1NfQ0xFQVJFRF9CWV9WSUVXRVIQARIrCidSRUFESU5FU1NfQ0xFQVJFRF9CWV9PUkdBTklaQVRJT05fQURNSU4QAhIqCiZSRUFESU5FU1NfQ0xFQVJFRF9CWV9QTEFURk9STV9PUEVSQVRPUhADEh8KG1JFQURJTkVTU19DTEVBUkVEX0JZX05PQk9EWRAEKp8CCg9SZWFkaW5lc3NBY3Rpb24SIAocUkVBRElORVNTX0FDVElPTl9VTlNQRUNJRklFRBAAEikKJVJFQURJTkVTU19BQ1RJT05fTk9ORV9BVF9USElTX1NVUkZBQ0UQARInCiNSRUFESU5FU1NfQUNUSU9OX1JFVFJZX0lOU1RBTExfU1RFUBACEiUKIVJFQURJTkVTU19BQ1RJT05fUkVDT05ORUNUX1NPVVJDRRADEicKI1JFQURJTkVTU19BQ1RJT05fVFJBTlNGRVJfT1dORVJTSElQEAQSKwonUkVBRElORVNTX0FDVElPTl9JTlNUQUxMX1JFQUNIRURfTU9EVUxFEAUSGQoVUkVBRElORVNTX0FDVElPTl9XQUlUEAYyqhIKE0luc3RhbGxhdGlvblNlcnZpY2USugEKD0luc3RhbGxTb2x1dGlvbhIoLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbFNvbHV0aW9uUmVxdWVzdBoeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uIl3C8xg9CAIQBCoMCgZvcmdfaWQQAhgBMAE6HQoZc2Fhcy5pbnN0YWxsYXRpb24uY3JlYXRlZBACQAFIBFADWANgAYLT5JMCFjoBKiIRL3YxL2luc3RhbGxhdGlvbnMS0gEKEVVuaW5zdGFsbFNvbHV0aW9uEiouc2Fhcy5hY2NvdW50cy52MS5Vbmluc3RhbGxTb2x1dGlvblJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiecLzGD0IAhAEKgwKBm9yZ19pZBACGAEwATodChlzYWFzLmluc3RhbGxhdGlvbi5yZXZva2VkEAJAAUgEUANYA2ABgtPkkwIyOgEqIi0vdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfTp1bmluc3RhbGwS0gEKEk5hcnJvd0luc3RhbGxhdGlvbhIrLnNhYXMuYWNjb3VudHMudjEuTmFycm93SW5zdGFsbGF0aW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJ3wvMYPggCEAQqDAoGb3JnX2lkEAIYATABOh4KGnNhYXMuaW5zdGFsbGF0aW9uLm5hcnJvd2VkEAJAAUgEUANYA2ABgtPkkwIvOgEqIiovdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfTpuYXJyb3cS1gEKEVJldHJ5SW5zdGFsbGF0aW9uEiouc2Fhcy5hY2NvdW50cy52MS5SZXRyeUluc3RhbGxhdGlvblJlcXVlc3QaHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbiJ1wvMYPQgCEAQqDAoGb3JnX2lkEAIYATABOh0KGXNhYXMuaW5zdGFsbGF0aW9uLnJldHJpZWQQAkABSARQA1gDYAGC0+STAi46ASoiKS92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9OnJldHJ5EokCCh1UcmFuc2Zlckluc3RhbGxhdGlvbk93bmVyc2hpcBI2LnNhYXMuYWNjb3VudHMudjEuVHJhbnNmZXJJbnN0YWxsYXRpb25Pd25lcnNoaXBSZXF1ZXN0Gh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24ijwHC8xhLCAIQBCoMCgZvcmdfaWQQAhgBMAE6Kwonc2Fhcy5pbnN0YWxsYXRpb24ub3duZXJzaGlwX3RyYW5zZmVycmVkEAJAAUgEUANYA2ABgtPkkwI6OgEqIjUvdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfTp0cmFuc2Zlck93bmVyc2hpcBK5AQoPR2V0SW5zdGFsbGF0aW9uEiguc2Fhcy5hY2NvdW50cy52MS5HZXRJbnN0YWxsYXRpb25SZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5HZXRJbnN0YWxsYXRpb25SZXNwb25zZSJRwvMYIggCEAMqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAiUSIy92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9EoYBChFMaXN0SW5zdGFsbGF0aW9ucxIqLnNhYXMuYWNjb3VudHMudjEuTGlzdEluc3RhbGxhdGlvbnNSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5MaXN0SW5zdGFsbGF0aW9uc1Jlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESygEKFkxpc3RBdmFpbGFibGVTb2x1dGlvbnMSLy5zYWFzLmFjY291bnRzLnYxLkxpc3RBdmFpbGFibGVTb2x1dGlvbnNSZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5MaXN0QXZhaWxhYmxlU29sdXRpb25zUmVzcG9uc2UiTcLzGCIIAhAEKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIhEh8vdjEvaW5zdGFsbGF0aW9uczpsaXN0QXZhaWxhYmxlEtEBCh1MaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9ucxI2LnNhYXMuYWNjb3VudHMudjEuTGlzdE9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbnNSZXF1ZXN0Gjcuc2Fhcy5hY2NvdW50cy52MS5MaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1Jlc3BvbnNlIj/C8xgiCAIQBCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCExIRL3YxL2luc3RhbGxhdGlvbnMS8AEKF0dyYW50SW5zdGFsbGF0aW9uVG9UZWFtEjAuc2Fhcy5hY2NvdW50cy52MS5HcmFudEluc3RhbGxhdGlvblRvVGVhbVJlcXVlc3QaMS5zYWFzLmFjY291bnRzLnYxLkdyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVzcG9uc2UicMLzGDgIAhAEKgwKBm9yZ19pZBACGAEwAToWChJzYWFzLnNjb3BlLmdyYW50ZWQQAkABSARQA1gDYAF4AoLT5JMCLjoBKiIpL3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH0vdGVhbXMS7QEKGlJldm9rZUluc3RhbGxhdGlvbkZyb21UZWFtEjMuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VJbnN0YWxsYXRpb25Gcm9tVGVhbVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkigQHC8xg4CAIQBCoMCgZvcmdfaWQQAhgBMAE6FgoSc2Fhcy5zY29wZS5yZXZva2VkEAJAAUgEUANYA2ABeAKC0+STAj86ASoiOi92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9L3RlYW1zL3t0ZWFtX2lkfTpyZXZva2ViBnByb3RvMw", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_policy_v1_options]); + +/** + * Installation is one solution's bound identity in an org. Server-assigned ids; + * the browser cannot attest to tenancy, the agent principal, or the scope node. + * + * @generated from message saas.accounts.v1.Installation + */ +export type Installation = Message<"saas.accounts.v1.Installation"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: string org_id = 2; + */ + orgId: string; + + /** + * @generated from field: string agent_principal_id = 3; + */ + agentPrincipalId: string; + + /** + * @generated from field: string owner_principal_id = 5; + */ + ownerPrincipalId: string; + + /** + * @generated from field: repeated string co_owner_principal_ids = 6; + */ + coOwnerPrincipalIds: string[]; + + /** + * @generated from field: string root_scope_node_id = 7; + */ + rootScopeNodeId: string; + + /** + * @generated from field: saas.accounts.v1.InstallationStatus status = 8; + */ + status: InstallationStatus; + + /** + * @generated from field: google.protobuf.Timestamp created_at = 9; + */ + createdAt?: Timestamp; + + /** + * @generated from field: optional google.protobuf.Timestamp revoked_at = 10; + */ + revokedAt?: Timestamp; + + /** + * The immutable solution target this installation was consented to: one + * continuous period of one binding's presence on this host. It is never + * reused, so a replacement binding — even one claiming the same route alias — + * is a different target and inherits nothing. + * + * Deliberately NOT accompanied by the route alias. The alias is a property of + * the target that a later generation may move, and resolving it here would + * mean joining the host's presence state into a request-path listing — which + * would require granting request traffic read over every organisation's + * presence to answer about one. A consumer that needs the alias asks + * ListAvailableSolutions, which serves accepted applied state, and joins on + * this id. + * + * Empty only on a revoked historical row whose target could not be + * established at the cutover; an active installation always names one. + * + * @generated from field: string target_id = 11; + */ + targetId: string; + + /** + * Why a revoked installation was revoked, when it was not an administrator + * uninstalling: the installation-identity cutover, or a tenancy narrowing that + * withdrew this organisation. Empty on an active row. + * + * @generated from field: string revoked_reason = 12; + */ + revokedReason: string; + + /** + * Why the install step failed: the solution's bounded refusal reason, or a + * fixed reason when it never answered. Empty unless status is FAILED. + * + * @generated from field: string failure_reason = 13; + */ + failureReason: string; +}; + +/** + * Describes the message saas.accounts.v1.Installation. + * Use `create(InstallationSchema)` to create a new message. + */ +export const InstallationSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_installations, 0); /** * @generated from message saas.accounts.v1.InstallSolutionRequest @@ -133,7 +229,7 @@ export type InstallSolutionRequest = Message<"saas.accounts.v1.InstallSolutionRe * Use `create(InstallSolutionRequestSchema)` to create a new message. */ export const InstallSolutionRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 0); + messageDesc(file_saas_accounts_v1_installations, 1); /** * An org administrator may only remove installed authority. The host reads @@ -165,7 +261,7 @@ export type NarrowInstallationRequest = Message<"saas.accounts.v1.NarrowInstalla * Use `create(NarrowInstallationRequestSchema)` to create a new message. */ export const NarrowInstallationRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 1); + messageDesc(file_saas_accounts_v1_installations, 2); /** * @generated from message saas.accounts.v1.RetryInstallationRequest @@ -187,7 +283,7 @@ export type RetryInstallationRequest = Message<"saas.accounts.v1.RetryInstallati * Use `create(RetryInstallationRequestSchema)` to create a new message. */ export const RetryInstallationRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 2); + messageDesc(file_saas_accounts_v1_installations, 3); /** * @generated from message saas.accounts.v1.UninstallSolutionRequest @@ -209,7 +305,7 @@ export type UninstallSolutionRequest = Message<"saas.accounts.v1.UninstallSoluti * Use `create(UninstallSolutionRequestSchema)` to create a new message. */ export const UninstallSolutionRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 3); + messageDesc(file_saas_accounts_v1_installations, 4); /** * @generated from message saas.accounts.v1.TransferInstallationOwnershipRequest @@ -245,7 +341,7 @@ export type TransferInstallationOwnershipRequest = Message<"saas.accounts.v1.Tra * Use `create(TransferInstallationOwnershipRequestSchema)` to create a new message. */ export const TransferInstallationOwnershipRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 4); + messageDesc(file_saas_accounts_v1_installations, 5); /** * @generated from message saas.accounts.v1.GetInstallationRequest @@ -267,7 +363,7 @@ export type GetInstallationRequest = Message<"saas.accounts.v1.GetInstallationRe * Use `create(GetInstallationRequestSchema)` to create a new message. */ export const GetInstallationRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 5); + messageDesc(file_saas_accounts_v1_installations, 6); /** * @generated from message saas.accounts.v1.GetInstallationResponse @@ -291,7 +387,105 @@ export type GetInstallationResponse = Message<"saas.accounts.v1.GetInstallationR * Use `create(GetInstallationResponseSchema)` to create a new message. */ export const GetInstallationResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 6); + messageDesc(file_saas_accounts_v1_installations, 7); + +/** + * ListInstallationsRequest asks what one organization has installed. Every other + * RPC on this service names a single installation, so until this existed nothing + * could answer "what has this org installed?" — which is the authority read a + * per-organization solution projection is built on (issue #949). + * + * @generated from message saas.accounts.v1.ListInstallationsRequest + */ +export type ListInstallationsRequest = Message<"saas.accounts.v1.ListInstallationsRequest"> & { + /** + * @generated from field: string org_id = 1; + */ + orgId: string; + + /** + * Restrict to one lifecycle status. UNSPECIFIED returns every status, so a + * caller that must see only live installs asks for ACTIVE explicitly rather + * than leaning on a default that would silently widen if the enum grew. + * + * @generated from field: saas.accounts.v1.InstallationStatus status = 2; + */ + status: InstallationStatus; + + /** + * Page bound. 0 -> server default; capped so an organization with many + * installations can never return an unbounded result in one call. + * + * @generated from field: int32 page_size = 3; + */ + pageSize: number; + + /** + * Opaque cursor from a previous response's next_page_token; empty for page one. + * + * @generated from field: string page_token = 4; + */ + pageToken: string; +}; + +/** + * Describes the message saas.accounts.v1.ListInstallationsRequest. + * Use `create(ListInstallationsRequestSchema)` to create a new message. + */ +export const ListInstallationsRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_installations, 8); + +/** + * InstallationSummary is one row of the listing: the installation, plus the health + * resolved live beside it exactly as GetInstallation resolves it for one. Health + * travels with the row because a consumer that renders an installed solution as + * unavailable would otherwise need one GetInstallation per installation. + * + * @generated from message saas.accounts.v1.InstallationSummary + */ +export type InstallationSummary = Message<"saas.accounts.v1.InstallationSummary"> & { + /** + * @generated from field: saas.accounts.v1.Installation installation = 1; + */ + installation?: Installation; + + /** + * @generated from field: saas.accounts.v1.InstallationHealth health = 2; + */ + health: InstallationHealth; +}; + +/** + * Describes the message saas.accounts.v1.InstallationSummary. + * Use `create(InstallationSummarySchema)` to create a new message. + */ +export const InstallationSummarySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_installations, 9); + +/** + * @generated from message saas.accounts.v1.ListInstallationsResponse + */ +export type ListInstallationsResponse = Message<"saas.accounts.v1.ListInstallationsResponse"> & { + /** + * @generated from field: repeated saas.accounts.v1.InstallationSummary installations = 1; + */ + installations: InstallationSummary[]; + + /** + * Set when more installations remain; pass it back as page_token. Empty on the + * last page. + * + * @generated from field: string next_page_token = 2; + */ + nextPageToken: string; +}; + +/** + * Describes the message saas.accounts.v1.ListInstallationsResponse. + * Use `create(ListInstallationsResponseSchema)` to create a new message. + */ +export const ListInstallationsResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_installations, 10); /** * @generated from message saas.accounts.v1.ListOrganizationInstallationsRequest @@ -322,7 +516,7 @@ export type ListOrganizationInstallationsRequest = Message<"saas.accounts.v1.Lis * Use `create(ListOrganizationInstallationsRequestSchema)` to create a new message. */ export const ListOrganizationInstallationsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 7); + messageDesc(file_saas_accounts_v1_installations, 11); /** * OrganizationInstallation is one active installation as its organisation's @@ -401,7 +595,7 @@ export type OrganizationInstallation = Message<"saas.accounts.v1.OrganizationIns * Use `create(OrganizationInstallationSchema)` to create a new message. */ export const OrganizationInstallationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 8); + messageDesc(file_saas_accounts_v1_installations, 12); /** * ReadinessCondition is one thing that is unmet, in the words of whoever owns @@ -454,7 +648,7 @@ export type ReadinessCondition = Message<"saas.accounts.v1.ReadinessCondition"> * Use `create(ReadinessConditionSchema)` to create a new message. */ export const ReadinessConditionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 9); + messageDesc(file_saas_accounts_v1_installations, 13); /** * InstallationReadiness is whether the installation will work, which is NOT @@ -485,7 +679,7 @@ export type InstallationReadiness = Message<"saas.accounts.v1.InstallationReadin * Use `create(InstallationReadinessSchema)` to create a new message. */ export const InstallationReadinessSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 10); + messageDesc(file_saas_accounts_v1_installations, 14); /** * ReachedModule is one module an installed target declares it reaches on the @@ -534,7 +728,7 @@ export type ReachedModule = Message<"saas.accounts.v1.ReachedModule"> & { * Use `create(ReachedModuleSchema)` to create a new message. */ export const ReachedModuleSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 11); + messageDesc(file_saas_accounts_v1_installations, 15); /** * @generated from message saas.accounts.v1.ListOrganizationInstallationsResponse @@ -558,7 +752,7 @@ export type ListOrganizationInstallationsResponse = Message<"saas.accounts.v1.Li * Use `create(ListOrganizationInstallationsResponseSchema)` to create a new message. */ export const ListOrganizationInstallationsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 12); + messageDesc(file_saas_accounts_v1_installations, 16); /** * @generated from message saas.accounts.v1.GrantInstallationToTeamRequest @@ -595,7 +789,7 @@ export type GrantInstallationToTeamRequest = Message<"saas.accounts.v1.GrantInst * Use `create(GrantInstallationToTeamRequestSchema)` to create a new message. */ export const GrantInstallationToTeamRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 13); + messageDesc(file_saas_accounts_v1_installations, 17); /** * CarriedModuleAccess is one derived grant a solution's grant carried: the @@ -658,7 +852,7 @@ export type CarriedModuleAccess = Message<"saas.accounts.v1.CarriedModuleAccess" * Use `create(CarriedModuleAccessSchema)` to create a new message. */ export const CarriedModuleAccessSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 14); + messageDesc(file_saas_accounts_v1_installations, 18); /** * @generated from message saas.accounts.v1.GrantInstallationToTeamResponse @@ -696,7 +890,7 @@ export type GrantInstallationToTeamResponse = Message<"saas.accounts.v1.GrantIns * Use `create(GrantInstallationToTeamResponseSchema)` to create a new message. */ export const GrantInstallationToTeamResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 15); + messageDesc(file_saas_accounts_v1_installations, 19); /** * @generated from message saas.accounts.v1.RevokeInstallationFromTeamRequest @@ -731,7 +925,7 @@ export type RevokeInstallationFromTeamRequest = Message<"saas.accounts.v1.Revoke * Use `create(RevokeInstallationFromTeamRequestSchema)` to create a new message. */ export const RevokeInstallationFromTeamRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 16); + messageDesc(file_saas_accounts_v1_installations, 20); /** * AvailableSolution is one installable solution target: its immutable identity, @@ -821,7 +1015,7 @@ export type AvailableSolution = Message<"saas.accounts.v1.AvailableSolution"> & * Use `create(AvailableSolutionSchema)` to create a new message. */ export const AvailableSolutionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 17); + messageDesc(file_saas_accounts_v1_installations, 21); /** * @generated from message saas.accounts.v1.ListAvailableSolutionsRequest @@ -851,7 +1045,7 @@ export type ListAvailableSolutionsRequest = Message<"saas.accounts.v1.ListAvaila * Use `create(ListAvailableSolutionsRequestSchema)` to create a new message. */ export const ListAvailableSolutionsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 18); + messageDesc(file_saas_accounts_v1_installations, 22); /** * @generated from message saas.accounts.v1.ListAvailableSolutionsResponse @@ -873,7 +1067,101 @@ export type ListAvailableSolutionsResponse = Message<"saas.accounts.v1.ListAvail * Use `create(ListAvailableSolutionsResponseSchema)` to create a new message. */ export const ListAvailableSolutionsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 19); + messageDesc(file_saas_accounts_v1_installations, 23); + +/** + * InstallationStatus is the lifecycle of an installation row. An uninstall is a + * soft delete (status=revoked, revoked_at set) so the agent, grants, and audit + * trail remain resolvable. + * + * @generated from enum saas.accounts.v1.InstallationStatus + */ +export enum InstallationStatus { + /** + * @generated from enum value: INSTALLATION_STATUS_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: INSTALLATION_STATUS_ACTIVE = 1; + */ + ACTIVE = 1, + + /** + * @generated from enum value: INSTALLATION_STATUS_REVOKED = 2; + */ + REVOKED = 2, + + /** + * Installed, and waiting for the solution to acknowledge its standard install + * step. Nothing can use a pending installation. + * + * @generated from enum value: INSTALLATION_STATUS_PENDING = 3; + */ + PENDING = 3, + + /** + * The solution refused its install step, or never answered within the + * attempt budget. Nothing can use it; an administrator may retry it. + * + * @generated from enum value: INSTALLATION_STATUS_FAILED = 4; + */ + FAILED = 4, +} + +/** + * Describes the enum saas.accounts.v1.InstallationStatus. + */ +export const InstallationStatusSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_saas_accounts_v1_installations, 0); + +/** + * InstallationHealth is resolved live from current authority facts, never + * stored. It is the observable form of the fail-closed mint rule: a + * StartInstallationTask fails exactly when health is not HEALTHY, and transfer + * or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record + * nor any co-owner is currently an org admin (the Zapier-class "connection owner + * left the org" failure), surfaced rather than silently reassigned. + * + * @generated from enum saas.accounts.v1.InstallationHealth + */ +export enum InstallationHealth { + /** + * @generated from enum value: INSTALLATION_HEALTH_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: INSTALLATION_HEALTH_HEALTHY = 1; + */ + HEALTHY = 1, + + /** + * @generated from enum value: INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER = 2; + */ + NO_ELIGIBLE_OWNER = 2, + + /** + * @generated from enum value: INSTALLATION_HEALTH_AGENT_REVOKED = 3; + */ + AGENT_REVOKED = 3, + + /** + * @generated from enum value: INSTALLATION_HEALTH_AGENT_DISABLED = 4; + */ + AGENT_DISABLED = 4, + + /** + * @generated from enum value: INSTALLATION_HEALTH_STANDING_GRANT_MISSING = 5; + */ + STANDING_GRANT_MISSING = 5, +} + +/** + * Describes the enum saas.accounts.v1.InstallationHealth. + */ +export const InstallationHealthSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_saas_accounts_v1_installations, 1); /** * ReadinessConditionKind is what kind of thing is unmet. A reader acts on the @@ -961,7 +1249,7 @@ export enum ReadinessConditionKind { * Describes the enum saas.accounts.v1.ReadinessConditionKind. */ export const ReadinessConditionKindSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_saas_accounts_v1_installations, 0); + enumDesc(file_saas_accounts_v1_installations, 2); /** * ReadinessClearedBy is who can clear a condition. It is stated rather than @@ -1006,7 +1294,7 @@ export enum ReadinessClearedBy { * Describes the enum saas.accounts.v1.ReadinessClearedBy. */ export const ReadinessClearedBySchema: GenEnum = /*@__PURE__*/ - enumDesc(file_saas_accounts_v1_installations, 1); + enumDesc(file_saas_accounts_v1_installations, 3); /** * ReadinessAction is what clears the condition, named as the act rather than as @@ -1059,7 +1347,7 @@ export enum ReadinessAction { * Describes the enum saas.accounts.v1.ReadinessAction. */ export const ReadinessActionSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_saas_accounts_v1_installations, 2); + enumDesc(file_saas_accounts_v1_installations, 4); /** * InstallationService composes and administers installations. The headless mint @@ -1146,15 +1434,11 @@ export const InstallationService: GenService<{ * ListInstallations enumerates one organization's installations with their live * health. It is internal-tier on purpose: it takes the organization as a request * field, so only a caller holding the cluster-internal credential may ask, and - * the tenant it asks about is never one a browser supplied. The auth-gateway is - * its caller, for two reads. Its viewer projection asks about the tenant it - * projected from a verified identity; it sits beside - * PermissionService.ListAccessibleScopes for that reason, the two read together - * to answer what a viewer's organization installed and what that viewer was - * granted. Its declared module route's bearer-less arm asks about the tenant an - * edge-inspected Work Context names, a value this issuer signed, to admit a - * headless operation context only when the target is installed there; whether - * the context is one is WorkContextService/AdmitWorkloadOperation's answer. + * the tenant it asks about is the one the auth-gateway projected from a verified + * identity — never one a browser supplied. It sits beside + * PermissionService.ListAccessibleScopes for that reason: the two are read + * together to answer what a viewer's organization installed and what that viewer + * was granted. * * @generated from rpc saas.accounts.v1.InstallationService.ListInstallations */ @@ -1195,8 +1479,7 @@ export const InstallationService: GenService<{ * It is the org-admin companion to the internal ListInstallations, and it is a * separate method rather than a relaxed policy on that one: ListInstallations * is read by the gateway for an organisation it projected from a verified - * identity or that an edge-verified Work Context names, and a bare tenant - * credential must never reach it. This one binds + * identity, and a bare tenant credential must never reach it. This one binds * the organisation to the caller's own admin membership, exactly as * ListAvailableSolutions does, so no organisation's administrator can read * another's. The one exception is inherited, not added here: a platform diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts index fcc7d03ce..d1a1a0dcf 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts @@ -12,8 +12,8 @@ import type { CollectionReadGrant } from "./authorization_pb"; import { file_saas_accounts_v1_authorization } from "./authorization_pb"; import type { Organization, OrgMembership, User } from "./common_pb"; import { file_saas_accounts_v1_common } from "./common_pb"; -import type { Installation } from "./installation_listing_pb"; -import { file_saas_accounts_v1_installation_listing } from "./installation_listing_pb"; +import type { Installation } from "./installations_pb"; +import { file_saas_accounts_v1_installations } from "./installations_pb"; import type { SolutionRegistration } from "./solution_registry_pb"; import { file_saas_accounts_v1_solution_registry } from "./solution_registry_pb"; import type { GetEventOperationsRequestSchema, GetEventOperationsResponseSchema, ListEventSubscriptionsRequestSchema, ListEventSubscriptionsResponseSchema } from "../../events/v1/operations_pb"; @@ -27,7 +27,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/platform_admin.proto. */ export const file_saas_accounts_v1_platform_admin: GenFile = /*@__PURE__*/ - fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEiVAoMQ2F0YWxvZ3VlR2FwEjQKBnJlYXNvbhgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwUmVhc29uEg4KBmRldGFpbBgCIAEoCSJEChBDYXRhbG9ndWVSZWxlYXNlEhEKCXB1Ymxpc2hlchgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkipQIKEENhdGFsb2d1ZURlc2lyZWQSGwoRZGVjbGFyZWRfcmV2aXNpb24YASABKANIABI/ChVkZWNsYXJlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEj4KEGRlY2xhcmVkX3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIARI+ChRkZWNsYXJlZF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGQoXZGVjbGFyZWRfcmV2aXNpb25fdmFsdWVCGAoWZGVjbGFyZWRfcmVsZWFzZV92YWx1ZSKCAQoWQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbhIbChNhdXRob3JpemVkX3JldmlzaW9uGAEgASgDEhgKEGludmVudG9yeV9kaWdlc3QYAiABKAkSFgoObWVtYmVyX2JpbmRpbmcYAyABKAkSGQoRYnVpbGRfaW5jYXJuYXRpb24YBCABKAQiKAoWQ2F0YWxvZ3VlTm90QXV0aG9yaXplZBIOCgZkZXRhaWwYASABKAki8AEKE0NhdGFsb2d1ZUF1dGhvcml6ZWQSQQoNYXV0aG9yaXphdGlvbhgBIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbkgAEkIKDm5vdF9hdXRob3JpemVkGAIgASgLMiguc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVOb3RBdXRob3JpemVkSAASOwoRYXV0aG9yaXphdGlvbl9nYXAYAyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAQhUKE2F1dGhvcml6YXRpb25fdmFsdWUiiAEKEENhdGFsb2d1ZUFwcGxpZWQSGgoQYXBwbGllZF9yZXZpc2lvbhgBIAEoA0gAEj4KFGFwcGxpZWRfcmV2aXNpb25fZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAEIYChZhcHBsaWVkX3JldmlzaW9uX3ZhbHVlIt8BChpDYXRhbG9ndWVPYnNlcnZlZEV4ZWN1dGlvbhJoChdjb250YWluZXJfaW1hZ2VfZGlnZXN0cxgBIAMoCzJHLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb24uQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSGQoRYnVpbGRfaW5jYXJuYXRpb24YAiABKAQaPAoaQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASLUBAoRQ2F0YWxvZ3VlT2JzZXJ2ZWQSGwoRb2JzZXJ2ZWRfcmV2aXNpb24YASABKANIABI/ChVvYnNlcnZlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEjEKC29ic2VydmVkX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgBEkMKGW9ic2VydmF0aW9uX2ZyZXNobmVzc19nYXAYBCABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkoKEm9ic2VydmVkX2V4ZWN1dGlvbhgFIAEoCzIsLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb25IAhJAChZvYnNlcnZlZF9leGVjdXRpb25fZ2FwGAYgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAhI9Cgd2ZXJkaWN0GAcgASgOMiouc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVPYnNlcnZlZFZlcmRpY3RIAxI1Cgt2ZXJkaWN0X2dhcBgIIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSANCGQoXb2JzZXJ2ZWRfcmV2aXNpb25fdmFsdWVCHQobb2JzZXJ2YXRpb25fZnJlc2huZXNzX3ZhbHVlQhoKGG9ic2VydmVkX2V4ZWN1dGlvbl92YWx1ZUIPCg12ZXJkaWN0X3ZhbHVlIuABChRDYXRhbG9ndWVXaXRoZHJhd2luZxI+ChR3aXRoZHJhd2FsX3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASSQofY3JlZGVudGlhbF9yZXZvY2F0aW9uX3N0YXRlX2dhcBgCIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGAoWd2l0aGRyYXdhbF9zdGF0ZV92YWx1ZUIjCiFjcmVkZW50aWFsX3Jldm9jYXRpb25fc3RhdGVfdmFsdWUibAoQQ2F0YWxvZ3VlUmV0aXJlZBI+ChRyZXRpcmVtZW50X3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSABCGAoWcmV0aXJlbWVudF9zdGF0ZV92YWx1ZSK4AwoVQ2F0YWxvZ3VlSW5zdGFsbGF0aW9uEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEhAKCG9yZ19uYW1lGAIgASgJEjsKDWFnZW50X3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIABI7ChFhZ2VudF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASNgoMcmV2aXNpb25fZ2FwGAUgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIARI8Cg1ncmFudGVkX3RlYW1zGAYgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50Ej4KD2luaGVyaXRlZF90ZWFtcxgHIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ29sbGVjdGlvblJlYWRHcmFudEIVChNhZ2VudF9yZWxlYXNlX3ZhbHVlQhAKDnJldmlzaW9uX3ZhbHVlIuMFCg5DYXRhbG9ndWVFbnRyeRIyCgRraW5kGAEgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVFbnRyeUtpbmQSDAoEbmFtZRgCIAEoCRITCglwdWJsaXNoZXIYAyABKAlIABI3Cg1wdWJsaXNoZXJfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABIzCgdkZXNpcmVkGAUgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVEZXNpcmVkEjkKCmF1dGhvcml6ZWQYBiABKAsyJS5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUF1dGhvcml6ZWQSMwoHYXBwbGllZBgHIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXBwbGllZBI1CghvYnNlcnZlZBgIIAEoCzIjLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWQSOwoLd2l0aGRyYXdpbmcYCSABKAsyJi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVdpdGhkcmF3aW5nEjMKB3JldGlyZWQYCiABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJldGlyZWQSOAoOYnVpbGRfc2l6ZV9nYXAYCyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkEKDHJlZ2lzdHJhdGlvbhgMIAEoCzImLnNhYXMuYWNjb3VudHMudjEuU29sdXRpb25SZWdpc3RyYXRpb25IAogBARI+Cg1pbnN0YWxsYXRpb25zGA0gAygLMicuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVJbnN0YWxsYXRpb25CEQoPcHVibGlzaGVyX3ZhbHVlQhIKEGJ1aWxkX3NpemVfdmFsdWVCDwoNX3JlZ2lzdHJhdGlvbiI6ChxMaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0EhoKEmluY2x1ZGVfdG9tYnN0b25lZBgBIAEoCCKRAgodTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVzcG9uc2USMQoHZW50cmllcxgBIAMoCzIgLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRW50cnkSGQoRcmVnaXN0cnlfcmV2aXNpb24YAiABKAMSZgoUYXBwcm92ZWRfaW52ZW50b3JpZXMYAyADKAsySC5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlLkFwcHJvdmVkSW52ZW50b3JpZXNFbnRyeRo6ChhBcHByb3ZlZEludmVudG9yaWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgMOgI4ASJ4Cg5Nb2R1bGVFbnZlbG9wZRIQCghyZXZpc2lvbhgBIAEoAxIQCghkb2N1bWVudBgCIAEoCRISCgp3cml0dGVuX2J5GAMgASgJEi4KCndyaXR0ZW5fYXQYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIhoKGEdldE1vZHVsZUVudmVsb3BlUmVxdWVzdCKeAQoZR2V0TW9kdWxlRW52ZWxvcGVSZXNwb25zZRI3CghlbnZlbG9wZRgBIAEoCzIgLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlRW52ZWxvcGVIAIgBARI7CglyZXF1ZXN0ZWQYAiADKAsyKC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUF1dGhvcml0eVJlcXVlc3RCCwoJX2VudmVsb3BlIoMBChZNb2R1bGVBdXRob3JpdHlSZXF1ZXN0EhEKCXByaW5jaXBhbBgBIAEoCRIPCgdiaW5kaW5nGAIgASgJEhIKCmdlbmVyYXRpb24YAyABKAMSDQoFYWxpYXMYBCABKAkSDwoHYWxsb3dlZBgFIAEoCBIRCglhdXRob3JpdHkYBiABKAkiXQoYUHV0TW9kdWxlRW52ZWxvcGVSZXF1ZXN0EiIKEWV4cGVjdGVkX3JldmlzaW9uGAEgASgDQge6SAQiAigAEh0KCGRvY3VtZW50GAIgASgJQgu6SAhyBhABKICAQCJPChlQdXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlEjIKCGVudmVsb3BlGAEgASgLMiAuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFbnZlbG9wZSp+ChJDYXRhbG9ndWVFbnRyeUtpbmQSJAogQ0FUQUxPR1VFX0VOVFJZX0tJTkRfVU5TUEVDSUZJRUQQABIfChtDQVRBTE9HVUVfRU5UUllfS0lORF9NT0RVTEUQARIhCh1DQVRBTE9HVUVfRU5UUllfS0lORF9TT0xVVElPThACKq8BChJDYXRhbG9ndWVHYXBSZWFzb24SJAogQ0FUQUxPR1VFX0dBUF9SRUFTT05fVU5TUEVDSUZJRUQQABIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVDT1JERUQQARIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVQT1JURUQQAhIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfT0JTRVJWRUQQAyreAQoYQ2F0YWxvZ3VlT2JzZXJ2ZWRWZXJkaWN0EioKJkNBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1VOU1BFQ0lGSUVEEAASMQotQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19BVVRIT1JJWkVEEAESLgoqQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19ESUZGRVJTEAISMwovQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19VTkFVVEhPUklaRUQQAzKiIwoUUGxhdGZvcm1BZG1pblNlcnZpY2USjgEKC1NlYXJjaFVzZXJzEiQuc2Fhcy5hY2NvdW50cy52MS5TZWFyY2hVc2Vyc1JlcXVlc3QaJS5zYWFzLmFjY291bnRzLnYxLlNlYXJjaFVzZXJzUmVzcG9uc2UiMsLzGBQIAhABMAE6AhABQAFIBVADWANgA4LT5JMCFBISL3YxL3BsYXRmb3JtL3VzZXJzEqsBCgtTdXNwZW5kVXNlchIkLnNhYXMuYWNjb3VudHMudjEuU3VzcGVuZFVzZXJSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5Il7C8xgrCAIQATABOhcKE3NhYXMudXNlci5zdXNwZW5kZWQQAkABSAVQA1gDYAV4AoLT5JMCKToBKiIkL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTpzdXNwZW5kErMBCg1VbnN1c3BlbmRVc2VyEiYuc2Fhcy5hY2NvdW50cy52MS5VbnN1c3BlbmRVc2VyUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJiwvMYLQgCEAEwAToZChVzYWFzLnVzZXIudW5zdXNwZW5kZWQQAkABSAVQA1gDYAV4AoLT5JMCKzoBKiImL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTp1bnN1c3BlbmQS1gEKD0ltcGVyc29uYXRlVXNlchIoLnNhYXMuYWNjb3VudHMudjEuSW1wZXJzb25hdGVVc2VyUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuSW1wZXJzb25hdGVVc2VyUmVzcG9uc2UibsLzGDcIAhABMAQ6Iwofc2Fhcy5wbGF0Zm9ybS51c2VyX2ltcGVyc29uYXRlZBACQAFIBVADWARgA3gCgtPkkwItOgEqIigvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OmltcGVyc29uYXRlEtgBChFTdG9wSW1wZXJzb25hdGlvbhIqLnNhYXMuYWNjb3VudHMudjEuU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5TdG9wSW1wZXJzb25hdGlvblJlc3BvbnNlImrC8xg8CAIQATABOioKJnNhYXMucGxhdGZvcm0udXNlcl9pbXBlcnNvbmF0aW9uX2VuZGVkEAJAAUgFUANYA2ABgtPkkwIkOgEqIh8vdjEvcGxhdGZvcm0vaW1wZXJzb25hdGlvbjpzdG9wEqYBChJMaXN0QWN0aXZlU2Vzc2lvbnMSKy5zYWFzLmFjY291bnRzLnYxLkxpc3RBY3RpdmVTZXNzaW9uc1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkxpc3RBY3RpdmVTZXNzaW9uc1Jlc3BvbnNlIjXC8xgUCAIQATABOgIQAUABSAVQA1gDYAOC0+STAhcSFS92MS9wbGF0Zm9ybS9zZXNzaW9ucxKpAQoNUmV2b2tlU2Vzc2lvbhImLnNhYXMuYWNjb3VudHMudjEuUmV2b2tlU2Vzc2lvblJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiWMLzGCoIAhABMAE6GAoUc2Fhcy5zZXNzaW9uLnJldm9rZWQQAkABSAVQA1gDYAOC0+STAiQqIi92MS9wbGF0Zm9ybS9zZXNzaW9ucy97c2Vzc2lvbl9pZH0SzwEKEkdldE9yZ0VudGl0bGVtZW50cxIrLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnRW50aXRsZW1lbnRzUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2UiXsLzGCIIAhADKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIyEjAvdjEvcGxhdGZvcm0vb3JnYW5pemF0aW9ucy97b3JnX2lkfS9lbnRpdGxlbWVudHMS5AEKE092ZXJyaWRlRW50aXRsZW1lbnQSLC5zYWFzLmFjY291bnRzLnYxLk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0Gi0uc2Fhcy5hY2NvdW50cy52MS5PdmVycmlkZUVudGl0bGVtZW50UmVzcG9uc2UicMLzGDEIAhABMAE6HQoZc2Fhcy5lbnRpdGxlbWVudC5vdmVycmlkZRACQAFIBVADWANgAngCgtPkkwI1OgEqIjAvdjEvcGxhdGZvcm0vb3JnYW5pemF0aW9ucy97b3JnX2lkfS9lbnRpdGxlbWVudHMSrQEKEUdyYW50UGxhdGZvcm1Sb2xlEiouc2Fhcy5hY2NvdW50cy52MS5HcmFudFBsYXRmb3JtUm9sZVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiVMLzGDIIAhABMAQ6Hgoac2Fhcy5wbGF0Zm9ybS5yb2xlX2dyYW50ZWQQAkABSAVQA1gDYAV4AoLT5JMCGDoBKiITL3YxL3BsYXRmb3JtL2FkbWlucxK2AQoSUmV2b2tlUGxhdGZvcm1Sb2xlEisuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VQbGF0Zm9ybVJvbGVSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IlvC8xgyCAIQATAEOh4KGnNhYXMucGxhdGZvcm0ucm9sZV9yZXZva2VkEAJAAUgFUANYA2AFeAKC0+STAh8qHS92MS9wbGF0Zm9ybS9hZG1pbnMve3VzZXJfaWR9EqQBChJMaXN0UGxhdGZvcm1BZG1pbnMSKy5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUFkbWluc1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUFkbWluc1Jlc3BvbnNlIjPC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhUSEy92MS9wbGF0Zm9ybS9hZG1pbnMSjwEKFExpc3RBbGxPcmdhbml6YXRpb25zEi0uc2Fhcy5hY2NvdW50cy52MS5MaXN0QWxsT3JnYW5pemF0aW9uc1JlcXVlc3QaLi5zYWFzLmFjY291bnRzLnYxLkxpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2UiGMLzGBQIAhABMAE6AhABQAFIBVADWANgAxKSAQoVR2V0T3JnYW5pemF0aW9uUm9zdGVyEi4uc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZSIYwvMYFAgCEAEwAToCEAFAAUgFUANYA2ADEqUBChBMaXN0RmVhdHVyZUZsYWdzEikuc2Fhcy5hY2NvdW50cy52MS5MaXN0RmVhdHVyZUZsYWdzUmVxdWVzdBoqLnNhYXMuYWNjb3VudHMudjEuTGlzdEZlYXR1cmVGbGFnc1Jlc3BvbnNlIjrC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhwSGi92MS9wbGF0Zm9ybS9mZWF0dXJlLWZsYWdzEtIBChFVcHNlcnRGZWF0dXJlRmxhZxIqLnNhYXMuYWNjb3VudHMudjEuVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5VcHNlcnRGZWF0dXJlRmxhZ1Jlc3BvbnNlImSIAgHC8xgxCAIQATABOh0KGXNhYXMuZmVhdHVyZV9mbGFnLnVwZGF0ZWQQAkABSAVQA1gDYAV4AoLT5JMCJjoBKhohL3YxL3BsYXRmb3JtL2ZlYXR1cmUtZmxhZ3Mve25hbWV9Ep8BChBHZXRKb2JPcGVyYXRpb25zEiUuc2Fhcy5qb2JzLnYxLkdldEpvYk9wZXJhdGlvbnNSZXF1ZXN0GiYuc2Fhcy5qb2JzLnYxLkdldEpvYk9wZXJhdGlvbnNSZXNwb25zZSI8wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIeEhwvdjEvcGxhdGZvcm0vam9icy9vcGVyYXRpb25zEnwKCExpc3RKb2JzEh0uc2Fhcy5qb2JzLnYxLkxpc3RKb2JzUmVxdWVzdBoeLnNhYXMuam9icy52MS5MaXN0Sm9ic1Jlc3BvbnNlIjHC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhMSES92MS9wbGF0Zm9ybS9qb2JzEn8KBkdldEpvYhIbLnNhYXMuam9icy52MS5HZXRKb2JSZXF1ZXN0Ghwuc2Fhcy5qb2JzLnYxLkdldEpvYlJlc3BvbnNlIjrC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhwSGi92MS9wbGF0Zm9ybS9qb2JzL3tqb2JfaWR9EqwBCglSZXBsYXlKb2ISHi5zYWFzLmpvYnMudjEuUmVwbGF5Sm9iUmVxdWVzdBofLnNhYXMuam9icy52MS5SZXBsYXlKb2JSZXNwb25zZSJewvMYJwgCEAEwAzoVChFzYWFzLmpvYi5yZXBsYXllZBACQANIBVADWANgBYLT5JMCLToBKiIoL3YxL3BsYXRmb3JtL2pvYnMve3NvdXJjZV9qb2JfaWR9OnJlcGxheRKrAQoSR2V0RXZlbnRPcGVyYXRpb25zEikuc2Fhcy5ldmVudHMudjEuR2V0RXZlbnRPcGVyYXRpb25zUmVxdWVzdBoqLnNhYXMuZXZlbnRzLnYxLkdldEV2ZW50T3BlcmF0aW9uc1Jlc3BvbnNlIj7C8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAiASHi92MS9wbGF0Zm9ybS9ldmVudHMvb3BlcmF0aW9ucxK6AQoWTGlzdEV2ZW50U3Vic2NyaXB0aW9ucxItLnNhYXMuZXZlbnRzLnYxLkxpc3RFdmVudFN1YnNjcmlwdGlvbnNSZXF1ZXN0Gi4uc2Fhcy5ldmVudHMudjEuTGlzdEV2ZW50U3Vic2NyaXB0aW9uc1Jlc3BvbnNlIkHC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAiMSIS92MS9wbGF0Zm9ybS9ldmVudHMvc3Vic2NyaXB0aW9ucxKwAQoVTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlEi4uc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXNwb25zZSI2wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIYEhYvdjEvcGxhdGZvcm0vY2F0YWxvZ3VlEsABChFHZXRNb2R1bGVFbnZlbG9wZRIqLnNhYXMuYWNjb3VudHMudjEuR2V0TW9kdWxlRW52ZWxvcGVSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5HZXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlIlLC8xgqCAIQASIUbW9kdWxlX2VudmVsb3BlOnJlYWQwAToCEAFAAUgFUANYA2AFgtPkkwIeEhwvdjEvcGxhdGZvcm0vbW9kdWxlLWVudmVsb3BlEu0BChFQdXRNb2R1bGVFbnZlbG9wZRIqLnNhYXMuYWNjb3VudHMudjEuUHV0TW9kdWxlRW52ZWxvcGVSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5QdXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlIn/C8xhUCAIQASIVbW9kdWxlX2VudmVsb3BlOndyaXRlMAM6KQolc2Fhcy5wbGF0Zm9ybS5tb2R1bGVfZW52ZWxvcGVfd3JpdHRlbhACQAFIBVADWANgBXgCgtPkkwIhOgEqGhwvdjEvcGxhdGZvcm0vbW9kdWxlLWVudmVsb3BlYgZwcm90bzM", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_accounts_v1_common, file_saas_accounts_v1_installation_listing, file_saas_accounts_v1_solution_registry, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); + fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEiVAoMQ2F0YWxvZ3VlR2FwEjQKBnJlYXNvbhgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwUmVhc29uEg4KBmRldGFpbBgCIAEoCSJEChBDYXRhbG9ndWVSZWxlYXNlEhEKCXB1Ymxpc2hlchgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkipQIKEENhdGFsb2d1ZURlc2lyZWQSGwoRZGVjbGFyZWRfcmV2aXNpb24YASABKANIABI/ChVkZWNsYXJlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEj4KEGRlY2xhcmVkX3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIARI+ChRkZWNsYXJlZF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGQoXZGVjbGFyZWRfcmV2aXNpb25fdmFsdWVCGAoWZGVjbGFyZWRfcmVsZWFzZV92YWx1ZSKCAQoWQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbhIbChNhdXRob3JpemVkX3JldmlzaW9uGAEgASgDEhgKEGludmVudG9yeV9kaWdlc3QYAiABKAkSFgoObWVtYmVyX2JpbmRpbmcYAyABKAkSGQoRYnVpbGRfaW5jYXJuYXRpb24YBCABKAQiKAoWQ2F0YWxvZ3VlTm90QXV0aG9yaXplZBIOCgZkZXRhaWwYASABKAki8AEKE0NhdGFsb2d1ZUF1dGhvcml6ZWQSQQoNYXV0aG9yaXphdGlvbhgBIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbkgAEkIKDm5vdF9hdXRob3JpemVkGAIgASgLMiguc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVOb3RBdXRob3JpemVkSAASOwoRYXV0aG9yaXphdGlvbl9nYXAYAyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAQhUKE2F1dGhvcml6YXRpb25fdmFsdWUiiAEKEENhdGFsb2d1ZUFwcGxpZWQSGgoQYXBwbGllZF9yZXZpc2lvbhgBIAEoA0gAEj4KFGFwcGxpZWRfcmV2aXNpb25fZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAEIYChZhcHBsaWVkX3JldmlzaW9uX3ZhbHVlIt8BChpDYXRhbG9ndWVPYnNlcnZlZEV4ZWN1dGlvbhJoChdjb250YWluZXJfaW1hZ2VfZGlnZXN0cxgBIAMoCzJHLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb24uQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSGQoRYnVpbGRfaW5jYXJuYXRpb24YAiABKAQaPAoaQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASLUBAoRQ2F0YWxvZ3VlT2JzZXJ2ZWQSGwoRb2JzZXJ2ZWRfcmV2aXNpb24YASABKANIABI/ChVvYnNlcnZlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEjEKC29ic2VydmVkX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgBEkMKGW9ic2VydmF0aW9uX2ZyZXNobmVzc19nYXAYBCABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkoKEm9ic2VydmVkX2V4ZWN1dGlvbhgFIAEoCzIsLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb25IAhJAChZvYnNlcnZlZF9leGVjdXRpb25fZ2FwGAYgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAhI9Cgd2ZXJkaWN0GAcgASgOMiouc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVPYnNlcnZlZFZlcmRpY3RIAxI1Cgt2ZXJkaWN0X2dhcBgIIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSANCGQoXb2JzZXJ2ZWRfcmV2aXNpb25fdmFsdWVCHQobb2JzZXJ2YXRpb25fZnJlc2huZXNzX3ZhbHVlQhoKGG9ic2VydmVkX2V4ZWN1dGlvbl92YWx1ZUIPCg12ZXJkaWN0X3ZhbHVlIuABChRDYXRhbG9ndWVXaXRoZHJhd2luZxI+ChR3aXRoZHJhd2FsX3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASSQofY3JlZGVudGlhbF9yZXZvY2F0aW9uX3N0YXRlX2dhcBgCIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGAoWd2l0aGRyYXdhbF9zdGF0ZV92YWx1ZUIjCiFjcmVkZW50aWFsX3Jldm9jYXRpb25fc3RhdGVfdmFsdWUibAoQQ2F0YWxvZ3VlUmV0aXJlZBI+ChRyZXRpcmVtZW50X3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSABCGAoWcmV0aXJlbWVudF9zdGF0ZV92YWx1ZSK4AwoVQ2F0YWxvZ3VlSW5zdGFsbGF0aW9uEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEhAKCG9yZ19uYW1lGAIgASgJEjsKDWFnZW50X3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIABI7ChFhZ2VudF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASNgoMcmV2aXNpb25fZ2FwGAUgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIARI8Cg1ncmFudGVkX3RlYW1zGAYgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50Ej4KD2luaGVyaXRlZF90ZWFtcxgHIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ29sbGVjdGlvblJlYWRHcmFudEIVChNhZ2VudF9yZWxlYXNlX3ZhbHVlQhAKDnJldmlzaW9uX3ZhbHVlIuMFCg5DYXRhbG9ndWVFbnRyeRIyCgRraW5kGAEgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVFbnRyeUtpbmQSDAoEbmFtZRgCIAEoCRITCglwdWJsaXNoZXIYAyABKAlIABI3Cg1wdWJsaXNoZXJfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABIzCgdkZXNpcmVkGAUgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVEZXNpcmVkEjkKCmF1dGhvcml6ZWQYBiABKAsyJS5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUF1dGhvcml6ZWQSMwoHYXBwbGllZBgHIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXBwbGllZBI1CghvYnNlcnZlZBgIIAEoCzIjLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWQSOwoLd2l0aGRyYXdpbmcYCSABKAsyJi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVdpdGhkcmF3aW5nEjMKB3JldGlyZWQYCiABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJldGlyZWQSOAoOYnVpbGRfc2l6ZV9nYXAYCyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkEKDHJlZ2lzdHJhdGlvbhgMIAEoCzImLnNhYXMuYWNjb3VudHMudjEuU29sdXRpb25SZWdpc3RyYXRpb25IAogBARI+Cg1pbnN0YWxsYXRpb25zGA0gAygLMicuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVJbnN0YWxsYXRpb25CEQoPcHVibGlzaGVyX3ZhbHVlQhIKEGJ1aWxkX3NpemVfdmFsdWVCDwoNX3JlZ2lzdHJhdGlvbiI6ChxMaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0EhoKEmluY2x1ZGVfdG9tYnN0b25lZBgBIAEoCCKRAgodTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVzcG9uc2USMQoHZW50cmllcxgBIAMoCzIgLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRW50cnkSGQoRcmVnaXN0cnlfcmV2aXNpb24YAiABKAMSZgoUYXBwcm92ZWRfaW52ZW50b3JpZXMYAyADKAsySC5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlLkFwcHJvdmVkSW52ZW50b3JpZXNFbnRyeRo6ChhBcHByb3ZlZEludmVudG9yaWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgMOgI4ASJ4Cg5Nb2R1bGVFbnZlbG9wZRIQCghyZXZpc2lvbhgBIAEoAxIQCghkb2N1bWVudBgCIAEoCRISCgp3cml0dGVuX2J5GAMgASgJEi4KCndyaXR0ZW5fYXQYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIhoKGEdldE1vZHVsZUVudmVsb3BlUmVxdWVzdCKeAQoZR2V0TW9kdWxlRW52ZWxvcGVSZXNwb25zZRI3CghlbnZlbG9wZRgBIAEoCzIgLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlRW52ZWxvcGVIAIgBARI7CglyZXF1ZXN0ZWQYAiADKAsyKC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUF1dGhvcml0eVJlcXVlc3RCCwoJX2VudmVsb3BlIoMBChZNb2R1bGVBdXRob3JpdHlSZXF1ZXN0EhEKCXByaW5jaXBhbBgBIAEoCRIPCgdiaW5kaW5nGAIgASgJEhIKCmdlbmVyYXRpb24YAyABKAMSDQoFYWxpYXMYBCABKAkSDwoHYWxsb3dlZBgFIAEoCBIRCglhdXRob3JpdHkYBiABKAkiXQoYUHV0TW9kdWxlRW52ZWxvcGVSZXF1ZXN0EiIKEWV4cGVjdGVkX3JldmlzaW9uGAEgASgDQge6SAQiAigAEh0KCGRvY3VtZW50GAIgASgJQgu6SAhyBhABKICAQCJPChlQdXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlEjIKCGVudmVsb3BlGAEgASgLMiAuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFbnZlbG9wZSp+ChJDYXRhbG9ndWVFbnRyeUtpbmQSJAogQ0FUQUxPR1VFX0VOVFJZX0tJTkRfVU5TUEVDSUZJRUQQABIfChtDQVRBTE9HVUVfRU5UUllfS0lORF9NT0RVTEUQARIhCh1DQVRBTE9HVUVfRU5UUllfS0lORF9TT0xVVElPThACKq8BChJDYXRhbG9ndWVHYXBSZWFzb24SJAogQ0FUQUxPR1VFX0dBUF9SRUFTT05fVU5TUEVDSUZJRUQQABIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVDT1JERUQQARIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVQT1JURUQQAhIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfT0JTRVJWRUQQAyreAQoYQ2F0YWxvZ3VlT2JzZXJ2ZWRWZXJkaWN0EioKJkNBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1VOU1BFQ0lGSUVEEAASMQotQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19BVVRIT1JJWkVEEAESLgoqQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19ESUZGRVJTEAISMwovQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19VTkFVVEhPUklaRUQQAzKiIwoUUGxhdGZvcm1BZG1pblNlcnZpY2USjgEKC1NlYXJjaFVzZXJzEiQuc2Fhcy5hY2NvdW50cy52MS5TZWFyY2hVc2Vyc1JlcXVlc3QaJS5zYWFzLmFjY291bnRzLnYxLlNlYXJjaFVzZXJzUmVzcG9uc2UiMsLzGBQIAhABMAE6AhABQAFIBVADWANgA4LT5JMCFBISL3YxL3BsYXRmb3JtL3VzZXJzEqsBCgtTdXNwZW5kVXNlchIkLnNhYXMuYWNjb3VudHMudjEuU3VzcGVuZFVzZXJSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5Il7C8xgrCAIQATABOhcKE3NhYXMudXNlci5zdXNwZW5kZWQQAkABSAVQA1gDYAV4AoLT5JMCKToBKiIkL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTpzdXNwZW5kErMBCg1VbnN1c3BlbmRVc2VyEiYuc2Fhcy5hY2NvdW50cy52MS5VbnN1c3BlbmRVc2VyUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJiwvMYLQgCEAEwAToZChVzYWFzLnVzZXIudW5zdXNwZW5kZWQQAkABSAVQA1gDYAV4AoLT5JMCKzoBKiImL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTp1bnN1c3BlbmQS1gEKD0ltcGVyc29uYXRlVXNlchIoLnNhYXMuYWNjb3VudHMudjEuSW1wZXJzb25hdGVVc2VyUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuSW1wZXJzb25hdGVVc2VyUmVzcG9uc2UibsLzGDcIAhABMAQ6Iwofc2Fhcy5wbGF0Zm9ybS51c2VyX2ltcGVyc29uYXRlZBACQAFIBVADWARgA3gCgtPkkwItOgEqIigvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OmltcGVyc29uYXRlEtgBChFTdG9wSW1wZXJzb25hdGlvbhIqLnNhYXMuYWNjb3VudHMudjEuU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5TdG9wSW1wZXJzb25hdGlvblJlc3BvbnNlImrC8xg8CAIQATABOioKJnNhYXMucGxhdGZvcm0udXNlcl9pbXBlcnNvbmF0aW9uX2VuZGVkEAJAAUgFUANYA2ABgtPkkwIkOgEqIh8vdjEvcGxhdGZvcm0vaW1wZXJzb25hdGlvbjpzdG9wEqYBChJMaXN0QWN0aXZlU2Vzc2lvbnMSKy5zYWFzLmFjY291bnRzLnYxLkxpc3RBY3RpdmVTZXNzaW9uc1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkxpc3RBY3RpdmVTZXNzaW9uc1Jlc3BvbnNlIjXC8xgUCAIQATABOgIQAUABSAVQA1gDYAOC0+STAhcSFS92MS9wbGF0Zm9ybS9zZXNzaW9ucxKpAQoNUmV2b2tlU2Vzc2lvbhImLnNhYXMuYWNjb3VudHMudjEuUmV2b2tlU2Vzc2lvblJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiWMLzGCoIAhABMAE6GAoUc2Fhcy5zZXNzaW9uLnJldm9rZWQQAkABSAVQA1gDYAOC0+STAiQqIi92MS9wbGF0Zm9ybS9zZXNzaW9ucy97c2Vzc2lvbl9pZH0SzwEKEkdldE9yZ0VudGl0bGVtZW50cxIrLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnRW50aXRsZW1lbnRzUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2UiXsLzGCIIAhADKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIyEjAvdjEvcGxhdGZvcm0vb3JnYW5pemF0aW9ucy97b3JnX2lkfS9lbnRpdGxlbWVudHMS5AEKE092ZXJyaWRlRW50aXRsZW1lbnQSLC5zYWFzLmFjY291bnRzLnYxLk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0Gi0uc2Fhcy5hY2NvdW50cy52MS5PdmVycmlkZUVudGl0bGVtZW50UmVzcG9uc2UicMLzGDEIAhABMAE6HQoZc2Fhcy5lbnRpdGxlbWVudC5vdmVycmlkZRACQAFIBVADWANgAngCgtPkkwI1OgEqIjAvdjEvcGxhdGZvcm0vb3JnYW5pemF0aW9ucy97b3JnX2lkfS9lbnRpdGxlbWVudHMSrQEKEUdyYW50UGxhdGZvcm1Sb2xlEiouc2Fhcy5hY2NvdW50cy52MS5HcmFudFBsYXRmb3JtUm9sZVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiVMLzGDIIAhABMAQ6Hgoac2Fhcy5wbGF0Zm9ybS5yb2xlX2dyYW50ZWQQAkABSAVQA1gDYAV4AoLT5JMCGDoBKiITL3YxL3BsYXRmb3JtL2FkbWlucxK2AQoSUmV2b2tlUGxhdGZvcm1Sb2xlEisuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VQbGF0Zm9ybVJvbGVSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IlvC8xgyCAIQATAEOh4KGnNhYXMucGxhdGZvcm0ucm9sZV9yZXZva2VkEAJAAUgFUANYA2AFeAKC0+STAh8qHS92MS9wbGF0Zm9ybS9hZG1pbnMve3VzZXJfaWR9EqQBChJMaXN0UGxhdGZvcm1BZG1pbnMSKy5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUFkbWluc1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUFkbWluc1Jlc3BvbnNlIjPC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhUSEy92MS9wbGF0Zm9ybS9hZG1pbnMSjwEKFExpc3RBbGxPcmdhbml6YXRpb25zEi0uc2Fhcy5hY2NvdW50cy52MS5MaXN0QWxsT3JnYW5pemF0aW9uc1JlcXVlc3QaLi5zYWFzLmFjY291bnRzLnYxLkxpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2UiGMLzGBQIAhABMAE6AhABQAFIBVADWANgAxKSAQoVR2V0T3JnYW5pemF0aW9uUm9zdGVyEi4uc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZSIYwvMYFAgCEAEwAToCEAFAAUgFUANYA2ADEqUBChBMaXN0RmVhdHVyZUZsYWdzEikuc2Fhcy5hY2NvdW50cy52MS5MaXN0RmVhdHVyZUZsYWdzUmVxdWVzdBoqLnNhYXMuYWNjb3VudHMudjEuTGlzdEZlYXR1cmVGbGFnc1Jlc3BvbnNlIjrC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhwSGi92MS9wbGF0Zm9ybS9mZWF0dXJlLWZsYWdzEtIBChFVcHNlcnRGZWF0dXJlRmxhZxIqLnNhYXMuYWNjb3VudHMudjEuVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5VcHNlcnRGZWF0dXJlRmxhZ1Jlc3BvbnNlImSIAgHC8xgxCAIQATABOh0KGXNhYXMuZmVhdHVyZV9mbGFnLnVwZGF0ZWQQAkABSAVQA1gDYAV4AoLT5JMCJjoBKhohL3YxL3BsYXRmb3JtL2ZlYXR1cmUtZmxhZ3Mve25hbWV9Ep8BChBHZXRKb2JPcGVyYXRpb25zEiUuc2Fhcy5qb2JzLnYxLkdldEpvYk9wZXJhdGlvbnNSZXF1ZXN0GiYuc2Fhcy5qb2JzLnYxLkdldEpvYk9wZXJhdGlvbnNSZXNwb25zZSI8wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIeEhwvdjEvcGxhdGZvcm0vam9icy9vcGVyYXRpb25zEnwKCExpc3RKb2JzEh0uc2Fhcy5qb2JzLnYxLkxpc3RKb2JzUmVxdWVzdBoeLnNhYXMuam9icy52MS5MaXN0Sm9ic1Jlc3BvbnNlIjHC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhMSES92MS9wbGF0Zm9ybS9qb2JzEn8KBkdldEpvYhIbLnNhYXMuam9icy52MS5HZXRKb2JSZXF1ZXN0Ghwuc2Fhcy5qb2JzLnYxLkdldEpvYlJlc3BvbnNlIjrC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhwSGi92MS9wbGF0Zm9ybS9qb2JzL3tqb2JfaWR9EqwBCglSZXBsYXlKb2ISHi5zYWFzLmpvYnMudjEuUmVwbGF5Sm9iUmVxdWVzdBofLnNhYXMuam9icy52MS5SZXBsYXlKb2JSZXNwb25zZSJewvMYJwgCEAEwAzoVChFzYWFzLmpvYi5yZXBsYXllZBACQANIBVADWANgBYLT5JMCLToBKiIoL3YxL3BsYXRmb3JtL2pvYnMve3NvdXJjZV9qb2JfaWR9OnJlcGxheRKrAQoSR2V0RXZlbnRPcGVyYXRpb25zEikuc2Fhcy5ldmVudHMudjEuR2V0RXZlbnRPcGVyYXRpb25zUmVxdWVzdBoqLnNhYXMuZXZlbnRzLnYxLkdldEV2ZW50T3BlcmF0aW9uc1Jlc3BvbnNlIj7C8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAiASHi92MS9wbGF0Zm9ybS9ldmVudHMvb3BlcmF0aW9ucxK6AQoWTGlzdEV2ZW50U3Vic2NyaXB0aW9ucxItLnNhYXMuZXZlbnRzLnYxLkxpc3RFdmVudFN1YnNjcmlwdGlvbnNSZXF1ZXN0Gi4uc2Fhcy5ldmVudHMudjEuTGlzdEV2ZW50U3Vic2NyaXB0aW9uc1Jlc3BvbnNlIkHC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAiMSIS92MS9wbGF0Zm9ybS9ldmVudHMvc3Vic2NyaXB0aW9ucxKwAQoVTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlEi4uc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXNwb25zZSI2wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIYEhYvdjEvcGxhdGZvcm0vY2F0YWxvZ3VlEsABChFHZXRNb2R1bGVFbnZlbG9wZRIqLnNhYXMuYWNjb3VudHMudjEuR2V0TW9kdWxlRW52ZWxvcGVSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5HZXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlIlLC8xgqCAIQASIUbW9kdWxlX2VudmVsb3BlOnJlYWQwAToCEAFAAUgFUANYA2AFgtPkkwIeEhwvdjEvcGxhdGZvcm0vbW9kdWxlLWVudmVsb3BlEu0BChFQdXRNb2R1bGVFbnZlbG9wZRIqLnNhYXMuYWNjb3VudHMudjEuUHV0TW9kdWxlRW52ZWxvcGVSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5QdXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlIn/C8xhUCAIQASIVbW9kdWxlX2VudmVsb3BlOndyaXRlMAM6KQolc2Fhcy5wbGF0Zm9ybS5tb2R1bGVfZW52ZWxvcGVfd3JpdHRlbhACQAFIBVADWANgBXgCgtPkkwIhOgEqGhwvdjEvcGxhdGZvcm0vbW9kdWxlLWVudmVsb3BlYgZwcm90bzM", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_accounts_v1_common, file_saas_accounts_v1_installations, file_saas_accounts_v1_solution_registry, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); /** * @generated from message saas.accounts.v1.SearchUsersRequest diff --git a/module/services/frontend/code/src/features/solution-installations/model/installations.test.ts b/module/services/frontend/code/src/features/solution-installations/model/installations.test.ts index fa4bf76b8..e1faa77ae 100644 --- a/module/services/frontend/code/src/features/solution-installations/model/installations.test.ts +++ b/module/services/frontend/code/src/features/solution-installations/model/installations.test.ts @@ -3,11 +3,9 @@ import { Code, ConnectError } from "@connectrpc/connect"; import { describe, expect, it } from "vitest"; import { RoleSchema } from "@/gen/saas/accounts/v1/common_pb"; import { + AvailableSolutionSchema, InstallationHealth, InstallationSchema, -} from "@/gen/saas/accounts/v1/installation_listing_pb"; -import { - AvailableSolutionSchema, OrganizationInstallationSchema, } from "@/gen/saas/accounts/v1/installations_pb"; import { diff --git a/module/services/frontend/code/src/features/solution-installations/model/installations.ts b/module/services/frontend/code/src/features/solution-installations/model/installations.ts index 184c8fa90..520f67daa 100644 --- a/module/services/frontend/code/src/features/solution-installations/model/installations.ts +++ b/module/services/frontend/code/src/features/solution-installations/model/installations.ts @@ -4,13 +4,11 @@ import type { ComponentProps } from "react"; import type { CollectionReadGrant } from "@/gen/saas/accounts/v1/authorization_pb"; import type { Role } from "@/gen/saas/accounts/v1/common_pb"; import { + type AvailableSolution, InstallationHealth, + type InstallationReadiness, InstallationStatus, -} from "@/gen/saas/accounts/v1/installation_listing_pb"; -import type { - AvailableSolution, - InstallationReadiness, - OrganizationInstallation, + type OrganizationInstallation, } from "@/gen/saas/accounts/v1/installations_pb"; import type { Badge } from "@/shared/ui"; diff --git a/module/services/frontend/code/src/features/solution-installations/service/queries.ts b/module/services/frontend/code/src/features/solution-installations/service/queries.ts index b0af97cda..60accf833 100644 --- a/module/services/frontend/code/src/features/solution-installations/service/queries.ts +++ b/module/services/frontend/code/src/features/solution-installations/service/queries.ts @@ -1,8 +1,8 @@ import { useQuery } from "@tanstack/react-query"; -import { InstallationStatus } from "@/gen/saas/accounts/v1/installation_listing_pb"; -import type { - AvailableSolution, - OrganizationInstallation, +import { + type AvailableSolution, + InstallationStatus, + type OrganizationInstallation, } from "@/gen/saas/accounts/v1/installations_pb"; import { useInstallationService } from "@/lib/hooks/use-api-client"; diff --git a/module/services/frontend/code/src/features/solution-installations/ui/installation-setup-page.tsx b/module/services/frontend/code/src/features/solution-installations/ui/installation-setup-page.tsx index c7612503b..e14b2fd17 100644 --- a/module/services/frontend/code/src/features/solution-installations/ui/installation-setup-page.tsx +++ b/module/services/frontend/code/src/features/solution-installations/ui/installation-setup-page.tsx @@ -5,11 +5,11 @@ import Link from "next/link"; import { useState } from "react"; import { toast } from "sonner"; import { EmptyState } from "@/components/empty-state"; -import { InstallationStatus } from "@/gen/saas/accounts/v1/installation_listing_pb"; import type { GrantInstallationToTeamResponse, OrganizationInstallation, } from "@/gen/saas/accounts/v1/installations_pb"; +import { InstallationStatus } from "@/gen/saas/accounts/v1/installations_pb"; import { useAuth } from "@/lib/auth"; import { Badge, diff --git a/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.tsx b/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.tsx index 9f435c99a..62141032f 100644 --- a/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.tsx +++ b/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.tsx @@ -6,8 +6,10 @@ import { useState } from "react"; import { toast } from "sonner"; import { EmptyState } from "@/components/empty-state"; import { OrgSelector } from "@/components/org-selector"; -import { InstallationStatus } from "@/gen/saas/accounts/v1/installation_listing_pb"; -import type { AvailableSolution } from "@/gen/saas/accounts/v1/installations_pb"; +import { + type AvailableSolution, + InstallationStatus, +} from "@/gen/saas/accounts/v1/installations_pb"; import { useAuth } from "@/lib/auth"; import { mayKeepRetainedRows, diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/installation_listing_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/installation_listing_pb.ts deleted file mode 100644 index c425a987b..000000000 --- a/module/services/frontend/code/src/gen/saas/accounts/v1/installation_listing_pb.ts +++ /dev/null @@ -1,307 +0,0 @@ -// @generated by protoc-gen-es v2.11.0 with parameter "target=ts,import_extension=none" -// @generated from file saas/accounts/v1/installation_listing.proto (package saas.accounts.v1, syntax proto3) -/* eslint-disable */ - -import type { GenEnum, GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; -import { enumDesc, fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; -import { file_buf_validate_validate } from "../../../buf/validate/validate_pb"; -import type { Timestamp } from "@bufbuild/protobuf/wkt"; -import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; -import type { Message } from "@bufbuild/protobuf"; - -/** - * Describes the file saas/accounts/v1/installation_listing.proto. - */ -export const file_saas_accounts_v1_installation_listing: GenFile = /*@__PURE__*/ - fileDesc("CitzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbl9saXN0aW5nLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIvkDCgxJbnN0YWxsYXRpb24SFAoCaWQYASABKAlCCLpIBXIDsAEBEhgKBm9yZ19pZBgCIAEoCUIIukgFcgOwAQESJAoSYWdlbnRfcHJpbmNpcGFsX2lkGAMgASgJQgi6SAVyA7ABARIkChJvd25lcl9wcmluY2lwYWxfaWQYBSABKAlCCLpIBXIDsAEBEh4KFmNvX293bmVyX3ByaW5jaXBhbF9pZHMYBiADKAkSJAoScm9vdF9zY29wZV9ub2RlX2lkGAcgASgJQgi6SAVyA7ABARI0CgZzdGF0dXMYCCABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN0YXR1cxIuCgpjcmVhdGVkX2F0GAkgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIzCgpyZXZva2VkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgAiAEBEh4KCXRhcmdldF9pZBgLIAEoCUILukgI2AEBcgOwAQESIAoOcmV2b2tlZF9yZWFzb24YDCABKAlCCLpIBXIDGIAEEiAKDmZhaWx1cmVfcmVhc29uGA0gASgJQgi6SAVyAxiABEINCgtfcmV2b2tlZF9hdEoECAQQBVITc29sdXRpb25faWRlbnRpZmllciKdAQoYTGlzdEluc3RhbGxhdGlvbnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoGc3RhdHVzGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdGF0dXMSHQoJcGFnZV9zaXplGAMgASgFQgq6SAcaBRj0AygAEhIKCnBhZ2VfdG9rZW4YBCABKAkigQEKE0luc3RhbGxhdGlvblN1bW1hcnkSNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SNAoGaGVhbHRoGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25IZWFsdGgicgoZTGlzdEluc3RhbGxhdGlvbnNSZXNwb25zZRI8Cg1pbnN0YWxsYXRpb25zGAEgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdW1tYXJ5EhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSq7AQoSSW5zdGFsbGF0aW9uU3RhdHVzEiMKH0lOU1RBTExBVElPTl9TVEFUVVNfVU5TUEVDSUZJRUQQABIeChpJTlNUQUxMQVRJT05fU1RBVFVTX0FDVElWRRABEh8KG0lOU1RBTExBVElPTl9TVEFUVVNfUkVWT0tFRBACEh8KG0lOU1RBTExBVElPTl9TVEFUVVNfUEVORElORxADEh4KGklOU1RBTExBVElPTl9TVEFUVVNfRkFJTEVEEAQqhAIKEkluc3RhbGxhdGlvbkhlYWx0aBIjCh9JTlNUQUxMQVRJT05fSEVBTFRIX1VOU1BFQ0lGSUVEEAASHwobSU5TVEFMTEFUSU9OX0hFQUxUSF9IRUFMVEhZEAESKQolSU5TVEFMTEFUSU9OX0hFQUxUSF9OT19FTElHSUJMRV9PV05FUhACEiUKIUlOU1RBTExBVElPTl9IRUFMVEhfQUdFTlRfUkVWT0tFRBADEiYKIklOU1RBTExBVElPTl9IRUFMVEhfQUdFTlRfRElTQUJMRUQQBBIuCipJTlNUQUxMQVRJT05fSEVBTFRIX1NUQU5ESU5HX0dSQU5UX01JU1NJTkcQBULAAQoUY29tLnNhYXMuYWNjb3VudHMudjFCGEluc3RhbGxhdGlvbkxpc3RpbmdQcm90b1ABWixhY2NvdW50cy9wa2cvZ2VuL3NhYXMvYWNjb3VudHMvdjE7YWNjb3VudHN2MaICA1NBWKoCEFNhYXMuQWNjb3VudHMuVjHKAhBTYWFzXEFjY291bnRzXFYx4gIcU2Fhc1xBY2NvdW50c1xWMVxHUEJNZXRhZGF0YeoCElNhYXM6OkFjY291bnRzOjpWMWIGcHJvdG8z", [file_buf_validate_validate, file_google_protobuf_timestamp]); - -/** - * Installation is one solution's bound identity in an org. Server-assigned ids; - * the browser cannot attest to tenancy, the agent principal, or the scope node. - * - * @generated from message saas.accounts.v1.Installation - */ -export type Installation = Message<"saas.accounts.v1.Installation"> & { - /** - * @generated from field: string id = 1; - */ - id: string; - - /** - * @generated from field: string org_id = 2; - */ - orgId: string; - - /** - * @generated from field: string agent_principal_id = 3; - */ - agentPrincipalId: string; - - /** - * @generated from field: string owner_principal_id = 5; - */ - ownerPrincipalId: string; - - /** - * @generated from field: repeated string co_owner_principal_ids = 6; - */ - coOwnerPrincipalIds: string[]; - - /** - * @generated from field: string root_scope_node_id = 7; - */ - rootScopeNodeId: string; - - /** - * @generated from field: saas.accounts.v1.InstallationStatus status = 8; - */ - status: InstallationStatus; - - /** - * @generated from field: google.protobuf.Timestamp created_at = 9; - */ - createdAt?: Timestamp; - - /** - * @generated from field: optional google.protobuf.Timestamp revoked_at = 10; - */ - revokedAt?: Timestamp; - - /** - * The immutable solution target this installation was consented to: one - * continuous period of one binding's presence on this host. It is never - * reused, so a replacement binding — even one claiming the same route alias — - * is a different target and inherits nothing. - * - * Deliberately NOT accompanied by the route alias. The alias is a property of - * the target that a later generation may move, and resolving it here would - * mean joining the host's presence state into a request-path listing — which - * would require granting request traffic read over every organisation's - * presence to answer about one. A consumer that needs the alias asks - * ListAvailableSolutions, which serves accepted applied state, and joins on - * this id. - * - * Empty only on a revoked historical row whose target could not be - * established at the cutover; an active installation always names one. - * - * @generated from field: string target_id = 11; - */ - targetId: string; - - /** - * Why a revoked installation was revoked, when it was not an administrator - * uninstalling: the installation-identity cutover, or a tenancy narrowing that - * withdrew this organisation. Empty on an active row. - * - * @generated from field: string revoked_reason = 12; - */ - revokedReason: string; - - /** - * Why the install step failed: the solution's bounded refusal reason, or a - * fixed reason when it never answered. Empty unless status is FAILED. - * - * @generated from field: string failure_reason = 13; - */ - failureReason: string; -}; - -/** - * Describes the message saas.accounts.v1.Installation. - * Use `create(InstallationSchema)` to create a new message. - */ -export const InstallationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installation_listing, 0); - -/** - * ListInstallationsRequest asks what one organization has installed. Every other - * RPC on this service names a single installation, so until this existed nothing - * could answer "what has this org installed?" — which is the authority read a - * per-organization solution projection is built on (issue #949). - * - * @generated from message saas.accounts.v1.ListInstallationsRequest - */ -export type ListInstallationsRequest = Message<"saas.accounts.v1.ListInstallationsRequest"> & { - /** - * @generated from field: string org_id = 1; - */ - orgId: string; - - /** - * Restrict to one lifecycle status. UNSPECIFIED returns every status, so a - * caller that must see only live installs asks for ACTIVE explicitly rather - * than leaning on a default that would silently widen if the enum grew. - * - * @generated from field: saas.accounts.v1.InstallationStatus status = 2; - */ - status: InstallationStatus; - - /** - * Page bound. 0 -> server default; capped so an organization with many - * installations can never return an unbounded result in one call. - * - * @generated from field: int32 page_size = 3; - */ - pageSize: number; - - /** - * Opaque cursor from a previous response's next_page_token; empty for page one. - * - * @generated from field: string page_token = 4; - */ - pageToken: string; -}; - -/** - * Describes the message saas.accounts.v1.ListInstallationsRequest. - * Use `create(ListInstallationsRequestSchema)` to create a new message. - */ -export const ListInstallationsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installation_listing, 1); - -/** - * InstallationSummary is one row of the listing: the installation, plus the health - * resolved live beside it exactly as GetInstallation resolves it for one. Health - * travels with the row because a consumer that renders an installed solution as - * unavailable would otherwise need one GetInstallation per installation. - * - * @generated from message saas.accounts.v1.InstallationSummary - */ -export type InstallationSummary = Message<"saas.accounts.v1.InstallationSummary"> & { - /** - * @generated from field: saas.accounts.v1.Installation installation = 1; - */ - installation?: Installation; - - /** - * @generated from field: saas.accounts.v1.InstallationHealth health = 2; - */ - health: InstallationHealth; -}; - -/** - * Describes the message saas.accounts.v1.InstallationSummary. - * Use `create(InstallationSummarySchema)` to create a new message. - */ -export const InstallationSummarySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installation_listing, 2); - -/** - * @generated from message saas.accounts.v1.ListInstallationsResponse - */ -export type ListInstallationsResponse = Message<"saas.accounts.v1.ListInstallationsResponse"> & { - /** - * @generated from field: repeated saas.accounts.v1.InstallationSummary installations = 1; - */ - installations: InstallationSummary[]; - - /** - * Set when more installations remain; pass it back as page_token. Empty on the - * last page. - * - * @generated from field: string next_page_token = 2; - */ - nextPageToken: string; -}; - -/** - * Describes the message saas.accounts.v1.ListInstallationsResponse. - * Use `create(ListInstallationsResponseSchema)` to create a new message. - */ -export const ListInstallationsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installation_listing, 3); - -/** - * InstallationStatus is the lifecycle of an installation row. An uninstall is a - * soft delete (status=revoked, revoked_at set) so the agent, grants, and audit - * trail remain resolvable. - * - * @generated from enum saas.accounts.v1.InstallationStatus - */ -export enum InstallationStatus { - /** - * @generated from enum value: INSTALLATION_STATUS_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - - /** - * @generated from enum value: INSTALLATION_STATUS_ACTIVE = 1; - */ - ACTIVE = 1, - - /** - * @generated from enum value: INSTALLATION_STATUS_REVOKED = 2; - */ - REVOKED = 2, - - /** - * Installed, and waiting for the solution to acknowledge its standard install - * step. Nothing can use a pending installation. - * - * @generated from enum value: INSTALLATION_STATUS_PENDING = 3; - */ - PENDING = 3, - - /** - * The solution refused its install step, or never answered within the - * attempt budget. Nothing can use it; an administrator may retry it. - * - * @generated from enum value: INSTALLATION_STATUS_FAILED = 4; - */ - FAILED = 4, -} - -/** - * Describes the enum saas.accounts.v1.InstallationStatus. - */ -export const InstallationStatusSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_saas_accounts_v1_installation_listing, 0); - -/** - * InstallationHealth is resolved live from current authority facts, never - * stored. It is the observable form of the fail-closed mint rule: a - * StartInstallationTask fails exactly when health is not HEALTHY, and transfer - * or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record - * nor any co-owner is currently an org admin (the Zapier-class "connection owner - * left the org" failure), surfaced rather than silently reassigned. - * - * @generated from enum saas.accounts.v1.InstallationHealth - */ -export enum InstallationHealth { - /** - * @generated from enum value: INSTALLATION_HEALTH_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - - /** - * @generated from enum value: INSTALLATION_HEALTH_HEALTHY = 1; - */ - HEALTHY = 1, - - /** - * @generated from enum value: INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER = 2; - */ - NO_ELIGIBLE_OWNER = 2, - - /** - * @generated from enum value: INSTALLATION_HEALTH_AGENT_REVOKED = 3; - */ - AGENT_REVOKED = 3, - - /** - * @generated from enum value: INSTALLATION_HEALTH_AGENT_DISABLED = 4; - */ - AGENT_DISABLED = 4, - - /** - * @generated from enum value: INSTALLATION_HEALTH_STANDING_GRANT_MISSING = 5; - */ - STANDING_GRANT_MISSING = 5, -} - -/** - * Describes the enum saas.accounts.v1.InstallationHealth. - */ -export const InstallationHealthSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_saas_accounts_v1_installation_listing, 1); - diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/installations_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/installations_pb.ts index 82f82346a..f377c7fc7 100644 --- a/module/services/frontend/code/src/gen/saas/accounts/v1/installations_pb.ts +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/installations_pb.ts @@ -6,12 +6,10 @@ import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobu import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; import { file_buf_validate_validate } from "../../../buf/validate/validate_pb"; import { file_google_api_annotations } from "../../../google/api/annotations_pb"; -import type { EmptySchema } from "@bufbuild/protobuf/wkt"; -import { file_google_protobuf_empty } from "@bufbuild/protobuf/wkt"; +import type { EmptySchema, Timestamp } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_empty, file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; import type { CollectionReadGrant, ScopeGrant } from "./authorization_pb"; import { file_saas_accounts_v1_authorization } from "./authorization_pb"; -import type { Installation, InstallationHealth, InstallationSchema, ListInstallationsRequestSchema, ListInstallationsResponseSchema } from "./installation_listing_pb"; -import { file_saas_accounts_v1_installation_listing } from "./installation_listing_pb"; import { file_saas_policy_v1_options } from "../../policy/v1/options_pb"; import type { Message } from "@bufbuild/protobuf"; @@ -19,7 +17,105 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/installations.proto. */ export const file_saas_accounts_v1_installations: GenFile = /*@__PURE__*/ - fileDesc("CiRzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbnMucHJvdG8SEHNhYXMuYWNjb3VudHMudjEi4AMKFkluc3RhbGxTb2x1dGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIkChBhZ2VudF9pZGVudGlmaWVyGAIgASgJQgq6SAdyBRABGIAEEh4KDGRpc3BsYXlfbmFtZRgEIAEoCUIIukgFcgMYgAQSIgoQcm9vdF9zY29wZV9sYWJlbBgGIAEoCUIIukgFcgMYgAQSGQoHcm9sZV9pZBgHIAEoCUIIukgFcgOwAQESJwoSb3duZXJfcHJpbmNpcGFsX2lkGAggASgJQgu6SAjYAQFyA7ABARIvChZjb19vd25lcl9wcmluY2lwYWxfaWRzGAkgAygJQg+6SAySAQkQICIFcgOwAQESKgoRYWxsb3dlZF9hdWRpZW5jZXMYCiADKAlCD7pIDJIBCRBAIgVyAxiABBInCg5hbGxvd2VkX3Njb3BlcxgLIAMoCUIPukgMkgEJEEAiBXIDGIABEhsKCXRhcmdldF9pZBgMIAEoCUIIukgFcgOwAQESHwoFc2NvcGUYDSABKAlCC7pICHIGEAIogIBASACIAQFCCAoGX3Njb3BlSgQIAxAESgQIBRAGUhNzb2x1dGlvbl9pZGVudGlmaWVyUg9yb290X3Njb3BlX3BhdGgidAoZTmFycm93SW5zdGFsbGF0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESGgoFc2NvcGUYAyABKAlCC7pICHIGEAIogIBAIlcKGFJldHJ5SW5zdGFsbGF0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEiVwoYVW5pbnN0YWxsU29sdXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABASK+AQokVHJhbnNmZXJJbnN0YWxsYXRpb25Pd25lcnNoaXBSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABARIoChZuZXdfb3duZXJfcHJpbmNpcGFsX2lkGAMgASgJQgi6SAVyA7ABARIvChZjb19vd25lcl9wcmluY2lwYWxfaWRzGAQgAygJQg+6SAySAQkQICIFcgOwAQEiVQoWR2V0SW5zdGFsbGF0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEihQEKF0dldEluc3RhbGxhdGlvblJlc3BvbnNlEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEjQKBmhlYWx0aBgCIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uSGVhbHRoInMKJExpc3RPcmdhbml6YXRpb25JbnN0YWxsYXRpb25zUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEh0KCXBhZ2Vfc2l6ZRgCIAEoBUIKukgHGgUY9AMoABISCgpwYWdlX3Rva2VuGAMgASgJIsgDChhPcmdhbml6YXRpb25JbnN0YWxsYXRpb24SNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SNAoGaGVhbHRoGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25IZWFsdGgSMwoGdGFyZ2V0GAMgASgLMiMuc2Fhcy5hY2NvdW50cy52MS5BdmFpbGFibGVTb2x1dGlvbhIXCg9yb290X3Njb3BlX3BhdGgYBCABKAkSPAoNZ3JhbnRlZF90ZWFtcxgFIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ29sbGVjdGlvblJlYWRHcmFudBI+Cg9pbmhlcml0ZWRfdGVhbXMYBiADKAsyJS5zYWFzLmFjY291bnRzLnYxLkNvbGxlY3Rpb25SZWFkR3JhbnQSOgoJcmVhZGluZXNzGAcgASgLMicuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25SZWFkaW5lc3MSOAoPcmVhY2hlZF9tb2R1bGVzGAggAygLMh8uc2Fhcy5hY2NvdW50cy52MS5SZWFjaGVkTW9kdWxlIu0BChJSZWFkaW5lc3NDb25kaXRpb24SNgoEa2luZBgBIAEoDjIoLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQ29uZGl0aW9uS2luZBIPCgdzdWJqZWN0GAIgASgJEg4KBmRldGFpbBgDIAEoCRI4CgpjbGVhcmVkX2J5GAQgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5SZWFkaW5lc3NDbGVhcmVkQnkSMQoGYWN0aW9uGAUgASgOMiEuc2Fhcy5hY2NvdW50cy52MS5SZWFkaW5lc3NBY3Rpb24SEQoJc291cmNlX2lkGAYgASgJIlsKFUluc3RhbGxhdGlvblJlYWRpbmVzcxINCgVyZWFkeRgBIAEoCBIzCgV1bm1ldBgCIAMoCzIkLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQ29uZGl0aW9uIk8KDVJlYWNoZWRNb2R1bGUSEAoIYXVkaWVuY2UYASABKAkSFwoPaW5zdGFsbGF0aW9uX2lkGAIgASgJEhMKC3JvdXRlX2FsaWFzGAMgASgJIoMBCiVMaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1Jlc3BvbnNlEkEKDWluc3RhbGxhdGlvbnMYASADKAsyKi5zYWFzLmFjY291bnRzLnYxLk9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkikwEKHkdyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESGQoHdGVhbV9pZBgDIAEoCUIIukgFcgOwAQESGQoHcm9sZV9pZBgEIAEoCUIIukgFcgOwAQEidQoTQ2FycmllZE1vZHVsZUFjY2VzcxIQCghhdWRpZW5jZRgBIAEoCRIXCg9pbnN0YWxsYXRpb25faWQYAiABKAkSEgoKc2NvcGVfcGF0aBgDIAEoCRIPCgdyb2xlX2lkGAQgASgJEg4KBnNjb3BlcxgFIAMoCSK7AQofR3JhbnRJbnN0YWxsYXRpb25Ub1RlYW1SZXNwb25zZRIrCgVncmFudBgBIAEoCzIcLnNhYXMuYWNjb3VudHMudjEuU2NvcGVHcmFudBI2CgdjYXJyaWVkGAIgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5DYXJyaWVkTW9kdWxlQWNjZXNzEjMKBXVubWV0GAMgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5SZWFkaW5lc3NDb25kaXRpb24ilgEKIVJldm9rZUluc3RhbGxhdGlvbkZyb21UZWFtUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESGQoHdGVhbV9pZBgDIAEoCUIIukgFcgOwAQESGQoHcm9sZV9pZBgEIAEoCUIIukgFcgOwAQEihAIKEUF2YWlsYWJsZVNvbHV0aW9uEhsKCXRhcmdldF9pZBgBIAEoCUIIukgFcgOwAQESEgoKYmluZGluZ19pZBgCIAEoCRITCgtyb3V0ZV9hbGlhcxgDIAEoCRIZChFyZWxlYXNlX3B1Ymxpc2hlchgEIAEoCRIUCgxyZWxlYXNlX25hbWUYBSABKAkSFwoPcmVsZWFzZV92ZXJzaW9uGAYgASgJEhkKEW9wZW5lZF9nZW5lcmF0aW9uGAcgASgEEhoKEmFwcGxpZWRfZ2VuZXJhdGlvbhgIIAEoBBIRCglpbnN0YWxsZWQYCSABKAgSFQoEa2luZBgKIAEoCUIHukgEcgIYICJsCh1MaXN0QXZhaWxhYmxlU29sdXRpb25zUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEh0KCXBhZ2Vfc2l6ZRgCIAEoBUIKukgHGgUY9AMoABISCgpwYWdlX3Rva2VuGAMgASgJInEKHkxpc3RBdmFpbGFibGVTb2x1dGlvbnNSZXNwb25zZRI2Cglzb2x1dGlvbnMYASADKAsyIy5zYWFzLmFjY291bnRzLnYxLkF2YWlsYWJsZVNvbHV0aW9uEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSrzAwoWUmVhZGluZXNzQ29uZGl0aW9uS2luZBIoCiRSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfVU5TUEVDSUZJRUQQABIxCi1SRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfSU5TVEFMTF9TVEVQX1BFTkRJTkcQARIwCixSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfSU5TVEFMTF9TVEVQX0ZBSUxFRBACEi0KKVJFQURJTkVTU19DT05ESVRJT05fS0lORF9SRUNPUkRfVU5IRUFMVEhZEAMSNQoxUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX01PRFVMRV9QUklOQ0lQQUxfT01JVFRFRBAEEjYKMlJFQURJTkVTU19DT05ESVRJT05fS0lORF9PUEVSQVRJT05fQklORElOR19PTUlUVEVEEAUSMQotUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX1NPVVJDRV9OT1RfREVMRUdBVEVEEAYSOQo1UkVBRElORVNTX0NPTkRJVElPTl9LSU5EX1JFQUNIRURfTU9EVUxFX05PVF9JTlNUQUxMRUQQBxI+CjpSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfQ0FSUklFRF9NT0RVTEVfQUNDRVNTX05PVF9ERVJJVkVEEAgq1QEKElJlYWRpbmVzc0NsZWFyZWRCeRIkCiBSRUFESU5FU1NfQ0xFQVJFRF9CWV9VTlNQRUNJRklFRBAAEh8KG1JFQURJTkVTU19DTEVBUkVEX0JZX1ZJRVdFUhABEisKJ1JFQURJTkVTU19DTEVBUkVEX0JZX09SR0FOSVpBVElPTl9BRE1JThACEioKJlJFQURJTkVTU19DTEVBUkVEX0JZX1BMQVRGT1JNX09QRVJBVE9SEAMSHwobUkVBRElORVNTX0NMRUFSRURfQllfTk9CT0RZEAQqnwIKD1JlYWRpbmVzc0FjdGlvbhIgChxSRUFESU5FU1NfQUNUSU9OX1VOU1BFQ0lGSUVEEAASKQolUkVBRElORVNTX0FDVElPTl9OT05FX0FUX1RISVNfU1VSRkFDRRABEicKI1JFQURJTkVTU19BQ1RJT05fUkVUUllfSU5TVEFMTF9TVEVQEAISJQohUkVBRElORVNTX0FDVElPTl9SRUNPTk5FQ1RfU09VUkNFEAMSJwojUkVBRElORVNTX0FDVElPTl9UUkFOU0ZFUl9PV05FUlNISVAQBBIrCidSRUFESU5FU1NfQUNUSU9OX0lOU1RBTExfUkVBQ0hFRF9NT0RVTEUQBRIZChVSRUFESU5FU1NfQUNUSU9OX1dBSVQQBjKqEgoTSW5zdGFsbGF0aW9uU2VydmljZRK6AQoPSW5zdGFsbFNvbHV0aW9uEiguc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsU29sdXRpb25SZXF1ZXN0Gh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24iXcLzGD0IAhAEKgwKBm9yZ19pZBACGAEwATodChlzYWFzLmluc3RhbGxhdGlvbi5jcmVhdGVkEAJAAUgEUANYA2ABgtPkkwIWOgEqIhEvdjEvaW5zdGFsbGF0aW9ucxLSAQoRVW5pbnN0YWxsU29sdXRpb24SKi5zYWFzLmFjY291bnRzLnYxLlVuaW5zdGFsbFNvbHV0aW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJ5wvMYPQgCEAQqDAoGb3JnX2lkEAIYATABOh0KGXNhYXMuaW5zdGFsbGF0aW9uLnJldm9rZWQQAkABSARQA1gDYAGC0+STAjI6ASoiLS92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9OnVuaW5zdGFsbBLSAQoSTmFycm93SW5zdGFsbGF0aW9uEisuc2Fhcy5hY2NvdW50cy52MS5OYXJyb3dJbnN0YWxsYXRpb25SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5InfC8xg+CAIQBCoMCgZvcmdfaWQQAhgBMAE6Hgoac2Fhcy5pbnN0YWxsYXRpb24ubmFycm93ZWQQAkABSARQA1gDYAGC0+STAi86ASoiKi92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9Om5hcnJvdxLWAQoRUmV0cnlJbnN0YWxsYXRpb24SKi5zYWFzLmFjY291bnRzLnYxLlJldHJ5SW5zdGFsbGF0aW9uUmVxdWVzdBoeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uInXC8xg9CAIQBCoMCgZvcmdfaWQQAhgBMAE6HQoZc2Fhcy5pbnN0YWxsYXRpb24ucmV0cmllZBACQAFIBFADWANgAYLT5JMCLjoBKiIpL3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH06cmV0cnkSiQIKHVRyYW5zZmVySW5zdGFsbGF0aW9uT3duZXJzaGlwEjYuc2Fhcy5hY2NvdW50cy52MS5UcmFuc2Zlckluc3RhbGxhdGlvbk93bmVyc2hpcFJlcXVlc3QaHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbiKPAcLzGEsIAhAEKgwKBm9yZ19pZBACGAEwATorCidzYWFzLmluc3RhbGxhdGlvbi5vd25lcnNoaXBfdHJhbnNmZXJyZWQQAkABSARQA1gDYAGC0+STAjo6ASoiNS92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9OnRyYW5zZmVyT3duZXJzaGlwErkBCg9HZXRJbnN0YWxsYXRpb24SKC5zYWFzLmFjY291bnRzLnYxLkdldEluc3RhbGxhdGlvblJlcXVlc3QaKS5zYWFzLmFjY291bnRzLnYxLkdldEluc3RhbGxhdGlvblJlc3BvbnNlIlHC8xgiCAIQAyoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCJRIjL3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH0ShgEKEUxpc3RJbnN0YWxsYXRpb25zEiouc2Fhcy5hY2NvdW50cy52MS5MaXN0SW5zdGFsbGF0aW9uc1JlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLkxpc3RJbnN0YWxsYXRpb25zUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARLKAQoWTGlzdEF2YWlsYWJsZVNvbHV0aW9ucxIvLnNhYXMuYWNjb3VudHMudjEuTGlzdEF2YWlsYWJsZVNvbHV0aW9uc1JlcXVlc3QaMC5zYWFzLmFjY291bnRzLnYxLkxpc3RBdmFpbGFibGVTb2x1dGlvbnNSZXNwb25zZSJNwvMYIggCEAQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAiESHy92MS9pbnN0YWxsYXRpb25zOmxpc3RBdmFpbGFibGUS0QEKHUxpc3RPcmdhbml6YXRpb25JbnN0YWxsYXRpb25zEjYuc2Fhcy5hY2NvdW50cy52MS5MaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1JlcXVlc3QaNy5zYWFzLmFjY291bnRzLnYxLkxpc3RPcmdhbml6YXRpb25JbnN0YWxsYXRpb25zUmVzcG9uc2UiP8LzGCIIAhAEKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwITEhEvdjEvaW5zdGFsbGF0aW9ucxLwAQoXR3JhbnRJbnN0YWxsYXRpb25Ub1RlYW0SMC5zYWFzLmFjY291bnRzLnYxLkdyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVxdWVzdBoxLnNhYXMuYWNjb3VudHMudjEuR3JhbnRJbnN0YWxsYXRpb25Ub1RlYW1SZXNwb25zZSJwwvMYOAgCEAQqDAoGb3JnX2lkEAIYATABOhYKEnNhYXMuc2NvcGUuZ3JhbnRlZBACQAFIBFADWANgAXgCgtPkkwIuOgEqIikvdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfS90ZWFtcxLtAQoaUmV2b2tlSW5zdGFsbGF0aW9uRnJvbVRlYW0SMy5zYWFzLmFjY291bnRzLnYxLlJldm9rZUluc3RhbGxhdGlvbkZyb21UZWFtUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSKBAcLzGDgIAhAEKgwKBm9yZ19pZBACGAEwAToWChJzYWFzLnNjb3BlLnJldm9rZWQQAkABSARQA1gDYAF4AoLT5JMCPzoBKiI6L3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH0vdGVhbXMve3RlYW1faWR9OnJldm9rZUK6AQoUY29tLnNhYXMuYWNjb3VudHMudjFCEkluc3RhbGxhdGlvbnNQcm90b1ABWixhY2NvdW50cy9wa2cvZ2VuL3NhYXMvYWNjb3VudHMvdjE7YWNjb3VudHN2MaICA1NBWKoCEFNhYXMuQWNjb3VudHMuVjHKAhBTYWFzXEFjY291bnRzXFYx4gIcU2Fhc1xBY2NvdW50c1xWMVxHUEJNZXRhZGF0YeoCElNhYXM6OkFjY291bnRzOjpWMWIGcHJvdG8z", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_saas_accounts_v1_authorization, file_saas_accounts_v1_installation_listing, file_saas_policy_v1_options]); + fileDesc("CiRzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbnMucHJvdG8SEHNhYXMuYWNjb3VudHMudjEi+QMKDEluc3RhbGxhdGlvbhIUCgJpZBgBIAEoCUIIukgFcgOwAQESGAoGb3JnX2lkGAIgASgJQgi6SAVyA7ABARIkChJhZ2VudF9wcmluY2lwYWxfaWQYAyABKAlCCLpIBXIDsAEBEiQKEm93bmVyX3ByaW5jaXBhbF9pZBgFIAEoCUIIukgFcgOwAQESHgoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgGIAMoCRIkChJyb290X3Njb3BlX25vZGVfaWQYByABKAlCCLpIBXIDsAEBEjQKBnN0YXR1cxgIIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uU3RhdHVzEi4KCmNyZWF0ZWRfYXQYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKCnJldm9rZWRfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSACIAQESHgoJdGFyZ2V0X2lkGAsgASgJQgu6SAjYAQFyA7ABARIgCg5yZXZva2VkX3JlYXNvbhgMIAEoCUIIukgFcgMYgAQSIAoOZmFpbHVyZV9yZWFzb24YDSABKAlCCLpIBXIDGIAEQg0KC19yZXZva2VkX2F0SgQIBBAFUhNzb2x1dGlvbl9pZGVudGlmaWVyIuADChZJbnN0YWxsU29sdXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESJAoQYWdlbnRfaWRlbnRpZmllchgCIAEoCUIKukgHcgUQARiABBIeCgxkaXNwbGF5X25hbWUYBCABKAlCCLpIBXIDGIAEEiIKEHJvb3Rfc2NvcGVfbGFiZWwYBiABKAlCCLpIBXIDGIAEEhkKB3JvbGVfaWQYByABKAlCCLpIBXIDsAEBEicKEm93bmVyX3ByaW5jaXBhbF9pZBgIIAEoCUILukgI2AEBcgOwAQESLwoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgJIAMoCUIPukgMkgEJECAiBXIDsAEBEioKEWFsbG93ZWRfYXVkaWVuY2VzGAogAygJQg+6SAySAQkQQCIFcgMYgAQSJwoOYWxsb3dlZF9zY29wZXMYCyADKAlCD7pIDJIBCRBAIgVyAxiAARIbCgl0YXJnZXRfaWQYDCABKAlCCLpIBXIDsAEBEh8KBXNjb3BlGA0gASgJQgu6SAhyBhACKICAQEgAiAEBQggKBl9zY29wZUoECAMQBEoECAUQBlITc29sdXRpb25faWRlbnRpZmllclIPcm9vdF9zY29wZV9wYXRoInQKGU5hcnJvd0luc3RhbGxhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhoKBXNjb3BlGAMgASgJQgu6SAhyBhACKICAQCJXChhSZXRyeUluc3RhbGxhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBIlcKGFVuaW5zdGFsbFNvbHV0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEivgEKJFRyYW5zZmVySW5zdGFsbGF0aW9uT3duZXJzaGlwUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESKAoWbmV3X293bmVyX3ByaW5jaXBhbF9pZBgDIAEoCUIIukgFcgOwAQESLwoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgEIAMoCUIPukgMkgEJECAiBXIDsAEBIlUKFkdldEluc3RhbGxhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBIoUBChdHZXRJbnN0YWxsYXRpb25SZXNwb25zZRI0CgxpbnN0YWxsYXRpb24YASABKAsyHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbhI0CgZoZWFsdGgYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbkhlYWx0aCKdAQoYTGlzdEluc3RhbGxhdGlvbnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoGc3RhdHVzGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdGF0dXMSHQoJcGFnZV9zaXplGAMgASgFQgq6SAcaBRj0AygAEhIKCnBhZ2VfdG9rZW4YBCABKAkigQEKE0luc3RhbGxhdGlvblN1bW1hcnkSNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SNAoGaGVhbHRoGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25IZWFsdGgicgoZTGlzdEluc3RhbGxhdGlvbnNSZXNwb25zZRI8Cg1pbnN0YWxsYXRpb25zGAEgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdW1tYXJ5EhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJzCiRMaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIdCglwYWdlX3NpemUYAiABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgDIAEoCSLIAwoYT3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEjQKBmhlYWx0aBgCIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uSGVhbHRoEjMKBnRhcmdldBgDIAEoCzIjLnNhYXMuYWNjb3VudHMudjEuQXZhaWxhYmxlU29sdXRpb24SFwoPcm9vdF9zY29wZV9wYXRoGAQgASgJEjwKDWdyYW50ZWRfdGVhbXMYBSADKAsyJS5zYWFzLmFjY291bnRzLnYxLkNvbGxlY3Rpb25SZWFkR3JhbnQSPgoPaW5oZXJpdGVkX3RlYW1zGAYgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50EjoKCXJlYWRpbmVzcxgHIAEoCzInLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uUmVhZGluZXNzEjgKD3JlYWNoZWRfbW9kdWxlcxgIIAMoCzIfLnNhYXMuYWNjb3VudHMudjEuUmVhY2hlZE1vZHVsZSLtAQoSUmVhZGluZXNzQ29uZGl0aW9uEjYKBGtpbmQYASABKA4yKC5zYWFzLmFjY291bnRzLnYxLlJlYWRpbmVzc0NvbmRpdGlvbktpbmQSDwoHc3ViamVjdBgCIAEoCRIOCgZkZXRhaWwYAyABKAkSOAoKY2xlYXJlZF9ieRgEIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQ2xlYXJlZEJ5EjEKBmFjdGlvbhgFIAEoDjIhLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQWN0aW9uEhEKCXNvdXJjZV9pZBgGIAEoCSJbChVJbnN0YWxsYXRpb25SZWFkaW5lc3MSDQoFcmVhZHkYASABKAgSMwoFdW5tZXQYAiADKAsyJC5zYWFzLmFjY291bnRzLnYxLlJlYWRpbmVzc0NvbmRpdGlvbiJPCg1SZWFjaGVkTW9kdWxlEhAKCGF1ZGllbmNlGAEgASgJEhcKD2luc3RhbGxhdGlvbl9pZBgCIAEoCRITCgtyb3V0ZV9hbGlhcxgDIAEoCSKDAQolTGlzdE9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbnNSZXNwb25zZRJBCg1pbnN0YWxsYXRpb25zGAEgAygLMiouc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb25JbnN0YWxsYXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIpMBCh5HcmFudEluc3RhbGxhdGlvblRvVGVhbVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhkKB3RlYW1faWQYAyABKAlCCLpIBXIDsAEBEhkKB3JvbGVfaWQYBCABKAlCCLpIBXIDsAEBInUKE0NhcnJpZWRNb2R1bGVBY2Nlc3MSEAoIYXVkaWVuY2UYASABKAkSFwoPaW5zdGFsbGF0aW9uX2lkGAIgASgJEhIKCnNjb3BlX3BhdGgYAyABKAkSDwoHcm9sZV9pZBgEIAEoCRIOCgZzY29wZXMYBSADKAkiuwEKH0dyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVzcG9uc2USKwoFZ3JhbnQYASABKAsyHC5zYWFzLmFjY291bnRzLnYxLlNjb3BlR3JhbnQSNgoHY2FycmllZBgCIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ2FycmllZE1vZHVsZUFjY2VzcxIzCgV1bm1ldBgDIAMoCzIkLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQ29uZGl0aW9uIpYBCiFSZXZva2VJbnN0YWxsYXRpb25Gcm9tVGVhbVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhkKB3RlYW1faWQYAyABKAlCCLpIBXIDsAEBEhkKB3JvbGVfaWQYBCABKAlCCLpIBXIDsAEBIoQCChFBdmFpbGFibGVTb2x1dGlvbhIbCgl0YXJnZXRfaWQYASABKAlCCLpIBXIDsAEBEhIKCmJpbmRpbmdfaWQYAiABKAkSEwoLcm91dGVfYWxpYXMYAyABKAkSGQoRcmVsZWFzZV9wdWJsaXNoZXIYBCABKAkSFAoMcmVsZWFzZV9uYW1lGAUgASgJEhcKD3JlbGVhc2VfdmVyc2lvbhgGIAEoCRIZChFvcGVuZWRfZ2VuZXJhdGlvbhgHIAEoBBIaChJhcHBsaWVkX2dlbmVyYXRpb24YCCABKAQSEQoJaW5zdGFsbGVkGAkgASgIEhUKBGtpbmQYCiABKAlCB7pIBHICGCAibAodTGlzdEF2YWlsYWJsZVNvbHV0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIdCglwYWdlX3NpemUYAiABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgDIAEoCSJxCh5MaXN0QXZhaWxhYmxlU29sdXRpb25zUmVzcG9uc2USNgoJc29sdXRpb25zGAEgAygLMiMuc2Fhcy5hY2NvdW50cy52MS5BdmFpbGFibGVTb2x1dGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkquwEKEkluc3RhbGxhdGlvblN0YXR1cxIjCh9JTlNUQUxMQVRJT05fU1RBVFVTX1VOU1BFQ0lGSUVEEAASHgoaSU5TVEFMTEFUSU9OX1NUQVRVU19BQ1RJVkUQARIfChtJTlNUQUxMQVRJT05fU1RBVFVTX1JFVk9LRUQQAhIfChtJTlNUQUxMQVRJT05fU1RBVFVTX1BFTkRJTkcQAxIeChpJTlNUQUxMQVRJT05fU1RBVFVTX0ZBSUxFRBAEKoQCChJJbnN0YWxsYXRpb25IZWFsdGgSIwofSU5TVEFMTEFUSU9OX0hFQUxUSF9VTlNQRUNJRklFRBAAEh8KG0lOU1RBTExBVElPTl9IRUFMVEhfSEVBTFRIWRABEikKJUlOU1RBTExBVElPTl9IRUFMVEhfTk9fRUxJR0lCTEVfT1dORVIQAhIlCiFJTlNUQUxMQVRJT05fSEVBTFRIX0FHRU5UX1JFVk9LRUQQAxImCiJJTlNUQUxMQVRJT05fSEVBTFRIX0FHRU5UX0RJU0FCTEVEEAQSLgoqSU5TVEFMTEFUSU9OX0hFQUxUSF9TVEFORElOR19HUkFOVF9NSVNTSU5HEAUq8wMKFlJlYWRpbmVzc0NvbmRpdGlvbktpbmQSKAokUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX1VOU1BFQ0lGSUVEEAASMQotUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX0lOU1RBTExfU1RFUF9QRU5ESU5HEAESMAosUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX0lOU1RBTExfU1RFUF9GQUlMRUQQAhItCilSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfUkVDT1JEX1VOSEVBTFRIWRADEjUKMVJFQURJTkVTU19DT05ESVRJT05fS0lORF9NT0RVTEVfUFJJTkNJUEFMX09NSVRURUQQBBI2CjJSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfT1BFUkFUSU9OX0JJTkRJTkdfT01JVFRFRBAFEjEKLVJFQURJTkVTU19DT05ESVRJT05fS0lORF9TT1VSQ0VfTk9UX0RFTEVHQVRFRBAGEjkKNVJFQURJTkVTU19DT05ESVRJT05fS0lORF9SRUFDSEVEX01PRFVMRV9OT1RfSU5TVEFMTEVEEAcSPgo6UkVBRElORVNTX0NPTkRJVElPTl9LSU5EX0NBUlJJRURfTU9EVUxFX0FDQ0VTU19OT1RfREVSSVZFRBAIKtUBChJSZWFkaW5lc3NDbGVhcmVkQnkSJAogUkVBRElORVNTX0NMRUFSRURfQllfVU5TUEVDSUZJRUQQABIfChtSRUFESU5FU1NfQ0xFQVJFRF9CWV9WSUVXRVIQARIrCidSRUFESU5FU1NfQ0xFQVJFRF9CWV9PUkdBTklaQVRJT05fQURNSU4QAhIqCiZSRUFESU5FU1NfQ0xFQVJFRF9CWV9QTEFURk9STV9PUEVSQVRPUhADEh8KG1JFQURJTkVTU19DTEVBUkVEX0JZX05PQk9EWRAEKp8CCg9SZWFkaW5lc3NBY3Rpb24SIAocUkVBRElORVNTX0FDVElPTl9VTlNQRUNJRklFRBAAEikKJVJFQURJTkVTU19BQ1RJT05fTk9ORV9BVF9USElTX1NVUkZBQ0UQARInCiNSRUFESU5FU1NfQUNUSU9OX1JFVFJZX0lOU1RBTExfU1RFUBACEiUKIVJFQURJTkVTU19BQ1RJT05fUkVDT05ORUNUX1NPVVJDRRADEicKI1JFQURJTkVTU19BQ1RJT05fVFJBTlNGRVJfT1dORVJTSElQEAQSKwonUkVBRElORVNTX0FDVElPTl9JTlNUQUxMX1JFQUNIRURfTU9EVUxFEAUSGQoVUkVBRElORVNTX0FDVElPTl9XQUlUEAYyqhIKE0luc3RhbGxhdGlvblNlcnZpY2USugEKD0luc3RhbGxTb2x1dGlvbhIoLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbFNvbHV0aW9uUmVxdWVzdBoeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uIl3C8xg9CAIQBCoMCgZvcmdfaWQQAhgBMAE6HQoZc2Fhcy5pbnN0YWxsYXRpb24uY3JlYXRlZBACQAFIBFADWANgAYLT5JMCFjoBKiIRL3YxL2luc3RhbGxhdGlvbnMS0gEKEVVuaW5zdGFsbFNvbHV0aW9uEiouc2Fhcy5hY2NvdW50cy52MS5Vbmluc3RhbGxTb2x1dGlvblJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiecLzGD0IAhAEKgwKBm9yZ19pZBACGAEwATodChlzYWFzLmluc3RhbGxhdGlvbi5yZXZva2VkEAJAAUgEUANYA2ABgtPkkwIyOgEqIi0vdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfTp1bmluc3RhbGwS0gEKEk5hcnJvd0luc3RhbGxhdGlvbhIrLnNhYXMuYWNjb3VudHMudjEuTmFycm93SW5zdGFsbGF0aW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJ3wvMYPggCEAQqDAoGb3JnX2lkEAIYATABOh4KGnNhYXMuaW5zdGFsbGF0aW9uLm5hcnJvd2VkEAJAAUgEUANYA2ABgtPkkwIvOgEqIiovdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfTpuYXJyb3cS1gEKEVJldHJ5SW5zdGFsbGF0aW9uEiouc2Fhcy5hY2NvdW50cy52MS5SZXRyeUluc3RhbGxhdGlvblJlcXVlc3QaHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbiJ1wvMYPQgCEAQqDAoGb3JnX2lkEAIYATABOh0KGXNhYXMuaW5zdGFsbGF0aW9uLnJldHJpZWQQAkABSARQA1gDYAGC0+STAi46ASoiKS92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9OnJldHJ5EokCCh1UcmFuc2Zlckluc3RhbGxhdGlvbk93bmVyc2hpcBI2LnNhYXMuYWNjb3VudHMudjEuVHJhbnNmZXJJbnN0YWxsYXRpb25Pd25lcnNoaXBSZXF1ZXN0Gh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24ijwHC8xhLCAIQBCoMCgZvcmdfaWQQAhgBMAE6Kwonc2Fhcy5pbnN0YWxsYXRpb24ub3duZXJzaGlwX3RyYW5zZmVycmVkEAJAAUgEUANYA2ABgtPkkwI6OgEqIjUvdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfTp0cmFuc2Zlck93bmVyc2hpcBK5AQoPR2V0SW5zdGFsbGF0aW9uEiguc2Fhcy5hY2NvdW50cy52MS5HZXRJbnN0YWxsYXRpb25SZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5HZXRJbnN0YWxsYXRpb25SZXNwb25zZSJRwvMYIggCEAMqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAiUSIy92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9EoYBChFMaXN0SW5zdGFsbGF0aW9ucxIqLnNhYXMuYWNjb3VudHMudjEuTGlzdEluc3RhbGxhdGlvbnNSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5MaXN0SW5zdGFsbGF0aW9uc1Jlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESygEKFkxpc3RBdmFpbGFibGVTb2x1dGlvbnMSLy5zYWFzLmFjY291bnRzLnYxLkxpc3RBdmFpbGFibGVTb2x1dGlvbnNSZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5MaXN0QXZhaWxhYmxlU29sdXRpb25zUmVzcG9uc2UiTcLzGCIIAhAEKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIhEh8vdjEvaW5zdGFsbGF0aW9uczpsaXN0QXZhaWxhYmxlEtEBCh1MaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9ucxI2LnNhYXMuYWNjb3VudHMudjEuTGlzdE9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbnNSZXF1ZXN0Gjcuc2Fhcy5hY2NvdW50cy52MS5MaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1Jlc3BvbnNlIj/C8xgiCAIQBCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCExIRL3YxL2luc3RhbGxhdGlvbnMS8AEKF0dyYW50SW5zdGFsbGF0aW9uVG9UZWFtEjAuc2Fhcy5hY2NvdW50cy52MS5HcmFudEluc3RhbGxhdGlvblRvVGVhbVJlcXVlc3QaMS5zYWFzLmFjY291bnRzLnYxLkdyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVzcG9uc2UicMLzGDgIAhAEKgwKBm9yZ19pZBACGAEwAToWChJzYWFzLnNjb3BlLmdyYW50ZWQQAkABSARQA1gDYAF4AoLT5JMCLjoBKiIpL3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH0vdGVhbXMS7QEKGlJldm9rZUluc3RhbGxhdGlvbkZyb21UZWFtEjMuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VJbnN0YWxsYXRpb25Gcm9tVGVhbVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkigQHC8xg4CAIQBCoMCgZvcmdfaWQQAhgBMAE6FgoSc2Fhcy5zY29wZS5yZXZva2VkEAJAAUgEUANYA2ABeAKC0+STAj86ASoiOi92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9L3RlYW1zL3t0ZWFtX2lkfTpyZXZva2VCugEKFGNvbS5zYWFzLmFjY291bnRzLnYxQhJJbnN0YWxsYXRpb25zUHJvdG9QAVosYWNjb3VudHMvcGtnL2dlbi9zYWFzL2FjY291bnRzL3YxO2FjY291bnRzdjGiAgNTQViqAhBTYWFzLkFjY291bnRzLlYxygIQU2Fhc1xBY2NvdW50c1xWMeICHFNhYXNcQWNjb3VudHNcVjFcR1BCTWV0YWRhdGHqAhJTYWFzOjpBY2NvdW50czo6VjFiBnByb3RvMw", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_policy_v1_options]); + +/** + * Installation is one solution's bound identity in an org. Server-assigned ids; + * the browser cannot attest to tenancy, the agent principal, or the scope node. + * + * @generated from message saas.accounts.v1.Installation + */ +export type Installation = Message<"saas.accounts.v1.Installation"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: string org_id = 2; + */ + orgId: string; + + /** + * @generated from field: string agent_principal_id = 3; + */ + agentPrincipalId: string; + + /** + * @generated from field: string owner_principal_id = 5; + */ + ownerPrincipalId: string; + + /** + * @generated from field: repeated string co_owner_principal_ids = 6; + */ + coOwnerPrincipalIds: string[]; + + /** + * @generated from field: string root_scope_node_id = 7; + */ + rootScopeNodeId: string; + + /** + * @generated from field: saas.accounts.v1.InstallationStatus status = 8; + */ + status: InstallationStatus; + + /** + * @generated from field: google.protobuf.Timestamp created_at = 9; + */ + createdAt?: Timestamp; + + /** + * @generated from field: optional google.protobuf.Timestamp revoked_at = 10; + */ + revokedAt?: Timestamp; + + /** + * The immutable solution target this installation was consented to: one + * continuous period of one binding's presence on this host. It is never + * reused, so a replacement binding — even one claiming the same route alias — + * is a different target and inherits nothing. + * + * Deliberately NOT accompanied by the route alias. The alias is a property of + * the target that a later generation may move, and resolving it here would + * mean joining the host's presence state into a request-path listing — which + * would require granting request traffic read over every organisation's + * presence to answer about one. A consumer that needs the alias asks + * ListAvailableSolutions, which serves accepted applied state, and joins on + * this id. + * + * Empty only on a revoked historical row whose target could not be + * established at the cutover; an active installation always names one. + * + * @generated from field: string target_id = 11; + */ + targetId: string; + + /** + * Why a revoked installation was revoked, when it was not an administrator + * uninstalling: the installation-identity cutover, or a tenancy narrowing that + * withdrew this organisation. Empty on an active row. + * + * @generated from field: string revoked_reason = 12; + */ + revokedReason: string; + + /** + * Why the install step failed: the solution's bounded refusal reason, or a + * fixed reason when it never answered. Empty unless status is FAILED. + * + * @generated from field: string failure_reason = 13; + */ + failureReason: string; +}; + +/** + * Describes the message saas.accounts.v1.Installation. + * Use `create(InstallationSchema)` to create a new message. + */ +export const InstallationSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_installations, 0); /** * @generated from message saas.accounts.v1.InstallSolutionRequest @@ -133,7 +229,7 @@ export type InstallSolutionRequest = Message<"saas.accounts.v1.InstallSolutionRe * Use `create(InstallSolutionRequestSchema)` to create a new message. */ export const InstallSolutionRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 0); + messageDesc(file_saas_accounts_v1_installations, 1); /** * An org administrator may only remove installed authority. The host reads @@ -165,7 +261,7 @@ export type NarrowInstallationRequest = Message<"saas.accounts.v1.NarrowInstalla * Use `create(NarrowInstallationRequestSchema)` to create a new message. */ export const NarrowInstallationRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 1); + messageDesc(file_saas_accounts_v1_installations, 2); /** * @generated from message saas.accounts.v1.RetryInstallationRequest @@ -187,7 +283,7 @@ export type RetryInstallationRequest = Message<"saas.accounts.v1.RetryInstallati * Use `create(RetryInstallationRequestSchema)` to create a new message. */ export const RetryInstallationRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 2); + messageDesc(file_saas_accounts_v1_installations, 3); /** * @generated from message saas.accounts.v1.UninstallSolutionRequest @@ -209,7 +305,7 @@ export type UninstallSolutionRequest = Message<"saas.accounts.v1.UninstallSoluti * Use `create(UninstallSolutionRequestSchema)` to create a new message. */ export const UninstallSolutionRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 3); + messageDesc(file_saas_accounts_v1_installations, 4); /** * @generated from message saas.accounts.v1.TransferInstallationOwnershipRequest @@ -245,7 +341,7 @@ export type TransferInstallationOwnershipRequest = Message<"saas.accounts.v1.Tra * Use `create(TransferInstallationOwnershipRequestSchema)` to create a new message. */ export const TransferInstallationOwnershipRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 4); + messageDesc(file_saas_accounts_v1_installations, 5); /** * @generated from message saas.accounts.v1.GetInstallationRequest @@ -267,7 +363,7 @@ export type GetInstallationRequest = Message<"saas.accounts.v1.GetInstallationRe * Use `create(GetInstallationRequestSchema)` to create a new message. */ export const GetInstallationRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 5); + messageDesc(file_saas_accounts_v1_installations, 6); /** * @generated from message saas.accounts.v1.GetInstallationResponse @@ -291,7 +387,105 @@ export type GetInstallationResponse = Message<"saas.accounts.v1.GetInstallationR * Use `create(GetInstallationResponseSchema)` to create a new message. */ export const GetInstallationResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 6); + messageDesc(file_saas_accounts_v1_installations, 7); + +/** + * ListInstallationsRequest asks what one organization has installed. Every other + * RPC on this service names a single installation, so until this existed nothing + * could answer "what has this org installed?" — which is the authority read a + * per-organization solution projection is built on (issue #949). + * + * @generated from message saas.accounts.v1.ListInstallationsRequest + */ +export type ListInstallationsRequest = Message<"saas.accounts.v1.ListInstallationsRequest"> & { + /** + * @generated from field: string org_id = 1; + */ + orgId: string; + + /** + * Restrict to one lifecycle status. UNSPECIFIED returns every status, so a + * caller that must see only live installs asks for ACTIVE explicitly rather + * than leaning on a default that would silently widen if the enum grew. + * + * @generated from field: saas.accounts.v1.InstallationStatus status = 2; + */ + status: InstallationStatus; + + /** + * Page bound. 0 -> server default; capped so an organization with many + * installations can never return an unbounded result in one call. + * + * @generated from field: int32 page_size = 3; + */ + pageSize: number; + + /** + * Opaque cursor from a previous response's next_page_token; empty for page one. + * + * @generated from field: string page_token = 4; + */ + pageToken: string; +}; + +/** + * Describes the message saas.accounts.v1.ListInstallationsRequest. + * Use `create(ListInstallationsRequestSchema)` to create a new message. + */ +export const ListInstallationsRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_installations, 8); + +/** + * InstallationSummary is one row of the listing: the installation, plus the health + * resolved live beside it exactly as GetInstallation resolves it for one. Health + * travels with the row because a consumer that renders an installed solution as + * unavailable would otherwise need one GetInstallation per installation. + * + * @generated from message saas.accounts.v1.InstallationSummary + */ +export type InstallationSummary = Message<"saas.accounts.v1.InstallationSummary"> & { + /** + * @generated from field: saas.accounts.v1.Installation installation = 1; + */ + installation?: Installation; + + /** + * @generated from field: saas.accounts.v1.InstallationHealth health = 2; + */ + health: InstallationHealth; +}; + +/** + * Describes the message saas.accounts.v1.InstallationSummary. + * Use `create(InstallationSummarySchema)` to create a new message. + */ +export const InstallationSummarySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_installations, 9); + +/** + * @generated from message saas.accounts.v1.ListInstallationsResponse + */ +export type ListInstallationsResponse = Message<"saas.accounts.v1.ListInstallationsResponse"> & { + /** + * @generated from field: repeated saas.accounts.v1.InstallationSummary installations = 1; + */ + installations: InstallationSummary[]; + + /** + * Set when more installations remain; pass it back as page_token. Empty on the + * last page. + * + * @generated from field: string next_page_token = 2; + */ + nextPageToken: string; +}; + +/** + * Describes the message saas.accounts.v1.ListInstallationsResponse. + * Use `create(ListInstallationsResponseSchema)` to create a new message. + */ +export const ListInstallationsResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_installations, 10); /** * @generated from message saas.accounts.v1.ListOrganizationInstallationsRequest @@ -322,7 +516,7 @@ export type ListOrganizationInstallationsRequest = Message<"saas.accounts.v1.Lis * Use `create(ListOrganizationInstallationsRequestSchema)` to create a new message. */ export const ListOrganizationInstallationsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 7); + messageDesc(file_saas_accounts_v1_installations, 11); /** * OrganizationInstallation is one active installation as its organisation's @@ -401,7 +595,7 @@ export type OrganizationInstallation = Message<"saas.accounts.v1.OrganizationIns * Use `create(OrganizationInstallationSchema)` to create a new message. */ export const OrganizationInstallationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 8); + messageDesc(file_saas_accounts_v1_installations, 12); /** * ReadinessCondition is one thing that is unmet, in the words of whoever owns @@ -454,7 +648,7 @@ export type ReadinessCondition = Message<"saas.accounts.v1.ReadinessCondition"> * Use `create(ReadinessConditionSchema)` to create a new message. */ export const ReadinessConditionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 9); + messageDesc(file_saas_accounts_v1_installations, 13); /** * InstallationReadiness is whether the installation will work, which is NOT @@ -485,7 +679,7 @@ export type InstallationReadiness = Message<"saas.accounts.v1.InstallationReadin * Use `create(InstallationReadinessSchema)` to create a new message. */ export const InstallationReadinessSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 10); + messageDesc(file_saas_accounts_v1_installations, 14); /** * ReachedModule is one module an installed target declares it reaches on the @@ -534,7 +728,7 @@ export type ReachedModule = Message<"saas.accounts.v1.ReachedModule"> & { * Use `create(ReachedModuleSchema)` to create a new message. */ export const ReachedModuleSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 11); + messageDesc(file_saas_accounts_v1_installations, 15); /** * @generated from message saas.accounts.v1.ListOrganizationInstallationsResponse @@ -558,7 +752,7 @@ export type ListOrganizationInstallationsResponse = Message<"saas.accounts.v1.Li * Use `create(ListOrganizationInstallationsResponseSchema)` to create a new message. */ export const ListOrganizationInstallationsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 12); + messageDesc(file_saas_accounts_v1_installations, 16); /** * @generated from message saas.accounts.v1.GrantInstallationToTeamRequest @@ -595,7 +789,7 @@ export type GrantInstallationToTeamRequest = Message<"saas.accounts.v1.GrantInst * Use `create(GrantInstallationToTeamRequestSchema)` to create a new message. */ export const GrantInstallationToTeamRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 13); + messageDesc(file_saas_accounts_v1_installations, 17); /** * CarriedModuleAccess is one derived grant a solution's grant carried: the @@ -658,7 +852,7 @@ export type CarriedModuleAccess = Message<"saas.accounts.v1.CarriedModuleAccess" * Use `create(CarriedModuleAccessSchema)` to create a new message. */ export const CarriedModuleAccessSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 14); + messageDesc(file_saas_accounts_v1_installations, 18); /** * @generated from message saas.accounts.v1.GrantInstallationToTeamResponse @@ -696,7 +890,7 @@ export type GrantInstallationToTeamResponse = Message<"saas.accounts.v1.GrantIns * Use `create(GrantInstallationToTeamResponseSchema)` to create a new message. */ export const GrantInstallationToTeamResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 15); + messageDesc(file_saas_accounts_v1_installations, 19); /** * @generated from message saas.accounts.v1.RevokeInstallationFromTeamRequest @@ -731,7 +925,7 @@ export type RevokeInstallationFromTeamRequest = Message<"saas.accounts.v1.Revoke * Use `create(RevokeInstallationFromTeamRequestSchema)` to create a new message. */ export const RevokeInstallationFromTeamRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 16); + messageDesc(file_saas_accounts_v1_installations, 20); /** * AvailableSolution is one installable solution target: its immutable identity, @@ -821,7 +1015,7 @@ export type AvailableSolution = Message<"saas.accounts.v1.AvailableSolution"> & * Use `create(AvailableSolutionSchema)` to create a new message. */ export const AvailableSolutionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 17); + messageDesc(file_saas_accounts_v1_installations, 21); /** * @generated from message saas.accounts.v1.ListAvailableSolutionsRequest @@ -851,7 +1045,7 @@ export type ListAvailableSolutionsRequest = Message<"saas.accounts.v1.ListAvaila * Use `create(ListAvailableSolutionsRequestSchema)` to create a new message. */ export const ListAvailableSolutionsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 18); + messageDesc(file_saas_accounts_v1_installations, 22); /** * @generated from message saas.accounts.v1.ListAvailableSolutionsResponse @@ -873,7 +1067,101 @@ export type ListAvailableSolutionsResponse = Message<"saas.accounts.v1.ListAvail * Use `create(ListAvailableSolutionsResponseSchema)` to create a new message. */ export const ListAvailableSolutionsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_installations, 19); + messageDesc(file_saas_accounts_v1_installations, 23); + +/** + * InstallationStatus is the lifecycle of an installation row. An uninstall is a + * soft delete (status=revoked, revoked_at set) so the agent, grants, and audit + * trail remain resolvable. + * + * @generated from enum saas.accounts.v1.InstallationStatus + */ +export enum InstallationStatus { + /** + * @generated from enum value: INSTALLATION_STATUS_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: INSTALLATION_STATUS_ACTIVE = 1; + */ + ACTIVE = 1, + + /** + * @generated from enum value: INSTALLATION_STATUS_REVOKED = 2; + */ + REVOKED = 2, + + /** + * Installed, and waiting for the solution to acknowledge its standard install + * step. Nothing can use a pending installation. + * + * @generated from enum value: INSTALLATION_STATUS_PENDING = 3; + */ + PENDING = 3, + + /** + * The solution refused its install step, or never answered within the + * attempt budget. Nothing can use it; an administrator may retry it. + * + * @generated from enum value: INSTALLATION_STATUS_FAILED = 4; + */ + FAILED = 4, +} + +/** + * Describes the enum saas.accounts.v1.InstallationStatus. + */ +export const InstallationStatusSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_saas_accounts_v1_installations, 0); + +/** + * InstallationHealth is resolved live from current authority facts, never + * stored. It is the observable form of the fail-closed mint rule: a + * StartInstallationTask fails exactly when health is not HEALTHY, and transfer + * or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record + * nor any co-owner is currently an org admin (the Zapier-class "connection owner + * left the org" failure), surfaced rather than silently reassigned. + * + * @generated from enum saas.accounts.v1.InstallationHealth + */ +export enum InstallationHealth { + /** + * @generated from enum value: INSTALLATION_HEALTH_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: INSTALLATION_HEALTH_HEALTHY = 1; + */ + HEALTHY = 1, + + /** + * @generated from enum value: INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER = 2; + */ + NO_ELIGIBLE_OWNER = 2, + + /** + * @generated from enum value: INSTALLATION_HEALTH_AGENT_REVOKED = 3; + */ + AGENT_REVOKED = 3, + + /** + * @generated from enum value: INSTALLATION_HEALTH_AGENT_DISABLED = 4; + */ + AGENT_DISABLED = 4, + + /** + * @generated from enum value: INSTALLATION_HEALTH_STANDING_GRANT_MISSING = 5; + */ + STANDING_GRANT_MISSING = 5, +} + +/** + * Describes the enum saas.accounts.v1.InstallationHealth. + */ +export const InstallationHealthSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_saas_accounts_v1_installations, 1); /** * ReadinessConditionKind is what kind of thing is unmet. A reader acts on the @@ -961,7 +1249,7 @@ export enum ReadinessConditionKind { * Describes the enum saas.accounts.v1.ReadinessConditionKind. */ export const ReadinessConditionKindSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_saas_accounts_v1_installations, 0); + enumDesc(file_saas_accounts_v1_installations, 2); /** * ReadinessClearedBy is who can clear a condition. It is stated rather than @@ -1006,7 +1294,7 @@ export enum ReadinessClearedBy { * Describes the enum saas.accounts.v1.ReadinessClearedBy. */ export const ReadinessClearedBySchema: GenEnum = /*@__PURE__*/ - enumDesc(file_saas_accounts_v1_installations, 1); + enumDesc(file_saas_accounts_v1_installations, 3); /** * ReadinessAction is what clears the condition, named as the act rather than as @@ -1059,7 +1347,7 @@ export enum ReadinessAction { * Describes the enum saas.accounts.v1.ReadinessAction. */ export const ReadinessActionSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_saas_accounts_v1_installations, 2); + enumDesc(file_saas_accounts_v1_installations, 4); /** * InstallationService composes and administers installations. The headless mint @@ -1146,15 +1434,11 @@ export const InstallationService: GenService<{ * ListInstallations enumerates one organization's installations with their live * health. It is internal-tier on purpose: it takes the organization as a request * field, so only a caller holding the cluster-internal credential may ask, and - * the tenant it asks about is never one a browser supplied. The auth-gateway is - * its caller, for two reads. Its viewer projection asks about the tenant it - * projected from a verified identity; it sits beside - * PermissionService.ListAccessibleScopes for that reason, the two read together - * to answer what a viewer's organization installed and what that viewer was - * granted. Its declared module route's bearer-less arm asks about the tenant an - * edge-inspected Work Context names, a value this issuer signed, to admit a - * headless operation context only when the target is installed there; whether - * the context is one is WorkContextService/AdmitWorkloadOperation's answer. + * the tenant it asks about is the one the auth-gateway projected from a verified + * identity — never one a browser supplied. It sits beside + * PermissionService.ListAccessibleScopes for that reason: the two are read + * together to answer what a viewer's organization installed and what that viewer + * was granted. * * @generated from rpc saas.accounts.v1.InstallationService.ListInstallations */ @@ -1195,8 +1479,7 @@ export const InstallationService: GenService<{ * It is the org-admin companion to the internal ListInstallations, and it is a * separate method rather than a relaxed policy on that one: ListInstallations * is read by the gateway for an organisation it projected from a verified - * identity or that an edge-verified Work Context names, and a bare tenant - * credential must never reach it. This one binds + * identity, and a bare tenant credential must never reach it. This one binds * the organisation to the caller's own admin membership, exactly as * ListAvailableSolutions does, so no organisation's administrator can read * another's. The one exception is inherited, not added here: a platform diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts index 07297b21f..d55800756 100644 --- a/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts @@ -12,8 +12,8 @@ import type { CollectionReadGrant } from "./authorization_pb"; import { file_saas_accounts_v1_authorization } from "./authorization_pb"; import type { Organization, OrgMembership, User } from "./common_pb"; import { file_saas_accounts_v1_common } from "./common_pb"; -import type { Installation } from "./installation_listing_pb"; -import { file_saas_accounts_v1_installation_listing } from "./installation_listing_pb"; +import type { Installation } from "./installations_pb"; +import { file_saas_accounts_v1_installations } from "./installations_pb"; import type { SolutionRegistration } from "./solution_registry_pb"; import { file_saas_accounts_v1_solution_registry } from "./solution_registry_pb"; import type { GetEventOperationsRequestSchema, GetEventOperationsResponseSchema, ListEventSubscriptionsRequestSchema, ListEventSubscriptionsResponseSchema } from "../../events/v1/operations_pb"; @@ -27,7 +27,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/platform_admin.proto. */ export const file_saas_accounts_v1_platform_admin: GenFile = /*@__PURE__*/ - fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEiVAoMQ2F0YWxvZ3VlR2FwEjQKBnJlYXNvbhgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwUmVhc29uEg4KBmRldGFpbBgCIAEoCSJEChBDYXRhbG9ndWVSZWxlYXNlEhEKCXB1Ymxpc2hlchgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkipQIKEENhdGFsb2d1ZURlc2lyZWQSGwoRZGVjbGFyZWRfcmV2aXNpb24YASABKANIABI/ChVkZWNsYXJlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEj4KEGRlY2xhcmVkX3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIARI+ChRkZWNsYXJlZF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGQoXZGVjbGFyZWRfcmV2aXNpb25fdmFsdWVCGAoWZGVjbGFyZWRfcmVsZWFzZV92YWx1ZSKCAQoWQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbhIbChNhdXRob3JpemVkX3JldmlzaW9uGAEgASgDEhgKEGludmVudG9yeV9kaWdlc3QYAiABKAkSFgoObWVtYmVyX2JpbmRpbmcYAyABKAkSGQoRYnVpbGRfaW5jYXJuYXRpb24YBCABKAQiKAoWQ2F0YWxvZ3VlTm90QXV0aG9yaXplZBIOCgZkZXRhaWwYASABKAki8AEKE0NhdGFsb2d1ZUF1dGhvcml6ZWQSQQoNYXV0aG9yaXphdGlvbhgBIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbkgAEkIKDm5vdF9hdXRob3JpemVkGAIgASgLMiguc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVOb3RBdXRob3JpemVkSAASOwoRYXV0aG9yaXphdGlvbl9nYXAYAyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAQhUKE2F1dGhvcml6YXRpb25fdmFsdWUiiAEKEENhdGFsb2d1ZUFwcGxpZWQSGgoQYXBwbGllZF9yZXZpc2lvbhgBIAEoA0gAEj4KFGFwcGxpZWRfcmV2aXNpb25fZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAEIYChZhcHBsaWVkX3JldmlzaW9uX3ZhbHVlIt8BChpDYXRhbG9ndWVPYnNlcnZlZEV4ZWN1dGlvbhJoChdjb250YWluZXJfaW1hZ2VfZGlnZXN0cxgBIAMoCzJHLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb24uQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSGQoRYnVpbGRfaW5jYXJuYXRpb24YAiABKAQaPAoaQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASLUBAoRQ2F0YWxvZ3VlT2JzZXJ2ZWQSGwoRb2JzZXJ2ZWRfcmV2aXNpb24YASABKANIABI/ChVvYnNlcnZlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEjEKC29ic2VydmVkX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgBEkMKGW9ic2VydmF0aW9uX2ZyZXNobmVzc19nYXAYBCABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkoKEm9ic2VydmVkX2V4ZWN1dGlvbhgFIAEoCzIsLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb25IAhJAChZvYnNlcnZlZF9leGVjdXRpb25fZ2FwGAYgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAhI9Cgd2ZXJkaWN0GAcgASgOMiouc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVPYnNlcnZlZFZlcmRpY3RIAxI1Cgt2ZXJkaWN0X2dhcBgIIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSANCGQoXb2JzZXJ2ZWRfcmV2aXNpb25fdmFsdWVCHQobb2JzZXJ2YXRpb25fZnJlc2huZXNzX3ZhbHVlQhoKGG9ic2VydmVkX2V4ZWN1dGlvbl92YWx1ZUIPCg12ZXJkaWN0X3ZhbHVlIuABChRDYXRhbG9ndWVXaXRoZHJhd2luZxI+ChR3aXRoZHJhd2FsX3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASSQofY3JlZGVudGlhbF9yZXZvY2F0aW9uX3N0YXRlX2dhcBgCIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGAoWd2l0aGRyYXdhbF9zdGF0ZV92YWx1ZUIjCiFjcmVkZW50aWFsX3Jldm9jYXRpb25fc3RhdGVfdmFsdWUibAoQQ2F0YWxvZ3VlUmV0aXJlZBI+ChRyZXRpcmVtZW50X3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSABCGAoWcmV0aXJlbWVudF9zdGF0ZV92YWx1ZSK4AwoVQ2F0YWxvZ3VlSW5zdGFsbGF0aW9uEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEhAKCG9yZ19uYW1lGAIgASgJEjsKDWFnZW50X3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIABI7ChFhZ2VudF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASNgoMcmV2aXNpb25fZ2FwGAUgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIARI8Cg1ncmFudGVkX3RlYW1zGAYgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50Ej4KD2luaGVyaXRlZF90ZWFtcxgHIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ29sbGVjdGlvblJlYWRHcmFudEIVChNhZ2VudF9yZWxlYXNlX3ZhbHVlQhAKDnJldmlzaW9uX3ZhbHVlIuMFCg5DYXRhbG9ndWVFbnRyeRIyCgRraW5kGAEgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVFbnRyeUtpbmQSDAoEbmFtZRgCIAEoCRITCglwdWJsaXNoZXIYAyABKAlIABI3Cg1wdWJsaXNoZXJfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABIzCgdkZXNpcmVkGAUgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVEZXNpcmVkEjkKCmF1dGhvcml6ZWQYBiABKAsyJS5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUF1dGhvcml6ZWQSMwoHYXBwbGllZBgHIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXBwbGllZBI1CghvYnNlcnZlZBgIIAEoCzIjLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWQSOwoLd2l0aGRyYXdpbmcYCSABKAsyJi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVdpdGhkcmF3aW5nEjMKB3JldGlyZWQYCiABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJldGlyZWQSOAoOYnVpbGRfc2l6ZV9nYXAYCyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkEKDHJlZ2lzdHJhdGlvbhgMIAEoCzImLnNhYXMuYWNjb3VudHMudjEuU29sdXRpb25SZWdpc3RyYXRpb25IAogBARI+Cg1pbnN0YWxsYXRpb25zGA0gAygLMicuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVJbnN0YWxsYXRpb25CEQoPcHVibGlzaGVyX3ZhbHVlQhIKEGJ1aWxkX3NpemVfdmFsdWVCDwoNX3JlZ2lzdHJhdGlvbiI6ChxMaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0EhoKEmluY2x1ZGVfdG9tYnN0b25lZBgBIAEoCCKRAgodTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVzcG9uc2USMQoHZW50cmllcxgBIAMoCzIgLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRW50cnkSGQoRcmVnaXN0cnlfcmV2aXNpb24YAiABKAMSZgoUYXBwcm92ZWRfaW52ZW50b3JpZXMYAyADKAsySC5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlLkFwcHJvdmVkSW52ZW50b3JpZXNFbnRyeRo6ChhBcHByb3ZlZEludmVudG9yaWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgMOgI4ASJ4Cg5Nb2R1bGVFbnZlbG9wZRIQCghyZXZpc2lvbhgBIAEoAxIQCghkb2N1bWVudBgCIAEoCRISCgp3cml0dGVuX2J5GAMgASgJEi4KCndyaXR0ZW5fYXQYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIhoKGEdldE1vZHVsZUVudmVsb3BlUmVxdWVzdCKeAQoZR2V0TW9kdWxlRW52ZWxvcGVSZXNwb25zZRI3CghlbnZlbG9wZRgBIAEoCzIgLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlRW52ZWxvcGVIAIgBARI7CglyZXF1ZXN0ZWQYAiADKAsyKC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUF1dGhvcml0eVJlcXVlc3RCCwoJX2VudmVsb3BlIoMBChZNb2R1bGVBdXRob3JpdHlSZXF1ZXN0EhEKCXByaW5jaXBhbBgBIAEoCRIPCgdiaW5kaW5nGAIgASgJEhIKCmdlbmVyYXRpb24YAyABKAMSDQoFYWxpYXMYBCABKAkSDwoHYWxsb3dlZBgFIAEoCBIRCglhdXRob3JpdHkYBiABKAkiXQoYUHV0TW9kdWxlRW52ZWxvcGVSZXF1ZXN0EiIKEWV4cGVjdGVkX3JldmlzaW9uGAEgASgDQge6SAQiAigAEh0KCGRvY3VtZW50GAIgASgJQgu6SAhyBhABKICAQCJPChlQdXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlEjIKCGVudmVsb3BlGAEgASgLMiAuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFbnZlbG9wZSp+ChJDYXRhbG9ndWVFbnRyeUtpbmQSJAogQ0FUQUxPR1VFX0VOVFJZX0tJTkRfVU5TUEVDSUZJRUQQABIfChtDQVRBTE9HVUVfRU5UUllfS0lORF9NT0RVTEUQARIhCh1DQVRBTE9HVUVfRU5UUllfS0lORF9TT0xVVElPThACKq8BChJDYXRhbG9ndWVHYXBSZWFzb24SJAogQ0FUQUxPR1VFX0dBUF9SRUFTT05fVU5TUEVDSUZJRUQQABIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVDT1JERUQQARIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVQT1JURUQQAhIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfT0JTRVJWRUQQAyreAQoYQ2F0YWxvZ3VlT2JzZXJ2ZWRWZXJkaWN0EioKJkNBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1VOU1BFQ0lGSUVEEAASMQotQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19BVVRIT1JJWkVEEAESLgoqQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19ESUZGRVJTEAISMwovQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19VTkFVVEhPUklaRUQQAzKiIwoUUGxhdGZvcm1BZG1pblNlcnZpY2USjgEKC1NlYXJjaFVzZXJzEiQuc2Fhcy5hY2NvdW50cy52MS5TZWFyY2hVc2Vyc1JlcXVlc3QaJS5zYWFzLmFjY291bnRzLnYxLlNlYXJjaFVzZXJzUmVzcG9uc2UiMsLzGBQIAhABMAE6AhABQAFIBVADWANgA4LT5JMCFBISL3YxL3BsYXRmb3JtL3VzZXJzEqsBCgtTdXNwZW5kVXNlchIkLnNhYXMuYWNjb3VudHMudjEuU3VzcGVuZFVzZXJSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5Il7C8xgrCAIQATABOhcKE3NhYXMudXNlci5zdXNwZW5kZWQQAkABSAVQA1gDYAV4AoLT5JMCKToBKiIkL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTpzdXNwZW5kErMBCg1VbnN1c3BlbmRVc2VyEiYuc2Fhcy5hY2NvdW50cy52MS5VbnN1c3BlbmRVc2VyUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJiwvMYLQgCEAEwAToZChVzYWFzLnVzZXIudW5zdXNwZW5kZWQQAkABSAVQA1gDYAV4AoLT5JMCKzoBKiImL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTp1bnN1c3BlbmQS1gEKD0ltcGVyc29uYXRlVXNlchIoLnNhYXMuYWNjb3VudHMudjEuSW1wZXJzb25hdGVVc2VyUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuSW1wZXJzb25hdGVVc2VyUmVzcG9uc2UibsLzGDcIAhABMAQ6Iwofc2Fhcy5wbGF0Zm9ybS51c2VyX2ltcGVyc29uYXRlZBACQAFIBVADWARgA3gCgtPkkwItOgEqIigvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OmltcGVyc29uYXRlEtgBChFTdG9wSW1wZXJzb25hdGlvbhIqLnNhYXMuYWNjb3VudHMudjEuU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5TdG9wSW1wZXJzb25hdGlvblJlc3BvbnNlImrC8xg8CAIQATABOioKJnNhYXMucGxhdGZvcm0udXNlcl9pbXBlcnNvbmF0aW9uX2VuZGVkEAJAAUgFUANYA2ABgtPkkwIkOgEqIh8vdjEvcGxhdGZvcm0vaW1wZXJzb25hdGlvbjpzdG9wEqYBChJMaXN0QWN0aXZlU2Vzc2lvbnMSKy5zYWFzLmFjY291bnRzLnYxLkxpc3RBY3RpdmVTZXNzaW9uc1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkxpc3RBY3RpdmVTZXNzaW9uc1Jlc3BvbnNlIjXC8xgUCAIQATABOgIQAUABSAVQA1gDYAOC0+STAhcSFS92MS9wbGF0Zm9ybS9zZXNzaW9ucxKpAQoNUmV2b2tlU2Vzc2lvbhImLnNhYXMuYWNjb3VudHMudjEuUmV2b2tlU2Vzc2lvblJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiWMLzGCoIAhABMAE6GAoUc2Fhcy5zZXNzaW9uLnJldm9rZWQQAkABSAVQA1gDYAOC0+STAiQqIi92MS9wbGF0Zm9ybS9zZXNzaW9ucy97c2Vzc2lvbl9pZH0SzwEKEkdldE9yZ0VudGl0bGVtZW50cxIrLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnRW50aXRsZW1lbnRzUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2UiXsLzGCIIAhADKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIyEjAvdjEvcGxhdGZvcm0vb3JnYW5pemF0aW9ucy97b3JnX2lkfS9lbnRpdGxlbWVudHMS5AEKE092ZXJyaWRlRW50aXRsZW1lbnQSLC5zYWFzLmFjY291bnRzLnYxLk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0Gi0uc2Fhcy5hY2NvdW50cy52MS5PdmVycmlkZUVudGl0bGVtZW50UmVzcG9uc2UicMLzGDEIAhABMAE6HQoZc2Fhcy5lbnRpdGxlbWVudC5vdmVycmlkZRACQAFIBVADWANgAngCgtPkkwI1OgEqIjAvdjEvcGxhdGZvcm0vb3JnYW5pemF0aW9ucy97b3JnX2lkfS9lbnRpdGxlbWVudHMSrQEKEUdyYW50UGxhdGZvcm1Sb2xlEiouc2Fhcy5hY2NvdW50cy52MS5HcmFudFBsYXRmb3JtUm9sZVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiVMLzGDIIAhABMAQ6Hgoac2Fhcy5wbGF0Zm9ybS5yb2xlX2dyYW50ZWQQAkABSAVQA1gDYAV4AoLT5JMCGDoBKiITL3YxL3BsYXRmb3JtL2FkbWlucxK2AQoSUmV2b2tlUGxhdGZvcm1Sb2xlEisuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VQbGF0Zm9ybVJvbGVSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IlvC8xgyCAIQATAEOh4KGnNhYXMucGxhdGZvcm0ucm9sZV9yZXZva2VkEAJAAUgFUANYA2AFeAKC0+STAh8qHS92MS9wbGF0Zm9ybS9hZG1pbnMve3VzZXJfaWR9EqQBChJMaXN0UGxhdGZvcm1BZG1pbnMSKy5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUFkbWluc1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUFkbWluc1Jlc3BvbnNlIjPC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhUSEy92MS9wbGF0Zm9ybS9hZG1pbnMSjwEKFExpc3RBbGxPcmdhbml6YXRpb25zEi0uc2Fhcy5hY2NvdW50cy52MS5MaXN0QWxsT3JnYW5pemF0aW9uc1JlcXVlc3QaLi5zYWFzLmFjY291bnRzLnYxLkxpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2UiGMLzGBQIAhABMAE6AhABQAFIBVADWANgAxKSAQoVR2V0T3JnYW5pemF0aW9uUm9zdGVyEi4uc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZSIYwvMYFAgCEAEwAToCEAFAAUgFUANYA2ADEqUBChBMaXN0RmVhdHVyZUZsYWdzEikuc2Fhcy5hY2NvdW50cy52MS5MaXN0RmVhdHVyZUZsYWdzUmVxdWVzdBoqLnNhYXMuYWNjb3VudHMudjEuTGlzdEZlYXR1cmVGbGFnc1Jlc3BvbnNlIjrC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhwSGi92MS9wbGF0Zm9ybS9mZWF0dXJlLWZsYWdzEtIBChFVcHNlcnRGZWF0dXJlRmxhZxIqLnNhYXMuYWNjb3VudHMudjEuVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5VcHNlcnRGZWF0dXJlRmxhZ1Jlc3BvbnNlImSIAgHC8xgxCAIQATABOh0KGXNhYXMuZmVhdHVyZV9mbGFnLnVwZGF0ZWQQAkABSAVQA1gDYAV4AoLT5JMCJjoBKhohL3YxL3BsYXRmb3JtL2ZlYXR1cmUtZmxhZ3Mve25hbWV9Ep8BChBHZXRKb2JPcGVyYXRpb25zEiUuc2Fhcy5qb2JzLnYxLkdldEpvYk9wZXJhdGlvbnNSZXF1ZXN0GiYuc2Fhcy5qb2JzLnYxLkdldEpvYk9wZXJhdGlvbnNSZXNwb25zZSI8wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIeEhwvdjEvcGxhdGZvcm0vam9icy9vcGVyYXRpb25zEnwKCExpc3RKb2JzEh0uc2Fhcy5qb2JzLnYxLkxpc3RKb2JzUmVxdWVzdBoeLnNhYXMuam9icy52MS5MaXN0Sm9ic1Jlc3BvbnNlIjHC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhMSES92MS9wbGF0Zm9ybS9qb2JzEn8KBkdldEpvYhIbLnNhYXMuam9icy52MS5HZXRKb2JSZXF1ZXN0Ghwuc2Fhcy5qb2JzLnYxLkdldEpvYlJlc3BvbnNlIjrC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhwSGi92MS9wbGF0Zm9ybS9qb2JzL3tqb2JfaWR9EqwBCglSZXBsYXlKb2ISHi5zYWFzLmpvYnMudjEuUmVwbGF5Sm9iUmVxdWVzdBofLnNhYXMuam9icy52MS5SZXBsYXlKb2JSZXNwb25zZSJewvMYJwgCEAEwAzoVChFzYWFzLmpvYi5yZXBsYXllZBACQANIBVADWANgBYLT5JMCLToBKiIoL3YxL3BsYXRmb3JtL2pvYnMve3NvdXJjZV9qb2JfaWR9OnJlcGxheRKrAQoSR2V0RXZlbnRPcGVyYXRpb25zEikuc2Fhcy5ldmVudHMudjEuR2V0RXZlbnRPcGVyYXRpb25zUmVxdWVzdBoqLnNhYXMuZXZlbnRzLnYxLkdldEV2ZW50T3BlcmF0aW9uc1Jlc3BvbnNlIj7C8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAiASHi92MS9wbGF0Zm9ybS9ldmVudHMvb3BlcmF0aW9ucxK6AQoWTGlzdEV2ZW50U3Vic2NyaXB0aW9ucxItLnNhYXMuZXZlbnRzLnYxLkxpc3RFdmVudFN1YnNjcmlwdGlvbnNSZXF1ZXN0Gi4uc2Fhcy5ldmVudHMudjEuTGlzdEV2ZW50U3Vic2NyaXB0aW9uc1Jlc3BvbnNlIkHC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAiMSIS92MS9wbGF0Zm9ybS9ldmVudHMvc3Vic2NyaXB0aW9ucxKwAQoVTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlEi4uc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXNwb25zZSI2wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIYEhYvdjEvcGxhdGZvcm0vY2F0YWxvZ3VlEsABChFHZXRNb2R1bGVFbnZlbG9wZRIqLnNhYXMuYWNjb3VudHMudjEuR2V0TW9kdWxlRW52ZWxvcGVSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5HZXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlIlLC8xgqCAIQASIUbW9kdWxlX2VudmVsb3BlOnJlYWQwAToCEAFAAUgFUANYA2AFgtPkkwIeEhwvdjEvcGxhdGZvcm0vbW9kdWxlLWVudmVsb3BlEu0BChFQdXRNb2R1bGVFbnZlbG9wZRIqLnNhYXMuYWNjb3VudHMudjEuUHV0TW9kdWxlRW52ZWxvcGVSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5QdXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlIn/C8xhUCAIQASIVbW9kdWxlX2VudmVsb3BlOndyaXRlMAM6KQolc2Fhcy5wbGF0Zm9ybS5tb2R1bGVfZW52ZWxvcGVfd3JpdHRlbhACQAFIBVADWANgBXgCgtPkkwIhOgEqGhwvdjEvcGxhdGZvcm0vbW9kdWxlLWVudmVsb3BlQroBChRjb20uc2Fhcy5hY2NvdW50cy52MUISUGxhdGZvcm1BZG1pblByb3RvUAFaLGFjY291bnRzL3BrZy9nZW4vc2Fhcy9hY2NvdW50cy92MTthY2NvdW50c3YxogIDU0FYqgIQU2Fhcy5BY2NvdW50cy5WMcoCEFNhYXNcQWNjb3VudHNcVjHiAhxTYWFzXEFjY291bnRzXFYxXEdQQk1ldGFkYXRh6gISU2Fhczo6QWNjb3VudHM6OlYxYgZwcm90bzM", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_accounts_v1_common, file_saas_accounts_v1_installation_listing, file_saas_accounts_v1_solution_registry, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); + fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEiVAoMQ2F0YWxvZ3VlR2FwEjQKBnJlYXNvbhgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwUmVhc29uEg4KBmRldGFpbBgCIAEoCSJEChBDYXRhbG9ndWVSZWxlYXNlEhEKCXB1Ymxpc2hlchgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkipQIKEENhdGFsb2d1ZURlc2lyZWQSGwoRZGVjbGFyZWRfcmV2aXNpb24YASABKANIABI/ChVkZWNsYXJlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEj4KEGRlY2xhcmVkX3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIARI+ChRkZWNsYXJlZF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGQoXZGVjbGFyZWRfcmV2aXNpb25fdmFsdWVCGAoWZGVjbGFyZWRfcmVsZWFzZV92YWx1ZSKCAQoWQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbhIbChNhdXRob3JpemVkX3JldmlzaW9uGAEgASgDEhgKEGludmVudG9yeV9kaWdlc3QYAiABKAkSFgoObWVtYmVyX2JpbmRpbmcYAyABKAkSGQoRYnVpbGRfaW5jYXJuYXRpb24YBCABKAQiKAoWQ2F0YWxvZ3VlTm90QXV0aG9yaXplZBIOCgZkZXRhaWwYASABKAki8AEKE0NhdGFsb2d1ZUF1dGhvcml6ZWQSQQoNYXV0aG9yaXphdGlvbhgBIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbkgAEkIKDm5vdF9hdXRob3JpemVkGAIgASgLMiguc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVOb3RBdXRob3JpemVkSAASOwoRYXV0aG9yaXphdGlvbl9nYXAYAyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAQhUKE2F1dGhvcml6YXRpb25fdmFsdWUiiAEKEENhdGFsb2d1ZUFwcGxpZWQSGgoQYXBwbGllZF9yZXZpc2lvbhgBIAEoA0gAEj4KFGFwcGxpZWRfcmV2aXNpb25fZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAEIYChZhcHBsaWVkX3JldmlzaW9uX3ZhbHVlIt8BChpDYXRhbG9ndWVPYnNlcnZlZEV4ZWN1dGlvbhJoChdjb250YWluZXJfaW1hZ2VfZGlnZXN0cxgBIAMoCzJHLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb24uQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSGQoRYnVpbGRfaW5jYXJuYXRpb24YAiABKAQaPAoaQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASLUBAoRQ2F0YWxvZ3VlT2JzZXJ2ZWQSGwoRb2JzZXJ2ZWRfcmV2aXNpb24YASABKANIABI/ChVvYnNlcnZlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEjEKC29ic2VydmVkX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgBEkMKGW9ic2VydmF0aW9uX2ZyZXNobmVzc19nYXAYBCABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkoKEm9ic2VydmVkX2V4ZWN1dGlvbhgFIAEoCzIsLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb25IAhJAChZvYnNlcnZlZF9leGVjdXRpb25fZ2FwGAYgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAhI9Cgd2ZXJkaWN0GAcgASgOMiouc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVPYnNlcnZlZFZlcmRpY3RIAxI1Cgt2ZXJkaWN0X2dhcBgIIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSANCGQoXb2JzZXJ2ZWRfcmV2aXNpb25fdmFsdWVCHQobb2JzZXJ2YXRpb25fZnJlc2huZXNzX3ZhbHVlQhoKGG9ic2VydmVkX2V4ZWN1dGlvbl92YWx1ZUIPCg12ZXJkaWN0X3ZhbHVlIuABChRDYXRhbG9ndWVXaXRoZHJhd2luZxI+ChR3aXRoZHJhd2FsX3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASSQofY3JlZGVudGlhbF9yZXZvY2F0aW9uX3N0YXRlX2dhcBgCIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGAoWd2l0aGRyYXdhbF9zdGF0ZV92YWx1ZUIjCiFjcmVkZW50aWFsX3Jldm9jYXRpb25fc3RhdGVfdmFsdWUibAoQQ2F0YWxvZ3VlUmV0aXJlZBI+ChRyZXRpcmVtZW50X3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSABCGAoWcmV0aXJlbWVudF9zdGF0ZV92YWx1ZSK4AwoVQ2F0YWxvZ3VlSW5zdGFsbGF0aW9uEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEhAKCG9yZ19uYW1lGAIgASgJEjsKDWFnZW50X3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIABI7ChFhZ2VudF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASNgoMcmV2aXNpb25fZ2FwGAUgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIARI8Cg1ncmFudGVkX3RlYW1zGAYgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50Ej4KD2luaGVyaXRlZF90ZWFtcxgHIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ29sbGVjdGlvblJlYWRHcmFudEIVChNhZ2VudF9yZWxlYXNlX3ZhbHVlQhAKDnJldmlzaW9uX3ZhbHVlIuMFCg5DYXRhbG9ndWVFbnRyeRIyCgRraW5kGAEgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVFbnRyeUtpbmQSDAoEbmFtZRgCIAEoCRITCglwdWJsaXNoZXIYAyABKAlIABI3Cg1wdWJsaXNoZXJfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABIzCgdkZXNpcmVkGAUgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVEZXNpcmVkEjkKCmF1dGhvcml6ZWQYBiABKAsyJS5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUF1dGhvcml6ZWQSMwoHYXBwbGllZBgHIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXBwbGllZBI1CghvYnNlcnZlZBgIIAEoCzIjLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWQSOwoLd2l0aGRyYXdpbmcYCSABKAsyJi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVdpdGhkcmF3aW5nEjMKB3JldGlyZWQYCiABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJldGlyZWQSOAoOYnVpbGRfc2l6ZV9nYXAYCyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkEKDHJlZ2lzdHJhdGlvbhgMIAEoCzImLnNhYXMuYWNjb3VudHMudjEuU29sdXRpb25SZWdpc3RyYXRpb25IAogBARI+Cg1pbnN0YWxsYXRpb25zGA0gAygLMicuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVJbnN0YWxsYXRpb25CEQoPcHVibGlzaGVyX3ZhbHVlQhIKEGJ1aWxkX3NpemVfdmFsdWVCDwoNX3JlZ2lzdHJhdGlvbiI6ChxMaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0EhoKEmluY2x1ZGVfdG9tYnN0b25lZBgBIAEoCCKRAgodTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVzcG9uc2USMQoHZW50cmllcxgBIAMoCzIgLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRW50cnkSGQoRcmVnaXN0cnlfcmV2aXNpb24YAiABKAMSZgoUYXBwcm92ZWRfaW52ZW50b3JpZXMYAyADKAsySC5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlLkFwcHJvdmVkSW52ZW50b3JpZXNFbnRyeRo6ChhBcHByb3ZlZEludmVudG9yaWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgMOgI4ASJ4Cg5Nb2R1bGVFbnZlbG9wZRIQCghyZXZpc2lvbhgBIAEoAxIQCghkb2N1bWVudBgCIAEoCRISCgp3cml0dGVuX2J5GAMgASgJEi4KCndyaXR0ZW5fYXQYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIhoKGEdldE1vZHVsZUVudmVsb3BlUmVxdWVzdCKeAQoZR2V0TW9kdWxlRW52ZWxvcGVSZXNwb25zZRI3CghlbnZlbG9wZRgBIAEoCzIgLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlRW52ZWxvcGVIAIgBARI7CglyZXF1ZXN0ZWQYAiADKAsyKC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUF1dGhvcml0eVJlcXVlc3RCCwoJX2VudmVsb3BlIoMBChZNb2R1bGVBdXRob3JpdHlSZXF1ZXN0EhEKCXByaW5jaXBhbBgBIAEoCRIPCgdiaW5kaW5nGAIgASgJEhIKCmdlbmVyYXRpb24YAyABKAMSDQoFYWxpYXMYBCABKAkSDwoHYWxsb3dlZBgFIAEoCBIRCglhdXRob3JpdHkYBiABKAkiXQoYUHV0TW9kdWxlRW52ZWxvcGVSZXF1ZXN0EiIKEWV4cGVjdGVkX3JldmlzaW9uGAEgASgDQge6SAQiAigAEh0KCGRvY3VtZW50GAIgASgJQgu6SAhyBhABKICAQCJPChlQdXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlEjIKCGVudmVsb3BlGAEgASgLMiAuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFbnZlbG9wZSp+ChJDYXRhbG9ndWVFbnRyeUtpbmQSJAogQ0FUQUxPR1VFX0VOVFJZX0tJTkRfVU5TUEVDSUZJRUQQABIfChtDQVRBTE9HVUVfRU5UUllfS0lORF9NT0RVTEUQARIhCh1DQVRBTE9HVUVfRU5UUllfS0lORF9TT0xVVElPThACKq8BChJDYXRhbG9ndWVHYXBSZWFzb24SJAogQ0FUQUxPR1VFX0dBUF9SRUFTT05fVU5TUEVDSUZJRUQQABIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVDT1JERUQQARIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVQT1JURUQQAhIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfT0JTRVJWRUQQAyreAQoYQ2F0YWxvZ3VlT2JzZXJ2ZWRWZXJkaWN0EioKJkNBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1VOU1BFQ0lGSUVEEAASMQotQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19BVVRIT1JJWkVEEAESLgoqQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19ESUZGRVJTEAISMwovQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19VTkFVVEhPUklaRUQQAzKiIwoUUGxhdGZvcm1BZG1pblNlcnZpY2USjgEKC1NlYXJjaFVzZXJzEiQuc2Fhcy5hY2NvdW50cy52MS5TZWFyY2hVc2Vyc1JlcXVlc3QaJS5zYWFzLmFjY291bnRzLnYxLlNlYXJjaFVzZXJzUmVzcG9uc2UiMsLzGBQIAhABMAE6AhABQAFIBVADWANgA4LT5JMCFBISL3YxL3BsYXRmb3JtL3VzZXJzEqsBCgtTdXNwZW5kVXNlchIkLnNhYXMuYWNjb3VudHMudjEuU3VzcGVuZFVzZXJSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5Il7C8xgrCAIQATABOhcKE3NhYXMudXNlci5zdXNwZW5kZWQQAkABSAVQA1gDYAV4AoLT5JMCKToBKiIkL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTpzdXNwZW5kErMBCg1VbnN1c3BlbmRVc2VyEiYuc2Fhcy5hY2NvdW50cy52MS5VbnN1c3BlbmRVc2VyUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJiwvMYLQgCEAEwAToZChVzYWFzLnVzZXIudW5zdXNwZW5kZWQQAkABSAVQA1gDYAV4AoLT5JMCKzoBKiImL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTp1bnN1c3BlbmQS1gEKD0ltcGVyc29uYXRlVXNlchIoLnNhYXMuYWNjb3VudHMudjEuSW1wZXJzb25hdGVVc2VyUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuSW1wZXJzb25hdGVVc2VyUmVzcG9uc2UibsLzGDcIAhABMAQ6Iwofc2Fhcy5wbGF0Zm9ybS51c2VyX2ltcGVyc29uYXRlZBACQAFIBVADWARgA3gCgtPkkwItOgEqIigvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OmltcGVyc29uYXRlEtgBChFTdG9wSW1wZXJzb25hdGlvbhIqLnNhYXMuYWNjb3VudHMudjEuU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5TdG9wSW1wZXJzb25hdGlvblJlc3BvbnNlImrC8xg8CAIQATABOioKJnNhYXMucGxhdGZvcm0udXNlcl9pbXBlcnNvbmF0aW9uX2VuZGVkEAJAAUgFUANYA2ABgtPkkwIkOgEqIh8vdjEvcGxhdGZvcm0vaW1wZXJzb25hdGlvbjpzdG9wEqYBChJMaXN0QWN0aXZlU2Vzc2lvbnMSKy5zYWFzLmFjY291bnRzLnYxLkxpc3RBY3RpdmVTZXNzaW9uc1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkxpc3RBY3RpdmVTZXNzaW9uc1Jlc3BvbnNlIjXC8xgUCAIQATABOgIQAUABSAVQA1gDYAOC0+STAhcSFS92MS9wbGF0Zm9ybS9zZXNzaW9ucxKpAQoNUmV2b2tlU2Vzc2lvbhImLnNhYXMuYWNjb3VudHMudjEuUmV2b2tlU2Vzc2lvblJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiWMLzGCoIAhABMAE6GAoUc2Fhcy5zZXNzaW9uLnJldm9rZWQQAkABSAVQA1gDYAOC0+STAiQqIi92MS9wbGF0Zm9ybS9zZXNzaW9ucy97c2Vzc2lvbl9pZH0SzwEKEkdldE9yZ0VudGl0bGVtZW50cxIrLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnRW50aXRsZW1lbnRzUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2UiXsLzGCIIAhADKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIyEjAvdjEvcGxhdGZvcm0vb3JnYW5pemF0aW9ucy97b3JnX2lkfS9lbnRpdGxlbWVudHMS5AEKE092ZXJyaWRlRW50aXRsZW1lbnQSLC5zYWFzLmFjY291bnRzLnYxLk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0Gi0uc2Fhcy5hY2NvdW50cy52MS5PdmVycmlkZUVudGl0bGVtZW50UmVzcG9uc2UicMLzGDEIAhABMAE6HQoZc2Fhcy5lbnRpdGxlbWVudC5vdmVycmlkZRACQAFIBVADWANgAngCgtPkkwI1OgEqIjAvdjEvcGxhdGZvcm0vb3JnYW5pemF0aW9ucy97b3JnX2lkfS9lbnRpdGxlbWVudHMSrQEKEUdyYW50UGxhdGZvcm1Sb2xlEiouc2Fhcy5hY2NvdW50cy52MS5HcmFudFBsYXRmb3JtUm9sZVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiVMLzGDIIAhABMAQ6Hgoac2Fhcy5wbGF0Zm9ybS5yb2xlX2dyYW50ZWQQAkABSAVQA1gDYAV4AoLT5JMCGDoBKiITL3YxL3BsYXRmb3JtL2FkbWlucxK2AQoSUmV2b2tlUGxhdGZvcm1Sb2xlEisuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VQbGF0Zm9ybVJvbGVSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IlvC8xgyCAIQATAEOh4KGnNhYXMucGxhdGZvcm0ucm9sZV9yZXZva2VkEAJAAUgFUANYA2AFeAKC0+STAh8qHS92MS9wbGF0Zm9ybS9hZG1pbnMve3VzZXJfaWR9EqQBChJMaXN0UGxhdGZvcm1BZG1pbnMSKy5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUFkbWluc1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUFkbWluc1Jlc3BvbnNlIjPC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhUSEy92MS9wbGF0Zm9ybS9hZG1pbnMSjwEKFExpc3RBbGxPcmdhbml6YXRpb25zEi0uc2Fhcy5hY2NvdW50cy52MS5MaXN0QWxsT3JnYW5pemF0aW9uc1JlcXVlc3QaLi5zYWFzLmFjY291bnRzLnYxLkxpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2UiGMLzGBQIAhABMAE6AhABQAFIBVADWANgAxKSAQoVR2V0T3JnYW5pemF0aW9uUm9zdGVyEi4uc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZSIYwvMYFAgCEAEwAToCEAFAAUgFUANYA2ADEqUBChBMaXN0RmVhdHVyZUZsYWdzEikuc2Fhcy5hY2NvdW50cy52MS5MaXN0RmVhdHVyZUZsYWdzUmVxdWVzdBoqLnNhYXMuYWNjb3VudHMudjEuTGlzdEZlYXR1cmVGbGFnc1Jlc3BvbnNlIjrC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhwSGi92MS9wbGF0Zm9ybS9mZWF0dXJlLWZsYWdzEtIBChFVcHNlcnRGZWF0dXJlRmxhZxIqLnNhYXMuYWNjb3VudHMudjEuVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5VcHNlcnRGZWF0dXJlRmxhZ1Jlc3BvbnNlImSIAgHC8xgxCAIQATABOh0KGXNhYXMuZmVhdHVyZV9mbGFnLnVwZGF0ZWQQAkABSAVQA1gDYAV4AoLT5JMCJjoBKhohL3YxL3BsYXRmb3JtL2ZlYXR1cmUtZmxhZ3Mve25hbWV9Ep8BChBHZXRKb2JPcGVyYXRpb25zEiUuc2Fhcy5qb2JzLnYxLkdldEpvYk9wZXJhdGlvbnNSZXF1ZXN0GiYuc2Fhcy5qb2JzLnYxLkdldEpvYk9wZXJhdGlvbnNSZXNwb25zZSI8wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIeEhwvdjEvcGxhdGZvcm0vam9icy9vcGVyYXRpb25zEnwKCExpc3RKb2JzEh0uc2Fhcy5qb2JzLnYxLkxpc3RKb2JzUmVxdWVzdBoeLnNhYXMuam9icy52MS5MaXN0Sm9ic1Jlc3BvbnNlIjHC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhMSES92MS9wbGF0Zm9ybS9qb2JzEn8KBkdldEpvYhIbLnNhYXMuam9icy52MS5HZXRKb2JSZXF1ZXN0Ghwuc2Fhcy5qb2JzLnYxLkdldEpvYlJlc3BvbnNlIjrC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhwSGi92MS9wbGF0Zm9ybS9qb2JzL3tqb2JfaWR9EqwBCglSZXBsYXlKb2ISHi5zYWFzLmpvYnMudjEuUmVwbGF5Sm9iUmVxdWVzdBofLnNhYXMuam9icy52MS5SZXBsYXlKb2JSZXNwb25zZSJewvMYJwgCEAEwAzoVChFzYWFzLmpvYi5yZXBsYXllZBACQANIBVADWANgBYLT5JMCLToBKiIoL3YxL3BsYXRmb3JtL2pvYnMve3NvdXJjZV9qb2JfaWR9OnJlcGxheRKrAQoSR2V0RXZlbnRPcGVyYXRpb25zEikuc2Fhcy5ldmVudHMudjEuR2V0RXZlbnRPcGVyYXRpb25zUmVxdWVzdBoqLnNhYXMuZXZlbnRzLnYxLkdldEV2ZW50T3BlcmF0aW9uc1Jlc3BvbnNlIj7C8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAiASHi92MS9wbGF0Zm9ybS9ldmVudHMvb3BlcmF0aW9ucxK6AQoWTGlzdEV2ZW50U3Vic2NyaXB0aW9ucxItLnNhYXMuZXZlbnRzLnYxLkxpc3RFdmVudFN1YnNjcmlwdGlvbnNSZXF1ZXN0Gi4uc2Fhcy5ldmVudHMudjEuTGlzdEV2ZW50U3Vic2NyaXB0aW9uc1Jlc3BvbnNlIkHC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAiMSIS92MS9wbGF0Zm9ybS9ldmVudHMvc3Vic2NyaXB0aW9ucxKwAQoVTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlEi4uc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXNwb25zZSI2wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIYEhYvdjEvcGxhdGZvcm0vY2F0YWxvZ3VlEsABChFHZXRNb2R1bGVFbnZlbG9wZRIqLnNhYXMuYWNjb3VudHMudjEuR2V0TW9kdWxlRW52ZWxvcGVSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5HZXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlIlLC8xgqCAIQASIUbW9kdWxlX2VudmVsb3BlOnJlYWQwAToCEAFAAUgFUANYA2AFgtPkkwIeEhwvdjEvcGxhdGZvcm0vbW9kdWxlLWVudmVsb3BlEu0BChFQdXRNb2R1bGVFbnZlbG9wZRIqLnNhYXMuYWNjb3VudHMudjEuUHV0TW9kdWxlRW52ZWxvcGVSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5QdXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlIn/C8xhUCAIQASIVbW9kdWxlX2VudmVsb3BlOndyaXRlMAM6KQolc2Fhcy5wbGF0Zm9ybS5tb2R1bGVfZW52ZWxvcGVfd3JpdHRlbhACQAFIBVADWANgBXgCgtPkkwIhOgEqGhwvdjEvcGxhdGZvcm0vbW9kdWxlLWVudmVsb3BlQroBChRjb20uc2Fhcy5hY2NvdW50cy52MUISUGxhdGZvcm1BZG1pblByb3RvUAFaLGFjY291bnRzL3BrZy9nZW4vc2Fhcy9hY2NvdW50cy92MTthY2NvdW50c3YxogIDU0FYqgIQU2Fhcy5BY2NvdW50cy5WMcoCEFNhYXNcQWNjb3VudHNcVjHiAhxTYWFzXEFjY291bnRzXFYxXEdQQk1ldGFkYXRh6gISU2Fhczo6QWNjb3VudHM6OlYxYgZwcm90bzM", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_accounts_v1_common, file_saas_accounts_v1_installations, file_saas_accounts_v1_solution_registry, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); /** * @generated from message saas.accounts.v1.SearchUsersRequest diff --git a/module/services/frontend/code/ui-inventory.json b/module/services/frontend/code/ui-inventory.json index ff76670a6..c025c5387 100644 --- a/module/services/frontend/code/ui-inventory.json +++ b/module/services/frontend/code/ui-inventory.json @@ -5018,7 +5018,6 @@ "react", "sonner", "@/components/empty-state", - "@/gen/saas/accounts/v1/installation_listing_pb", "@/gen/saas/accounts/v1/installations_pb", "@/lib/auth", "@/shared/ui", @@ -5044,7 +5043,6 @@ "sonner", "@/components/empty-state", "@/components/org-selector", - "@/gen/saas/accounts/v1/installation_listing_pb", "@/gen/saas/accounts/v1/installations_pb", "@/lib/auth", "@/shared/lib/read-outcome", From 23517b2ed2d20c46e49903b4bb8e62654622a5d4 Mon Sep 17 00:00:00 2001 From: saurabhmaheshwari-obin <242053479+saurabhmaheshwari-obin@users.noreply.github.com> Date: Sat, 24 Oct 2026 17:13:02 -0400 Subject: [PATCH 16/17] fix(tools): the naming gate's size refusal names raising the cap for generated text A file over the cap was told to split or be allowlisted. For generated text that grew, the remedy this tree took is raising MAX_BYTES, so the message now names it beside the other two. Co-Authored-By: Claude Opus 5.5 --- module/tools/naming-gate.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/module/tools/naming-gate.mjs b/module/tools/naming-gate.mjs index 6a0f312b6..2b53fbe7f 100644 --- a/module/tools/naming-gate.mjs +++ b/module/tools/naming-gate.mjs @@ -313,7 +313,8 @@ export function namingErrors(moduleRoot = MODULE_ROOT, scanRoot = canonicalScanR // gate is declining to read, and a reader of a green run has to be able to see that. if (statSync(abs).size > MAX_BYTES) { errors.push( - `${rel}: too large to scan (over ${MAX_BYTES} bytes) — split it, or allowlist it in ` + + `${rel}: too large to scan (over ${MAX_BYTES} bytes): split it; for generated text that ` + + "grew, raise MAX_BYTES in tools/naming-gate.mjs; or allowlist it in " + "tools/naming-allowlist.json with a reason and a ticket", ); continue; From 3b16ae2d31a327502a8bd8dd8acc72c79efa76dc Mon Sep 17 00:00:00 2001 From: saurabhmaheshwari-obin <242053479+saurabhmaheshwari-obin@users.noreply.github.com> Date: Sat, 24 Oct 2026 17:42:36 -0400 Subject: [PATCH 17/17] docs(accounts): say which refusal a seal the owner no longer holds is WorkloadOperationRefusal's comments follow the read-back mapping: a seal on an installation the owner no longer holds, an uninstalled module and a module whose grant now names another tenant are NOT_CURRENT (core reports a seal that is no longer held as revoked), and SEAL_NOT_OWNERS is the owner no module declares. The gateway's copy follows byte for byte. Regenerated with Codefly source candidate 5234ce24 (accounts and gateway proto, go generate, contracts) and the saas-sdk library with released CLI 0.1.191. Co-Authored-By: Claude Opus 5.5 --- .../api/accounts/authority/contract.binpb | Bin 1205762 -> 1205854 bytes .../v1/workload_operation_admission.proto | 8 +++++--- .../api/accounts/connect/contract.binpb | Bin 1205762 -> 1205854 bytes .../v1/workload_operation_admission.proto | 8 +++++--- .../api/auth-gateway/grpc/contract.binpb | Bin 353207 -> 353299 bytes .../v1/workload_operation_admission.proto | 8 +++++--- module/contracts/api/catalog.codefly.json | 6 +++--- module/module.package.codefly.yaml | 6 +++--- .../v1/workload_operation_admission.pb.go | 8 +++++--- .../v1/workload_operation_admission.proto | 8 +++++--- .../v1/workload_operation_admission.pb.go | 8 +++++--- .../v1/workload_operation_admission.proto | 8 +++++--- .../generated/contract/catalog.codefly.json | 2 +- .../generated/contract/contract.binpb | Bin 1205762 -> 1205854 bytes .../saas-sdk/generated/library.codefly.yaml | 2 +- .../v1/workload_operation_admission_pb.ts | 8 +++++--- .../v1/workload_operation_admission_pb.ts | 8 +++++--- 17 files changed, 53 insertions(+), 35 deletions(-) diff --git a/module/contracts/api/accounts/authority/contract.binpb b/module/contracts/api/accounts/authority/contract.binpb index 7ef3f1903569272098d3729e4fc7b7204c1c9ece..8c5b0c9c69a9087c80be29925b7d4196de94a56b 100644 GIT binary patch delta 957 zcmY+BK~K~`6vsQ$*`*zZ1-c8aMdN6K8g`NB1w(?6tdi8Q*_Z|5NmJS4Zc?TRE$(rR z))0?|LoXhTAHWmI8sj&R5W`Ug;jCUvxbvpl5+;Maj1qQ z%!JvnFVsRkq+$OK`@M&Lb^q+K9aaBuiDnI=C@ayM#^B}X%RZx-+sJ&zqX%DEvHN$Q zz3j_vP?Aw_#!yT6rs+hlzOkuWzQ={fTfz@)*R_L|C%Cu9TY*pcsx3IAMT0+anl1PQ zO^5qVSO3mBs4$JM(+I~EHA9`oljdt2z5T&fBYePkr<|y5mkWE{@wqMN|2DFk*A~u( zY{2!zQ}}CoZo@BgZzHDT+iN~w_Zpk7!`n7AwJDNboraVNuMOQv+UJ1>CLWXp+H!;~ zf*X{=9$;n-LyM&{r4$pzAT!E+1pg{gkfF|K<5EQwtiWn!u|~HMB{DR~ES`_n4%qhN zS$!lGsURwzrvp7i1Ccqe(jlr#MBCDH;B|+T{>=a~N2Zbj2wW=Q62vCRAQik2Jv?N= zql%tOH9%B`jl=|jNhQCKnxMqw?wLuP0puQ;ObW?&QV9Uo z$Y>hDBUUH` z1UUfe$v_CJ04xE}YYeWI@WrIE3U*6+F^%Ni*o7n_fGwY4Lw^lYnMNDh)P$@*KYa=lYIcyTW%8~{QgiTFWy$BII zP;Uhb?$saQF%a>lS4)xHEVbI=Q7`@i-)?q_hk^ax`@Z+R?b0F_du#OTkPT2~1z(||tD?y(c*%Ete#Dx%+w;C1 zv0U*QrLZb&rC=cB7b&HfNE%s{?j!h?iG(zHRax*^M8Xm*U%(38gD5`KNCB^TpTD!c zM+J38kYpfb&(p+2q5w%>lWB^ozC_tkli+oeD2Ow}U!NmX(#)(taLm%VJK_G=H=Bh@_I7YR*mpx}Os-1`%17_9x^ph>-t807K@9{}7zkk*1T_#$7K2s|&xey`u&b$Af&AXwg%A;t)z4(a2b2E*!USY> cH6@53tEV$N)+vLRfR}_f1ur$SPNx(6AF33K5C8xG diff --git a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/workload_operation_admission.proto b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/workload_operation_admission.proto index 006f844b3..6d140031b 100644 --- a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/workload_operation_admission.proto +++ b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/workload_operation_admission.proto @@ -39,8 +39,8 @@ enum WorkloadOperationRefusal { // Owned by the module principal the alias names: a module does not reach // itself through this route. WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET = 4; - // The owner is not a declared module principal, or the seal is not an ACTIVE - // installation in the tenant installed under that principal. + // The owner is not a declared module principal, so no installation of its + // can stand behind the seal. WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS = 5; // The tenant has no ACTIVE installation of the route's target. The gateway // answers this one 403, as it answers a person whose organization never @@ -50,7 +50,9 @@ enum WorkloadOperationRefusal { // module it addresses. WORKLOAD_OPERATION_REFUSAL_SEAL_ON_TARGET = 7; // The issuer's live state has moved past the capability: its authorization - // revision is superseded, or an epoch it carries has advanced. + // revision is superseded, an epoch it carries has advanced, the seal names an + // installation the owner no longer holds, or the owner's module was + // uninstalled or now belongs to another tenant. WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT = 8; } diff --git a/module/contracts/api/accounts/connect/contract.binpb b/module/contracts/api/accounts/connect/contract.binpb index 7ef3f1903569272098d3729e4fc7b7204c1c9ece..8c5b0c9c69a9087c80be29925b7d4196de94a56b 100644 GIT binary patch delta 957 zcmY+BK~K~`6vsQ$*`*zZ1-c8aMdN6K8g`NB1w(?6tdi8Q*_Z|5NmJS4Zc?TRE$(rR z))0?|LoXhTAHWmI8sj&R5W`Ug;jCUvxbvpl5+;Maj1qQ z%!JvnFVsRkq+$OK`@M&Lb^q+K9aaBuiDnI=C@ayM#^B}X%RZx-+sJ&zqX%DEvHN$Q zz3j_vP?Aw_#!yT6rs+hlzOkuWzQ={fTfz@)*R_L|C%Cu9TY*pcsx3IAMT0+anl1PQ zO^5qVSO3mBs4$JM(+I~EHA9`oljdt2z5T&fBYePkr<|y5mkWE{@wqMN|2DFk*A~u( zY{2!zQ}}CoZo@BgZzHDT+iN~w_Zpk7!`n7AwJDNboraVNuMOQv+UJ1>CLWXp+H!;~ zf*X{=9$;n-LyM&{r4$pzAT!E+1pg{gkfF|K<5EQwtiWn!u|~HMB{DR~ES`_n4%qhN zS$!lGsURwzrvp7i1Ccqe(jlr#MBCDH;B|+T{>=a~N2Zbj2wW=Q62vCRAQik2Jv?N= zql%tOH9%B`jl=|jNhQCKnxMqw?wLuP0puQ;ObW?&QV9Uo z$Y>hDBUUH` z1UUfe$v_CJ04xE}YYeWI@WrIE3U*6+F^%Ni*o7n_fGwY4Lw^lYnMNDh)P$@*KYa=lYIcyTW%8~{QgiTFWy$BII zP;Uhb?$saQF%a>lS4)xHEVbI=Q7`@i-)?q_hk^ax`@Z+R?b0F_du#OTkPT2~1z(||tD?y(c*%Ete#Dx%+w;C1 zv0U*QrLZb&rC=cB7b&HfNE%s{?j!h?iG(zHRax*^M8Xm*U%(38gD5`KNCB^TpTD!c zM+J38kYpfb&(p+2q5w%>lWB^ozC_tkli+oeD2Ow}U!NmX(#)(taLm%VJK_G=H=Bh@_I7YR*mpx}Os-1`%17_9x^ph>-t807K@9{}7zkk*1T_#$7K2s|&xey`u&b$Af&AXwg%A;t)z4(a2b2E*!USY> cH6@53tEV$N)+vLRfR}_f1ur$SPNx(6AF33K5C8xG diff --git a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/workload_operation_admission.proto b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/workload_operation_admission.proto index 006f844b3..6d140031b 100644 --- a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/workload_operation_admission.proto +++ b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/workload_operation_admission.proto @@ -39,8 +39,8 @@ enum WorkloadOperationRefusal { // Owned by the module principal the alias names: a module does not reach // itself through this route. WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET = 4; - // The owner is not a declared module principal, or the seal is not an ACTIVE - // installation in the tenant installed under that principal. + // The owner is not a declared module principal, so no installation of its + // can stand behind the seal. WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS = 5; // The tenant has no ACTIVE installation of the route's target. The gateway // answers this one 403, as it answers a person whose organization never @@ -50,7 +50,9 @@ enum WorkloadOperationRefusal { // module it addresses. WORKLOAD_OPERATION_REFUSAL_SEAL_ON_TARGET = 7; // The issuer's live state has moved past the capability: its authorization - // revision is superseded, or an epoch it carries has advanced. + // revision is superseded, an epoch it carries has advanced, the seal names an + // installation the owner no longer holds, or the owner's module was + // uninstalled or now belongs to another tenant. WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT = 8; } diff --git a/module/contracts/api/auth-gateway/grpc/contract.binpb b/module/contracts/api/auth-gateway/grpc/contract.binpb index 4ca65c037a32d0b2aa549abb1320121238cb6c3f..08ff612c36a1f32270515929d4376c6b3617a939 100644 GIT binary patch delta 909 zcmY+BL5tHs6vs1@nKqeft!dM3vxDHaV=fdY>5rmHC zIpNqB+<(T$VZivIBRHhRkUw=tWB7z4mj~|hPm#`0X&c{Q5so`*hPsV6tv5J&Wzxgw zmr41oo~Rv<3+KoUxFgvAHnN*P74Ae1;Q8V?{Ed8X7}U8xiRrj{J>WkE7$%BAc9e$*{#BwNL*3Cf zq>3n5fzvdw#ukVX85*R4n^C(=7f%|xnTk{pRjTa#3eiAhHC0w%x6>^OzxeT#2G;Dlg*@o zd}SL<-mg}Hk?g2Yp(it2H&O`zwpmUictm@xnM{qPMHw=cz4&G@Wm`E|-3-Ca+S;|4 zL-EZzIKy(V4Mc|8!KJhW!cO)=8i8HMu!>2DGKSj47P}d9pzKDEstBkBY~7^=}d1*~}Dg`^-GQdEV#U*-vNw!+9}9Q#>V8ddyC;==J<} zmK_u8|ImFN9roobLF^ul!ssA8n*Xxd z1Qj;$HLg-kK{oK3Ox<_b0UnN$?+&|Mzrpz#&R{ANgTY(noMUPTx@qhn_$Jgqf^3>A z(nSqy!0}kz%w26SJROV+FhzX|37U6krNyL0ivHL z$X&Xw9zc-dLrR>6AVGaxPu?xF_=#^VX%7(od7(lOg!pz{haeU5;4~yX0c4x5sR!hL zXh6t^MHLvSkqk=gCT!s!*kFElpsx9&{tq=X3uE_aZORNl|&gqTDZcmryMw2$)_?~^;*>{+5tdoMd!#u zZ7B3b7D5;bwUfVP_I1)$h2RW;wkm`$0iXlGVlcSs;EU>I0&yLyq?Np@UQmhx);-0j Z6pKHPFa@k@<+Ty8u3ebluChNn{{RKPYnuQ7 diff --git a/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/workload_operation_admission.proto b/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/workload_operation_admission.proto index 006f844b3..6d140031b 100644 --- a/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/workload_operation_admission.proto +++ b/module/contracts/api/auth-gateway/grpc/proto/saas/accounts/v1/workload_operation_admission.proto @@ -39,8 +39,8 @@ enum WorkloadOperationRefusal { // Owned by the module principal the alias names: a module does not reach // itself through this route. WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET = 4; - // The owner is not a declared module principal, or the seal is not an ACTIVE - // installation in the tenant installed under that principal. + // The owner is not a declared module principal, so no installation of its + // can stand behind the seal. WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS = 5; // The tenant has no ACTIVE installation of the route's target. The gateway // answers this one 403, as it answers a person whose organization never @@ -50,7 +50,9 @@ enum WorkloadOperationRefusal { // module it addresses. WORKLOAD_OPERATION_REFUSAL_SEAL_ON_TARGET = 7; // The issuer's live state has moved past the capability: its authorization - // revision is superseded, or an epoch it carries has advanced. + // revision is superseded, an epoch it carries has advanced, the seal names an + // installation the owner no longer holds, or the owner's module was + // uninstalled or now belongs to another tenant. WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT = 8; } diff --git a/module/contracts/api/catalog.codefly.json b/module/contracts/api/catalog.codefly.json index 378e7c0b6..91f47af0c 100644 --- a/module/contracts/api/catalog.codefly.json +++ b/module/contracts/api/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/authority/contract.binpb", - "digest": "sha256:978af58084c99638d6b0d979ea5991d78439f6b4d5deca0d34ff4d547e675d0c", + "digest": "sha256:c2af68c66470556658bef9c7d6f36b577057067ba51446698c9c60356109a898", "services": [ { "name": "ModuleCapabilitiesService", @@ -65,7 +65,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/connect/contract.binpb", - "digest": "sha256:978af58084c99638d6b0d979ea5991d78439f6b4d5deca0d34ff4d547e675d0c", + "digest": "sha256:c2af68c66470556658bef9c7d6f36b577057067ba51446698c9c60356109a898", "services": [ { "name": "APIKeyService", @@ -563,7 +563,7 @@ "kind": "protobuf", "package": "saas.gateway.auth.v1", "path": "contracts/api/auth-gateway/grpc/contract.binpb", - "digest": "sha256:9d11dfc0427a0d24d14071347ccb12db8c8f1a94cfcb51713c5afad9fbea8ba9", + "digest": "sha256:282b2820ed0d1e8a693dad827b0446d65843a7d1f0328a6d137e8aec0c129291", "services": [ { "name": "AuthGatewayService", diff --git a/module/module.package.codefly.yaml b/module/module.package.codefly.yaml index 72ab953db..0c3e32a19 100644 --- a/module/module.package.codefly.yaml +++ b/module/module.package.codefly.yaml @@ -18,12 +18,12 @@ services: kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/authority/contract.binpb - digest: sha256:978af58084c99638d6b0d979ea5991d78439f6b4d5deca0d34ff4d547e675d0c + digest: sha256:c2af68c66470556658bef9c7d6f36b577057067ba51446698c9c60356109a898 - endpoint: connect kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/connect/contract.binpb - digest: sha256:978af58084c99638d6b0d979ea5991d78439f6b4d5deca0d34ff4d547e675d0c + digest: sha256:c2af68c66470556658bef9c7d6f36b577057067ba51446698c9c60356109a898 - name: auth-gateway endpoints: [grpc, rest] api-contracts: @@ -31,7 +31,7 @@ services: kind: protobuf package: saas.gateway.auth.v1 path: contracts/api/auth-gateway/grpc/contract.binpb - digest: sha256:9d11dfc0427a0d24d14071347ccb12db8c8f1a94cfcb51713c5afad9fbea8ba9 + digest: sha256:282b2820ed0d1e8a693dad827b0446d65843a7d1f0328a6d137e8aec0c129291 - name: cache endpoints: [read, write] - name: frontend diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/workload_operation_admission.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/workload_operation_admission.pb.go index dde00c76c..b8ec0e4b1 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/workload_operation_admission.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/workload_operation_admission.pb.go @@ -43,8 +43,8 @@ const ( // Owned by the module principal the alias names: a module does not reach // itself through this route. WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET WorkloadOperationRefusal = 4 - // The owner is not a declared module principal, or the seal is not an ACTIVE - // installation in the tenant installed under that principal. + // The owner is not a declared module principal, so no installation of its + // can stand behind the seal. WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS WorkloadOperationRefusal = 5 // The tenant has no ACTIVE installation of the route's target. The gateway // answers this one 403, as it answers a person whose organization never @@ -54,7 +54,9 @@ const ( // module it addresses. WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_SEAL_ON_TARGET WorkloadOperationRefusal = 7 // The issuer's live state has moved past the capability: its authorization - // revision is superseded, or an epoch it carries has advanced. + // revision is superseded, an epoch it carries has advanced, the seal names an + // installation the owner no longer holds, or the owner's module was + // uninstalled or now belongs to another tenant. WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT WorkloadOperationRefusal = 8 ) diff --git a/module/services/accounts/proto/saas/accounts/v1/workload_operation_admission.proto b/module/services/accounts/proto/saas/accounts/v1/workload_operation_admission.proto index 006f844b3..6d140031b 100644 --- a/module/services/accounts/proto/saas/accounts/v1/workload_operation_admission.proto +++ b/module/services/accounts/proto/saas/accounts/v1/workload_operation_admission.proto @@ -39,8 +39,8 @@ enum WorkloadOperationRefusal { // Owned by the module principal the alias names: a module does not reach // itself through this route. WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET = 4; - // The owner is not a declared module principal, or the seal is not an ACTIVE - // installation in the tenant installed under that principal. + // The owner is not a declared module principal, so no installation of its + // can stand behind the seal. WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS = 5; // The tenant has no ACTIVE installation of the route's target. The gateway // answers this one 403, as it answers a person whose organization never @@ -50,7 +50,9 @@ enum WorkloadOperationRefusal { // module it addresses. WORKLOAD_OPERATION_REFUSAL_SEAL_ON_TARGET = 7; // The issuer's live state has moved past the capability: its authorization - // revision is superseded, or an epoch it carries has advanced. + // revision is superseded, an epoch it carries has advanced, the seal names an + // installation the owner no longer holds, or the owner's module was + // uninstalled or now belongs to another tenant. WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT = 8; } diff --git a/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/workload_operation_admission.pb.go b/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/workload_operation_admission.pb.go index 9726c09e7..cb2bfb393 100644 --- a/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/workload_operation_admission.pb.go +++ b/module/services/auth-gateway/code/pkg/gen/saas/accounts/v1/workload_operation_admission.pb.go @@ -43,8 +43,8 @@ const ( // Owned by the module principal the alias names: a module does not reach // itself through this route. WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET WorkloadOperationRefusal = 4 - // The owner is not a declared module principal, or the seal is not an ACTIVE - // installation in the tenant installed under that principal. + // The owner is not a declared module principal, so no installation of its + // can stand behind the seal. WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS WorkloadOperationRefusal = 5 // The tenant has no ACTIVE installation of the route's target. The gateway // answers this one 403, as it answers a person whose organization never @@ -54,7 +54,9 @@ const ( // module it addresses. WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_SEAL_ON_TARGET WorkloadOperationRefusal = 7 // The issuer's live state has moved past the capability: its authorization - // revision is superseded, or an epoch it carries has advanced. + // revision is superseded, an epoch it carries has advanced, the seal names an + // installation the owner no longer holds, or the owner's module was + // uninstalled or now belongs to another tenant. WorkloadOperationRefusal_WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT WorkloadOperationRefusal = 8 ) diff --git a/module/services/auth-gateway/proto/saas/accounts/v1/workload_operation_admission.proto b/module/services/auth-gateway/proto/saas/accounts/v1/workload_operation_admission.proto index 006f844b3..6d140031b 100644 --- a/module/services/auth-gateway/proto/saas/accounts/v1/workload_operation_admission.proto +++ b/module/services/auth-gateway/proto/saas/accounts/v1/workload_operation_admission.proto @@ -39,8 +39,8 @@ enum WorkloadOperationRefusal { // Owned by the module principal the alias names: a module does not reach // itself through this route. WORKLOAD_OPERATION_REFUSAL_OWNER_IS_TARGET = 4; - // The owner is not a declared module principal, or the seal is not an ACTIVE - // installation in the tenant installed under that principal. + // The owner is not a declared module principal, so no installation of its + // can stand behind the seal. WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS = 5; // The tenant has no ACTIVE installation of the route's target. The gateway // answers this one 403, as it answers a person whose organization never @@ -50,7 +50,9 @@ enum WorkloadOperationRefusal { // module it addresses. WORKLOAD_OPERATION_REFUSAL_SEAL_ON_TARGET = 7; // The issuer's live state has moved past the capability: its authorization - // revision is superseded, or an epoch it carries has advanced. + // revision is superseded, an epoch it carries has advanced, the seal names an + // installation the owner no longer holds, or the owner's module was + // uninstalled or now belongs to another tenant. WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT = 8; } diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json index e85a11ebd..8eb4913af 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json +++ b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contract.binpb", - "digest": "sha256:978af58084c99638d6b0d979ea5991d78439f6b4d5deca0d34ff4d547e675d0c", + "digest": "sha256:c2af68c66470556658bef9c7d6f36b577057067ba51446698c9c60356109a898", "services": [ { "name": "APIKeyService", diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb b/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb index 7ef3f1903569272098d3729e4fc7b7204c1c9ece..8c5b0c9c69a9087c80be29925b7d4196de94a56b 100644 GIT binary patch delta 957 zcmY+BK~K~`6vsQ$*`*zZ1-c8aMdN6K8g`NB1w(?6tdi8Q*_Z|5NmJS4Zc?TRE$(rR z))0?|LoXhTAHWmI8sj&R5W`Ug;jCUvxbvpl5+;Maj1qQ z%!JvnFVsRkq+$OK`@M&Lb^q+K9aaBuiDnI=C@ayM#^B}X%RZx-+sJ&zqX%DEvHN$Q zz3j_vP?Aw_#!yT6rs+hlzOkuWzQ={fTfz@)*R_L|C%Cu9TY*pcsx3IAMT0+anl1PQ zO^5qVSO3mBs4$JM(+I~EHA9`oljdt2z5T&fBYePkr<|y5mkWE{@wqMN|2DFk*A~u( zY{2!zQ}}CoZo@BgZzHDT+iN~w_Zpk7!`n7AwJDNboraVNuMOQv+UJ1>CLWXp+H!;~ zf*X{=9$;n-LyM&{r4$pzAT!E+1pg{gkfF|K<5EQwtiWn!u|~HMB{DR~ES`_n4%qhN zS$!lGsURwzrvp7i1Ccqe(jlr#MBCDH;B|+T{>=a~N2Zbj2wW=Q62vCRAQik2Jv?N= zql%tOH9%B`jl=|jNhQCKnxMqw?wLuP0puQ;ObW?&QV9Uo z$Y>hDBUUH` z1UUfe$v_CJ04xE}YYeWI@WrIE3U*6+F^%Ni*o7n_fGwY4Lw^lYnMNDh)P$@*KYa=lYIcyTW%8~{QgiTFWy$BII zP;Uhb?$saQF%a>lS4)xHEVbI=Q7`@i-)?q_hk^ax`@Z+R?b0F_du#OTkPT2~1z(||tD?y(c*%Ete#Dx%+w;C1 zv0U*QrLZb&rC=cB7b&HfNE%s{?j!h?iG(zHRax*^M8Xm*U%(38gD5`KNCB^TpTD!c zM+J38kYpfb&(p+2q5w%>lWB^ozC_tkli+oeD2Ow}U!NmX(#)(taLm%VJK_G=H=Bh@_I7YR*mpx}Os-1`%17_9x^ph>-t807K@9{}7zkk*1T_#$7K2s|&xey`u&b$Af&AXwg%A;t)z4(a2b2E*!USY> cH6@53tEV$N)+vLRfR}_f1ur$SPNx(6AF33K5C8xG diff --git a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml index c86d9b367..46b703cea 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml +++ b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml @@ -12,7 +12,7 @@ sources: module: saas-starter service: accounts endpoint: connect - contract-digest: sha256:978af58084c99638d6b0d979ea5991d78439f6b4d5deca0d34ff4d547e675d0c + contract-digest: sha256:c2af68c66470556658bef9c7d6f36b577057067ba51446698c9c60356109a898 services: - APIKeyService - AccessibleScopeService diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/workload_operation_admission_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/workload_operation_admission_pb.ts index 8bdd5b995..35356900f 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/workload_operation_admission_pb.ts +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/workload_operation_admission_pb.ts @@ -139,8 +139,8 @@ export enum WorkloadOperationRefusal { OWNER_IS_TARGET = 4, /** - * The owner is not a declared module principal, or the seal is not an ACTIVE - * installation in the tenant installed under that principal. + * The owner is not a declared module principal, so no installation of its + * can stand behind the seal. * * @generated from enum value: WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS = 5; */ @@ -165,7 +165,9 @@ export enum WorkloadOperationRefusal { /** * The issuer's live state has moved past the capability: its authorization - * revision is superseded, or an epoch it carries has advanced. + * revision is superseded, an epoch it carries has advanced, the seal names an + * installation the owner no longer holds, or the owner's module was + * uninstalled or now belongs to another tenant. * * @generated from enum value: WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT = 8; */ diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/workload_operation_admission_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/workload_operation_admission_pb.ts index c7ad596cf..cf2d02b58 100644 --- a/module/services/frontend/code/src/gen/saas/accounts/v1/workload_operation_admission_pb.ts +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/workload_operation_admission_pb.ts @@ -139,8 +139,8 @@ export enum WorkloadOperationRefusal { OWNER_IS_TARGET = 4, /** - * The owner is not a declared module principal, or the seal is not an ACTIVE - * installation in the tenant installed under that principal. + * The owner is not a declared module principal, so no installation of its + * can stand behind the seal. * * @generated from enum value: WORKLOAD_OPERATION_REFUSAL_SEAL_NOT_OWNERS = 5; */ @@ -165,7 +165,9 @@ export enum WorkloadOperationRefusal { /** * The issuer's live state has moved past the capability: its authorization - * revision is superseded, or an epoch it carries has advanced. + * revision is superseded, an epoch it carries has advanced, the seal names an + * installation the owner no longer holds, or the owner's module was + * uninstalled or now belongs to another tenant. * * @generated from enum value: WORKLOAD_OPERATION_REFUSAL_NOT_CURRENT = 8; */