From 374b850a97160261db35f594d196ca7a19e80ed1 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 1/8] 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 017917f45..32d7e1d1e 100644 --- a/module/WORK_CONTEXTS.md +++ b/module/WORK_CONTEXTS.md @@ -262,6 +262,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 3f1f93ff6..b6e62cd2b 100644 --- a/module/services/accounts/code/pkg/adapters/work_context_rpcs.go +++ b/module/services/accounts/code/pkg/adapters/work_context_rpcs.go @@ -1386,6 +1386,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()) @@ -1424,6 +1427,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 d598c4203bf9cccd5eda37d1ab11ee5a42c44399 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 2/8] 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 32d7e1d1e..62933166c 100644 --- a/module/WORK_CONTEXTS.md +++ b/module/WORK_CONTEXTS.md @@ -563,6 +563,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 5a4135c62..f5cbba534 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 34ea2ee34..7e1da5243 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 d90b1fd2a..c760860f1 100644 --- a/module/services/auth-gateway/code/main.go +++ b/module/services/auth-gateway/code/main.go @@ -272,6 +272,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 401b8756b50bf2df0a1356e092d25b18a2a2ee67 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 3/8] 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 fba690cbbbcdea7fe55f3fb9e557fdfbeddd08c3 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 4/8] 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 62933166c..343f85401 100644 --- a/module/WORK_CONTEXTS.md +++ b/module/WORK_CONTEXTS.md @@ -262,16 +262,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 b6e62cd2b..76f9c1efd 100644 --- a/module/services/accounts/code/pkg/adapters/work_context_rpcs.go +++ b/module/services/accounts/code/pkg/adapters/work_context_rpcs.go @@ -1441,15 +1441,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 b1fe03833cc63a88a0192365b22eb0db2b0444c4 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 5/8] 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 zcma)Ddwdne^}jniySaCFLz2rY39nTk2!xmQ2GO}ids~pf{6GoK2WQof*`(HirW6Z=gjPGpuazUf8_JI-!o^< zoX4D*von)TzaRgbd&e(sHCjch*(zJDR=c&JHQAbKb=JGztdSc}ary^qwik6vUQl1x z+)~$YQE>CH!t?T9taSsC&*j~NMt;d=_j$uPbI%Z`AZ;ZQR|X&MvIn^TwjE(5(y~*+ z4_&Fz1Fr1+E@#15G^vQ+&#yi|)CUo}yL#W?4KilL(8))B> zZEDEWvq{;8Y!ei1h82EoBL=;&vB~j+D~1)8Ns<;!*k0WA!m1-oZmycs)Y#JK_;r`e zX>5Y^Ewk#-v93N_!vi!c!&sRtMqTJPV%(-9WP~p9;o^l3b!*B7@1I;ayqHBSsB6ml z&9gFdvQE(Fl)`?wlbC!_rX{-|v#?LIKeKVpLLZ|>LsvTfjD;;((w~38V~%552_tQG zvN}7SV_6A7sIxf6aUDsBKt9A6eDqiMcSSug+*T8_)JV(Y z5(D$s?{c>qx#P552HVwF+Xa+1`)a!YphRD7mm$cmep*`~0!mu|F!s}`7=qgRi#}`? z5P?cI3xH6+{!Wg3Y6!BcQuNfEFso7=r#Ve+IkP$yN-`j~ zI^8~Ogxu;5Beh&Z%dPG>LbsEFLnFDcZ>TskY{Do*Q(Zy?u|vD}2ob~%^~YbTy&tDAF~@zBRS3qoAt_nZ#@uEgT&(1_Qz- z3_02@2@ysao#u%GbtFa^-Jj| zq?~Q1&67n3tsH~UA2#Hk+%XK04Y4OjVhg}|is+)d6%gp3(`FQ)-zi;r>47>0=yyur ze!49Q^T$TMfNo<&b=U-vW8GdMf{?Mj2Zjhj#ts^yQ3~cyhA;NooCK+~oGi|j<@x0w zy0gslC#TXOA9_vh*x4y$zoe4jD?QhAiku|$O2HLf(&iL7RL?RZP*O~l{dIo_<4nUi z?_kA356qlt!K6>z_s*J@;x(6O7-Cv^*RTT&n$~Tc7U+;bYVUrAZg(N@dl@58ybFRywiG+L`lLz= zbb)~S4jnm+KnP=fr$z}UCoz_QdL}5H8%=h*reKsyq|=rHAq`Tk4_MaGJ{O*u&K96) zt|%>^=ue!JnL97*&z#rP9Q1q29@kTC0CQ7nxdS5BPMw`C1c-nN3;-Ax7DbE@x==d^TNAo1(h12M)uN~y`E`-@E1J-45xTih z#muFx%M(``2Cs3nT3w#xty!8Pft0&KryLLg-f*Z1V_o5P=h+E_ZddeB3l0#(UxCet zlE)O-ALJ4P9-9S^0W#`VKFyz;ZSfb($_B-jcU}b=xFuB@c0*{E7+t!~u(_xj?A)_K zIKe~Pi+TrL*SZ%6_pfy;jH3(N%${WL{8j7RAFTYG$KAA>KVysAtD9@u1Abz(WXbvD2p~dN|3T>(+d-%EZX#FGwlkoSF1FKOS+04vV4Iby#1+r z+0+ClNY_-EUKJ>iUAuPE%anon-L-oUHJiao>4*KR8&)N?c{2rFy2}C1H-_E}dj=1F z<{msZkwScX9*W z3z1d9%X{6s&%tkSRz*&v%&dYF)gDNDa#i=Re&8*c+IvJ>GUN_|+^1B8Foy;O&+Ky_ zpM-4}Vh5#)wIJGr2el82FA1@OI*!stO1&};>NHYw(PwK*50E#&KcZflj(SD6VX{pxJy}n-usWGuHb%EQFmVPi-u39O#oMXfEUOJMcvZl|+JgjAU`yZdRH zTU9DaXBw*u`|E?sL+&K@E~*R+RG(@W@>TYxLdw`V+KI3RqfZhk`v)W04Ok5`w%DxxpTD>0U>v;b|N7qcP^Yrc3}5! zNi2wqk``?Tstb%aPmc#gK+OeJhRoAus4`@pHUlV$Z_#<$4uWdDdD;%HQ_T61TxdRD z+rg!YIbYiW1T*GqGk{>md~F6HC3Zf{pz2Ddt%ZrJba9oV4Ps$Zl}VBfS)>O9qS{Rv z0szKE+7KWNxJVlUgaH?6I|!)(7il{r$K$d{ECgMq?T`~WAeU)FzzMmRX+wY@_cCn= zAtm=RJ*g2*`2BLoJr!{V0to)=dw0aS6lxXt%Twwj90-yxKUOWuK#+WSuL(Sd2`R~! z_x=e_6Ul{G4$ILPmTac2oVYUR@Pj*)69(-VIj5H+AefZX+YS&$%Jr{w+OZbcysg}T zD-F{r!51}JF&6e6x!Az6+a4Q_Ceg-&>XLUJA+5B1wUJncD7mPK=q%}K1BG6ple45n zVUpKS{Yo1GAfr*d$^(J<8UvA{ofr_)_8J4B(hz&VH3niu+Mrcg4ibW;IH)*Y8ViRs zpfke89Am^wGei1Pl*A!~^rZ$OrljkLfy5yb(oWmg8Hp9zEbP|uIwPrS2Ak@4dA)&e z46+K4k)TM8TG4S<*!;~#Er{Mq zqZ8K^1R-#xF>EXrNQxfs||}B6Vjm9ZH8UZmITSa5AMFl zYoEK_udl?X_)>Y@bYHpYm`H@Tg()kq7j_si%C@DCLhhg7EvJM$Oq; z4+DPB8**#pcSEw@^9IgeL%%Z(et#_XJCh*!al`KCbYM3!3AYUP6GqN{I*J055rsd; zQD72B;h8uJiU^U<#!+AzqOdvkC6f?^=i?|a2~pUhqhKd;f@z4tUykaDD7+9yfvn$V z$d{rh7*a)Ho8i6`ML`h}g%=IGMn^%>5QXg&g+HSDhHpjMe$^0r4KsMf^;YFxHEd3J zYJpv^8fAkhn+PHZuNot%I$#JjUNht;Q4oX*!fS^6NfZR8AqcO>L0}RjzYzz4Nfd-P zjSlwPQ4pAnAiNW-yW8!Md(Xh$rKS_N6hO%C*zHV%+dqh%$0WG@!`SUig4_QVxm_>` zUjOK*j_~@&vDd{^*t^G&pGR&N+U0cV|5)saProrh4W2Z9- zlD~?b&LndB*O>W#kDShAIQ<{-%x4nM{C~zXUlAela2x`rAp}3fo@5e2@M9bTCLsj> zib9aX9*hJ=4E}voPsCuch}Ivn{z@UQhV^RwF?9&86zv8hy~c$QQMf@&zCcF-b(Otb$UCDbNELtnAm3GUt??Bm;V!QGF>?q)8yJKv_`R5AkoevH`EN()2f6GHwZ z62$8}NbXtMtrq(qT0J5BGo63o!~zR6Dz2?Uydli;{dWHC4xVGA8t9_eZ*0g0ue?~? z;aB`2pPAP(Ykx6^dxO!u@c3mW~VY^Juk${&xW`E^;`b^AC6ot>S@ zm+qP7i*XG;qj6pfU;nmb8!`p-p?3qdJYwUVY?FF+FjI?aY^YyIr^pS;z)TYi zqx*0DMgb%96*}L!WxtbJIL2ngNg=8c6 zTSZhM;rt7*W{OS~oUqy3gnT_p6{F%6=(3%hq)5|ZMj zI4PKml43`k6ih=>yd2MMCLt+yQsSuvhe=3^SK_1?!Zc*Xt4H-jQoKfNFRJNXkhVV& z;(JW*Cu~0$z0>WK?^)_qnX^9;Hh+{L1xTk)M7NW)&u|_Gc-lUIn{w%or;-dDs&xO4|S~9o|-Z>C^he@Q#m$7%42Jak z^HscLFbUrII`$5e;GA!wjz7Z{yIL~7B{nrXV46HElNf&na?@9ffs^0V z-Fw5xEjObrHHo2@o6(kv<4VBEm+H{q*wbEN$~(-kut}=Wpene-3=12kAv8Cd(U!_2 zNWRI8wp1q3+J3Xy(Z0nDw^Sx0P`8@Fl53Ogb7r)ha|>d#(u{UbCLuPzGo$64Nr=sD zX0&@U39kVB%w6Wk6DerV2DS+|v~eX8ghQhdZm)d{5!K$`sEd zHn(BEfB3Z9`O#y&LNoV}Y4eo}rh5uq9x}W0=R869^da+%3$;&E@aY;;Zi;-GQa)W{ zx|<@OG7Uam8~c<=ko<7$Qzns5AAwKTM?PgTe7eC5?(U5;C2uBs>FLHzlmw5NQJKOt zIQog$Urd6dH=0qI!X!BQ$=K0Mf}@{0sv{izbQ`WZvabkVZ6<1O^_4hjN8cN3JOez^A=ZllJcbMW+Gk<%9=ZM@6)8<19_}+o3 zJIrHF(Lu*KHQH{=_ zd~^tsK0qrg;Opi6CU%2KIE(^J+dv1+U}Hb;%p5S}?>F%vK@abv|gSLquW%4d-+Rz(q?)$Ochgns^tWB;$Bgeoa=M&w3!fUz@>o zjme+&F2LzFAv^`ptmyf*iMIePa2+)Ghne_wU{rdIzr=--{D+C7ZemRGA>AQUAJPG) zB~a7|-a{sy0%%?cJ!I-Lx+I^`9X9nD9UyicGuTn-jU58ONFUMxqUYhbC#qPvSc>+} zKQL{e1T0ycKW%{5Y@jO%S}Z%1>p|&6VzKN-XQiYsdFd(%AKXj#iok%u7ggR}=!s)n zwCS@OOMfAsrp+k@tjcUM8&|(q{IiRCz9pnVDTXD-JwZ=_fKeZ)uApR1l=sl z)RiS51l=tAS32+E#FQ49KqfZG;Maq_wW*8 zVValXa=#QqoI^@3f_T3SMh^470WW=`-Y;eyIKb44)^W`FV*O) zW7EjjYo!<#zBNTy*277w(aHO6kB_5>tiKENE$iyo$=rGE= z7uQQTIh9ri7Q~l|@&zfn^Pqq@LzQ?BOQQf`&==$}$7)D+ZzHeJq+`>z$)I4gHzfxE z)@>3GYgJDg;~$0ZrBRumt-WEX6TO4`E)f#NI4$}ygQ_JzTl-|r^GTcnH9za-6@0P$9O+2MH^Um z%JMGGb#xbwg7XcT_)rG7j`3PAzzx5ALnhT)o8ryjO)2O`T1vpQ1Y-XY0`Hr$OuYmG zgr#rlse)FzW!gpS@~>$*0wx$NJlR`b4ZsM`R^SF|L)Tpr?^YBf)837i^c4B`T^ZDz z;@z18K-YJr-n;?P^3b4ekWv_pK)tkFip+bmqq>d)FSp{xP>o6}{%$Gg z86WA3SOA5_QJ{9qbbqyiq7{92hg(UCp1aBqNaORkFQcG!tT(9-+R*g_xLFTF3;qYP zV<+c1ydg&kV1F(X`(^&evEEZgPds~&pUb2g)lpT9#E0wg5DSRHL`T zDg^)`cAqR#9(8c3kd|MBI}#uxwcZ24(l4YNjzYosLUvbvKr1ADK~waP^f1j$+uzH? z)s|5n*#4w!X6CGH?S#hs($hV9orRv?%cOei=(4&$k{9T@77z!(GI(;lH+l#FSbvl$ zRTBYXkRPRfCF$~HSz@8*E}AUBxG4<=p6R_Y7{3WvdJ_jk&n1@L#9dBi$Z1LsmJ$%~Wp=!Q=&v-EZj$Y?0X z4+zd%X1U5+fato+>UBccib0lHL#mxJnp7Seb{(lV5$XZrJ~TM~C*D~({3a0E1&E&4 zkzJ%-A->S9#I0JrLU1m(lDuBaLU!{Cl1UR45U0(Su5E$9yTa1x0SKupES(;J*ilwk zsBLppQxaCi?Fvg5w}q_njj?oy15&rcXk@5k0SM_ghOq#I^cyXGIZRLqztO_wFu9|U zqjeJr$1OYQj&K2v4qGApCQGjhXoU2eEWIiKLi$Y>p0g>{^_4zIc+aM!Qz>{0Nxy>% zxB_0!Z`+(7y`msDn7<0RY zK($NTtE|MIto+!C-WJib9i*+YaK}svwc|+LM+#wwA()o;SwZC_Z^}skV7<@M2?2K(sOuE&iN-Zn(07VA5rdM;)`Tttiz(XjNTjrPZQ}R!CYGra)2JUT-Cy(J4>_ z&h=K3_f1*ENw~oh^gdb=#9@Sm&5oo3fp>#ds1{s6NZnvz!R?}&X=&U@3cf+Jq=@{m z(aQfg)jQib6$t6Q(b9E4Ah-5VsRsyI8!c7gqAP6@n@ALYK>!oX-+Z3e-N*q!)Fw+W zvw#q_$83h+;vHK{it?$XGx|>RT2Fyqm*( z2ZYqkl3Nt2Mm0*G;UTAjymSO_ZH zc3P#%HzgdF*GcwAWE(K9)2*Osy0^O;fRWB!KnQ<5%-s@B`?sho{p%XK!s%~W!PVz` zza9dBu5X2#A|SfHMVq41i_(485qKRe zcT->f9~EGN!N3c=mC%?z({{%i)A#ajE7BNGdgc4^u=p+mQU@I+{Du6!g}2X2MfyU1 z-_i#iCG;X!er%yGdy1A2V1^yb*J7h-!sQa(4dqY&mDkrmu;`=iW2-<_W`Gd&aX14@ z1YY0Dy`;tGN)%4t_FDOSGhVF$3#c65Yo!Xc8g$gV*D6*k8(J~$UaP%IZi1La%+Z0~ z6(a^XRI(C(*Q-hzvi4g^bzG3<$$KC)9}s6vVHE)c-UCy^Wd#^w4}@g|AO=1Vt||o8 z^gR%+Drug+Us*UIYl4FS(>BnzmQ_AO{o{;P>y65Rj&Z)S^hp^YhWUy%K{_b|#5vVh zR;O<*by7wIJ}LXk>in&xPRcY5PW&2NoKBhqQQx2fW6z~IC%%iVMM`{!7k_F}p%rR; z7w(0C==&Yz6FISc+CF3@zSmA{56(kjDb}7V#easCB4Anq@kgTs!esoXg?|p9IRuEV z|FpWPtppJNI|r=ylJ@Me!=Z+NI3L4~ultgp4u`cO8lm7}Yw6;whrdGnwaYKg7M*aR zu`&45i$#b3<6l%peqB%&?0=y+735wnF3SI~y=eGwqk3tzRgAxqe6^Lne%${8<2PR7 delta 15097 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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 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 f5cbba534..fd90cec47 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 7e1da5243..a6f117309 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 64291390b..2b1baba75 100644 --- a/module/services/frontend/code/ui-inventory.json +++ b/module/services/frontend/code/ui-inventory.json @@ -4975,6 +4975,7 @@ "react", "sonner", "@/components/empty-state", + "@/gen/saas/accounts/v1/installation_listing_pb", "@/gen/saas/accounts/v1/installations_pb", "@/lib/auth", "@/shared/ui", @@ -5000,6 +5001,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 3e3a89e3768b17fe6bd07d0dd68bc43da05d4b72 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 6/8] 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 fd90cec47..89f770cbf 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 a6f117309..74ee039b6 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 afdf0b09d55095d25daba73700bcdfe3588df6a1 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 7/8] 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 89f770cbf..d32852ed0 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 74ee039b6..ece3bfe86 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 91e404bb6a453992636838460cf5d067e87086a0 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 8/8] 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 cb7822b6c..5257b4b08 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",