From 13cf75310dc52e39954ad44da1d9a8d08458ff72 Mon Sep 17 00:00:00 2001 From: crisfiorini-oss <301773486+crisfiorini-oss@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:19:56 +0000 Subject: [PATCH 1/4] feat(accounts): add audit storage interface and journal delivery --- .gitattributes | 30 +- MODULE.md | 15 + configurations/local/audit-storage.env | 1 + .../contract/catalog.codefly.json | 3 +- .../source-read-sdk/contract/contract.binpb | Bin 445059 -> 449825 bytes .../source-read-sdk/go/accounts_facade.pb.go | 8 + .../module_capabilities.connect.go | 37 + .../accounts/v1/module_capabilities.pb.go | 1055 +++++++++++------ .../source-read-sdk/library.codefly.yaml | 2 +- module/DATABASE_AUTHORITY.md | 28 +- module/EVENTS.md | 93 +- module/configurations/local/audit-storage.env | 23 + .../accounts/saas.events.codefly.yaml | 9 + .../api/accounts/authority/contract.binpb | Bin 1301662 -> 1310942 bytes .../proto/saas/accounts/v1/audit.proto | 108 +- .../accounts/v1/module_capabilities.proto | 80 ++ .../api/accounts/connect/contract.binpb | Bin 1301662 -> 1310942 bytes .../proto/saas/accounts/v1/audit.proto | 108 +- .../accounts/v1/module_capabilities.proto | 80 ++ module/contracts/api/catalog.codefly.json | 8 +- .../connect/DeliverAuditEvents/operation.json | 34 + .../DeliverAuditEvents/runnable-package.json | 1 + module/contracts/runnables/index.json | 11 + module/deployment/generated/asyncapi.json | 25 + module/deployment/generated/communication.md | 8 + .../deployment/generated/event-catalog.json | 100 ++ .../adr/0003-typed-audit-event-registry.md | 1 + .../0006-audit-sink-and-retention-tiers.md | 2 +- module/docs/adr/0009-audit-store-swap.md | 206 ++++ module/module.package.codefly.yaml | 4 +- module/services/accounts/AUTHZ_MATRIX.md | 9 +- .../services/accounts/code/audit_storage.go | 61 + .../code/pkg/adapters/audit_delivery.go | 208 ++++ .../code/pkg/adapters/audit_delivery_test.go | 177 +++ .../pkg/adapters/connect_auth_interceptor.go | 3 + .../code/pkg/adapters/connect_handlers.go | 3 + .../pkg/adapters/grpc_auth_interceptor.go | 10 + .../adapters/module_capabilities_connect.go | 4 + .../adapters/module_capabilities_server.go | 52 + .../adapters/source_operation_authority.go | 4 +- .../accounts/code/pkg/auditstorage/config.go | 90 ++ .../code/pkg/auditstorage/config_test.go | 66 ++ .../accounts/code/pkg/business/audit.go | 179 +-- .../code/pkg/business/audit_delivery.go | 197 +++ .../code/pkg/business/audit_delivery_test.go | 413 +++++++ .../business/audit_durability_gate_test.go | 6 +- .../pkg/business/audit_events_contribution.go | 14 + .../code/pkg/business/audit_export.go | 10 +- .../code/pkg/business/audit_journal.go | 166 +++ .../pkg/business/audit_journal_module_test.go | 85 ++ .../code/pkg/business/audit_journal_test.go | 378 ++++++ .../code/pkg/business/audit_service.go | 256 ++++ .../code/pkg/business/audit_service_test.go | 256 ++++ .../code/pkg/business/audit_storage.go | 313 +++++ .../business/audit_storage_contract_test.go | 44 + .../code/pkg/business/audit_storage_test.go | 156 +++ .../pkg/business/auditstoragetest/suite.go | 506 ++++++++ .../code/pkg/business/introspection.go | 3 + .../code/pkg/business/journal_read.go | 397 +++++++ .../code/pkg/business/journal_read_test.go | 297 +++++ .../pkg/business/module_authority_endpoint.go | 1 + .../accounts/code/pkg/business/retention.go | 26 + .../code/pkg/business/retention_test.go | 74 ++ .../pkg/business/rpc_policy_inventory_test.go | 1 + .../accounts/code/pkg/business/service.go | 17 + .../pkg/business/source_operation_receipts.go | 2 +- .../accounts/code/pkg/business/store.go | 24 + .../code/pkg/cataloggen/authz_methods_test.go | 17 +- .../testdata/mesh-policy.golden.yaml | 1 + .../code/pkg/eventcatalog/catalog_gen.go | 1 + .../v1/accountsv1connect/audit.connect.go | 84 +- .../module_capabilities.connect.go | 37 + .../code/pkg/gen/saas/accounts/v1/audit.pb.go | 231 +++- .../pkg/gen/saas/accounts/v1/audit_grpc.pb.go | 102 +- .../accounts/v1/module_capabilities.pb.go | 1055 +++++++++++------ .../v1/module_capabilities_grpc.pb.go | 46 + .../accounts/code/pkg/infra/postgres_audit.go | 28 +- .../pkg/infra/postgres_audit_delivery_test.go | 269 +++++ .../postgres_audit_journal_emitters_test.go | 158 +++ .../postgres_audit_journal_publish_test.go | 210 ++++ .../infra/postgres_audit_journal_retention.go | 102 ++ .../postgres_audit_journal_retention_test.go | 188 +++ .../code/pkg/infra/postgres_journal_read.go | 158 +++ .../pkg/infra/postgres_journal_read_test.go | 495 ++++++++ .../pkg/infra/postgres_role_hardening_test.go | 65 +- .../testdata/control_plane_call_sites.txt | 7 +- .../code/pkg/relationcatalog/catalog.go | 5 +- module/services/accounts/code/work.go | 31 +- .../services/accounts/code/work_audit_test.go | 74 ++ .../generated/api-contract-surfaces.json | 4 + .../accounts/generated/authz-methods.json | 111 ++ .../accounts/generated/gateway-routes.json | 64 + .../generated/openapi-raw/api.swagger.json | 2 +- .../accounts/generated/service-catalog.json | 132 +++ .../proto/saas/accounts/v1/audit.proto | 108 +- .../accounts/v1/module_capabilities.proto | 80 ++ module/services/accounts/service.codefly.yaml | 1 + .../auth-gateway/code/authz_catalog_gen.go | 3 + .../auth-gateway/code/routing_catalog_gen.go | 4 + .../services/frontend/code/package-lock.json | 2 +- .../generated/contract/catalog.codefly.json | 4 +- .../generated/contract/contract.binpb | Bin 612094 -> 616608 bytes .../saas-sdk/generated/library.codefly.yaml | 2 +- .../src/gen/saas/accounts/v1/audit_pb.ts | 117 +- .../accounts/v1/module_capabilities_pb.ts | 276 ++++- .../code/packages/saas-sdk/package.json | 2 +- .../code/src/gen/saas/accounts/v1/audit_pb.ts | 117 +- .../accounts/v1/module_capabilities_pb.ts | 276 ++++- .../migration_61_publish_domain_event_test.go | 103 ++ ...migration_62_domain_events_xact_id_test.go | 200 ++++ ...audit_journal_user_scoped_publish.down.sql | 99 ++ ...1_audit_journal_user_scoped_publish.up.sql | 146 +++ .../62_domain_events_xact_id.down.sql | 8 + .../62_domain_events_xact_id.up.sql | 59 + .../63_audit_journal_retention.down.sql | 4 + .../63_audit_journal_retention.up.sql | 17 + module/tools/interface-docs-gate.mjs | 1 + 117 files changed, 10779 insertions(+), 1115 deletions(-) create mode 120000 configurations/local/audit-storage.env create mode 100644 module/configurations/local/audit-storage.env create mode 100644 module/contracts/runnables/accounts/connect/DeliverAuditEvents/operation.json create mode 100644 module/contracts/runnables/accounts/connect/DeliverAuditEvents/runnable-package.json create mode 100644 module/docs/adr/0009-audit-store-swap.md create mode 100644 module/services/accounts/code/audit_storage.go create mode 100644 module/services/accounts/code/pkg/adapters/audit_delivery.go create mode 100644 module/services/accounts/code/pkg/adapters/audit_delivery_test.go create mode 100644 module/services/accounts/code/pkg/auditstorage/config.go create mode 100644 module/services/accounts/code/pkg/auditstorage/config_test.go create mode 100644 module/services/accounts/code/pkg/business/audit_delivery.go create mode 100644 module/services/accounts/code/pkg/business/audit_delivery_test.go create mode 100644 module/services/accounts/code/pkg/business/audit_journal.go create mode 100644 module/services/accounts/code/pkg/business/audit_journal_module_test.go create mode 100644 module/services/accounts/code/pkg/business/audit_journal_test.go create mode 100644 module/services/accounts/code/pkg/business/audit_service.go create mode 100644 module/services/accounts/code/pkg/business/audit_service_test.go create mode 100644 module/services/accounts/code/pkg/business/audit_storage.go create mode 100644 module/services/accounts/code/pkg/business/audit_storage_contract_test.go create mode 100644 module/services/accounts/code/pkg/business/audit_storage_test.go create mode 100644 module/services/accounts/code/pkg/business/auditstoragetest/suite.go create mode 100644 module/services/accounts/code/pkg/business/journal_read.go create mode 100644 module/services/accounts/code/pkg/business/journal_read_test.go create mode 100644 module/services/accounts/code/pkg/infra/postgres_audit_delivery_test.go create mode 100644 module/services/accounts/code/pkg/infra/postgres_audit_journal_emitters_test.go create mode 100644 module/services/accounts/code/pkg/infra/postgres_audit_journal_publish_test.go create mode 100644 module/services/accounts/code/pkg/infra/postgres_audit_journal_retention.go create mode 100644 module/services/accounts/code/pkg/infra/postgres_audit_journal_retention_test.go create mode 100644 module/services/accounts/code/pkg/infra/postgres_journal_read.go create mode 100644 module/services/accounts/code/pkg/infra/postgres_journal_read_test.go create mode 100644 module/services/store/code/migration_61_publish_domain_event_test.go create mode 100644 module/services/store/code/migration_62_domain_events_xact_id_test.go create mode 100644 module/services/store/migrations/61_audit_journal_user_scoped_publish.down.sql create mode 100644 module/services/store/migrations/61_audit_journal_user_scoped_publish.up.sql create mode 100644 module/services/store/migrations/62_domain_events_xact_id.down.sql create mode 100644 module/services/store/migrations/62_domain_events_xact_id.up.sql create mode 100644 module/services/store/migrations/63_audit_journal_retention.down.sql create mode 100644 module/services/store/migrations/63_audit_journal_retention.up.sql diff --git a/.gitattributes b/.gitattributes index 61c3cadbd..6cf435c66 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,12 +2,30 @@ # Keep normal whitespace checks everywhere else. module/services/frontend/code/src/gen/** whitespace=-blank-at-eof -# Generated artifacts changed by the telemetry service removal. -# Their owning generators remain responsible for the committed outputs. -module/deployment/generated/** linguist-generated=true +# Dedicated generator outputs; authored inputs and mixed source directories stay visible. +module/contracts/api/** linguist-generated=true module/contracts/runnables/** linguist-generated=true -module/services/telemetry/code/pkg/gen/** linguist-generated=true - -# Build recipes emitted by the pinned Go service agent. +module/deployment/generated/** linguist-generated=true +module/generated/** linguist-generated=true module/services/accounts/builder/** linguist-generated=true +module/services/accounts/code/external/saas-starter/** linguist-generated=true +module/services/accounts/code/pkg/gen/** linguist-generated=true +module/services/accounts/code/python/gen/** linguist-generated=true +module/services/accounts/generated/** linguist-generated=true module/services/auth-gateway/builder/** linguist-generated=true +module/services/auth-gateway/code/pkg/gen/** linguist-generated=true +module/services/auth-gateway/code/external/saas-starter/** linguist-generated=true +module/services/frontend/code/packages/saas-sdk/generated/** linguist-generated=true +module/services/frontend/code/src/gen/** linguist-generated=true +module/services/frontend/code/src/generated/** linguist-generated=true +module/services/frontend/generated/** linguist-generated=true +module/services/marketing/code/src/generated/** linguist-generated=true +module/services/policy-log/builder/** linguist-generated=true +module/services/policy-log/code/pkg/gen/** linguist-generated=true +module/services/telemetry/builder/** linguist-generated=true +module/services/telemetry/code/pkg/gen/** linguist-generated=true + +# Keep dependency locks reviewable even inside exported generator snapshots. +**/buf.lock linguist-generated=false +**/package-lock.json linguist-generated=false +**/go.sum linguist-generated=false diff --git a/MODULE.md b/MODULE.md index 456abb2b1..97349ed1b 100644 --- a/MODULE.md +++ b/MODULE.md @@ -611,6 +611,21 @@ content and disables a plan CTA when the catalog says checkout is unavailable. `UserService`, `APIKeyService`, `AuditService`, `AuditExportService`, `InvitationService`, `WebhookService`, `BillingService`, `PlatformAdminService`, `UsageService`, `SSOAdminService`, `MFAService`, `ConsentService`, `NotificationService`, `OnboardingService`, `GDPRService`, `UserSettingsService`, `AuthService`, `IdentityService`. +### Audit storage + +`AuditService` owns where the audit trail is kept, chosen once at startup from +the `audit-storage` configuration group (`AUDIT_STORAGE_BACKEND = postgres | +warehouse`, plus dataset and table names only). Under `postgres`, the default, +every entry is written to `audit_events` in the caller's transaction. Under +`warehouse`, the entry is published to the journal as the internal event +`saas.audit.recorded.v1` in that transaction (see [EVENTS.md](./module/EVENTS.md)) +and appended to the warehouse by a later task; the activity list and aggregation +read the warehouse, which refuses the percentile metric. A module's audit through +`ModuleEmitAuditEvent` takes the same path. A "platform principal" in these +documents is a module principal with a `cross_tenant` grant. The warehouse backend +is refused at startup until its storage exists in the build. See +[ADR 0009](./module/docs/adr/0009-audit-store-swap.md). + ## Usage metering `UsageService.ConsumeUsage` is an internal-only protobuf API for trusted product diff --git a/configurations/local/audit-storage.env b/configurations/local/audit-storage.env new file mode 120000 index 000000000..6d9a19cdb --- /dev/null +++ b/configurations/local/audit-storage.env @@ -0,0 +1 @@ +../../module/configurations/local/audit-storage.env \ No newline at end of file diff --git a/libraries/source-read-sdk/contract/catalog.codefly.json b/libraries/source-read-sdk/contract/catalog.codefly.json index bfbc830ca..a37753bd5 100644 --- a/libraries/source-read-sdk/contract/catalog.codefly.json +++ b/libraries/source-read-sdk/contract/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contract.binpb", - "digest": "sha256:30bfd77c270428933734b6ae35f5cca165db62fbe181c3f7717a663b1aa0ed73", + "digest": "sha256:06734c27a0cee5a6706f13e2776d72b8f61888b2a54b07cfc0e1e79cdffad7a8", "services": [ { "name": "ModuleCapabilitiesService", @@ -43,6 +43,7 @@ "/saas.accounts.v1.ModuleCapabilitiesService/NotifyUser", "/saas.accounts.v1.ModuleCapabilitiesService/PlaceRecord", "/saas.accounts.v1.ModuleCapabilitiesService/PublishEvent", + "/saas.accounts.v1.ModuleCapabilitiesService/ReadJournal", "/saas.accounts.v1.ModuleCapabilitiesService/ReplayEvents", "/saas.accounts.v1.ModuleCapabilitiesService/RequestApproval", "/saas.accounts.v1.ModuleCapabilitiesService/ResolveWorkContextAuthority", diff --git a/libraries/source-read-sdk/contract/contract.binpb b/libraries/source-read-sdk/contract/contract.binpb index 0ce03e21ecaef4fa5f9c6c82d19bb7c9f4ad0d2d..d230c1660b3442ff9c7029a7027a17a72b76a6f1 100644 GIT binary patch delta 32423 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 delta 27954 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

GackAB4;2UA>(wbX%F3>N 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 O<$pZ!_+K7Rcl!TQuw1VI diff --git a/libraries/source-read-sdk/go/accounts_facade.pb.go b/libraries/source-read-sdk/go/accounts_facade.pb.go index 9b4400b27..a7994ee5b 100644 --- a/libraries/source-read-sdk/go/accounts_facade.pb.go +++ b/libraries/source-read-sdk/go/accounts_facade.pb.go @@ -201,6 +201,14 @@ func (a *ModuleCapabilitiesClient) EmitAuditEvent(ctx context.Context, req *v1.M return resp.Msg, nil } +func (a *ModuleCapabilitiesClient) ReadJournal(ctx context.Context, req *v1.ModuleReadJournalRequest) (*v1.ModuleReadJournalResponse, error) { + resp, err := a.inner.ReadJournal(ctx, connect.NewRequest(req)) + if err != nil { + return nil, err + } + return resp.Msg, nil +} + func (a *ModuleCapabilitiesClient) DeclareAuditEventTypes(ctx context.Context, req *v1.ModuleDeclareAuditEventTypesRequest) (*v1.ModuleDeclareAuditEventTypesResponse, error) { resp, err := a.inner.DeclareAuditEventTypes(ctx, connect.NewRequest(req)) if err != nil { diff --git a/libraries/source-read-sdk/go/gen/saas/accounts/v1/accountsv1connect/module_capabilities.connect.go b/libraries/source-read-sdk/go/gen/saas/accounts/v1/accountsv1connect/module_capabilities.connect.go index dd98e414c..8fffb70ea 100644 --- a/libraries/source-read-sdk/go/gen/saas/accounts/v1/accountsv1connect/module_capabilities.connect.go +++ b/libraries/source-read-sdk/go/gen/saas/accounts/v1/accountsv1connect/module_capabilities.connect.go @@ -96,6 +96,9 @@ const ( // ModuleCapabilitiesServiceEmitAuditEventProcedure is the fully-qualified name of the // ModuleCapabilitiesService's EmitAuditEvent RPC. ModuleCapabilitiesServiceEmitAuditEventProcedure = "/saas.accounts.v1.ModuleCapabilitiesService/EmitAuditEvent" + // ModuleCapabilitiesServiceReadJournalProcedure is the fully-qualified name of the + // ModuleCapabilitiesService's ReadJournal RPC. + ModuleCapabilitiesServiceReadJournalProcedure = "/saas.accounts.v1.ModuleCapabilitiesService/ReadJournal" // ModuleCapabilitiesServiceDeclareAuditEventTypesProcedure is the fully-qualified name of the // ModuleCapabilitiesService's DeclareAuditEventTypes RPC. ModuleCapabilitiesServiceDeclareAuditEventTypesProcedure = "/saas.accounts.v1.ModuleCapabilitiesService/DeclareAuditEventTypes" @@ -201,6 +204,11 @@ type ModuleCapabilitiesServiceClient interface { CancelApproval(context.Context, *connect.Request[v1.ModuleCancelApprovalRequest]) (*connect.Response[emptypb.Empty], error) // EmitAuditEvent records a registered audit event on the tenant's spine. EmitAuditEvent(context.Context, *connect.Request[v1.ModuleEmitAuditEventRequest]) (*connect.Response[emptypb.Empty], error) + // ReadJournal serves the platform journal across every tenant, and the rows with + // no tenant, to a module principal whose grant is cross_tenant. Rows are served + // in the order their writing transactions are final, so a reader that resumes + // from next_after skips nothing; it enqueues nothing and is not a replay. + ReadJournal(context.Context, *connect.Request[v1.ModuleReadJournalRequest]) (*connect.Response[v1.ModuleReadJournalResponse], error) // DeclareAuditEventTypes admits the audit event types a composed module owns, // into the namespaces the operator bound to it; see the request. DeclareAuditEventTypes(context.Context, *connect.Request[v1.ModuleDeclareAuditEventTypesRequest]) (*connect.Response[v1.ModuleDeclareAuditEventTypesResponse], error) @@ -402,6 +410,12 @@ func NewModuleCapabilitiesServiceClient(httpClient connect.HTTPClient, baseURL s connect.WithSchema(moduleCapabilitiesServiceMethods.ByName("EmitAuditEvent")), connect.WithClientOptions(opts...), ), + readJournal: connect.NewClient[v1.ModuleReadJournalRequest, v1.ModuleReadJournalResponse]( + httpClient, + baseURL+ModuleCapabilitiesServiceReadJournalProcedure, + connect.WithSchema(moduleCapabilitiesServiceMethods.ByName("ReadJournal")), + connect.WithClientOptions(opts...), + ), declareAuditEventTypes: connect.NewClient[v1.ModuleDeclareAuditEventTypesRequest, v1.ModuleDeclareAuditEventTypesResponse]( httpClient, baseURL+ModuleCapabilitiesServiceDeclareAuditEventTypesProcedure, @@ -505,6 +519,7 @@ type moduleCapabilitiesServiceClient struct { getApproval *connect.Client[v1.ModuleGetApprovalRequest, v1.ModuleApproval] cancelApproval *connect.Client[v1.ModuleCancelApprovalRequest, emptypb.Empty] emitAuditEvent *connect.Client[v1.ModuleEmitAuditEventRequest, emptypb.Empty] + readJournal *connect.Client[v1.ModuleReadJournalRequest, v1.ModuleReadJournalResponse] declareAuditEventTypes *connect.Client[v1.ModuleDeclareAuditEventTypesRequest, v1.ModuleDeclareAuditEventTypesResponse] listSubjectVisibility *connect.Client[v1.ModuleListSubjectVisibilityRequest, v1.ModuleListSubjectVisibilityResponse] fetchDatasourceBlob *connect.Client[v1.FetchDatasourceBlobRequest, v1.FetchDatasourceBlobChunk] @@ -627,6 +642,11 @@ func (c *moduleCapabilitiesServiceClient) EmitAuditEvent(ctx context.Context, re return c.emitAuditEvent.CallUnary(ctx, req) } +// ReadJournal calls saas.accounts.v1.ModuleCapabilitiesService.ReadJournal. +func (c *moduleCapabilitiesServiceClient) ReadJournal(ctx context.Context, req *connect.Request[v1.ModuleReadJournalRequest]) (*connect.Response[v1.ModuleReadJournalResponse], error) { + return c.readJournal.CallUnary(ctx, req) +} + // DeclareAuditEventTypes calls saas.accounts.v1.ModuleCapabilitiesService.DeclareAuditEventTypes. func (c *moduleCapabilitiesServiceClient) DeclareAuditEventTypes(ctx context.Context, req *connect.Request[v1.ModuleDeclareAuditEventTypesRequest]) (*connect.Response[v1.ModuleDeclareAuditEventTypesResponse], error) { return c.declareAuditEventTypes.CallUnary(ctx, req) @@ -761,6 +781,11 @@ type ModuleCapabilitiesServiceHandler interface { CancelApproval(context.Context, *connect.Request[v1.ModuleCancelApprovalRequest]) (*connect.Response[emptypb.Empty], error) // EmitAuditEvent records a registered audit event on the tenant's spine. EmitAuditEvent(context.Context, *connect.Request[v1.ModuleEmitAuditEventRequest]) (*connect.Response[emptypb.Empty], error) + // ReadJournal serves the platform journal across every tenant, and the rows with + // no tenant, to a module principal whose grant is cross_tenant. Rows are served + // in the order their writing transactions are final, so a reader that resumes + // from next_after skips nothing; it enqueues nothing and is not a replay. + ReadJournal(context.Context, *connect.Request[v1.ModuleReadJournalRequest]) (*connect.Response[v1.ModuleReadJournalResponse], error) // DeclareAuditEventTypes admits the audit event types a composed module owns, // into the namespaces the operator bound to it; see the request. DeclareAuditEventTypes(context.Context, *connect.Request[v1.ModuleDeclareAuditEventTypesRequest]) (*connect.Response[v1.ModuleDeclareAuditEventTypesResponse], error) @@ -958,6 +983,12 @@ func NewModuleCapabilitiesServiceHandler(svc ModuleCapabilitiesServiceHandler, o connect.WithSchema(moduleCapabilitiesServiceMethods.ByName("EmitAuditEvent")), connect.WithHandlerOptions(opts...), ) + moduleCapabilitiesServiceReadJournalHandler := connect.NewUnaryHandler( + ModuleCapabilitiesServiceReadJournalProcedure, + svc.ReadJournal, + connect.WithSchema(moduleCapabilitiesServiceMethods.ByName("ReadJournal")), + connect.WithHandlerOptions(opts...), + ) moduleCapabilitiesServiceDeclareAuditEventTypesHandler := connect.NewUnaryHandler( ModuleCapabilitiesServiceDeclareAuditEventTypesProcedure, svc.DeclareAuditEventTypes, @@ -1078,6 +1109,8 @@ func NewModuleCapabilitiesServiceHandler(svc ModuleCapabilitiesServiceHandler, o moduleCapabilitiesServiceCancelApprovalHandler.ServeHTTP(w, r) case ModuleCapabilitiesServiceEmitAuditEventProcedure: moduleCapabilitiesServiceEmitAuditEventHandler.ServeHTTP(w, r) + case ModuleCapabilitiesServiceReadJournalProcedure: + moduleCapabilitiesServiceReadJournalHandler.ServeHTTP(w, r) case ModuleCapabilitiesServiceDeclareAuditEventTypesProcedure: moduleCapabilitiesServiceDeclareAuditEventTypesHandler.ServeHTTP(w, r) case ModuleCapabilitiesServiceListSubjectVisibilityProcedure: @@ -1193,6 +1226,10 @@ func (UnimplementedModuleCapabilitiesServiceHandler) EmitAuditEvent(context.Cont return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.ModuleCapabilitiesService.EmitAuditEvent is not implemented")) } +func (UnimplementedModuleCapabilitiesServiceHandler) ReadJournal(context.Context, *connect.Request[v1.ModuleReadJournalRequest]) (*connect.Response[v1.ModuleReadJournalResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.ModuleCapabilitiesService.ReadJournal is not implemented")) +} + func (UnimplementedModuleCapabilitiesServiceHandler) DeclareAuditEventTypes(context.Context, *connect.Request[v1.ModuleDeclareAuditEventTypesRequest]) (*connect.Response[v1.ModuleDeclareAuditEventTypesResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.ModuleCapabilitiesService.DeclareAuditEventTypes is not implemented")) } diff --git a/libraries/source-read-sdk/go/gen/saas/accounts/v1/module_capabilities.pb.go b/libraries/source-read-sdk/go/gen/saas/accounts/v1/module_capabilities.pb.go index 34cdcf24a..39869877c 100644 --- a/libraries/source-read-sdk/go/gen/saas/accounts/v1/module_capabilities.pb.go +++ b/libraries/source-read-sdk/go/gen/saas/accounts/v1/module_capabilities.pb.go @@ -2039,6 +2039,252 @@ func (x *ModuleEmitAuditEventRequest) GetIdempotencyKey() string { return "" } +// ModuleReadJournalRequest reads the platform journal across every tenant. Only a +// module principal whose grant is cross_tenant may call it, the gate a +// system-scoped EmitAuditEvent has; any other caller is PermissionDenied. +type ModuleReadJournalRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // after is the position the previous page ended at (a page's next_after). The + // literal "start" is a valid position meaning "before the oldest retained + // entry", and the empty string is accepted as an alias of it; so a reader with + // nothing read yet has a non-empty position to correlate its first page on. + // Positions are opaque: store one and hand it back unread. No entry's position + // is ever "start". A value this host did not issue is InvalidArgument. + After string `protobuf:"bytes,1,opt,name=after,proto3" json:"after,omitempty"` + // limit is the page size: 0 is 100, and more than 500 is served as 500. A + // page is also bounded to 2 MiB of event data, though the first entry is always + // served. + Limit int32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` + // types are the event types to read, exactly (no pattern). At least one is + // required, so no caller reads every tenant's every event by leaving the + // filter out. + Types []string `protobuf:"bytes,3,rep,name=types,proto3" json:"types,omitempty"` + // delivery_bindings are the binding ids the caller's own delivery of the journal + // runs under. They decide which entries are marked delivery_machinery. + DeliveryBindings []string `protobuf:"bytes,4,rep,name=delivery_bindings,json=deliveryBindings,proto3" json:"delivery_bindings,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModuleReadJournalRequest) Reset() { + *x = ModuleReadJournalRequest{} + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModuleReadJournalRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModuleReadJournalRequest) ProtoMessage() {} + +func (x *ModuleReadJournalRequest) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[20] + 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 ModuleReadJournalRequest.ProtoReflect.Descriptor instead. +func (*ModuleReadJournalRequest) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{20} +} + +func (x *ModuleReadJournalRequest) GetAfter() string { + if x != nil { + return x.After + } + return "" +} + +func (x *ModuleReadJournalRequest) GetLimit() int32 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *ModuleReadJournalRequest) GetTypes() []string { + if x != nil { + return x.Types + } + return nil +} + +func (x *ModuleReadJournalRequest) GetDeliveryBindings() []string { + if x != nil { + return x.DeliveryBindings + } + return nil +} + +// ModuleJournalEntry is one journal row. Rows are served in the order their +// writing transactions are final (see ModuleReadJournalRequest), so a reader that +// resumes from the last position it was given skips nothing. +type ModuleJournalEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + // position is where the row sits in the read order: opaque, and the value a + // reader passes as after to resume past it. + Position string `protobuf:"bytes,1,opt,name=position,proto3" json:"position,omitempty"` + EventId string `protobuf:"bytes,2,opt,name=event_id,json=eventId,proto3" json:"event_id,omitempty"` + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + // organization_id is the row's tenant, empty for a row that has none. + OrganizationId string `protobuf:"bytes,4,opt,name=organization_id,json=organizationId,proto3" json:"organization_id,omitempty"` + OccurredAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=occurred_at,json=occurredAt,proto3" json:"occurred_at,omitempty"` + // data is the event's JSON object; empty when the event's data is not one. + Data *structpb.Struct `protobuf:"bytes,6,opt,name=data,proto3" json:"data,omitempty"` + // delivery_machinery is true when the entry the row carries is the footprint of + // the caller's own delivery of the journal: the host computes it, so a caller + // never parses this module's payloads. It is true for a + // saas.module.operation_context_minted or + // saas.module.delegated_audience_exchange entry whose binding_id is one of the + // request's delivery_bindings, and for a saas.module.work_context_minted entry + // of the caller's own module principal. + DeliveryMachinery bool `protobuf:"varint,7,opt,name=delivery_machinery,json=deliveryMachinery,proto3" json:"delivery_machinery,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModuleJournalEntry) Reset() { + *x = ModuleJournalEntry{} + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModuleJournalEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModuleJournalEntry) ProtoMessage() {} + +func (x *ModuleJournalEntry) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[21] + 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 ModuleJournalEntry.ProtoReflect.Descriptor instead. +func (*ModuleJournalEntry) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{21} +} + +func (x *ModuleJournalEntry) GetPosition() string { + if x != nil { + return x.Position + } + return "" +} + +func (x *ModuleJournalEntry) GetEventId() string { + if x != nil { + return x.EventId + } + return "" +} + +func (x *ModuleJournalEntry) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *ModuleJournalEntry) GetOrganizationId() string { + if x != nil { + return x.OrganizationId + } + return "" +} + +func (x *ModuleJournalEntry) GetOccurredAt() *timestamppb.Timestamp { + if x != nil { + return x.OccurredAt + } + return nil +} + +func (x *ModuleJournalEntry) GetData() *structpb.Struct { + if x != nil { + return x.Data + } + return nil +} + +func (x *ModuleJournalEntry) GetDeliveryMachinery() bool { + if x != nil { + return x.DeliveryMachinery + } + return false +} + +// ModuleReadJournalResponse is one page and where to resume. +type ModuleReadJournalResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Entries []*ModuleJournalEntry `protobuf:"bytes,1,rep,name=entries,proto3" json:"entries,omitempty"` + // next_after is the last entry's position, or the request's after when the page + // is empty. + NextAfter string `protobuf:"bytes,2,opt,name=next_after,json=nextAfter,proto3" json:"next_after,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModuleReadJournalResponse) Reset() { + *x = ModuleReadJournalResponse{} + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModuleReadJournalResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModuleReadJournalResponse) ProtoMessage() {} + +func (x *ModuleReadJournalResponse) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[22] + 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 ModuleReadJournalResponse.ProtoReflect.Descriptor instead. +func (*ModuleReadJournalResponse) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{22} +} + +func (x *ModuleReadJournalResponse) GetEntries() []*ModuleJournalEntry { + if x != nil { + return x.Entries + } + return nil +} + +func (x *ModuleReadJournalResponse) GetNextAfter() string { + if x != nil { + return x.NextAfter + } + return "" +} + // ModuleAuditFieldDeclaration is one payload field of a declared audit event // type. `solution` is stamped by the host from the emitting scope and may not // be declared. @@ -2058,7 +2304,7 @@ type ModuleAuditFieldDeclaration struct { func (x *ModuleAuditFieldDeclaration) Reset() { *x = ModuleAuditFieldDeclaration{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[20] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2070,7 +2316,7 @@ func (x *ModuleAuditFieldDeclaration) String() string { func (*ModuleAuditFieldDeclaration) ProtoMessage() {} func (x *ModuleAuditFieldDeclaration) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[20] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2083,7 +2329,7 @@ func (x *ModuleAuditFieldDeclaration) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleAuditFieldDeclaration.ProtoReflect.Descriptor instead. func (*ModuleAuditFieldDeclaration) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{20} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{23} } func (x *ModuleAuditFieldDeclaration) GetName() string { @@ -2136,7 +2382,7 @@ type ModuleAuditEventTypeDeclaration struct { func (x *ModuleAuditEventTypeDeclaration) Reset() { *x = ModuleAuditEventTypeDeclaration{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[21] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2148,7 +2394,7 @@ func (x *ModuleAuditEventTypeDeclaration) String() string { func (*ModuleAuditEventTypeDeclaration) ProtoMessage() {} func (x *ModuleAuditEventTypeDeclaration) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[21] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2161,7 +2407,7 @@ func (x *ModuleAuditEventTypeDeclaration) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleAuditEventTypeDeclaration.ProtoReflect.Descriptor instead. func (*ModuleAuditEventTypeDeclaration) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{21} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{24} } func (x *ModuleAuditEventTypeDeclaration) GetType() string { @@ -2211,7 +2457,7 @@ type ModuleDeclareAuditEventTypesRequest struct { func (x *ModuleDeclareAuditEventTypesRequest) Reset() { *x = ModuleDeclareAuditEventTypesRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[22] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2223,7 +2469,7 @@ func (x *ModuleDeclareAuditEventTypesRequest) String() string { func (*ModuleDeclareAuditEventTypesRequest) ProtoMessage() {} func (x *ModuleDeclareAuditEventTypesRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[22] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2236,7 +2482,7 @@ func (x *ModuleDeclareAuditEventTypesRequest) ProtoReflect() protoreflect.Messag // Deprecated: Use ModuleDeclareAuditEventTypesRequest.ProtoReflect.Descriptor instead. func (*ModuleDeclareAuditEventTypesRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{22} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{25} } func (x *ModuleDeclareAuditEventTypesRequest) GetPrefix() string { @@ -2266,7 +2512,7 @@ type ModuleDeclareAuditEventTypesResponse struct { func (x *ModuleDeclareAuditEventTypesResponse) Reset() { *x = ModuleDeclareAuditEventTypesResponse{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[23] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2278,7 +2524,7 @@ func (x *ModuleDeclareAuditEventTypesResponse) String() string { func (*ModuleDeclareAuditEventTypesResponse) ProtoMessage() {} func (x *ModuleDeclareAuditEventTypesResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[23] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2291,7 +2537,7 @@ func (x *ModuleDeclareAuditEventTypesResponse) ProtoReflect() protoreflect.Messa // Deprecated: Use ModuleDeclareAuditEventTypesResponse.ProtoReflect.Descriptor instead. func (*ModuleDeclareAuditEventTypesResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{23} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{26} } func (x *ModuleDeclareAuditEventTypesResponse) GetAdmittedTypes() []string { @@ -2325,7 +2571,7 @@ type SubjectVisibilityGrant struct { func (x *SubjectVisibilityGrant) Reset() { *x = SubjectVisibilityGrant{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[24] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2337,7 +2583,7 @@ func (x *SubjectVisibilityGrant) String() string { func (*SubjectVisibilityGrant) ProtoMessage() {} func (x *SubjectVisibilityGrant) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[24] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2350,7 +2596,7 @@ func (x *SubjectVisibilityGrant) ProtoReflect() protoreflect.Message { // Deprecated: Use SubjectVisibilityGrant.ProtoReflect.Descriptor instead. func (*SubjectVisibilityGrant) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{24} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{27} } func (x *SubjectVisibilityGrant) GetVisibleSubjectId() string { @@ -2388,7 +2634,7 @@ type ModuleListSubjectVisibilityRequest struct { func (x *ModuleListSubjectVisibilityRequest) Reset() { *x = ModuleListSubjectVisibilityRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[25] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2400,7 +2646,7 @@ func (x *ModuleListSubjectVisibilityRequest) String() string { func (*ModuleListSubjectVisibilityRequest) ProtoMessage() {} func (x *ModuleListSubjectVisibilityRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[25] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2413,7 +2659,7 @@ func (x *ModuleListSubjectVisibilityRequest) ProtoReflect() protoreflect.Message // Deprecated: Use ModuleListSubjectVisibilityRequest.ProtoReflect.Descriptor instead. func (*ModuleListSubjectVisibilityRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{25} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{28} } func (x *ModuleListSubjectVisibilityRequest) GetTenant() string { @@ -2452,7 +2698,7 @@ type ModuleListSubjectVisibilityResponse struct { func (x *ModuleListSubjectVisibilityResponse) Reset() { *x = ModuleListSubjectVisibilityResponse{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[26] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2464,7 +2710,7 @@ func (x *ModuleListSubjectVisibilityResponse) String() string { func (*ModuleListSubjectVisibilityResponse) ProtoMessage() {} func (x *ModuleListSubjectVisibilityResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[26] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2477,7 +2723,7 @@ func (x *ModuleListSubjectVisibilityResponse) ProtoReflect() protoreflect.Messag // Deprecated: Use ModuleListSubjectVisibilityResponse.ProtoReflect.Descriptor instead. func (*ModuleListSubjectVisibilityResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{26} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{29} } func (x *ModuleListSubjectVisibilityResponse) GetGrants() []*SubjectVisibilityGrant { @@ -2505,7 +2751,7 @@ type FetchDatasourceBlobRequest struct { func (x *FetchDatasourceBlobRequest) Reset() { *x = FetchDatasourceBlobRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[27] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2517,7 +2763,7 @@ func (x *FetchDatasourceBlobRequest) String() string { func (*FetchDatasourceBlobRequest) ProtoMessage() {} func (x *FetchDatasourceBlobRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[27] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2530,7 +2776,7 @@ func (x *FetchDatasourceBlobRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FetchDatasourceBlobRequest.ProtoReflect.Descriptor instead. func (*FetchDatasourceBlobRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{27} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{30} } func (x *FetchDatasourceBlobRequest) GetSourceId() string { @@ -2562,7 +2808,7 @@ type FetchDatasourceBlobChunk struct { func (x *FetchDatasourceBlobChunk) Reset() { *x = FetchDatasourceBlobChunk{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[28] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2574,7 +2820,7 @@ func (x *FetchDatasourceBlobChunk) String() string { func (*FetchDatasourceBlobChunk) ProtoMessage() {} func (x *FetchDatasourceBlobChunk) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[28] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2587,7 +2833,7 @@ func (x *FetchDatasourceBlobChunk) ProtoReflect() protoreflect.Message { // Deprecated: Use FetchDatasourceBlobChunk.ProtoReflect.Descriptor instead. func (*FetchDatasourceBlobChunk) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{28} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{31} } func (x *FetchDatasourceBlobChunk) GetData() []byte { @@ -2625,7 +2871,7 @@ type DatasourceFileRef struct { func (x *DatasourceFileRef) Reset() { *x = DatasourceFileRef{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[29] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2637,7 +2883,7 @@ func (x *DatasourceFileRef) String() string { func (*DatasourceFileRef) ProtoMessage() {} func (x *DatasourceFileRef) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[29] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2650,7 +2896,7 @@ func (x *DatasourceFileRef) ProtoReflect() protoreflect.Message { // Deprecated: Use DatasourceFileRef.ProtoReflect.Descriptor instead. func (*DatasourceFileRef) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{29} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{32} } func (x *DatasourceFileRef) GetItemId() string { @@ -2686,7 +2932,7 @@ type FetchDatasourceFilesRequest struct { func (x *FetchDatasourceFilesRequest) Reset() { *x = FetchDatasourceFilesRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[30] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2698,7 +2944,7 @@ func (x *FetchDatasourceFilesRequest) String() string { func (*FetchDatasourceFilesRequest) ProtoMessage() {} func (x *FetchDatasourceFilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[30] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2711,7 +2957,7 @@ func (x *FetchDatasourceFilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FetchDatasourceFilesRequest.ProtoReflect.Descriptor instead. func (*FetchDatasourceFilesRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{30} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{33} } func (x *FetchDatasourceFilesRequest) GetSourceId() string { @@ -2753,7 +2999,7 @@ type DatasourceProvenance struct { func (x *DatasourceProvenance) Reset() { *x = DatasourceProvenance{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[31] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2765,7 +3011,7 @@ func (x *DatasourceProvenance) String() string { func (*DatasourceProvenance) ProtoMessage() {} func (x *DatasourceProvenance) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[31] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2778,7 +3024,7 @@ func (x *DatasourceProvenance) ProtoReflect() protoreflect.Message { // Deprecated: Use DatasourceProvenance.ProtoReflect.Descriptor instead. func (*DatasourceProvenance) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{31} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{34} } func (x *DatasourceProvenance) GetSourceId() string { @@ -2844,7 +3090,7 @@ type DatasourceItemReaders struct { func (x *DatasourceItemReaders) Reset() { *x = DatasourceItemReaders{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[32] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2856,7 +3102,7 @@ func (x *DatasourceItemReaders) String() string { func (*DatasourceItemReaders) ProtoMessage() {} func (x *DatasourceItemReaders) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[32] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2869,7 +3115,7 @@ func (x *DatasourceItemReaders) ProtoReflect() protoreflect.Message { // Deprecated: Use DatasourceItemReaders.ProtoReflect.Descriptor instead. func (*DatasourceItemReaders) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{32} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{35} } func (x *DatasourceItemReaders) GetBasis() DatasourceItemReadersBasis { @@ -2930,7 +3176,7 @@ type DatasourceFileHeader struct { func (x *DatasourceFileHeader) Reset() { *x = DatasourceFileHeader{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[33] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2942,7 +3188,7 @@ func (x *DatasourceFileHeader) String() string { func (*DatasourceFileHeader) ProtoMessage() {} func (x *DatasourceFileHeader) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[33] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2955,7 +3201,7 @@ func (x *DatasourceFileHeader) ProtoReflect() protoreflect.Message { // Deprecated: Use DatasourceFileHeader.ProtoReflect.Descriptor instead. func (*DatasourceFileHeader) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{33} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{36} } func (x *DatasourceFileHeader) GetProvenance() *DatasourceProvenance { @@ -3009,7 +3255,7 @@ type FetchDatasourceFilesFrame struct { func (x *FetchDatasourceFilesFrame) Reset() { *x = FetchDatasourceFilesFrame{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[34] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3021,7 +3267,7 @@ func (x *FetchDatasourceFilesFrame) String() string { func (*FetchDatasourceFilesFrame) ProtoMessage() {} func (x *FetchDatasourceFilesFrame) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[34] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3034,7 +3280,7 @@ func (x *FetchDatasourceFilesFrame) ProtoReflect() protoreflect.Message { // Deprecated: Use FetchDatasourceFilesFrame.ProtoReflect.Descriptor instead. func (*FetchDatasourceFilesFrame) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{34} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{37} } func (x *FetchDatasourceFilesFrame) GetFrame() isFetchDatasourceFilesFrame_Frame { @@ -3093,7 +3339,7 @@ type ModulePublishEventRequest struct { func (x *ModulePublishEventRequest) Reset() { *x = ModulePublishEventRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[35] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3105,7 +3351,7 @@ func (x *ModulePublishEventRequest) String() string { func (*ModulePublishEventRequest) ProtoMessage() {} func (x *ModulePublishEventRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[35] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3118,7 +3364,7 @@ func (x *ModulePublishEventRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ModulePublishEventRequest.ProtoReflect.Descriptor instead. func (*ModulePublishEventRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{35} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{38} } func (x *ModulePublishEventRequest) GetTenant() string { @@ -3148,7 +3394,7 @@ type ModulePublishEventResponse struct { func (x *ModulePublishEventResponse) Reset() { *x = ModulePublishEventResponse{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[36] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3160,7 +3406,7 @@ func (x *ModulePublishEventResponse) String() string { func (*ModulePublishEventResponse) ProtoMessage() {} func (x *ModulePublishEventResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[36] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3173,7 +3419,7 @@ func (x *ModulePublishEventResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ModulePublishEventResponse.ProtoReflect.Descriptor instead. func (*ModulePublishEventResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{36} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{39} } func (x *ModulePublishEventResponse) GetEventId() string { @@ -3199,7 +3445,7 @@ type ModuleSubscribeRequest struct { func (x *ModuleSubscribeRequest) Reset() { *x = ModuleSubscribeRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[37] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3211,7 +3457,7 @@ func (x *ModuleSubscribeRequest) String() string { func (*ModuleSubscribeRequest) ProtoMessage() {} func (x *ModuleSubscribeRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[37] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3224,7 +3470,7 @@ func (x *ModuleSubscribeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleSubscribeRequest.ProtoReflect.Descriptor instead. func (*ModuleSubscribeRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{37} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{40} } func (x *ModuleSubscribeRequest) GetTypePattern() string { @@ -3263,7 +3509,7 @@ type ModuleSubscription struct { func (x *ModuleSubscription) Reset() { *x = ModuleSubscription{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[38] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3275,7 +3521,7 @@ func (x *ModuleSubscription) String() string { func (*ModuleSubscription) ProtoMessage() {} func (x *ModuleSubscription) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[38] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3288,7 +3534,7 @@ func (x *ModuleSubscription) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleSubscription.ProtoReflect.Descriptor instead. func (*ModuleSubscription) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{38} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{41} } func (x *ModuleSubscription) GetId() string { @@ -3342,7 +3588,7 @@ type ModuleSubscribeResponse struct { func (x *ModuleSubscribeResponse) Reset() { *x = ModuleSubscribeResponse{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[39] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3354,7 +3600,7 @@ func (x *ModuleSubscribeResponse) String() string { func (*ModuleSubscribeResponse) ProtoMessage() {} func (x *ModuleSubscribeResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[39] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3367,7 +3613,7 @@ func (x *ModuleSubscribeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleSubscribeResponse.ProtoReflect.Descriptor instead. func (*ModuleSubscribeResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{39} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{42} } func (x *ModuleSubscribeResponse) GetSubscription() *ModuleSubscription { @@ -3387,7 +3633,7 @@ type ModuleUnsubscribeRequest struct { func (x *ModuleUnsubscribeRequest) Reset() { *x = ModuleUnsubscribeRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[40] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3399,7 +3645,7 @@ func (x *ModuleUnsubscribeRequest) String() string { func (*ModuleUnsubscribeRequest) ProtoMessage() {} func (x *ModuleUnsubscribeRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[40] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3412,7 +3658,7 @@ func (x *ModuleUnsubscribeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleUnsubscribeRequest.ProtoReflect.Descriptor instead. func (*ModuleUnsubscribeRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{40} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{43} } func (x *ModuleUnsubscribeRequest) GetSubscriptionId() string { @@ -3432,7 +3678,7 @@ type ModuleListSubscriptionsRequest struct { func (x *ModuleListSubscriptionsRequest) Reset() { *x = ModuleListSubscriptionsRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[41] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3444,7 +3690,7 @@ func (x *ModuleListSubscriptionsRequest) String() string { func (*ModuleListSubscriptionsRequest) ProtoMessage() {} func (x *ModuleListSubscriptionsRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[41] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3457,7 +3703,7 @@ func (x *ModuleListSubscriptionsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleListSubscriptionsRequest.ProtoReflect.Descriptor instead. func (*ModuleListSubscriptionsRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{41} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{44} } type ModuleListSubscriptionsResponse struct { @@ -3469,7 +3715,7 @@ type ModuleListSubscriptionsResponse struct { func (x *ModuleListSubscriptionsResponse) Reset() { *x = ModuleListSubscriptionsResponse{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[42] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3481,7 +3727,7 @@ func (x *ModuleListSubscriptionsResponse) String() string { func (*ModuleListSubscriptionsResponse) ProtoMessage() {} func (x *ModuleListSubscriptionsResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[42] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3494,7 +3740,7 @@ func (x *ModuleListSubscriptionsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleListSubscriptionsResponse.ProtoReflect.Descriptor instead. func (*ModuleListSubscriptionsResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{42} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{45} } func (x *ModuleListSubscriptionsResponse) GetSubscriptions() []*ModuleSubscription { @@ -3519,7 +3765,7 @@ type ModuleReplayEventsRequest struct { func (x *ModuleReplayEventsRequest) Reset() { *x = ModuleReplayEventsRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[43] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3531,7 +3777,7 @@ func (x *ModuleReplayEventsRequest) String() string { func (*ModuleReplayEventsRequest) ProtoMessage() {} func (x *ModuleReplayEventsRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[43] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3544,7 +3790,7 @@ func (x *ModuleReplayEventsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleReplayEventsRequest.ProtoReflect.Descriptor instead. func (*ModuleReplayEventsRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{43} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{46} } func (x *ModuleReplayEventsRequest) GetTenant() string { @@ -3579,7 +3825,7 @@ type ModuleReplayEventsResponse struct { func (x *ModuleReplayEventsResponse) Reset() { *x = ModuleReplayEventsResponse{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[44] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3591,7 +3837,7 @@ func (x *ModuleReplayEventsResponse) String() string { func (*ModuleReplayEventsResponse) ProtoMessage() {} func (x *ModuleReplayEventsResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[44] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3604,7 +3850,7 @@ func (x *ModuleReplayEventsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleReplayEventsResponse.ProtoReflect.Descriptor instead. func (*ModuleReplayEventsResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{44} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{47} } func (x *ModuleReplayEventsResponse) GetRedelivered() int32 { @@ -3649,7 +3895,7 @@ type ReadableSourceCollection struct { func (x *ReadableSourceCollection) Reset() { *x = ReadableSourceCollection{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[45] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3661,7 +3907,7 @@ func (x *ReadableSourceCollection) String() string { func (*ReadableSourceCollection) ProtoMessage() {} func (x *ReadableSourceCollection) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[45] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3674,7 +3920,7 @@ func (x *ReadableSourceCollection) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadableSourceCollection.ProtoReflect.Descriptor instead. func (*ReadableSourceCollection) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{45} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{48} } func (x *ReadableSourceCollection) GetSourceId() string { @@ -3768,7 +4014,7 @@ type ReadableCollectionGrant struct { func (x *ReadableCollectionGrant) Reset() { *x = ReadableCollectionGrant{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[46] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3780,7 +4026,7 @@ func (x *ReadableCollectionGrant) String() string { func (*ReadableCollectionGrant) ProtoMessage() {} func (x *ReadableCollectionGrant) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[46] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3793,7 +4039,7 @@ func (x *ReadableCollectionGrant) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadableCollectionGrant.ProtoReflect.Descriptor instead. func (*ReadableCollectionGrant) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{46} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{49} } func (x *ReadableCollectionGrant) GetSubjectLabel() string { @@ -3866,7 +4112,7 @@ type CollectionSyncProvenance struct { func (x *CollectionSyncProvenance) Reset() { *x = CollectionSyncProvenance{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[47] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3878,7 +4124,7 @@ func (x *CollectionSyncProvenance) String() string { func (*CollectionSyncProvenance) ProtoMessage() {} func (x *CollectionSyncProvenance) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[47] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3891,7 +4137,7 @@ func (x *CollectionSyncProvenance) ProtoReflect() protoreflect.Message { // Deprecated: Use CollectionSyncProvenance.ProtoReflect.Descriptor instead. func (*CollectionSyncProvenance) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{47} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{50} } func (x *CollectionSyncProvenance) GetStage() SourceSyncStage { @@ -3958,7 +4204,7 @@ type ListReadableSourceCollectionsRequest struct { func (x *ListReadableSourceCollectionsRequest) Reset() { *x = ListReadableSourceCollectionsRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[48] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3970,7 +4216,7 @@ func (x *ListReadableSourceCollectionsRequest) String() string { func (*ListReadableSourceCollectionsRequest) ProtoMessage() {} func (x *ListReadableSourceCollectionsRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[48] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3983,7 +4229,7 @@ func (x *ListReadableSourceCollectionsRequest) ProtoReflect() protoreflect.Messa // Deprecated: Use ListReadableSourceCollectionsRequest.ProtoReflect.Descriptor instead. func (*ListReadableSourceCollectionsRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{48} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{51} } func (x *ListReadableSourceCollectionsRequest) GetPageSize() int32 { @@ -4017,7 +4263,7 @@ type ListReadableSourceCollectionsResponse struct { func (x *ListReadableSourceCollectionsResponse) Reset() { *x = ListReadableSourceCollectionsResponse{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[49] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4029,7 +4275,7 @@ func (x *ListReadableSourceCollectionsResponse) String() string { func (*ListReadableSourceCollectionsResponse) ProtoMessage() {} func (x *ListReadableSourceCollectionsResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[49] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4042,7 +4288,7 @@ func (x *ListReadableSourceCollectionsResponse) ProtoReflect() protoreflect.Mess // Deprecated: Use ListReadableSourceCollectionsResponse.ProtoReflect.Descriptor instead. func (*ListReadableSourceCollectionsResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{49} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{52} } func (x *ListReadableSourceCollectionsResponse) GetCollections() []*ReadableSourceCollection { @@ -4086,7 +4332,7 @@ type ModulePlaceRecordRequest struct { func (x *ModulePlaceRecordRequest) Reset() { *x = ModulePlaceRecordRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[50] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4098,7 +4344,7 @@ func (x *ModulePlaceRecordRequest) String() string { func (*ModulePlaceRecordRequest) ProtoMessage() {} func (x *ModulePlaceRecordRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[50] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4111,7 +4357,7 @@ func (x *ModulePlaceRecordRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ModulePlaceRecordRequest.ProtoReflect.Descriptor instead. func (*ModulePlaceRecordRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{50} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{53} } func (x *ModulePlaceRecordRequest) GetTenant() string { @@ -4170,7 +4416,7 @@ type ModulePlaceRecordResponse struct { func (x *ModulePlaceRecordResponse) Reset() { *x = ModulePlaceRecordResponse{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[51] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4182,7 +4428,7 @@ func (x *ModulePlaceRecordResponse) String() string { func (*ModulePlaceRecordResponse) ProtoMessage() {} func (x *ModulePlaceRecordResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[51] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4195,7 +4441,7 @@ func (x *ModulePlaceRecordResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ModulePlaceRecordResponse.ProtoReflect.Descriptor instead. func (*ModulePlaceRecordResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{51} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{54} } func (x *ModulePlaceRecordResponse) GetNodeId() string { @@ -4222,7 +4468,7 @@ type CheckWorkContextRecordAccessRequest struct { func (x *CheckWorkContextRecordAccessRequest) Reset() { *x = CheckWorkContextRecordAccessRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[52] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4234,7 +4480,7 @@ func (x *CheckWorkContextRecordAccessRequest) String() string { func (*CheckWorkContextRecordAccessRequest) ProtoMessage() {} func (x *CheckWorkContextRecordAccessRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[52] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[55] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4247,7 +4493,7 @@ func (x *CheckWorkContextRecordAccessRequest) ProtoReflect() protoreflect.Messag // Deprecated: Use CheckWorkContextRecordAccessRequest.ProtoReflect.Descriptor instead. func (*CheckWorkContextRecordAccessRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{52} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{55} } func (x *CheckWorkContextRecordAccessRequest) GetResourceType() string { @@ -4290,7 +4536,7 @@ type CheckWorkContextRecordAccessResponse struct { func (x *CheckWorkContextRecordAccessResponse) Reset() { *x = CheckWorkContextRecordAccessResponse{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[53] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4302,7 +4548,7 @@ func (x *CheckWorkContextRecordAccessResponse) String() string { func (*CheckWorkContextRecordAccessResponse) ProtoMessage() {} func (x *CheckWorkContextRecordAccessResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[53] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4315,7 +4561,7 @@ func (x *CheckWorkContextRecordAccessResponse) ProtoReflect() protoreflect.Messa // Deprecated: Use CheckWorkContextRecordAccessResponse.ProtoReflect.Descriptor instead. func (*CheckWorkContextRecordAccessResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{53} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{56} } func (x *CheckWorkContextRecordAccessResponse) GetAllowed() bool { @@ -4344,7 +4590,7 @@ type ModuleExchangeDelegatedReadAudienceRequest struct { func (x *ModuleExchangeDelegatedReadAudienceRequest) Reset() { *x = ModuleExchangeDelegatedReadAudienceRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[54] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4356,7 +4602,7 @@ func (x *ModuleExchangeDelegatedReadAudienceRequest) String() string { func (*ModuleExchangeDelegatedReadAudienceRequest) ProtoMessage() {} func (x *ModuleExchangeDelegatedReadAudienceRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[54] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4369,7 +4615,7 @@ func (x *ModuleExchangeDelegatedReadAudienceRequest) ProtoReflect() protoreflect // Deprecated: Use ModuleExchangeDelegatedReadAudienceRequest.ProtoReflect.Descriptor instead. func (*ModuleExchangeDelegatedReadAudienceRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{54} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{57} } func (x *ModuleExchangeDelegatedReadAudienceRequest) GetBindingId() string { @@ -4425,7 +4671,7 @@ type ModuleExchangeDelegatedOperationAudienceRequest struct { func (x *ModuleExchangeDelegatedOperationAudienceRequest) Reset() { *x = ModuleExchangeDelegatedOperationAudienceRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[55] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4437,7 +4683,7 @@ func (x *ModuleExchangeDelegatedOperationAudienceRequest) String() string { func (*ModuleExchangeDelegatedOperationAudienceRequest) ProtoMessage() {} func (x *ModuleExchangeDelegatedOperationAudienceRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[55] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4450,7 +4696,7 @@ func (x *ModuleExchangeDelegatedOperationAudienceRequest) ProtoReflect() protore // Deprecated: Use ModuleExchangeDelegatedOperationAudienceRequest.ProtoReflect.Descriptor instead. func (*ModuleExchangeDelegatedOperationAudienceRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{55} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{58} } func (x *ModuleExchangeDelegatedOperationAudienceRequest) GetBindingId() string { @@ -4493,7 +4739,7 @@ type ExecutableArtifactContract struct { func (x *ExecutableArtifactContract) Reset() { *x = ExecutableArtifactContract{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[56] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4505,7 +4751,7 @@ func (x *ExecutableArtifactContract) String() string { func (*ExecutableArtifactContract) ProtoMessage() {} func (x *ExecutableArtifactContract) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[56] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4518,7 +4764,7 @@ func (x *ExecutableArtifactContract) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecutableArtifactContract.ProtoReflect.Descriptor instead. func (*ExecutableArtifactContract) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{56} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{59} } func (x *ExecutableArtifactContract) GetKind() string { @@ -4557,7 +4803,7 @@ type ExecutableArtifactIdentity struct { func (x *ExecutableArtifactIdentity) Reset() { *x = ExecutableArtifactIdentity{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[57] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4569,7 +4815,7 @@ func (x *ExecutableArtifactIdentity) String() string { func (*ExecutableArtifactIdentity) ProtoMessage() {} func (x *ExecutableArtifactIdentity) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[57] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4582,7 +4828,7 @@ func (x *ExecutableArtifactIdentity) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecutableArtifactIdentity.ProtoReflect.Descriptor instead. func (*ExecutableArtifactIdentity) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{57} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{60} } func (x *ExecutableArtifactIdentity) GetSchema() string { @@ -4632,7 +4878,7 @@ type ModuleExecutableArtifactRequest struct { func (x *ModuleExecutableArtifactRequest) Reset() { *x = ModuleExecutableArtifactRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[58] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4644,7 +4890,7 @@ func (x *ModuleExecutableArtifactRequest) String() string { func (*ModuleExecutableArtifactRequest) ProtoMessage() {} func (x *ModuleExecutableArtifactRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[58] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4657,7 +4903,7 @@ func (x *ModuleExecutableArtifactRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleExecutableArtifactRequest.ProtoReflect.Descriptor instead. func (*ModuleExecutableArtifactRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{58} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{61} } func (x *ModuleExecutableArtifactRequest) GetParentWorkContextToken() string { @@ -4699,7 +4945,7 @@ type ModuleRevokeExecutableArtifactRequest struct { func (x *ModuleRevokeExecutableArtifactRequest) Reset() { *x = ModuleRevokeExecutableArtifactRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[59] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4711,7 +4957,7 @@ func (x *ModuleRevokeExecutableArtifactRequest) String() string { func (*ModuleRevokeExecutableArtifactRequest) ProtoMessage() {} func (x *ModuleRevokeExecutableArtifactRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[59] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4724,7 +4970,7 @@ func (x *ModuleRevokeExecutableArtifactRequest) ProtoReflect() protoreflect.Mess // Deprecated: Use ModuleRevokeExecutableArtifactRequest.ProtoReflect.Descriptor instead. func (*ModuleRevokeExecutableArtifactRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{59} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{62} } func (x *ModuleRevokeExecutableArtifactRequest) GetParentWorkContextToken() string { @@ -4760,7 +5006,7 @@ type ModuleExecutableArtifactResponse struct { func (x *ModuleExecutableArtifactResponse) Reset() { *x = ModuleExecutableArtifactResponse{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[60] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4772,7 +5018,7 @@ func (x *ModuleExecutableArtifactResponse) String() string { func (*ModuleExecutableArtifactResponse) ProtoMessage() {} func (x *ModuleExecutableArtifactResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[60] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4785,7 +5031,7 @@ func (x *ModuleExecutableArtifactResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleExecutableArtifactResponse.ProtoReflect.Descriptor instead. func (*ModuleExecutableArtifactResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{60} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{63} } func (x *ModuleExecutableArtifactResponse) GetSchemaVersion() string { @@ -4828,7 +5074,7 @@ type ModuleCurrentInstallationRequest struct { func (x *ModuleCurrentInstallationRequest) Reset() { *x = ModuleCurrentInstallationRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[61] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4840,7 +5086,7 @@ func (x *ModuleCurrentInstallationRequest) String() string { func (*ModuleCurrentInstallationRequest) ProtoMessage() {} func (x *ModuleCurrentInstallationRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[61] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4853,7 +5099,7 @@ func (x *ModuleCurrentInstallationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleCurrentInstallationRequest.ProtoReflect.Descriptor instead. func (*ModuleCurrentInstallationRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{61} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{64} } func (x *ModuleCurrentInstallationRequest) GetParentWorkContextToken() string { @@ -4886,7 +5132,7 @@ type ModuleCurrentInstallationResponse struct { func (x *ModuleCurrentInstallationResponse) Reset() { *x = ModuleCurrentInstallationResponse{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[62] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4898,7 +5144,7 @@ func (x *ModuleCurrentInstallationResponse) String() string { func (*ModuleCurrentInstallationResponse) ProtoMessage() {} func (x *ModuleCurrentInstallationResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[62] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[65] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4911,7 +5157,7 @@ func (x *ModuleCurrentInstallationResponse) ProtoReflect() protoreflect.Message // Deprecated: Use ModuleCurrentInstallationResponse.ProtoReflect.Descriptor instead. func (*ModuleCurrentInstallationResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{62} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{65} } func (x *ModuleCurrentInstallationResponse) GetInstallationId() string { @@ -4960,7 +5206,7 @@ type WorkContextSealQuestion struct { func (x *WorkContextSealQuestion) Reset() { *x = WorkContextSealQuestion{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[63] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4972,7 +5218,7 @@ func (x *WorkContextSealQuestion) String() string { func (*WorkContextSealQuestion) ProtoMessage() {} func (x *WorkContextSealQuestion) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[63] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4985,7 +5231,7 @@ func (x *WorkContextSealQuestion) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkContextSealQuestion.ProtoReflect.Descriptor instead. func (*WorkContextSealQuestion) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{63} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{66} } func (x *WorkContextSealQuestion) GetPrincipalId() string { @@ -5016,7 +5262,7 @@ type WorkContextPrincipalEpochQuestion struct { func (x *WorkContextPrincipalEpochQuestion) Reset() { *x = WorkContextPrincipalEpochQuestion{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[64] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5028,7 +5274,7 @@ func (x *WorkContextPrincipalEpochQuestion) String() string { func (*WorkContextPrincipalEpochQuestion) ProtoMessage() {} func (x *WorkContextPrincipalEpochQuestion) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[64] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5041,7 +5287,7 @@ func (x *WorkContextPrincipalEpochQuestion) ProtoReflect() protoreflect.Message // Deprecated: Use WorkContextPrincipalEpochQuestion.ProtoReflect.Descriptor instead. func (*WorkContextPrincipalEpochQuestion) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{64} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{67} } func (x *WorkContextPrincipalEpochQuestion) GetPrincipalId() string { @@ -5065,7 +5311,7 @@ type WorkContextApprovedBuildQuestion struct { func (x *WorkContextApprovedBuildQuestion) Reset() { *x = WorkContextApprovedBuildQuestion{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[65] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5077,7 +5323,7 @@ func (x *WorkContextApprovedBuildQuestion) String() string { func (*WorkContextApprovedBuildQuestion) ProtoMessage() {} func (x *WorkContextApprovedBuildQuestion) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[65] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5090,7 +5336,7 @@ func (x *WorkContextApprovedBuildQuestion) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkContextApprovedBuildQuestion.ProtoReflect.Descriptor instead. func (*WorkContextApprovedBuildQuestion) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{65} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{68} } func (x *WorkContextApprovedBuildQuestion) GetPrincipalId() string { @@ -5116,7 +5362,7 @@ type WorkContextOperationBindingQuestion struct { func (x *WorkContextOperationBindingQuestion) Reset() { *x = WorkContextOperationBindingQuestion{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[66] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5128,7 +5374,7 @@ func (x *WorkContextOperationBindingQuestion) String() string { func (*WorkContextOperationBindingQuestion) ProtoMessage() {} func (x *WorkContextOperationBindingQuestion) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[66] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5141,7 +5387,7 @@ func (x *WorkContextOperationBindingQuestion) ProtoReflect() protoreflect.Messag // Deprecated: Use WorkContextOperationBindingQuestion.ProtoReflect.Descriptor instead. func (*WorkContextOperationBindingQuestion) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{66} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{69} } func (x *WorkContextOperationBindingQuestion) GetBindingId() string { @@ -5169,7 +5415,7 @@ type WorkContextAuthorizationRevisionQuestion struct { func (x *WorkContextAuthorizationRevisionQuestion) Reset() { *x = WorkContextAuthorizationRevisionQuestion{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[67] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5181,7 +5427,7 @@ func (x *WorkContextAuthorizationRevisionQuestion) String() string { func (*WorkContextAuthorizationRevisionQuestion) ProtoMessage() {} func (x *WorkContextAuthorizationRevisionQuestion) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[67] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5194,7 +5440,7 @@ func (x *WorkContextAuthorizationRevisionQuestion) ProtoReflect() protoreflect.M // Deprecated: Use WorkContextAuthorizationRevisionQuestion.ProtoReflect.Descriptor instead. func (*WorkContextAuthorizationRevisionQuestion) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{67} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{70} } func (x *WorkContextAuthorizationRevisionQuestion) GetOwnerPrincipalId() string { @@ -5216,7 +5462,7 @@ type WorkContextApprovalGrantQuestion struct { func (x *WorkContextApprovalGrantQuestion) Reset() { *x = WorkContextApprovalGrantQuestion{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[68] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5228,7 +5474,7 @@ func (x *WorkContextApprovalGrantQuestion) String() string { func (*WorkContextApprovalGrantQuestion) ProtoMessage() {} func (x *WorkContextApprovalGrantQuestion) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[68] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5241,7 +5487,7 @@ func (x *WorkContextApprovalGrantQuestion) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkContextApprovalGrantQuestion.ProtoReflect.Descriptor instead. func (*WorkContextApprovalGrantQuestion) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{68} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{71} } func (x *WorkContextApprovalGrantQuestion) GetGrantId() string { @@ -5264,7 +5510,7 @@ type WorkContextSealAnswer struct { func (x *WorkContextSealAnswer) Reset() { *x = WorkContextSealAnswer{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[69] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5276,7 +5522,7 @@ func (x *WorkContextSealAnswer) String() string { func (*WorkContextSealAnswer) ProtoMessage() {} func (x *WorkContextSealAnswer) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[69] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5289,7 +5535,7 @@ func (x *WorkContextSealAnswer) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkContextSealAnswer.ProtoReflect.Descriptor instead. func (*WorkContextSealAnswer) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{69} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{72} } func (x *WorkContextSealAnswer) GetState() WorkContextSealState { @@ -5324,7 +5570,7 @@ type WorkContextPrincipalEpochAnswer struct { func (x *WorkContextPrincipalEpochAnswer) Reset() { *x = WorkContextPrincipalEpochAnswer{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[70] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5336,7 +5582,7 @@ func (x *WorkContextPrincipalEpochAnswer) String() string { func (*WorkContextPrincipalEpochAnswer) ProtoMessage() {} func (x *WorkContextPrincipalEpochAnswer) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[70] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5349,7 +5595,7 @@ func (x *WorkContextPrincipalEpochAnswer) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkContextPrincipalEpochAnswer.ProtoReflect.Descriptor instead. func (*WorkContextPrincipalEpochAnswer) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{70} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{73} } func (x *WorkContextPrincipalEpochAnswer) GetState() WorkContextPrincipalState { @@ -5387,7 +5633,7 @@ type WorkContextApprovedBuildAnswer struct { func (x *WorkContextApprovedBuildAnswer) Reset() { *x = WorkContextApprovedBuildAnswer{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[71] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5399,7 +5645,7 @@ func (x *WorkContextApprovedBuildAnswer) String() string { func (*WorkContextApprovedBuildAnswer) ProtoMessage() {} func (x *WorkContextApprovedBuildAnswer) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[71] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5412,7 +5658,7 @@ func (x *WorkContextApprovedBuildAnswer) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkContextApprovedBuildAnswer.ProtoReflect.Descriptor instead. func (*WorkContextApprovedBuildAnswer) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{71} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{74} } func (x *WorkContextApprovedBuildAnswer) GetState() WorkContextApprovedBuildState { @@ -5463,7 +5709,7 @@ type WorkContextOperationBindingAnswer struct { func (x *WorkContextOperationBindingAnswer) Reset() { *x = WorkContextOperationBindingAnswer{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[72] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5475,7 +5721,7 @@ func (x *WorkContextOperationBindingAnswer) String() string { func (*WorkContextOperationBindingAnswer) ProtoMessage() {} func (x *WorkContextOperationBindingAnswer) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[72] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5488,7 +5734,7 @@ func (x *WorkContextOperationBindingAnswer) ProtoReflect() protoreflect.Message // Deprecated: Use WorkContextOperationBindingAnswer.ProtoReflect.Descriptor instead. func (*WorkContextOperationBindingAnswer) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{72} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{75} } func (x *WorkContextOperationBindingAnswer) GetState() WorkContextOperationBindingState { @@ -5550,7 +5796,7 @@ type WorkContextAuthorizationRevisionAnswer struct { func (x *WorkContextAuthorizationRevisionAnswer) Reset() { *x = WorkContextAuthorizationRevisionAnswer{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[73] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5562,7 +5808,7 @@ func (x *WorkContextAuthorizationRevisionAnswer) String() string { func (*WorkContextAuthorizationRevisionAnswer) ProtoMessage() {} func (x *WorkContextAuthorizationRevisionAnswer) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[73] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5575,7 +5821,7 @@ func (x *WorkContextAuthorizationRevisionAnswer) ProtoReflect() protoreflect.Mes // Deprecated: Use WorkContextAuthorizationRevisionAnswer.ProtoReflect.Descriptor instead. func (*WorkContextAuthorizationRevisionAnswer) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{73} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{76} } func (x *WorkContextAuthorizationRevisionAnswer) GetOwnerPrincipalId() string { @@ -5602,7 +5848,7 @@ type WorkContextApprovalGrantAnswer struct { func (x *WorkContextApprovalGrantAnswer) Reset() { *x = WorkContextApprovalGrantAnswer{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[74] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5614,7 +5860,7 @@ func (x *WorkContextApprovalGrantAnswer) String() string { func (*WorkContextApprovalGrantAnswer) ProtoMessage() {} func (x *WorkContextApprovalGrantAnswer) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[74] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5627,7 +5873,7 @@ func (x *WorkContextApprovalGrantAnswer) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkContextApprovalGrantAnswer.ProtoReflect.Descriptor instead. func (*WorkContextApprovalGrantAnswer) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{74} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{77} } func (x *WorkContextApprovalGrantAnswer) GetState() WorkContextApprovalGrantState { @@ -5670,7 +5916,7 @@ type ModuleResolveWorkContextAuthorityRequest struct { func (x *ModuleResolveWorkContextAuthorityRequest) Reset() { *x = ModuleResolveWorkContextAuthorityRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[75] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5682,7 +5928,7 @@ func (x *ModuleResolveWorkContextAuthorityRequest) String() string { func (*ModuleResolveWorkContextAuthorityRequest) ProtoMessage() {} func (x *ModuleResolveWorkContextAuthorityRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[75] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[78] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5695,7 +5941,7 @@ func (x *ModuleResolveWorkContextAuthorityRequest) ProtoReflect() protoreflect.M // Deprecated: Use ModuleResolveWorkContextAuthorityRequest.ProtoReflect.Descriptor instead. func (*ModuleResolveWorkContextAuthorityRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{75} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{78} } func (x *ModuleResolveWorkContextAuthorityRequest) GetWorkContext() string { @@ -5833,7 +6079,7 @@ type ModuleResolveWorkContextAuthorityResponse struct { func (x *ModuleResolveWorkContextAuthorityResponse) Reset() { *x = ModuleResolveWorkContextAuthorityResponse{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[76] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5845,7 +6091,7 @@ func (x *ModuleResolveWorkContextAuthorityResponse) String() string { func (*ModuleResolveWorkContextAuthorityResponse) ProtoMessage() {} func (x *ModuleResolveWorkContextAuthorityResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[76] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[79] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5858,7 +6104,7 @@ func (x *ModuleResolveWorkContextAuthorityResponse) ProtoReflect() protoreflect. // Deprecated: Use ModuleResolveWorkContextAuthorityResponse.ProtoReflect.Descriptor instead. func (*ModuleResolveWorkContextAuthorityResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{76} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{79} } func (x *ModuleResolveWorkContextAuthorityResponse) GetAnswer() isModuleResolveWorkContextAuthorityResponse_Answer { @@ -6093,7 +6339,25 @@ const file_saas_accounts_v1_module_capabilities_proto_rawDesc = "" + "\xbaH\ar\x05\x10\x01\x18\x80\x01R\bsolution\x12#\n" + "\bentry_id\x18\x05 \x01(\tB\b\xbaH\x05r\x03\x18\xff\x01R\aentryId\x12/\n" + "\x06fields\x18\x06 \x01(\v2\x17.google.protobuf.StructR\x06fields\x121\n" + - "\x0fidempotency_key\x18\a \x01(\tB\b\xbaH\x05r\x03\x18\xff\x01R\x0eidempotencyKey\"\xb8\x01\n" + + "\x0fidempotency_key\x18\a \x01(\tB\b\xbaH\x05r\x03\x18\xff\x01R\x0eidempotencyKey\"\xc3\x01\n" + + "\x18ModuleReadJournalRequest\x12\x1d\n" + + "\x05after\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x18@R\x05after\x12\x1d\n" + + "\x05limit\x18\x02 \x01(\x05B\a\xbaH\x04\x1a\x02(\x00R\x05limit\x12)\n" + + "\x05types\x18\x03 \x03(\tB\x13\xbaH\x10\x92\x01\r\b\x01\x10\x10\"\ar\x05\x10\x01\x18\x80\x02R\x05types\x12>\n" + + "\x11delivery_bindings\x18\x04 \x03(\tB\x11\xbaH\x0e\x92\x01\v\x10@\"\ar\x05\x10\x01\x18\x80\x02R\x10deliveryBindings\"\xa1\x02\n" + + "\x12ModuleJournalEntry\x12\x1a\n" + + "\bposition\x18\x01 \x01(\tR\bposition\x12\x19\n" + + "\bevent_id\x18\x02 \x01(\tR\aeventId\x12\x12\n" + + "\x04type\x18\x03 \x01(\tR\x04type\x12'\n" + + "\x0forganization_id\x18\x04 \x01(\tR\x0eorganizationId\x12;\n" + + "\voccurred_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "occurredAt\x12+\n" + + "\x04data\x18\x06 \x01(\v2\x17.google.protobuf.StructR\x04data\x12-\n" + + "\x12delivery_machinery\x18\a \x01(\bR\x11deliveryMachinery\"z\n" + + "\x19ModuleReadJournalResponse\x12>\n" + + "\aentries\x18\x01 \x03(\v2$.saas.accounts.v1.ModuleJournalEntryR\aentries\x12\x1d\n" + + "\n" + + "next_after\x18\x02 \x01(\tR\tnextAfter\"\xb8\x01\n" + "\x1bModuleAuditFieldDeclaration\x12\x1d\n" + "\x04name\x18\x01 \x01(\tB\t\xbaH\x06r\x04\x10\x01\x18@R\x04name\x12F\n" + "\x04kind\x18\x02 \x01(\x0e2&.saas.accounts.v1.ModuleAuditFieldKindB\n" + @@ -6430,7 +6694,7 @@ const file_saas_accounts_v1_module_capabilities_proto_rawDesc = "" + "+WORK_CONTEXT_OPERATION_BINDING_STATE_ABSENT\x10\x02*\x86\x01\n" + "\x1dWorkContextApprovalGrantState\x121\n" + "-WORK_CONTEXT_APPROVAL_GRANT_STATE_UNSPECIFIED\x10\x00\x122\n" + - ".WORK_CONTEXT_APPROVAL_GRANT_STATE_NOT_RECORDED\x10\x012\xe0'\n" + + ".WORK_CONTEXT_APPROVAL_GRANT_STATE_NOT_RECORDED\x10\x012\xe3(\n" + "\x19ModuleCapabilitiesService\x12\x9b\x01\n" + "\x16GetCurrentInstallation\x122.saas.accounts.v1.ModuleCurrentInstallationRequest\x1a3.saas.accounts.v1.ModuleCurrentInstallationResponse\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x04X\x03`\x01\x12\xc6\x01\n" + "\x19ApproveExecutableArtifact\x121.saas.accounts.v1.ModuleExecutableArtifactRequest\x1a2.saas.accounts.v1.ModuleExecutableArtifactResponse\"B\xc2\xf3\x18>\b\x03\x10\x010\x01:,\n" + @@ -6458,7 +6722,8 @@ const file_saas_accounts_v1_module_capabilities_proto_rawDesc = "" + "\x0fRequestApproval\x12..saas.accounts.v1.ModuleRequestApprovalRequest\x1a/.saas.accounts.v1.ModuleRequestApprovalResponse\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01\x12u\n" + "\vGetApproval\x12*.saas.accounts.v1.ModuleGetApprovalRequest\x1a .saas.accounts.v1.ModuleApproval\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01\x12q\n" + "\x0eCancelApproval\x12-.saas.accounts.v1.ModuleCancelApprovalRequest\x1a\x16.google.protobuf.Empty\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01\x12q\n" + - "\x0eEmitAuditEvent\x12-.saas.accounts.v1.ModuleEmitAuditEventRequest\x1a\x16.google.protobuf.Empty\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01\x12\xc3\x01\n" + + "\x0eEmitAuditEvent\x12-.saas.accounts.v1.ModuleEmitAuditEventRequest\x1a\x16.google.protobuf.Empty\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01\x12\x80\x01\n" + + "\vReadJournal\x12*.saas.accounts.v1.ModuleReadJournalRequest\x1a+.saas.accounts.v1.ModuleReadJournalResponse\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01\x12\xc3\x01\n" + "\x16DeclareAuditEventTypes\x125.saas.accounts.v1.ModuleDeclareAuditEventTypesRequest\x1a6.saas.accounts.v1.ModuleDeclareAuditEventTypesResponse\":\xc2\xf3\x186\b\x03\x10\x010\x01:$\n" + " saas.module.audit_types_declared\x10\x02@\x01H\aP\x03X\x03`\x01\x12\x9e\x01\n" + "\x15ListSubjectVisibility\x124.saas.accounts.v1.ModuleListSubjectVisibilityRequest\x1a5.saas.accounts.v1.ModuleListSubjectVisibilityResponse\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01\x12\x8e\x01\n" + @@ -6491,7 +6756,7 @@ func file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP() []byte { } var file_saas_accounts_v1_module_capabilities_proto_enumTypes = make([]protoimpl.EnumInfo, 11) -var file_saas_accounts_v1_module_capabilities_proto_msgTypes = make([]protoimpl.MessageInfo, 77) +var file_saas_accounts_v1_module_capabilities_proto_msgTypes = make([]protoimpl.MessageInfo, 80) var file_saas_accounts_v1_module_capabilities_proto_goTypes = []any{ (ModuleAuditFieldKind)(0), // 0: saas.accounts.v1.ModuleAuditFieldKind (ModuleAuditEventVisibility)(0), // 1: saas.accounts.v1.ModuleAuditEventVisibility @@ -6524,223 +6789,231 @@ var file_saas_accounts_v1_module_capabilities_proto_goTypes = []any{ (*ModuleApproval)(nil), // 28: saas.accounts.v1.ModuleApproval (*ModuleCancelApprovalRequest)(nil), // 29: saas.accounts.v1.ModuleCancelApprovalRequest (*ModuleEmitAuditEventRequest)(nil), // 30: saas.accounts.v1.ModuleEmitAuditEventRequest - (*ModuleAuditFieldDeclaration)(nil), // 31: saas.accounts.v1.ModuleAuditFieldDeclaration - (*ModuleAuditEventTypeDeclaration)(nil), // 32: saas.accounts.v1.ModuleAuditEventTypeDeclaration - (*ModuleDeclareAuditEventTypesRequest)(nil), // 33: saas.accounts.v1.ModuleDeclareAuditEventTypesRequest - (*ModuleDeclareAuditEventTypesResponse)(nil), // 34: saas.accounts.v1.ModuleDeclareAuditEventTypesResponse - (*SubjectVisibilityGrant)(nil), // 35: saas.accounts.v1.SubjectVisibilityGrant - (*ModuleListSubjectVisibilityRequest)(nil), // 36: saas.accounts.v1.ModuleListSubjectVisibilityRequest - (*ModuleListSubjectVisibilityResponse)(nil), // 37: saas.accounts.v1.ModuleListSubjectVisibilityResponse - (*FetchDatasourceBlobRequest)(nil), // 38: saas.accounts.v1.FetchDatasourceBlobRequest - (*FetchDatasourceBlobChunk)(nil), // 39: saas.accounts.v1.FetchDatasourceBlobChunk - (*DatasourceFileRef)(nil), // 40: saas.accounts.v1.DatasourceFileRef - (*FetchDatasourceFilesRequest)(nil), // 41: saas.accounts.v1.FetchDatasourceFilesRequest - (*DatasourceProvenance)(nil), // 42: saas.accounts.v1.DatasourceProvenance - (*DatasourceItemReaders)(nil), // 43: saas.accounts.v1.DatasourceItemReaders - (*DatasourceFileHeader)(nil), // 44: saas.accounts.v1.DatasourceFileHeader - (*FetchDatasourceFilesFrame)(nil), // 45: saas.accounts.v1.FetchDatasourceFilesFrame - (*ModulePublishEventRequest)(nil), // 46: saas.accounts.v1.ModulePublishEventRequest - (*ModulePublishEventResponse)(nil), // 47: saas.accounts.v1.ModulePublishEventResponse - (*ModuleSubscribeRequest)(nil), // 48: saas.accounts.v1.ModuleSubscribeRequest - (*ModuleSubscription)(nil), // 49: saas.accounts.v1.ModuleSubscription - (*ModuleSubscribeResponse)(nil), // 50: saas.accounts.v1.ModuleSubscribeResponse - (*ModuleUnsubscribeRequest)(nil), // 51: saas.accounts.v1.ModuleUnsubscribeRequest - (*ModuleListSubscriptionsRequest)(nil), // 52: saas.accounts.v1.ModuleListSubscriptionsRequest - (*ModuleListSubscriptionsResponse)(nil), // 53: saas.accounts.v1.ModuleListSubscriptionsResponse - (*ModuleReplayEventsRequest)(nil), // 54: saas.accounts.v1.ModuleReplayEventsRequest - (*ModuleReplayEventsResponse)(nil), // 55: saas.accounts.v1.ModuleReplayEventsResponse - (*ReadableSourceCollection)(nil), // 56: saas.accounts.v1.ReadableSourceCollection - (*ReadableCollectionGrant)(nil), // 57: saas.accounts.v1.ReadableCollectionGrant - (*CollectionSyncProvenance)(nil), // 58: saas.accounts.v1.CollectionSyncProvenance - (*ListReadableSourceCollectionsRequest)(nil), // 59: saas.accounts.v1.ListReadableSourceCollectionsRequest - (*ListReadableSourceCollectionsResponse)(nil), // 60: saas.accounts.v1.ListReadableSourceCollectionsResponse - (*ModulePlaceRecordRequest)(nil), // 61: saas.accounts.v1.ModulePlaceRecordRequest - (*ModulePlaceRecordResponse)(nil), // 62: saas.accounts.v1.ModulePlaceRecordResponse - (*CheckWorkContextRecordAccessRequest)(nil), // 63: saas.accounts.v1.CheckWorkContextRecordAccessRequest - (*CheckWorkContextRecordAccessResponse)(nil), // 64: saas.accounts.v1.CheckWorkContextRecordAccessResponse - (*ModuleExchangeDelegatedReadAudienceRequest)(nil), // 65: saas.accounts.v1.ModuleExchangeDelegatedReadAudienceRequest - (*ModuleExchangeDelegatedOperationAudienceRequest)(nil), // 66: saas.accounts.v1.ModuleExchangeDelegatedOperationAudienceRequest - (*ExecutableArtifactContract)(nil), // 67: saas.accounts.v1.ExecutableArtifactContract - (*ExecutableArtifactIdentity)(nil), // 68: saas.accounts.v1.ExecutableArtifactIdentity - (*ModuleExecutableArtifactRequest)(nil), // 69: saas.accounts.v1.ModuleExecutableArtifactRequest - (*ModuleRevokeExecutableArtifactRequest)(nil), // 70: saas.accounts.v1.ModuleRevokeExecutableArtifactRequest - (*ModuleExecutableArtifactResponse)(nil), // 71: saas.accounts.v1.ModuleExecutableArtifactResponse - (*ModuleCurrentInstallationRequest)(nil), // 72: saas.accounts.v1.ModuleCurrentInstallationRequest - (*ModuleCurrentInstallationResponse)(nil), // 73: saas.accounts.v1.ModuleCurrentInstallationResponse - (*WorkContextSealQuestion)(nil), // 74: saas.accounts.v1.WorkContextSealQuestion - (*WorkContextPrincipalEpochQuestion)(nil), // 75: saas.accounts.v1.WorkContextPrincipalEpochQuestion - (*WorkContextApprovedBuildQuestion)(nil), // 76: saas.accounts.v1.WorkContextApprovedBuildQuestion - (*WorkContextOperationBindingQuestion)(nil), // 77: saas.accounts.v1.WorkContextOperationBindingQuestion - (*WorkContextAuthorizationRevisionQuestion)(nil), // 78: saas.accounts.v1.WorkContextAuthorizationRevisionQuestion - (*WorkContextApprovalGrantQuestion)(nil), // 79: saas.accounts.v1.WorkContextApprovalGrantQuestion - (*WorkContextSealAnswer)(nil), // 80: saas.accounts.v1.WorkContextSealAnswer - (*WorkContextPrincipalEpochAnswer)(nil), // 81: saas.accounts.v1.WorkContextPrincipalEpochAnswer - (*WorkContextApprovedBuildAnswer)(nil), // 82: saas.accounts.v1.WorkContextApprovedBuildAnswer - (*WorkContextOperationBindingAnswer)(nil), // 83: saas.accounts.v1.WorkContextOperationBindingAnswer - (*WorkContextAuthorizationRevisionAnswer)(nil), // 84: saas.accounts.v1.WorkContextAuthorizationRevisionAnswer - (*WorkContextApprovalGrantAnswer)(nil), // 85: saas.accounts.v1.WorkContextApprovalGrantAnswer - (*ModuleResolveWorkContextAuthorityRequest)(nil), // 86: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest - (*ModuleResolveWorkContextAuthorityResponse)(nil), // 87: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse - (*v1.NewJob)(nil), // 88: saas.jobs.v1.NewJob - (v1.JobEnqueueDisposition)(0), // 89: saas.jobs.v1.JobEnqueueDisposition - (*durationpb.Duration)(nil), // 90: google.protobuf.Duration - (*v1.JobEnvelope)(nil), // 91: saas.jobs.v1.JobEnvelope - (*v1.JobLeaseReference)(nil), // 92: saas.jobs.v1.JobLeaseReference - (*v1.JobLease)(nil), // 93: saas.jobs.v1.JobLease - (*v1.JobFailure)(nil), // 94: saas.jobs.v1.JobFailure - (*timestamppb.Timestamp)(nil), // 95: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 96: google.protobuf.Struct - (*v11.EventEnvelope)(nil), // 97: saas.events.v1.EventEnvelope - (*ModuleMintWorkContextRequest)(nil), // 98: saas.accounts.v1.ModuleMintWorkContextRequest - (*ModuleMintOperationContextRequest)(nil), // 99: saas.accounts.v1.ModuleMintOperationContextRequest - (*ModuleMintSourceOperationContextRequest)(nil), // 100: saas.accounts.v1.ModuleMintSourceOperationContextRequest - (*IssuedWorkContext)(nil), // 101: saas.accounts.v1.IssuedWorkContext - (*emptypb.Empty)(nil), // 102: google.protobuf.Empty - (*ModuleMintWorkContextResponse)(nil), // 103: saas.accounts.v1.ModuleMintWorkContextResponse - (*ModuleMintOperationContextResponse)(nil), // 104: saas.accounts.v1.ModuleMintOperationContextResponse - (*ModuleMintSourceOperationContextResponse)(nil), // 105: saas.accounts.v1.ModuleMintSourceOperationContextResponse + (*ModuleReadJournalRequest)(nil), // 31: saas.accounts.v1.ModuleReadJournalRequest + (*ModuleJournalEntry)(nil), // 32: saas.accounts.v1.ModuleJournalEntry + (*ModuleReadJournalResponse)(nil), // 33: saas.accounts.v1.ModuleReadJournalResponse + (*ModuleAuditFieldDeclaration)(nil), // 34: saas.accounts.v1.ModuleAuditFieldDeclaration + (*ModuleAuditEventTypeDeclaration)(nil), // 35: saas.accounts.v1.ModuleAuditEventTypeDeclaration + (*ModuleDeclareAuditEventTypesRequest)(nil), // 36: saas.accounts.v1.ModuleDeclareAuditEventTypesRequest + (*ModuleDeclareAuditEventTypesResponse)(nil), // 37: saas.accounts.v1.ModuleDeclareAuditEventTypesResponse + (*SubjectVisibilityGrant)(nil), // 38: saas.accounts.v1.SubjectVisibilityGrant + (*ModuleListSubjectVisibilityRequest)(nil), // 39: saas.accounts.v1.ModuleListSubjectVisibilityRequest + (*ModuleListSubjectVisibilityResponse)(nil), // 40: saas.accounts.v1.ModuleListSubjectVisibilityResponse + (*FetchDatasourceBlobRequest)(nil), // 41: saas.accounts.v1.FetchDatasourceBlobRequest + (*FetchDatasourceBlobChunk)(nil), // 42: saas.accounts.v1.FetchDatasourceBlobChunk + (*DatasourceFileRef)(nil), // 43: saas.accounts.v1.DatasourceFileRef + (*FetchDatasourceFilesRequest)(nil), // 44: saas.accounts.v1.FetchDatasourceFilesRequest + (*DatasourceProvenance)(nil), // 45: saas.accounts.v1.DatasourceProvenance + (*DatasourceItemReaders)(nil), // 46: saas.accounts.v1.DatasourceItemReaders + (*DatasourceFileHeader)(nil), // 47: saas.accounts.v1.DatasourceFileHeader + (*FetchDatasourceFilesFrame)(nil), // 48: saas.accounts.v1.FetchDatasourceFilesFrame + (*ModulePublishEventRequest)(nil), // 49: saas.accounts.v1.ModulePublishEventRequest + (*ModulePublishEventResponse)(nil), // 50: saas.accounts.v1.ModulePublishEventResponse + (*ModuleSubscribeRequest)(nil), // 51: saas.accounts.v1.ModuleSubscribeRequest + (*ModuleSubscription)(nil), // 52: saas.accounts.v1.ModuleSubscription + (*ModuleSubscribeResponse)(nil), // 53: saas.accounts.v1.ModuleSubscribeResponse + (*ModuleUnsubscribeRequest)(nil), // 54: saas.accounts.v1.ModuleUnsubscribeRequest + (*ModuleListSubscriptionsRequest)(nil), // 55: saas.accounts.v1.ModuleListSubscriptionsRequest + (*ModuleListSubscriptionsResponse)(nil), // 56: saas.accounts.v1.ModuleListSubscriptionsResponse + (*ModuleReplayEventsRequest)(nil), // 57: saas.accounts.v1.ModuleReplayEventsRequest + (*ModuleReplayEventsResponse)(nil), // 58: saas.accounts.v1.ModuleReplayEventsResponse + (*ReadableSourceCollection)(nil), // 59: saas.accounts.v1.ReadableSourceCollection + (*ReadableCollectionGrant)(nil), // 60: saas.accounts.v1.ReadableCollectionGrant + (*CollectionSyncProvenance)(nil), // 61: saas.accounts.v1.CollectionSyncProvenance + (*ListReadableSourceCollectionsRequest)(nil), // 62: saas.accounts.v1.ListReadableSourceCollectionsRequest + (*ListReadableSourceCollectionsResponse)(nil), // 63: saas.accounts.v1.ListReadableSourceCollectionsResponse + (*ModulePlaceRecordRequest)(nil), // 64: saas.accounts.v1.ModulePlaceRecordRequest + (*ModulePlaceRecordResponse)(nil), // 65: saas.accounts.v1.ModulePlaceRecordResponse + (*CheckWorkContextRecordAccessRequest)(nil), // 66: saas.accounts.v1.CheckWorkContextRecordAccessRequest + (*CheckWorkContextRecordAccessResponse)(nil), // 67: saas.accounts.v1.CheckWorkContextRecordAccessResponse + (*ModuleExchangeDelegatedReadAudienceRequest)(nil), // 68: saas.accounts.v1.ModuleExchangeDelegatedReadAudienceRequest + (*ModuleExchangeDelegatedOperationAudienceRequest)(nil), // 69: saas.accounts.v1.ModuleExchangeDelegatedOperationAudienceRequest + (*ExecutableArtifactContract)(nil), // 70: saas.accounts.v1.ExecutableArtifactContract + (*ExecutableArtifactIdentity)(nil), // 71: saas.accounts.v1.ExecutableArtifactIdentity + (*ModuleExecutableArtifactRequest)(nil), // 72: saas.accounts.v1.ModuleExecutableArtifactRequest + (*ModuleRevokeExecutableArtifactRequest)(nil), // 73: saas.accounts.v1.ModuleRevokeExecutableArtifactRequest + (*ModuleExecutableArtifactResponse)(nil), // 74: saas.accounts.v1.ModuleExecutableArtifactResponse + (*ModuleCurrentInstallationRequest)(nil), // 75: saas.accounts.v1.ModuleCurrentInstallationRequest + (*ModuleCurrentInstallationResponse)(nil), // 76: saas.accounts.v1.ModuleCurrentInstallationResponse + (*WorkContextSealQuestion)(nil), // 77: saas.accounts.v1.WorkContextSealQuestion + (*WorkContextPrincipalEpochQuestion)(nil), // 78: saas.accounts.v1.WorkContextPrincipalEpochQuestion + (*WorkContextApprovedBuildQuestion)(nil), // 79: saas.accounts.v1.WorkContextApprovedBuildQuestion + (*WorkContextOperationBindingQuestion)(nil), // 80: saas.accounts.v1.WorkContextOperationBindingQuestion + (*WorkContextAuthorizationRevisionQuestion)(nil), // 81: saas.accounts.v1.WorkContextAuthorizationRevisionQuestion + (*WorkContextApprovalGrantQuestion)(nil), // 82: saas.accounts.v1.WorkContextApprovalGrantQuestion + (*WorkContextSealAnswer)(nil), // 83: saas.accounts.v1.WorkContextSealAnswer + (*WorkContextPrincipalEpochAnswer)(nil), // 84: saas.accounts.v1.WorkContextPrincipalEpochAnswer + (*WorkContextApprovedBuildAnswer)(nil), // 85: saas.accounts.v1.WorkContextApprovedBuildAnswer + (*WorkContextOperationBindingAnswer)(nil), // 86: saas.accounts.v1.WorkContextOperationBindingAnswer + (*WorkContextAuthorizationRevisionAnswer)(nil), // 87: saas.accounts.v1.WorkContextAuthorizationRevisionAnswer + (*WorkContextApprovalGrantAnswer)(nil), // 88: saas.accounts.v1.WorkContextApprovalGrantAnswer + (*ModuleResolveWorkContextAuthorityRequest)(nil), // 89: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest + (*ModuleResolveWorkContextAuthorityResponse)(nil), // 90: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse + (*v1.NewJob)(nil), // 91: saas.jobs.v1.NewJob + (v1.JobEnqueueDisposition)(0), // 92: saas.jobs.v1.JobEnqueueDisposition + (*durationpb.Duration)(nil), // 93: google.protobuf.Duration + (*v1.JobEnvelope)(nil), // 94: saas.jobs.v1.JobEnvelope + (*v1.JobLeaseReference)(nil), // 95: saas.jobs.v1.JobLeaseReference + (*v1.JobLease)(nil), // 96: saas.jobs.v1.JobLease + (*v1.JobFailure)(nil), // 97: saas.jobs.v1.JobFailure + (*timestamppb.Timestamp)(nil), // 98: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 99: google.protobuf.Struct + (*v11.EventEnvelope)(nil), // 100: saas.events.v1.EventEnvelope + (*ModuleMintWorkContextRequest)(nil), // 101: saas.accounts.v1.ModuleMintWorkContextRequest + (*ModuleMintOperationContextRequest)(nil), // 102: saas.accounts.v1.ModuleMintOperationContextRequest + (*ModuleMintSourceOperationContextRequest)(nil), // 103: saas.accounts.v1.ModuleMintSourceOperationContextRequest + (*IssuedWorkContext)(nil), // 104: saas.accounts.v1.IssuedWorkContext + (*emptypb.Empty)(nil), // 105: google.protobuf.Empty + (*ModuleMintWorkContextResponse)(nil), // 106: saas.accounts.v1.ModuleMintWorkContextResponse + (*ModuleMintOperationContextResponse)(nil), // 107: saas.accounts.v1.ModuleMintOperationContextResponse + (*ModuleMintSourceOperationContextResponse)(nil), // 108: saas.accounts.v1.ModuleMintSourceOperationContextResponse } var file_saas_accounts_v1_module_capabilities_proto_depIdxs = []int32{ - 88, // 0: saas.accounts.v1.ModuleEnqueueJobRequest.job:type_name -> saas.jobs.v1.NewJob - 89, // 1: saas.accounts.v1.ModuleEnqueueJobResponse.disposition:type_name -> saas.jobs.v1.JobEnqueueDisposition - 90, // 2: saas.accounts.v1.ModuleClaimJobsRequest.lease_duration:type_name -> google.protobuf.Duration - 91, // 3: saas.accounts.v1.ModuleClaimJobsResponse.jobs:type_name -> saas.jobs.v1.JobEnvelope - 92, // 4: saas.accounts.v1.ModuleHeartbeatJobRequest.lease:type_name -> saas.jobs.v1.JobLeaseReference - 90, // 5: saas.accounts.v1.ModuleHeartbeatJobRequest.extension:type_name -> google.protobuf.Duration - 93, // 6: saas.accounts.v1.ModuleHeartbeatJobResponse.lease:type_name -> saas.jobs.v1.JobLease - 92, // 7: saas.accounts.v1.ModuleAckJobRequest.lease:type_name -> saas.jobs.v1.JobLeaseReference - 92, // 8: saas.accounts.v1.ModuleNackJobRequest.lease:type_name -> saas.jobs.v1.JobLeaseReference - 94, // 9: saas.accounts.v1.ModuleNackJobRequest.failure:type_name -> saas.jobs.v1.JobFailure - 95, // 10: saas.accounts.v1.ModuleNackJobRequest.retry_at:type_name -> google.protobuf.Timestamp - 96, // 11: saas.accounts.v1.ModuleResumeRef.payload:type_name -> google.protobuf.Struct - 96, // 12: saas.accounts.v1.ModuleRequestApprovalRequest.subject:type_name -> google.protobuf.Struct + 91, // 0: saas.accounts.v1.ModuleEnqueueJobRequest.job:type_name -> saas.jobs.v1.NewJob + 92, // 1: saas.accounts.v1.ModuleEnqueueJobResponse.disposition:type_name -> saas.jobs.v1.JobEnqueueDisposition + 93, // 2: saas.accounts.v1.ModuleClaimJobsRequest.lease_duration:type_name -> google.protobuf.Duration + 94, // 3: saas.accounts.v1.ModuleClaimJobsResponse.jobs:type_name -> saas.jobs.v1.JobEnvelope + 95, // 4: saas.accounts.v1.ModuleHeartbeatJobRequest.lease:type_name -> saas.jobs.v1.JobLeaseReference + 93, // 5: saas.accounts.v1.ModuleHeartbeatJobRequest.extension:type_name -> google.protobuf.Duration + 96, // 6: saas.accounts.v1.ModuleHeartbeatJobResponse.lease:type_name -> saas.jobs.v1.JobLease + 95, // 7: saas.accounts.v1.ModuleAckJobRequest.lease:type_name -> saas.jobs.v1.JobLeaseReference + 95, // 8: saas.accounts.v1.ModuleNackJobRequest.lease:type_name -> saas.jobs.v1.JobLeaseReference + 97, // 9: saas.accounts.v1.ModuleNackJobRequest.failure:type_name -> saas.jobs.v1.JobFailure + 98, // 10: saas.accounts.v1.ModuleNackJobRequest.retry_at:type_name -> google.protobuf.Timestamp + 99, // 11: saas.accounts.v1.ModuleResumeRef.payload:type_name -> google.protobuf.Struct + 99, // 12: saas.accounts.v1.ModuleRequestApprovalRequest.subject:type_name -> google.protobuf.Struct 23, // 13: saas.accounts.v1.ModuleRequestApprovalRequest.policy:type_name -> saas.accounts.v1.ModuleApprovalPolicy 24, // 14: saas.accounts.v1.ModuleRequestApprovalRequest.resume_ref:type_name -> saas.accounts.v1.ModuleResumeRef - 95, // 15: saas.accounts.v1.ModuleRequestApprovalRequest.expires_at:type_name -> google.protobuf.Timestamp - 95, // 16: saas.accounts.v1.ModuleRequestApprovalRequest.escalate_at:type_name -> google.protobuf.Timestamp - 96, // 17: saas.accounts.v1.ModuleApproval.subject:type_name -> google.protobuf.Struct + 98, // 15: saas.accounts.v1.ModuleRequestApprovalRequest.expires_at:type_name -> google.protobuf.Timestamp + 98, // 16: saas.accounts.v1.ModuleRequestApprovalRequest.escalate_at:type_name -> google.protobuf.Timestamp + 99, // 17: saas.accounts.v1.ModuleApproval.subject:type_name -> google.protobuf.Struct 24, // 18: saas.accounts.v1.ModuleApproval.resume_ref:type_name -> saas.accounts.v1.ModuleResumeRef - 95, // 19: saas.accounts.v1.ModuleApproval.expires_at:type_name -> google.protobuf.Timestamp - 95, // 20: saas.accounts.v1.ModuleApproval.escalate_at:type_name -> google.protobuf.Timestamp - 95, // 21: saas.accounts.v1.ModuleApproval.created_at:type_name -> google.protobuf.Timestamp - 96, // 22: saas.accounts.v1.ModuleEmitAuditEventRequest.fields:type_name -> google.protobuf.Struct - 0, // 23: saas.accounts.v1.ModuleAuditFieldDeclaration.kind:type_name -> saas.accounts.v1.ModuleAuditFieldKind - 31, // 24: saas.accounts.v1.ModuleAuditEventTypeDeclaration.fields:type_name -> saas.accounts.v1.ModuleAuditFieldDeclaration - 1, // 25: saas.accounts.v1.ModuleAuditEventTypeDeclaration.visibility:type_name -> saas.accounts.v1.ModuleAuditEventVisibility - 32, // 26: saas.accounts.v1.ModuleDeclareAuditEventTypesRequest.types:type_name -> saas.accounts.v1.ModuleAuditEventTypeDeclaration - 95, // 27: saas.accounts.v1.SubjectVisibilityGrant.expires_at:type_name -> google.protobuf.Timestamp - 35, // 28: saas.accounts.v1.ModuleListSubjectVisibilityResponse.grants:type_name -> saas.accounts.v1.SubjectVisibilityGrant - 40, // 29: saas.accounts.v1.FetchDatasourceFilesRequest.files:type_name -> saas.accounts.v1.DatasourceFileRef - 2, // 30: saas.accounts.v1.DatasourceItemReaders.basis:type_name -> saas.accounts.v1.DatasourceItemReadersBasis - 42, // 31: saas.accounts.v1.DatasourceFileHeader.provenance:type_name -> saas.accounts.v1.DatasourceProvenance - 43, // 32: saas.accounts.v1.DatasourceFileHeader.readers:type_name -> saas.accounts.v1.DatasourceItemReaders - 44, // 33: saas.accounts.v1.FetchDatasourceFilesFrame.header:type_name -> saas.accounts.v1.DatasourceFileHeader - 97, // 34: saas.accounts.v1.ModulePublishEventRequest.envelope:type_name -> saas.events.v1.EventEnvelope - 3, // 35: saas.accounts.v1.ModuleSubscribeRequest.delivery:type_name -> saas.accounts.v1.EventDelivery - 3, // 36: saas.accounts.v1.ModuleSubscription.delivery:type_name -> saas.accounts.v1.EventDelivery - 95, // 37: saas.accounts.v1.ModuleSubscription.created_at:type_name -> google.protobuf.Timestamp - 49, // 38: saas.accounts.v1.ModuleSubscribeResponse.subscription:type_name -> saas.accounts.v1.ModuleSubscription - 49, // 39: saas.accounts.v1.ModuleListSubscriptionsResponse.subscriptions:type_name -> saas.accounts.v1.ModuleSubscription - 95, // 40: saas.accounts.v1.ModuleReplayEventsRequest.since:type_name -> google.protobuf.Timestamp - 4, // 41: saas.accounts.v1.ReadableSourceCollection.grant_disclosure:type_name -> saas.accounts.v1.MetadataDisclosure - 57, // 42: saas.accounts.v1.ReadableSourceCollection.read_grants:type_name -> saas.accounts.v1.ReadableCollectionGrant - 58, // 43: saas.accounts.v1.ReadableSourceCollection.sync:type_name -> saas.accounts.v1.CollectionSyncProvenance - 95, // 44: saas.accounts.v1.ReadableCollectionGrant.expires_at:type_name -> google.protobuf.Timestamp - 5, // 45: saas.accounts.v1.CollectionSyncProvenance.stage:type_name -> saas.accounts.v1.SourceSyncStage - 95, // 46: saas.accounts.v1.CollectionSyncProvenance.at:type_name -> google.protobuf.Timestamp - 4, // 47: saas.accounts.v1.CollectionSyncProvenance.requester_disclosure:type_name -> saas.accounts.v1.MetadataDisclosure - 95, // 48: saas.accounts.v1.CollectionSyncProvenance.requested_at:type_name -> google.protobuf.Timestamp - 56, // 49: saas.accounts.v1.ListReadableSourceCollectionsResponse.collections:type_name -> saas.accounts.v1.ReadableSourceCollection - 67, // 50: saas.accounts.v1.ExecutableArtifactIdentity.contracts:type_name -> saas.accounts.v1.ExecutableArtifactContract - 68, // 51: saas.accounts.v1.ModuleExecutableArtifactRequest.identity:type_name -> saas.accounts.v1.ExecutableArtifactIdentity - 6, // 52: saas.accounts.v1.WorkContextSealAnswer.state:type_name -> saas.accounts.v1.WorkContextSealState - 7, // 53: saas.accounts.v1.WorkContextPrincipalEpochAnswer.state:type_name -> saas.accounts.v1.WorkContextPrincipalState - 8, // 54: saas.accounts.v1.WorkContextApprovedBuildAnswer.state:type_name -> saas.accounts.v1.WorkContextApprovedBuildState - 9, // 55: saas.accounts.v1.WorkContextOperationBindingAnswer.state:type_name -> saas.accounts.v1.WorkContextOperationBindingState - 10, // 56: saas.accounts.v1.WorkContextApprovalGrantAnswer.state:type_name -> saas.accounts.v1.WorkContextApprovalGrantState - 74, // 57: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest.seal:type_name -> saas.accounts.v1.WorkContextSealQuestion - 75, // 58: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest.principal_epoch:type_name -> saas.accounts.v1.WorkContextPrincipalEpochQuestion - 76, // 59: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest.approved_build:type_name -> saas.accounts.v1.WorkContextApprovedBuildQuestion - 77, // 60: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest.operation_binding:type_name -> saas.accounts.v1.WorkContextOperationBindingQuestion - 78, // 61: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest.authorization_revision:type_name -> saas.accounts.v1.WorkContextAuthorizationRevisionQuestion - 79, // 62: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest.approval_grant:type_name -> saas.accounts.v1.WorkContextApprovalGrantQuestion - 80, // 63: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse.seal:type_name -> saas.accounts.v1.WorkContextSealAnswer - 81, // 64: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse.principal_epoch:type_name -> saas.accounts.v1.WorkContextPrincipalEpochAnswer - 82, // 65: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse.approved_build:type_name -> saas.accounts.v1.WorkContextApprovedBuildAnswer - 83, // 66: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse.operation_binding:type_name -> saas.accounts.v1.WorkContextOperationBindingAnswer - 84, // 67: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse.authorization_revision:type_name -> saas.accounts.v1.WorkContextAuthorizationRevisionAnswer - 85, // 68: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse.approval_grant:type_name -> saas.accounts.v1.WorkContextApprovalGrantAnswer - 72, // 69: saas.accounts.v1.ModuleCapabilitiesService.GetCurrentInstallation:input_type -> saas.accounts.v1.ModuleCurrentInstallationRequest - 69, // 70: saas.accounts.v1.ModuleCapabilitiesService.ApproveExecutableArtifact:input_type -> saas.accounts.v1.ModuleExecutableArtifactRequest - 69, // 71: saas.accounts.v1.ModuleCapabilitiesService.AuthorizeExecutableArtifact:input_type -> saas.accounts.v1.ModuleExecutableArtifactRequest - 70, // 72: saas.accounts.v1.ModuleCapabilitiesService.RevokeExecutableArtifact:input_type -> saas.accounts.v1.ModuleRevokeExecutableArtifactRequest - 65, // 73: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedReadAudience:input_type -> saas.accounts.v1.ModuleExchangeDelegatedReadAudienceRequest - 66, // 74: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedOperationAudience:input_type -> saas.accounts.v1.ModuleExchangeDelegatedOperationAudienceRequest - 63, // 75: saas.accounts.v1.ModuleCapabilitiesService.CheckWorkContextRecordAccess:input_type -> saas.accounts.v1.CheckWorkContextRecordAccessRequest - 59, // 76: saas.accounts.v1.ModuleCapabilitiesService.ListReadableSourceCollections:input_type -> saas.accounts.v1.ListReadableSourceCollectionsRequest - 61, // 77: saas.accounts.v1.ModuleCapabilitiesService.PlaceRecord:input_type -> saas.accounts.v1.ModulePlaceRecordRequest - 11, // 78: saas.accounts.v1.ModuleCapabilitiesService.EnqueueJob:input_type -> saas.accounts.v1.ModuleEnqueueJobRequest - 13, // 79: saas.accounts.v1.ModuleCapabilitiesService.ClaimJobs:input_type -> saas.accounts.v1.ModuleClaimJobsRequest - 15, // 80: saas.accounts.v1.ModuleCapabilitiesService.HeartbeatJob:input_type -> saas.accounts.v1.ModuleHeartbeatJobRequest - 17, // 81: saas.accounts.v1.ModuleCapabilitiesService.AckJob:input_type -> saas.accounts.v1.ModuleAckJobRequest - 18, // 82: saas.accounts.v1.ModuleCapabilitiesService.NackJob:input_type -> saas.accounts.v1.ModuleNackJobRequest - 19, // 83: saas.accounts.v1.ModuleCapabilitiesService.NotifyUser:input_type -> saas.accounts.v1.ModuleNotifyUserRequest - 21, // 84: saas.accounts.v1.ModuleCapabilitiesService.NotifyOrgAdmins:input_type -> saas.accounts.v1.ModuleNotifyOrgAdminsRequest - 25, // 85: saas.accounts.v1.ModuleCapabilitiesService.RequestApproval:input_type -> saas.accounts.v1.ModuleRequestApprovalRequest - 27, // 86: saas.accounts.v1.ModuleCapabilitiesService.GetApproval:input_type -> saas.accounts.v1.ModuleGetApprovalRequest - 29, // 87: saas.accounts.v1.ModuleCapabilitiesService.CancelApproval:input_type -> saas.accounts.v1.ModuleCancelApprovalRequest - 30, // 88: saas.accounts.v1.ModuleCapabilitiesService.EmitAuditEvent:input_type -> saas.accounts.v1.ModuleEmitAuditEventRequest - 33, // 89: saas.accounts.v1.ModuleCapabilitiesService.DeclareAuditEventTypes:input_type -> saas.accounts.v1.ModuleDeclareAuditEventTypesRequest - 36, // 90: saas.accounts.v1.ModuleCapabilitiesService.ListSubjectVisibility:input_type -> saas.accounts.v1.ModuleListSubjectVisibilityRequest - 38, // 91: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceBlob:input_type -> saas.accounts.v1.FetchDatasourceBlobRequest - 41, // 92: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceFiles:input_type -> saas.accounts.v1.FetchDatasourceFilesRequest - 98, // 93: saas.accounts.v1.ModuleCapabilitiesService.MintModuleWorkContext:input_type -> saas.accounts.v1.ModuleMintWorkContextRequest - 99, // 94: saas.accounts.v1.ModuleCapabilitiesService.MintModuleOperationContext:input_type -> saas.accounts.v1.ModuleMintOperationContextRequest - 100, // 95: saas.accounts.v1.ModuleCapabilitiesService.MintSourceOperationContext:input_type -> saas.accounts.v1.ModuleMintSourceOperationContextRequest - 46, // 96: saas.accounts.v1.ModuleCapabilitiesService.PublishEvent:input_type -> saas.accounts.v1.ModulePublishEventRequest - 48, // 97: saas.accounts.v1.ModuleCapabilitiesService.Subscribe:input_type -> saas.accounts.v1.ModuleSubscribeRequest - 51, // 98: saas.accounts.v1.ModuleCapabilitiesService.Unsubscribe:input_type -> saas.accounts.v1.ModuleUnsubscribeRequest - 52, // 99: saas.accounts.v1.ModuleCapabilitiesService.ListSubscriptions:input_type -> saas.accounts.v1.ModuleListSubscriptionsRequest - 54, // 100: saas.accounts.v1.ModuleCapabilitiesService.ReplayEvents:input_type -> saas.accounts.v1.ModuleReplayEventsRequest - 86, // 101: saas.accounts.v1.ModuleCapabilitiesService.ResolveWorkContextAuthority:input_type -> saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest - 73, // 102: saas.accounts.v1.ModuleCapabilitiesService.GetCurrentInstallation:output_type -> saas.accounts.v1.ModuleCurrentInstallationResponse - 71, // 103: saas.accounts.v1.ModuleCapabilitiesService.ApproveExecutableArtifact:output_type -> saas.accounts.v1.ModuleExecutableArtifactResponse - 71, // 104: saas.accounts.v1.ModuleCapabilitiesService.AuthorizeExecutableArtifact:output_type -> saas.accounts.v1.ModuleExecutableArtifactResponse - 71, // 105: saas.accounts.v1.ModuleCapabilitiesService.RevokeExecutableArtifact:output_type -> saas.accounts.v1.ModuleExecutableArtifactResponse - 101, // 106: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedReadAudience:output_type -> saas.accounts.v1.IssuedWorkContext - 101, // 107: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedOperationAudience:output_type -> saas.accounts.v1.IssuedWorkContext - 64, // 108: saas.accounts.v1.ModuleCapabilitiesService.CheckWorkContextRecordAccess:output_type -> saas.accounts.v1.CheckWorkContextRecordAccessResponse - 60, // 109: saas.accounts.v1.ModuleCapabilitiesService.ListReadableSourceCollections:output_type -> saas.accounts.v1.ListReadableSourceCollectionsResponse - 62, // 110: saas.accounts.v1.ModuleCapabilitiesService.PlaceRecord:output_type -> saas.accounts.v1.ModulePlaceRecordResponse - 12, // 111: saas.accounts.v1.ModuleCapabilitiesService.EnqueueJob:output_type -> saas.accounts.v1.ModuleEnqueueJobResponse - 14, // 112: saas.accounts.v1.ModuleCapabilitiesService.ClaimJobs:output_type -> saas.accounts.v1.ModuleClaimJobsResponse - 16, // 113: saas.accounts.v1.ModuleCapabilitiesService.HeartbeatJob:output_type -> saas.accounts.v1.ModuleHeartbeatJobResponse - 102, // 114: saas.accounts.v1.ModuleCapabilitiesService.AckJob:output_type -> google.protobuf.Empty - 102, // 115: saas.accounts.v1.ModuleCapabilitiesService.NackJob:output_type -> google.protobuf.Empty - 20, // 116: saas.accounts.v1.ModuleCapabilitiesService.NotifyUser:output_type -> saas.accounts.v1.ModuleNotifyUserResponse - 22, // 117: saas.accounts.v1.ModuleCapabilitiesService.NotifyOrgAdmins:output_type -> saas.accounts.v1.ModuleNotifyOrgAdminsResponse - 26, // 118: saas.accounts.v1.ModuleCapabilitiesService.RequestApproval:output_type -> saas.accounts.v1.ModuleRequestApprovalResponse - 28, // 119: saas.accounts.v1.ModuleCapabilitiesService.GetApproval:output_type -> saas.accounts.v1.ModuleApproval - 102, // 120: saas.accounts.v1.ModuleCapabilitiesService.CancelApproval:output_type -> google.protobuf.Empty - 102, // 121: saas.accounts.v1.ModuleCapabilitiesService.EmitAuditEvent:output_type -> google.protobuf.Empty - 34, // 122: saas.accounts.v1.ModuleCapabilitiesService.DeclareAuditEventTypes:output_type -> saas.accounts.v1.ModuleDeclareAuditEventTypesResponse - 37, // 123: saas.accounts.v1.ModuleCapabilitiesService.ListSubjectVisibility:output_type -> saas.accounts.v1.ModuleListSubjectVisibilityResponse - 39, // 124: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceBlob:output_type -> saas.accounts.v1.FetchDatasourceBlobChunk - 45, // 125: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceFiles:output_type -> saas.accounts.v1.FetchDatasourceFilesFrame - 103, // 126: saas.accounts.v1.ModuleCapabilitiesService.MintModuleWorkContext:output_type -> saas.accounts.v1.ModuleMintWorkContextResponse - 104, // 127: saas.accounts.v1.ModuleCapabilitiesService.MintModuleOperationContext:output_type -> saas.accounts.v1.ModuleMintOperationContextResponse - 105, // 128: saas.accounts.v1.ModuleCapabilitiesService.MintSourceOperationContext:output_type -> saas.accounts.v1.ModuleMintSourceOperationContextResponse - 47, // 129: saas.accounts.v1.ModuleCapabilitiesService.PublishEvent:output_type -> saas.accounts.v1.ModulePublishEventResponse - 50, // 130: saas.accounts.v1.ModuleCapabilitiesService.Subscribe:output_type -> saas.accounts.v1.ModuleSubscribeResponse - 102, // 131: saas.accounts.v1.ModuleCapabilitiesService.Unsubscribe:output_type -> google.protobuf.Empty - 53, // 132: saas.accounts.v1.ModuleCapabilitiesService.ListSubscriptions:output_type -> saas.accounts.v1.ModuleListSubscriptionsResponse - 55, // 133: saas.accounts.v1.ModuleCapabilitiesService.ReplayEvents:output_type -> saas.accounts.v1.ModuleReplayEventsResponse - 87, // 134: saas.accounts.v1.ModuleCapabilitiesService.ResolveWorkContextAuthority:output_type -> saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse - 102, // [102:135] is the sub-list for method output_type - 69, // [69:102] is the sub-list for method input_type - 69, // [69:69] is the sub-list for extension type_name - 69, // [69:69] is the sub-list for extension extendee - 0, // [0:69] is the sub-list for field type_name + 98, // 19: saas.accounts.v1.ModuleApproval.expires_at:type_name -> google.protobuf.Timestamp + 98, // 20: saas.accounts.v1.ModuleApproval.escalate_at:type_name -> google.protobuf.Timestamp + 98, // 21: saas.accounts.v1.ModuleApproval.created_at:type_name -> google.protobuf.Timestamp + 99, // 22: saas.accounts.v1.ModuleEmitAuditEventRequest.fields:type_name -> google.protobuf.Struct + 98, // 23: saas.accounts.v1.ModuleJournalEntry.occurred_at:type_name -> google.protobuf.Timestamp + 99, // 24: saas.accounts.v1.ModuleJournalEntry.data:type_name -> google.protobuf.Struct + 32, // 25: saas.accounts.v1.ModuleReadJournalResponse.entries:type_name -> saas.accounts.v1.ModuleJournalEntry + 0, // 26: saas.accounts.v1.ModuleAuditFieldDeclaration.kind:type_name -> saas.accounts.v1.ModuleAuditFieldKind + 34, // 27: saas.accounts.v1.ModuleAuditEventTypeDeclaration.fields:type_name -> saas.accounts.v1.ModuleAuditFieldDeclaration + 1, // 28: saas.accounts.v1.ModuleAuditEventTypeDeclaration.visibility:type_name -> saas.accounts.v1.ModuleAuditEventVisibility + 35, // 29: saas.accounts.v1.ModuleDeclareAuditEventTypesRequest.types:type_name -> saas.accounts.v1.ModuleAuditEventTypeDeclaration + 98, // 30: saas.accounts.v1.SubjectVisibilityGrant.expires_at:type_name -> google.protobuf.Timestamp + 38, // 31: saas.accounts.v1.ModuleListSubjectVisibilityResponse.grants:type_name -> saas.accounts.v1.SubjectVisibilityGrant + 43, // 32: saas.accounts.v1.FetchDatasourceFilesRequest.files:type_name -> saas.accounts.v1.DatasourceFileRef + 2, // 33: saas.accounts.v1.DatasourceItemReaders.basis:type_name -> saas.accounts.v1.DatasourceItemReadersBasis + 45, // 34: saas.accounts.v1.DatasourceFileHeader.provenance:type_name -> saas.accounts.v1.DatasourceProvenance + 46, // 35: saas.accounts.v1.DatasourceFileHeader.readers:type_name -> saas.accounts.v1.DatasourceItemReaders + 47, // 36: saas.accounts.v1.FetchDatasourceFilesFrame.header:type_name -> saas.accounts.v1.DatasourceFileHeader + 100, // 37: saas.accounts.v1.ModulePublishEventRequest.envelope:type_name -> saas.events.v1.EventEnvelope + 3, // 38: saas.accounts.v1.ModuleSubscribeRequest.delivery:type_name -> saas.accounts.v1.EventDelivery + 3, // 39: saas.accounts.v1.ModuleSubscription.delivery:type_name -> saas.accounts.v1.EventDelivery + 98, // 40: saas.accounts.v1.ModuleSubscription.created_at:type_name -> google.protobuf.Timestamp + 52, // 41: saas.accounts.v1.ModuleSubscribeResponse.subscription:type_name -> saas.accounts.v1.ModuleSubscription + 52, // 42: saas.accounts.v1.ModuleListSubscriptionsResponse.subscriptions:type_name -> saas.accounts.v1.ModuleSubscription + 98, // 43: saas.accounts.v1.ModuleReplayEventsRequest.since:type_name -> google.protobuf.Timestamp + 4, // 44: saas.accounts.v1.ReadableSourceCollection.grant_disclosure:type_name -> saas.accounts.v1.MetadataDisclosure + 60, // 45: saas.accounts.v1.ReadableSourceCollection.read_grants:type_name -> saas.accounts.v1.ReadableCollectionGrant + 61, // 46: saas.accounts.v1.ReadableSourceCollection.sync:type_name -> saas.accounts.v1.CollectionSyncProvenance + 98, // 47: saas.accounts.v1.ReadableCollectionGrant.expires_at:type_name -> google.protobuf.Timestamp + 5, // 48: saas.accounts.v1.CollectionSyncProvenance.stage:type_name -> saas.accounts.v1.SourceSyncStage + 98, // 49: saas.accounts.v1.CollectionSyncProvenance.at:type_name -> google.protobuf.Timestamp + 4, // 50: saas.accounts.v1.CollectionSyncProvenance.requester_disclosure:type_name -> saas.accounts.v1.MetadataDisclosure + 98, // 51: saas.accounts.v1.CollectionSyncProvenance.requested_at:type_name -> google.protobuf.Timestamp + 59, // 52: saas.accounts.v1.ListReadableSourceCollectionsResponse.collections:type_name -> saas.accounts.v1.ReadableSourceCollection + 70, // 53: saas.accounts.v1.ExecutableArtifactIdentity.contracts:type_name -> saas.accounts.v1.ExecutableArtifactContract + 71, // 54: saas.accounts.v1.ModuleExecutableArtifactRequest.identity:type_name -> saas.accounts.v1.ExecutableArtifactIdentity + 6, // 55: saas.accounts.v1.WorkContextSealAnswer.state:type_name -> saas.accounts.v1.WorkContextSealState + 7, // 56: saas.accounts.v1.WorkContextPrincipalEpochAnswer.state:type_name -> saas.accounts.v1.WorkContextPrincipalState + 8, // 57: saas.accounts.v1.WorkContextApprovedBuildAnswer.state:type_name -> saas.accounts.v1.WorkContextApprovedBuildState + 9, // 58: saas.accounts.v1.WorkContextOperationBindingAnswer.state:type_name -> saas.accounts.v1.WorkContextOperationBindingState + 10, // 59: saas.accounts.v1.WorkContextApprovalGrantAnswer.state:type_name -> saas.accounts.v1.WorkContextApprovalGrantState + 77, // 60: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest.seal:type_name -> saas.accounts.v1.WorkContextSealQuestion + 78, // 61: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest.principal_epoch:type_name -> saas.accounts.v1.WorkContextPrincipalEpochQuestion + 79, // 62: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest.approved_build:type_name -> saas.accounts.v1.WorkContextApprovedBuildQuestion + 80, // 63: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest.operation_binding:type_name -> saas.accounts.v1.WorkContextOperationBindingQuestion + 81, // 64: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest.authorization_revision:type_name -> saas.accounts.v1.WorkContextAuthorizationRevisionQuestion + 82, // 65: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest.approval_grant:type_name -> saas.accounts.v1.WorkContextApprovalGrantQuestion + 83, // 66: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse.seal:type_name -> saas.accounts.v1.WorkContextSealAnswer + 84, // 67: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse.principal_epoch:type_name -> saas.accounts.v1.WorkContextPrincipalEpochAnswer + 85, // 68: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse.approved_build:type_name -> saas.accounts.v1.WorkContextApprovedBuildAnswer + 86, // 69: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse.operation_binding:type_name -> saas.accounts.v1.WorkContextOperationBindingAnswer + 87, // 70: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse.authorization_revision:type_name -> saas.accounts.v1.WorkContextAuthorizationRevisionAnswer + 88, // 71: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse.approval_grant:type_name -> saas.accounts.v1.WorkContextApprovalGrantAnswer + 75, // 72: saas.accounts.v1.ModuleCapabilitiesService.GetCurrentInstallation:input_type -> saas.accounts.v1.ModuleCurrentInstallationRequest + 72, // 73: saas.accounts.v1.ModuleCapabilitiesService.ApproveExecutableArtifact:input_type -> saas.accounts.v1.ModuleExecutableArtifactRequest + 72, // 74: saas.accounts.v1.ModuleCapabilitiesService.AuthorizeExecutableArtifact:input_type -> saas.accounts.v1.ModuleExecutableArtifactRequest + 73, // 75: saas.accounts.v1.ModuleCapabilitiesService.RevokeExecutableArtifact:input_type -> saas.accounts.v1.ModuleRevokeExecutableArtifactRequest + 68, // 76: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedReadAudience:input_type -> saas.accounts.v1.ModuleExchangeDelegatedReadAudienceRequest + 69, // 77: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedOperationAudience:input_type -> saas.accounts.v1.ModuleExchangeDelegatedOperationAudienceRequest + 66, // 78: saas.accounts.v1.ModuleCapabilitiesService.CheckWorkContextRecordAccess:input_type -> saas.accounts.v1.CheckWorkContextRecordAccessRequest + 62, // 79: saas.accounts.v1.ModuleCapabilitiesService.ListReadableSourceCollections:input_type -> saas.accounts.v1.ListReadableSourceCollectionsRequest + 64, // 80: saas.accounts.v1.ModuleCapabilitiesService.PlaceRecord:input_type -> saas.accounts.v1.ModulePlaceRecordRequest + 11, // 81: saas.accounts.v1.ModuleCapabilitiesService.EnqueueJob:input_type -> saas.accounts.v1.ModuleEnqueueJobRequest + 13, // 82: saas.accounts.v1.ModuleCapabilitiesService.ClaimJobs:input_type -> saas.accounts.v1.ModuleClaimJobsRequest + 15, // 83: saas.accounts.v1.ModuleCapabilitiesService.HeartbeatJob:input_type -> saas.accounts.v1.ModuleHeartbeatJobRequest + 17, // 84: saas.accounts.v1.ModuleCapabilitiesService.AckJob:input_type -> saas.accounts.v1.ModuleAckJobRequest + 18, // 85: saas.accounts.v1.ModuleCapabilitiesService.NackJob:input_type -> saas.accounts.v1.ModuleNackJobRequest + 19, // 86: saas.accounts.v1.ModuleCapabilitiesService.NotifyUser:input_type -> saas.accounts.v1.ModuleNotifyUserRequest + 21, // 87: saas.accounts.v1.ModuleCapabilitiesService.NotifyOrgAdmins:input_type -> saas.accounts.v1.ModuleNotifyOrgAdminsRequest + 25, // 88: saas.accounts.v1.ModuleCapabilitiesService.RequestApproval:input_type -> saas.accounts.v1.ModuleRequestApprovalRequest + 27, // 89: saas.accounts.v1.ModuleCapabilitiesService.GetApproval:input_type -> saas.accounts.v1.ModuleGetApprovalRequest + 29, // 90: saas.accounts.v1.ModuleCapabilitiesService.CancelApproval:input_type -> saas.accounts.v1.ModuleCancelApprovalRequest + 30, // 91: saas.accounts.v1.ModuleCapabilitiesService.EmitAuditEvent:input_type -> saas.accounts.v1.ModuleEmitAuditEventRequest + 31, // 92: saas.accounts.v1.ModuleCapabilitiesService.ReadJournal:input_type -> saas.accounts.v1.ModuleReadJournalRequest + 36, // 93: saas.accounts.v1.ModuleCapabilitiesService.DeclareAuditEventTypes:input_type -> saas.accounts.v1.ModuleDeclareAuditEventTypesRequest + 39, // 94: saas.accounts.v1.ModuleCapabilitiesService.ListSubjectVisibility:input_type -> saas.accounts.v1.ModuleListSubjectVisibilityRequest + 41, // 95: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceBlob:input_type -> saas.accounts.v1.FetchDatasourceBlobRequest + 44, // 96: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceFiles:input_type -> saas.accounts.v1.FetchDatasourceFilesRequest + 101, // 97: saas.accounts.v1.ModuleCapabilitiesService.MintModuleWorkContext:input_type -> saas.accounts.v1.ModuleMintWorkContextRequest + 102, // 98: saas.accounts.v1.ModuleCapabilitiesService.MintModuleOperationContext:input_type -> saas.accounts.v1.ModuleMintOperationContextRequest + 103, // 99: saas.accounts.v1.ModuleCapabilitiesService.MintSourceOperationContext:input_type -> saas.accounts.v1.ModuleMintSourceOperationContextRequest + 49, // 100: saas.accounts.v1.ModuleCapabilitiesService.PublishEvent:input_type -> saas.accounts.v1.ModulePublishEventRequest + 51, // 101: saas.accounts.v1.ModuleCapabilitiesService.Subscribe:input_type -> saas.accounts.v1.ModuleSubscribeRequest + 54, // 102: saas.accounts.v1.ModuleCapabilitiesService.Unsubscribe:input_type -> saas.accounts.v1.ModuleUnsubscribeRequest + 55, // 103: saas.accounts.v1.ModuleCapabilitiesService.ListSubscriptions:input_type -> saas.accounts.v1.ModuleListSubscriptionsRequest + 57, // 104: saas.accounts.v1.ModuleCapabilitiesService.ReplayEvents:input_type -> saas.accounts.v1.ModuleReplayEventsRequest + 89, // 105: saas.accounts.v1.ModuleCapabilitiesService.ResolveWorkContextAuthority:input_type -> saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest + 76, // 106: saas.accounts.v1.ModuleCapabilitiesService.GetCurrentInstallation:output_type -> saas.accounts.v1.ModuleCurrentInstallationResponse + 74, // 107: saas.accounts.v1.ModuleCapabilitiesService.ApproveExecutableArtifact:output_type -> saas.accounts.v1.ModuleExecutableArtifactResponse + 74, // 108: saas.accounts.v1.ModuleCapabilitiesService.AuthorizeExecutableArtifact:output_type -> saas.accounts.v1.ModuleExecutableArtifactResponse + 74, // 109: saas.accounts.v1.ModuleCapabilitiesService.RevokeExecutableArtifact:output_type -> saas.accounts.v1.ModuleExecutableArtifactResponse + 104, // 110: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedReadAudience:output_type -> saas.accounts.v1.IssuedWorkContext + 104, // 111: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedOperationAudience:output_type -> saas.accounts.v1.IssuedWorkContext + 67, // 112: saas.accounts.v1.ModuleCapabilitiesService.CheckWorkContextRecordAccess:output_type -> saas.accounts.v1.CheckWorkContextRecordAccessResponse + 63, // 113: saas.accounts.v1.ModuleCapabilitiesService.ListReadableSourceCollections:output_type -> saas.accounts.v1.ListReadableSourceCollectionsResponse + 65, // 114: saas.accounts.v1.ModuleCapabilitiesService.PlaceRecord:output_type -> saas.accounts.v1.ModulePlaceRecordResponse + 12, // 115: saas.accounts.v1.ModuleCapabilitiesService.EnqueueJob:output_type -> saas.accounts.v1.ModuleEnqueueJobResponse + 14, // 116: saas.accounts.v1.ModuleCapabilitiesService.ClaimJobs:output_type -> saas.accounts.v1.ModuleClaimJobsResponse + 16, // 117: saas.accounts.v1.ModuleCapabilitiesService.HeartbeatJob:output_type -> saas.accounts.v1.ModuleHeartbeatJobResponse + 105, // 118: saas.accounts.v1.ModuleCapabilitiesService.AckJob:output_type -> google.protobuf.Empty + 105, // 119: saas.accounts.v1.ModuleCapabilitiesService.NackJob:output_type -> google.protobuf.Empty + 20, // 120: saas.accounts.v1.ModuleCapabilitiesService.NotifyUser:output_type -> saas.accounts.v1.ModuleNotifyUserResponse + 22, // 121: saas.accounts.v1.ModuleCapabilitiesService.NotifyOrgAdmins:output_type -> saas.accounts.v1.ModuleNotifyOrgAdminsResponse + 26, // 122: saas.accounts.v1.ModuleCapabilitiesService.RequestApproval:output_type -> saas.accounts.v1.ModuleRequestApprovalResponse + 28, // 123: saas.accounts.v1.ModuleCapabilitiesService.GetApproval:output_type -> saas.accounts.v1.ModuleApproval + 105, // 124: saas.accounts.v1.ModuleCapabilitiesService.CancelApproval:output_type -> google.protobuf.Empty + 105, // 125: saas.accounts.v1.ModuleCapabilitiesService.EmitAuditEvent:output_type -> google.protobuf.Empty + 33, // 126: saas.accounts.v1.ModuleCapabilitiesService.ReadJournal:output_type -> saas.accounts.v1.ModuleReadJournalResponse + 37, // 127: saas.accounts.v1.ModuleCapabilitiesService.DeclareAuditEventTypes:output_type -> saas.accounts.v1.ModuleDeclareAuditEventTypesResponse + 40, // 128: saas.accounts.v1.ModuleCapabilitiesService.ListSubjectVisibility:output_type -> saas.accounts.v1.ModuleListSubjectVisibilityResponse + 42, // 129: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceBlob:output_type -> saas.accounts.v1.FetchDatasourceBlobChunk + 48, // 130: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceFiles:output_type -> saas.accounts.v1.FetchDatasourceFilesFrame + 106, // 131: saas.accounts.v1.ModuleCapabilitiesService.MintModuleWorkContext:output_type -> saas.accounts.v1.ModuleMintWorkContextResponse + 107, // 132: saas.accounts.v1.ModuleCapabilitiesService.MintModuleOperationContext:output_type -> saas.accounts.v1.ModuleMintOperationContextResponse + 108, // 133: saas.accounts.v1.ModuleCapabilitiesService.MintSourceOperationContext:output_type -> saas.accounts.v1.ModuleMintSourceOperationContextResponse + 50, // 134: saas.accounts.v1.ModuleCapabilitiesService.PublishEvent:output_type -> saas.accounts.v1.ModulePublishEventResponse + 53, // 135: saas.accounts.v1.ModuleCapabilitiesService.Subscribe:output_type -> saas.accounts.v1.ModuleSubscribeResponse + 105, // 136: saas.accounts.v1.ModuleCapabilitiesService.Unsubscribe:output_type -> google.protobuf.Empty + 56, // 137: saas.accounts.v1.ModuleCapabilitiesService.ListSubscriptions:output_type -> saas.accounts.v1.ModuleListSubscriptionsResponse + 58, // 138: saas.accounts.v1.ModuleCapabilitiesService.ReplayEvents:output_type -> saas.accounts.v1.ModuleReplayEventsResponse + 90, // 139: saas.accounts.v1.ModuleCapabilitiesService.ResolveWorkContextAuthority:output_type -> saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse + 106, // [106:140] is the sub-list for method output_type + 72, // [72:106] is the sub-list for method input_type + 72, // [72:72] is the sub-list for extension type_name + 72, // [72:72] is the sub-list for extension extendee + 0, // [0:72] is the sub-list for field type_name } func init() { file_saas_accounts_v1_module_capabilities_proto_init() } @@ -6750,11 +7023,11 @@ func file_saas_accounts_v1_module_capabilities_proto_init() { } file_saas_accounts_v1_work_contexts_proto_init() file_saas_accounts_v1_module_registration_proto_init() - file_saas_accounts_v1_module_capabilities_proto_msgTypes[34].OneofWrappers = []any{ + file_saas_accounts_v1_module_capabilities_proto_msgTypes[37].OneofWrappers = []any{ (*FetchDatasourceFilesFrame_Header)(nil), (*FetchDatasourceFilesFrame_Data)(nil), } - file_saas_accounts_v1_module_capabilities_proto_msgTypes[75].OneofWrappers = []any{ + file_saas_accounts_v1_module_capabilities_proto_msgTypes[78].OneofWrappers = []any{ (*ModuleResolveWorkContextAuthorityRequest_Seal)(nil), (*ModuleResolveWorkContextAuthorityRequest_PrincipalEpoch)(nil), (*ModuleResolveWorkContextAuthorityRequest_ApprovedBuild)(nil), @@ -6762,7 +7035,7 @@ func file_saas_accounts_v1_module_capabilities_proto_init() { (*ModuleResolveWorkContextAuthorityRequest_AuthorizationRevision)(nil), (*ModuleResolveWorkContextAuthorityRequest_ApprovalGrant)(nil), } - file_saas_accounts_v1_module_capabilities_proto_msgTypes[76].OneofWrappers = []any{ + file_saas_accounts_v1_module_capabilities_proto_msgTypes[79].OneofWrappers = []any{ (*ModuleResolveWorkContextAuthorityResponse_Seal)(nil), (*ModuleResolveWorkContextAuthorityResponse_PrincipalEpoch)(nil), (*ModuleResolveWorkContextAuthorityResponse_ApprovedBuild)(nil), @@ -6776,7 +7049,7 @@ func file_saas_accounts_v1_module_capabilities_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_module_capabilities_proto_rawDesc), len(file_saas_accounts_v1_module_capabilities_proto_rawDesc)), NumEnums: 11, - NumMessages: 77, + NumMessages: 80, NumExtensions: 0, NumServices: 1, }, diff --git a/libraries/source-read-sdk/library.codefly.yaml b/libraries/source-read-sdk/library.codefly.yaml index 0171cc00f..d588416cd 100644 --- a/libraries/source-read-sdk/library.codefly.yaml +++ b/libraries/source-read-sdk/library.codefly.yaml @@ -12,7 +12,7 @@ sources: module: saas-starter service: accounts endpoint: authority - contract-digest: sha256:743b63bad9a546f105e994c754d845865e189465e6c72816e5785efe38ab7831 + contract-digest: sha256:12c6216596c7460c71bf83e3962a3724c19bd79401459088a51f7f3b3fc1e3d9 services: - ModuleCapabilitiesService generated-by: diff --git a/module/DATABASE_AUTHORITY.md b/module/DATABASE_AUTHORITY.md index 5d6e35fe9..ba6f71476 100644 --- a/module/DATABASE_AUTHORITY.md +++ b/module/DATABASE_AUTHORITY.md @@ -538,10 +538,21 @@ a live database. Migration `65_role_permissions_rls` closes the former child-table gap: `role_permissions` reads follow parent-role visibility, while inserts may target only a custom role owned by the current tenant. -Callable-source declarations, consumed OAuth states, operation attempts and -effect receipts grant no table privileges to `app_control_plane`. Their reads, -writes and retention use the tenant transaction. The live exact-grants test -asserts this absence for all four tables as well as their forced tenant RLS. +The published scope column describes the permissive policies that grant request +traffic access. Every such predicate is checked separately: another permissive +policy cannot grant unscoped access beside the scoped one. Restrictive policies +are combined with those grants using AND and can only remove access; they need +not repeat the tenant comparison. The live checks separately pin exact +control-plane grants and the audit-retention filter's role, command, restrictive +mode, audit type and 720-hour minimum age. + +Callable-source declarations, consumed OAuth states and operation attempts grant +no table privileges to `app_control_plane`. Their reads, writes and retention use +the tenant transaction. Effect receipts give the control plane SELECT only, +with RLS exposing exactly `AuditService/DeliverAuditEvents` receipts for audit +journal retention (migration 63). Other responses remain tenant-only; the +control plane cannot insert, update or delete receipts. The live exact-grants +test asserts these grants alongside forced tenant RLS. ## Partitions carry their parent's row security @@ -1137,4 +1148,11 @@ existing encrypted envelope. No control-plane role can read either new relation. attempt markers under the request pool's RLS. Expired response bytes, a typed audit event and the cleanup operation's own receipt commit atomically. Compact markers are retained so expiry cannot authorize redispatch of a mutation. -No control-plane grant or host retention timer is added. +Audit delivery receipts are outside that cleanup's method filter, so the full +committed range survives. `RunRetention` uses the existing control-plane role to +verify audit receipt coverage and retire journal entries in warehouse mode only. +Migration 48 adds a restrictive DELETE policy to `domain_events`: even a direct +control-plane DELETE cannot remove another event type or an audit entry younger +than 720 hours. SELECT, INSERT and UPDATE keep their existing policies. Receipt +coverage remains an application check over the SDK's protobuf response and +original request digest, exercised with real PostgreSQL transactions. diff --git a/module/EVENTS.md b/module/EVENTS.md index 1c053d8c4..0c4f06b23 100644 --- a/module/EVENTS.md +++ b/module/EVENTS.md @@ -103,7 +103,10 @@ Rules: - `type` is immutable once published. A breaking change is a new `type` or a new major in `dataschema`; it is never a re-typed field under the same name. - `tenant_id` is required except for platform-scope events published by a - platform principal. + platform principal. In this document **platform principal** means a module + principal whose grant is `cross_tenant` (the `cross_tenant` key of the module + envelope's grant ceiling): the same test that admits a system-scoped + `ModuleEmitAuditEvent`, and the only one that reads across tenants. - `partition_key` is the only ordering knob a producer sets; it is the contract form of `JobOrderingKey`. Absent, the event is unordered. - `data` is capped at 960 KiB — below the 1 MiB `job_messages.payload` cap @@ -277,6 +280,85 @@ type Transport interface { subscriber in tenant A never sees tenant B), and zero-subscriber publish. A consumer written against `FakeTransport` runs unchanged against Postgres. +## The audit journal type + +`saas.audit.recorded.v1` is the journal record of the warehouse audit backend +([ADR 0009](./docs/adr/0009-audit-store-swap.md)). Under that backend every audit +entry, including one with no organization (`tenant_id` empty), is published as +this type in the writer's own transaction, and its `data` is the whole entry +(`id`, `event_type`, `schema_version`, actor, resource, `organization_id`, +`payload`, client, impersonation and occurrence time). Its envelope `id` is +derived from the audit entry id, because the entry id is already the id of the +organization's webhook event, and an `id` is unique in the journal. + +The writer may be a user-scoped transaction, which runs as `app_tenant` with only +`app.current_user_id` set. `publish_domain_event` refuses a tenant-less event from +`app_tenant` except this one: the type is `saas.audit.recorded.v1`, +`app.current_user_id` is set and non-empty, and the envelope's `actor_principal_id` +(the entry's actor id, for a person their user id) is that same user. That is the +rule the `audit_events_tenant` policy applies to the same entry's row, so the +journal admits what `audit_events` admits and nothing wider (migration 61). Any +other tenant-less event from `app_tenant`, and any event whose tenant is not the +transaction's signed scope, is refused as before. + +It is code-owned (in the audit events contribution, not an audit registry type), +`visibility: internal` and declares no partition. It is therefore never fanned +out, relayed or replayed to a subscriber, and no subscription pattern may match +it; the `saas` namespace stays unsubscribable by a module. The only reader is a +platform principal (a `cross_tenant` grant), through the cross-tenant journal read +below. The `data` limit below applies: an entry too large for it is refused, not +truncated. + +### The cross-tenant journal read + +`Service.ModuleReadJournal` (the host side of `ReadJournal` on +`ModuleCapabilitiesService`) serves journal rows of every tenant, and the rows with +no tenant, to a module principal whose grant is `cross_tenant`, the gate a +system-scoped `ModuleEmitAuditEvent` has. It reads on the control plane, enqueues +nothing and does not touch `pkg/jobs`. A caller names the event types it reads, +exactly and at least one, so no principal reads every event of every tenant by +leaving the filter out. + +`seq` is the order rows were inserted, not the order their transactions commit, so +a cursor on `seq` can pass a row whose transaction is still open and never return +for it (the next section's re-read window exists for that). This read instead keys +on `(xact_id, seq)`, where `domain_events.xact_id` is the id of the transaction +that wrote the row (migration 62), and serves a row only while +`xact_id < pg_snapshot_xmin(pg_current_snapshot())`, the oldest transaction still +running. Every transaction that can still commit took its id at or after that +bound, so a row below it belongs to a finished transaction, no row that has not +appeared yet can sort before one that has, and a cursor that stops at the last row +served skips nothing. A row written before migration 62 has no `xact_id`, reads as +transaction 0 and sorts first, in `seq` order. + +The position is an opaque string, `v1..` in decimal. The literal +`"start"` is also a valid position, meaning "before the oldest retained entry": a +reader with nothing read passes it as `after`, and `""` is accepted as an alias. No +entry's position is ever `"start"`, and it exists so the first page of a journal has +a non-empty position to correlate on. A malformed position is `InvalidArgument`. +`next_after` is the last entry's position, or the request's `after` (`"start"` +included) when the page is empty. A page is at most 500 +entries and 2 MiB of event data (the first row is always served). The read holds +behind a write transaction that stays open, and behind rows whose transaction ids +are ahead of the cluster after a logical restore into a new one: both stall the +reader and lose nothing. + +Each entry says whether it is `delivery_machinery`, computed here so a caller never +parses this module's payloads: the entry the row carries is a +`saas.module.operation_context_minted` or `saas.module.delegated_audience_exchange` +on one of the `delivery_bindings` the caller names (both carry `binding_id`), or a +`saas.module.work_context_minted` for the caller's own module principal. It is the +caller's own delivery footprint, which delivered as ordinary work would produce more +of itself. + +`DeliverAuditEvents{from, to}` (an operation on `AuditService`, effect id +`saas.audit.recorded.v1/`) appends the `saas.audit.recorded.v1` records with a +position in `(from, to]` to the audit storage. `from` is a position or `"start"`, +never empty; the end must be below the oldest transaction still running and the range +at most 500 records, or it is refused before anything is appended. Each record's +organization is its row's, never the caller's. It writes no audit of its own, and one +receipt per range is committed in the installation's tenant. + ## Authority - **Publish** is allowed only for types in the caller principal's declared @@ -313,6 +395,15 @@ The properties a consumer may rely on: | Size | `data` <= 960 KiB; larger payloads use a claim-check reference | | Latency | relay tick <= 1 s p50 on Postgres; not a hard real-time channel | +`RunRetention` enforces the thirty-day journal window only for +`saas.audit.recorded.v1` under the warehouse audit backend, and only where a +committed `DeliverAuditEvents` receipt proves the row's position was delivered. +Receipt responses retain the exact `(from, to]` interval; missing legacy coverage +or an invalid receipt keeps the row. The cutoff uses `created_at` and the database +clock, independently of the producer's timestamp. Each run logs the number of old +audit entries with unproven delivery. PostgreSQL audit mode does not remove these +journal rows, and retention for the other event types remains unenforced. + ## The subscriptions stream `GET /v1/subscriptions/stream` is the signed-in person's live view of their diff --git a/module/configurations/local/audit-storage.env b/module/configurations/local/audit-storage.env new file mode 100644 index 000000000..6c9ac1cd2 --- /dev/null +++ b/module/configurations/local/audit-storage.env @@ -0,0 +1,23 @@ +# Where the audit trail is kept. +# +# The trail is kept in ONE place, chosen here and never per call: +# +# postgres audit_events, written in the same transaction as the change it +# records. The default, and the only backend this build serves. +# warehouse a warehouse table. The warehouse cannot join a change's +# transaction, so each entry is published to the journal in that +# transaction and appended to the warehouse afterwards; the +# warehouse REPLACES audit_events as the store of record, it is not +# a second copy beside it. Refused at startup until the warehouse +# storage exists in the build, and refused with AUDIT_SINK=both. +# +# AUDIT_STORAGE_BACKEND postgres | warehouse. Empty is postgres. +# AUDIT_STORAGE_DATASET the warehouse dataset. Required for `warehouse`. +# AUDIT_STORAGE_TABLE the warehouse table. Required for `warehouse`. +# +# This group carries NAMES and nothing else: no endpoint, project, address, +# credential or token. The warehouse is reached through the service the +# composition provides. +AUDIT_STORAGE_BACKEND=postgres +AUDIT_STORAGE_DATASET= +AUDIT_STORAGE_TABLE= diff --git a/module/contracts/accounts/saas.events.codefly.yaml b/module/contracts/accounts/saas.events.codefly.yaml index 0ef82f641..fc3f6c1d1 100644 --- a/module/contracts/accounts/saas.events.codefly.yaml +++ b/module/contracts/accounts/saas.events.codefly.yaml @@ -15,6 +15,11 @@ # serialize every audited mutation in an organization. Nothing consumes the # ordering: an outbound webhook is dispatched in subscription-id order, and the # platform namespace is not subscribable by a module. +# +# The last entry, saas.audit.recorded.v1, is the one type here that is not an +# audit event: the journal record of the warehouse audit backend, carrying the +# whole entry. It is internal, so it is never fanned out, relayed or subscribable; +# only the platform's own reader and the delivery task read it. schema: codefly/saas/events-contribution/v1 namespace: saas queues: [] @@ -811,3 +816,7 @@ publishes: schema: saas/events/v1/events.proto#EventEnvelope visibility: external retention: 30d + - type: saas.audit.recorded.v1 + schema: saas/events/v1/events.proto#EventEnvelope + visibility: internal + retention: 30d diff --git a/module/contracts/api/accounts/authority/contract.binpb b/module/contracts/api/accounts/authority/contract.binpb index 8cfc99aa6440f15bb3a019fd7983d61c3a6f64b3..4546de754e8ab5d76598fed0450582ad88512445 100644 GIT binary patch delta 43645 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 W*4fkNH1Mwr|H_@y=XC9N@c#qN5IZ>l delta 35097 zcmZ9VdAtG-t-Y&?%O89`{MO!U?X`bv@3n`j zuCwmyGaj1$i{aNf*SXhauk)_+uM4gVr{?-ha$mW<-fvQFQoZ%&YPUxFr*l^|WeiR#{=Tw@Gs*hV<>{yuUM6Q>qdQF*q>dWWkre>RR)%pc5R$81&{jV=p zdevWevC_8I92Fzq%eanH%~tYlVw457Yj2V3SF>&0TvUiWYPteC#zm;L$+wLWYHgZ3 zSd>Mridr94qp0fk>)EOG%D?{k(!X4J=_Qw5(t7dV>*vkOHD6lImt!5NR*mgtbD(y8 zqf!*ma5g7}?fclO9_HI7UU>rI&Ukp$HtCFqTHAK*>q}m$wA4CTZJ#Rb^6g?om3AHC zP6b$QpI8nA#1#Y7+9%!ysI^bL4XN4ewvT#ge?tL>_H5p(zkiyagY@QZw&4&9%{_X? zi2gSBI>h!jl>RpFGsG6=@Tx=N6*W5~QOMy{ha?K3)**>P9)0uJ5Zn#`lu zu}e&79=(qH_P1pb&2^!<&m8Se1gZ~NklS4Ak~9>d(4{$Mt$;#TH?$2E1az^s3bfNT z7e}XnTGvjUZCMfjy3rFexQYT|hl;3m>(m-oM6Fw|-e+1^qPgw~9VG$rz$>BGJqdpa zz3%Pe#+y*~3| zWXAPR)R=MoT|1u~VJh3-&L7$D=tE9Ko=%ajpXIHG7ksXS>(7OEevEJ7L}^7>J4-c@~vWs;=r6qbJRp{Al)me zl`u!u=7YSb66G)&=0-3hyn!7K$|A|f61shWmyabisf3jSy`pVFrs=^6NkrjbJ53!| zrs=_U_JB}3*iKW&&7(FbQDd48a!snD2(>|pn#|HccG5ZSDKbq5Cwk1%!FC!rZiL?8 zB$P5)2is}jxCQ7ulw5U%a>Qgkw7%li+<*2zwAoBPYEb`Bn++VdNHd4i%s81gKv4bi z*K&{dKip0mWGEcYw2}41abYVDeiKt^uq3_O%f1rpFh9RWb+ z83nUmg3vpo*(3@O*3al}hPbQq|4bV%P&Ip|YkC6^lsD-VgxZ;%)kuxzWZIl%vnxnI zTvM!^l{0H5Md+Qyu#)V>@I5Cp%ydR3_BnJ$`jXY`^(*TJsG9wi&0YX1{>o+*5azo4 z%BGd9x_>TuUEu&!&7NET&GKCLVdo|a;`g~peR2D|q#fKo&t{secK$p&JVB_PXERM! z6V&;rNwx!3v**{>FDKibpEM(r?);<~*(jWEvmNziYMpPhUDlPfbwT0-LA=0HM-e@^ zz)md?Y8TiUnRR_?UP!nflo1T9dKbFxMfLw)k$bRqp&gKjv2dXskRU8v*xEw}B!~fd zVb6=qfE1_$^1@yhnE`1fkn(FgAgx3Pt+U|r$M~LjtNjT zdx>l234ofHx-D^$pu8E!6rpx$N0Wvag3$Ooqjr!? z06}I|1)%pk8%q#+zay5@UB)21U7q8$(~6zKZ&jB^tnL_Nscz%G{KJSLK_mg1CEC8?%c0>h9lA_@&mz z+rPOTTTZI)TA4fXkjQF?nZMcaqk;b4Z1{a0f`3Q{Hr&az=pYEBqu$kIdz$n-prW>MA^VG~9W2TUL?ILQY6p`B zvRCk~NmZ!}>JS$yB$?W}s9xi=Z&p|fr2F2rsV->^)X7$+-#}Jtqf5tux?HWz+FnM3W&O{prK0d=%>r874^6G5@6Yz=QoRVCNn606oi+4Fd}I_zE~>_A1mtxqHk3fcA;7s(nx z_B{r&@9{>ass{Cn3sD{Eu=TNZQ61^9^)ZmGk9RwZZ!Wr9=1J(X`?M30$f^1%8L>vH3d z9PN-AjWg_9s8sD7Lrjczy2YK2FmbP#kS$2)Mxc6^^|}8%@?NJ9bIcBgIvwK9vV{Tm zh^Z)GVT@QfSuGSm^~LLRrye=RDRi}sa`K=~x0rY~HNbxR*-~~j-h=k>o4n< zFUfVPKe!^-zdrDz+~V5gG`ww~cCy3LXWDLq*~w08�^ercF-l^qT5;Y_)O(xQ(uU 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" field; non-numeric payload values are // ignored. string field = 2; - // percentile ∈ (0,1], used only when op == percentile (0.95 → p95). + // percentile ∈ (0,1], used only when op == percentile (0.95 → p95). Only the + // Postgres audit backend computes it: the warehouse backend keeps entries + // at-least-once and deduplicates them on read, and a percentile over that is not + // an exact answer, so a deployment on the warehouse backend refuses a query that + // asks for one with InvalidArgument. Every other op is served by both backends. double percentile = 3; // alias names this metric in the response map. Defaults to "count" for count, // else "_". @@ -238,6 +243,43 @@ message ListAuditEventTypesResponse { repeated AuditEventType types = 1; } +// DeliverAuditEventsRequest names one range of the platform journal, (from, to], +// whose audit records the operation appends to the warehouse audit storage. Both +// ends are positions a ModuleReadJournal page returned; from may instead be the +// literal "start", meaning before the oldest retained entry, and is never empty so +// that the first range of a fresh journal has a non-empty value to correlate on. +// The range ends at a position that is final (below the oldest transaction still +// running) and holds at most 500 audit records, or it is refused before anything +// is appended. The effect id is "saas.audit.recorded.v1/", derived by the +// host: one range, one receipt. A runtime task may carry its own transport +// effect identity; the range receipt is independent of that task identity. +message DeliverAuditEventsRequest { + string from = 1 [(buf.validate.field).string = { + min_len: 1 + max_len: 64 + }]; + string to = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 64 + }]; +} + +message DeliverAuditEventsResponse { + string effect_id = 1; + // status is "committed" when the range is recorded. An absent lookup + // returns NOT_FOUND: absence is inconclusive, never a committed response. + string status = 2; + // delivered is the number of audit records of the range appended to the + // storage; a range with none still commits a receipt. + int32 delivered = 3; + // The exact committed interval is (from, to]. Retention verifies these + // positions against the receipt's effect identity and request digest before + // retiring journal rows. Legacy receipts without both positions prove no + // coverage; delivered alone never proves an interval. + string from = 4; + string to = 5; +} + // AuditService — append-only audit event log service AuditService { rpc QueryAuditLog(QueryAuditLogRequest) returns (QueryAuditLogResponse) { @@ -322,4 +364,68 @@ service AuditService { body: "*" }; } + + // DeliverAuditEvents appends one range of the journal's audit records to the + // warehouse audit storage: the operation module-runtime calls after reading the + // range through ReadJournal. It is an operation, not a sweep. It takes each + // record's organization from the journal row and never from the caller, emits no + // audit of its own, and commits one receipt per range under the installation's + // tenant; delivery is at least once, and every read of the storage returns each + // entry once by id. Its receipt is read back through LookupDeliverAuditEvents. + rpc DeliverAuditEvents(DeliverAuditEventsRequest) returns (DeliverAuditEventsResponse) { + option (saas.policy.v1.method_policy) = { + exposure: EXPOSURE_AUTHENTICATED + // Headless module principals are authorized by their live grant and + // saas.audit-delivery scope, not by a human organization membership. + tenant: TENANT_REQUIREMENT_NONE + mfa: MFA_REQUIREMENT_NONE + audit: {emission: AUDIT_EMISSION_NONE} + idempotency: IDEMPOTENCY_REQUIREMENT_REQUIRED + rate_limit: RATE_LIMIT_CLASS_STANDARD_WRITE + request_sensitivity: SENSITIVITY_INTERNAL + response_sensitivity: SENSITIVITY_INTERNAL + platform_role: PLATFORM_ROLE_REQUIREMENT_NONE + }; + option (codefly.runnable.v0.operation) = { + attempt_timeout: {seconds: 60} + total_timeout: {seconds: 600} + max_attempts: 3 + backoff: {seconds: 5} + retryable_codes: [ + "UNAVAILABLE", + "RESOURCE_EXHAUSTED", + "ABORTED" + ] + audience: "saas-audit" + required_scope_slots: { + name: "audit_delivery" + required_actions: [ + "invoke", + "read" + ] + lookup: true + } + lookup_method: "/saas.accounts.v1.AuditService/LookupDeliverAuditEvents" + max_input_bytes: 256 + max_output_bytes: 1024 + completion: COMPLETION_CALL + }; + } + // LookupDeliverAuditEvents takes the invocation's same {from, to} body and + // derives the range receipt identity from from. It never delivers. + rpc LookupDeliverAuditEvents(DeliverAuditEventsRequest) returns (DeliverAuditEventsResponse) { + option (saas.policy.v1.method_policy) = { + exposure: EXPOSURE_AUTHENTICATED + // Headless module principals are authorized by their live grant and + // saas.audit-delivery scope, not by a human organization membership. + tenant: TENANT_REQUIREMENT_NONE + mfa: MFA_REQUIREMENT_NONE + audit: {emission: AUDIT_EMISSION_NONE} + idempotency: IDEMPOTENCY_REQUIREMENT_FORBIDDEN + rate_limit: RATE_LIMIT_CLASS_STANDARD_READ + request_sensitivity: SENSITIVITY_INTERNAL + response_sensitivity: SENSITIVITY_INTERNAL + platform_role: PLATFORM_ROLE_REQUIREMENT_NONE + }; + } } diff --git a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/module_capabilities.proto b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/module_capabilities.proto index 1eb2b358e..07ad13bfc 100644 --- a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/module_capabilities.proto +++ b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/module_capabilities.proto @@ -355,6 +355,69 @@ message ModuleEmitAuditEventRequest { string idempotency_key = 7 [(buf.validate.field).string.max_len = 255]; } +// ModuleReadJournalRequest reads the platform journal across every tenant. Only a +// module principal whose grant is cross_tenant may call it, the gate a +// system-scoped EmitAuditEvent has; any other caller is PermissionDenied. +message ModuleReadJournalRequest { + // after is the position the previous page ended at (a page's next_after). The + // literal "start" is a valid position meaning "before the oldest retained + // entry", and the empty string is accepted as an alias of it; so a reader with + // nothing read yet has a non-empty position to correlate its first page on. + // Positions are opaque: store one and hand it back unread. No entry's position + // is ever "start". A value this host did not issue is InvalidArgument. + string after = 1 [(buf.validate.field).string.max_len = 64]; + // limit is the page size: 0 is 100, and more than 500 is served as 500. A + // page is also bounded to 2 MiB of event data, though the first entry is always + // served. + int32 limit = 2 [(buf.validate.field).int32.gte = 0]; + // types are the event types to read, exactly (no pattern). At least one is + // required, so no caller reads every tenant's every event by leaving the + // filter out. + repeated string types = 3 [(buf.validate.field).repeated = { + min_items: 1 + max_items: 16 + items: {string: {min_len: 1, max_len: 256}} + }]; + // delivery_bindings are the binding ids the caller's own delivery of the journal + // runs under. They decide which entries are marked delivery_machinery. + repeated string delivery_bindings = 4 [(buf.validate.field).repeated = { + max_items: 64 + items: {string: {min_len: 1, max_len: 256}} + }]; +} + +// ModuleJournalEntry is one journal row. Rows are served in the order their +// writing transactions are final (see ModuleReadJournalRequest), so a reader that +// resumes from the last position it was given skips nothing. +message ModuleJournalEntry { + // position is where the row sits in the read order: opaque, and the value a + // reader passes as after to resume past it. + string position = 1; + string event_id = 2; + string type = 3; + // organization_id is the row's tenant, empty for a row that has none. + string organization_id = 4; + google.protobuf.Timestamp occurred_at = 5; + // data is the event's JSON object; empty when the event's data is not one. + google.protobuf.Struct data = 6; + // delivery_machinery is true when the entry the row carries is the footprint of + // the caller's own delivery of the journal: the host computes it, so a caller + // never parses this module's payloads. It is true for a + // saas.module.operation_context_minted or + // saas.module.delegated_audience_exchange entry whose binding_id is one of the + // request's delivery_bindings, and for a saas.module.work_context_minted entry + // of the caller's own module principal. + bool delivery_machinery = 7; +} + +// ModuleReadJournalResponse is one page and where to resume. +message ModuleReadJournalResponse { + repeated ModuleJournalEntry entries = 1; + // next_after is the last entry's position, or the request's after when the page + // is empty. + string next_after = 2; +} + // ModuleAuditFieldKind is the kind of one declared audit payload field: the // same kinds a solution may declare in its manifest. enum ModuleAuditFieldKind { @@ -1656,6 +1719,23 @@ service ModuleCapabilitiesService { platform_role: PLATFORM_ROLE_REQUIREMENT_NONE }; } + // ReadJournal serves the platform journal across every tenant, and the rows with + // no tenant, to a module principal whose grant is cross_tenant. Rows are served + // in the order their writing transactions are final, so a reader that resumes + // from next_after skips nothing; it enqueues nothing and is not a replay. + rpc ReadJournal(ModuleReadJournalRequest) returns (ModuleReadJournalResponse) { + option (saas.policy.v1.method_policy) = { + exposure: EXPOSURE_INTERNAL + tenant: TENANT_REQUIREMENT_NONE + mfa: MFA_REQUIREMENT_NONE + audit: {emission: AUDIT_EMISSION_NONE} + idempotency: IDEMPOTENCY_REQUIREMENT_FORBIDDEN + rate_limit: RATE_LIMIT_CLASS_INTERNAL + request_sensitivity: SENSITIVITY_CONFIDENTIAL + response_sensitivity: SENSITIVITY_CONFIDENTIAL + platform_role: PLATFORM_ROLE_REQUIREMENT_NONE + }; + } // DeclareAuditEventTypes admits the audit event types a composed module owns, // into the namespaces the operator bound to it; see the request. rpc DeclareAuditEventTypes(ModuleDeclareAuditEventTypesRequest) returns (ModuleDeclareAuditEventTypesResponse) { diff --git a/module/contracts/api/accounts/connect/contract.binpb b/module/contracts/api/accounts/connect/contract.binpb index 8cfc99aa6440f15bb3a019fd7983d61c3a6f64b3..4546de754e8ab5d76598fed0450582ad88512445 100644 GIT binary patch delta 43645 zcma*Qd7MD#w&&vXyXK5WgRf*^v5G2({1sJJE?lP5aDs1uPPGoyKY^8Kbk z5L^HSg;oR<_XTlxR2KJL+%@jvj?uVp(Wu||IaT*wLh`(RzdztLpL6R}o%&SOsZ-1C zyX?+`-k5&yw3*IKccwSfpP8K*%*@>yZoVS>;Vn+oxAu9T`mVKIR)mXcYhH_n)aDh# zew|;e2s?_^LBEQ6@7X-L<&@^OaVJk|>DcR(<|!TRhc^H5ZPm|6un>_Za7u zFMD0>L~giK*$ZFtdwGN0Fk4%?O<{O3YHw<4->ON-9RG{0YrCxqdmMLIbK5B`6Pvd_ zptbeHlc)ZBfd@}>kDL}BIcS-3O6!TuWxuVtX;Lqze2x?K`d_m7 zUnXbXEu*+rH+24 z?3Xf|Ion)S`)om%LuxzU9UkMA!g6ieD-DhP8lt)=>u21IQ}!D2g&29DVyV{oO2Z-B z_p(BlB;|b97?Gshqo+kuU{^PcHjjKV<5Abz@9znR)()7FFZVSicp=hmTSWCyAW1?= z)P3{X?_ODH~gMOljIP7wtyLP78N5o+D@-C~4V_nxuU0V#VV zYJq?_VSrkXd?~8iOeqM^>rsyDmm_IUw|g{Li^~COZTE(oReSd6Z_CM%zURO}F`{}s z2XAkymy_!C95Ti}3^AftVgxCBxv_mAM)XQp6}4Uot9fkb?e?)%&I|a(2%BXdz21E{ zj}dykhYhzVqPTt(_cuq4k3h8p?+ZV!_Dc$iQ0UhqHllz+e>Z0fDhTLn^D0nI|1fT# z0&4wx_p#6Fux|i0F-=k@AU3EDwE?{=aYEDv4BqSz3+pMalF(5vAnr)@=v9&)P>)`v zTU>Y%wIQfIVX9RG)$$L7PgaNI%QojC3PZa0j3WBIQ}(yWOmO(y^BuC?A~bb!+zc5F zwdI2Zm_7j@;ZW%lbO{jI4YgguaUIl#C2Axb=BjCPFg8ud=!Ot(!)q7}Ba!_;wQ zm~LxF4+ynw?J#xRJZjq|Y7Eou+?ZM!rQ0QH*yL?z2c6^YC&P41qQ@v5V~2s`M(B-6 znoothqjU%B4IH0ulNh};huV7j=g)@Ny@?O-&jxMY2|-oyC(>>-&jxMse?KS zHSuv-spAnf_^lmU zAk=%+Zk>z@h_tNz~hOrDd~?`_Wnk=5@j z1GQ&@=$XGC)MR?5K<$~oAKYYmrj@|X(Y9w=iT2E6Y|r$|{-n$arZL6tNnwA}GktB% zX4^SHWv|&a0|r3K=Fm7N5N0)(OuXi6V;*OT0hPVu+(zvm06}@vp+Tq}*UR`L`XYQD zZ`4N1NDySER{(m)+eQVUcRY=Hr%Pvqz}8H)^Vny?UpjqTZGX+G?SCY<39t+*dw;BT zJ{ul9{Es$}L`E)uESl*wt45zp`kSO#fMxIG+BVOHw~RVDUsU6fp>T2+Go5GE+&`nR z$WsN({j=Mv@eHrl_4#n`YUQ9QlTWR5oX{Lq4sLDlIKHj9y`tfF<=Cc)CpJ%+RGHX% z($teX5Vv&fCj&Hlbf5`JGN?_GO@YR zL>`lxC!IWXa?8Xf$%i~ns!~N9CAs})%i5Zc!%(VJVKvt#6>XCI+D~k+ z9M{@bX{}D7J}n(5RQ8*4O4H<)N#oj%KUs1dSz#GO`c|?f*;Fy5ofS;;q{^|UR*pp+ zSH68@WNDYS__G~M&`u-#*$$?x4(Pwo2vf8Tc=Q*yYhzQTskZR>@DKg}k~EzR>%Y)+ zS4xiw%E9SQ=FHB1FNB*p)zh7@QO8ptea4@~3`)Vi4^-4Oen`wV%OOXZ)p>{$oaK{OMspq}wVg-lR3EvmDf?mZM%1L;Zr8L2K)1*o@8OznY0Kt`A912qJ4{aiUX z+sWMJILsZMPYmLp?SvIgnbXS5iE{;(eUQlpMA|tHUawvv9WdF>aqxRmN=^oYKR4AS zG051Vm1!vFG+P|}!) z0|pXH8b~bZFGy7l8XP}Fb%8@TY15**z#*J8kZ{sp=(zP^biTNpoJ&Nxu=dzX;i9b; zrnVAFk}9btQJ`OwY96L4`2SjM?#tn?M%J`3wegsxNRzZ>9xfIWwMCbiV)DRR!z*gn!&=sZ)qKCMqON&$zOL zg}{L^6$Ns*SaR4`b0~mn%U%t~?{u+K=w}OMHAkBMG4TY7z@b~(r$mbWrBd@hX%2Ou z+K|`6Q%7Fv*nyRkP?0o*ex?@0ivAU%ahq9D;{BTq!xNb}~^$LQ5zPol0o8@evE z+%L2or79SGf9=+H!r$$Df6NGJ$mo8jY#IYd4);5~jpGkpk5+$JTK!{FDo-D-9r$ke z$;gMD9)?8{iPW*dS18kn|G0S6H;oQPJYH*gFMO-|xYIpmKwQn^jz;OOpDzbbI+vwslljzI zECcjKhy4~QwxF&1S1Cp;29|x0nd3mD{i~BVEe52xzgFWWE9eCCk~Fi?B{9fMPCQf? zGC65EV(Lq=KP%`2^Kx1&tqqE8l4gE6_MW6g_2t-m7OE01_^+h8JO!EAs32PuKs&($ z^{JiU3mRv>>bQZmnAQTt#mWTps*^JvlC-G4niT7@JLtbgsry)61C7TiNPy2%JHZ3d zeN85KQ+c)s{Wrzp1=7n3E*;^`TKHjj{>V3DH-QkHH{*b!AR86_`(o-f5}Ja6osWGO zUgwN_-^s-}%gpk=!%EN;Bs0q=qV$bXVru!M^Rkb^@0{ue&IBlz6qp|FQji?7Enrk zwH*BHWX^D%#`A;9zil|j2MU!bO(zkMwi0-@HBC95aQ8TkU8hcN>L4UNsq%-`liQ{= zO&(DhD(4O(Djlu=H6p6~>4a7SZWEceHBW49o1}p>p=onlQ%4H{a8q^aRN{5St(yeH zMytJ}wXNy+=8=`%iC!b3b&?XvSEjbMw@3yOBp=v1>Ey}HhZ2~NH&#@pqC%wJflbYm z##Y3n%1KS9R$8XCcaW@dY_k*=RVKG|RIy}oWl(!ZQ(MQN5tWmgo2Il(Ii3_$OH)-^ zCr{!?MY3yZAv;|$oN87866!Zg;NN`G)Q(dL@FR`YwI;{4w24;CRufh4tPMI84+D@J#H5{f(m=jv7lZDZa%1NxFDq}{C`q$PPQDME&qH7wn zC=u?InWibHcAP*ykt{Nrv=N&pjZ|y(fC!yQ@@SBps*Kde9BH}$$xa!)^-j_#y9^pp zWkuA+DV8o#v{C3Ytl5&Th6&Q^nkP25Ol5^LPAbsRauQUrAktQy*wn_Nr=^4b`6rfC zQst?wZLF9QLu8HCaq3uQ#&Bu#tF%;EB2HmZ2D!Q>lYR{ynt6+eP-L+rO8|DL{h#AG zrCvSsG&X6YB7Oi^4nQ;9tPXiFsi(QtM*|sl(_Cg%i4uU!?bF=y47X~+1d+_n7e&~0 zhHJtEE5ip*cm1M94pu~7r@6jy;q~h7XQqZYI!ZreM;i9T^niglx)v-?Gmo2^_wTUOT0~{-qvNYTXs=8-=PG1~h zM|G2IBb?OI(LwKI@ahRDeZka7Xr1S>$g5h6(eqrpFevIoI^SguKFb&ldB7fx|A+pr zr?%utvGn-Ll#@?7R@Ups{p;9^eXQxQ@es3)GGaKZXB=CpHd+6mTLM#LwCGT`#*3SY zM^9|KP&nUZN8Ok%6Zw4CE`y3Xk1rCwmr5WEF8jc8<2=_4Z{5^LnW?r+>0pxq|3iIJ z_dl|7Xmf{*A6ySEvgJ5-S){t{_}z`X_whJYJdf~Tt=xi5898DW4`Rljx~evFv|X9h za=f@>@KC@Q#Po+q-xN}L7@lvqZt%iLgdBdVfEIgqw5b%R^k&Nc#m_9oYTFlhvF2sgQb26IqDv<~bw$`&GS_a=Aj;kFSR8sTQw zzdvaNN5~af;r^r%R7S41NF!7YXDTD>Thm5Rku<_>DUT|XMz}p~1eHl6bh^Q+q!Cm~ zBdp#;SsGzY+6Ycn3?yz{n{uhDG{W6!BdCZ*xF>A{710RyZlXvd+?O^2g!m7*{%*Dr z9N#p;18!kg(+E}9$S~^xx3Y_E1h*VKDFYh{bWK~)sza|Po;EgGU$HV4YuC60O)>ZlT^_CY)ZGR>YsQ0 zT@ocj_w#OH=cGa+!m8)p%1*Fd6~{vlebIIQp3p6h??pGzwWT~J`HOCM-Bl6@-7mUh z_P2C<(EV4}-;mJl8M^=K7B(bwtBhPX#_j|R7NAbo6@Ty8sWXP&#Q<=ct2@AS4A|! z2a?KoVj<*1*Z&wox~kXpOs_{+_!vfvk2OM>82(l}YZMK^Qe3dQ8`jq5J) zyvF7Je^|cR$oxO7-0W=0WV^}oOxIy7-IDW?e_4oem{Mh`P8V<|>EdgvI4GakG)yH+}kusCT5N5O%&5{LR;&}$DcrG&=fD$MppLI^#v1ak}CmNtE_W4H))=#2|Aii zBz$OYns`D~l}VkYB&N5v9nW&@f6P)+S9uagY!nD+qEbdH%W2rOC4>|Cugr0Js#QJ3- zA*Gxs$y!lj5|*HL0d1o=WU^SbH8e|1)susp7S>h!)jqtwZa}s2zuX{`^BgmoNj&k5 z8|XfS^bdOUH*W83;-~bYZ`_@aupQ8+`+V#AGrTwy@Xd())-BBN;!r?k=+n~E4y|4he}ieyavn2srxF{XY>2aAdrQa`)F=#4XDNKNw+zb2`w9%E{{ zmrVJp$(TCLOU9H`X{P62v{98-$;3L-D_lgC%#iYZWHMi$?)4hHu}r3qSzeDpi5aAW zve{nHU6NIWQPCBa&+r=J@J40gWzO`1xn3OJs1!!#c|l(r2dSt?!gIV}(Z42yfSMOf z*tlc}xWG$h4s@&Ygn$brznFB4fq=zcFi_9DF)&*WF818(aS3yr|Fyrcz?A_d!$s~X za>hxV&WLJfpQrL?87s1f^M47KC!H!?H_~N#^%SeK$s#(A({+r=e>!H(xU); z+{Ip?XBlEH{o`V9zz(*D$`0Md-X8yDdno7m!6lx*!Zc@5(wi=NiC0)rn|fOPkm}`L z(g*b!j9=j;(Wr`~4_@gd0j0|5gI9S;JgFj@;Oew| z56(W}tB`+r{zd#N@UM=4^_?etRs6H--t~0nd*2k@F4g>-G*S-x4ochpd zXc&}h%W?;JntN!~Bq=ld+txYt=wsOh!CANW@>Q?PUHcXnIE9AYQmuwK?d~?|pyeD^ znQGXLx4k$_KegN#h4BK*)Zx$sbMrWCK4mD&x}#ernG48==J1MBI`*3+SB1rNy+fQo zRQ|3#v#8v?HuJc0kJ|p{m3Ph$kNinZ9ZkK0+FScZ{eE|-qViTzIX3O+lb9jcrE5F2 zSB~q&;h{+d4~!oS`k$!xf8gHUZ{8)!VGK6{YR4SkrBChPy-LA$8&?i%&4mHlA)3NBi##je6GxK2hx5`ROl9Hx%lpu{n~=eYLGiQIFas z2b98mG8@*GpOWvhschU4b z;AT$qoW}Lezi*1#?xnMj8TgxS-d36>wzal12k9(jrnHgfszp=n-rKr#t(yJZ;~D)r zWoz=IO+vd9H<1|5rkiSI0! zxmjky0grR(g|b-Q&V)?Nv0J< za%%UbY{%Uw+i_rpzi3?Ua0oz`l)|Mol#TY5|DoceL^gWVDAQCYaU4)32~8rqFJEu)9o{?63s-(zWNbU~2fK>?qqB!oinrS$8p{f^Z-Qa$mY#$Ji9I=KI#=5Y%$O z+8OwRn{B+T!{VSJ2B<6Nn|Y{~uc zgwUI=vwm`_QT49B%hmSqIHc9w%`H{DhxI*ltW?f9)* zHV0@tg_3Vw4$x$88_3r7w{G7b?AEpjWtH*72-WcY_`Pv@;> zmCewy|GnFNfS%b~sh-(??+)C`(#)!k=oXrOl0ywMzg6XKmpOQ|YaBh!X=fbb5Y$thO45n!n{xElGVuf*m9p0#1eeoD!e#r;oi*_=6cV>M6z`^nvW3(GE>K~!w$bk7_dkif9}lRIp>7qhD} zNc%aZQbo$-G%sP&3TeM-9!!29^I7+DFx$&4W=Hc(#~JcB~iaN_6d(HrBRnoE^%-l!?rqju{<$jMm=V{2%Af&E(yxVfG@Q^}^|?FE}Ofs8ZcOak0;wN>bShN1$?Wp^i(;VLePOIXj!$RK<6vbByu3%*EE| zsZH&AzmQt%N?nJz)PP})TW*O59A;qUS)RQY2Bd+`^6W`LclD}gi_vnb1LW8Sn`InS z1u9N-#b2GB>dH;9IjJsv1Y|Babk_?db3A)^21Ip^XI)))J)mGG+T7Cu$`Z@%*SOb4 z`vJA@f19nZLJ|YW1SqoLmqS{)LCJ}To|7`^%Q>u^nb<)5AwU~ zy_9o2dz%f&H95~U7fC>ObFe2Q`k@2|-#n z@w^y4{;-24R9cVy1Lq)Pt>rYnX=`+h4^klPS<0|N_I5K4iF(>Ow3d^9n3&ph>f~0Ymv}R&Lo7Zn zkfx3*wNv&h4aw3u_;bWAr7b4d{S_0`!65{g-mAak?~V0A84&+kD&OFeu4sfXSdS?1aMPe2ZKJXg1AeGnaB znP+bb0#RM&*_(nuY+U9I9j1M$hYtDWGWJfO?*KXGp-VR&wNcDmsoKskS)7T6HPS>j zy3{KL(l6|R9*>L@N;;U-&4iCKU@dnPrDfUctw8Kr?s2sm%l^%Daq%|4R)Hiyy_sDRM8`DH` zx%no~mA8nb9$aYx>5Vr*RO=>h*p|`lGAZ{g2c2H#A&9=psnkBomHN~som)D0cu%tE z^y~$to;sb~DcxjOx(ToxfL0R&ZCyE__SOZZuC;CEmj+aMg5I66rw2mIo#N>wf&&s` z-0789+ZaOxdQ0L?uj^{h#28kFk#~9amV_0N)hf^4lIZC=RDF%iPI6rXSPs^BRrenD z1jW%Kz>qawPjiD5i1r$<{{U-<+>%)1Rqja)k@&hnWCQO>3{e?|tY!SkcA?}d$>dDp z?$i>QHSUwacxYZtLF2yKkqb%_ntN(|d!J|HTOdZ==atQ51jMNOJUbZy3Hk5y`t7e_ zt3aY#$;|6N-d0K?x(CJ8FO#UEr|3UeE1Xwayg7f9_k-*~#!2OwS6_^t;TAtVELi7d zUhRDRywbtWfOTG0ua9N*HA(*&=^PJ{qhPuJxZ}doBmJN8Ms6*6Arvm3m;Ur$nnqBq zvZ!?Qfaj$jnKV+hS3GxU^bc+spo8JBbS_&|`m@vUiZ^@&x1n9XS2=jo%Y0fJc0uV6 zoAVB}|E3q3*|nGUuy>?~=?zhEIRL(2yYqt5OI0AL---JbklMWy_bVXH`Hok9-!`WR zv|qjBb$#D9rkVW3eFprk6Uz55x|Mfq^MOXPWW$$wEotK&Ecm8onX`s7#Z*u$23(c+C z-rATy2#L$3pMuK)@aNhc%Swx@KvMtU$qN-!3`p5OcwDs+Cjq1}fAG5fY#UPq+L%9h z-G8=?X=PaS&$uzIh^&5;#+1dN6~V$!o^NRD?Q-zVqQqyoisJxtN6U9-_{ML5Nj}Xl zn8_4~snh)7dh9Q$$$y%^?RM5waU;|GF*AI#X5u-fvLc=4Z$HB~{>;kAX1cF-^%ui5 z9p<0rvn1sryz4h|+TdsA__ZmQ(BT_Nf2JRrNVHMs`?JJm>6AdCcAx1_=mX&t&ho8~ z0it@AUpB8J0I~Qi-@b#<$e}w?dzuqzL2)s%t8%vFG~DDYl|S1rn?8arS)J_@|89^T z(5D<+=x3Jrou4f)9qU&6koH2K*WpC9j~cbux5Eg?nTGEgHwGf@Vn1(uGmv~2`}k(7 zO5a=TSN7938(rkvsJ)MjlZ&?Y%>+l$V)aEnZ`7OY#OjND{M6G~ ztnOP5uJJP~{M)Z6J?2*XlJpusG>zR?jlMQE8pz3j@9v`cfJl3-Z#ylJY_Ik0{V*U# zU+Z@>TUG)wx`&9mn=Pxp>R7HzGbgv}e(m$_uvp1c!G7M{u?Ld-b$&m?CXn2(^9Ss! zln6ArU*}i$iSCqpzx~R=DnGNvcN(t^Dz*80m-^MVt}6w#+VjQw&Zn*}{p23qj}lh- zc8K=VF10%D2taO(_^#RW0739-zhJK015sV=57G-rJ`m$p`}p*;rRe_U;Bi0mOy`Id zrNFE9C+Xw9y(idT8GRzIG?0rDzB^9q3?l6lT=b72**@XhYl1wb&QJI~^#+MA(A4<} zzt^r-LAvWY$y`Qqe;LW^cuV(bJCqTS|2p3;Sb^lf&S$YJZU)HMUFY|B+KycjP=d&M zK5fUYmC^a0x?Z$)JFlnm!vfI^LD9)=)U6fdg&)QYE&r)pZJ+?I|tre`p~OZNcxE%npI9k zYx8MLJFpypzObx=N&TsxH}ffwj6e0ec2m}Yu>Psv{R_*w2$c0t{T^Rf)~yWIKU3DL zxN<8aug}F}ip>?}=F8L&+8^{yVu-NwWn4p2qx_}RP?{fzAz%89-z0|E=Ks>~^G#xi z%7$XeS8@A`A_kM!*V6tkh$kFa4yI=_XJ&0`i}c zwT%HJ{~1}^7(h6fk?lS!Yo;#|Xk*OC_L!A5)0dS&!Ax%U>JYFZvN}Br1=1D+xy;1& zS5{`#s#?NkH1Mp%5ZRTOoh_IbC`gW7v$Orobz&fy&CYJNxwZ`uyJly%I4iNsZuHL1 z4m~TeE8gfmLyVB=Ui6rKyJPE_S-)#^wT#U{Z*4wi%?%pO>nV7Gz$^;h{o1R{&8xFUwq^cZ&z9Ca%x%YN`R==vJA9CS$NP7F3M)E$U2RC zR`zVF&DgHgtMT3b45z_RRCBgI;Dgu>-nX0tX;L=MZdwY6(Y?Um(O6F-wk$hti1j?`=Df(6zN zxx2Kqe0g^CHqj#KK7-4_b=k~q+1u|aH8|D5s9cxj_Zx(i!8%K>$m$R5_yW0@nsrUE z1S0Kd+Az7_Vvyy^VX6>7+JSC@rVA*VE^YVvaN%~>Wp*%love&Ob9ZO(nA!_bsYc5~Lg6bD4}=B$0O zaIn7mb*pqTc{LHpt=Md>>;0u)Z4N;5)~sE40nxlQ%fd@z=ON|bu59L>+PwQqFIR_< z@~(J@579bTPbiF)YIR#jSM zb=Gca^AOe5SvESfk=QK4`ZcL8tpYLwhKJ~`i3bc2-8C{`j8U@4Un>L}i+Kn#bKVf; zer?u93qXBpqlF=|oxP`jx41B)3;7`PA{kF9_U>#i)6IbB-klvZIQmM~ADfkf2eX;S z7^v^D5%l>!rQWss9xOdw-3+A%v+O*|46|9tZ2wT!eH1S&#)5hLP}Uw|Zl-KM%raWb z2l5gqJe#!2_QToUrhFb!{=-tf=A>*t!W)`am3%>6&5Cj}9R!bLF~TNB_mM0I#?Q*` z21jzwW-~8#?(}f!5U2Vq&PtMHJuUG$J3*6W7znQh?96)k^Gdto?{YfT)v-7=0vX%S_ znCov*4&KgYHe}6vLR*m4+gTg`Z=sd?Kq_^EY(aq8Aj!JBHD2OYI%hv#8sbbqhRO$7 z8>Ry({e!GMlLAuu2iYFG>J%@~#6TZpd+rjwAp8y`pv-1|fZvDUcY3+hqqDTGw4GBO zO1__F?TU1$n(~>LA{`l64nSYo00m6y&*Cu&#EQ>kOiHu}BqI4N+xV4@NJOBK(r4K| zU)hMn%4qw~HByqW)yl~0i#STLBEq3Bv;J6ZfT7y=zfNr#V&kL2B~* zS~|b6O`^E3vt9p@*k+=*D#C96NQ_e<#(fiqapWU)Ag^yz+f;;Y-w|tA+az-PKI_U$ zVZs|(fXoyK!m{tP4Q4|VNN(Rt&;L#2=KWzDC}%UL2en6^DjilGM$(_M_JDMlHpkC# zcL#E&oV9P!fJpmu*1qEgB-@{}yhS73eVDQ}EubaEA|P8bf!z%ODa@M*5Qyrufb9?U z%fr+!PfvBJ4aoRq9-=!vuz?H^-RXh#%fobfJS|n_A;=6t9-?|$V4Dc2Pwj=4Vd9so z{*1tq2qp{#WM!?S&2;o4oJtJUVJc4Y8 zmxDP$W?tYl?pLY3(T#X6`)p}UbvP<>0=wiLuFX3)CJR{hLHlTaAkxka?3pZ(?B~W6 z1~TI32L1Nd5iih;__;yoVU_FkcEMb!hl52feAc)5dD5(V2{y&s4fipoB0Tc z%?ks<(mTWljpPNcAafai4{1&Tm@+p?hR^1!SDt&GufabQ=0Rzy~p1a=iT(iLBEsrJ=l)DjecCfedD zW?UNdHN6Um?xoVJq@A`d2Ui4{Yiv7hP0A|*JBw_s~$@J=gW5iEnZ)#LIxGTs!PP^QhtyP{VHg-PqVrjfr9fi_eac3Q+`LBu_ z2S^kc*kvGyw5tNULe|#|jS*9ebs2bfsw**KVB^bCCPobG z4~_uI`R>5Rm!st8*!+9q0RvQI*7P8ScG&{NihBZkmkUTv_XK8@(1ZHPt%duCu2ip=ld_&eeZR zrVa6(qj=F-P+Y3~RLNsOALD0vi0Wg(pdry3iB!4%{!Ea0feyLStsKZt5Xha6Jv)E% zMrkXj`b?13I`cb+(&?TJ2vsRTpduYdg6JvF%D}-Ip!Blm0#=VEtw9=d%R|yW7nmR$ zD81_Wz@Bge6`7-EPX7(&g!^HS!85uXydGrU>+G|>bf5EIqtSdlo<2tFka;8SL_h*i z8TY~uh_r76_Q!mHQ1C`zmwBUg$gCGt`Hf*<**EP{w2qgz z(lRj~WO@k?QGF}!^gvYKiaY&i9WQUEsyqalsD+29z8&PN#)p9QvHhW~(XK3L-w9Y2 zOKSranQapgy6*%zvkeAB^_{?On~c`E``vi%26Ffq&)pzY-;L*PU^jH%mC5^l=_$PS z@llZZymQJsrQMzW9|c);4D4K1?+T4 z`+`XOQ#`bR82(c{K)2Dg#?Nu3fy8csYgTz6h4yE@fT;c)(Bm3S&O*`j9DU>0S}aXG zJy+YXp>)W0+h_|<&)IbX5Y_29yH40fTX=?OZl{_&oRO=Y^kHdhG^K%OF_E5&C_$;&21SVny2SRm}8P{w0URcY-k4LWuu%u zLjj>WE7#W?ngCIql^br3+O|>mH(Sg#{9*#g1g$(ob#^Y_-;5_Py0dfa)6AD(W7~2t zH%8}GrGiu4mc(;&_NZ`M9hmd@eKgG&$nCeBJ!Jxsc3#f@Pymp8=fxBP@rd(s zJ@(awo^n)jo1=&hEMa z$^G2i787j+WxJT4EIT(h^jKS-O6YXw=Jq*cV-ZH4n>(xtPF;Vya$TC zT#yS*m)K52%JcZuH>DrQ#&6EuPOSsc%;)6_W-I_vJuf#zZ&~<2N;xmL&9*kN>;j%2 zS9CjJ5oFd~AcYb^_hCeW(LF!Mn&D9id&Uq<to$vg;w1-z6L*grPHZ&ijJ><$b zV<7KRY#6L9CNN8C(uPH|MfSIT+h+$k{jMbxJMe_bYi${+$Dy{h~2&%tj%Q}WW;VE zH?_B?)U`RgGTUB+B04?E~@50VLZ8a(3nb(i9Kmx|^mD zhR*(DCvu#PzxYk*2&eiBGJ7ItPvC!{1+Rb-6yjQb#6Gh(Xf#aP?YN0xj!uW6fx8EbC}IXe9V)n z36u#sJbtLqw$3kfq&_LFV@wcF{ba89Vb%mQPLVVoYE4iHCOnz@D%Wj-HNgx=B;60RCd7mBsoZw7k!;9` zl40Sg+-?WhQe?yZ>C^-$0+|`*7aBV}9nUC0eQIYEASOH=&nN=5A)bzB6f2=pPv`d8 z&rB&+ruo<@We4`JbD6)|DP;#L{cJp;@1Wh}`5fWr@93UDLiSqQ_oXAZ(=fFU!HJxy&22|NfzL zRrQyod^H!EmCrBLnAdWSi7tTr{9ewPtUQ89`x-ysA476|EoWECJcX*)ay?Jfwh;(b z5c67&!)!UewIWp-_XnFtReI>_X@Qgg`oH$6RO9t{B>z&Y@p{gzi^xuT=W&ojO)mTe z<$C^({F3muIp+uUK{+5{61&M(B%@{ZX*+4F;#XNz??VmAEK&t%Dm?t1+|1;)E z;Ou-=i27$tl#$6u{%6h-wWBunk7_n&fh%V0;H>1KxXm|6060=OIi zXNO+n*R?0!VMpdouc68L9S_K6W*C}<6_9LZhIRuU$Yd}xY@8js)=V90INQ@hcbGZ= zNi#d&hbTKF;5v%N99s^~4m0P3wI$hx!>eP-|LoB2DUH<@nj>~em4NJChEB6q7)07R zp*>{-V#}P+RtX4UbHXl@)DnTFC(jAXzuZ`)N@FM5EVU=kO$($1&~f&uw9wqNKxv`5 zX@Sy0bHgspHutzE&rJ)A8*Oel);O-Qx>}x>mRF_GfX4Illmh34p>Z8R3Y-^O*8!x! zd0|)M$^}wGLaBLSH*@B*Q#m*{%v=zf7{!9U`*|Tw7{6!1z}R442OlG6PRC#-bu#8!mxHg*s!b$pumNpb;>}>TbPz7%q~pJ z6J|@Q-fc%)XBDZ^PH_e06~slMV~$35k{m&%V}WRxMPZ~HK0f8kI9e2LX11%W^uVmotZG|Vis{bpw}yD+r7@jGk3SsXVbkSm{|b@d?9E)J~=15)tf z(7JjcwOSnF>g88`e8HyQEDo)&w<6WT*Nc03RP>U#*{n#FY_sj6&9*!)5XkMGxY>5$SH%?DcH=3!%j0$f zQsDBq-2|F;TOK#tF6uTfO>?K&K&JcikjyU)?O_9u%r6b?<^?e1l<(58m+Ah3P41V5 zy-oMuwH#a?X0EY3?n-8thoNbMU6scx;(~#!*`+b1fk33aBD5E0fE0X1XqN-KDkE2_ zs$9MUvr12Og^?>$U18+PR9Cv`RcW!*7i4xqch&k{6+3Gns#nFnY*(GyuZ}$hkdv3K zL&t1U04etB*i``KvABvWCC1#X99$n}ZVYRsV#6iX-AHMi14Zuf{^$8lb9SLoC=UhP$m;C0z! zI!81#T;=?JT%PjplJD|Kp97J}#*nU|P}b1n_#vHDp`&lr0Z?2OM&`(4oDQM2V%iz9 z2Lxul9o9yd8)p0(K(1?J&j-Y`wXx>|Lg(7h-u4h^=v*tFPp#5UcdulAp~)QmduvhG zhJ_=>X_vbO2=Q~#qZRqb*5s-dK-rf=bQp%FN4OD>ys!Q^X7w64xl?Y59 zT$=aw7ceDD5uvs$Z?~p)*XF!LN|l`{U^xI?miM*RV9LBC&wk$#!U7oWOY)2+*(w22 z=q34nmnDU2KZN(?X`w1Y?NTZ9W!atBgUgNi%#ASem|Jsx+0ec7!7UqhbgFw$*mZf^ zOZQMhR>YK`+Jz0m}jCWor&DWJRdGBnExxlrU%?-uugE-i54JoZ1^hO9N{^ zZQHQ6x({W&88_oTYT5eOGGI9Xy<;r{lX`vL9{&I-VtpQ;CL;idW$W`j-m#X6z*zQH zY?&3I_O@7dDIf8Y1Ds$(KJ$5PuQ3hF$8X5nH;e-tA3W}a+1sx z`<8<*^O=9-&6;Xo6u!*ctH%54$-!57dy5sw8wq)Hk(>4POASnvwfBglb`aL)1umsJ2X66{VAW-WZ9fr{<9D(Gy^#d z%)5FXB8U=x&f81f=1WS`BF;2TTFL|IOiEfg=AIUrcorzuOpmyDV$$*uq*r&cIevIm zvArd=ANvYX=8VYDz8{J+BfBKoPia3rvIl%Xwi_f@;R8h4(<6JG1xS@okL;3UKc#&Z zA0}3|fb2^|deN*!bcN|zk)1<;=+27lO|$)!>Dj3+=7LO2#zRzRNA`38i0bT!mHXFn zYRo7&C(0~}oW}R9-|XCLY{Rde>N!zX55=;f+I?;W%|Z^4#d~Cqs02~ixsf@90*amU zBVG_RX$=}J1js|u&X2ZM&jJ*~7Dn#&(M!_D!3xJVKI&9)AFre@72U-$PQWaHqT1438WtW0Ky_)VDx@z>Rb>NtSz0U) zK_(g+uR+PO$iCSJ)Ti9upDrVVuiRV~Wv->Y+RZFdyJgpgW2%=$S*>9>TYmKgnIIVs`S_{mZkQCGS zkW(2~Td-ThQ`KuC-j$Hn+Mg*r%G_yNYk#s`5!vWqe{HSnBUe6wLx^x|oxiQ8=bUPz^<^n{u zGqPtc`|G8FJNO}a%QXvrknsyVMD>oy?mGeH*J|z%#~@930PlxJnYFeF4?$=Le< zsnC6>cq%3X$mvu}21udh0*LBU!i88gp&UFLW&WlX4fXs9Bz-or5!(c9o>wK? zmt_YXTn>P5bZ)tC!?R8mO!8ObUJfMZSEZLrU<71Jcr|K#BQom|5tzW}waBhVtO&K& zBY$iCF{=qI5#Nm5dLh%FAnU?6qk`Gs2T@)Et6<1gEDOU4EDYC6tyNPNhU+7~$t$ax z3G(vDTao)ArpU+ulha#KXpW14Wb}6AkJ4QzfwSck)KR<}ZLZw~&cqkI8}0B*`&4$;-ivsR z#i;TSw1atARZsC=wAr?CVtu^5dT6qe%SG=;{uWjjPXRJ(Ssr5X`_a(29B~xyM?0Bg z{0Vxy=!3}DxtE-96rg==G2$pbh_>3pY^(DW-4CL@%?;}bu6$~7Lu#@?)@1b*8&Zqq zcF~3ii=`_bR1Q9lGT+(3aS)|^9EGE!XT-Z6Bn|jkfCR8S^yxjkVBsQv~1sT(MsBnlO z8;I%`(e_ot_CeanUq-yTV039@kj{=|EdAxnXs|{v0@3|4+Rn83LAs6pRop*-#L$ty zld=s$^{Z&}xEPrYzKV7nYoGe!Gro>|4P(ifMh2PvBM`b@N5jX(iP8N!+H)@p<#U?f zq!!c2px9z*S(={)~cpkXS}} zKSy4F*}08HP%B z;0@h46~5*4g16hL=J*BYfE1L!MyA zoWcMdi~?a|PGP?m%Y=NpNknRJwk(~hDtn7AZmLPapKJY_;f zFmY+hgo#E3)SBhMCnn3T8D4Ir2qUBdE*R`K0IIq@@{55+zPZYBHE+zEH&*48&7w6_ zqR*u7JeM+}G8lOwWkf|V@?y$}ieTigDI+R^k(W|NR7Br-SqNEa2;nu2V12>;xVHb1 z4cAxK7XsaWfRO+@))xlr^e7NU))x*r*)kHq$Xf;fqeA=%i@YI2AQi#H zhC=c=Diy)RhnpzE#NQ;g&z|tR0b7arBtYhoWD+~P!Uvovk?_46AQjgsZbeI ze3#ZpMNsjNlnNC=#rG){DuRk1Hc^C%e@d$RO{ZWjlATfKo>NyF^Sg$7t262XT_jWY z5JP6vm35&d5GrQWjhSGn;MJXAW}QE;F76efp<-rTVP0L_D^vy*r`ILDLPg{}t1jsk zDuRmHb!o3qnNV>?UD7L51{G)4CA~sLP;pjW(koO16=&Bay+TD$F=rD+sF*9MikO*F7k$p zMRkQs5-wB*7qyfN6_N7=DHke&iwig6LS@3m;*<-O!No;sl~e>5OHwXW1Q$zFE>r{; z%QjJji;E@I5lRI+0hjr8?hSRd;xP@ss$N#d_mE7`5n;z=bq%^R2ov(1^vmi-$6vnV zOY6bqb^gkPiO4W3aJPt zu1%Rx5lmc{GNB@vSh0yBOk6Li_|!WUrx~{r) zCJ-iWs{6$uc6{)~_~7O`e|5rye#Jy&h1CfYDuanzQYKVH&bOvas0b!*+lUF32@|)c zOsEVdI@1cN2qx}GnNSf-+?g_=BAB>q6GfO5+CiqP(lor>^i=LWIg7;@*@96_N9ODG@4y zi2FApLS;h411S+IgNO&y`ltvZ9!iN&5kx$k5}_i9cw`erh|?W0806AeJb%e4o(gO9>GwgNUb6B2+}q zPp3qv2qK=@hzOMl5znSXs0<>WOY5T|h>kUbH+{ z|NZrK?#G>@n;TX+)%B^vl)>>+4g8S%iwW>imxqDvE}Rx9SQXB~+*kD&9`1 zP!T!5lTx7~sCaiHDpV#^yq8j;GN^bzt&xhL;)9e56+y*@lnNC=#fO_HLdD-C)fHy) zC^30_Ugu7$@BH-mhQUtt^Ey6gWn8A*Cj7jvhhEr*v=Ur=Ubl1niNq3Id{O8BoN!Sx zTzpYi_&MQ1WpMFj%7u!^`Ky!*6~V>V%7t0gs!ZeYZ|m4KGGFI1LX^L&u`|4EXWRV_c|ds@9auf8_y53FiWt54{WUokzc zKB1=pdQPiP=xKnS8TJ0$`Z&&LF!ao*FU+lv;|!HS&&>KH&QK9KpI)ED87hLFS@mh0 zp)%d=z{LD||DuG6E{2Ku^@WQPCR7F! z3sNRjM9$}>OsEJZ&fka$l?f9IQzldf6N}OcsR$-&DHAG!i3?IDR0ILbR#BICQMwGGNCekFL;6Dos=l_?V{BIg@YCR79yH*Um)%7lrVQYKUe6E~+7QV~qtk}{zpn7B1% zLPaof+a`)Ial52?$T(8Iu~l7D?><`ZHQwfK{25F0wVrnU;=%u)4{N0#=ipNm?a9ZS zl8;sJNscD_eS`En0{q&4YdhaN|Bqkb`nPYN@EiL7_VJVdk8hv+`?pQ(cLn&YkAB>b zFJo2hH_!g{t0rURqfpIL`0NNDNr^voq93{9V*uuBo^8z#V}H+|UL{}hT9dk$ZZzbY z)V;{d?Q80j1zk6~(%O3e5xP> zuLP<{y3&1VS5g^W>Hf4wsOTX2(gSH;eVZrh z-B)Y>)7o%hRXzvvL_HsNG_$h&DCHCN{o>8k?y&Jh{r;`i#dL>_b@l!$$-wGv$XHij zcqJi2W#sx~N`{Ka`Kgo)70Is+JuPgQSy^Q|D?eM$n_OmAHbRu2t50TSl|j)9X+>28 zLocQbsR)Mtnlhv!7<1lE2Pk@3l3i>8$DuRx`rF5tWIzHM&5jH-SR4=OY$-8`f;j4Og zda?7V_J)I=Z{fDuceCHqnH>pM}2J=3B{pP2kL;yRhgr ze!~?^ezCAy=SinDY;dabWq~t`d}!5-yjhC~75(!QqIwyk&MFqpkBM?sMj>YxlL$~n6f~z8R5!B`n#w3`ZZUC8DiWgR6_W^1 zWe{~vF^K?G1X1S}lL$~n5H-ID1117g5j-vUSH)g3>dup7^2!}m<&RHk%w7Q^kWCpSWr??~xV8Fby1(xoEkT9wkJBIsJ3(xoEkTC)*dDub@ILf30q z?Nj{v|KmmX73g|ATdCdn1}}1c^OuHB^BsW4Q@Z+K*W*R=%l**R2f7|F9`Q8YPo{LKNa%VhrAuW>*E4DFGD4J}P3ck@bUmNa zr6TBhA*D-2(Dh}JJ+iYgT zVO9Am!QYC>hHpPi`CBpB@a+c~e=8;%zWpHMqoV(HLPkGB#z)1%*9jRaBiD~pGE_v) ze^1F!k&y98N`}gWj89WCR0bKJrFBvfWPF~Ip(4omA|*pbkn!avijeV@q&h%{QOMU5 zzAL&jOSQk8&i0;sJ>k1zvPAEXE#DQBC3=6@_^vo+yd6CKVdEb~e?}>u)B77X{!uK< zD8-u1Kc$sY5p4XNvY{f_m{v-GICv%vY{e!uBB|KNZ7a_WkY4c z#)T;xDua#1X{A&I8yBT)s0cQeq->}NHkNLp2ph{J)!7>E@wx*SG)nHRrOv;c*^o7# zS-4s(H&Na|thu^0XnYLeP~32tIDwL=ir2Ce^XQ)V9`+ W*4fkNH1Mwr|H_@y=XC9N@c#qN5IZ>l delta 35097 zcmZ9VdAtG-t-Y&?%O89`{MO!U?X`bv@3n`j zuCwmyGaj1$i{aNf*SXhauk)_+uM4gVr{?-ha$mW<-fvQFQoZ%&YPUxFr*l^|WeiR#{=Tw@Gs*hV<>{yuUM6Q>qdQF*q>dWWkre>RR)%pc5R$81&{jV=p zdevWevC_8I92Fzq%eanH%~tYlVw457Yj2V3SF>&0TvUiWYPteC#zm;L$+wLWYHgZ3 zSd>Mridr94qp0fk>)EOG%D?{k(!X4J=_Qw5(t7dV>*vkOHD6lImt!5NR*mgtbD(y8 zqf!*ma5g7}?fclO9_HI7UU>rI&Ukp$HtCFqTHAK*>q}m$wA4CTZJ#Rb^6g?om3AHC zP6b$QpI8nA#1#Y7+9%!ysI^bL4XN4ewvT#ge?tL>_H5p(zkiyagY@QZw&4&9%{_X? zi2gSBI>h!jl>RpFGsG6=@Tx=N6*W5~QOMy{ha?K3)**>P9)0uJ5Zn#`lu zu}e&79=(qH_P1pb&2^!<&m8Se1gZ~NklS4Ak~9>d(4{$Mt$;#TH?$2E1az^s3bfNT z7e}XnTGvjUZCMfjy3rFexQYT|hl;3m>(m-oM6Fw|-e+1^qPgw~9VG$rz$>BGJqdpa zz3%Pe#+y*~3| zWXAPR)R=MoT|1u~VJh3-&L7$D=tE9Ko=%ajpXIHG7ksXS>(7OEevEJ7L}^7>J4-c@~vWs;=r6qbJRp{Al)me zl`u!u=7YSb66G)&=0-3hyn!7K$|A|f61shWmyabisf3jSy`pVFrs=^6NkrjbJ53!| zrs=_U_JB}3*iKW&&7(FbQDd48a!snD2(>|pn#|HccG5ZSDKbq5Cwk1%!FC!rZiL?8 zB$P5)2is}jxCQ7ulw5U%a>Qgkw7%li+<*2zwAoBPYEb`Bn++VdNHd4i%s81gKv4bi z*K&{dKip0mWGEcYw2}41abYVDeiKt^uq3_O%f1rpFh9RWb+ z83nUmg3vpo*(3@O*3al}hPbQq|4bV%P&Ip|YkC6^lsD-VgxZ;%)kuxzWZIl%vnxnI zTvM!^l{0H5Md+Qyu#)V>@I5Cp%ydR3_BnJ$`jXY`^(*TJsG9wi&0YX1{>o+*5azo4 z%BGd9x_>TuUEu&!&7NET&GKCLVdo|a;`g~peR2D|q#fKo&t{secK$p&JVB_PXERM! z6V&;rNwx!3v**{>FDKibpEM(r?);<~*(jWEvmNziYMpPhUDlPfbwT0-LA=0HM-e@^ zz)md?Y8TiUnRR_?UP!nflo1T9dKbFxMfLw)k$bRqp&gKjv2dXskRU8v*xEw}B!~fd zVb6=qfE1_$^1@yhnE`1fkn(FgAgx3Pt+U|r$M~LjtNjT zdx>l234ofHx-D^$pu8E!6rpx$N0Wvag3$Ooqjr!? z06}I|1)%pk8%q#+zay5@UB)21U7q8$(~6zKZ&jB^tnL_Nscz%G{KJSLK_mg1CEC8?%c0>h9lA_@&mz z+rPOTTTZI)TA4fXkjQF?nZMcaqk;b4Z1{a0f`3Q{Hr&az=pYEBqu$kIdz$n-prW>MA^VG~9W2TUL?ILQY6p`B zvRCk~NmZ!}>JS$yB$?W}s9xi=Z&p|fr2F2rsV->^)X7$+-#}Jtqf5tux?HWz+FnM3W&O{prK0d=%>r874^6G5@6Yz=QoRVCNn606oi+4Fd}I_zE~>_A1mtxqHk3fcA;7s(nx z_B{r&@9{>ass{Cn3sD{Eu=TNZQ61^9^)ZmGk9RwZZ!Wr9=1J(X`?M30$f^1%8L>vH3d z9PN-AjWg_9s8sD7Lrjczy2YK2FmbP#kS$2)Mxc6^^|}8%@?NJ9bIcBgIvwK9vV{Tm zh^Z)GVT@QfSuGSm^~LLRrye=RDRi}sa`K=~x0rY~HNbxR*-~~j-h=k>o4n< zFUfVPKe!^-zdrDz+~V5gG`ww~cCy3LXWDLq*~w08�^ercF-l^qT5;Y_)O(xQ(uU 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" field; non-numeric payload values are // ignored. string field = 2; - // percentile ∈ (0,1], used only when op == percentile (0.95 → p95). + // percentile ∈ (0,1], used only when op == percentile (0.95 → p95). Only the + // Postgres audit backend computes it: the warehouse backend keeps entries + // at-least-once and deduplicates them on read, and a percentile over that is not + // an exact answer, so a deployment on the warehouse backend refuses a query that + // asks for one with InvalidArgument. Every other op is served by both backends. double percentile = 3; // alias names this metric in the response map. Defaults to "count" for count, // else "_". @@ -238,6 +243,43 @@ message ListAuditEventTypesResponse { repeated AuditEventType types = 1; } +// DeliverAuditEventsRequest names one range of the platform journal, (from, to], +// whose audit records the operation appends to the warehouse audit storage. Both +// ends are positions a ModuleReadJournal page returned; from may instead be the +// literal "start", meaning before the oldest retained entry, and is never empty so +// that the first range of a fresh journal has a non-empty value to correlate on. +// The range ends at a position that is final (below the oldest transaction still +// running) and holds at most 500 audit records, or it is refused before anything +// is appended. The effect id is "saas.audit.recorded.v1/", derived by the +// host: one range, one receipt. A runtime task may carry its own transport +// effect identity; the range receipt is independent of that task identity. +message DeliverAuditEventsRequest { + string from = 1 [(buf.validate.field).string = { + min_len: 1 + max_len: 64 + }]; + string to = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 64 + }]; +} + +message DeliverAuditEventsResponse { + string effect_id = 1; + // status is "committed" when the range is recorded. An absent lookup + // returns NOT_FOUND: absence is inconclusive, never a committed response. + string status = 2; + // delivered is the number of audit records of the range appended to the + // storage; a range with none still commits a receipt. + int32 delivered = 3; + // The exact committed interval is (from, to]. Retention verifies these + // positions against the receipt's effect identity and request digest before + // retiring journal rows. Legacy receipts without both positions prove no + // coverage; delivered alone never proves an interval. + string from = 4; + string to = 5; +} + // AuditService — append-only audit event log service AuditService { rpc QueryAuditLog(QueryAuditLogRequest) returns (QueryAuditLogResponse) { @@ -322,4 +364,68 @@ service AuditService { body: "*" }; } + + // DeliverAuditEvents appends one range of the journal's audit records to the + // warehouse audit storage: the operation module-runtime calls after reading the + // range through ReadJournal. It is an operation, not a sweep. It takes each + // record's organization from the journal row and never from the caller, emits no + // audit of its own, and commits one receipt per range under the installation's + // tenant; delivery is at least once, and every read of the storage returns each + // entry once by id. Its receipt is read back through LookupDeliverAuditEvents. + rpc DeliverAuditEvents(DeliverAuditEventsRequest) returns (DeliverAuditEventsResponse) { + option (saas.policy.v1.method_policy) = { + exposure: EXPOSURE_AUTHENTICATED + // Headless module principals are authorized by their live grant and + // saas.audit-delivery scope, not by a human organization membership. + tenant: TENANT_REQUIREMENT_NONE + mfa: MFA_REQUIREMENT_NONE + audit: {emission: AUDIT_EMISSION_NONE} + idempotency: IDEMPOTENCY_REQUIREMENT_REQUIRED + rate_limit: RATE_LIMIT_CLASS_STANDARD_WRITE + request_sensitivity: SENSITIVITY_INTERNAL + response_sensitivity: SENSITIVITY_INTERNAL + platform_role: PLATFORM_ROLE_REQUIREMENT_NONE + }; + option (codefly.runnable.v0.operation) = { + attempt_timeout: {seconds: 60} + total_timeout: {seconds: 600} + max_attempts: 3 + backoff: {seconds: 5} + retryable_codes: [ + "UNAVAILABLE", + "RESOURCE_EXHAUSTED", + "ABORTED" + ] + audience: "saas-audit" + required_scope_slots: { + name: "audit_delivery" + required_actions: [ + "invoke", + "read" + ] + lookup: true + } + lookup_method: "/saas.accounts.v1.AuditService/LookupDeliverAuditEvents" + max_input_bytes: 256 + max_output_bytes: 1024 + completion: COMPLETION_CALL + }; + } + // LookupDeliverAuditEvents takes the invocation's same {from, to} body and + // derives the range receipt identity from from. It never delivers. + rpc LookupDeliverAuditEvents(DeliverAuditEventsRequest) returns (DeliverAuditEventsResponse) { + option (saas.policy.v1.method_policy) = { + exposure: EXPOSURE_AUTHENTICATED + // Headless module principals are authorized by their live grant and + // saas.audit-delivery scope, not by a human organization membership. + tenant: TENANT_REQUIREMENT_NONE + mfa: MFA_REQUIREMENT_NONE + audit: {emission: AUDIT_EMISSION_NONE} + idempotency: IDEMPOTENCY_REQUIREMENT_FORBIDDEN + rate_limit: RATE_LIMIT_CLASS_STANDARD_READ + request_sensitivity: SENSITIVITY_INTERNAL + response_sensitivity: SENSITIVITY_INTERNAL + platform_role: PLATFORM_ROLE_REQUIREMENT_NONE + }; + } } diff --git a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/module_capabilities.proto b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/module_capabilities.proto index 1eb2b358e..07ad13bfc 100644 --- a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/module_capabilities.proto +++ b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/module_capabilities.proto @@ -355,6 +355,69 @@ message ModuleEmitAuditEventRequest { string idempotency_key = 7 [(buf.validate.field).string.max_len = 255]; } +// ModuleReadJournalRequest reads the platform journal across every tenant. Only a +// module principal whose grant is cross_tenant may call it, the gate a +// system-scoped EmitAuditEvent has; any other caller is PermissionDenied. +message ModuleReadJournalRequest { + // after is the position the previous page ended at (a page's next_after). The + // literal "start" is a valid position meaning "before the oldest retained + // entry", and the empty string is accepted as an alias of it; so a reader with + // nothing read yet has a non-empty position to correlate its first page on. + // Positions are opaque: store one and hand it back unread. No entry's position + // is ever "start". A value this host did not issue is InvalidArgument. + string after = 1 [(buf.validate.field).string.max_len = 64]; + // limit is the page size: 0 is 100, and more than 500 is served as 500. A + // page is also bounded to 2 MiB of event data, though the first entry is always + // served. + int32 limit = 2 [(buf.validate.field).int32.gte = 0]; + // types are the event types to read, exactly (no pattern). At least one is + // required, so no caller reads every tenant's every event by leaving the + // filter out. + repeated string types = 3 [(buf.validate.field).repeated = { + min_items: 1 + max_items: 16 + items: {string: {min_len: 1, max_len: 256}} + }]; + // delivery_bindings are the binding ids the caller's own delivery of the journal + // runs under. They decide which entries are marked delivery_machinery. + repeated string delivery_bindings = 4 [(buf.validate.field).repeated = { + max_items: 64 + items: {string: {min_len: 1, max_len: 256}} + }]; +} + +// ModuleJournalEntry is one journal row. Rows are served in the order their +// writing transactions are final (see ModuleReadJournalRequest), so a reader that +// resumes from the last position it was given skips nothing. +message ModuleJournalEntry { + // position is where the row sits in the read order: opaque, and the value a + // reader passes as after to resume past it. + string position = 1; + string event_id = 2; + string type = 3; + // organization_id is the row's tenant, empty for a row that has none. + string organization_id = 4; + google.protobuf.Timestamp occurred_at = 5; + // data is the event's JSON object; empty when the event's data is not one. + google.protobuf.Struct data = 6; + // delivery_machinery is true when the entry the row carries is the footprint of + // the caller's own delivery of the journal: the host computes it, so a caller + // never parses this module's payloads. It is true for a + // saas.module.operation_context_minted or + // saas.module.delegated_audience_exchange entry whose binding_id is one of the + // request's delivery_bindings, and for a saas.module.work_context_minted entry + // of the caller's own module principal. + bool delivery_machinery = 7; +} + +// ModuleReadJournalResponse is one page and where to resume. +message ModuleReadJournalResponse { + repeated ModuleJournalEntry entries = 1; + // next_after is the last entry's position, or the request's after when the page + // is empty. + string next_after = 2; +} + // ModuleAuditFieldKind is the kind of one declared audit payload field: the // same kinds a solution may declare in its manifest. enum ModuleAuditFieldKind { @@ -1656,6 +1719,23 @@ service ModuleCapabilitiesService { platform_role: PLATFORM_ROLE_REQUIREMENT_NONE }; } + // ReadJournal serves the platform journal across every tenant, and the rows with + // no tenant, to a module principal whose grant is cross_tenant. Rows are served + // in the order their writing transactions are final, so a reader that resumes + // from next_after skips nothing; it enqueues nothing and is not a replay. + rpc ReadJournal(ModuleReadJournalRequest) returns (ModuleReadJournalResponse) { + option (saas.policy.v1.method_policy) = { + exposure: EXPOSURE_INTERNAL + tenant: TENANT_REQUIREMENT_NONE + mfa: MFA_REQUIREMENT_NONE + audit: {emission: AUDIT_EMISSION_NONE} + idempotency: IDEMPOTENCY_REQUIREMENT_FORBIDDEN + rate_limit: RATE_LIMIT_CLASS_INTERNAL + request_sensitivity: SENSITIVITY_CONFIDENTIAL + response_sensitivity: SENSITIVITY_CONFIDENTIAL + platform_role: PLATFORM_ROLE_REQUIREMENT_NONE + }; + } // DeclareAuditEventTypes admits the audit event types a composed module owns, // into the namespaces the operator bound to it; see the request. rpc DeclareAuditEventTypes(ModuleDeclareAuditEventTypesRequest) returns (ModuleDeclareAuditEventTypesResponse) { diff --git a/module/contracts/api/catalog.codefly.json b/module/contracts/api/catalog.codefly.json index 192675738..9f420d2ad 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:743b63bad9a546f105e994c754d845865e189465e6c72816e5785efe38ab7831", + "digest": "sha256:12c6216596c7460c71bf83e3962a3724c19bd79401459088a51f7f3b3fc1e3d9", "services": [ { "name": "ModuleCapabilitiesService", @@ -41,6 +41,7 @@ "/saas.accounts.v1.ModuleCapabilitiesService/NotifyUser", "/saas.accounts.v1.ModuleCapabilitiesService/PlaceRecord", "/saas.accounts.v1.ModuleCapabilitiesService/PublishEvent", + "/saas.accounts.v1.ModuleCapabilitiesService/ReadJournal", "/saas.accounts.v1.ModuleCapabilitiesService/ReplayEvents", "/saas.accounts.v1.ModuleCapabilitiesService/RequestApproval", "/saas.accounts.v1.ModuleCapabilitiesService/ResolveWorkContextAuthority", @@ -66,7 +67,7 @@ "kind": "protobuf", "package": "", "path": "contracts/api/accounts/connect/contract.binpb", - "digest": "sha256:743b63bad9a546f105e994c754d845865e189465e6c72816e5785efe38ab7831", + "digest": "sha256:12c6216596c7460c71bf83e3962a3724c19bd79401459088a51f7f3b3fc1e3d9", "services": [ { "name": "ArtifactService", @@ -153,8 +154,10 @@ "fullName": "saas.accounts.v1.AuditService", "procedures": [ "/saas.accounts.v1.AuditService/AggregateAuditLog", + "/saas.accounts.v1.AuditService/DeliverAuditEvents", "/saas.accounts.v1.AuditService/ExportAuditLog", "/saas.accounts.v1.AuditService/ListAuditEventTypes", + "/saas.accounts.v1.AuditService/LookupDeliverAuditEvents", "/saas.accounts.v1.AuditService/QueryAuditLog" ] }, @@ -371,6 +374,7 @@ "/saas.accounts.v1.ModuleCapabilitiesService/NotifyUser", "/saas.accounts.v1.ModuleCapabilitiesService/PlaceRecord", "/saas.accounts.v1.ModuleCapabilitiesService/PublishEvent", + "/saas.accounts.v1.ModuleCapabilitiesService/ReadJournal", "/saas.accounts.v1.ModuleCapabilitiesService/ReplayEvents", "/saas.accounts.v1.ModuleCapabilitiesService/RequestApproval", "/saas.accounts.v1.ModuleCapabilitiesService/ResolveWorkContextAuthority", diff --git a/module/contracts/runnables/accounts/connect/DeliverAuditEvents/operation.json b/module/contracts/runnables/accounts/connect/DeliverAuditEvents/operation.json new file mode 100644 index 000000000..288899669 --- /dev/null +++ b/module/contracts/runnables/accounts/connect/DeliverAuditEvents/operation.json @@ -0,0 +1,34 @@ +{ + "method": "/saas.accounts.v1.AuditService/DeliverAuditEvents", + "attempt_timeout": "1m0s", + "total_timeout": "10m0s", + "max_attempts": 3, + "backoff": "5s", + "retryable_codes": [ + "UNAVAILABLE", + "RESOURCE_EXHAUSTED", + "ABORTED" + ], + "audience": "saas-audit", + "invoke_scopes": [], + "lookup_scopes": [], + "lookup_method": "/saas.accounts.v1.AuditService/LookupDeliverAuditEvents", + "max_input_bytes": 256, + "max_output_bytes": 1024, + "completion": "COMPLETION_CALL", + "tool": { + "description": "DeliverAuditEvents appends one range of the journal's audit records to the warehouse audit storage: the operation module-runtime calls after reading the range through ReadJournal. It is an operation, not a sweep. It takes each record's organization from the journal row and never from the caller, emits no audit of its own, and commits one receipt per range under the installation's tenant; delivery is at least once, and every read of the storage returns each entry once by id. Its receipt is read back through LookupDeliverAuditEvents.", + "effect": "EFFECT_MUTATION", + "name": "DeliverAuditEvents" + }, + "required_scope_slots": [ + { + "lookup": true, + "name": "audit_delivery", + "required_actions": [ + "invoke", + "read" + ] + } + ] +} diff --git a/module/contracts/runnables/accounts/connect/DeliverAuditEvents/runnable-package.json b/module/contracts/runnables/accounts/connect/DeliverAuditEvents/runnable-package.json new file mode 100644 index 000000000..e699739ad --- /dev/null +++ b/module/contracts/runnables/accounts/connect/DeliverAuditEvents/runnable-package.json @@ -0,0 +1 @@ +{"agent":{"kind":"SERVICE","name":"go-grpc","publisher":"codefly.dev","version":"0.1.58"},"contract":{"input":{"fields":[{"name":"from","optional":true,"type":"STRING"},{"name":"to","optional":true,"type":"STRING"}]},"output":{"fields":[{"name":"effectId","optional":true,"type":"STRING"},{"name":"status","optional":true,"type":"STRING"},{"name":"delivered","optional":true,"type":"INTEGER"},{"name":"from","optional":true,"type":"STRING"},{"name":"to","optional":true,"type":"STRING"}]},"protocol":"codefly.runnable.service/v1"},"digest":"ec57c25182a75de7ffe87df1491777ad902b7bf7c61eae7f2cbdcc111db7d19f","execution":{"cancellation":"CANCELLATION_NONE","completion":"COMPLETION_CALL","facilities":[{"kind":"SERVICE"}],"max_input_bytes":"256","max_output_bytes":"1024","recovery":"RECOVERY_RECEIPT","timeout":"600s"},"identity":{"module":"saas-starter","name":"accounts-connect-deliver-audit-events","version":"0.2.0","workspace":"saas-starter-dev"},"schema":"codefly.runnable-package/v1","service_operations":[{"adaptation":"ADAPTATION_BOUNDED_JSON_V1","endpoint":"connect","input_message":"saas.accounts.v1.DeliverAuditEventsRequest","module":"saas-starter","name":"accounts","operation":"/saas.accounts.v1.AuditService/DeliverAuditEvents","output_message":"saas.accounts.v1.DeliverAuditEventsResponse"}]} diff --git a/module/contracts/runnables/index.json b/module/contracts/runnables/index.json index 26b0de9f5..dc3090bcf 100644 --- a/module/contracts/runnables/index.json +++ b/module/contracts/runnables/index.json @@ -3,6 +3,17 @@ "workspace": "saas-starter-dev", "module": "saas-starter", "operations": [ + { + "name": "accounts-connect-deliver-audit-events", + "version": "0.2.0", + "digest": "ec57c25182a75de7ffe87df1491777ad902b7bf7c61eae7f2cbdcc111db7d19f", + "service": "accounts", + "endpoint": "connect", + "method": "/saas.accounts.v1.AuditService/DeliverAuditEvents", + "input_message": "saas.accounts.v1.DeliverAuditEventsRequest", + "output_message": "saas.accounts.v1.DeliverAuditEventsResponse", + "path": "contracts/runnables/accounts/connect/DeliverAuditEvents" + }, { "name": "accounts-connect-invoke-source-operation", "version": "0.2.0", diff --git a/module/deployment/generated/asyncapi.json b/module/deployment/generated/asyncapi.json index 313fc2393..5d3b0c45d 100644 --- a/module/deployment/generated/asyncapi.json +++ b/module/deployment/generated/asyncapi.json @@ -152,6 +152,17 @@ "x-visibility": "external", "x-retention": "30d" }, + "saas.audit.recorded.v1": { + "address": "saas.audit.recorded.v1", + "title": "saas.audit.recorded.v1", + "messages": { + "envelope": { + "$ref": "#/components/messages/saas.audit.recorded.v1" + } + }, + "x-visibility": "internal", + "x-retention": "30d" + }, "saas.auth.client_authorized": { "address": "saas.auth.client_authorized", "title": "saas.auth.client_authorized", @@ -2345,6 +2356,13 @@ }, "title": "saas publishes saas.approval.timeout" }, + "send:saas.audit.recorded.v1": { + "action": "send", + "channel": { + "$ref": "#/channels/saas.audit.recorded.v1" + }, + "title": "saas publishes saas.audit.recorded.v1" + }, "send:saas.auth.client_authorized": { "action": "send", "channel": { @@ -3769,6 +3787,13 @@ "$ref": "#/components/schemas/saas_events_v1_events.proto_EventEnvelope" } }, + "saas.audit.recorded.v1": { + "name": "saas.audit.recorded.v1", + "title": "saas.audit.recorded.v1", + "payload": { + "$ref": "#/components/schemas/saas_events_v1_events.proto_EventEnvelope" + } + }, "saas.auth.client_authorized": { "name": "saas.auth.client_authorized", "title": "saas.auth.client_authorized", diff --git a/module/deployment/generated/communication.md b/module/deployment/generated/communication.md index 900100317..d5fb54ed9 100644 --- a/module/deployment/generated/communication.md +++ b/module/deployment/generated/communication.md @@ -112,6 +112,14 @@ Every domain event type, who publishes it, and who consumes it. The machine-read - **Retention:** 30d - **Consumers:** _none_ +## saas.audit.recorded.v1 + +- **Publisher:** saas +- **Visibility:** internal +- **Schema:** `saas/events/v1/events.proto#EventEnvelope` (major v1) +- **Retention:** 30d +- **Consumers:** _none_ + ## saas.auth.client_authorized - **Publisher:** saas diff --git a/module/deployment/generated/event-catalog.json b/module/deployment/generated/event-catalog.json index a893d768b..c4191e7fd 100644 --- a/module/deployment/generated/event-catalog.json +++ b/module/deployment/generated/event-catalog.json @@ -1304,6 +1304,106 @@ } ] }, + { + "type": "saas.audit.recorded.v1", + "namespace": "saas", + "schema": "saas/events/v1/events.proto#EventEnvelope", + "major": 1, + "visibility": "internal", + "retention": "30d", + "fields": [ + { + "name": "id", + "number": 1, + "type": "string" + }, + { + "name": "type", + "number": 2, + "type": "string" + }, + { + "name": "source", + "number": 3, + "type": "string" + }, + { + "name": "subject", + "number": 4, + "type": "string" + }, + { + "name": "time", + "number": 5, + "type": "google.protobuf.Timestamp" + }, + { + "name": "specversion", + "number": 6, + "type": "string" + }, + { + "name": "datacontenttype", + "number": 7, + "type": "string" + }, + { + "name": "dataschema", + "number": 8, + "type": "string" + }, + { + "name": "data", + "number": 9, + "type": "bytes" + }, + { + "name": "tenant_id", + "number": 20, + "type": "string" + }, + { + "name": "boundary_id", + "number": 21, + "type": "string" + }, + { + "name": "partition_key", + "number": 22, + "type": "string" + }, + { + "name": "correlation_id", + "number": 23, + "type": "string" + }, + { + "name": "causation_id", + "number": 24, + "type": "string" + }, + { + "name": "actor_principal_id", + "number": 25, + "type": "string" + }, + { + "name": "owner_principal_id", + "number": 26, + "type": "string" + }, + { + "name": "traceparent", + "number": 27, + "type": "string" + }, + { + "name": "schema_version", + "number": 28, + "type": "uint32" + } + ] + }, { "type": "saas.auth.client_authorized", "namespace": "saas", diff --git a/module/docs/adr/0003-typed-audit-event-registry.md b/module/docs/adr/0003-typed-audit-event-registry.md index 1d50d2050..1a5a34e21 100644 --- a/module/docs/adr/0003-typed-audit-event-registry.md +++ b/module/docs/adr/0003-typed-audit-event-registry.md @@ -1,6 +1,7 @@ # ADR 0003: Typed audit-event registry — single-table STI, a code-owned catalog, and in-Postgres analytics - Status: Proposed +- Refined by: [ADR 0009](./0009-audit-store-swap.md) — a deployment may select a warehouse as its audit storage, so "analytics stays in Postgres" holds for the `postgres` backend only - Date: 2026-08-18 - Task: closes the review-and-planning deliverable of #170. Implementation is deferred to follow-up tickets that this ADR authorizes but does not open. diff --git a/module/docs/adr/0006-audit-sink-and-retention-tiers.md b/module/docs/adr/0006-audit-sink-and-retention-tiers.md index deb73c329..9bfd45e54 100644 --- a/module/docs/adr/0006-audit-sink-and-retention-tiers.md +++ b/module/docs/adr/0006-audit-sink-and-retention-tiers.md @@ -1,6 +1,6 @@ # ADR 0006: Audit sink and retention tiers — a swappable compliance backend, Postgres as the atomic hot source of truth -- Status: Proposed +- Status: Proposed; superseded in part by [ADR 0009](./0009-audit-store-swap.md) — the sink decision "a tee, not a swap" (Decision item 2; option C of Options considered). A deployment may now select a warehouse as its store of record instead of a tee beside Postgres. The tee, the admission policy and the sizing stand. - Date: 2026-09-02 - Task: prepares the audit performance/size and compliance workstreams (see #430). Refines — for the **sink and retention dimension only** — the diff --git a/module/docs/adr/0009-audit-store-swap.md b/module/docs/adr/0009-audit-store-swap.md new file mode 100644 index 000000000..3cbdb256a --- /dev/null +++ b/module/docs/adr/0009-audit-store-swap.md @@ -0,0 +1,206 @@ +# ADR 0009: Audit storage swap — a warehouse as the audit store of record, reached through the journal + +- Status: Proposed +- Date: 2026-10-12 +- Supersedes, in part: [ADR 0006](./0006-audit-sink-and-retention-tiers.md) — its + sink decision, "a tee, not a swap" (Decision item 2; option C of its Options + considered). A deployment that selects the warehouse backend replaces + `audit_events` as its store of record instead of copying from it. ADR 0006's + admission policy and sizing stand, and the tee (`AUDIT_SINK=both`) is unchanged + for every deployment that does not select the warehouse. +- Refines: [ADR 0003](./0003-typed-audit-event-registry.md) — "analytics stays in + Postgres" now holds for the `postgres` backend only. The registry and the single + write path are unchanged. +- This ADR replaces an earlier draft of the same number that put a dedicated + outbox table, a relay and an archive inside this module. + +## Context + +ADR 0006 kept Postgres as the only store of record and added an opt-in tee. That +is the right answer for a deployment that wants a copy elsewhere. It does not +serve one whose audit must be kept for years and queried heavily: the tee grows +Postgres instead of shrinking it, its copy lacks the entries with no organization +and the ones the authentication resolver records, and every reader has to know +which copy answers which question. + +The requirement ADR 0006 refused to trade away still stands: no state change +commits without its record, and no external system can join a Postgres +transaction. + +This module already has a transactional record that is not `audit_events`: the +journal, `domain_events`, written in the producer's transaction through the event +transport ([EVENTS.md](../../EVENTS.md)). It is durable, replayable and carries a +tenant, and an `internal` type in it is never fanned out. + +## Decision + +1. **A swap, not a tee.** `audit-storage` is a workspace group with + `AUDIT_STORAGE_BACKEND = postgres | warehouse`, plus the warehouse dataset and + table names and nothing else: no endpoint, project, credential or token. The + backend is chosen once, at startup, by `AuditService`, over one interface, + `AuditStorage` (append, list, aggregate). `postgres` is the default and is + today's behaviour exactly: the entry is written to `audit_events` in the + caller's transaction, and `VerifyAuditWiring` holds. Under `warehouse`, + Postgres keeps no audit history, and `AUDIT_SINK=both` is refused at startup: + the tee copies `audit_events` rows and there would be none. + +2. **The journal, not an outbox.** Under `warehouse`, `AuditService` publishes + every audit entry, including one with no organization, to the journal as the + internal type `saas.audit.recorded.v1`, carrying the whole entry, in the + caller's transaction through the existing transport. No table, relay or drain is + added to this module. Migration 46 admits user-scoped publication (item 2a), + and migration 62 records the writing transaction for the journal cursor. The type is + code-owned and `internal`: the + relay marks it published with no fan-out, a replay skips it, and no + subscription may match it. The organization-scoped webhook event of an external + audit type is published beside it, unchanged. The journal event's `data` is + limited to 960 KiB ([EVENTS.md](../../EVENTS.md)); an entry that does not fit + is refused and fails the caller's transaction, because until it is delivered + the journal event is the entry's only copy. + + 2a. **A user-scoped transaction publishes its own no-organization entries.** + Some entries with no organization are written inside a user-scoped transaction + (the WebAuthn registration and the MFA enrolment, for example). That + transaction runs as `app_tenant` and sets only `app.current_user_id`, and + `publish_domain_event` refused every event with no tenant from `app_tenant`. + Migration 46 replaces the function with an identical body except that the + `app_tenant` branch also admits one case: the tenant is NULL, the type is + `saas.audit.recorded.v1`, `app.current_user_id` is set and non-empty, and the + event's actor (`p_actor_principal_id`) is that same user. The record in `data` + must name that actor and no organization as well; an envelope cannot disguise + a record that the original audit row policy would refuse. It mirrors the rule + the `audit_events_tenant` policy already applies to the same entry's row + (`org_id IS NULL` and `actor_id` equal to `app.current_user_id`), so the journal + admits exactly what `audit_events` admits and nothing wider; every other + refusal, the advisory lock and the insert are unchanged. The emitter puts the + entry's actor id on the envelope's `actor_principal_id`, which for a person is + their user id (a human principal's id is the `users.uuid` that + `sync_human_principal` copies). The entry is published on the transaction the + caller already holds; nothing moves the writer to the control plane. The type + is internal, so the relay never fans the row out. + +3. **module-runtime reads the journal.** This module never calls module-runtime + and mints no identity of its own. module-runtime, which holds a `cross_tenant` + grant, reads the journal through a cross-tenant read RPC on + `ModuleCapabilitiesService` (`ReadJournal`) and hands this module one range of + it at a time. The read serves rows in `(xact_id, seq)` order and only below the + oldest transaction still running (migration 62; [EVENTS.md](../../EVENTS.md)), so + a cursor never passes a row that can still commit; its positions are opaque, + and `"start"` is the position before the oldest retained entry. It marks the + entries that are the reader's own delivery footprint (`delivery_machinery`), so + the reader never parses this module's payloads, and so delivering the audit + trail does not feed itself. The reader then calls a Runnable operation on this + module, `DeliverAuditEvents{from, to}`, for the range `(from, to]`. The + operation refuses a range whose end is not yet visible or that holds more than + 500 audit records, reads the range's records from the journal, takes each + record's organization from its row and never from the caller, and appends each + entry through `AuditStorage` to the warehouse. It emits no audit of its own. + Its receipt is one per range, effect id `saas.audit.recorded.v1/`, in + the installation's tenant. Both invocation and lookup take the same `{from, to}` + body. The host derives its range receipt key internally; the runtime's + transport effect id identifies the task operation separately. Lookup verifies + the same request digest and reports a missing receipt as `NOT_FOUND`; a + successful lookup is always proof of a committed range. An entry with no + organization needs no tenant of its own. Delivery is at-least-once, and overlapping ranges append an entry + twice; every read returns each entry once by id. + Delivery and lookup require a verified, idempotent operation context addressed + to `saas-audit`, a live cross-tenant module grant, and `saas.audit-delivery` + with `invoke` or `read`, respectively. Human membership and API-key scopes do + not grant this cross-tenant operation. Authorization is repeated after the + receipt serialization hold, before either execution or replay. + A binding id is marked as delivery machinery only for the calling module; + another module reusing that id remains ordinary traffic. Journal pages fit + the cumulative JSON byte bound and a 3 MiB projected protobuf bound, with + the cursor ending at the last entry actually returned. The producer also + checks the encoded size of each audit record before committing it, so a + compact numeric array cannot leave an unreadable record in the journal. + + *Revised 2026-10-13.* The first draft started one task per event and read the + journal row by event id. Delivering per event made each delivery's own audit (the + mint of its Work Context, its exchanges) an event to deliver, and so on without + end. Page delivery holds machinery-only pages until their oldest entry is + older than one hour, then delivers them too. An idle deployment therefore + settles at one delivery per hold instead of one per polling pass. The swap, + journal type and remaining decisions stand. + +4. **Reads come from the warehouse.** The activity list, aggregation and export go + through `AuditStorage`. A warehouse read names its scope (an organization, or + explicitly the platform), because row-level security does not reach it. The + warehouse refuses the percentile metric, which it cannot compute exactly over a + deduplicated, at-least-once store; the service answers it as an invalid + argument. The grant check of a resource-gated aggregate still runs in the + organization's Postgres transaction, but the read that follows is a second step + with no shared snapshot. Reads lag the write by the time delivery takes, and a + page that shows an "as of" is honest about it. + +5. **Retention is the warehouse's.** `RunRetention` skips the `audit_events` policy + under `warehouse` and logs why; how long an entry is kept is the warehouse + table's own expiry. Rows written before the switch stay in `audit_events`. + Under `warehouse`, the same retention run removes journal entries of exactly + `saas.audit.recorded.v1` whose `created_at` is older than thirty 24-hour days + on the database clock and whose position lies in a committed delivery's + `(from, to]` interval. The committed response stores both positions; retention + verifies the response status, range effect identity and original request + digest before trusting an interval. Overlapping or touching intervals may be + combined, but gaps remain unproven. Legacy receipts without both positions, + malformed receipts and mismatched digests preserve the journal rows; the + `delivered` count never supplies missing coverage. Every warehouse retention + run logs `deleted`, `old_unproven` and `retention_days`, making old entries + without delivery proof visible. PostgreSQL mode and other journal types are + unchanged. + + Source receipt cleanup names only the two datasource operation methods, so + the full audit delivery receipt, including its range, survives cleanup. + Migration 48 gives the existing control-plane role SELECT only on audit + delivery receipts and restricts its new journal DELETE grant by event type + and a database-enforced 720-hour minimum age. No receipt mutation, table, + relay, queue or identity is added. + +## Options considered + +| Option | Verdict | +| --- | --- | +| Keep the tee only | Rejected for the long-retention deployment: Postgres still grows and neither copy is complete. Kept as is for the deployments that need only a copy. | +| A dedicated audit outbox table, relay and archive in this module (the earlier draft) | Rejected: a fourth module-local outbox beside three that exist, a migration, a relay that must call the delivery runtime with an identity this module would have to mint. The journal already has every property the outbox was built for. | +| Write to the warehouse on the request path | Rejected: an external system cannot join the transaction, so a state change could commit without its record. | + +## Consequences + +- No state change commits without its journal event, and no entry is lost between + the commit and the warehouse as long as the journal row is retained until + delivered. Undelivered or otherwise unproven audit journal entries survive + indefinitely. Delivered audit entries expire from the journal only after + thirty days in warehouse mode; retention for the other `domain_events` types + remains unenforced. +- A deployment cannot select `warehouse` until the warehouse `AuditStorage` and + the delivery path exist in the build; startup refuses it by name until then. +- Every no-organization entry is published on the transaction the caller already + holds, including a user-scoped one: `publish_domain_event` admits a tenant-less + `saas.audit.recorded.v1` from `app_tenant` when its actor is the transaction's + user (migration 61). A tenant-less event of any other type, or for any other + actor, is still refused from `app_tenant`. The comparison is on the text of the + actor id and of `app.current_user_id`, which are the canonical lowercase user id + everywhere they are set; a non-canonical spelling of the same id is refused where + `audit_events` would accept it, which fails closed. + +## Build or reuse + +| Component | Searched and found | Decision | +| --- | --- | --- | +| Audit storage and journal delivery | This repository's existing journal, closed #1000's storage interfaces and parity suite, and the shared `core`, `sdk-go`, and `saas-sdk-go` implementations; existing receipts and module capability verification are implemented and tested. | Reuse the journal, storage shapes, SDK receipt guard, and live module authority checks. Extend the existing adapters; no parallel queue, identity provider, or receipt implementation. | +| Audit journal retention | Read `RunRetention`, journal position SQL, `PruneSourceOperationReceipts`, SDK `receipts` protobuf/digest storage and PostgreSQL receipt tests; searched shared `core`/`sdk-go` implementations and used Go's standard sorting. Source cleanup already preserves audit receipts, but the old committed response omitted the interval end. | Extend the existing committed response with `from`/`to` and reuse its effect identity, request digest, receipt table and control-plane transaction. Preserve legacy coverage as unproven; add only the bounded role permissions needed to retire delivered rows. | +| Integration with delivered module authority | Read the current envelope registry, `BeginModuleAuthorityUse`, `AuthorizeModuleCapability`, SDK receipt storage, the migration replay gate and its `throwawayPostgres` helper, and all shipped migrations before reconciling the branch. The existing authority checks already capture the current registry and hold the installation lock. Existing dedicated generated directories cover contract, binding, catalog and deployment snapshots. | Reuse those current checks and generators; use the existing disposable PostgreSQL helper so the new migration tests run in the canonical replay gate. Preserve shipped migrations 41–45 and move only the unshipped audit migrations to 46–48. Mark dedicated generated outputs for review while keeping authored inputs and dependency locks visible. | +| Service compiler security update | Read the existing service compiler and runtime constants/templates, checked released `service-go-grpc` 0.1.58 and its successful release assets, and used the CLI version/protocol admission before moving the manifests. The shared owner already supplies Go 1.27.3. | Adopt the released owner through `codefly update service`; regenerate its complete outputs. Preserve each service minimum and avoid a local renderer or a second compiler/image mechanism. | + +## Compiler qualification during merge preparation + +Native CI and the released service compiler use Go 1.27.3, including the +[GO-2026-6809 fix](https://go.dev/doc/devel/release#go1.27.3). The gRPC +services consume released `service-go-grpc` 0.1.58 through `codefly update +service`; the service agent owns their regenerated Alpine builder images and +its locked Go runtime from the +[compiler owner's fix](https://github.com/codefly-dev/service-go-grpc/pull/189). +Accounts and auth-gateway require Go 1.27.3, matching the +[host toolchain update](https://github.com/codefly-dev/module-saas-starter/pull/1205). +The actual compiler and CI runtime use the same patch; no Dockerfile output is +edited by hand. diff --git a/module/module.package.codefly.yaml b/module/module.package.codefly.yaml index 8f9a3cb0a..72be843ee 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:743b63bad9a546f105e994c754d845865e189465e6c72816e5785efe38ab7831 + digest: sha256:12c6216596c7460c71bf83e3962a3724c19bd79401459088a51f7f3b3fc1e3d9 - endpoint: connect kind: protobuf package: "" path: contracts/api/accounts/connect/contract.binpb - digest: sha256:743b63bad9a546f105e994c754d845865e189465e6c72816e5785efe38ab7831 + digest: sha256:12c6216596c7460c71bf83e3962a3724c19bd79401459088a51f7f3b3fc1e3d9 - name: auth-gateway endpoints: [grpc, rest] api-contracts: diff --git a/module/services/accounts/AUTHZ_MATRIX.md b/module/services/accounts/AUTHZ_MATRIX.md index 2e90fa0d0..de98ce7c3 100644 --- a/module/services/accounts/AUTHZ_MATRIX.md +++ b/module/services/accounts/AUTHZ_MATRIX.md @@ -2,7 +2,7 @@ > Generated from protobuf service descriptors and `saas.policy.v1.method_policy`; only the prose description is joined from `pkg/business/introspection.go`. Do not edit by hand. Run `go generate ./pkg/business` from `module/services/accounts/code`. -Inventory: **296 RPCs** across **41 services**. Missing, invalid, or unclassified procedures are denied by both Connect and gRPC interceptors. +Inventory: **299 RPCs** across **41 services**. Missing, invalid, or unclassified procedures are denied by both Connect and gRPC interceptors. The compact tier is retained for compatibility. Guards, resource bindings, audit events, limiter class, and data sensitivity are descriptor-authoritative. Domain handlers may enforce stronger state-dependent rules but may not weaken this floor. @@ -41,8 +41,10 @@ The compact tier is retained for compatibility. Guards, resource bindings, audit | `/saas.accounts.v1.ApprovalReviewService/DecideApprovalReview` | unary | `POST /v1/approvals/{id}:decide` | `org_member` | exposure=AUTHENTICATED; tenant=ORG_MEMBER; impersonation=FORBIDDEN | — | org_id → ORGANIZATION/DIRECT_ID | SUCCESS: saas.approval.decision_recorded, saas.approval.approved, saas.approval.denied | FORBIDDEN / STANDARD_WRITE | CONFIDENTIAL → CONFIDENTIAL | Record an assigned approver decision bound to the exact subject reviewed. | | `/saas.accounts.v1.ApprovalReviewService/GetApprovalReview` | unary | `GET /v1/approvals/{id}` | `org_member` | exposure=AUTHENTICATED; tenant=ORG_MEMBER | — | org_id → ORGANIZATION/DIRECT_ID | — | FORBIDDEN / STANDARD_READ | CONFIDENTIAL → CONFIDENTIAL | Read the immutable subject, assigned approvers and decisions for a request visible to the caller. | | `/saas.accounts.v1.AuditService/AggregateAuditLog` | unary | `GET /v1/audit-log:aggregate` | `org_member` | exposure=AUTHENTICATED; tenant=ORG_MEMBER | perm=audit:read; scope=audit:read | org_id → ORGANIZATION/DIRECT_ID | — | FORBIDDEN / STANDARD_READ | CONFIDENTIAL → CONFIDENTIAL | Aggregate audit events (counts, time buckets, group-by) for analytics. | +| `/saas.accounts.v1.AuditService/DeliverAuditEvents` | unary | `—` | `auth` | exposure=AUTHENTICATED; tenant=NONE | — | — | — | REQUIRED / STANDARD_WRITE | INTERNAL → INTERNAL | Deliver one final audit journal range under the installed module's delivery authority, committing a receipt. | | `/saas.accounts.v1.AuditService/ExportAuditLog` | unary | `POST /v1/audit-log:export` | `org_admin` | exposure=AUTHENTICATED; tenant=ORG_ADMIN | perm=audit:read; scope=audit:read | org_id → ORGANIZATION/DIRECT_ID | — | FORBIDDEN / STANDARD_READ | CONFIDENTIAL → CONFIDENTIAL | Download audit log as CSV/JSON. | | `/saas.accounts.v1.AuditService/ListAuditEventTypes` | unary | `GET /v1/audit-event-types` | `auth` | exposure=AUTHENTICATED; tenant=NONE | perm=audit:read; scope=audit:read | — | — | FORBIDDEN / STANDARD_READ | INTERNAL → INTERNAL | List the registered audit event-type catalog for search facets. | +| `/saas.accounts.v1.AuditService/LookupDeliverAuditEvents` | unary | `—` | `auth` | exposure=AUTHENTICATED; tenant=NONE | — | — | — | FORBIDDEN / STANDARD_READ | INTERNAL → INTERNAL | Read the receipt of one audit delivery without appending any entries. | | `/saas.accounts.v1.AuditService/QueryAuditLog` | unary | `GET /v1/audit-log` | `org_member` | exposure=AUTHENTICATED; tenant=ORG_MEMBER | perm=audit:read; scope=audit:read | org_id → ORGANIZATION/DIRECT_ID | — | FORBIDDEN / STANDARD_READ | CONFIDENTIAL → CONFIDENTIAL | Read audit events (org member sees own org; platform admin sees all). | | `/saas.accounts.v1.AuthService/Authenticate` | unary | `POST /v1/auth/authenticate` | `public` | exposure=PUBLIC; tenant=NONE | — | — | SUCCESS: saas.auth.login, saas.auth.mfa_challenge_started | FORBIDDEN / AUTHENTICATION | SECRET → SECRET | Exchange typed OAuth-code or explicit fixture credentials for tokens. | | `/saas.accounts.v1.AuthService/BeginOAuth` | unary | `POST /v1/auth/oauth/begin` | `public` | exposure=PUBLIC; tenant=NONE | — | — | — | FORBIDDEN / AUTHENTICATION | CONFIDENTIAL → SECRET | Mint signed OAuth state for an allowlisted redirect. | @@ -169,6 +171,7 @@ The compact tier is retained for compatibility. Guards, resource bindings, audit | `/saas.accounts.v1.ModuleCapabilitiesService/NotifyUser` | unary | `—` | `internal` | exposure=INTERNAL; tenant=NONE | — | — | — | FORBIDDEN / INTERNAL | CONFIDENTIAL → CONFIDENTIAL | Notify a user subject to category policy. | | `/saas.accounts.v1.ModuleCapabilitiesService/PlaceRecord` | unary | `—` | `internal` | exposure=INTERNAL; tenant=NONE | — | — | — | FORBIDDEN / INTERNAL | CONFIDENTIAL → CONFIDENTIAL | Place one of the caller's own records at a scope node. | | `/saas.accounts.v1.ModuleCapabilitiesService/PublishEvent` | unary | `—` | `internal` | exposure=INTERNAL; tenant=NONE | — | — | — | FORBIDDEN / INTERNAL | CONFIDENTIAL → CONFIDENTIAL | Publish one domain event to the outbox for the caller's tenant. | +| `/saas.accounts.v1.ModuleCapabilitiesService/ReadJournal` | unary | `—` | `internal` | exposure=INTERNAL; tenant=NONE | — | — | — | FORBIDDEN / INTERNAL | CONFIDENTIAL → CONFIDENTIAL | Read a page of the cross-tenant journal under a cross-tenant module grant, without enqueueing work. | | `/saas.accounts.v1.ModuleCapabilitiesService/ReplayEvents` | unary | `—` | `internal` | exposure=INTERNAL; tenant=NONE | — | — | — | FORBIDDEN / INTERNAL | CONFIDENTIAL → CONFIDENTIAL | Re-deliver durable events to the caller's own subscriptions. | | `/saas.accounts.v1.ModuleCapabilitiesService/RequestApproval` | unary | `—` | `internal` | exposure=INTERNAL; tenant=NONE | — | — | — | FORBIDDEN / INTERNAL | CONFIDENTIAL → CONFIDENTIAL | Open a pending approval whose resume job the module claims. | | `/saas.accounts.v1.ModuleCapabilitiesService/ResolveWorkContextAuthority` | unary | `—` | `internal` | exposure=INTERNAL; tenant=NONE | — | — | — | FORBIDDEN / INTERNAL | CONFIDENTIAL → CONFIDENTIAL | Answer one live value a callee verifier holds a capability's seal against. | @@ -307,8 +310,8 @@ The compact tier is retained for compatibility. Guards, resource bindings, audit ## Tier totals -- `auth`: 50 -- `internal`: 55 +- `auth`: 52 +- `internal`: 56 - `mfa`: 5 - `org_admin`: 68 - `org_member`: 57 diff --git a/module/services/accounts/code/audit_storage.go b/module/services/accounts/code/audit_storage.go new file mode 100644 index 000000000..1253cdc2d --- /dev/null +++ b/module/services/accounts/code/audit_storage.go @@ -0,0 +1,61 @@ +package main + +import ( + "errors" + "fmt" + + "accounts/pkg/auditstorage" + "accounts/pkg/business" +) + +// errWarehouseAuditStorageUnavailable is why `warehouse` is refused at startup +// until the warehouse AuditStorage exists. +var errWarehouseAuditStorageUnavailable = errors.New( + "AUDIT_STORAGE_BACKEND=warehouse is not available yet: this build has no warehouse audit storage, " + + "and a host that accepted the setting would journal every audit entry and never append one; " + + "set it to postgres, or leave the `audit-storage` group unset") + +// buildWarehouseAuditStorage builds the warehouse AuditStorage for the group's +// dataset and table. It is the seam the warehouse implementation plugs into: it +// is given the group's names only, and reaches the warehouse through the service +// the composition provides, never through an endpoint or a token configured +// here. Until that implementation lands it refuses. +func buildWarehouseAuditStorage(auditstorage.Config) (business.AuditStorage, error) { + return nil, errWarehouseAuditStorageUnavailable +} + +// selectAuditStorage makes the one choice of audit backend, at startup, from the +// `audit-storage` group and AUDIT_SINK: the backend, and for the warehouse the +// storage built by build. Combinations that cannot work are refused here by +// name, before the host serves anything. +// +// AUDIT_SINK=both is refused with the warehouse. The tee copies audit_events +// rows to an HTTP endpoint, and the warehouse backend writes none: there would +// be nothing to copy, and the setting would read as an export that never runs. +// The warehouse is a swap of the store of record, not a second copy beside it. +func selectAuditStorage( + config auditstorage.Config, + sink auditSinkMode, + build func(auditstorage.Config) (business.AuditStorage, error), +) (business.AuditBackend, business.AuditStorage, error) { + switch config.Backend { + case business.AuditBackendPostgres: + return business.AuditBackendPostgres, nil, nil + case business.AuditBackendWarehouse: + if sink == auditSinkBoth { + return "", nil, errors.New( + "AUDIT_SINK=both cannot be combined with AUDIT_STORAGE_BACKEND=warehouse: the tee copies audit_events rows and the warehouse backend writes none; " + + "the warehouse replaces audit_events as the store of record, so unset AUDIT_SINK or set it to postgres") + } + storage, err := build(config) + if err != nil { + return "", nil, err + } + if storage == nil { + return "", nil, fmt.Errorf("the warehouse audit storage for %s.%s was not built", config.Dataset, config.Table) + } + return business.AuditBackendWarehouse, storage, nil + default: + return "", nil, fmt.Errorf("unknown audit storage backend %q", config.Backend) + } +} diff --git a/module/services/accounts/code/pkg/adapters/audit_delivery.go b/module/services/accounts/code/pkg/adapters/audit_delivery.go new file mode 100644 index 000000000..f5dd099f0 --- /dev/null +++ b/module/services/accounts/code/pkg/adapters/audit_delivery.go @@ -0,0 +1,208 @@ +package adapters + +import ( + "bytes" + "context" + "errors" + "net/http" + + "accounts/pkg/auth" + "accounts/pkg/business" + gen "accounts/pkg/gen/saas/accounts/v1" + + "connectrpc.com/connect" + "github.com/codefly-dev/core/workcontext" + "github.com/codefly-dev/sdk-go/receipts" + "github.com/codefly-dev/sdk-go/receipts/connecttransport" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/protoadapt" +) + +// Delivery reads across tenants. A login or API key cannot substitute for the +// installed module's scoped operation context, including on receipt replay. +func authenticateAuditDeliveryContext(ctx context.Context, headers http.Header, lookup bool) (context.Context, error) { + authority := WorkContextSingleton() + if !authority.configured() { + return ctx, connect.NewError(connect.CodeUnavailable, errors.New("audit delivery authority unavailable")) + } + values := headers.Values(workContextHeader) + if len(values) != 1 { + return ctx, connect.NewError(connect.CodeUnauthenticated, errors.New("one audit delivery Work Context required")) + } + verified, err := authority.verifyFor(ctx, values[0], business.AuditDeliveryAudience) + if err != nil { + return ctx, connect.NewError(connect.CodeUnauthenticated, errors.New("invalid audit delivery Work Context")) + } + claims := verified.Context() + action := "invoke" + if lookup { + action = "read" + } + if claims.GetTenantId() == "" || claims.GetReplayPolicy() != workcontext.ReplayIdempotent || + !workContextGrants(claims, business.AuditDeliveryResourceKind, action, "") { + return ctx, connect.NewError(connect.CodePermissionDenied, errors.New("audit delivery scope required")) + } + principal := claims.GetOwnerPrincipalId() + if actor := verified.Actor(); actor != nil { + principal = actor.GetPrincipalId() + } + seal := claims.GetSeal() + if seal == nil { + return ctx, connect.NewError(connect.CodePermissionDenied, errors.New("audit delivery installation seal required")) + } + installation, revision, epoch := seal.GetInstallationId(), seal.GetInstallationRevision(), seal.GetPrincipalEpoch() + sealed := &business.SealedModuleAuthority{InstallationID: &installation, InstallationRevision: &revision, ProducerEpoch: &epoch} + current, err := service.AuthorizeModuleCapability(ctx, business.ModuleCaller{ + PrincipalID: principal, BoundOrg: claims.GetTenantId(), Sealed: sealed, + }) + if err != nil { + return ctx, translateGRPCError(err) + } + if !current.Grant.CrossTenant { + return ctx, connect.NewError(connect.CodePermissionDenied, errors.New("cross-tenant audit delivery grant required")) + } + ctx = stampRequestIdentity(ctx, auth.OrdinaryRequestIdentity(claims.GetOwnerPrincipalId(), claims.GetTenantId()), auth.Assurance{}) + return ctx, nil +} + +func (h *auditConnectHandler) DeliverAuditEvents(ctx context.Context, req *connect.Request[gen.DeliverAuditEventsRequest]) (*connect.Response[gen.DeliverAuditEventsResponse], error) { + ctx = connectCtx(ctx, req.Header()) + if err := Validate(req.Msg); err != nil { + return nil, translateGRPCError(err) + } + ctx, release, err := service.BeginModuleAuthorityUse(ctx) + if err != nil { + return nil, translateGRPCError(err) + } + defer release() + ctx, err = authenticateAuditDeliveryContext(ctx, req.Header(), false) + if err != nil { + return nil, err + } + effect := business.AuditDeliveryEffectID(req.Msg.From) + if !sourceWireFits(req.Msg, 256) { + return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("audit delivery input exceeds wire bound")) + } + normalized := connect.NewRequest(proto.CloneOf(req.Msg)) + normalized.Header().Set(receipts.EffectIDHeaderName, effect) + store, guard := service.SourceOperationReceipts() + if store == nil || guard == nil { + return nil, connect.NewError(connect.CodeUnavailable, errors.New("audit delivery receipts unavailable")) + } + ctx = business.WithSourceOperationRecheck(ctx, func(ctx context.Context) error { + _, err := authenticateAuditDeliveryContext(ctx, req.Header(), false) + return err + }) + return connecttransport.WrapUnary(guard, business.DeliverAuditEventsMethod, func(ctx context.Context, r *connect.Request[gen.DeliverAuditEventsRequest]) (*connect.Response[gen.DeliverAuditEventsResponse], error) { + var out *gen.DeliverAuditEventsResponse + _, err := service.DeliverAuditEvents(ctx, r.Msg.From, r.Msg.To, func(ctx context.Context, result business.AuditDeliveryResult) error { + out = &gen.DeliverAuditEventsResponse{EffectId: effect, Status: "committed", Delivered: int32(result.Delivered), From: r.Msg.From, To: r.Msg.To} + return service.RecordSourceOperationReceipt(ctx, out) + }) + if err != nil { + return nil, translateGRPCError(err) + } + return connect.NewResponse(out), nil + })(ctx, normalized) +} + +func (h *auditConnectHandler) LookupDeliverAuditEvents(ctx context.Context, req *connect.Request[gen.DeliverAuditEventsRequest]) (*connect.Response[gen.DeliverAuditEventsResponse], error) { + ctx = connectCtx(ctx, req.Header()) + if err := Validate(req.Msg); err != nil { + return nil, translateGRPCError(err) + } + ctx, release, err := service.BeginModuleAuthorityUse(ctx) + if err != nil { + return nil, translateGRPCError(err) + } + defer release() + ctx, err = authenticateAuditDeliveryContext(ctx, req.Header(), true) + if err != nil { + return nil, err + } + // The runtime POSTs the invocation's original body again for recovery. + // Its transport effect id identifies a task operation; the host receipt is + // keyed independently by the journal range's from position. + if _, err := business.ParseJournalPosition(req.Msg.From); err != nil { + return nil, translateGRPCError(err) + } + effect := business.AuditDeliveryEffectID(req.Msg.From) + tenant, _, _ := auth.VerifiedDatabaseIdentity(ctx) + store, _ := service.SourceOperationReceipts() + if store == nil { + return nil, connect.NewError(connect.CodeUnavailable, errors.New("audit delivery receipts unavailable")) + } + saved, found, err := store.Lookup(ctx, tenant, effect, business.DeliverAuditEventsMethod) + if err != nil { + return nil, connect.NewError(connect.CodeUnavailable, errors.New("audit delivery receipt unavailable")) + } + if !found { + // A generic Runnable owner reports absence as NOT_FOUND. A 200 + // response would be read as proof of a committed effect. + return nil, connect.NewError(connect.CodeNotFound, errors.New("audit delivery outcome unknown")) + } + digest, err := receipts.RequestDigest(req.Msg) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, err) + } + if !bytes.Equal(saved.RequestDigest, digest) { + return nil, connect.NewError(connect.CodeAlreadyExists, receipts.ErrEffectIDReused) + } + out := &gen.DeliverAuditEventsResponse{} + if proto.Unmarshal(saved.Response, out) != nil || !sourceWireFits(out, 1024) || out.EffectId != effect || out.Status != "committed" { + return nil, connect.NewError(connect.CodeInternal, errors.New("audit delivery receipt invalid")) + } + return connect.NewResponse(out), nil +} + +// Both transports use the same operation handler and receipt guard. +func (s *AuditServer) DeliverAuditEvents(ctx context.Context, req *gen.DeliverAuditEventsRequest) (*gen.DeliverAuditEventsResponse, error) { + r := connect.NewRequest(req) + copyAuditDeliveryMetadata(ctx, r.Header()) + out, err := (&auditConnectHandler{inner: s}).DeliverAuditEvents(ctx, r) + if err != nil { + return nil, auditDeliveryGRPCError(err) + } + return out.Msg, nil +} + +func (s *AuditServer) LookupDeliverAuditEvents(ctx context.Context, req *gen.DeliverAuditEventsRequest) (*gen.DeliverAuditEventsResponse, error) { + r := connect.NewRequest(req) + copyAuditDeliveryMetadata(ctx, r.Header()) + out, err := (&auditConnectHandler{inner: s}).LookupDeliverAuditEvents(ctx, r) + if err != nil { + return nil, auditDeliveryGRPCError(err) + } + return out.Msg, nil +} + +func copyAuditDeliveryMetadata(ctx context.Context, header http.Header) { + md, _ := metadata.FromIncomingContext(ctx) + for key, values := range md { + for _, value := range values { + header.Add(key, value) + } + } +} + +func auditDeliveryGRPCError(err error) error { + var transport *connect.Error + if !errors.As(err, &transport) { + return err + } + converted := status.New(codes.Code(transport.Code()), transport.Message()) + for _, detail := range transport.Details() { + value, err := detail.Value() + if err != nil { + continue + } + enriched, err := converted.WithDetails(protoadapt.MessageV1Of(value)) + if err == nil { + converted = enriched + } + } + return converted.Err() +} diff --git a/module/services/accounts/code/pkg/adapters/audit_delivery_test.go b/module/services/accounts/code/pkg/adapters/audit_delivery_test.go new file mode 100644 index 000000000..4aaa561d3 --- /dev/null +++ b/module/services/accounts/code/pkg/adapters/audit_delivery_test.go @@ -0,0 +1,177 @@ +package adapters + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "accounts/pkg/auth" + "accounts/pkg/business" + gen "accounts/pkg/gen/saas/accounts/v1" + + "connectrpc.com/connect" + "github.com/codefly-dev/sdk-go/receipts" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestAuditDeliveryRequiresTheModuleGrantAndDeliveryScope(t *testing.T) { + for _, tc := range []struct { + name string + crossTenant bool + actions string + audience string + lookup bool + want connect.Code + }{ + {"headless invoke", true, `"invoke","read"`, "saas-audit", false, 0}, + {"headless lookup", true, `"invoke","read"`, "saas-audit", true, 0}, + {"tenant bound module", false, `"invoke","read"`, "saas-audit", false, connect.CodePermissionDenied}, + {"lookup cannot invoke", true, `"read"`, "saas-audit", false, connect.CodePermissionDenied}, + {"invoke cannot lookup", true, `"invoke"`, "saas-audit", true, connect.CodePermissionDenied}, + {"wrong audience", true, `"invoke","read"`, "other-service", false, connect.CodeUnauthenticated}, + } { + t.Run(tc.name, func(t *testing.T) { + declared := `{"documents":{"cross_tenant":` + fmt.Sprint(tc.crossTenant) + `,"workload":{"service_account":"module","namespace":"acme-prod","container":"app"},"tenant":"` + moduleWorkContextTenant + `","operation_audiences":{"audit":{"audience":"` + tc.audience + `","invoke_scopes":[{"resource_kind":"saas.audit-delivery","actions":[` + tc.actions + `]}],"headless_scopes":[{"resource_kind":"saas.audit-delivery","actions":[` + tc.actions + `]}]}}}}` + installModuleOperationContextService(t, declared) + installModuleWorkContextAuthority(t) + issued, err := mintModuleOperationContext("documents", moduleToken(t, "documents"), "audit") + require.NoError(t, err) + header := make(http.Header) + header.Set(workContextHeader, issued.GetToken()) + ctx, err := authenticateAuditDeliveryContext(t.Context(), header, tc.lookup) + if tc.want != 0 { + require.Equal(t, tc.want, connect.CodeOf(err)) + return + } + require.NoError(t, err) + tenant, _, ok := auth.VerifiedDatabaseIdentity(ctx) + require.True(t, ok) + require.Equal(t, moduleWorkContextTenant, tenant) + // A context cannot outlive the declared grant it was minted under. + service.SetModulePrincipals(nil) + _, err = authenticateAuditDeliveryContext(t.Context(), header, tc.lookup) + require.Error(t, err) + }) + } +} + +func TestReadJournalBoundsTheProjectedWireAndResumesAtTheLastSentEntry(t *testing.T) { + data := map[string]any{} + for n := 0; n < 20000; n++ { + data[fmt.Sprintf("key%05d", n)] = float64(0) + } + page := business.ModuleReadJournalResult{NextAfter: "last"} + for n := 0; n < 12; n++ { + page.Entries = append(page.Entries, business.ModuleJournalEntry{Position: fmt.Sprintf("p%d", n), Type: business.AuditRecordedEventType, Data: data, OccurredAt: time.Now()}) + } + out, err := moduleJournalPageToProto("start", page) + require.NoError(t, err) + require.NotEmpty(t, out.Entries) + require.Less(t, len(out.Entries), len(page.Entries)) + require.LessOrEqual(t, proto.Size(out), 3<<20) + require.Equal(t, out.Entries[len(out.Entries)-1].Position, out.NextAfter) + tail, err := moduleJournalPageToProto(out.NextAfter, business.ModuleReadJournalResult{Entries: page.Entries[len(out.Entries):]}) + require.NoError(t, err) + require.Equal(t, page.Entries[len(out.Entries)].Position, tail.Entries[0].Position) + _, err = moduleJournalPageToProto("start", business.ModuleReadJournalResult{Entries: []business.ModuleJournalEntry{{Data: map[string]any{"large": strings.Repeat("x", 4<<20)}}}}) + require.Error(t, err) + empty, err := moduleJournalPageToProto("cursor", business.ModuleReadJournalResult{}) + require.NoError(t, err) + require.Equal(t, "cursor", empty.NextAfter) +} + +// Reuse the existing receipt fixture and module issuer; no warehouse is needed +// to prove how a committed receipt and an inconclusive lookup travel on the wire. +type auditReceiptWireStore struct{ *recordingModuleStore } + +func (s *auditReceiptWireStore) ReadJournalPage(context.Context, business.JournalPageQuery) ([]business.JournalRow, error) { + return []business.JournalRow{{Position: business.JournalPosition{Xact: 10, Seq: 1}, EventID: "event", Type: "example.recorded.v1", ContentType: "application/json", Data: []byte(`{"value":1}`), OccurredAt: time.Now()}}, nil +} + +func (s *auditReceiptWireStore) WithOrgTx(ctx context.Context, _ string, fn func(context.Context) error) error { + return fn(ctx) +} +func (s *auditReceiptWireStore) RecordSourceOperationReceipt(ctx context.Context, store receipts.Store, response proto.Message) error { + return receipts.Record(ctx, store, sourceWireTx{}, response) +} + +func TestAuditDeliveryRuntimeRecoveryUsesTheOriginalInputAndRangeReceipt(t *testing.T) { + declared := strings.ReplaceAll(headlessPrincipals, "modelservice", "saas-audit") + declared = strings.ReplaceAll(declared, "saas-audit.profiles", "saas.audit-delivery") + declared = strings.ReplaceAll(declared, `,"resource_ids":["profile-digest"]`, "") + declared = strings.Replace(declared, `{"documents":{`, `{"documents":{"cross_tenant":true,`, 1) + underlying, audit := installModuleOperationContextService(t, declared) + registry := service.ModulePrincipals() + svc, err := business.NewService(&auditReceiptWireStore{underlying}) + require.NoError(t, err) + svc.SetAuditEmitter(audit) + svc.SetModuleAuthorityReads(currentModuleAuthority{service: svc}, nil) + svc.SetModuleCapabilities(nil, nil, registry) + bindModuleExecution(t, svc, registry) + WithService(svc) + installModuleWorkContextAuthority(t) + // The shared Connect-to-gRPC adapter must preserve the module credential. + identity, err := mintModuleWorkContext(t, "documents", moduleToken(t, "documents")) + require.NoError(t, err) + capHandler := &moduleCapabilitiesConnectHandler{inner: ModuleCapabilitiesSingleton()} + read := connect.NewRequest(&gen.ModuleReadJournalRequest{After: business.JournalStart, Types: []string{"example.recorded.v1"}}) + read.Header().Set(workContextHeader, identity.Token) + page, err := capHandler.ReadJournal(t.Context(), read) + require.NoError(t, err) + require.Len(t, page.Msg.Entries, 1) + require.Equal(t, "v1.10.1", page.Msg.NextAfter) + read.Header().Add(workContextHeader, identity.Token) + _, err = capHandler.ReadJournal(t.Context(), read) + require.Equal(t, connect.CodeUnauthenticated, connect.CodeOf(err)) + store := receipts.NewMemoryStore() + require.NoError(t, svc.ConfigureSourceOperationReceipts(store)) + issued, err := mintModuleOperationContext("documents", moduleToken(t, "documents"), "model") + require.NoError(t, err) + handler := &auditConnectHandler{} + mux := http.NewServeMux() + mux.Handle(business.DeliverAuditEventsMethod, connect.NewUnaryHandler(business.DeliverAuditEventsMethod, handler.DeliverAuditEvents)) + mux.Handle(business.LookupDeliverAuditEventsMethod, connect.NewUnaryHandler(business.LookupDeliverAuditEventsMethod, handler.LookupDeliverAuditEvents)) + server := httptest.NewServer(mux) + defer server.Close() + invoke := connect.NewClient[gen.DeliverAuditEventsRequest, gen.DeliverAuditEventsResponse](server.Client(), server.URL+business.DeliverAuditEventsMethod, connect.WithProtoJSON()) + lookup := connect.NewClient[gen.DeliverAuditEventsRequest, gen.DeliverAuditEventsResponse](server.Client(), server.URL+business.LookupDeliverAuditEventsMethod, connect.WithProtoJSON()) + input := &gen.DeliverAuditEventsRequest{From: business.JournalStart, To: "v1.10.1"} + request := func(msg *gen.DeliverAuditEventsRequest) *connect.Request[gen.DeliverAuditEventsRequest] { + req := connect.NewRequest(proto.CloneOf(msg)) + req.Header().Set(workContextHeader, issued.Token) + req.Header().Set(receipts.EffectIDHeaderName, "task/operations/deliver") + req.Header().Set("Codefly-Runnable-Effect-Id", "task/operations/deliver") + return req + } + _, err = lookup.CallUnary(t.Context(), request(input)) + require.Equal(t, connect.CodeNotFound, connect.CodeOf(err), "absence must not be an HTTP 200 committed outcome") + // This deployment is Postgres; reaching its business refusal proves that the + // task's transport identity was accepted and normalized to the range identity. + _, err = invoke.CallUnary(t.Context(), request(input)) + require.Equal(t, connect.CodeFailedPrecondition, connect.CodeOf(err)) + effect := business.AuditDeliveryEffectID(input.From) + digest, err := receipts.RequestDigest(input) + require.NoError(t, err) + committed := &gen.DeliverAuditEventsResponse{EffectId: effect, Status: "committed", Delivered: 2} + ctx := receipts.WithEffect(t.Context(), receipts.Effect{ID: effect, Tenant: moduleWorkContextTenant, Method: business.DeliverAuditEventsMethod, RequestDigest: digest}) + require.NoError(t, receipts.Record(ctx, store, sourceWireTx{}, committed)) + replay, err := invoke.CallUnary(t.Context(), request(input)) + require.NoError(t, err) + require.True(t, proto.Equal(committed, replay.Msg)) + recovered, err := lookup.CallUnary(t.Context(), request(input)) + require.NoError(t, err) + require.True(t, proto.Equal(committed, recovered.Msg)) + changed := proto.CloneOf(input) + changed.To = "v1.10.2" + _, err = lookup.CallUnary(t.Context(), request(changed)) + require.Equal(t, connect.CodeAlreadyExists, connect.CodeOf(err)) + // A stamped login still lacks the operation context required by the handler. + login := auth.WithVerifiedDatabaseIdentity(t.Context(), business.ModulePrincipalID("documents"), moduleWorkContextTenant) + _, err = handler.DeliverAuditEvents(login, connect.NewRequest(input)) + require.Equal(t, connect.CodeUnauthenticated, connect.CodeOf(err)) +} diff --git a/module/services/accounts/code/pkg/adapters/connect_auth_interceptor.go b/module/services/accounts/code/pkg/adapters/connect_auth_interceptor.go index 936c0d702..d5ca271c4 100644 --- a/module/services/accounts/code/pkg/adapters/connect_auth_interceptor.go +++ b/module/services/accounts/code/pkg/adapters/connect_auth_interceptor.go @@ -272,6 +272,9 @@ func (i *connectPolicyInterceptor) authorize(ctx context.Context, procedure stri return ctx, connect.NewError(connect.CodePermissionDenied, errors.New("internal RPC is not exposed on the tenant listener")) } + if (procedure == business.DeliverAuditEventsMethod || procedure == business.LookupDeliverAuditEventsMethod) && len(headers.Values(workContextHeader)) != 0 { + return authenticateAuditDeliveryContext(ctx, headers, procedure == business.LookupDeliverAuditEventsMethod) + } if sourceOperationProcedure(procedure) && len(headers.Values(workContextHeader)) != 0 { return authenticateSourceOperationContext(ctx, headers) } diff --git a/module/services/accounts/code/pkg/adapters/connect_handlers.go b/module/services/accounts/code/pkg/adapters/connect_handlers.go index 8892bf1cd..21d35579d 100644 --- a/module/services/accounts/code/pkg/adapters/connect_handlers.go +++ b/module/services/accounts/code/pkg/adapters/connect_handlers.go @@ -35,6 +35,9 @@ func connectCtx(ctx context.Context, h http.Header) context.Context { md.Set(string(key), values...) } } + if v := h.Values(workContextHeader); len(v) > 0 { + md.Set(workContextHeader, v...) + } if v := h.Values("Authorization"); len(v) > 0 { md.Set("authorization", v...) } diff --git a/module/services/accounts/code/pkg/adapters/grpc_auth_interceptor.go b/module/services/accounts/code/pkg/adapters/grpc_auth_interceptor.go index 0cc22971f..dbf866bf9 100644 --- a/module/services/accounts/code/pkg/adapters/grpc_auth_interceptor.go +++ b/module/services/accounts/code/pkg/adapters/grpc_auth_interceptor.go @@ -4,6 +4,7 @@ import ( "context" "crypto/subtle" "errors" + "net/http" "strings" "sync" "time" @@ -377,6 +378,15 @@ func (i *grpcPolicyAuthorizer) authorize(ctx context.Context, fullMethod string) return ctx, nil } + if (fullMethod == business.DeliverAuditEventsMethod || fullMethod == business.LookupDeliverAuditEventsMethod) && len(md.Get(workContextHeader)) != 0 { + headers := make(http.Header) + copyAuditDeliveryMetadata(ctx, headers) + authenticated, err := authenticateAuditDeliveryContext(ctx, headers, fullMethod == business.LookupDeliverAuditEventsMethod) + if err != nil { + return ctx, auditDeliveryGRPCError(err) + } + return authenticated, nil + } if trustedForwarded && hasForwardedIdentity(md) { forwarded, err := stampForwardedGRPCIdentity(ctx, md) if err != nil { diff --git a/module/services/accounts/code/pkg/adapters/module_capabilities_connect.go b/module/services/accounts/code/pkg/adapters/module_capabilities_connect.go index 79d4210a5..6ae353077 100644 --- a/module/services/accounts/code/pkg/adapters/module_capabilities_connect.go +++ b/module/services/accounts/code/pkg/adapters/module_capabilities_connect.go @@ -111,3 +111,7 @@ func (h *moduleCapabilitiesConnectHandler) ReplayEvents(ctx context.Context, req func (h *moduleCapabilitiesConnectHandler) ResolveWorkContextAuthority(ctx context.Context, req *connect.Request[gen.ModuleResolveWorkContextAuthorityRequest]) (*connect.Response[gen.ModuleResolveWorkContextAuthorityResponse], error) { return unary(ctx, req, h.inner.ResolveWorkContextAuthority) } + +func (h *moduleCapabilitiesConnectHandler) ReadJournal(ctx context.Context, req *connect.Request[gen.ModuleReadJournalRequest]) (*connect.Response[gen.ModuleReadJournalResponse], error) { + return unary(ctx, req, h.inner.ReadJournal) +} diff --git a/module/services/accounts/code/pkg/adapters/module_capabilities_server.go b/module/services/accounts/code/pkg/adapters/module_capabilities_server.go index 3762eb6cb..18206b762 100644 --- a/module/services/accounts/code/pkg/adapters/module_capabilities_server.go +++ b/module/services/accounts/code/pkg/adapters/module_capabilities_server.go @@ -14,6 +14,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" @@ -984,3 +985,54 @@ func moduleApprovalProto(a *business.ApprovalRequest) *gen.ModuleApproval { CreatedAt: timestamppb.New(a.CreatedAt), } } + +func (s *ModuleCapabilitiesServer) ReadJournal(ctx context.Context, req *gen.ModuleReadJournalRequest) (*gen.ModuleReadJournalResponse, error) { + ctx, releaseAuthority, fenceErr := service.BeginModuleAuthorityUse(ctx) + if fenceErr != nil { + return nil, fenceErr + } + defer releaseAuthority() + + if err := Validate(req); err != nil { + return nil, err + } + caller, err := moduleCaller(ctx) + if err != nil { + return nil, err + } + page, err := service.ModuleReadJournal(ctx, caller, business.ModuleReadJournalInput{ + After: req.GetAfter(), Limit: req.GetLimit(), Types: req.GetTypes(), DeliveryBindings: req.GetDeliveryBindings(), + }) + if err != nil { + return nil, err + } + return moduleJournalPageToProto(req.GetAfter(), page) +} + +func moduleJournalPageToProto(after string, page business.ModuleReadJournalResult) (*gen.ModuleReadJournalResponse, error) { + out := &gen.ModuleReadJournalResponse{NextAfter: after} + for _, entry := range page.Entries { + data, err := structpb.NewStruct(entry.Data) + if err != nil { + return nil, status.Error(codes.Internal, "journal entry cannot be represented as an object") + } + projected := &gen.ModuleJournalEntry{ + Position: entry.Position, EventId: entry.EventID, Type: entry.Type, OrganizationId: entry.OrganizationID, + OccurredAt: timestamppb.New(entry.OccurredAt), Data: data, DeliveryMachinery: entry.DeliveryMachinery, + } + // Struct encodes objects more widely than their JSON bytes. Leave room + // below the transport's 4 MiB limit and resume at the last entry sent. + out.Entries = append(out.Entries, projected) + previous := out.NextAfter + out.NextAfter = entry.Position + if proto.Size(out) > business.ModuleJournalMaxWireBytes { + out.Entries = out.Entries[:len(out.Entries)-1] + out.NextAfter = previous + if len(out.Entries) == 0 { + return nil, status.Error(codes.ResourceExhausted, "journal entry exceeds the transport bound") + } + break + } + } + return out, nil +} diff --git a/module/services/accounts/code/pkg/adapters/source_operation_authority.go b/module/services/accounts/code/pkg/adapters/source_operation_authority.go index 970c7a58c..ee6156722 100644 --- a/module/services/accounts/code/pkg/adapters/source_operation_authority.go +++ b/module/services/accounts/code/pkg/adapters/source_operation_authority.go @@ -35,7 +35,9 @@ type sourceOperationDelegationKey struct{} func sourceOperationProcedure(method string) bool { switch method { - case business.SourceOperationMethod, + case business.DeliverAuditEventsMethod, + business.LookupDeliverAuditEventsMethod, + business.SourceOperationMethod, "/saas.accounts.v1.DatasourceService/ListSourceOperations", "/saas.accounts.v1.DatasourceService/LookupInvokeSourceOperation", business.SourceReceiptRetentionMethod, diff --git a/module/services/accounts/code/pkg/auditstorage/config.go b/module/services/accounts/code/pkg/auditstorage/config.go new file mode 100644 index 000000000..6ab76711c --- /dev/null +++ b/module/services/accounts/code/pkg/auditstorage/config.go @@ -0,0 +1,90 @@ +// Package auditstorage reads the `audit-storage` workspace group: which backend +// keeps the audit trail, and the names of the warehouse dataset and table when +// that backend is the warehouse. +// +// The group carries names and nothing else. A warehouse is reached through the +// service the composition provides, resolved at startup like every other +// endpoint, so no endpoint, address, project, credential or token is configured +// here, and none may be added to the group. +package auditstorage + +import ( + "context" + "fmt" + "os" + "regexp" + "strings" + + codefly "github.com/codefly-dev/sdk-go" + + "accounts/pkg/business" +) + +// ConfigurationGroup is the workspace group that selects the audit storage. +const ConfigurationGroup = "audit-storage" + +// The keys the group carries. Values only: a dataset or table name is not a +// secret, and the group holds no credential. +const ( + // BackendKey selects where audit entries are kept: `postgres` (the default, + // audit_events) or `warehouse`. + BackendKey = "AUDIT_STORAGE_BACKEND" + // DatasetKey names the warehouse dataset. Required for `warehouse`. + DatasetKey = "AUDIT_STORAGE_DATASET" + // TableKey names the warehouse table. Required for `warehouse`. + TableKey = "AUDIT_STORAGE_TABLE" +) + +// Config is what the group resolved to. +type Config struct { + Backend business.AuditBackend + // Dataset and Table name the warehouse table entries are appended to. They + // are empty, and ignored, under the Postgres backend. + Dataset string + Table string +} + +// name is the shape a dataset or table name may take: letters, digits, +// underscores and dashes, not starting with a dash, at most maxNameLength long. +// It names no project, path or address, so a value that does is a mistake caught +// at startup. +var name = regexp.MustCompile(`^[A-Za-z0-9_][A-Za-z0-9_-]*$`) + +const maxNameLength = 1024 + +// Load reads the `audit-storage` group, falling back to a plain process +// variable exactly as the rest of accounts' workspace reads do. +func Load(ctx context.Context) (Config, error) { + return LoadFrom(func(key string) string { + value, err := codefly.For(ctx).WorkspaceValue(ConfigurationGroup, key) + if err != nil || value == "" { + value = os.Getenv(key) + } + return value + }) +} + +// LoadFrom resolves the group from read, which answers one key. +func LoadFrom(read func(key string) string) (Config, error) { + backend, err := business.ParseAuditBackend(read(BackendKey)) + if err != nil { + return Config{}, fmt.Errorf("%s in the `%s` group: %w", BackendKey, ConfigurationGroup, err) + } + config := Config{ + Backend: backend, + Dataset: strings.TrimSpace(read(DatasetKey)), + Table: strings.TrimSpace(read(TableKey)), + } + if backend != business.AuditBackendWarehouse { + return config, nil + } + for _, key := range []struct{ name, value string }{{DatasetKey, config.Dataset}, {TableKey, config.Table}} { + if key.value == "" { + return Config{}, fmt.Errorf("the `%s` audit backend requires %s in the `%s` group: it names the warehouse table the audit trail is appended to", backend, key.name, ConfigurationGroup) + } + if len(key.value) > maxNameLength || !name.MatchString(key.value) { + return Config{}, fmt.Errorf("%s in the `%s` group is %q, which is not a dataset or table name: use letters, digits, underscores and dashes, and no project, path or address", key.name, ConfigurationGroup, key.value) + } + } + return config, nil +} diff --git a/module/services/accounts/code/pkg/auditstorage/config_test.go b/module/services/accounts/code/pkg/auditstorage/config_test.go new file mode 100644 index 000000000..face52577 --- /dev/null +++ b/module/services/accounts/code/pkg/auditstorage/config_test.go @@ -0,0 +1,66 @@ +package auditstorage + +import ( + "strings" + "testing" + + "accounts/pkg/business" +) + +func values(m map[string]string) func(string) string { + return func(key string) string { return m[key] } +} + +func TestLoadDefaultsToPostgres(t *testing.T) { + for _, raw := range []string{"", "postgres", " POSTGRES "} { + config, err := LoadFrom(values(map[string]string{BackendKey: raw})) + if err != nil || config.Backend != business.AuditBackendPostgres { + t.Fatalf("backend %q = %+v %v", raw, config, err) + } + } + // Names under Postgres are carried but never required. + config, err := LoadFrom(values(map[string]string{DatasetKey: "audit"})) + if err != nil || config.Dataset != "audit" { + t.Fatalf("config = %+v %v", config, err) + } +} + +func TestLoadWarehouseNeedsBothNames(t *testing.T) { + config, err := LoadFrom(values(map[string]string{BackendKey: "warehouse", DatasetKey: " audit_v1 ", TableKey: "entries-2026"})) + if err != nil { + t.Fatal(err) + } + if config.Backend != business.AuditBackendWarehouse || config.Dataset != "audit_v1" || config.Table != "entries-2026" { + t.Fatalf("config = %+v", config) + } + for name, in := range map[string]map[string]string{ + "no dataset": {BackendKey: "warehouse", TableKey: "t"}, + "no table": {BackendKey: "warehouse", DatasetKey: "d"}, + } { + if _, err := LoadFrom(values(in)); err == nil || !strings.Contains(err.Error(), ConfigurationGroup) { + t.Errorf("%s: err = %v, want a refusal naming the group", name, err) + } + } +} + +// The group carries names, and a value that looks like a project, a path or an +// address is refused rather than quietly passed on. +func TestLoadRefusesWhatIsNotAName(t *testing.T) { + for _, bad := range []string{"project.dataset", "my-project:audit", "a/b", "https://example.com", "-leading", "has space", strings.Repeat("a", 1025)} { + in := map[string]string{BackendKey: "warehouse", DatasetKey: bad, TableKey: "t"} + if _, err := LoadFrom(values(in)); err == nil { + t.Errorf("dataset %q was accepted", bad) + } + in = map[string]string{BackendKey: "warehouse", DatasetKey: "d", TableKey: bad} + if _, err := LoadFrom(values(in)); err == nil { + t.Errorf("table %q was accepted", bad) + } + } +} + +func TestLoadRefusesAnUnknownBackend(t *testing.T) { + _, err := LoadFrom(values(map[string]string{BackendKey: "clickhouse"})) + if err == nil || !strings.Contains(err.Error(), BackendKey) { + t.Fatalf("err = %v", err) + } +} diff --git a/module/services/accounts/code/pkg/business/audit.go b/module/services/accounts/code/pkg/business/audit.go index 317b173d0..af1a20ebb 100644 --- a/module/services/accounts/code/pkg/business/audit.go +++ b/module/services/accounts/code/pkg/business/audit.go @@ -136,6 +136,13 @@ type DurableAuditEmitter struct { producer jobs.Producer transport events.Transport teeExternal bool + // storage is where the entry is appended on the caller's transaction. It + // defaults to the Postgres storage over store, so an emitter built without + // AuditService writes audit_events exactly as it always has. + storage AuditStorage + // journal publishes each entry to the journal (AuditRecordedEventType) in + // the caller's transaction instead of appending it. Set by WithAuditJournal. + journal bool } // DurableAuditEmitterOption tunes the emitter at construction. @@ -149,6 +156,23 @@ func WithExternalTee() DurableAuditEmitterOption { return func(e *DurableAuditEmitter) { e.teeExternal = true } } +// WithAuditStorage sets the storage each entry is appended to, on the caller's +// transaction. AuditService passes the storage of the backend it chose. +func WithAuditStorage(storage AuditStorage) DurableAuditEmitterOption { + return func(e *DurableAuditEmitter) { e.storage = storage } +} + +// WithAuditJournal records each entry as a journal event, saas.audit.recorded.v1, +// published on the caller's transaction through the domain-event transport, in +// place of appending it to a storage: the entry's store of record is outside +// Postgres, and a later task appends it (AuditService.Deliver). Every entry is +// published, including one with no organization. It needs the transport +// (WithDomainEventTransport), and it excludes the external tee, which copies +// audit_events rows that this mode never writes. +func WithAuditJournal() DurableAuditEmitterOption { + return func(e *DurableAuditEmitter) { e.journal = true } +} + // WithDomainEventTransport publishes an external-visibility domain event beside // each org-scoped audit row, in the same transaction. That event is what an // outbound webhook subscription is fanned out from; without a transport wired the @@ -168,6 +192,23 @@ func NewDurableAuditEmitter(store Store, producer jobs.Producer, opts ...Durable for _, opt := range opts { opt(emitter) } + if emitter.journal { + if emitter.transport == nil { + return nil, errors.New("audit: the journal needs the domain-event transport") + } + if emitter.teeExternal { + return nil, errors.New("audit: the external tee copies audit_events rows, and the journal mode writes none") + } + if emitter.storage != nil { + return nil, errors.New("audit: an entry is either appended to a storage or published to the journal, not both") + } + } else if emitter.storage == nil { + storage, err := NewPostgresAuditStorage(store) + if err != nil { + return nil, err + } + emitter.storage = storage + } return emitter, nil } @@ -204,8 +245,9 @@ func (e *DurableAuditEmitter) resolve(ctx context.Context, entry *AuditEntry) (R return resolved, nil } -// write inserts the audit row and publishes its domain event using whatever -// transaction is already on ctx (getQueryExecutor / Publish both pick it up). +// write appends the audit entry to the storage and publishes its domain event +// using whatever transaction is already on ctx (getQueryExecutor / Publish both +// pick it up). func (e *DurableAuditEmitter) write(ctx context.Context, entry AuditEntry) error { resolved, err := e.resolve(ctx, &entry) if err != nil { @@ -226,7 +268,11 @@ func (e *DurableAuditEmitter) write(ctx context.Context, entry AuditEntry) error return nil } } - if err := e.store.InsertAuditEvent(ctx, entry); err != nil { + if e.journal { + if err := e.publishJournal(ctx, entry); err != nil { + return err + } + } else if err := e.storage.Append(ctx, entry); err != nil { return err } if entry.OrgID == "" { @@ -247,6 +293,21 @@ func (e *DurableAuditEmitter) write(ctx context.Context, entry AuditEntry) error return nil } +// publishJournal records the entry as a journal event on the transaction already +// on ctx: the organization's for an organization-scoped entry, and for one with +// no organization whichever the caller holds. That is the control plane's when +// Emit opens it, or the caller's own user-scoped transaction under EmitTx, where +// publish_domain_event admits the event because its actor is that transaction's +// user (migration 41). The event commits with the mutation or not at all; there +// is no second write to lose. +func (e *DurableAuditEmitter) publishJournal(ctx context.Context, entry AuditEntry) error { + envelope, err := NewAuditJournalEnvelope(entry) + if err != nil { + return err + } + return e.transport.Publish(ctx, moduleTx(ctx), envelope) +} + func (e *DurableAuditEmitter) Emit(ctx context.Context, entry AuditEntry) { e.normalize(&entry) var err error @@ -435,30 +496,16 @@ type AuditAggregateBucket struct { Metrics map[string]float64 } -// QueryAuditLog delegates to the store, scoping the read to the -// requested org under WithOrgTx so RLS lets the rows through. When -// OrgID is empty the caller is platform-admin (handler authz -// already enforced this in adapters/rpcs.go AuditServer.QueryAuditLog) -// and we use WithControlPlane to span tenants. +// QueryAuditLog reads one page of the activity list through the AuditService, +// scoping the read to the requested org so RLS lets the rows through. When +// OrgID is empty the caller is platform-admin (handler authz already enforced +// this in adapters/rpcs.go AuditServer.QueryAuditLog) and the read spans tenants. func (s *Service) QueryAuditLog(ctx context.Context, q AuditQuery) ([]AuditEntry, string, int32, error) { if q.CollectionID != "" { return nil, "", 0, status.Error(codes.InvalidArgument, "collection analytics requires reader authorization") } - var entries []AuditEntry - var nextToken string - var total int32 - wrap := func(ctx context.Context) error { - ev, nt, tot, err := s.store.QueryAuditLog(ctx, q) - entries, nextToken, total = ev, nt, tot - return err - } - var err error - if q.OrgID == "" { - err = s.store.WithControlPlane(ctx, wrap) - } else { - err = s.store.WithOrgTx(ctx, q.OrgID, wrap) - } - return entries, nextToken, total, err + entries, nextToken, err := s.auditReads.List(ctx, q) + return entries, nextToken, int32(len(entries)), err } // AggregateAuditLogForReader additionally gates exact-resource analytics on the @@ -500,44 +547,46 @@ func (s *Service) AggregateAuditLogForReader(ctx context.Context, reader string, payload["boundary"] = q.CollectionID q.PayloadContains = payload } - var out []AuditAggregateBucket - err := s.store.WithOrgTx(ctx, q.OrgID, func(ctx context.Context) error { - // Each scope proves itself; there is no permissive default to fall - // through to. `proven` records that at least one check actually ran, so - // a future caller reaching here with neither scope set is denied rather - // than served on the strength of an untested assumption. - denied := status.Error(codes.PermissionDenied, "resource read access required") - proven := false - if q.CollectionID != "" { - allowed, err := s.canReadAuditCollection(ctx, reader, q.OrgID, q.CollectionID) - if err != nil { - return err - } - if !allowed { - return denied - } - proven = true + compiled := q + compiled.CollectionID = "" // Authorized and compiled into the boundary predicate above. + return s.auditReads.AggregateAuthorized(ctx, compiled, spec, func(ctx context.Context) error { + return s.authorizeAuditResourceRead(ctx, reader, q) + }) +} + +// authorizeAuditResourceRead proves the reader may read the resource or +// collection q names, inside the caller's tenant transaction. Each scope proves +// itself; there is no permissive default to fall through to. `proven` records +// that at least one check actually ran, so a future caller reaching here with +// neither scope set is denied rather than served on the strength of an +// untested assumption. +func (s *Service) authorizeAuditResourceRead(ctx context.Context, reader string, q AuditQuery) error { + denied := status.Error(codes.PermissionDenied, "resource read access required") + proven := false + if q.CollectionID != "" { + allowed, err := s.canReadAuditCollection(ctx, reader, q.OrgID, q.CollectionID) + if err != nil { + return err } - if q.ResourceID != "" { - allowed, err := s.canReadAuditResource(ctx, reader, q) - if err != nil { - return err - } - if !allowed { - return denied - } - proven = true + if !allowed { + return denied } - if !proven { + proven = true + } + if q.ResourceID != "" { + allowed, err := s.canReadAuditResource(ctx, reader, q) + if err != nil { + return err + } + if !allowed { return denied } - var err error - compiled := q - compiled.CollectionID = "" // Authorized and compiled into the boundary predicate above. - out, err = s.store.AggregateAuditLog(ctx, compiled, spec) - return err - }) - return out, err + proven = true + } + if !proven { + return denied + } + return nil } // isCollectionBoundaryEvent reports whether eventType is a registered event @@ -595,24 +644,12 @@ func (s *Service) canReadAuditCollection(ctx context.Context, reader, orgID, bou // The spec selects the group dimensions (event type, category, actor, time // bucket, or a payload field), the aggregations (count, distinct-count, sum, // avg, min, max, percentile over payload fields), and any derived ratios, -// filtered by the same predicates as QueryAuditLog. +// filtered by the same predicates as QueryAuditLog and read in the same scope. func (s *Service) AggregateAuditLog(ctx context.Context, q AuditQuery, spec AuditAggregationSpec) ([]AuditAggregateBucket, error) { if q.CollectionID != "" { return nil, status.Error(codes.InvalidArgument, "collection analytics requires reader authorization") } - var out []AuditAggregateBucket - wrap := func(ctx context.Context) error { - var err error - out, err = s.store.AggregateAuditLog(ctx, q, spec) - return err - } - var err error - if q.OrgID == "" { - err = s.store.WithControlPlane(ctx, wrap) - } else { - err = s.store.WithOrgTx(ctx, q.OrgID, wrap) - } - return out, err + return s.auditReads.Aggregate(ctx, q, spec) } // buildAuditEntry assembles an AuditEntry. actorID is the effective subject the diff --git a/module/services/accounts/code/pkg/business/audit_delivery.go b/module/services/accounts/code/pkg/business/audit_delivery.go new file mode 100644 index 000000000..3f0a72119 --- /dev/null +++ b/module/services/accounts/code/pkg/business/audit_delivery.go @@ -0,0 +1,197 @@ +package business + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/codefly-dev/sdk-go/receipts" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// DeliverAuditEventsMethod is the host operation that appends a range of the +// journal's audit records to the warehouse (ADR 0009). It is an operation, not a +// sweep: it names one range, it carries the effect identity AuditDeliveryEffectID +// derives from it, and its receipt is read back rather than inferred. +const AuditDeliveryAudience = "saas-audit" +const AuditDeliveryResourceKind = "saas.audit-delivery" +const LookupDeliverAuditEventsMethod = "/saas.accounts.v1.AuditService/LookupDeliverAuditEvents" + +const DeliverAuditEventsMethod = "/saas.accounts.v1.AuditService/DeliverAuditEvents" + +// AuditDeliveryMaxRange is the most audit records one delivery takes: the page +// maximum of the journal read that produced the range. +const AuditDeliveryMaxRange = ModuleJournalMaxPage + +// Reasons a delivery is refused, on the ErrorInfo of its refusal. +const ( + // AuditDeliveryRangeNotVisibleReason: the range's end is not yet below the + // oldest transaction still running, so rows could still appear inside it. + AuditDeliveryRangeNotVisibleReason = "AUDIT_RANGE_NOT_VISIBLE" + // AuditDeliveryRangeTooWideReason: more audit records than a delivery takes. + AuditDeliveryRangeTooWideReason = "AUDIT_RANGE_TOO_WIDE" + // AuditDeliveryNotWarehouseReason: this host's audit storage is not the + // warehouse, so the journal holds no audit records to deliver. + AuditDeliveryNotWarehouseReason = "AUDIT_BACKEND_NOT_WAREHOUSE" + // AuditDeliveryRecordRefusedReason: a journal row's record contradicts its + // row, and is not delivered. + AuditDeliveryRecordRefusedReason = "AUDIT_RECORD_REFUSED" +) + +// AuditDeliveryEffectID is the effect identity of the delivery of the range that +// starts after from: one range, one receipt. from is the position or JournalStart, +// exactly as the request spells it. +func AuditDeliveryEffectID(from string) string { + return AuditRecordedEventType + "/" + from +} + +// AuditDeliveryResult is what a delivery committed. +type AuditDeliveryResult struct { + // Delivered is the number of audit records of the range appended to the + // storage. + Delivered int +} + +// DeliverAuditEvents appends every saas.audit.recorded.v1 record with a position in +// (from, to] to the audit storage, once the range is final, and commits the range's +// receipt. +// +// It is called only as an admitted operation: the receipt guard has placed the +// effect in ctx, under the tenant of the installation's operation context, and the +// effect's id must be the range's (AuditDeliveryEffectID). Replay of a committed +// range is answered by the guard from its receipt and never reaches here. +// +// - The range is read with the visibility rule of the journal read. If to is not +// yet below the oldest transaction still running the range could still gain a +// row, so the delivery is refused (FailedPrecondition, AUDIT_RANGE_NOT_VISIBLE) +// and nothing is appended; a range more than AuditDeliveryMaxRange records wide +// is refused (InvalidArgument, AUDIT_RANGE_TOO_WIDE) before anything is. +// - Each record is appended to the storage with its organization taken from its +// journal row, never from the caller; a record that names another organization +// than its row is refused. An entry with no organization has none. +// - Delivery is at least once: a range delivered twice, or two ranges that +// overlap, append a record twice, and every read of the storage returns each +// entry once, by entry id. A failed append is Unavailable and no receipt is +// committed, so the whole range is attempted again. +// - commit runs in a transaction of the effect's tenant, after every append, and +// records the receipt there. A range with no audit records still commits one. +// - It emits no audit of its own: delivering the audit trail must not write to it. +func (s *Service) DeliverAuditEvents(ctx context.Context, from, to string, commit func(context.Context, AuditDeliveryResult) error) (AuditDeliveryResult, error) { + effect, ok := receipts.EffectFromContext(ctx) + if !ok || effect.Method != DeliverAuditEventsMethod || effect.Tenant == "" || effect.ID != AuditDeliveryEffectID(from) || commit == nil { + return AuditDeliveryResult{}, status.Error(codes.FailedPrecondition, "audit delivery runs only as an admitted operation for its own range") + } + if from == "" { + return AuditDeliveryResult{}, status.Error(codes.InvalidArgument, `from names the start of the range; the beginning is "start"`) + } + fromPos, err := ParseJournalPosition(from) + if err != nil { + return AuditDeliveryResult{}, err + } + toPos, err := ParseJournalPosition(to) + if err != nil { + return AuditDeliveryResult{}, err + } + if to == "" || to == JournalStart || !fromPos.Less(toPos) { + return AuditDeliveryResult{}, status.Error(codes.InvalidArgument, "to must be a position after from") + } + if s.auditReads == nil || s.auditReads.Backend() != AuditBackendWarehouse { + return AuditDeliveryResult{}, moduleRefusal(codes.FailedPrecondition, AuditDeliveryNotWarehouseReason, + "this host keeps audit in Postgres; there is no journal record to deliver") + } + storage := s.auditReads.Storage() + + query := JournalPageQuery{After: fromPos, Through: &toPos, Types: []string{AuditRecordedEventType}} + var visible bool + var width int + if err := s.store.WithControlPlane(ctx, func(ctx context.Context) error { + var err error + if visible, err = s.store.JournalPositionVisible(ctx, toPos); err != nil || !visible { + return err + } + width, err = s.store.CountJournalRange(ctx, query, AuditDeliveryMaxRange+1) + return err + }); err != nil { + return AuditDeliveryResult{}, status.Error(codes.Unavailable, "read the journal range: "+err.Error()) + } + if !visible { + return AuditDeliveryResult{}, moduleRefusal(codes.FailedPrecondition, AuditDeliveryRangeNotVisibleReason, + "the end of the range is not yet below the oldest transaction still running") + } + if width > AuditDeliveryMaxRange { + return AuditDeliveryResult{}, moduleRefusal(codes.InvalidArgument, AuditDeliveryRangeTooWideReason, + fmt.Sprintf("the range holds more than %d audit records", AuditDeliveryMaxRange)) + } + + var result AuditDeliveryResult + query.Limit, query.MaxBytes = ModuleJournalDefaultPage, ModuleJournalMaxPageBytes + for page := 0; page <= AuditDeliveryMaxRange; page++ { + var rows []JournalRow + if err := s.store.WithControlPlane(ctx, func(ctx context.Context) error { + var err error + rows, err = s.store.ReadJournalPage(ctx, query) + return err + }); err != nil { + return AuditDeliveryResult{}, status.Error(codes.Unavailable, "read the journal range: "+err.Error()) + } + if len(rows) == 0 { + break + } + for _, row := range rows { + entry, err := journalDecodeAudit(row) + if err != nil { + return AuditDeliveryResult{}, moduleRefusal(codes.FailedPrecondition, AuditDeliveryRecordRefusedReason, err.Error()) + } + if err := storage.Append(ctx, entry); err != nil { + return AuditDeliveryResult{}, status.Error(codes.Unavailable, "append audit entry "+entry.ID+": "+err.Error()) + } + result.Delivered++ + } + query.After = rows[len(rows)-1].Position + } + + // The receipt commits in a transaction of the effect's tenant, which is what + // binds it to the installation the operation ran for. The organizations of the + // records are the rows', and have no part in it. + if err := s.store.WithOrgTx(ctx, effect.Tenant, func(ctx context.Context) error { + return commit(ctx, result) + }); err != nil { + if status.Code(err) == codes.Unknown { + return AuditDeliveryResult{}, status.Error(codes.Unavailable, "commit the delivery receipt: "+err.Error()) + } + return AuditDeliveryResult{}, err + } + return result, nil +} + +// ErrJournalRowDisagrees is the refusal of a journal row whose own record +// contradicts the row. +var ErrJournalRowDisagrees = errors.New("journal row disagrees with its audit record") + +func journalRowDisagrees(row JournalRow, why string) error { + return fmt.Errorf("%w: event %s: %s", ErrJournalRowDisagrees, row.EventID, why) +} + +// journalDecodeAudit reads the audit entry out of a saas.audit.recorded.v1 row. The +// organization is the row's: the publish guard binds a row's tenant to its +// transaction, and a record that names another organization than its row is +// refused, not corrected. +func journalDecodeAudit(row JournalRow) (AuditEntry, error) { + if row.Type != AuditRecordedEventType { + return AuditEntry{}, journalRowDisagrees(row, "not an audit record") + } + entry, err := AuditEntryFromJournal(row.Data) + if err != nil { + return AuditEntry{}, journalRowDisagrees(row, err.Error()) + } + if row.EventID != AuditJournalEventID(entry.ID) { + return AuditEntry{}, journalRowDisagrees(row, "the event id is not the entry's") + } + if !strings.EqualFold(entry.OrgID, row.TenantID) { + return AuditEntry{}, journalRowDisagrees(row, "the record names another organization than the row") + } + entry.OrgID = row.TenantID + return entry, nil +} diff --git a/module/services/accounts/code/pkg/business/audit_delivery_test.go b/module/services/accounts/code/pkg/business/audit_delivery_test.go new file mode 100644 index 000000000..e9094cbbf --- /dev/null +++ b/module/services/accounts/code/pkg/business/audit_delivery_test.go @@ -0,0 +1,413 @@ +package business_test + +import ( + "context" + "errors" + "fmt" + "sort" + "testing" + "time" + + "accounts/pkg/business" + + "github.com/codefly-dev/sdk-go/receipts" + "github.com/google/uuid" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// deliveryStorage is the warehouse of a test: it keeps every append it is given, as +// an at-least-once store does, and every read returns each entry once, by id. +type deliveryStorage struct { + appended []business.AuditEntry + failOn string // an entry id whose append fails +} + +func (d *deliveryStorage) Append(_ context.Context, entry business.AuditEntry) error { + if entry.ID == d.failOn { + return errors.New("warehouse unavailable") + } + d.appended = append(d.appended, entry) + return nil +} + +func (d *deliveryStorage) List(context.Context, business.AuditRead) ([]business.AuditEntry, string, error) { + seen := map[string]business.AuditEntry{} + for _, entry := range d.appended { + seen[entry.ID] = entry + } + out := make([]business.AuditEntry, 0, len(seen)) + for _, entry := range seen { + out = append(out, entry) + } + sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID }) + return out, "", nil +} + +func (d *deliveryStorage) Aggregate(context.Context, business.AuditRead, business.AuditAggregationSpec) ([]business.AuditAggregateBucket, error) { + return nil, nil +} + +// rangeJournalStore answers the cross-tenant journal from rows it holds, with the +// real store's rules: after the position, up to the end, of the types, a page at a +// time, and only below the frontier. +type rangeJournalStore struct { + fakeTxStore + rows []business.JournalRow + frontier uint64 // the oldest transaction still running +} + +func (r *rangeJournalStore) matching(q business.JournalPageQuery) []business.JournalRow { + var out []business.JournalRow + for _, row := range r.rows { + if !q.After.Less(row.Position) || row.Position.Xact >= r.frontier { + continue + } + if q.Through != nil && q.Through.Less(row.Position) { + continue + } + for _, t := range q.Types { + if t == row.Type { + out = append(out, row) + break + } + } + } + return out +} + +func (r *rangeJournalStore) ReadJournalPage(_ context.Context, q business.JournalPageQuery) ([]business.JournalRow, error) { + rows := r.matching(q) + if len(rows) > q.Limit { + rows = rows[:q.Limit] + } + return rows, nil +} + +func (r *rangeJournalStore) CountJournalRange(_ context.Context, q business.JournalPageQuery, limit int) (int, error) { + return min(len(r.matching(q)), limit), nil +} + +func (r *rangeJournalStore) JournalPositionVisible(_ context.Context, pos business.JournalPosition) (bool, error) { + return pos.Xact < r.frontier, nil +} + +const deliveryTenant = "55555555-5555-5555-5555-555555555555" + +// auditJournalRows are n audit records at transaction 10, seq 10, 20, ..., 10n; row i belongs to +// the organization orgs[i%len(orgs)]. +func auditJournalRows(t *testing.T, n int, orgs ...string) []business.JournalRow { + t.Helper() + rows := make([]business.JournalRow, 0, n) + for i := 1; i <= n; i++ { + org := orgs[i%len(orgs)] + envelope, err := business.NewAuditJournalEnvelope(business.AuditEntry{ + ID: uuid.NewString(), ActorID: moduleUserA, ActorType: business.ActorTypeUser, + EventType: business.EventMFATOTPSetupStarted, Resource: "mfa_device", ResourceID: fmt.Sprint(i), + OrgID: org, Payload: map[string]any{"n": i}, CreatedAt: time.Date(2026, 10, 13, 0, 0, i, 0, time.UTC), + }) + if err != nil { + t.Fatal(err) + } + rows = append(rows, business.JournalRow{ + Position: business.JournalPosition{Xact: 10, Seq: int64(i) * 10}, EventID: envelope.Id, Type: envelope.Type, + TenantID: org, ContentType: envelope.Datacontenttype, Data: envelope.Data, + }) + } + return rows +} + +func positionOf(row business.JournalRow) string { return row.Position.String() } + +func deliveryService(t *testing.T, store *rangeJournalStore, storage business.AuditStorage) (*business.Service, *journalRecorder) { + t.Helper() + svc := newModuleServiceWithStore(t, store, &fakeJobBackend{}, true) + transport := &journalRecorder{} + audit, err := business.NewAuditService(business.AuditServiceConfig{ + Backend: business.AuditBackendWarehouse, Store: store, Producer: idleProducer{}, + Transport: transport, Warehouse: storage, + }) + if err != nil { + t.Fatal(err) + } + svc.SetAuditService(audit) + return svc, transport +} + +func admitted(from string) context.Context { + return receipts.WithEffect(context.Background(), receipts.Effect{ + ID: business.AuditDeliveryEffectID(from), Tenant: deliveryTenant, + Method: business.DeliverAuditEventsMethod, RequestDigest: []byte("digest"), + }) +} + +type commitSpy struct { + calls int + result business.AuditDeliveryResult + err error +} + +func (c *commitSpy) commit(_ context.Context, r business.AuditDeliveryResult) error { + c.calls++ + c.result = r + return c.err +} + +func TestDeliverAuditEventsAppendsTheRangeWithTheRowsOrganization(t *testing.T) { + audit := auditJournalRows(t, 5, moduleTenantA, moduleTenantB, "") // organizations: B, "", A, B, "" + // A row of another type inside the range is not an audit record. + other := business.JournalRow{Position: business.JournalPosition{Xact: 10, Seq: 35}, EventID: uuid.NewString(), Type: "saas.test.other", Data: []byte(`{}`)} + all := append(append(append([]business.JournalRow{}, audit[:3]...), other), audit[3:]...) + store := &rangeJournalStore{rows: all, frontier: 11} + storage := &deliveryStorage{} + svc, transport := deliveryService(t, store, storage) + + // (record 1, record 4]: records 2, 3 and 4, and not the other row between 3 and 4. + from, to := positionOf(audit[0]), positionOf(audit[3]) + spy := &commitSpy{} + result, err := svc.DeliverAuditEvents(admitted(from), from, to, spy.commit) + if err != nil { + t.Fatal(err) + } + if result.Delivered != 3 || spy.calls != 1 || spy.result != result || len(storage.appended) != 3 { + t.Fatalf("delivered %+v, commits %d, appended %d", result, spy.calls, len(storage.appended)) + } + for i, want := range audit[1:4] { + if storage.appended[i].ID != entryIDOf(t, want) { + t.Fatalf("appended[%d] = %s, want record %d", i, storage.appended[i].ID, i+2) + } + // The organization is the row's, "" for the row that has none. + if storage.appended[i].OrgID != want.TenantID { + t.Fatalf("appended[%d] organization %q, want the row's %q", i, storage.appended[i].OrgID, want.TenantID) + } + } + if storage.appended[0].OrgID != "" || storage.appended[1].OrgID != moduleTenantA || storage.appended[2].OrgID != moduleTenantB { + t.Fatalf("organizations %q %q %q", storage.appended[0].OrgID, storage.appended[1].OrgID, storage.appended[2].OrgID) + } + if len(transport.published) != 0 { + t.Fatalf("delivering the audit trail wrote to it: %d events published", len(transport.published)) + } +} + +func entryIDOf(t *testing.T, row business.JournalRow) string { + t.Helper() + entry, err := business.AuditEntryFromJournal(row.Data) + if err != nil { + t.Fatal(err) + } + return entry.ID +} + +func TestDeliverAuditEventsFromTheStartAndAnEmptyRange(t *testing.T) { + rows := auditJournalRows(t, 3, moduleTenantA) + store := &rangeJournalStore{rows: rows, frontier: 11} + storage := &deliveryStorage{} + svc, _ := deliveryService(t, store, storage) + + spy := &commitSpy{} + result, err := svc.DeliverAuditEvents(admitted(business.JournalStart), business.JournalStart, positionOf(rows[2]), spy.commit) + if err != nil || result.Delivered != 3 || len(storage.appended) != 3 { + t.Fatalf("from the start: %+v, %v, appended %d", result, err, len(storage.appended)) + } + if business.AuditDeliveryEffectID(business.JournalStart) != "saas.audit.recorded.v1/start" { + t.Fatalf("effect id = %q", business.AuditDeliveryEffectID(business.JournalStart)) + } + + // A range with no audit record still commits its receipt. + storage.appended, spy.calls = nil, 0 + from := positionOf(rows[2]) + later := business.JournalPosition{Xact: 10, Seq: 999} + result, err = svc.DeliverAuditEvents(admitted(from), from, later.String(), spy.commit) + if err != nil || result.Delivered != 0 || spy.calls != 1 || len(storage.appended) != 0 { + t.Fatalf("empty range: %+v, %v, commits %d", result, err, spy.calls) + } +} + +func TestDeliverAuditEventsRefusesWhatItIsNotAsked(t *testing.T) { + rows := auditJournalRows(t, 3, moduleTenantA) + store := &rangeJournalStore{rows: rows, frontier: 11} + storage := &deliveryStorage{} + svc, _ := deliveryService(t, store, storage) + from, to := positionOf(rows[0]), positionOf(rows[2]) + spy := &commitSpy{} + + for name, tc := range map[string]struct { + ctx context.Context + from, to string + code codes.Code + }{ + "no admitted effect": {context.Background(), from, to, codes.FailedPrecondition}, + "another range's effect": {admitted(positionOf(rows[1])), from, to, codes.FailedPrecondition}, + "another method's effect": {receipts.WithEffect(context.Background(), receipts.Effect{ + ID: business.AuditDeliveryEffectID(from), Tenant: deliveryTenant, Method: business.ReconcileSourceMethod, RequestDigest: []byte("d")}), from, to, codes.FailedPrecondition}, + "an effect with no tenant": {receipts.WithEffect(context.Background(), receipts.Effect{ + ID: business.AuditDeliveryEffectID(from), Method: business.DeliverAuditEventsMethod, RequestDigest: []byte("d")}), from, to, codes.FailedPrecondition}, + "an empty from": {admitted(""), "", to, codes.InvalidArgument}, + "a malformed from": {admitted("v1.1"), "v1.1", to, codes.InvalidArgument}, + "a malformed to": {admitted(from), from, "v1.x.2", codes.InvalidArgument}, + "an empty to": {admitted(from), from, "", codes.InvalidArgument}, + "start as to": {admitted(from), from, business.JournalStart, codes.InvalidArgument}, + "to equal to from": {admitted(from), from, from, codes.InvalidArgument}, + "to before from": {admitted(to), to, from, codes.InvalidArgument}, + "an end not yet visible": {admitted(from), from, business.JournalPosition{Xact: 11, Seq: 1}.String(), codes.FailedPrecondition}, + } { + if _, err := svc.DeliverAuditEvents(tc.ctx, tc.from, tc.to, spy.commit); status.Code(err) != tc.code { + t.Errorf("%s: %v, want %s", name, err, tc.code) + } + } + if _, err := svc.DeliverAuditEvents(admitted(from), from, to, nil); status.Code(err) != codes.FailedPrecondition { + t.Errorf("no commit: %v", err) + } + if len(storage.appended) != 0 || spy.calls != 0 { + t.Fatalf("a refused delivery appended %d and committed %d", len(storage.appended), spy.calls) + } +} + +func TestDeliverAuditEventsRefusesAnEndThatIsNotVisibleWithTheReason(t *testing.T) { + rows := auditJournalRows(t, 2, moduleTenantA) + store := &rangeJournalStore{rows: rows, frontier: 10} // transaction 10 may still be running + svc, _ := deliveryService(t, store, &deliveryStorage{}) + _, err := svc.DeliverAuditEvents(admitted(business.JournalStart), business.JournalStart, positionOf(rows[1]), (&commitSpy{}).commit) + requireCode(t, err, codes.FailedPrecondition) + requireReason(t, err, business.AuditDeliveryRangeNotVisibleReason) +} + +func requireReason(t *testing.T, err error, reason string) { + t.Helper() + for _, d := range status.Convert(err).Details() { + if info, ok := d.(interface{ GetReason() string }); ok && info.GetReason() == reason { + return + } + } + t.Fatalf("error %v does not carry reason %s", err, reason) +} + +func TestDeliverAuditEventsCapsTheRange(t *testing.T) { + rows := auditJournalRows(t, business.AuditDeliveryMaxRange+1, moduleTenantA) + store := &rangeJournalStore{rows: rows, frontier: 11} + storage := &deliveryStorage{} + svc, _ := deliveryService(t, store, storage) + spy := &commitSpy{} + + // 501 records: refused before anything is appended. + _, err := svc.DeliverAuditEvents(admitted(business.JournalStart), business.JournalStart, positionOf(rows[len(rows)-1]), spy.commit) + requireCode(t, err, codes.InvalidArgument) + requireReason(t, err, business.AuditDeliveryRangeTooWideReason) + if len(storage.appended) != 0 || spy.calls != 0 { + t.Fatalf("a range too wide appended %d and committed %d", len(storage.appended), spy.calls) + } + + // Exactly 500, read in several pages, are all delivered once. + result, err := svc.DeliverAuditEvents(admitted(business.JournalStart), business.JournalStart, positionOf(rows[len(rows)-2]), spy.commit) + if err != nil || result.Delivered != business.AuditDeliveryMaxRange || len(storage.appended) != business.AuditDeliveryMaxRange { + t.Fatalf("a full range: %+v, %v, appended %d", result, err, len(storage.appended)) + } +} + +func TestDeliverAuditEventsTakesTheOrganizationFromTheRowNeverTheRecord(t *testing.T) { + rows := auditJournalRows(t, 3, moduleTenantA) + // The record in row 2 names organization B; the row says A. + entry, err := business.AuditEntryFromJournal(rows[1].Data) + if err != nil { + t.Fatal(err) + } + entry.OrgID = moduleTenantB + forged, err := business.NewAuditJournalEnvelope(entry) + if err != nil { + t.Fatal(err) + } + rows[1].Data = forged.Data + store := &rangeJournalStore{rows: rows, frontier: 11} + storage := &deliveryStorage{} + svc, _ := deliveryService(t, store, storage) + + spy := &commitSpy{} + _, err = svc.DeliverAuditEvents(admitted(business.JournalStart), business.JournalStart, positionOf(rows[2]), spy.commit) + requireCode(t, err, codes.FailedPrecondition) + requireReason(t, err, business.AuditDeliveryRecordRefusedReason) + if spy.calls != 0 { + t.Fatal("a refused record was committed") + } + for _, appended := range storage.appended { + if appended.OrgID == moduleTenantB { + t.Fatal("the record's organization reached the warehouse") + } + } +} + +func TestDeliverAuditEventsFailureLeavesNoReceiptAndAReplayAppendsAtLeastOnce(t *testing.T) { + rows := auditJournalRows(t, 4, moduleTenantA) + store := &rangeJournalStore{rows: rows, frontier: 11} + storage := &deliveryStorage{failOn: entryIDOf(t, rows[2])} + svc, _ := deliveryService(t, store, storage) + to := positionOf(rows[3]) + + spy := &commitSpy{} + _, err := svc.DeliverAuditEvents(admitted(business.JournalStart), business.JournalStart, to, spy.commit) + requireCode(t, err, codes.Unavailable) + if spy.calls != 0 || len(storage.appended) != 2 { + t.Fatalf("commits %d, appended %d: the records before the failure are appended and no receipt is written", spy.calls, len(storage.appended)) + } + + // The same range again, once the warehouse is back: the records before the + // failure are appended a second time, and every read returns each once. + storage.failOn = "" + result, err := svc.DeliverAuditEvents(admitted(business.JournalStart), business.JournalStart, to, spy.commit) + if err != nil || result.Delivered != 4 || spy.calls != 1 || len(storage.appended) != 6 { + t.Fatalf("retry: %+v, %v, commits %d, appended %d", result, err, spy.calls, len(storage.appended)) + } + listed, _, _ := storage.List(context.Background(), business.AuditRead{}) + if len(listed) != 4 { + t.Fatalf("a read returns %d entries, want each of the 4 once", len(listed)) + } + + // A commit that fails is not a delivery. + spy.err = errors.New("receipt store down") + _, err = svc.DeliverAuditEvents(admitted(business.JournalStart), business.JournalStart, to, spy.commit) + requireCode(t, err, codes.Unavailable) +} + +func TestOverlappingRangesAppendAtLeastOnceAndReadsDeduplicate(t *testing.T) { + rows := auditJournalRows(t, 5, moduleTenantA) + store := &rangeJournalStore{rows: rows, frontier: 11} + storage := &deliveryStorage{} + svc, _ := deliveryService(t, store, storage) + spy := &commitSpy{} + + // (start, 3] and (2, 5] overlap on record 3. + if _, err := svc.DeliverAuditEvents(admitted(business.JournalStart), business.JournalStart, positionOf(rows[2]), spy.commit); err != nil { + t.Fatal(err) + } + from := positionOf(rows[1]) + if _, err := svc.DeliverAuditEvents(admitted(from), from, positionOf(rows[4]), spy.commit); err != nil { + t.Fatal(err) + } + if len(storage.appended) != 6 { + t.Fatalf("appended %d, want 3 + 3 with record 3 twice", len(storage.appended)) + } + listed, _, _ := storage.List(context.Background(), business.AuditRead{}) + if len(listed) != 5 { + t.Fatalf("a read returns %d entries, want the 5 once each", len(listed)) + } +} + +// A host whose audit is Postgres has no audit record in the journal, and +// appending to its storage from here would write audit_events outside the writer's +// transaction: refused. +func TestDeliverAuditEventsRefusesAHostThatKeepsAuditInPostgres(t *testing.T) { + rows := auditJournalRows(t, 2, moduleTenantA) + store := &rangeJournalStore{rows: rows, frontier: 11} + svc := newModuleServiceWithStore(t, store, &fakeJobBackend{}, true) // no AuditService wired + _, err := svc.DeliverAuditEvents(admitted(business.JournalStart), business.JournalStart, positionOf(rows[1]), (&commitSpy{}).commit) + requireCode(t, err, codes.FailedPrecondition) + requireReason(t, err, business.AuditDeliveryNotWarehouseReason) + + postgres, err := business.NewAuditService(business.AuditServiceConfig{Backend: business.AuditBackendPostgres, Store: store, Producer: idleProducer{}}) + if err != nil { + t.Fatal(err) + } + svc.SetAuditService(postgres) + _, err = svc.DeliverAuditEvents(admitted(business.JournalStart), business.JournalStart, positionOf(rows[1]), (&commitSpy{}).commit) + requireCode(t, err, codes.FailedPrecondition) + requireReason(t, err, business.AuditDeliveryNotWarehouseReason) +} diff --git a/module/services/accounts/code/pkg/business/audit_durability_gate_test.go b/module/services/accounts/code/pkg/business/audit_durability_gate_test.go index 8b8a1e80d..d2cd1224c 100644 --- a/module/services/accounts/code/pkg/business/audit_durability_gate_test.go +++ b/module/services/accounts/code/pkg/business/audit_durability_gate_test.go @@ -298,7 +298,9 @@ func auditEventConstants(files []*ast.File) map[string]EventType { // durability rules would otherwise have: a caller can write an audit row without // any emit site by reaching for the store directly, and every classification the // gate above enforces would be silently irrelevant to it. InsertAuditEvent is the -// emitter's own call; nothing else in this package may make it. +// emitter's own call, made by it directly and through PostgresAuditStorage.Append +// (audit_storage.go), the one storage the emitter appends to under Postgres; +// nothing else in this package may make it. func TestAuditDurability_NoBypassOfTheClassifiedEmitPath(t *testing.T) { fset := token.NewFileSet() entries, err := os.ReadDir(".") @@ -321,7 +323,7 @@ func TestAuditDurability_NoBypassOfTheClassifiedEmitPath(t *testing.T) { if !ok || sel.Sel.Name != "InsertAuditEvent" { return true } - if name == "audit.go" { + if name == "audit.go" || name == "audit_storage.go" { return true } callers = append(callers, fmt.Sprintf("%s:%d", name, fset.Position(call.Pos()).Line)) diff --git a/module/services/accounts/code/pkg/business/audit_events_contribution.go b/module/services/accounts/code/pkg/business/audit_events_contribution.go index 646690b06..376fbb97d 100644 --- a/module/services/accounts/code/pkg/business/audit_events_contribution.go +++ b/module/services/accounts/code/pkg/business/audit_events_contribution.go @@ -44,6 +44,11 @@ const auditEventsSchemaRef = "saas/events/v1/events.proto#EventEnvelope" // A platform-scope record (one emitted with no organization) is registered but // never published: the emitter publishes only for a tenant, so those types exist // in the catalog without ever reaching a subscriber. +// +// One type is not an audit type: AuditRecordedEventType, the journal event of +// the warehouse backend. It is code-owned and `internal`, so the relay marks it +// published without fanning it out, a replay skips it, and no subscription may +// match it. func RenderAuditEventsContribution() []byte { var out bytes.Buffer out.WriteString(`# Generated from the audit registry by RenderAuditEventsContribution. @@ -65,6 +70,11 @@ func RenderAuditEventsContribution() []byte { # serialize every audited mutation in an organization. Nothing consumes the # ordering: an outbound webhook is dispatched in subscription-id order, and the # platform namespace is not subscribable by a module. +# +# The last entry, saas.audit.recorded.v1, is the one type here that is not an +# audit event: the journal record of the warehouse audit backend, carrying the +# whole entry. It is internal, so it is never fanned out, relayed or subscribable; +# only the platform's own reader and the delivery task read it. `) fmt.Fprintf(&out, "schema: %s\n", "codefly/saas/events-contribution/v1") fmt.Fprintf(&out, "namespace: %s\n", auditEventsNamespace) @@ -76,5 +86,9 @@ func RenderAuditEventsContribution() []byte { out.WriteString(" visibility: external\n") out.WriteString(" retention: 30d\n") } + fmt.Fprintf(&out, " - type: %s\n", AuditRecordedEventType) + fmt.Fprintf(&out, " schema: %s\n", auditEventsSchemaRef) + out.WriteString(" visibility: internal\n") + out.WriteString(" retention: 30d\n") return out.Bytes() } diff --git a/module/services/accounts/code/pkg/business/audit_export.go b/module/services/accounts/code/pkg/business/audit_export.go index c3af8cd1a..d771bdc83 100644 --- a/module/services/accounts/code/pkg/business/audit_export.go +++ b/module/services/accounts/code/pkg/business/audit_export.go @@ -29,10 +29,12 @@ func (s *Service) ExportAuditLog(ctx context.Context, orgID, format, actorID, ev var all []AuditEntry pageToken := "" for { - // Service.QueryAuditLog wraps in WithOrgTx (or WithControlPlane when - // orgID is empty for platform-admin export) so RLS lets the - // rows through. Don't use s.store.QueryAuditLog directly here - // — that bypasses the wrap and returns zero rows. + // Service.QueryAuditLog reads through the AuditService, which scopes the + // read to the organization (WithOrgTx, so RLS lets the rows through) or, + // when orgID is empty for platform-admin export, to the platform. Don't + // use s.store.QueryAuditLog directly here — that bypasses the scope and + // returns zero rows — and don't read a storage directly: the export must + // come from the same storage the audit was written to. entries, nextToken, _, err := s.QueryAuditLog(ctx, AuditQuery{ OrgID: orgID, ActorID: actorID, EventType: eventType, EventTypes: eventTypes, PageSize: 100, PageToken: pageToken, diff --git a/module/services/accounts/code/pkg/business/audit_journal.go b/module/services/accounts/code/pkg/business/audit_journal.go new file mode 100644 index 000000000..2e2677ffd --- /dev/null +++ b/module/services/accounts/code/pkg/business/audit_journal.go @@ -0,0 +1,166 @@ +package business + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "time" + + "accounts/pkg/events" + + "github.com/google/uuid" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// AuditRecordedEventType is the journal type of the warehouse backend: one +// event per audit entry, carrying the whole entry, published in the caller's +// transaction beside the mutation it records. It is code-owned and `internal`: +// it is read only by the platform's own reader (ReadJournal, for a cross-tenant +// module principal) and by DeliverAuditEvent, never fanned out to a subscriber, +// never relayed to a webhook, and never subscribable (the platform namespace is +// not subscribable by a module, and no pattern may match an internal type). +// +// It is not an audit event type: it has no audit_event_types row, and +// publishing it writes no audit entry. The audit entry's own webhook event, for +// an organization-scoped type declared external, is published separately and is +// unchanged. +const AuditRecordedEventType = "saas.audit.recorded.v1" + +// maxAuditJournalData is the largest `data` a domain event carries (EVENTS.md: +// 960 KiB, below the 1 MiB job_messages.payload cap). domain_events.data has no +// CHECK of its own, so the producer enforces it. +const maxAuditJournalData = 960 * 1024 + +// ErrAuditEntryTooLarge is the refusal of an entry whose journal record would +// exceed the data limit. The journal is the audit entry's only copy until a +// delivery appends it, so the entry is not truncated and not dropped: the emit +// fails, and the caller's transaction with it, exactly as a failed append does. +var ErrAuditEntryTooLarge = errors.New("audit: the entry exceeds the journal JSON or encoded page limit") + +// auditJournalNamespace derives the journal event id from the audit entry id. +// The two cannot be one id: an organization-scoped entry of an external type is +// also published as a webhook event under the entry's own id (the id an endpoint +// deduplicates on), and an event id is unique in the journal. +var auditJournalNamespace = uuid.NewSHA1(uuid.NameSpaceURL, []byte("urn:saas:"+AuditRecordedEventType)) + +// AuditJournalEventID is the id of the journal event that carries the audit +// entry id. It is deterministic, so a retried publish of one entry is one event. +func AuditJournalEventID(entryID string) string { + return uuid.NewSHA1(auditJournalNamespace, []byte(entryID)).String() +} + +// auditJournalRecord is the `data` of the journal event: the whole entry, in the +// JSON the delivery reads back. Its field names are the contract between the +// publisher and DeliverAuditEvent. +type auditJournalRecord struct { + ID string `json:"id"` + EventType string `json:"event_type"` + SchemaVersion int `json:"schema_version"` + ActorID string `json:"actor_id,omitempty"` + ActorType string `json:"actor_type"` + Resource string `json:"resource"` + ResourceID string `json:"resource_id,omitempty"` + OrganizationID string `json:"organization_id,omitempty"` + Payload map[string]any `json:"payload"` + IPAddress string `json:"ip_address,omitempty"` + ImpersonatedBy string `json:"impersonated_by,omitempty"` + IsImpersonated bool `json:"is_impersonated"` + ClientID string `json:"client_id,omitempty"` + OccurredAt time.Time `json:"occurred_at"` +} + +// NewAuditJournalEnvelope is the journal event of one audit entry. The entry +// must already carry its id, its occurrence and its schema version, which the +// emitter backfills before it gets here. +// +// There is no partition key. A partition is a promise of FIFO within it, bought +// with an advisory lock held until the producer's transaction commits, and +// nothing reads the journal in partition order. The tenant is the entry's +// organization, empty for an entry with no organization. +// +// The envelope's actor is the entry's actor id: for a person it is their user id, +// which is also their principal id. publish_domain_event reads it to admit an +// entry with no organization from a user-scoped transaction (a caller running as +// app_tenant with only app.current_user_id set): such an event is admitted only +// when its actor is that user, the rule audit_events_tenant applies to the same +// entry's row (migration 41). +func NewAuditJournalEnvelope(entry AuditEntry) (*events.EventEnvelope, error) { + if entry.ID == "" { + return nil, errors.New("audit: a journal event needs the entry id") + } + version := entry.SchemaVersion + if version == 0 { + version = 1 // what audit_events records for an entry with no registered version + } + payload := entry.Payload + if payload == nil { + payload = map[string]any{} + } + data, err := json.Marshal(auditJournalRecord{ + ID: entry.ID, EventType: string(entry.EventType), SchemaVersion: version, + ActorID: entry.ActorID, ActorType: entry.ActorType, + Resource: entry.Resource, ResourceID: entry.ResourceID, OrganizationID: entry.OrgID, + Payload: payload, IPAddress: entry.IPAddress, + ImpersonatedBy: entry.ImpersonatedBy, IsImpersonated: entry.IsImpersonated, + ClientID: entry.ClientID, OccurredAt: entry.CreatedAt.UTC(), + }) + if err != nil { + return nil, fmt.Errorf("audit: journal record of %s: %w", entry.ID, err) + } + if len(data) > maxAuditJournalData { + return nil, fmt.Errorf("%w: entry %s is %d bytes", ErrAuditEntryTooLarge, entry.ID, len(data)) + } + // JSON arrays of small numbers grow substantially as protobuf Struct values. + // Refuse an entry before commit if ReadJournal could never return it alone. + var object map[string]any + if err := json.Unmarshal(data, &object); err != nil { + return nil, fmt.Errorf("audit: journal object: %w", err) + } + projected, err := structpb.NewStruct(object) + if err != nil { + return nil, fmt.Errorf("audit: journal object: %w", err) + } + if size := proto.Size(projected); size > ModuleJournalMaxWireBytes-1024 { + return nil, fmt.Errorf("%w: entry %s requires %d protobuf bytes", ErrAuditEntryTooLarge, entry.ID, size) + } + return &events.EventEnvelope{ + Id: AuditJournalEventID(entry.ID), + Type: AuditRecordedEventType, + Source: domainEventSource, + Subject: entry.ID, + Specversion: "1.0", + Datacontenttype: "application/json", + Time: timestamppb.New(entry.CreatedAt.UTC()), + Data: data, + TenantId: entry.OrgID, + // The same value audit_events.actor_id records for the entry. + ActorPrincipalId: entry.ActorID, + SchemaVersion: 1, + }, nil +} + +// AuditEntryFromJournal reads the entry back out of a journal event's `data`. +// Numbers keep the text they were written with (json.Number), so an amount in a +// payload reaches the storage exactly as it was recorded. +func AuditEntryFromJournal(data []byte) (AuditEntry, error) { + var record auditJournalRecord + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.UseNumber() + if err := decoder.Decode(&record); err != nil { + return AuditEntry{}, fmt.Errorf("audit: journal record: %w", err) + } + if record.ID == "" || record.EventType == "" { + return AuditEntry{}, errors.New("audit: journal record names no entry") + } + return AuditEntry{ + ID: record.ID, EventType: EventType(record.EventType), SchemaVersion: record.SchemaVersion, + ActorID: record.ActorID, ActorType: record.ActorType, + Resource: record.Resource, ResourceID: record.ResourceID, OrgID: record.OrganizationID, + Payload: record.Payload, IPAddress: record.IPAddress, + ImpersonatedBy: record.ImpersonatedBy, IsImpersonated: record.IsImpersonated, + ClientID: record.ClientID, CreatedAt: record.OccurredAt, + }, nil +} diff --git a/module/services/accounts/code/pkg/business/audit_journal_module_test.go b/module/services/accounts/code/pkg/business/audit_journal_module_test.go new file mode 100644 index 000000000..fdf6dac72 --- /dev/null +++ b/module/services/accounts/code/pkg/business/audit_journal_module_test.go @@ -0,0 +1,85 @@ +package business_test + +import ( + "context" + "testing" + + "accounts/pkg/business" + "accounts/pkg/events" + eventsv1 "accounts/pkg/gen/saas/events/v1" + jobsv1 "accounts/pkg/gen/saas/jobs/v1" +) + +type journalRecorder struct { + events.Transport + published []*eventsv1.EventEnvelope +} + +func (j *journalRecorder) Publish(_ context.Context, _ events.TxHandle, e *eventsv1.EventEnvelope) error { + j.published = append(j.published, e) + return nil +} + +type idleProducer struct{} + +func (idleProducer) EnqueueJob(context.Context, *jobsv1.EnqueueJobRequest) (*jobsv1.EnqueueJobResponse, error) { + return &jobsv1.EnqueueJobResponse{}, nil +} + +type noWarehouse struct{} + +func (noWarehouse) Append(context.Context, business.AuditEntry) error { return nil } +func (noWarehouse) List(context.Context, business.AuditRead) ([]business.AuditEntry, string, error) { + return nil, "", nil +} +func (noWarehouse) Aggregate(context.Context, business.AuditRead, business.AuditAggregationSpec) ([]business.AuditAggregateBucket, error) { + return nil, nil +} + +// Audit a module emits through ModuleEmitAuditEvent takes the same path as the +// host's own: under the warehouse backend it is journaled on the caller's +// transaction, whether it is tenant-scoped or system-scoped. +func TestModuleEmitAuditEventIsJournaledUnderTheWarehouseBackend(t *testing.T) { + store := fakeTxStore{} + svc := newModuleServiceWithStore(t, store, &fakeJobBackend{}, true) + transport := &journalRecorder{} + audit, err := business.NewAuditService(business.AuditServiceConfig{ + Backend: business.AuditBackendWarehouse, Store: store, Producer: idleProducer{}, + Transport: transport, Warehouse: noWarehouse{}, + }) + if err != nil { + t.Fatalf("NewAuditService: %v", err) + } + svc.SetAuditService(audit) + if err := svc.VerifyAuditWiring(); err != nil { + t.Fatalf("the warehouse backend writes on the caller's transaction too: %v", err) + } + + for name, tenant := range map[string]string{"tenant-scoped": moduleTenantA, "system-scoped": ""} { + transport.published = nil + if err := svc.ModuleEmitAuditEvent(context.Background(), moduleCaller(), + tenant, "saas.document.ingested", modulePrincSvc, "example-solution", "entry-1", "", nil); err != nil { + t.Fatalf("%s emit: %v", name, err) + } + var journal *eventsv1.EventEnvelope + for _, e := range transport.published { + if e.GetType() == business.AuditRecordedEventType { + journal = e + } + } + if journal == nil { + t.Fatalf("%s: no journal event among %d published", name, len(transport.published)) + } + if journal.GetTenantId() != tenant { + t.Fatalf("%s: journal tenant = %q, want %q", name, journal.GetTenantId(), tenant) + } + entry, err := business.AuditEntryFromJournal(journal.GetData()) + if err != nil { + t.Fatal(err) + } + if entry.EventType != "saas.document.ingested" || entry.OrgID != tenant || entry.ResourceID != "entry-1" || + entry.ActorID != modulePrincSvc || entry.ActorType != business.ActorTypeSystem { + t.Fatalf("%s: journaled entry = %+v", name, entry) + } + } +} diff --git a/module/services/accounts/code/pkg/business/audit_journal_test.go b/module/services/accounts/code/pkg/business/audit_journal_test.go new file mode 100644 index 000000000..706261968 --- /dev/null +++ b/module/services/accounts/code/pkg/business/audit_journal_test.go @@ -0,0 +1,378 @@ +package business + +import ( + "context" + "encoding/json" + "errors" + "strings" + "testing" + "time" + + "accounts/pkg/eventcatalog" + "accounts/pkg/events" + eventsv1 "accounts/pkg/gen/saas/events/v1" + + "github.com/google/uuid" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// journalTransport records every envelope published, with the transaction +// handle it was published on, in order. +type journalTransport struct { + events.Transport + published []*eventsv1.EventEnvelope +} + +func (j *journalTransport) Publish(_ context.Context, _ events.TxHandle, e *eventsv1.EventEnvelope) error { + j.published = append(j.published, e) + return nil +} + +func (j *journalTransport) types() []string { + var out []string + for _, e := range j.published { + out = append(out, e.GetType()) + } + return out +} + +func newWarehouseAuditService(t *testing.T) (*AuditService, *auditServiceStore, *journalTransport, *recordingStorage) { + t.Helper() + store := &auditServiceStore{} + transport := &journalTransport{} + warehouse := &recordingStorage{} + service, err := NewAuditService(AuditServiceConfig{ + Backend: AuditBackendWarehouse, Store: store, Producer: silentProducer{}, Transport: transport, Warehouse: warehouse, + }) + if err != nil { + t.Fatalf("NewAuditService: %v", err) + } + return service, store, transport, warehouse +} + +// The journal event carries the whole entry, and the entry comes back out of it +// exactly: the delivery reads nothing else. +func TestAuditJournalEnvelopeCarriesTheWholeEntry(t *testing.T) { + at := time.Date(2026, 10, 12, 8, 30, 0, 123456000, time.UTC) + entry := AuditEntry{ + ID: uuid.NewString(), EventType: EventDocumentRead, SchemaVersion: 2, + ActorID: uuid.NewString(), ActorType: ActorTypeUser, Resource: "datasource", ResourceID: "doc-1", + OrgID: auditServiceOrg, IPAddress: "203.0.113.7", ClientID: "client-1", + IsImpersonated: true, ImpersonatedBy: uuid.NewString(), CreatedAt: at, + Payload: map[string]any{"amount": json.Number("12345678901234567890.50"), "nested": map[string]any{"k": []any{"x", json.Number("1")}}, "label": "&é"}, + } + envelope, err := NewAuditJournalEnvelope(entry) + if err != nil { + t.Fatal(err) + } + if envelope.GetType() != AuditRecordedEventType || envelope.GetType() != "saas.audit.recorded.v1" { + t.Fatalf("type = %q", envelope.GetType()) + } + if envelope.GetTenantId() != entry.OrgID || envelope.GetSubject() != entry.ID || envelope.GetPartitionKey() != "" { + t.Fatalf("tenant %q subject %q partition %q", envelope.GetTenantId(), envelope.GetSubject(), envelope.GetPartitionKey()) + } + if !envelope.GetTime().AsTime().Equal(at) { + t.Fatalf("time = %v", envelope.GetTime().AsTime()) + } + // publish_domain_event admits a no-organization entry from a user-scoped + // transaction only when this is that user (migration 41). + if envelope.GetActorPrincipalId() != entry.ActorID { + t.Fatalf("actor principal = %q, want the entry's actor %q", envelope.GetActorPrincipalId(), entry.ActorID) + } + if envelope.GetId() == entry.ID || envelope.GetId() != AuditJournalEventID(entry.ID) { + t.Fatalf("journal id %q must be derived from, and differ from, the entry id %q", envelope.GetId(), entry.ID) + } + if _, err := uuid.Parse(envelope.GetId()); err != nil { + t.Fatalf("the journal id is a uuid: %v", err) + } + if AuditJournalEventID(entry.ID) != AuditJournalEventID(entry.ID) || AuditJournalEventID(entry.ID) == AuditJournalEventID(uuid.NewString()) { + t.Fatal("the journal id is deterministic per entry") + } + + back, err := AuditEntryFromJournal(envelope.GetData()) + if err != nil { + t.Fatal(err) + } + if back.Payload["amount"] != json.Number("12345678901234567890.50") { + t.Fatalf("a number lost its text: %#v", back.Payload["amount"]) + } + back.Payload, entry.Payload = nil, nil + if back.ID != entry.ID || back.EventType != entry.EventType || back.SchemaVersion != 2 || + back.ActorID != entry.ActorID || back.ActorType != entry.ActorType || back.Resource != entry.Resource || + back.ResourceID != entry.ResourceID || back.OrgID != entry.OrgID || back.IPAddress != entry.IPAddress || + back.ClientID != entry.ClientID || back.IsImpersonated != true || back.ImpersonatedBy != entry.ImpersonatedBy || + !back.CreatedAt.Equal(at) { + t.Fatalf("entry did not round-trip:\n got %+v\nwant %+v", back, entry) + } +} + +func TestAuditJournalEnvelopeOfAnEntryWithNoOrganizationHasNoTenant(t *testing.T) { + envelope, err := NewAuditJournalEnvelope(AuditEntry{ID: uuid.NewString(), EventType: EventAuthLogin, CreatedAt: time.Now()}) + if err != nil { + t.Fatal(err) + } + if envelope.GetTenantId() != "" { + t.Fatalf("tenant = %q", envelope.GetTenantId()) + } + var record map[string]any + if err := json.Unmarshal(envelope.GetData(), &record); err != nil { + t.Fatal(err) + } + if _, present := record["organization_id"]; present { + t.Fatal("an entry with no organization names none") + } + if record["schema_version"] != float64(1) { + t.Fatalf("an entry with no registered version is version 1, as audit_events records it: %v", record["schema_version"]) + } + back, err := AuditEntryFromJournal(envelope.GetData()) + if err != nil || back.OrgID != "" { + t.Fatalf("back = %+v %v", back, err) + } +} + +func TestAuditJournalEnvelopeRespectsTheDataLimit(t *testing.T) { + entry := func(size int) AuditEntry { + return AuditEntry{ID: uuid.NewString(), EventType: EventDocumentRead, CreatedAt: time.Now(), Payload: map[string]any{"blob": strings.Repeat("a", size)}} + } + if _, err := NewAuditJournalEnvelope(entry(maxAuditJournalData - 1024)); err != nil { + t.Fatalf("an entry under the limit is published: %v", err) + } + _, err := NewAuditJournalEnvelope(entry(maxAuditJournalData)) + if !errors.Is(err, ErrAuditEntryTooLarge) { + t.Fatalf("an entry over the limit = %v, want ErrAuditEntryTooLarge", err) + } + if _, err := NewAuditJournalEnvelope(AuditEntry{}); err == nil { + t.Fatal("an entry with no id has no journal event") + } + if _, err := AuditEntryFromJournal([]byte(`{}`)); err == nil { + t.Fatal("a journal record that names no entry is refused") + } +} + +// The warehouse backend replaces the audit_events write with the journal event, +// in the caller's transaction, and keeps the webhook publication for an +// organization-scoped external type exactly as it is. +func TestWarehouseBackendPublishesTheJournalInsteadOfAppending(t *testing.T) { + service, store, transport, warehouse := newWarehouseAuditService(t) + if err := service.EmitTx(t.Context(), AuditEntry{EventType: EventUserCreated, OrgID: auditServiceOrg, ActorType: ActorTypeSystem, Resource: "user"}); err != nil { + t.Fatal(err) + } + if got := transport.types(); len(got) != 2 || got[0] != AuditRecordedEventType || got[1] != string(EventUserCreated) { + t.Fatalf("published %v, want the journal event then the webhook event", got) + } + if len(store.inserted) != 0 || len(warehouse.appended) != 0 { + t.Fatalf("an entry was appended: audit_events %d, warehouse %d; the write path publishes only", len(store.inserted), len(warehouse.appended)) + } + journal, webhook := transport.published[0], transport.published[1] + if journal.GetTenantId() != auditServiceOrg || webhook.GetTenantId() != auditServiceOrg { + t.Fatalf("tenants %q %q", journal.GetTenantId(), webhook.GetTenantId()) + } + if webhook.GetId() != journal.GetSubject() { + t.Fatalf("the webhook event keeps the entry id (%q); the journal event names that entry (%q)", webhook.GetId(), journal.GetSubject()) + } + if journal.GetId() == webhook.GetId() { + t.Fatal("the journal event and the webhook event cannot share an id: an id is unique in the journal") + } + if len(store.scopes) != 0 { + t.Fatalf("EmitTx opened a transaction: %v", store.scopes) + } +} + +// An entry with no organization is journaled too — it has no webhook event, and +// under the warehouse the journal is the only record it gets. +func TestWarehouseBackendJournalsAnEntryWithNoOrganization(t *testing.T) { + service, store, transport, _ := newWarehouseAuditService(t) + service.Emit(t.Context(), AuditEntry{EventType: EventAuthLogin, ActorType: ActorTypeSystem, Resource: "session"}) + if got := transport.types(); len(got) != 1 || got[0] != AuditRecordedEventType { + t.Fatalf("published %v, want the journal event alone", got) + } + if transport.published[0].GetTenantId() != "" { + t.Fatalf("tenant = %q", transport.published[0].GetTenantId()) + } + if len(store.scopes) != 1 || store.scopes[0] != "control-plane" { + t.Fatalf("a no-organization entry is published on the control plane's transaction, got %v", store.scopes) + } +} + +// Every audit write goes the same way, so a type that is not external, and one +// no catalog knows, are journaled as well: the journal is the record, the +// webhook event is only a notification. +func TestWarehouseBackendJournalsEveryType(t *testing.T) { + service, _, transport, _ := newWarehouseAuditService(t) + for _, eventType := range []EventType{EventUserCreated, EventAuthLogin, "example.item.closed"} { + if err := service.EmitTx(t.Context(), AuditEntry{EventType: eventType, OrgID: auditServiceOrg}); err != nil { + t.Fatalf("%s: %v", eventType, err) + } + } + journaled := 0 + for _, e := range transport.published { + if e.GetType() == AuditRecordedEventType { + journaled++ + } + } + if journaled != 3 { + t.Fatalf("journal events = %d, want one per entry: %v", journaled, transport.types()) + } +} + +// A retried emit collapses to one journal event: the idempotency guard runs +// before the journal publication, as it runs before the append. +func TestWarehouseBackendIdempotencyGuardsTheJournal(t *testing.T) { + service, _, transport, _ := newWarehouseAuditService(t) + entry := AuditEntry{EventType: EventUserCreated, OrgID: auditServiceOrg, IdempotencyKey: "once"} + for range 2 { + if err := service.EmitTx(t.Context(), entry); err != nil { + t.Fatal(err) + } + } + if got := transport.types(); len(got) != 2 { + t.Fatalf("published %v, want one journal event and one webhook event for two emits", got) + } +} + +// An entry that cannot be journaled fails the emit, and the webhook event is not +// published either: the caller's transaction rolls back with both. +func TestWarehouseBackendFailsClosedOnAnEntryTooLargeToJournal(t *testing.T) { + service, _, transport, _ := newWarehouseAuditService(t) + err := service.EmitTx(t.Context(), AuditEntry{ + EventType: EventUserCreated, OrgID: auditServiceOrg, + Payload: map[string]any{"blob": strings.Repeat("a", maxAuditJournalData)}, + }) + if !errors.Is(err, ErrAuditEntryTooLarge) { + t.Fatalf("EmitTx = %v, want ErrAuditEntryTooLarge", err) + } + if len(transport.published) != 0 { + t.Fatalf("published %v after a refused entry", transport.types()) + } +} + +func TestNewAuditServiceUnderTheWarehouseRefusesWhatCannotWork(t *testing.T) { + store := &auditServiceStore{} + transport := &journalTransport{} + for name, cfg := range map[string]AuditServiceConfig{ + "no warehouse storage": {Backend: AuditBackendWarehouse, Store: store, Producer: silentProducer{}, Transport: transport}, + "no transport": {Backend: AuditBackendWarehouse, Store: store, Producer: silentProducer{}, Warehouse: &recordingStorage{}}, + "the external tee": {Backend: AuditBackendWarehouse, Store: store, Producer: silentProducer{}, Transport: transport, Warehouse: &recordingStorage{}, ExternalTee: true}, + "a warehouse under postgres": {Backend: AuditBackendPostgres, Store: store, Producer: silentProducer{}, Transport: transport, Warehouse: &recordingStorage{}}, + } { + if _, err := NewAuditService(cfg); err == nil { + t.Errorf("%s: NewAuditService succeeded", name) + } + } +} + +// Under the warehouse the Service's reads go to the warehouse, with the +// event-type facts the warehouse cannot join itself. +func TestWarehouseBackendReadsFromTheWarehouseWithTheTypeIndex(t *testing.T) { + service, store, _, warehouse := newWarehouseAuditService(t) + store.types = []AuditEventTypeRow{{Name: string(EventAuthLogin), Category: "auth", Namespace: "saas"}} + + if _, _, err := service.List(t.Context(), AuditQuery{OrgID: auditServiceOrg}); err != nil { + t.Fatal(err) + } + if warehouse.reads[0].Types != nil { + t.Fatal("a read that filters on neither category nor namespace needs no index") + } + if _, _, err := service.List(t.Context(), AuditQuery{OrgID: auditServiceOrg, Category: "auth"}); err != nil { + t.Fatal(err) + } + facts, ok := warehouse.reads[1].Types[string(EventAuthLogin)] + if !ok || facts.Category != "auth" || facts.Namespace != "saas" { + t.Fatalf("type index = %v", warehouse.reads[1].Types) + } + if _, err := service.Aggregate(t.Context(), AuditQuery{}, AuditAggregationSpec{GroupBy: []string{"category"}}); err != nil { + t.Fatal(err) + } + if !warehouse.aggregates[0].Scope.Platform() || warehouse.aggregates[0].Types == nil { + t.Fatalf("aggregate read = %+v", warehouse.aggregates[0]) + } + if got := store.scopes; len(got) != 2 || got[0] != "org:"+auditServiceOrg || got[1] != "control-plane" { + t.Fatalf("the index is read in the read's own scope: %v", got) + } +} + +// The resource-grant check runs in the organization's Postgres transaction, and +// the aggregate is read from the warehouse only after it passes. +func TestWarehouseBackendChecksBeforeItReads(t *testing.T) { + service, store, _, warehouse := newWarehouseAuditService(t) + q := AuditQuery{OrgID: auditServiceOrg, Resource: "datasource", ResourceID: "s-1", EventType: "t"} + + if _, err := service.AggregateAuthorized(t.Context(), q, AuditAggregationSpec{}, func(context.Context) error { return errors.New("denied") }); err == nil { + t.Fatal("a refused check must refuse the read") + } + if len(warehouse.aggregates) != 0 { + t.Fatal("a refused read reached the warehouse") + } + if _, err := service.AggregateAuthorized(t.Context(), q, AuditAggregationSpec{}, func(context.Context) error { return nil }); err != nil { + t.Fatal(err) + } + if len(warehouse.aggregates) != 1 || len(store.scopes) != 2 { + t.Fatalf("warehouse reads %d, transactions %v", len(warehouse.aggregates), store.scopes) + } +} + +// The journal type is internal: the relay marks it published with no fan-out, a +// replay skips it, and no subscription pattern may match it. +func TestAuditRecordedEventTypeIsInternal(t *testing.T) { + if !eventcatalog.IsInternalPublished(AuditRecordedEventType) { + t.Fatalf("%s must be an internal published type", AuditRecordedEventType) + } + if eventcatalog.IsExternalPublished(AuditRecordedEventType) { + t.Fatalf("%s must never be eligible for an outbound webhook", AuditRecordedEventType) + } + found := false + for _, internal := range eventcatalog.InternalPublishedTypes() { + found = found || internal == AuditRecordedEventType + } + if !found { + t.Fatal("InternalPublishedTypes is what the subscribe gate matches patterns against; the journal type is missing from it") + } + if eventcatalog.Namespace(AuditRecordedEventType) != auditEventsNamespace { + t.Fatalf("namespace = %q, want the platform namespace, which no module may subscribe to", eventcatalog.Namespace(AuditRecordedEventType)) + } + for _, pattern := range []string{AuditRecordedEventType, "saas.audit.*", "saas.*"} { + if !events.Matches(pattern, AuditRecordedEventType) { + t.Errorf("pattern %q should match the journal type, so the subscribe gate refuses it", pattern) + } + } +} + +// The journal type is a published type and not an audit event: it has no audit +// registry entry, so no audit entry can ever be written under it. +func TestAuditRecordedEventTypeIsNotAnAuditEventType(t *testing.T) { + if _, registered := LookupAuditEvent(EventType(AuditRecordedEventType)); registered { + t.Fatalf("%s must not be an audit event type", AuditRecordedEventType) + } +} + +// A storage that cannot compute the percentile metric refuses it, and the +// service answers that as the caller's invalid request, on both read paths. +func TestWarehouseBackendAnswersARefusedPercentileAsInvalidArgument(t *testing.T) { + service, _, _, warehouse := newWarehouseAuditService(t) + warehouse.refusePercentile = true + spec := AuditAggregationSpec{Metrics: []AuditMetric{{Op: "percentile", Field: "payload:duration_ms", Percentile: 0.5}}} + + _, err := service.Aggregate(t.Context(), AuditQuery{OrgID: auditServiceOrg}, spec) + if status.Code(err) != codes.InvalidArgument { + t.Fatalf("Aggregate = %v, want InvalidArgument", err) + } + q := AuditQuery{OrgID: auditServiceOrg, Resource: "datasource", ResourceID: "s-1", EventType: "t"} + _, err = service.AggregateAuthorized(t.Context(), q, spec, func(context.Context) error { return nil }) + if status.Code(err) != codes.InvalidArgument { + t.Fatalf("AggregateAuthorized = %v, want InvalidArgument", err) + } + if _, err := service.Aggregate(t.Context(), AuditQuery{OrgID: auditServiceOrg}, AuditAggregationSpec{}); err != nil { + t.Fatalf("every other metric is served: %v", err) + } + if !spec.HasPercentile() || (AuditAggregationSpec{}).HasPercentile() { + t.Fatal("HasPercentile") + } +} + +func TestAuditJournalRefusesAnEntryThatFitsJSONButCannotFitReadJournal(t *testing.T) { + entry := AuditEntry{ID: uuid.NewString(), EventType: EventMFATOTPSetupStarted, Payload: map[string]any{"values": make([]int, 350000)}, CreatedAt: time.Now()} + if _, err := NewAuditJournalEnvelope(entry); !errors.Is(err, ErrAuditEntryTooLarge) { + t.Fatalf("numeric array = %v, want a pre-commit size refusal", err) + } +} diff --git a/module/services/accounts/code/pkg/business/audit_service.go b/module/services/accounts/code/pkg/business/audit_service.go new file mode 100644 index 000000000..91b2c46a6 --- /dev/null +++ b/module/services/accounts/code/pkg/business/audit_service.go @@ -0,0 +1,256 @@ +package business + +import ( + "context" + "errors" + "fmt" + "strings" + + "accounts/pkg/events" + "accounts/pkg/jobs" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// AuditBackend names where audit entries are kept. It is chosen once, at +// startup, from configuration, and never per call. +type AuditBackend string + +const ( + // AuditBackendPostgres keeps every entry in audit_events. The default: the + // entry is written in the caller's transaction, and Service.VerifyAuditWiring + // holds exactly as before. + AuditBackendPostgres AuditBackend = "postgres" + // AuditBackendWarehouse keeps entries in a warehouse. The warehouse cannot + // join the caller's transaction, so the entry is published to the journal + // (domain_events) on that transaction instead, and a task appends it to the + // warehouse afterwards through Storage. It replaces audit_events as the + // store of record: a swap, not a tee. + AuditBackendWarehouse AuditBackend = "warehouse" +) + +// ParseAuditBackend reads the audit-storage backend. Empty is the default. +func ParseAuditBackend(raw string) (AuditBackend, error) { + switch strings.ToLower(strings.TrimSpace(raw)) { + case "", string(AuditBackendPostgres): + return AuditBackendPostgres, nil + case string(AuditBackendWarehouse): + return AuditBackendWarehouse, nil + default: + return "", fmt.Errorf("audit storage backend must be %s or %s", AuditBackendPostgres, AuditBackendWarehouse) + } +} + +// AuditServiceConfig is what AuditService is built from, once, at startup. +type AuditServiceConfig struct { + Backend AuditBackend + Store Store + // Producer is the transactional job producer the external tee enqueues on. + Producer jobs.Producer + // Transport publishes the org-scoped webhook event beside each audit row + // and, under the warehouse backend, the journal event of every entry. The + // warehouse backend needs it. + Transport events.Transport + // ExternalTee enqueues an audit-export job beside each org-scoped audit row + // (AUDIT_SINK=both). It copies audit_events rows, so it is refused under the + // warehouse backend. + ExternalTee bool + // Warehouse is the storage of the warehouse backend. It is required exactly + // when Backend is AuditBackendWarehouse. + Warehouse AuditStorage +} + +// AuditService is the one place audit is written and read. It owns the choice +// of AuditStorage, which it makes once at construction: +// +// - under Postgres, Emit and EmitTx append to audit_events on the caller's +// transaction, and every read goes to audit_events; +// - under the warehouse, Emit and EmitTx publish the entry to the journal on +// the caller's transaction (including an entry with no organization) and +// append nothing, and every read goes to the warehouse. +// +// It is the TxAuditEmitter the Service writes through, so a module's audit +// (ModuleEmitAuditEvent) and every host mutation take the same path, and it is +// what the Service's activity list, aggregation and export read through. +type AuditService struct { + backend AuditBackend + store Store + // storage serves every read, and is what a delivery appends to under the + // warehouse backend. + storage AuditStorage + // postgres is the Postgres storage, non-nil exactly under the Postgres + // backend. It is what lets a resource-gated read check and read in one + // transaction. + postgres *PostgresAuditStorage + // emitter is nil on a read-only AuditService (newAuditReads). + emitter *DurableAuditEmitter +} + +var _ TxAuditEmitter = (*AuditService)(nil) + +// NewAuditService builds the AuditService for cfg.Backend. A backend it cannot +// build is an error here, at startup, not a failure on the first audited write. +func NewAuditService(cfg AuditServiceConfig) (*AuditService, error) { + if cfg.Store == nil { + return nil, errors.New("audit: store is required") + } + postgres, err := NewPostgresAuditStorage(cfg.Store) + if err != nil { + return nil, err + } + service := &AuditService{backend: cfg.Backend, store: cfg.Store} + var opts []DurableAuditEmitterOption + if cfg.ExternalTee { + opts = append(opts, WithExternalTee()) + } + if cfg.Transport != nil { + opts = append(opts, WithDomainEventTransport(cfg.Transport)) + } + switch cfg.Backend { + case AuditBackendPostgres, "": + if cfg.Warehouse != nil { + return nil, errors.New("audit: a warehouse storage is wired but the audit backend is postgres") + } + service.backend, service.storage, service.postgres = AuditBackendPostgres, postgres, postgres + opts = append(opts, WithAuditStorage(postgres)) + case AuditBackendWarehouse: + if cfg.Warehouse == nil { + return nil, errors.New("audit: the warehouse backend needs a warehouse storage") + } + if cfg.ExternalTee { + return nil, errors.New("audit: the external tee copies audit_events rows and cannot run under the warehouse backend") + } + service.storage = cfg.Warehouse + opts = append(opts, WithAuditJournal()) + default: + return nil, fmt.Errorf("audit: unknown backend %q", cfg.Backend) + } + service.emitter, err = NewDurableAuditEmitter(cfg.Store, cfg.Producer, opts...) + if err != nil { + return nil, err + } + return service, nil +} + +// newAuditReads is the read side of the Postgres backend, which a Service uses +// until it is given an AuditService, so every Service reads audit_events exactly +// as it always has. It writes nothing. +func newAuditReads(store Store) (*AuditService, error) { + postgres, err := NewPostgresAuditStorage(store) + if err != nil { + return nil, err + } + return &AuditService{backend: AuditBackendPostgres, store: store, storage: postgres, postgres: postgres}, nil +} + +// Backend is the backend chosen at startup. +func (a *AuditService) Backend() AuditBackend { return a.backend } + +// Storage is the AuditStorage the backend reads from, and a delivery appends to +// under the warehouse backend. +func (a *AuditService) Storage() AuditStorage { return a.storage } + +// Emit writes an entry on a transaction of its own (AuditEmitter). +func (a *AuditService) Emit(ctx context.Context, entry AuditEntry) { + if a.emitter == nil { + return + } + a.emitter.Emit(ctx, entry) +} + +// EmitTx writes an entry on the caller's ambient transaction (TxAuditEmitter). +func (a *AuditService) EmitTx(ctx context.Context, entry AuditEntry) error { + if a.emitter == nil { + return errors.New("audit: this AuditService only reads") + } + return a.emitter.EmitTx(ctx, entry) +} + +// Close releases nothing today; it keeps the emitter's contract. +func (a *AuditService) Close() {} + +// List reads one page of the activity list in the scope q names: its +// organization, or the platform's when it names none (a read the transport +// admits only for a platform admin). +func (a *AuditService) List(ctx context.Context, q AuditQuery) ([]AuditEntry, string, error) { + read, err := a.read(ctx, q, nil) + if err != nil { + return nil, "", err + } + return a.storage.List(ctx, read) +} + +// Aggregate groups the entries q selects per spec, in the same scope as List. +func (a *AuditService) Aggregate(ctx context.Context, q AuditQuery, spec AuditAggregationSpec) ([]AuditAggregateBucket, error) { + read, err := a.read(ctx, q, &spec) + if err != nil { + return nil, err + } + out, err := a.storage.Aggregate(ctx, read, spec) + return out, refusalOfUnsupportedMetric(err) +} + +// refusalOfUnsupportedMetric answers a storage's refusal of the percentile +// metric as what it is: the request asked for something this deployment's audit +// storage cannot compute, not a fault in the service. +func refusalOfUnsupportedMetric(err error) error { + if errors.Is(err, ErrAuditPercentileUnsupported) { + return status.Error(codes.InvalidArgument, err.Error()) + } + return err +} + +// AggregateAuthorized aggregates after authorize has passed inside the +// organization's transaction. Under Postgres the check and the read share that +// one transaction, one snapshot. Under the warehouse the check still runs in the +// organization's Postgres transaction and the aggregate is read afterwards from +// the warehouse: two steps, no shared snapshot. +func (a *AuditService) AggregateAuthorized(ctx context.Context, q AuditQuery, spec AuditAggregationSpec, authorize func(context.Context) error) ([]AuditAggregateBucket, error) { + read, err := a.read(ctx, q, &spec) + if err != nil { + return nil, err + } + if a.postgres != nil { + var out []AuditAggregateBucket + err = a.postgres.within(ctx, read.Scope, func(ctx context.Context) error { + if err := authorize(ctx); err != nil { + return err + } + var err error + out, err = a.postgres.AggregateOnTx(ctx, read, spec) + return err + }) + return out, err + } + if read.Scope.Platform() { + return nil, status.Error(codes.InvalidArgument, "a resource-gated read names its organization") + } + if err := a.store.WithOrgTx(ctx, read.Scope.OrgID(), authorize); err != nil { + return nil, err + } + out, err := a.storage.Aggregate(ctx, read, spec) + return out, refusalOfUnsupportedMetric(err) +} + +// read is the read of q in its scope. A storage outside Postgres also gets the +// event-type facts the query or the aggregation needs, read from +// audit_event_types in that same scope — the rows the Postgres reads join in SQL. +func (a *AuditService) read(ctx context.Context, q AuditQuery, spec *AuditAggregationSpec) (AuditRead, error) { + read := AuditRead{Scope: auditReadScopeFor(q), Query: q} + if a.postgres != nil || !auditReadNeedsTypes(q, spec) { + return read, nil + } + err := withinAuditScope(ctx, a.store, read.Scope, func(ctx context.Context) error { + rows, err := a.store.ListAuditEventTypes(ctx) + if err != nil { + return err + } + read.Types = NewAuditEventTypeIndex(rows) + return nil + }) + if err != nil { + return AuditRead{}, fmt.Errorf("audit read: event types: %w", err) + } + return read, nil +} diff --git a/module/services/accounts/code/pkg/business/audit_service_test.go b/module/services/accounts/code/pkg/business/audit_service_test.go new file mode 100644 index 000000000..13bc25471 --- /dev/null +++ b/module/services/accounts/code/pkg/business/audit_service_test.go @@ -0,0 +1,256 @@ +package business + +import ( + "context" + "errors" + "testing" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// recordingStorage is an AuditStorage that remembers what it was asked, so the +// AuditService is tested for which storage it reaches and in which scope. +type recordingStorage struct { + appended []AuditEntry + reads []AuditRead + aggregates []AuditRead + page []AuditEntry + appendErr error + // refusePercentile makes Aggregate refuse the percentile metric, as the + // warehouse storage does. + refusePercentile bool +} + +func (r *recordingStorage) Append(_ context.Context, entry AuditEntry) error { + r.appended = append(r.appended, entry) + return r.appendErr +} + +func (r *recordingStorage) List(_ context.Context, read AuditRead) ([]AuditEntry, string, error) { + r.reads = append(r.reads, read) + return r.page, "", nil +} + +func (r *recordingStorage) Aggregate(_ context.Context, read AuditRead, spec AuditAggregationSpec) ([]AuditAggregateBucket, error) { + if r.refusePercentile && spec.HasPercentile() { + return nil, ErrAuditPercentileUnsupported + } + r.aggregates = append(r.aggregates, read) + return []AuditAggregateBucket{{Key: "k", Count: 1}}, nil +} + +// auditServiceStore is the Store an AuditService test runs on: transactions run +// inline and are named, and nothing needs a database. +type auditServiceStore struct { + scopeStore + reserved map[string]bool + types []AuditEventTypeRow +} + +func (s *auditServiceStore) ListAuditEventTypes(context.Context) ([]AuditEventTypeRow, error) { + return s.types, nil +} + +func (s *auditServiceStore) GetDeclaredAuditEventType(context.Context, EventType) (*DeclaredAuditEventType, error) { + return nil, nil +} + +func (s *auditServiceStore) ReserveAuditIdempotency(_ context.Context, orgID, eventType, key string) (bool, error) { + id := orgID + "|" + eventType + "|" + key + if s.reserved[id] { + return false, nil + } + if s.reserved == nil { + s.reserved = map[string]bool{} + } + s.reserved[id] = true + return true, nil +} + +const auditServiceOrg = "11111111-1111-1111-1111-111111111111" + +func TestAuditServiceUnderPostgresAppendsThroughItsStorage(t *testing.T) { + store := &auditServiceStore{} + transport := &recordingTransport{} + service, err := NewAuditService(AuditServiceConfig{ + Backend: AuditBackendPostgres, Store: store, Producer: silentProducer{}, Transport: transport, + }) + if err != nil { + t.Fatal(err) + } + if service.Backend() != AuditBackendPostgres { + t.Fatalf("backend = %q", service.Backend()) + } + entry := AuditEntry{EventType: EventUserCreated, OrgID: auditServiceOrg, ActorType: ActorTypeSystem, Resource: "user"} + if err := service.EmitTx(t.Context(), entry); err != nil { + t.Fatal(err) + } + if len(store.inserted) != 1 || store.inserted[0].EventType != EventUserCreated { + t.Fatalf("audit_events rows = %v, want the one entry", store.inserted) + } + if len(transport.published) != 1 { + t.Fatalf("webhook publications = %d, want 1 for an org-scoped external type", len(transport.published)) + } + // EmitTx joins the caller's transaction: it opens none of its own. + if len(store.scopes) != 0 { + t.Fatalf("EmitTx opened a transaction: %v", store.scopes) + } + + // Emit has no caller transaction, so it opens the one its scope names: the + // organization's, or the control plane's for an entry with no organization. + service.Emit(t.Context(), entry) + service.Emit(t.Context(), AuditEntry{EventType: EventAuthLogin, ActorType: ActorTypeSystem, Resource: "session"}) + if got := store.scopes; len(got) != 2 || got[0] != "org:"+auditServiceOrg || got[1] != "control-plane" { + t.Fatalf("Emit transactions = %v", got) + } +} + +func TestAuditServiceEmitterAppendsToTheStorageItIsGiven(t *testing.T) { + store := &auditServiceStore{} + storage := &recordingStorage{} + emitter, err := NewDurableAuditEmitter(store, silentProducer{}, WithAuditStorage(storage)) + if err != nil { + t.Fatal(err) + } + if err := emitter.EmitTx(t.Context(), AuditEntry{EventType: EventUserCreated, OrgID: auditServiceOrg, ActorType: ActorTypeSystem}); err != nil { + t.Fatal(err) + } + if len(storage.appended) != 1 || len(store.inserted) != 0 { + t.Fatalf("appended to storage %d, to audit_events %d; want 1 and 0", len(storage.appended), len(store.inserted)) + } + if storage.appended[0].ID == "" || storage.appended[0].CreatedAt.IsZero() { + t.Fatalf("the emitter backfills id and time before appending: %+v", storage.appended[0]) + } + // A failed append is the caller's failure, so its transaction rolls back. + storage.appendErr = errors.New("boom") + if err := emitter.EmitTx(t.Context(), AuditEntry{EventType: EventUserCreated, OrgID: auditServiceOrg}); err == nil { + t.Fatal("a failed append must fail the emit") + } +} + +func TestAuditServiceIdempotencyStillGuardsTheAppend(t *testing.T) { + store := &auditServiceStore{} + storage := &recordingStorage{} + emitter, err := NewDurableAuditEmitter(store, silentProducer{}, WithAuditStorage(storage)) + if err != nil { + t.Fatal(err) + } + entry := AuditEntry{EventType: EventUserCreated, OrgID: auditServiceOrg, IdempotencyKey: "once"} + for range 2 { + if err := emitter.EmitTx(t.Context(), entry); err != nil { + t.Fatal(err) + } + } + if len(storage.appended) != 1 { + t.Fatalf("appended %d, want 1: a retried emit collapses to one entry", len(storage.appended)) + } +} + +func TestNewAuditServiceRefusesWhatItCannotBuild(t *testing.T) { + store := &auditServiceStore{} + for name, cfg := range map[string]AuditServiceConfig{ + "no store": {Backend: AuditBackendPostgres, Producer: silentProducer{}}, + "unknown backend": {Backend: "carrier-pigeon", Store: store, Producer: silentProducer{}}, + "no producer": {Backend: AuditBackendPostgres, Store: store}, + } { + if _, err := NewAuditService(cfg); err == nil { + t.Errorf("%s: NewAuditService succeeded", name) + } + } +} + +func TestAuditServiceReadsInTheScopeOfTheQuery(t *testing.T) { + store := &auditServiceStore{} + service, err := NewAuditService(AuditServiceConfig{Backend: AuditBackendPostgres, Store: store, Producer: silentProducer{}}) + if err != nil { + t.Fatal(err) + } + if _, _, err := service.List(t.Context(), AuditQuery{OrgID: auditServiceOrg}); err != nil { + t.Fatal(err) + } + if _, err := service.Aggregate(t.Context(), AuditQuery{}, AuditAggregationSpec{}); err != nil { + t.Fatal(err) + } + if got := store.scopes; len(got) != 2 || got[0] != "org:"+auditServiceOrg || got[1] != "control-plane" { + t.Fatalf("read transactions = %v", got) + } +} + +func TestAuditServiceChecksAndReadsInOneTransaction(t *testing.T) { + store := &auditServiceStore{} + service, err := NewAuditService(AuditServiceConfig{Backend: AuditBackendPostgres, Store: store, Producer: silentProducer{}}) + if err != nil { + t.Fatal(err) + } + q := AuditQuery{OrgID: auditServiceOrg, Resource: "datasource", ResourceID: "s-1", EventType: "t"} + + checked := 0 + out, err := service.AggregateAuthorized(t.Context(), q, AuditAggregationSpec{}, func(context.Context) error { + checked++ + return nil + }) + if err != nil || len(out) != 1 || checked != 1 { + t.Fatalf("AggregateAuthorized = %v %v, checked %d", out, err, checked) + } + if len(store.scopes) != 1 { + t.Fatalf("the check and the read opened %d transactions, want 1: %v", len(store.scopes), store.scopes) + } + + denied := status.Error(codes.PermissionDenied, "no") + store.aggregated = nil + if _, err := service.AggregateAuthorized(t.Context(), q, AuditAggregationSpec{}, func(context.Context) error { return denied }); !errors.Is(err, denied) { + t.Fatalf("a refused check = %v, want the refusal", err) + } + if len(store.aggregated) != 0 { + t.Fatal("a refused read reached the store") + } +} + +// The Service's reads go through the AuditService it was given, not through the +// Store: the write and the read cannot name two different stores. +func TestServiceReadsThroughTheAuditServiceItIsGiven(t *testing.T) { + store := &auditServiceStore{} + storage := &recordingStorage{page: []AuditEntry{{ID: "a"}, {ID: "b"}}} + service := &AuditService{backend: AuditBackendPostgres, store: store, storage: storage} + svc, err := NewService(store) + if err != nil { + t.Fatal(err) + } + svc.SetAuditService(service) + + entries, _, total, err := svc.QueryAuditLog(t.Context(), AuditQuery{OrgID: auditServiceOrg}) + if err != nil || len(entries) != 2 || total != 2 { + t.Fatalf("QueryAuditLog = %v %d %v", entries, total, err) + } + if len(store.queried) != 0 { + t.Fatal("the Service read audit_events directly") + } + if len(storage.reads) != 1 || storage.reads[0].Scope.OrgID() != auditServiceOrg { + t.Fatalf("storage reads = %+v", storage.reads) + } + if _, err := svc.AggregateAuditLog(t.Context(), AuditQuery{}, AuditAggregationSpec{}); err != nil { + t.Fatal(err) + } + if len(storage.aggregates) != 1 || !storage.aggregates[0].Scope.Platform() { + t.Fatalf("storage aggregates = %+v", storage.aggregates) + } + if _, _, _, err := svc.QueryAuditLog(t.Context(), AuditQuery{OrgID: auditServiceOrg, CollectionID: "c"}); status.Code(err) != codes.InvalidArgument { + t.Fatalf("a collection filter without a reader = %v, want InvalidArgument", err) + } +} + +func TestServiceWithoutAnAuditServiceReadsAuditEvents(t *testing.T) { + store := &auditServiceStore{} + svc, err := NewService(store) + if err != nil { + t.Fatal(err) + } + entries, next, total, err := svc.QueryAuditLog(t.Context(), AuditQuery{OrgID: auditServiceOrg}) + if err != nil || len(entries) != 1 || next != "next" || total != 1 { + t.Fatalf("QueryAuditLog = %v %q %d %v", entries, next, total, err) + } + if len(store.queried) != 1 || len(store.scopes) != 1 { + t.Fatalf("store reads %d in %v", len(store.queried), store.scopes) + } +} diff --git a/module/services/accounts/code/pkg/business/audit_storage.go b/module/services/accounts/code/pkg/business/audit_storage.go new file mode 100644 index 000000000..bd0d8cf8d --- /dev/null +++ b/module/services/accounts/code/pkg/business/audit_storage.go @@ -0,0 +1,313 @@ +package business + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "strings" + "time" +) + +// AuditStorage is where audit entries are kept and read back. It is the one +// interface the audit write path, the activity list and the aggregation go +// through, so the store of record can be chosen once at startup (AuditService) +// instead of being wired into every caller. +// +// Two implementations are meant to exist, and they differ in one way that the +// interface makes explicit rather than hides: +// +// - PostgresAuditStorage keeps entries in audit_events. Append joins the +// caller's transaction, so an entry commits or rolls back with the mutation +// it records. +// - A warehouse implementation keeps entries outside Postgres. Append cannot +// join the caller's transaction, so AuditService never calls it from one: +// the entry is published to the journal (domain_events) on the caller's +// transaction, and a later task appends it (DeliverAuditEvent). +// +// The registry of event types and the partition methods stay on Store: they are +// about Postgres tables, not about where entries are kept. +// +// Contract, held for every implementation by auditstoragetest.Run: +// +// - Append stores one entry. An implementation may be at-least-once: an entry +// appended twice may be held twice, and every read returns each entry once, +// by entry id. +// - List returns one page, newest first by occurrence and then by entry id, +// both descending, and the token of the next page (empty after the last). +// A zero page size is 50. +// - Aggregate groups the matching entries per spec, which the caller has +// validated. +// - A read names its scope (AuditRead.Scope). An implementation refuses a read +// with no scope (ErrAuditReadUnscoped), and one whose query reaches past it, +// instead of treating a missing scope as "everything". +// - An implementation that cannot compute the percentile metric refuses it +// with ErrAuditPercentileUnsupported before it reads anything. The +// warehouse implementation is one: a percentile over a deduplicated, +// at-least-once store is not an answer it can give exactly. Every other +// metric and dimension is served by every implementation. +type AuditStorage interface { + Append(ctx context.Context, entry AuditEntry) error + List(ctx context.Context, read AuditRead) ([]AuditEntry, string, error) + Aggregate(ctx context.Context, read AuditRead, spec AuditAggregationSpec) ([]AuditAggregateBucket, error) +} + +// ErrAuditReadUnscoped is the refusal of a read that names no scope. +var ErrAuditReadUnscoped = errors.New("audit read: no scope; a read names its organization, or is explicitly a platform read") + +// ErrAuditPercentileUnsupported is the refusal of an aggregation that asks for +// the percentile metric from a storage that cannot compute it. AuditService +// answers it as invalid-argument: the request, not the service, is what cannot +// be served. +var ErrAuditPercentileUnsupported = errors.New("audit aggregate: the percentile metric is not supported by this audit storage") + +// HasPercentile reports whether the spec asks for a percentile metric. +func (s AuditAggregationSpec) HasPercentile() bool { + for _, metric := range s.Metrics { + if metric.Op == "percentile" { + return true + } + } + return false +} + +// AuditReadScope is whose entries a read may return. It is either one +// organization's entries, or — explicitly — the platform's view, which spans +// every organization and the platform's own entries (no organization). The zero +// value is no scope, and every storage refuses it. +// +// The platform scope is exactly what Postgres serves under WithControlPlane +// today: a read whose query names no organization, which the transport admits +// only for a platform admin. +type AuditReadScope struct { + orgID string + platform bool +} + +// OrganizationAuditScope scopes a read to one organization's entries. +func OrganizationAuditScope(orgID string) AuditReadScope { + return AuditReadScope{orgID: orgID} +} + +// PlatformAuditScope is the explicit platform read: every organization's entries +// and the platform's own. Only the platform-admin path takes it. +func PlatformAuditScope() AuditReadScope { + return AuditReadScope{platform: true} +} + +// OrgID is the organization an organization-scoped read is confined to; empty +// for the platform scope. +func (s AuditReadScope) OrgID() string { return s.orgID } + +// Platform reports whether the read is the explicit platform read. +func (s AuditReadScope) Platform() bool { return s.platform } + +// Validate refuses the zero scope. +func (s AuditReadScope) Validate() error { + if s.platform == (s.orgID != "") { + return ErrAuditReadUnscoped + } + return nil +} + +// String names the scope for errors and logs. +func (s AuditReadScope) String() string { + switch { + case s.platform: + return "platform" + case s.orgID != "": + return "organization " + s.orgID + default: + return "unscoped" + } +} + +// auditReadScopeFor is the scope of a read of q: its organization, or the +// platform scope when it names none. +func auditReadScopeFor(q AuditQuery) AuditReadScope { + if q.OrgID == "" { + return PlatformAuditScope() + } + return OrganizationAuditScope(q.OrgID) +} + +// AuditEventTypeFacts is what a read needs to know about an event type it does +// not hold itself: the category and namespace its audit_event_types row names. +type AuditEventTypeFacts struct { + Category string + Namespace string +} + +// AuditEventTypeIndex is the audit_event_types projection by type name — the +// registry's code-owned and declared types alike. In Postgres the category and +// namespace filters and the category dimension read that table in SQL; a +// storage outside Postgres takes the same facts from this index. +type AuditEventTypeIndex map[string]AuditEventTypeFacts + +// NewAuditEventTypeIndex indexes audit_event_types rows. +func NewAuditEventTypeIndex(rows []AuditEventTypeRow) AuditEventTypeIndex { + index := make(AuditEventTypeIndex, len(rows)) + for _, row := range rows { + index[row.Name] = AuditEventTypeFacts{Category: row.Category, Namespace: row.Namespace} + } + return index +} + +// AuditRead is one read of the storage: whose entries, which ones, and the +// event-type facts the query needs. +type AuditRead struct { + Scope AuditReadScope + Query AuditQuery + // Types is set when the query filters on a category or namespace, or the + // aggregation groups or counts by category, and the storage is not + // Postgres. A storage that needs it and finds it nil refuses the read + // rather than treating every type as unknown. + Types AuditEventTypeIndex +} + +// Validate checks the read's scope and that the query does not reach past it: +// an organization-scoped read's query names that organization, and a platform +// read's query names none. +func (r AuditRead) Validate() error { + if err := r.Scope.Validate(); err != nil { + return err + } + if r.Query.OrgID != r.Scope.orgID { + return fmt.Errorf("audit read: query organization %q is outside the %s scope", r.Query.OrgID, r.Scope) + } + if r.Query.CollectionID != "" { + return errors.New("uncompiled collection filter: reader authorization required") + } + return nil +} + +// EncodeAuditPageToken / DecodeAuditPageToken carry the keyset position of the +// activity list: the last returned entry's (occurrence, id). The token is opaque +// to callers — base64 of "|" — and every storage reads the +// same token, so a page boundary means the same thing in all of them. +func EncodeAuditPageToken(occurredAt time.Time, id string) string { + raw := occurredAt.UTC().Format(time.RFC3339Nano) + "|" + id + return base64.RawURLEncoding.EncodeToString([]byte(raw)) +} + +// DecodeAuditPageToken reads a token EncodeAuditPageToken wrote. +func DecodeAuditPageToken(token string) (time.Time, string, error) { + raw, err := base64.RawURLEncoding.DecodeString(token) + if err != nil { + return time.Time{}, "", err + } + parts := strings.SplitN(string(raw), "|", 2) + if len(parts) != 2 { + return time.Time{}, "", fmt.Errorf("malformed cursor") + } + occurredAt, err := time.Parse(time.RFC3339Nano, parts[0]) + if err != nil { + return time.Time{}, "", err + } + return occurredAt, parts[1], nil +} + +// auditReadNeedsTypes reports whether a read of q, aggregated per spec when it +// is not nil, needs the event-type index. +func auditReadNeedsTypes(q AuditQuery, spec *AuditAggregationSpec) bool { + if q.Category != "" || q.Namespace != "" { + return true + } + if spec == nil { + return false + } + for _, dimension := range spec.GroupBy { + if dimension == "category" { + return true + } + } + for _, metric := range spec.Metrics { + if metric.Op == "count_distinct" && metric.Field == "category" { + return true + } + } + return false +} + +// PostgresAuditStorage is the AuditStorage over audit_events — what every +// deployment has always used. Append writes on the transaction already on ctx; +// each read runs in its own transaction, the organization's (row-level security +// admits its rows) or, for the platform scope, the control plane's (which spans +// tenants). Postgres reads the event-type facts itself, in SQL, so it ignores +// AuditRead.Types. +type PostgresAuditStorage struct { + store Store +} + +// NewPostgresAuditStorage is the Postgres AuditStorage over store. +func NewPostgresAuditStorage(store Store) (*PostgresAuditStorage, error) { + if store == nil { + return nil, errors.New("audit: store is required") + } + return &PostgresAuditStorage{store: store}, nil +} + +var _ AuditStorage = (*PostgresAuditStorage)(nil) + +// within runs fn in the transaction the scope names. +func (p *PostgresAuditStorage) within(ctx context.Context, scope AuditReadScope, fn func(context.Context) error) error { + return withinAuditScope(ctx, p.store, scope, fn) +} + +// withinAuditScope runs fn in the Postgres transaction a read's scope names: the +// organization's, which row-level security confines to its rows, or the control +// plane's for the platform scope, which spans tenants. It is the one place a +// read opens its transaction. +func withinAuditScope(ctx context.Context, store Store, scope AuditReadScope, fn func(context.Context) error) error { + if scope.Platform() { + return store.WithControlPlane(ctx, fn) + } + return store.WithOrgTx(ctx, scope.OrgID(), fn) +} + +// Append inserts the entry into audit_events on the caller's ambient +// transaction. It MUST run inside an active transaction: the entry then commits +// or rolls back with the mutation it records. +func (p *PostgresAuditStorage) Append(ctx context.Context, entry AuditEntry) error { + return p.store.InsertAuditEvent(ctx, entry) +} + +// List reads one page of the activity list. +func (p *PostgresAuditStorage) List(ctx context.Context, read AuditRead) ([]AuditEntry, string, error) { + if err := read.Validate(); err != nil { + return nil, "", err + } + var entries []AuditEntry + var nextToken string + err := p.within(ctx, read.Scope, func(ctx context.Context) error { + var err error + entries, nextToken, _, err = p.store.QueryAuditLog(ctx, read.Query) + return err + }) + return entries, nextToken, err +} + +// Aggregate groups the matching entries per spec. +func (p *PostgresAuditStorage) Aggregate(ctx context.Context, read AuditRead, spec AuditAggregationSpec) ([]AuditAggregateBucket, error) { + if err := read.Validate(); err != nil { + return nil, err + } + var out []AuditAggregateBucket + err := p.within(ctx, read.Scope, func(ctx context.Context) error { + var err error + out, err = p.AggregateOnTx(ctx, read, spec) + return err + }) + return out, err +} + +// AggregateOnTx is Aggregate on the transaction already on ctx, for a caller +// that has to check something and read in one snapshot (the resource-grant gate +// of AggregateAuditLogForReader). The caller owns the scope: ctx's transaction +// is the organization's, or the control plane's. +func (p *PostgresAuditStorage) AggregateOnTx(ctx context.Context, read AuditRead, spec AuditAggregationSpec) ([]AuditAggregateBucket, error) { + if err := read.Validate(); err != nil { + return nil, err + } + return p.store.AggregateAuditLog(ctx, read.Query, spec) +} diff --git a/module/services/accounts/code/pkg/business/audit_storage_contract_test.go b/module/services/accounts/code/pkg/business/audit_storage_contract_test.go new file mode 100644 index 000000000..e5637c51d --- /dev/null +++ b/module/services/accounts/code/pkg/business/audit_storage_contract_test.go @@ -0,0 +1,44 @@ +//go:build !pure + +package business_test + +import ( + "context" + "testing" + + "accounts/pkg/business" + "accounts/pkg/business/auditstoragetest" + + "github.com/stretchr/testify/require" +) + +// TestPostgresAuditStorageContract runs the AuditStorage contract suite against +// the Postgres implementation, the reference every other storage is held to. +// Entries are appended the way the emitter writes them: inside the +// organization's transaction, or the control plane's for an entry with no +// organization. +func TestPostgresAuditStorageContract(t *testing.T) { + auditstoragetest.Run(t, auditstoragetest.Harness{ + New: func(t *testing.T) *auditstoragetest.Subject { + clearData(t) + require.NoError(t, testStore.WithControlPlane(testCtx, func(ctx context.Context) error { + if err := testStore.SyncAuditEventTypes(ctx, business.AuditEventCatalog()); err != nil { + return err + } + return testStore.EnsureAuditPartitions(ctx, 1) + })) + storage, err := business.NewPostgresAuditStorage(testStore) + require.NoError(t, err) + return &auditstoragetest.Subject{ + Storage: storage, + Append: func(ctx context.Context, entry business.AuditEntry) error { + write := func(ctx context.Context) error { return storage.Append(ctx, entry) } + if entry.OrgID == "" { + return testStore.WithControlPlane(ctx, write) + } + return testStore.WithOrgTx(ctx, entry.OrgID, write) + }, + } + }, + }) +} diff --git a/module/services/accounts/code/pkg/business/audit_storage_test.go b/module/services/accounts/code/pkg/business/audit_storage_test.go new file mode 100644 index 000000000..1ae044177 --- /dev/null +++ b/module/services/accounts/code/pkg/business/audit_storage_test.go @@ -0,0 +1,156 @@ +package business + +import ( + "context" + "errors" + "testing" + "time" +) + +// scopeStore records which transaction a read ran in, and what the Store was +// asked, so PostgresAuditStorage's scoping is checked without a database. +type scopeStore struct { + Store + scopes []string + inserted []AuditEntry + queried []AuditQuery + aggregated []AuditQuery + insertError error +} + +func (s *scopeStore) WithOrgTx(ctx context.Context, orgID string, fn func(context.Context) error) error { + s.scopes = append(s.scopes, "org:"+orgID) + return fn(ctx) +} + +func (s *scopeStore) WithControlPlane(ctx context.Context, fn func(context.Context) error) error { + s.scopes = append(s.scopes, "control-plane") + return fn(ctx) +} + +func (s *scopeStore) InsertAuditEvent(_ context.Context, entry AuditEntry) error { + s.inserted = append(s.inserted, entry) + return s.insertError +} + +func (s *scopeStore) QueryAuditLog(_ context.Context, q AuditQuery) ([]AuditEntry, string, int32, error) { + s.queried = append(s.queried, q) + return []AuditEntry{{ID: "one"}}, "next", 1, nil +} + +func (s *scopeStore) AggregateAuditLog(_ context.Context, q AuditQuery, _ AuditAggregationSpec) ([]AuditAggregateBucket, error) { + s.aggregated = append(s.aggregated, q) + return []AuditAggregateBucket{{Key: "k", Count: 2}}, nil +} + +func TestAuditReadScopeNamesExactlyOneScope(t *testing.T) { + for _, tc := range []struct { + name string + scope AuditReadScope + ok bool + }{ + {"zero", AuditReadScope{}, false}, + {"organization", OrganizationAuditScope("org-1"), true}, + {"platform", PlatformAuditScope(), true}, + {"empty organization is the zero scope", OrganizationAuditScope(""), false}, + } { + t.Run(tc.name, func(t *testing.T) { + err := tc.scope.Validate() + if tc.ok && err != nil { + t.Fatalf("Validate = %v, want nil", err) + } + if !tc.ok && !errors.Is(err, ErrAuditReadUnscoped) { + t.Fatalf("Validate = %v, want ErrAuditReadUnscoped", err) + } + }) + } +} + +func TestAuditReadRefusesAQueryOutsideItsScope(t *testing.T) { + for _, tc := range []struct { + name string + read AuditRead + ok bool + }{ + {"organization", AuditRead{Scope: OrganizationAuditScope("a"), Query: AuditQuery{OrgID: "a"}}, true}, + {"platform", AuditRead{Scope: PlatformAuditScope()}, true}, + {"another organization", AuditRead{Scope: OrganizationAuditScope("a"), Query: AuditQuery{OrgID: "b"}}, false}, + {"platform naming an organization", AuditRead{Scope: PlatformAuditScope(), Query: AuditQuery{OrgID: "a"}}, false}, + {"organization naming none", AuditRead{Scope: OrganizationAuditScope("a")}, false}, + {"uncompiled collection", AuditRead{Scope: OrganizationAuditScope("a"), Query: AuditQuery{OrgID: "a", CollectionID: "c"}}, false}, + } { + t.Run(tc.name, func(t *testing.T) { + if err := tc.read.Validate(); (err == nil) != tc.ok { + t.Fatalf("Validate = %v, ok = %v", err, tc.ok) + } + }) + } +} + +func TestAuditPageTokenRoundTrips(t *testing.T) { + at := time.Date(2026, 10, 12, 8, 30, 0, 123456000, time.UTC) + gotAt, gotID, err := DecodeAuditPageToken(EncodeAuditPageToken(at, "entry-1")) + if err != nil || !gotAt.Equal(at) || gotID != "entry-1" { + t.Fatalf("round trip = %v %q %v", gotAt, gotID, err) + } + if _, _, err := DecodeAuditPageToken("not base64 !"); err == nil { + t.Fatal("a malformed token is refused") + } +} + +func TestPostgresAuditStorageReadsInTheScopesTransaction(t *testing.T) { + store := &scopeStore{} + storage, err := NewPostgresAuditStorage(store) + if err != nil { + t.Fatal(err) + } + ctx := t.Context() + + orgRead := AuditRead{Scope: OrganizationAuditScope("org-1"), Query: AuditQuery{OrgID: "org-1"}} + entries, next, err := storage.List(ctx, orgRead) + if err != nil || len(entries) != 1 || next != "next" { + t.Fatalf("List = %v %q %v", entries, next, err) + } + if _, err := storage.Aggregate(ctx, AuditRead{Scope: PlatformAuditScope()}, AuditAggregationSpec{}); err != nil { + t.Fatal(err) + } + if got, want := store.scopes, []string{"org:org-1", "control-plane"}; len(got) != 2 || got[0] != want[0] || got[1] != want[1] { + t.Fatalf("transactions = %v, want %v", got, want) + } + + // An unscoped read never opens a transaction, let alone reads. + store.scopes, store.queried = nil, nil + if _, _, err := storage.List(ctx, AuditRead{Query: AuditQuery{OrgID: "org-1"}}); !errors.Is(err, ErrAuditReadUnscoped) { + t.Fatalf("unscoped List = %v, want ErrAuditReadUnscoped", err) + } + if len(store.scopes) != 0 || len(store.queried) != 0 { + t.Fatalf("an unscoped read reached the store: %v %v", store.scopes, store.queried) + } +} + +func TestPostgresAuditStorageAppendWritesOnTheAmbientTransaction(t *testing.T) { + store := &scopeStore{} + storage, err := NewPostgresAuditStorage(store) + if err != nil { + t.Fatal(err) + } + if err := storage.Append(t.Context(), AuditEntry{ID: "e-1", EventType: EventAuthLogin}); err != nil { + t.Fatal(err) + } + if len(store.inserted) != 1 || store.inserted[0].ID != "e-1" { + t.Fatalf("inserted = %v", store.inserted) + } + if len(store.scopes) != 0 { + t.Fatalf("Append opened a transaction of its own: %v", store.scopes) + } + store.insertError = errors.New("boom") + if err := storage.Append(t.Context(), AuditEntry{ID: "e-2"}); err == nil { + t.Fatal("a failed insert is the caller's failure, so its transaction rolls back") + } +} + +func TestNewPostgresAuditStorageNeedsAStore(t *testing.T) { + if _, err := NewPostgresAuditStorage(nil); err == nil { + t.Fatal("a storage over no store is refused") + } +} diff --git a/module/services/accounts/code/pkg/business/auditstoragetest/suite.go b/module/services/accounts/code/pkg/business/auditstoragetest/suite.go new file mode 100644 index 000000000..0113125bb --- /dev/null +++ b/module/services/accounts/code/pkg/business/auditstoragetest/suite.go @@ -0,0 +1,506 @@ +// Package auditstoragetest is the contract suite of business.AuditStorage. Every +// implementation runs it, so a reader written against one conforming storage +// reads the same answers from another: the activity list page by page, every +// aggregation, the scope every read must name, and the at-least-once delivery a +// storage outside Postgres is allowed. +// +// One fixture is appended through the storage under test, and every answer is +// checked two ways. The suite holds its own oracle of the predicates it asks +// about (organization, actor, type, resource, client, window, flat payload +// values, and category and namespace through the event-type index), so a storage +// is measured against the fixture rather than against another implementation. +// When the harness also supplies a reference — the Postgres storage, over the +// same fixture — every list page and every aggregation must equal the +// reference's answer exactly. +package auditstoragetest + +import ( + "context" + "sort" + "testing" + "time" + + "accounts/pkg/business" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" +) + +// Subject is one storage under test, and what the suite needs to feed it. +type Subject struct { + // Storage is what the suite reads. + Storage business.AuditStorage + // Append writes one entry the way a deployment does: Postgres inside the + // organization's (or, for an entry with no organization, the control plane's) + // transaction, a warehouse as the delivery task does. + Append func(ctx context.Context, entry business.AuditEntry) error + // AtLeastOnce says the storage may hold an entry more than once after a + // redelivery. The suite then appends two entries a second time, and every + // answer must be unchanged: a read returns each entry once, by id. + AtLeastOnce bool + // NoPercentile says the storage refuses the percentile metric. The suite then + // requires an aggregation that asks for one to fail with + // business.ErrAuditPercentileUnsupported, and every other one to succeed. + NoPercentile bool + // Reference, when set, is a second storage the answers are compared with, + // and AppendReference writes the same fixture into it. It is how a + // warehouse storage is held to the Postgres one. Leave both nil when the + // storage under test is the Postgres one. + Reference business.AuditStorage + AppendReference func(ctx context.Context, entry business.AuditEntry) error +} + +// Harness constructs the subject. New runs once per Run, on an empty store, and +// provisions whatever the storage needs to accept entries written now (for +// Postgres, the monthly partitions). +type Harness struct { + New func(t *testing.T) *Subject +} + +// fixture is the entries the suite appends and the ids that name its parts. +type fixture struct { + org, otherOrg string + actor, other string + // resource names this run's entries and no one else's. Harness.New asks for + // an empty store, but the Postgres one is append-only and shared with every + // other test of its package, so a read across organizations has to be pinned + // to what this suite wrote rather than to whatever the store already holds. + resource string + entries []business.AuditEntry + types business.AuditEventTypeIndex +} + +func entry(org, actor string, eventType business.EventType, resourceID string, at time.Time, payload map[string]any) business.AuditEntry { + return business.AuditEntry{ + ID: uuid.NewString(), + OrgID: org, + ActorID: actor, + ActorType: business.ActorTypeUser, + EventType: eventType, + SchemaVersion: 1, + Resource: "datasource", // replaced by the fixture's own, see newFixture + ResourceID: resourceID, + Payload: payload, + CreatedAt: at, + } +} + +func newFixture() *fixture { + f := &fixture{ + org: uuid.NewString(), otherOrg: uuid.NewString(), + actor: uuid.NewString(), other: uuid.NewString(), + resource: "contract-" + uuid.NewString(), + } + // Every instant falls in a partition EnsureAuditPartitions provisions: this + // month or the one before it. + base := time.Now().UTC().Truncate(time.Microsecond).Add(-time.Minute) + day := 24 * time.Hour + shared := base.Add(-5 * time.Hour) // two entries share this instant; the id breaks the tie + readPayload := func(boundary string, duration any, count any, outcome string) map[string]any { + payload := map[string]any{"boundary": boundary, "duration_ms": duration, "outcome": outcome} + if count != nil { + payload["result_count"] = count + } + return payload + } + f.entries = []business.AuditEntry{ + entry(f.org, f.actor, business.EventAuthLogin, "", base, map[string]any{"method": "password", "client_id": "cli"}), + entry(f.org, f.actor, business.EventDocumentRead, "doc-1", base.Add(-time.Hour), readPayload("b-1", 12.5, 3, "ok")), + entry(f.org, f.other, business.EventDocumentRead, "doc-1", base.Add(-2*time.Hour), readPayload("b-1", "7.25", 1, "ok")), + entry(f.org, f.other, business.EventDocumentRead, "doc-2", shared, readPayload("b-2", "fast", nil, "denied")), + entry(f.org, f.actor, business.EventDocumentRead, "doc-2", shared, readPayload("b-2", 0.5, 4, "ok")), + entry(f.org, "", business.EventOrgUpdated, "", base.Add(-26*time.Hour), map[string]any{"name": "Renamed", "slug": "renamed"}), + entry(f.org, f.actor, business.EventDocumentRead, "doc-3", base.Add(-3*day), map[string]any{ + "boundary": "b-1", "duration_ms": 100, "outcome": "ok", "result_count": 2, + "tags": []any{"x", map[string]any{"k": 1}}, "nested": map[string]any{"zz": 1, "a": "q"}, "label": "&é", + }), + entry(f.org, f.other, business.EventAuthLogin, "", base.Add(-9*day), map[string]any{"method": "passkey"}), + entry(f.org, f.actor, business.EventDocumentRead, "doc-1", base.Add(-12*day), readPayload("b-3", 2.25, 8, "ok")), + entry(f.org, f.actor, business.EventOrgUpdated, "", base.Add(-15*day), map[string]any{"name": "First"}), + entry(f.org, f.other, business.EventDocumentRead, "doc-4", base.Add(-20*day), map[string]any{}), + // Another organization, and the platform, which only the platform read sees. + entry(f.otherOrg, f.actor, business.EventDocumentRead, "doc-1", base.Add(-time.Hour), readPayload("b-1", 1, 1, "ok")), + entry(f.otherOrg, f.actor, business.EventAuthLogin, "", base.Add(-4*day), nil), + entry("", f.actor, business.EventAuthLogin, "", base.Add(-30*time.Minute), map[string]any{"method": "password"}), + } + impersonated := entry(f.org, f.actor, business.EventOrgUpdated, "", base.Add(-6*day), map[string]any{"name": "Acting"}) + impersonated.IsImpersonated, impersonated.ImpersonatedBy, impersonated.ClientID, impersonated.IPAddress = true, f.other, "client-1", "203.0.113.7" + f.entries = append(f.entries, impersonated) + for i := range f.entries { + f.entries[i].Resource = f.resource + } + + // The facts a storage outside Postgres takes from the registry's projection. + f.types = business.AuditEventTypeIndex{} + for _, definition := range business.AuditEventCatalog() { + f.types[string(definition.Type)] = business.AuditEventTypeFacts{ + Category: string(definition.Category), Namespace: definition.Namespace, + } + } + return f +} + +// Run runs the contract against the harness's storage. +func Run(t *testing.T, h Harness) { + ctx := context.Background() + subject := h.New(t) + require.NotNil(t, subject) + require.NotNil(t, subject.Storage) + require.NotNil(t, subject.Append) + require.Equal(t, subject.Reference == nil, subject.AppendReference == nil, "a reference needs both its storage and its writer") + + f := newFixture() + for _, e := range f.entries { + require.NoError(t, subject.Append(ctx, e)) + if subject.AppendReference != nil { + require.NoError(t, subject.AppendReference(ctx, e)) + } + } + if subject.AtLeastOnce { + // A delivery that was retried after its write succeeded. + require.NoError(t, subject.Append(ctx, f.entries[0])) + require.NoError(t, subject.Append(ctx, f.entries[5])) + } + + week := time.Now().Add(-7 * 24 * time.Hour) + recent := time.Now().Add(-3 * time.Hour) + queries := map[string]business.AuditQuery{ + "organization": {OrgID: f.org}, + "actor": {OrgID: f.org, ActorID: f.actor}, + "event type": {OrgID: f.org, EventType: string(business.EventDocumentRead)}, + "event type set": {OrgID: f.org, EventTypes: []string{string(business.EventAuthLogin), string(business.EventOrgUpdated)}}, + "type and set": {OrgID: f.org, EventType: string(business.EventAuthLogin), EventTypes: []string{string(business.EventOrgUpdated)}}, + "category": {OrgID: f.org, Category: "organization"}, + "namespace": {OrgID: f.org, Namespace: "saas"}, + "resource": {OrgID: f.org, Resource: f.resource, ResourceID: "doc-1"}, + "client": {OrgID: f.org, ClientID: "client-1"}, + "payload": {OrgID: f.org, PayloadContains: map[string]any{"boundary": "b-1"}}, + "payload number": {OrgID: f.org, PayloadContains: map[string]any{"result_count": 3.0}}, + "payload nested": {OrgID: f.org, PayloadContains: map[string]any{"nested": map[string]any{"a": "q"}, "tags": []any{"x"}}}, + "payload pair": {OrgID: f.org, PayloadContains: map[string]any{"boundary": "b-2", "outcome": "denied"}}, + "payload absent": {OrgID: f.org, PayloadContains: map[string]any{"outcome": nil}}, + "window": {OrgID: f.org, From: &week, To: &recent}, + "platform for actor": {ActorID: f.actor}, + // Every organization and the entries with none, which only the platform + // scope reads: pinned to this run's resource, see fixture.resource. + "platform": {Resource: f.resource}, + "other organization": {OrgID: f.otherOrg}, + } + + read := func(q business.AuditQuery) business.AuditRead { + return business.AuditRead{Scope: scopeOf(q), Query: q, Types: f.types} + } + + t.Run("list", func(t *testing.T) { + all, _, err := subject.Storage.List(ctx, read(business.AuditQuery{OrgID: f.org, PageSize: 100})) + require.NoError(t, err) + require.Len(t, all, 12, "the fixture is what the comparisons compare: not two empty answers") + + for name, q := range queries { + want, oracle := f.expected(q) + for _, size := range []int32{0, 1, 3} { + q := q + q.PageSize = size + var got []business.AuditEntry + var reference []business.AuditEntry + var token, referenceToken string + for pages := 0; ; pages++ { + require.Less(t, pages, 200, "%s, page size %d: the walk does not end", name, size) + q.PageToken = token + page, next, err := subject.Storage.List(ctx, read(q)) + require.NoError(t, err, name) + limit := int(size) + if limit == 0 { + limit = 50 + } + require.LessOrEqual(t, len(page), limit, "%s, page size %d", name, size) + got = append(got, page...) + if subject.Reference != nil { + q.PageToken = referenceToken + refPage, refNext, err := subject.Reference.List(ctx, read(q)) + require.NoError(t, err, name) + require.Equal(t, refPage, page, "%s, page size %d, page %d", name, size, pages) + require.Equal(t, refNext, next, "%s, page size %d, page %d: next token", name, size, pages) + reference, referenceToken = append(reference, refPage...), refNext + } + if next == "" { + break + } + token = next + } + if oracle { + require.Equal(t, ids(want), ids(got), "%s, page size %d", name, size) + } + ordered := sort.SliceIsSorted(got, func(i, j int) bool { + if !got[i].CreatedAt.Equal(got[j].CreatedAt) { + return got[i].CreatedAt.After(got[j].CreatedAt) + } + return got[i].ID > got[j].ID + }) + require.True(t, ordered, "%s, page size %d: newest first, ties by id descending", name, size) + require.Equal(t, len(ids(got)), len(unique(ids(got))), "%s, page size %d: an entry once", name, size) + } + } + }) + + t.Run("round trip", func(t *testing.T) { + got, _, err := subject.Storage.List(ctx, read(business.AuditQuery{OrgID: f.org, PageSize: 100})) + require.NoError(t, err) + byID := map[string]business.AuditEntry{} + for _, e := range got { + byID[e.ID] = e + } + for _, e := range f.entries { + if e.OrgID != f.org { + continue + } + stored, ok := byID[e.ID] + require.True(t, ok, "entry %s is listed", e.ID) + require.Equal(t, e.EventType, stored.EventType) + require.Equal(t, e.ActorType, stored.ActorType) + require.Equal(t, e.Resource, stored.Resource) + require.Equal(t, e.ResourceID, stored.ResourceID) + require.Equal(t, e.ActorID, stored.ActorID) + require.Equal(t, e.IsImpersonated, stored.IsImpersonated) + require.Equal(t, e.ImpersonatedBy, stored.ImpersonatedBy) + require.Equal(t, e.ClientID, stored.ClientID) + require.True(t, e.CreatedAt.Equal(stored.CreatedAt), "occurrence %s != %s", e.CreatedAt, stored.CreatedAt) + } + }) + + t.Run("aggregate", func(t *testing.T) { + for queryName, q := range queries { + for specName, spec := range aggregationSpecs() { + got, err := subject.Storage.Aggregate(ctx, read(q), spec) + if subject.NoPercentile && spec.HasPercentile() { + require.ErrorIs(t, err, business.ErrAuditPercentileUnsupported, "%s / %s", queryName, specName) + continue + } + require.NoError(t, err, "%s / %s", queryName, specName) + if queryName == "organization" { + require.NotEmpty(t, got, specName) + } + if subject.Reference != nil { + want, err := subject.Reference.Aggregate(ctx, read(q), spec) + require.NoError(t, err, "%s / %s", queryName, specName) + require.Equal(t, want, got, "%s / %s", queryName, specName) + } + } + // The default aggregation counts per event type: its buckets add up + // to exactly the entries the same query lists. + if expected, oracle := f.expected(q); oracle { + got, err := subject.Storage.Aggregate(ctx, read(q), business.AuditAggregationSpec{}) + require.NoError(t, err, queryName) + var total int64 + for _, bucket := range got { + total += bucket.Count + } + require.EqualValues(t, len(expected), total, "%s: the default aggregation counts the entries listed", queryName) + } + } + }) + + t.Run("scope", func(t *testing.T) { + _, _, err := subject.Storage.List(ctx, business.AuditRead{Query: business.AuditQuery{OrgID: f.org}}) + require.ErrorIs(t, err, business.ErrAuditReadUnscoped) + _, err = subject.Storage.Aggregate(ctx, business.AuditRead{}, business.AuditAggregationSpec{}) + require.ErrorIs(t, err, business.ErrAuditReadUnscoped) + _, _, err = subject.Storage.List(ctx, business.AuditRead{ + Scope: business.OrganizationAuditScope(f.org), Query: business.AuditQuery{OrgID: f.otherOrg}, + }) + require.Error(t, err, "a query naming another organization than its scope is refused") + _, _, err = subject.Storage.List(ctx, business.AuditRead{ + Scope: business.PlatformAuditScope(), Query: business.AuditQuery{OrgID: f.org}, + }) + require.Error(t, err, "the platform scope's query names no organization") + _, err = subject.Storage.Aggregate(ctx, business.AuditRead{ + Scope: business.OrganizationAuditScope(f.org), Query: business.AuditQuery{OrgID: f.org, CollectionID: "uncompiled"}, + }, business.AuditAggregationSpec{}) + require.Error(t, err, "a collection filter reaches a storage only compiled into a payload predicate") + }) +} + +// scopeOf is the scope a read of q is made in: its organization, or the +// platform's when it names none. +func scopeOf(q business.AuditQuery) business.AuditReadScope { + if q.OrgID == "" { + return business.PlatformAuditScope() + } + return business.OrganizationAuditScope(q.OrgID) +} + +func aggregationSpecs() map[string]business.AuditAggregationSpec { + return map[string]business.AuditAggregationSpec{ + "default": {}, + "category": {GroupBy: []string{"category"}}, + "actor": {GroupBy: []string{"actor"}, Metrics: []business.AuditMetric{{Op: "count"}, {Op: "count_distinct", Field: "resource_id"}}}, + "day": {GroupBy: []string{"time"}, Bucket: "day"}, + "week": {GroupBy: []string{"time"}, Bucket: "week", Metrics: []business.AuditMetric{{Op: "count_distinct", Field: "actor_id"}}}, + "month": {GroupBy: []string{"time"}, Bucket: "month"}, + "payload dimensions": {GroupBy: []string{"event_type", "payload:outcome"}, Metrics: []business.AuditMetric{ + {Op: "count_distinct", Field: "payload:boundary"}, + {Op: "count_distinct", Field: "category"}, + {Op: "count_distinct", Field: "event_type"}, + {Op: "count_distinct", Field: "resource"}, + }}, + "nested payload dimension": {GroupBy: []string{"payload:tags", "payload:nested", "payload:label"}}, + "numeric payload dimension": {GroupBy: []string{"payload:result_count"}, Metrics: []business.AuditMetric{ + {Op: "count_distinct", Field: "payload:result_count"}, + }}, + "fractional payload dimension": {GroupBy: []string{"payload:duration_ms"}}, + "metrics by type": {GroupBy: []string{"event_type"}, Metrics: []business.AuditMetric{ + {Op: "count", Alias: "events"}, + {Op: "sum", Field: "payload:duration_ms"}, + {Op: "avg", Field: "payload:result_count"}, + {Op: "min", Field: "payload:duration_ms"}, + {Op: "max", Field: "payload:result_count"}, + {Op: "percentile", Field: "payload:duration_ms", Percentile: 0.25, Alias: "p25"}, + {Op: "count_distinct", Field: "payload:outcome"}, + {Op: "count_distinct", Field: "category"}, + {Op: "count_distinct", Field: "actor_id"}, + }, Derived: []business.AuditDerivedMetric{ + {Alias: "duration_per_event", Numerator: "sum_duration_ms", Denominator: "events"}, + }}, + "category by month": {GroupBy: []string{"category", "time"}, Bucket: "month", Metrics: []business.AuditMetric{ + {Op: "count_distinct", Field: "resource_id"}, + }}, + "metrics": {GroupBy: []string{"payload:boundary"}, Metrics: []business.AuditMetric{ + {Op: "count", Alias: "reads"}, + {Op: "sum", Field: "payload:duration_ms"}, + {Op: "avg", Field: "payload:duration_ms"}, + {Op: "min", Field: "payload:duration_ms"}, + {Op: "max", Field: "payload:duration_ms"}, + {Op: "percentile", Field: "payload:duration_ms", Percentile: 0.5, Alias: "p50"}, + {Op: "percentile", Field: "payload:duration_ms", Percentile: 0.9, Alias: "p90"}, + {Op: "sum", Field: "payload:result_count", Alias: "results"}, + {Op: "max", Field: "payload:missing", Alias: "absent"}, + }, Derived: []business.AuditDerivedMetric{ + {Alias: "results_per_read", Numerator: "results", Denominator: "reads"}, + {Alias: "absent_ratio", Numerator: "absent", Denominator: "reads"}, + }}, + } +} + +// expected is the fixture's entries a read of q returns, newest first, and +// whether the suite's oracle can answer q at all: a payload predicate on a +// nested value or a null is left to the reference. +func (f *fixture) expected(q business.AuditQuery) ([]business.AuditEntry, bool) { + var out []business.AuditEntry + for _, e := range f.entries { + match, ok := f.matches(q, e) + if !ok { + return nil, false + } + if match { + out = append(out, e) + } + } + sort.Slice(out, func(i, j int) bool { + if !out[i].CreatedAt.Equal(out[j].CreatedAt) { + return out[i].CreatedAt.After(out[j].CreatedAt) + } + return out[i].ID > out[j].ID + }) + return out, true +} + +func (f *fixture) matches(q business.AuditQuery, e business.AuditEntry) (match, ok bool) { + if q.OrgID != "" && e.OrgID != q.OrgID { + return false, true + } + if q.ActorID != "" && e.ActorID != q.ActorID { + return false, true + } + if q.EventType != "" && string(e.EventType) != q.EventType { + return false, true + } + if len(q.EventTypes) > 0 { + in := false + for _, eventType := range q.EventTypes { + in = in || string(e.EventType) == eventType + } + if !in { + return false, true + } + } + facts := f.types[string(e.EventType)] + if q.Category != "" && facts.Category != q.Category { + return false, true + } + if q.Namespace != "" && facts.Namespace != q.Namespace { + return false, true + } + if q.Resource != "" && e.Resource != q.Resource { + return false, true + } + if q.ResourceID != "" && e.ResourceID != q.ResourceID { + return false, true + } + if q.ClientID != "" && e.ClientID != q.ClientID { + return false, true + } + if q.From != nil && e.CreatedAt.Before(*q.From) { + return false, true + } + if q.To != nil && e.CreatedAt.After(*q.To) { + return false, true + } + for key, want := range q.PayloadContains { + wantNumber, isNumber := number(want) + switch want.(type) { + case string, bool: + default: + if !isNumber { + return false, false // nested, a list or null: the reference answers it + } + } + have, present := e.Payload[key] + if !present { + return false, true + } + if isNumber { + haveNumber, ok := number(have) + if !ok || haveNumber != wantNumber { + return false, true + } + continue + } + if have != want { + return false, true + } + } + return true, true +} + +func number(value any) (float64, bool) { + switch v := value.(type) { + case int: + return float64(v), true + case int64: + return float64(v), true + case float64: + return v, true + default: + return 0, false + } +} + +func ids(entries []business.AuditEntry) []string { + out := make([]string, 0, len(entries)) + for _, e := range entries { + out = append(out, e.ID) + } + return out +} + +func unique(values []string) []string { + seen := map[string]bool{} + var out []string + for _, v := range values { + if !seen[v] { + seen[v] = true + out = append(out, v) + } + } + return out +} diff --git a/module/services/accounts/code/pkg/business/introspection.go b/module/services/accounts/code/pkg/business/introspection.go index 6b7fe3e2f..232757edf 100644 --- a/module/services/accounts/code/pkg/business/introspection.go +++ b/module/services/accounts/code/pkg/business/introspection.go @@ -73,6 +73,9 @@ var serviceInfo = &gen.ServiceInfo{ // options; descriptions remain editorial prose until source comments are // compiled into the service catalog. var rpcDescriptions = withWorkContextConsumerDescriptions(map[string]string{ + "AuditService/DeliverAuditEvents": "Deliver one final audit journal range under the installed module's delivery authority, committing a receipt.", + "AuditService/LookupDeliverAuditEvents": "Read the receipt of one audit delivery without appending any entries.", + "ModuleCapabilitiesService/ReadJournal": "Read a page of the cross-tenant journal under a cross-tenant module grant, without enqueueing work.", "DatasourceService/PruneSourceOperationReceipts": "Expire old source receipt output while retaining effect tombstones.", "DatasourceService/LookupPruneSourceOperationReceipts": "Read an authorized source receipt-cleanup result.", "DatasourceService/DelegateSourceTriggers": "Record the delegation a declared release's triggers run under for a source.", diff --git a/module/services/accounts/code/pkg/business/journal_read.go b/module/services/accounts/code/pkg/business/journal_read.go new file mode 100644 index 000000000..d66dc32a6 --- /dev/null +++ b/module/services/accounts/code/pkg/business/journal_read.go @@ -0,0 +1,397 @@ +package business + +import ( + "context" + "encoding/json" + "regexp" + "strconv" + "strings" + "time" + + "github.com/google/uuid" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// The cross-tenant journal read (ReadJournal on ModuleCapabilitiesService, ADR +// 0009). It is the read the platform's own reader tails: every tenant's journal +// rows, and the rows with no tenant, in an order a cursor can follow without +// skipping a row. +// +// # Position +// +// seq is the order rows were inserted, not the order their transactions commit, +// so a cursor on seq passes a row whose transaction is still open and never comes +// back for it. A position is therefore (xact_id, seq): the id of the transaction +// that wrote the row (migration 62), then seq. Rows are served in that order and +// only while xact_id is below the oldest transaction still running, +// pg_snapshot_xmin(pg_current_snapshot()). Every transaction that can still commit +// took its id at or after that bound, so a row below it belongs to a transaction +// that has finished, and no row that has not appeared yet can ever sort before one +// that has. A cursor that stops at the last row it served therefore skips nothing. +// +// The same gate holds the whole read behind a write transaction left open, and +// behind rows whose ids are ahead of the cluster after a logical restore into a +// new one. Both stall the reader without losing anything. + +// JournalPosition is where a row sits in the cross-tenant read order: the id of +// the transaction that wrote it, then its seq. A row written before migration 62 +// has no transaction id and sits at transaction 0, before every other row, in seq +// order. The zero value is before the first row. +type JournalPosition struct { + Xact uint64 + Seq int64 +} + +// IsZero reports whether the position is before the first row. +func (p JournalPosition) IsZero() bool { return p == JournalPosition{} } + +// Less reports whether p sorts before q. +func (p JournalPosition) Less(q JournalPosition) bool { + if p.Xact != q.Xact { + return p.Xact < q.Xact + } + return p.Seq < q.Seq +} + +// JournalStart is the position that means "before the oldest retained entry": a +// caller with nothing yet read names it as `after`, and DeliverAuditEvents takes it +// as `from`. It exists so the first page of a journal has a non-empty position to +// correlate on. No entry's position is ever this string (every entry's is +// "v1.."), and "" is accepted as an alias of it on a read. +const JournalStart = "start" + +const ( + journalPositionPrefix = "v1." + // journalPositionMaxLength bounds what a caller can make the host parse: + // "v1." and two decimals of at most 20 and 19 digits and a separator. + journalPositionMaxLength = 3 + 20 + 1 + 19 +) + +// String is the position's wire form: an opaque, versioned string that a caller +// stores and hands back unread. It is "v1.." in decimal. The zero +// position, which no entry has, is "" here; the caller-facing spelling of it is +// JournalStart. The version leads so that a later encoding is told apart from this +// one rather than misread as it. +func (p JournalPosition) String() string { + if p.IsZero() { + return "" + } + return journalPositionPrefix + strconv.FormatUint(p.Xact, 10) + "." + strconv.FormatInt(p.Seq, 10) +} + +// ParseJournalPosition reads a position back. JournalStart ("start") and the empty +// string are both the zero position. Anything else must be exactly what String writes: the "v1." prefix, a +// canonical unsigned decimal transaction id and a canonical non-negative decimal +// seq, with no sign, no leading zero, no space and nothing after. A malformed +// position is InvalidArgument, never read as "the beginning": a caller that +// mangled its cursor must be told, not silently sent back over the whole journal. +func ParseJournalPosition(raw string) (JournalPosition, error) { + if raw == "" || raw == JournalStart { + return JournalPosition{}, nil + } + invalid := func() (JournalPosition, error) { + return JournalPosition{}, status.Error(codes.InvalidArgument, "journal position is not one this host issued") + } + if len(raw) > journalPositionMaxLength || !strings.HasPrefix(raw, journalPositionPrefix) { + return invalid() + } + xact, seq, found := strings.Cut(strings.TrimPrefix(raw, journalPositionPrefix), ".") + if !found { + return invalid() + } + x, err := strconv.ParseUint(xact, 10, 64) + if err != nil { + return invalid() + } + s, err := strconv.ParseInt(seq, 10, 64) + if err != nil || s < 0 { + return invalid() + } + position := JournalPosition{Xact: x, Seq: s} + // Round trip: rejects the spellings strconv is lenient about (a leading + // "+", leading zeros) and the zero position spelled out, so one position has + // one spelling. + if position.String() != raw { + return invalid() + } + return position, nil +} + +// JournalRow is one row of the cross-tenant read: the event's identity, the +// tenant it belongs to ("" for none) and the producer's own bytes. +type JournalRow struct { + Position JournalPosition + EventID string + Type string + TenantID string + OccurredAt time.Time + ContentType string + Data []byte +} + +// JournalPageQuery selects rows of the cross-tenant read. Rows are those after +// After, up to Through when it is set, of one of Types, below the visibility +// bound, in position order. +type JournalPageQuery struct { + After JournalPosition + Through *JournalPosition // inclusive; nil reads to the visibility bound + Types []string + Limit int + // MaxBytes bounds the producers' bytes one page carries. The first row is + // always served, so a row larger than the bound still moves the cursor. + MaxBytes int +} + +// Limits of the read. +const ( + // ModuleJournalDefaultPage is the page size when the caller names none. + ModuleJournalDefaultPage = 100 + // ModuleJournalMaxPage is the largest page, and the widest range + // DeliverAuditEvents takes. + ModuleJournalMaxPage = 500 + // ModuleJournalMaxPageBytes bounds the producers' bytes in one page, below + // the transport's message limit once the data is a JSON object. + ModuleJournalMaxPageBytes = 2 << 20 + // ModuleJournalMaxWireBytes bounds the encoded response below 4 MiB. + ModuleJournalMaxWireBytes = 3 << 20 + moduleJournalMaxTypes = 16 + moduleJournalMaxBindings = 64 + moduleJournalMaxIDLength = 256 +) + +// journalTypePattern is the shape domain_events.type admits (its CHECK). +var journalTypePattern = regexp.MustCompile(`^[a-z][a-z0-9_]*(?:\.[a-z0-9_]+)+$`) + +// ModuleReadJournalInput is what a caller of ReadJournal asks for. +type ModuleReadJournalInput struct { + // After is the position the previous page ended at. JournalStart, or empty, + // reads from before the oldest retained entry. + After string + // Limit is the page size; 0 is ModuleJournalDefaultPage, and a larger value + // than ModuleJournalMaxPage is served as that maximum. + Limit int32 + // Types are the event types to read, exactly: there is no pattern. At least + // one is required, so no caller reads every tenant's every event by leaving + // the filter out. + Types []string + // DeliveryBindings are the binding ids the caller's own delivery runs under. + // They decide which entries are marked DeliveryMachinery. + DeliveryBindings []string +} + +// ModuleJournalEntry is one served row. +type ModuleJournalEntry struct { + Position string + EventID string + Type string + OrganizationID string // "" for a row with no tenant + OccurredAt time.Time + // Data is the event's JSON object. A row whose data is not a JSON object is + // served with an empty one: the platform's own events are JSON objects, and + // a producer's opaque bytes are not this read's to relay. + Data map[string]any + // DeliveryMachinery marks an entry this host recognizes as produced by the + // caller's own delivery of the journal, so the caller need not parse saas- + // starter's payloads to tell its own footprint from the work it carries. + DeliveryMachinery bool +} + +// ModuleReadJournalResult is a page and where to resume. +type ModuleReadJournalResult struct { + Entries []ModuleJournalEntry + // NextAfter is the last served entry's position, or the request's After when + // the page is empty. + NextAfter string +} + +// ModuleReadJournal serves the cross-tenant journal to a module principal whose +// grant is CrossTenant: the same gate as a system-scoped ModuleEmitAuditEvent. It +// reads on the control plane, so it serves every tenant's rows and the rows with +// no tenant, and it never enqueues anything: the read is not a replay, and no +// subscriber is touched. Rows are in position order and only below the visibility +// bound (see the file comment), so the cursor it returns never passes a +// transaction that can still commit. +func (s *Service) ModuleReadJournal(ctx context.Context, caller ModuleCaller, in ModuleReadJournalInput) (ModuleReadJournalResult, error) { + ctx, releaseAuthority, fenceErr := s.BeginModuleAuthorityUse(ctx) + if fenceErr != nil { + return ModuleReadJournalResult{}, fenceErr + } + defer releaseAuthority() + + grant, err := s.moduleCapability(ctx, caller) + if err != nil { + return ModuleReadJournalResult{}, err + } + if !grant.CrossTenant { + return ModuleReadJournalResult{}, status.Errorf(codes.PermissionDenied, + "principal %s may not read the journal of every tenant", caller.PrincipalID) + } + + after, err := ParseJournalPosition(in.After) + if err != nil { + return ModuleReadJournalResult{}, err + } + if in.Limit < 0 { + return ModuleReadJournalResult{}, status.Error(codes.InvalidArgument, "limit must not be negative") + } + limit := int(in.Limit) + if limit == 0 { + limit = ModuleJournalDefaultPage + } + if limit > ModuleJournalMaxPage { + limit = ModuleJournalMaxPage + } + types, err := journalReadTypes(in.Types) + if err != nil { + return ModuleReadJournalResult{}, err + } + bindings, err := journalDeliveryBindings(in.DeliveryBindings) + if err != nil { + return ModuleReadJournalResult{}, err + } + + var rows []JournalRow + if err := s.store.WithControlPlane(ctx, func(ctx context.Context) error { + var err error + rows, err = s.store.ReadJournalPage(ctx, JournalPageQuery{ + After: after, Types: types, Limit: limit, MaxBytes: ModuleJournalMaxPageBytes, + }) + return err + }); err != nil { + return ModuleReadJournalResult{}, status.Error(codes.Internal, "read the journal: "+err.Error()) + } + + out := ModuleReadJournalResult{NextAfter: in.After, Entries: make([]ModuleJournalEntry, 0, len(rows))} + for _, row := range rows { + out.Entries = append(out.Entries, ModuleJournalEntry{ + Position: row.Position.String(), + EventID: row.EventID, + Type: row.Type, + OrganizationID: row.TenantID, + OccurredAt: row.OccurredAt, + Data: journalDataObject(row.ContentType, row.Data), + DeliveryMachinery: JournalRowIsDeliveryMachinery(row, caller.PrincipalID, bindings), + }) + out.NextAfter = row.Position.String() + } + return out, nil +} + +func journalReadTypes(raw []string) ([]string, error) { + if len(raw) == 0 { + return nil, status.Error(codes.InvalidArgument, "at least one event type is required") + } + if len(raw) > moduleJournalMaxTypes { + return nil, status.Errorf(codes.InvalidArgument, "at most %d event types may be read at once", moduleJournalMaxTypes) + } + seen := make(map[string]struct{}, len(raw)) + out := make([]string, 0, len(raw)) + for _, t := range raw { + if len(t) > moduleJournalMaxIDLength || !journalTypePattern.MatchString(t) { + return nil, status.Errorf(codes.InvalidArgument, "%q is not an event type", t) + } + if _, dup := seen[t]; dup { + continue + } + seen[t] = struct{}{} + out = append(out, t) + } + return out, nil +} + +func journalDeliveryBindings(raw []string) (map[string]struct{}, error) { + if len(raw) > moduleJournalMaxBindings { + return nil, status.Errorf(codes.InvalidArgument, "at most %d delivery bindings may be named", moduleJournalMaxBindings) + } + out := make(map[string]struct{}, len(raw)) + for _, id := range raw { + if id == "" || len(id) > moduleJournalMaxIDLength { + return nil, status.Error(codes.InvalidArgument, "a delivery binding id is not empty and at most 256 bytes") + } + out[id] = struct{}{} + } + return out, nil +} + +// journalDataObject is the event's data as a JSON object, or an empty one when it +// is not. +func journalDataObject(contentType string, data []byte) map[string]any { + out := map[string]any{} + if contentType != "" && !strings.HasPrefix(strings.ToLower(contentType), "application/json") { + return out + } + if err := json.Unmarshal(data, &out); err != nil || out == nil { + return map[string]any{} + } + return out +} + +// JournalRowIsDeliveryMachinery reports whether the row carries an audit entry +// that is the footprint of the caller's own delivery of the journal: the audit the +// delivery itself generates, which, if it were delivered as ordinary work, would +// produce more of itself. It is one function so the rule is stated once. +// +// Only a row of type saas.audit.recorded.v1 carries an audit entry, and the rule +// reads that entry: +// +// - saas.module.operation_context_minted, whose binding_id is one of +// deliveryBindings: the headless Work Context a delivery runs under, issued +// for one of the caller's delivery bindings. +// - saas.module.delegated_audience_exchange, whose binding_id is one of +// deliveryBindings: an exchange on one of the caller's delivery bindings. +// - saas.module.work_context_minted whose actor is the caller's own module +// principal: the caller's own refresh of its Work Context. Another module's +// mint is that module's, not the caller's. +// +// Everything else is false, including a row of another type and a record that +// cannot be read: unmarked work is delivered, never skipped. +func JournalRowIsDeliveryMachinery(row JournalRow, callerPrincipal string, deliveryBindings map[string]struct{}) bool { + if row.Type != AuditRecordedEventType { + return false + } + entry, err := AuditEntryFromJournal(row.Data) + if err != nil { + return false + } + return AuditEntryIsDeliveryMachinery(entry, callerPrincipal, deliveryBindings) +} + +// AuditEntryIsDeliveryMachinery is the rule of JournalRowIsDeliveryMachinery on +// the audit entry itself. +func AuditEntryIsDeliveryMachinery(entry AuditEntry, callerPrincipal string, deliveryBindings map[string]struct{}) bool { + switch entry.EventType { + case EventModuleOperationContextMint, EventDelegatedAudienceExchange: + principal := entry.ActorID + if entry.EventType == EventDelegatedAudienceExchange { + principal, _ = entry.Payload["module_principal_id"].(string) + kind, _ := entry.Payload["binding_kind"].(string) + if kind != "operation" { + return false + } + } + if callerPrincipal == "" || principal == "" || canonicalPrincipalID(principal) != canonicalPrincipalID(callerPrincipal) { + return false + } + binding, ok := entry.Payload["binding_id"].(string) + if !ok || binding == "" { + return false + } + _, ours := deliveryBindings[binding] + return ours + case EventModuleWorkContextMint: + return callerPrincipal != "" && entry.ActorID != "" && + canonicalPrincipalID(entry.ActorID) == canonicalPrincipalID(callerPrincipal) + default: + return false + } +} + +// canonicalPrincipalID is a principal id in the one text form the audit trail +// records, so an id compares equal however it was spelled. +func canonicalPrincipalID(id string) string { + if parsed, err := uuid.Parse(id); err == nil { + return parsed.String() + } + return id +} diff --git a/module/services/accounts/code/pkg/business/journal_read_test.go b/module/services/accounts/code/pkg/business/journal_read_test.go new file mode 100644 index 000000000..bc6e1ab5b --- /dev/null +++ b/module/services/accounts/code/pkg/business/journal_read_test.go @@ -0,0 +1,297 @@ +package business_test + +import ( + "context" + "strings" + "testing" + "time" + + "accounts/pkg/business" + + "github.com/google/uuid" + "google.golang.org/grpc/codes" + "google.golang.org/protobuf/types/known/structpb" +) + +func TestJournalPositionEncoding(t *testing.T) { + // The encoding is versioned text, one spelling per position. + for _, p := range []business.JournalPosition{ + {Xact: 0, Seq: 7}, + {Xact: 1, Seq: 0}, + {Xact: 812, Seq: 99}, + {Xact: 1<<64 - 1, Seq: 1<<63 - 1}, + } { + got, err := business.ParseJournalPosition(p.String()) + if err != nil || got != p { + t.Errorf("round trip of %+v through %q = %+v, %v", p, p.String(), got, err) + } + } + if (business.JournalPosition{}).String() != "" { + t.Error("the zero position is the empty string") + } + for _, start := range []string{"", business.JournalStart} { + if got, err := business.ParseJournalPosition(start); err != nil || !got.IsZero() { + t.Errorf("%q is the beginning: %+v, %v", start, got, err) + } + } + if business.JournalStart != "start" { + t.Error("the start position is the literal \"start\"") + } + if (business.JournalPosition{Xact: 5, Seq: 9}).String() != "v1.5.9" { + t.Errorf("the wire form is v1..: %q", business.JournalPosition{Xact: 5, Seq: 9}.String()) + } + for _, malformed := range []string{ + "garbage", "Start", "START", "start ", "starts", "1.2", "v1", "v1.", "v1.1", "v1.1.", "v1..1", "v1.1.2.3", "v2.1.2", "V1.1.2", + "v1.-1.2", "v1.1.-2", "v1.+1.2", "v1.1.+2", "v1.01.2", "v1.1.02", "v1.0.0", " v1.1.2", "v1.1.2 ", + "v1.1.2\n", "v1.0x1.2", "v1.1.9223372036854775808", "v1.18446744073709551616.1", + "v1." + strings.Repeat("9", 200), "v1.1.2;DROP TABLE domain_events", + } { + _, err := business.ParseJournalPosition(malformed) + requireCode(t, err, codes.InvalidArgument) + } + if !(business.JournalPosition{Xact: 1, Seq: 9}).Less(business.JournalPosition{Xact: 2, Seq: 1}) || + !(business.JournalPosition{Xact: 2, Seq: 1}).Less(business.JournalPosition{Xact: 2, Seq: 2}) || + (business.JournalPosition{Xact: 2, Seq: 2}).Less(business.JournalPosition{Xact: 2, Seq: 2}) { + t.Error("positions order by transaction, then seq") + } +} + +// auditRow is a journal row of type saas.audit.recorded.v1 carrying a real audit +// entry, built by the publisher's own envelope. +func auditRow(t *testing.T, eventType business.EventType, actor string, payload map[string]any) business.JournalRow { + t.Helper() + entry := business.AuditEntry{ + ID: uuid.NewString(), ActorID: actor, ActorType: business.ActorTypeSystem, + EventType: eventType, Resource: "module", ResourceID: "runtime", + Payload: payload, CreatedAt: time.Date(2026, 10, 13, 1, 2, 3, 0, time.UTC), + } + envelope, err := business.NewAuditJournalEnvelope(entry) + if err != nil { + t.Fatal(err) + } + return business.JournalRow{ + EventID: envelope.Id, Type: envelope.Type, ContentType: envelope.Datacontenttype, Data: envelope.Data, + } +} + +// The delivery_machinery rule is one function. Each case is a row as the journal +// holds it, read from the caller's side: the caller is the module principal +// callerID, its delivery runs under binding delivery-binding. +func TestJournalRowIsDeliveryMachinery(t *testing.T) { + const ( + callerID = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + otherID = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" + ) + delivery := map[string]struct{}{"delivery-binding": {}} + exchange := func(binding any, outcome string) map[string]any { + return map[string]any{ + "module_principal_id": callerID, "binding_kind": "operation", "binding_id": binding, + "lookup": false, "outcome": outcome, "actor_principal_id": otherID, + } + } + mint := func(binding any) map[string]any { + return map[string]any{"prefix": "runtime", "tenant": "t", "binding_id": binding, "audience": "saas-audit"} + } + nonAudit := auditRow(t, business.EventModuleOperationContextMint, callerID, mint("delivery-binding")) + nonAudit.Type = "saas.something.else.v1" + + for _, tc := range []struct { + name string + row business.JournalRow + caller string + bindings map[string]struct{} + want bool + }{ + {"operation mint for a delivery binding", auditRow(t, business.EventModuleOperationContextMint, callerID, mint("delivery-binding")), callerID, delivery, true}, + {"another module's operation mint with the same binding id", auditRow(t, business.EventModuleOperationContextMint, otherID, mint("delivery-binding")), callerID, delivery, false}, + {"operation mint with no caller", auditRow(t, business.EventModuleOperationContextMint, callerID, mint("delivery-binding")), "", delivery, false}, + {"operation mint for another binding", auditRow(t, business.EventModuleOperationContextMint, callerID, mint("customer-binding")), callerID, delivery, false}, + {"operation mint with no binding", auditRow(t, business.EventModuleOperationContextMint, callerID, map[string]any{"prefix": "runtime"}), callerID, delivery, false}, + {"operation mint with a binding that is not text", auditRow(t, business.EventModuleOperationContextMint, callerID, mint(7)), callerID, delivery, false}, + {"operation mint and no delivery bindings named", auditRow(t, business.EventModuleOperationContextMint, callerID, mint("delivery-binding")), callerID, nil, false}, + {"exchange on a delivery binding, issued", auditRow(t, business.EventDelegatedAudienceExchange, otherID, exchange("delivery-binding", business.DelegatedAudienceExchangeIssued)), callerID, delivery, true}, + {"exchange on a delivery binding, refused", auditRow(t, business.EventDelegatedAudienceExchange, otherID, exchange("delivery-binding", business.DelegatedAudienceExchangeRefused)), callerID, delivery, true}, + {"another module's exchange with the same binding id", auditRow(t, business.EventDelegatedAudienceExchange, otherID, map[string]any{"module_principal_id": otherID, "binding_kind": "operation", "binding_id": "delivery-binding"}), callerID, delivery, false}, + {"source delegation with the same binding id", auditRow(t, business.EventDelegatedAudienceExchange, otherID, map[string]any{"module_principal_id": callerID, "binding_kind": "source_delegation", "binding_id": "delivery-binding"}), callerID, delivery, false}, + {"exchange on another binding", auditRow(t, business.EventDelegatedAudienceExchange, otherID, exchange("customer-binding", business.DelegatedAudienceExchangeIssued)), callerID, delivery, false}, + {"exchange with a binding that is not text", auditRow(t, business.EventDelegatedAudienceExchange, otherID, exchange(true, business.DelegatedAudienceExchangeIssued)), callerID, delivery, false}, + {"exchange and no delivery bindings named", auditRow(t, business.EventDelegatedAudienceExchange, otherID, exchange("delivery-binding", business.DelegatedAudienceExchangeIssued)), callerID, map[string]struct{}{}, false}, + {"work context mint of the caller's own principal", auditRow(t, business.EventModuleWorkContextMint, callerID, map[string]any{"prefix": "runtime", "tenant": "t"}), callerID, delivery, true}, + {"work context mint of the caller's own principal, spelled in capitals", auditRow(t, business.EventModuleWorkContextMint, strings.ToUpper(callerID), map[string]any{"prefix": "runtime"}), callerID, nil, true}, + {"work context mint of another module's principal", auditRow(t, business.EventModuleWorkContextMint, otherID, map[string]any{"prefix": "other"}), callerID, delivery, false}, + {"work context mint and no caller", auditRow(t, business.EventModuleWorkContextMint, callerID, map[string]any{"prefix": "runtime"}), "", delivery, false}, + {"work context mint with no actor", auditRow(t, business.EventModuleWorkContextMint, "", map[string]any{"prefix": "runtime"}), callerID, delivery, false}, + {"an ordinary audit entry", auditRow(t, business.EventMFATOTPSetupStarted, callerID, map[string]any{"binding_id": "delivery-binding"}), callerID, delivery, false}, + {"a row that is not an audit record", nonAudit, callerID, delivery, false}, + {"a record that cannot be read", business.JournalRow{Type: business.AuditRecordedEventType, Data: []byte("not json")}, callerID, delivery, false}, + {"a record that names no entry", business.JournalRow{Type: business.AuditRecordedEventType, Data: []byte("{}")}, callerID, delivery, false}, + } { + t.Run(tc.name, func(t *testing.T) { + if got := business.JournalRowIsDeliveryMachinery(tc.row, tc.caller, tc.bindings); got != tc.want { + t.Fatalf("delivery_machinery = %v, want %v", got, tc.want) + } + }) + } +} + +// journalStore answers the cross-tenant read from rows it holds and records what +// it was asked. +type journalStore struct { + fakeTxStore + rows []business.JournalRow + queries []business.JournalPageQuery + err error +} + +func (j *journalStore) ReadJournalPage(_ context.Context, q business.JournalPageQuery) ([]business.JournalRow, error) { + j.queries = append(j.queries, q) + if j.err != nil { + return nil, j.err + } + return j.rows, nil +} + +func newJournalService(t *testing.T, store business.Store, crossTenant bool) *business.Service { + t.Helper() + return newModuleServiceWithStore(t, store, &fakeJobBackend{}, crossTenant) +} + +// The read is the system-scoped gate of ModuleEmitAuditEvent: a principal bound to +// one tenant is refused before the store is asked anything, and so is a principal +// the registry does not name, and so is a caller naming none. +func TestModuleReadJournalRefusesWhoIsNotCrossTenant(t *testing.T) { + store := &journalStore{rows: []business.JournalRow{{EventID: "e1", Type: "saas.audit.recorded.v1"}}} + in := business.ModuleReadJournalInput{Types: []string{business.AuditRecordedEventType}} + + svc := newJournalService(t, store, false) + _, err := svc.ModuleReadJournal(context.Background(), moduleCaller(), in) + requireCode(t, err, codes.PermissionDenied) + + _, err = svc.ModuleReadJournal(context.Background(), business.ModuleCaller{PrincipalID: moduleUserA, BoundOrg: moduleTenantA}, in) + // Current admission treats an unregistered module as an invalid identity. + requireCode(t, err, codes.Unauthenticated) + if !strings.Contains(err.Error(), "not a registered module principal") { + t.Fatalf("unregistered identity refusal lost its reason: %v", err) + } + + _, err = svc.ModuleReadJournal(context.Background(), business.ModuleCaller{}, in) + requireCode(t, err, codes.Unauthenticated) + + // A bad request from a principal that may not read is still a refusal of the + // principal: the gate runs before the input is looked at. + _, err = svc.ModuleReadJournal(context.Background(), moduleCaller(), business.ModuleReadJournalInput{After: "garbage"}) + requireCode(t, err, codes.PermissionDenied) + + if len(store.queries) != 0 { + t.Fatalf("a refused caller reached the store: %+v", store.queries) + } +} + +func TestModuleReadJournalRefusesBadInput(t *testing.T) { + store := &journalStore{} + svc := newJournalService(t, store, true) + audit := []string{business.AuditRecordedEventType} + many := make([]string, 17) + for i := range many { + many[i] = "saas.test.type" + strings.Repeat("x", i) + } + bindings := make([]string, 65) + for i := range bindings { + bindings[i] = strings.Repeat("b", i+1) + } + for name, in := range map[string]business.ModuleReadJournalInput{ + "a malformed position": {After: "v1.1", Types: audit}, + "a negative limit": {Limit: -1, Types: audit}, + "no type": {}, + "a pattern, not a type": {Types: []string{"saas.audit.*"}}, + "an uppercase type": {Types: []string{"Saas.Audit"}}, + "a type that is one word": {Types: []string{"audit"}}, + "too many types": {Types: many}, + "an empty delivery binding": {Types: audit, DeliveryBindings: []string{""}}, + "too many bindings": {Types: audit, DeliveryBindings: bindings}, + "an oversized binding": {Types: audit, DeliveryBindings: []string{strings.Repeat("b", 257)}}, + } { + _, err := svc.ModuleReadJournal(context.Background(), moduleCaller(), in) + if err == nil { + t.Errorf("%s was served", name) + continue + } + requireCode(t, err, codes.InvalidArgument) + } + if len(store.queries) != 0 { + t.Fatalf("a refused request reached the store: %+v", store.queries) + } +} + +func TestModuleReadJournalServesEntriesAndResumes(t *testing.T) { + const caller = modulePrincSvc + minted := auditRow(t, business.EventModuleOperationContextMint, caller, map[string]any{"binding_id": "delivery-binding"}) + minted.Position = business.JournalPosition{Xact: 40, Seq: 3} + minted.OccurredAt = time.Date(2026, 10, 13, 1, 2, 3, 0, time.UTC) + ordinary := auditRow(t, business.EventMFATOTPSetupStarted, moduleUserA, map[string]any{"n": 1}) + ordinary.Position = business.JournalPosition{Xact: 41, Seq: 2} + ordinary.TenantID = moduleTenantA + opaque := business.JournalRow{ + Position: business.JournalPosition{Xact: 41, Seq: 5}, EventID: uuid.NewString(), Type: "saas.test.opaque", + ContentType: "application/octet-stream", Data: []byte{0xff, 0x00}, + } + + store := &journalStore{rows: []business.JournalRow{minted, ordinary, opaque}} + svc := newJournalService(t, store, true) + got, err := svc.ModuleReadJournal(context.Background(), moduleCaller(), business.ModuleReadJournalInput{ + After: "v1.39.1", Limit: 10_000, Types: []string{business.AuditRecordedEventType, "saas.test.opaque", business.AuditRecordedEventType}, + DeliveryBindings: []string{"delivery-binding"}, + }) + if err != nil { + t.Fatal(err) + } + + // What the store was asked: the position decoded, the limit held to the page + // maximum, the types exactly once each, a byte bound on the page. + if len(store.queries) != 1 { + t.Fatalf("queries = %+v", store.queries) + } + q := store.queries[0] + if q.After != (business.JournalPosition{Xact: 39, Seq: 1}) || q.Through != nil || + q.Limit != business.ModuleJournalMaxPage || q.MaxBytes != business.ModuleJournalMaxPageBytes || + strings.Join(q.Types, ",") != business.AuditRecordedEventType+",saas.test.opaque" { + t.Fatalf("query = %+v", q) + } + + if len(got.Entries) != 3 || got.NextAfter != "v1.41.5" { + t.Fatalf("entries = %d, next_after = %q", len(got.Entries), got.NextAfter) + } + first := got.Entries[0] + if first.Position != "v1.40.3" || first.EventID != minted.EventID || first.Type != business.AuditRecordedEventType || + first.OrganizationID != "" || !first.DeliveryMachinery || !first.OccurredAt.Equal(minted.OccurredAt) { + t.Fatalf("first = %+v", first) + } + if first.Data["event_type"] != string(business.EventModuleOperationContextMint) { + t.Fatalf("data is the event's JSON object: %+v", first.Data) + } + if _, err := structpb.NewStruct(first.Data); err != nil { + t.Fatalf("data does not fit a google.protobuf.Struct: %v", err) + } + if second := got.Entries[1]; second.OrganizationID != moduleTenantA || second.DeliveryMachinery { + t.Fatalf("second = %+v", second) + } + if third := got.Entries[2]; len(third.Data) != 0 || third.DeliveryMachinery { + t.Fatalf("an opaque payload is served as an empty object: %+v", third) + } + + // An empty page resumes where it was asked from, not from the beginning. + store.rows = nil + empty, err := svc.ModuleReadJournal(context.Background(), moduleCaller(), business.ModuleReadJournalInput{After: "v1.39.1", Types: []string{"saas.test.opaque"}}) + if err != nil || len(empty.Entries) != 0 || empty.NextAfter != "v1.39.1" || store.queries[1].Limit != business.ModuleJournalDefaultPage { + t.Fatalf("empty page = %+v, %v; query %+v", empty, err, store.queries[1]) + } + // From the beginning, spelled either way: the same query, and an empty page + // resumes where it was asked from. + for _, start := range []string{"", business.JournalStart} { + store.queries = nil + none, err := svc.ModuleReadJournal(context.Background(), moduleCaller(), business.ModuleReadJournalInput{After: start, Types: []string{"saas.test.opaque"}}) + if err != nil || none.NextAfter != start || len(store.queries) != 1 || !store.queries[0].After.IsZero() { + t.Fatalf("empty page from %q = %+v, %v; queries %+v", start, none, err, store.queries) + } + } +} diff --git a/module/services/accounts/code/pkg/business/module_authority_endpoint.go b/module/services/accounts/code/pkg/business/module_authority_endpoint.go index ba7893183..aec0eaf08 100644 --- a/module/services/accounts/code/pkg/business/module_authority_endpoint.go +++ b/module/services/accounts/code/pkg/business/module_authority_endpoint.go @@ -134,6 +134,7 @@ var moduleAuthorityProcedures = []string{ "/saas.accounts.v1.ModuleCapabilitiesService/PlaceRecord", "/saas.accounts.v1.ModuleCapabilitiesService/PublishEvent", "/saas.accounts.v1.ModuleCapabilitiesService/ReplayEvents", + "/saas.accounts.v1.ModuleCapabilitiesService/ReadJournal", "/saas.accounts.v1.ModuleCapabilitiesService/RequestApproval", "/saas.accounts.v1.ModuleCapabilitiesService/ResolveWorkContextAuthority", "/saas.accounts.v1.ModuleCapabilitiesService/Subscribe", diff --git a/module/services/accounts/code/pkg/business/retention.go b/module/services/accounts/code/pkg/business/retention.go index 54ec4a001..7d2cfca20 100644 --- a/module/services/accounts/code/pkg/business/retention.go +++ b/module/services/accounts/code/pkg/business/retention.go @@ -133,6 +133,23 @@ func (s *Service) RunRetention(ctx context.Context) (map[string]int64, error) { if err != nil { fail("data_retention_policies", RetentionStepLoad, err) } + // Until a committed delivery proves otherwise the journal is the audit + // entry's only copy. This fixed window is independent of audit_events policy + // and applies only when the warehouse is the store of record. + if s.auditReads != nil && s.auditReads.Backend() == AuditBackendWarehouse { + var removed, oldUnproven int64 + if err := s.store.WithControlPlane(ctx, func(ctx context.Context) error { + var err error + removed, oldUnproven, err = s.store.DeleteDeliveredAuditJournal(ctx, now.UTC().AddDate(0, 0, -30)) + return err + }); err != nil { + fail("audit_journal", RetentionStepRemove, err) + } else { + deleted["audit_journal"] = removed + w.Info("retention: audit journal delivery coverage", + wool.Field("deleted", removed), wool.Field("old_unproven", oldUnproven), wool.Field("retention_days", 30)) + } + } for _, p := range policies { before, anyOlder, windowErr := retentionCutoff(now, p.RetentionDays) if windowErr != nil { @@ -143,6 +160,15 @@ func (s *Service) RunRetention(ctx context.Context) (map[string]int64, error) { var count int64 var removeErr error switch { + case p.ResourceType == "audit_events" && s.auditReads != nil && s.auditReads.Backend() == AuditBackendWarehouse: + // Under the warehouse backend audit_events is no longer the store of + // record and nothing writes it, so this policy has nothing to retire + // there. How long the warehouse keeps an entry is the warehouse's own + // expiry, set where its table is provisioned, not a partition drop + // here. Rows written before the switch are left where they are. + w.Info("retention: audit_events policy skipped: the audit backend is warehouse, whose expiry governs retention; audit_events is not written and is left as it is", + wool.Field("retention_days", p.RetentionDays)) + continue case !anyOlder: // Nothing is older than the cutoff. case p.ResourceType == "audit_events": diff --git a/module/services/accounts/code/pkg/business/retention_test.go b/module/services/accounts/code/pkg/business/retention_test.go index c5e252b25..ad338bea9 100644 --- a/module/services/accounts/code/pkg/business/retention_test.go +++ b/module/services/accounts/code/pkg/business/retention_test.go @@ -25,6 +25,7 @@ type retentionStoreFake struct { ensured []bool cutoffs map[string][]time.Time auditDrops []bool + journalCalls []bool } // retentionDatabaseNow is the database's clock in these tests: a fixed instant, @@ -92,6 +93,12 @@ func (f *retentionStoreFake) DeleteOldNotifications(_ context.Context, before ti return f.remove("notifications", before) } +func (f *retentionStoreFake) DeleteDeliveredAuditJournal(_ context.Context, before time.Time) (int64, int64, error) { + f.journalCalls = append(f.journalCalls, f.inControlPlane) + removed, err := f.remove("audit_journal", before) + return removed, 2, err +} + var retainedResourceTypes = []string{"audit_events", "sessions", "webhook_deliveries", "notifications"} func retentionPolicies(days int) []*business.RetentionPolicy { @@ -218,3 +225,70 @@ func TestRetentionDropsAuditPartitionsOutsideTheProvisioningTransaction(t *testi require.Equal(t, []bool{true}, store.ensured, "partitions are provisioned in a control-plane transaction") require.Equal(t, []bool{false}, store.auditDrops, "the drop runs in a transaction of its own") } + +// Under the warehouse backend audit_events is not the store of record and +// nothing writes it, so its policy is skipped — no partition is dropped — while +// every other resource is retained as usual. +func TestRetentionSkipsAuditEventsUnderTheWarehouseBackend(t *testing.T) { + store := newRetentionStoreFake(retentionPolicies(365)...) + service, err := business.NewService(store) + require.NoError(t, err) + audit, err := business.NewAuditService(business.AuditServiceConfig{ + Backend: business.AuditBackendWarehouse, Store: store, Producer: idleProducer{}, + Transport: &journalRecorder{}, Warehouse: noWarehouse{}, + }) + require.NoError(t, err) + service.SetAuditService(audit) + + deleted, err := service.RunRetention(context.Background()) + require.NoError(t, err) + require.Empty(t, store.auditDrops, "no audit partition is dropped under the warehouse backend") + require.NotContains(t, deleted, "audit_events") + require.Equal(t, int64(1), deleted["sessions"], "the other resources are retained as usual") + require.Equal(t, []bool{true}, store.ensured) + require.Equal(t, []bool{true}, store.journalCalls) + require.Equal(t, []time.Time{retentionDatabaseNow.UTC().AddDate(0, 0, -30)}, store.cutoffs["audit_journal"]) + require.EqualValues(t, 1, deleted["audit_journal"]) +} + +// Under Postgres, the default, audit_events retention is exactly what it was. +func TestRetentionDropsAuditPartitionsUnderThePostgresBackend(t *testing.T) { + store := newRetentionStoreFake(retentionPolicies(30)...) + deleted, err := runRetention(t, store) + require.NoError(t, err) + require.Len(t, store.auditDrops, 1) + require.Equal(t, int64(1), deleted["audit_events"]) + require.Empty(t, store.journalCalls, "Postgres mode never retires the journal") +} + +func TestWarehouseJournalRetentionRunsWithoutAPolicyAndReportsFailures(t *testing.T) { + for _, failed := range []bool{false, true} { + t.Run(map[bool]string{false: "without a policy", true: "store failure"}[failed], func(t *testing.T) { + store := newRetentionStoreFake() + if failed { + store.removeErr["audit_journal"] = errors.New("receipt read failed") + } + svc, err := business.NewService(store) + require.NoError(t, err) + audit, err := business.NewAuditService(business.AuditServiceConfig{ + Backend: business.AuditBackendWarehouse, Store: store, Producer: idleProducer{}, + Transport: &journalRecorder{}, Warehouse: noWarehouse{}, + }) + require.NoError(t, err) + svc.SetAuditService(audit) + deleted, err := svc.RunRetention(context.Background()) + if failed { + var failure *business.RetentionError + require.ErrorAs(t, err, &failure) + require.Len(t, failure.Failures, 1) + require.Equal(t, "audit_journal", failure.Failures[0].Resource) + require.NotContains(t, deleted, "audit_journal") + } else { + require.NoError(t, err) + require.EqualValues(t, 1, deleted["audit_journal"]) + } + require.Equal(t, []bool{true}, store.journalCalls) + require.Equal(t, []time.Time{retentionDatabaseNow.UTC().AddDate(0, 0, -30)}, store.cutoffs["audit_journal"]) + }) + } +} diff --git a/module/services/accounts/code/pkg/business/rpc_policy_inventory_test.go b/module/services/accounts/code/pkg/business/rpc_policy_inventory_test.go index bf831deea..3d44b0d59 100644 --- a/module/services/accounts/code/pkg/business/rpc_policy_inventory_test.go +++ b/module/services/accounts/code/pkg/business/rpc_policy_inventory_test.go @@ -73,6 +73,7 @@ func TestRPCPolicyInventoryIsCompleteAndClassified(t *testing.T) { "/saas.accounts.v1.ModuleCapabilitiesService/Unsubscribe", "/saas.accounts.v1.ModuleCapabilitiesService/ListSubscriptions", "/saas.accounts.v1.ModuleCapabilitiesService/ReplayEvents", + "/saas.accounts.v1.ModuleCapabilitiesService/ReadJournal", "/saas.accounts.v1.PermissionService/CheckAccess", "/saas.accounts.v1.PermissionService/CheckPermission", "/saas.accounts.v1.PermissionService/Decide", diff --git a/module/services/accounts/code/pkg/business/service.go b/module/services/accounts/code/pkg/business/service.go index cbf7602c7..5858e4760 100644 --- a/module/services/accounts/code/pkg/business/service.go +++ b/module/services/accounts/code/pkg/business/service.go @@ -48,6 +48,7 @@ type Service struct { orgCreationPolicy OrganizationCreationPolicy audit AuditEmitter auditTx TxAuditEmitter // set when audit also writes on the caller's tx; required in production + auditReads *AuditService // selected at startup for list, aggregation and export entitlements EntitlementChecker membership MembershipInvalidator slack *SlackNotifier // optional: sends critical notifications to Slack @@ -486,7 +487,14 @@ func NewService(store Store) (*Service, error) { return nil, err } authority, _ := store.(ModuleAuthorityStore) + var auditReads *AuditService + if store != nil { + if auditReads, err = newAuditReads(store); err != nil { + return nil, err + } + } return &Service{ + auditReads: auditReads, store: store, moduleAuthority: authority, acquisitionMode: gen.AcquisitionMode_ACQUISITION_MODE_OPEN_SIGNUP, @@ -705,6 +713,15 @@ func (s *Service) SetAuditEmitter(a AuditEmitter) { s.auditTx, _ = a.(TxAuditEmitter) } +// SetAuditService wires the AuditService startup built: the emitter every audited +// write goes through and the storage every audit read comes from. It replaces +// SetAuditEmitter for the process, so the write and the read can never name two +// different stores. +func (s *Service) SetAuditService(a *AuditService) { + s.SetAuditEmitter(a) + s.auditReads = a +} + // VerifyAuditWiring fails startup when the wired audit emitter cannot write on // the caller's transaction. Every security mutation commits its audit row and // webhook fan-out inside its own transaction; an emitter without EmitTx would diff --git a/module/services/accounts/code/pkg/business/source_operation_receipts.go b/module/services/accounts/code/pkg/business/source_operation_receipts.go index 5ebdf8f4e..47633b60a 100644 --- a/module/services/accounts/code/pkg/business/source_operation_receipts.go +++ b/module/services/accounts/code/pkg/business/source_operation_receipts.go @@ -36,7 +36,7 @@ func (s *Service) ConfigureSourceOperationReceipts(store receipts.Store) error { if err != nil { return err } - for _, method := range []string{SourceOperationMethod, SourceReceiptRetentionMethod, ReconcileSourceMethod} { + for _, method := range []string{SourceOperationMethod, SourceReceiptRetentionMethod, ReconcileSourceMethod, DeliverAuditEventsMethod} { if !guard.IsOperation(method) { // Checked at configuration rather than on the first call: an // operation whose marking is missing is a method the SDK will not diff --git a/module/services/accounts/code/pkg/business/store.go b/module/services/accounts/code/pkg/business/store.go index f86c20b6d..676bfb5ee 100644 --- a/module/services/accounts/code/pkg/business/store.go +++ b/module/services/accounts/code/pkg/business/store.go @@ -897,6 +897,26 @@ type Store interface { // skipped rather than leaving it to believe it is caught up. ResolveTenantJournalCursor(ctx context.Context, orgID, eventID string) (seq int64, resolved bool, err error) + // The cross-tenant journal, for the platform's own reader (ModuleReadJournal, + // DeliverAuditEvents). All three run under WithControlPlane and refuse to run + // outside a transaction: they read every tenant's rows and the rows with no + // tenant, so the tenant floor is not beneath them and the caller's gate is the + // only one. + // + // ReadJournalPage returns one page of rows after q.After, in (xact_id, seq) + // order, of the types q names, and only rows whose writing transaction is below + // the oldest transaction still running (pg_snapshot_xmin), so a cursor that + // stops at the last row it was given never passes a transaction that can still + // commit. It carries the producers' bytes, at most q.MaxBytes of them beyond + // the first row. + ReadJournalPage(ctx context.Context, q JournalPageQuery) ([]JournalRow, error) + // CountJournalRange counts the rows ReadJournalPage would serve for q with no + // page limit, stopping at limit: the answer is at most limit. + CountJournalRange(ctx context.Context, q JournalPageQuery, limit int) (int, error) + // JournalPositionVisible reports whether every row at or before pos is final: + // pos's transaction is below the oldest transaction still running. + JournalPositionVisible(ctx context.Context, pos JournalPosition) (bool, error) + // MFA — exposed on the main Store interface so the auth layer's // requireMFA gate can check enrollment without casting to MFAStore. HasVerifiedMFA(ctx context.Context, userID string) (bool, error) @@ -926,6 +946,10 @@ type Store interface { // DatabaseNow reads the database's clock. Retention counts every cutoff // from it rather than from this process's clock, which may run ahead. DatabaseNow(ctx context.Context) (time.Time, error) + // DeleteDeliveredAuditJournal removes old audit journal rows only within + // committed delivery ranges. It requires a control-plane transaction and + // returns deleted rows and old rows whose delivery remains unproven. + DeleteDeliveredAuditJournal(ctx context.Context, before time.Time) (deleted, oldUnproven int64, err error) DeleteOldSessions(ctx context.Context, before time.Time) (int64, error) DeleteOldWebhookDeliveries(ctx context.Context, before time.Time) (int64, error) DeleteOldNotifications(ctx context.Context, before time.Time) (int64, error) diff --git a/module/services/accounts/code/pkg/cataloggen/authz_methods_test.go b/module/services/accounts/code/pkg/cataloggen/authz_methods_test.go index 40ee4bcc4..48cd1c83f 100644 --- a/module/services/accounts/code/pkg/cataloggen/authz_methods_test.go +++ b/module/services/accounts/code/pkg/cataloggen/authz_methods_test.go @@ -41,18 +41,11 @@ func TestAuthorizationCatalogCompilationAndPolicyProjection(t *testing.T) { require.Contains(t, method.GetProcedure(), "Complete") } } - // Artifact approval/authorization/revocation and current installation lookup - // are internal module capabilities, never public caller endpoints; so is the - // declared-presence RPC this branch adds. Base 49, main +4, this branch +1; - // then -2 when MintModuleRegistration and MintSolutionRegistration were - // deleted with their secrets (#1056), and +1 for the live-authority read a - // callee's verifier asks (#1107), +1 for the issuer's admission of a - // bearer-less module route's capability (AdmitWorkloadOperation), and +1 - // for the composition interface's ApplyManifest. Picking either side's - // number would be silently wrong, so this is read off the regenerated - // generated/authz-methods.json. - require.Equal(t, 55, internalCount) // ApplyManifest now authenticates its signed carrier on the tenant listener - require.Equal(t, 28, failClosedCount) // +3: the artifact interface's Approve, Authorize and Revoke, authenticated by the module's workload credential; +1: signed composition delivery; +2: the session interface's SignIn and ListFixturePeople are rate-limited as authentication and fail closed; +4: the authority interface's workload-credential calls (RegisterAdmissionKey, AttachResource, DetachResource, AuthorizeTransfer); +1: the installation interface's Current, authenticated by the module's workload credential + // ReadJournal extends the existing internal capability catalog. + require.Equal(t, 56, internalCount) + require.Equal(t, policyv1.Exposure_EXPOSURE_INTERNAL, + methodByProcedure(catalog, "/saas.accounts.v1.ModuleCapabilitiesService/ReadJournal").GetPolicy().GetExposure()) + require.Equal(t, 28, failClosedCount) require.Equal(t, 2, factorAttemptCount) require.Equal( t, diff --git a/module/services/accounts/code/pkg/cataloggen/testdata/mesh-policy.golden.yaml b/module/services/accounts/code/pkg/cataloggen/testdata/mesh-policy.golden.yaml index 9c85361fc..690459892 100644 --- a/module/services/accounts/code/pkg/cataloggen/testdata/mesh-policy.golden.yaml +++ b/module/services/accounts/code/pkg/cataloggen/testdata/mesh-policy.golden.yaml @@ -59,6 +59,7 @@ spec: - /saas.accounts.v1.ModuleCapabilitiesService/NotifyUser - /saas.accounts.v1.ModuleCapabilitiesService/PlaceRecord - /saas.accounts.v1.ModuleCapabilitiesService/PublishEvent + - /saas.accounts.v1.ModuleCapabilitiesService/ReadJournal - /saas.accounts.v1.ModuleCapabilitiesService/ReplayEvents - /saas.accounts.v1.ModuleCapabilitiesService/RequestApproval - /saas.accounts.v1.ModuleCapabilitiesService/ResolveWorkContextAuthority diff --git a/module/services/accounts/code/pkg/eventcatalog/catalog_gen.go b/module/services/accounts/code/pkg/eventcatalog/catalog_gen.go index df6c60ae0..c91fa5f43 100644 --- a/module/services/accounts/code/pkg/eventcatalog/catalog_gen.go +++ b/module/services/accounts/code/pkg/eventcatalog/catalog_gen.go @@ -38,6 +38,7 @@ var published = [...]PublishedEvent{ {Type: "saas.approval.denied", Namespace: "saas", Schema: "saas/events/v1/events.proto#EventEnvelope", Major: 1, Visibility: "external", Partition: "", Retention: "30d"}, {Type: "saas.approval.escalated", Namespace: "saas", Schema: "saas/events/v1/events.proto#EventEnvelope", Major: 1, Visibility: "external", Partition: "", Retention: "30d"}, {Type: "saas.approval.timeout", Namespace: "saas", Schema: "saas/events/v1/events.proto#EventEnvelope", Major: 1, Visibility: "external", Partition: "", Retention: "30d"}, + {Type: "saas.audit.recorded.v1", Namespace: "saas", Schema: "saas/events/v1/events.proto#EventEnvelope", Major: 1, Visibility: "internal", Partition: "", Retention: "30d"}, {Type: "saas.auth.client_authorized", Namespace: "saas", Schema: "saas/events/v1/events.proto#EventEnvelope", Major: 1, Visibility: "external", Partition: "", Retention: "30d"}, {Type: "saas.auth.login", Namespace: "saas", Schema: "saas/events/v1/events.proto#EventEnvelope", Major: 1, Visibility: "external", Partition: "", Retention: "30d"}, {Type: "saas.auth.magic_link_login", Namespace: "saas", Schema: "saas/events/v1/events.proto#EventEnvelope", Major: 1, Visibility: "external", Partition: "", Retention: "30d"}, diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/audit.connect.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/audit.connect.go index c392f94a1..197ded7cb 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/audit.connect.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/audit.connect.go @@ -46,6 +46,12 @@ const ( // AuditServiceExportAuditLogProcedure is the fully-qualified name of the AuditService's // ExportAuditLog RPC. AuditServiceExportAuditLogProcedure = "/saas.accounts.v1.AuditService/ExportAuditLog" + // AuditServiceDeliverAuditEventsProcedure is the fully-qualified name of the AuditService's + // DeliverAuditEvents RPC. + AuditServiceDeliverAuditEventsProcedure = "/saas.accounts.v1.AuditService/DeliverAuditEvents" + // AuditServiceLookupDeliverAuditEventsProcedure is the fully-qualified name of the AuditService's + // LookupDeliverAuditEvents RPC. + AuditServiceLookupDeliverAuditEventsProcedure = "/saas.accounts.v1.AuditService/LookupDeliverAuditEvents" ) // AuditServiceClient is a client for the saas.accounts.v1.AuditService service. @@ -54,6 +60,17 @@ type AuditServiceClient interface { AggregateAuditLog(context.Context, *connect.Request[v1.AggregateAuditLogRequest]) (*connect.Response[v1.AggregateAuditLogResponse], error) ListAuditEventTypes(context.Context, *connect.Request[v1.ListAuditEventTypesRequest]) (*connect.Response[v1.ListAuditEventTypesResponse], error) ExportAuditLog(context.Context, *connect.Request[v1.ExportAuditLogRequest]) (*connect.Response[v1.ExportAuditLogResponse], error) + // DeliverAuditEvents appends one range of the journal's audit records to the + // warehouse audit storage: the operation module-runtime calls after reading the + // range through ReadJournal. It is an operation, not a sweep. It takes each + // record's organization from the journal row and never from the caller, emits no + // audit of its own, and commits one receipt per range under the installation's + // tenant; delivery is at least once, and every read of the storage returns each + // entry once by id. Its receipt is read back through LookupDeliverAuditEvents. + DeliverAuditEvents(context.Context, *connect.Request[v1.DeliverAuditEventsRequest]) (*connect.Response[v1.DeliverAuditEventsResponse], error) + // LookupDeliverAuditEvents takes the invocation's same {from, to} body and + // derives the range receipt identity from from. It never delivers. + LookupDeliverAuditEvents(context.Context, *connect.Request[v1.DeliverAuditEventsRequest]) (*connect.Response[v1.DeliverAuditEventsResponse], error) } // NewAuditServiceClient constructs a client for the saas.accounts.v1.AuditService service. By @@ -91,15 +108,29 @@ func NewAuditServiceClient(httpClient connect.HTTPClient, baseURL string, opts . connect.WithSchema(auditServiceMethods.ByName("ExportAuditLog")), connect.WithClientOptions(opts...), ), + deliverAuditEvents: connect.NewClient[v1.DeliverAuditEventsRequest, v1.DeliverAuditEventsResponse]( + httpClient, + baseURL+AuditServiceDeliverAuditEventsProcedure, + connect.WithSchema(auditServiceMethods.ByName("DeliverAuditEvents")), + connect.WithClientOptions(opts...), + ), + lookupDeliverAuditEvents: connect.NewClient[v1.DeliverAuditEventsRequest, v1.DeliverAuditEventsResponse]( + httpClient, + baseURL+AuditServiceLookupDeliverAuditEventsProcedure, + connect.WithSchema(auditServiceMethods.ByName("LookupDeliverAuditEvents")), + connect.WithClientOptions(opts...), + ), } } // auditServiceClient implements AuditServiceClient. type auditServiceClient struct { - queryAuditLog *connect.Client[v1.QueryAuditLogRequest, v1.QueryAuditLogResponse] - aggregateAuditLog *connect.Client[v1.AggregateAuditLogRequest, v1.AggregateAuditLogResponse] - listAuditEventTypes *connect.Client[v1.ListAuditEventTypesRequest, v1.ListAuditEventTypesResponse] - exportAuditLog *connect.Client[v1.ExportAuditLogRequest, v1.ExportAuditLogResponse] + queryAuditLog *connect.Client[v1.QueryAuditLogRequest, v1.QueryAuditLogResponse] + aggregateAuditLog *connect.Client[v1.AggregateAuditLogRequest, v1.AggregateAuditLogResponse] + listAuditEventTypes *connect.Client[v1.ListAuditEventTypesRequest, v1.ListAuditEventTypesResponse] + exportAuditLog *connect.Client[v1.ExportAuditLogRequest, v1.ExportAuditLogResponse] + deliverAuditEvents *connect.Client[v1.DeliverAuditEventsRequest, v1.DeliverAuditEventsResponse] + lookupDeliverAuditEvents *connect.Client[v1.DeliverAuditEventsRequest, v1.DeliverAuditEventsResponse] } // QueryAuditLog calls saas.accounts.v1.AuditService.QueryAuditLog. @@ -122,12 +153,33 @@ func (c *auditServiceClient) ExportAuditLog(ctx context.Context, req *connect.Re return c.exportAuditLog.CallUnary(ctx, req) } +// DeliverAuditEvents calls saas.accounts.v1.AuditService.DeliverAuditEvents. +func (c *auditServiceClient) DeliverAuditEvents(ctx context.Context, req *connect.Request[v1.DeliverAuditEventsRequest]) (*connect.Response[v1.DeliverAuditEventsResponse], error) { + return c.deliverAuditEvents.CallUnary(ctx, req) +} + +// LookupDeliverAuditEvents calls saas.accounts.v1.AuditService.LookupDeliverAuditEvents. +func (c *auditServiceClient) LookupDeliverAuditEvents(ctx context.Context, req *connect.Request[v1.DeliverAuditEventsRequest]) (*connect.Response[v1.DeliverAuditEventsResponse], error) { + return c.lookupDeliverAuditEvents.CallUnary(ctx, req) +} + // AuditServiceHandler is an implementation of the saas.accounts.v1.AuditService service. type AuditServiceHandler interface { QueryAuditLog(context.Context, *connect.Request[v1.QueryAuditLogRequest]) (*connect.Response[v1.QueryAuditLogResponse], error) AggregateAuditLog(context.Context, *connect.Request[v1.AggregateAuditLogRequest]) (*connect.Response[v1.AggregateAuditLogResponse], error) ListAuditEventTypes(context.Context, *connect.Request[v1.ListAuditEventTypesRequest]) (*connect.Response[v1.ListAuditEventTypesResponse], error) ExportAuditLog(context.Context, *connect.Request[v1.ExportAuditLogRequest]) (*connect.Response[v1.ExportAuditLogResponse], error) + // DeliverAuditEvents appends one range of the journal's audit records to the + // warehouse audit storage: the operation module-runtime calls after reading the + // range through ReadJournal. It is an operation, not a sweep. It takes each + // record's organization from the journal row and never from the caller, emits no + // audit of its own, and commits one receipt per range under the installation's + // tenant; delivery is at least once, and every read of the storage returns each + // entry once by id. Its receipt is read back through LookupDeliverAuditEvents. + DeliverAuditEvents(context.Context, *connect.Request[v1.DeliverAuditEventsRequest]) (*connect.Response[v1.DeliverAuditEventsResponse], error) + // LookupDeliverAuditEvents takes the invocation's same {from, to} body and + // derives the range receipt identity from from. It never delivers. + LookupDeliverAuditEvents(context.Context, *connect.Request[v1.DeliverAuditEventsRequest]) (*connect.Response[v1.DeliverAuditEventsResponse], error) } // NewAuditServiceHandler builds an HTTP handler from the service implementation. It returns the @@ -161,6 +213,18 @@ func NewAuditServiceHandler(svc AuditServiceHandler, opts ...connect.HandlerOpti connect.WithSchema(auditServiceMethods.ByName("ExportAuditLog")), connect.WithHandlerOptions(opts...), ) + auditServiceDeliverAuditEventsHandler := connect.NewUnaryHandler( + AuditServiceDeliverAuditEventsProcedure, + svc.DeliverAuditEvents, + connect.WithSchema(auditServiceMethods.ByName("DeliverAuditEvents")), + connect.WithHandlerOptions(opts...), + ) + auditServiceLookupDeliverAuditEventsHandler := connect.NewUnaryHandler( + AuditServiceLookupDeliverAuditEventsProcedure, + svc.LookupDeliverAuditEvents, + connect.WithSchema(auditServiceMethods.ByName("LookupDeliverAuditEvents")), + connect.WithHandlerOptions(opts...), + ) return "/saas.accounts.v1.AuditService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case AuditServiceQueryAuditLogProcedure: @@ -171,6 +235,10 @@ func NewAuditServiceHandler(svc AuditServiceHandler, opts ...connect.HandlerOpti auditServiceListAuditEventTypesHandler.ServeHTTP(w, r) case AuditServiceExportAuditLogProcedure: auditServiceExportAuditLogHandler.ServeHTTP(w, r) + case AuditServiceDeliverAuditEventsProcedure: + auditServiceDeliverAuditEventsHandler.ServeHTTP(w, r) + case AuditServiceLookupDeliverAuditEventsProcedure: + auditServiceLookupDeliverAuditEventsHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -195,3 +263,11 @@ func (UnimplementedAuditServiceHandler) ListAuditEventTypes(context.Context, *co func (UnimplementedAuditServiceHandler) ExportAuditLog(context.Context, *connect.Request[v1.ExportAuditLogRequest]) (*connect.Response[v1.ExportAuditLogResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.AuditService.ExportAuditLog is not implemented")) } + +func (UnimplementedAuditServiceHandler) DeliverAuditEvents(context.Context, *connect.Request[v1.DeliverAuditEventsRequest]) (*connect.Response[v1.DeliverAuditEventsResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.AuditService.DeliverAuditEvents is not implemented")) +} + +func (UnimplementedAuditServiceHandler) LookupDeliverAuditEvents(context.Context, *connect.Request[v1.DeliverAuditEventsRequest]) (*connect.Response[v1.DeliverAuditEventsResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.AuditService.LookupDeliverAuditEvents is not implemented")) +} diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/module_capabilities.connect.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/module_capabilities.connect.go index 6ba694e7f..aec90c95d 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/module_capabilities.connect.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/module_capabilities.connect.go @@ -96,6 +96,9 @@ const ( // ModuleCapabilitiesServiceEmitAuditEventProcedure is the fully-qualified name of the // ModuleCapabilitiesService's EmitAuditEvent RPC. ModuleCapabilitiesServiceEmitAuditEventProcedure = "/saas.accounts.v1.ModuleCapabilitiesService/EmitAuditEvent" + // ModuleCapabilitiesServiceReadJournalProcedure is the fully-qualified name of the + // ModuleCapabilitiesService's ReadJournal RPC. + ModuleCapabilitiesServiceReadJournalProcedure = "/saas.accounts.v1.ModuleCapabilitiesService/ReadJournal" // ModuleCapabilitiesServiceDeclareAuditEventTypesProcedure is the fully-qualified name of the // ModuleCapabilitiesService's DeclareAuditEventTypes RPC. ModuleCapabilitiesServiceDeclareAuditEventTypesProcedure = "/saas.accounts.v1.ModuleCapabilitiesService/DeclareAuditEventTypes" @@ -201,6 +204,11 @@ type ModuleCapabilitiesServiceClient interface { CancelApproval(context.Context, *connect.Request[v1.ModuleCancelApprovalRequest]) (*connect.Response[emptypb.Empty], error) // EmitAuditEvent records a registered audit event on the tenant's spine. EmitAuditEvent(context.Context, *connect.Request[v1.ModuleEmitAuditEventRequest]) (*connect.Response[emptypb.Empty], error) + // ReadJournal serves the platform journal across every tenant, and the rows with + // no tenant, to a module principal whose grant is cross_tenant. Rows are served + // in the order their writing transactions are final, so a reader that resumes + // from next_after skips nothing; it enqueues nothing and is not a replay. + ReadJournal(context.Context, *connect.Request[v1.ModuleReadJournalRequest]) (*connect.Response[v1.ModuleReadJournalResponse], error) // DeclareAuditEventTypes admits the audit event types a composed module owns, // into the namespaces the operator bound to it; see the request. DeclareAuditEventTypes(context.Context, *connect.Request[v1.ModuleDeclareAuditEventTypesRequest]) (*connect.Response[v1.ModuleDeclareAuditEventTypesResponse], error) @@ -402,6 +410,12 @@ func NewModuleCapabilitiesServiceClient(httpClient connect.HTTPClient, baseURL s connect.WithSchema(moduleCapabilitiesServiceMethods.ByName("EmitAuditEvent")), connect.WithClientOptions(opts...), ), + readJournal: connect.NewClient[v1.ModuleReadJournalRequest, v1.ModuleReadJournalResponse]( + httpClient, + baseURL+ModuleCapabilitiesServiceReadJournalProcedure, + connect.WithSchema(moduleCapabilitiesServiceMethods.ByName("ReadJournal")), + connect.WithClientOptions(opts...), + ), declareAuditEventTypes: connect.NewClient[v1.ModuleDeclareAuditEventTypesRequest, v1.ModuleDeclareAuditEventTypesResponse]( httpClient, baseURL+ModuleCapabilitiesServiceDeclareAuditEventTypesProcedure, @@ -505,6 +519,7 @@ type moduleCapabilitiesServiceClient struct { getApproval *connect.Client[v1.ModuleGetApprovalRequest, v1.ModuleApproval] cancelApproval *connect.Client[v1.ModuleCancelApprovalRequest, emptypb.Empty] emitAuditEvent *connect.Client[v1.ModuleEmitAuditEventRequest, emptypb.Empty] + readJournal *connect.Client[v1.ModuleReadJournalRequest, v1.ModuleReadJournalResponse] declareAuditEventTypes *connect.Client[v1.ModuleDeclareAuditEventTypesRequest, v1.ModuleDeclareAuditEventTypesResponse] listSubjectVisibility *connect.Client[v1.ModuleListSubjectVisibilityRequest, v1.ModuleListSubjectVisibilityResponse] fetchDatasourceBlob *connect.Client[v1.FetchDatasourceBlobRequest, v1.FetchDatasourceBlobChunk] @@ -627,6 +642,11 @@ func (c *moduleCapabilitiesServiceClient) EmitAuditEvent(ctx context.Context, re return c.emitAuditEvent.CallUnary(ctx, req) } +// ReadJournal calls saas.accounts.v1.ModuleCapabilitiesService.ReadJournal. +func (c *moduleCapabilitiesServiceClient) ReadJournal(ctx context.Context, req *connect.Request[v1.ModuleReadJournalRequest]) (*connect.Response[v1.ModuleReadJournalResponse], error) { + return c.readJournal.CallUnary(ctx, req) +} + // DeclareAuditEventTypes calls saas.accounts.v1.ModuleCapabilitiesService.DeclareAuditEventTypes. func (c *moduleCapabilitiesServiceClient) DeclareAuditEventTypes(ctx context.Context, req *connect.Request[v1.ModuleDeclareAuditEventTypesRequest]) (*connect.Response[v1.ModuleDeclareAuditEventTypesResponse], error) { return c.declareAuditEventTypes.CallUnary(ctx, req) @@ -761,6 +781,11 @@ type ModuleCapabilitiesServiceHandler interface { CancelApproval(context.Context, *connect.Request[v1.ModuleCancelApprovalRequest]) (*connect.Response[emptypb.Empty], error) // EmitAuditEvent records a registered audit event on the tenant's spine. EmitAuditEvent(context.Context, *connect.Request[v1.ModuleEmitAuditEventRequest]) (*connect.Response[emptypb.Empty], error) + // ReadJournal serves the platform journal across every tenant, and the rows with + // no tenant, to a module principal whose grant is cross_tenant. Rows are served + // in the order their writing transactions are final, so a reader that resumes + // from next_after skips nothing; it enqueues nothing and is not a replay. + ReadJournal(context.Context, *connect.Request[v1.ModuleReadJournalRequest]) (*connect.Response[v1.ModuleReadJournalResponse], error) // DeclareAuditEventTypes admits the audit event types a composed module owns, // into the namespaces the operator bound to it; see the request. DeclareAuditEventTypes(context.Context, *connect.Request[v1.ModuleDeclareAuditEventTypesRequest]) (*connect.Response[v1.ModuleDeclareAuditEventTypesResponse], error) @@ -958,6 +983,12 @@ func NewModuleCapabilitiesServiceHandler(svc ModuleCapabilitiesServiceHandler, o connect.WithSchema(moduleCapabilitiesServiceMethods.ByName("EmitAuditEvent")), connect.WithHandlerOptions(opts...), ) + moduleCapabilitiesServiceReadJournalHandler := connect.NewUnaryHandler( + ModuleCapabilitiesServiceReadJournalProcedure, + svc.ReadJournal, + connect.WithSchema(moduleCapabilitiesServiceMethods.ByName("ReadJournal")), + connect.WithHandlerOptions(opts...), + ) moduleCapabilitiesServiceDeclareAuditEventTypesHandler := connect.NewUnaryHandler( ModuleCapabilitiesServiceDeclareAuditEventTypesProcedure, svc.DeclareAuditEventTypes, @@ -1078,6 +1109,8 @@ func NewModuleCapabilitiesServiceHandler(svc ModuleCapabilitiesServiceHandler, o moduleCapabilitiesServiceCancelApprovalHandler.ServeHTTP(w, r) case ModuleCapabilitiesServiceEmitAuditEventProcedure: moduleCapabilitiesServiceEmitAuditEventHandler.ServeHTTP(w, r) + case ModuleCapabilitiesServiceReadJournalProcedure: + moduleCapabilitiesServiceReadJournalHandler.ServeHTTP(w, r) case ModuleCapabilitiesServiceDeclareAuditEventTypesProcedure: moduleCapabilitiesServiceDeclareAuditEventTypesHandler.ServeHTTP(w, r) case ModuleCapabilitiesServiceListSubjectVisibilityProcedure: @@ -1193,6 +1226,10 @@ func (UnimplementedModuleCapabilitiesServiceHandler) EmitAuditEvent(context.Cont return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.ModuleCapabilitiesService.EmitAuditEvent is not implemented")) } +func (UnimplementedModuleCapabilitiesServiceHandler) ReadJournal(context.Context, *connect.Request[v1.ModuleReadJournalRequest]) (*connect.Response[v1.ModuleReadJournalResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.ModuleCapabilitiesService.ReadJournal is not implemented")) +} + func (UnimplementedModuleCapabilitiesServiceHandler) DeclareAuditEventTypes(context.Context, *connect.Request[v1.ModuleDeclareAuditEventTypesRequest]) (*connect.Response[v1.ModuleDeclareAuditEventTypesResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.ModuleCapabilitiesService.DeclareAuditEventTypes is not implemented")) } diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/audit.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/audit.pb.go index 3796396d1..8e3e18912 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/audit.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/audit.pb.go @@ -13,6 +13,7 @@ import ( unsafe "unsafe" _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + _ "github.com/codefly-dev/core/generated/go/codefly/runnable/v0" _ "google.golang.org/genproto/googleapis/api/annotations" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" @@ -589,7 +590,11 @@ type AuditMetric struct { // percentile) require a "payload:" field; non-numeric payload values are // ignored. Field string `protobuf:"bytes,2,opt,name=field,proto3" json:"field,omitempty"` - // percentile ∈ (0,1], used only when op == percentile (0.95 → p95). + // percentile ∈ (0,1], used only when op == percentile (0.95 → p95). Only the + // Postgres audit backend computes it: the warehouse backend keeps entries + // at-least-once and deduplicates them on read, and a percentile over that is not + // an exact answer, so a deployment on the warehouse backend refuses a query that + // asks for one with InvalidArgument. Every other op is served by both backends. Percentile float64 `protobuf:"fixed64,3,opt,name=percentile,proto3" json:"percentile,omitempty"` // alias names this metric in the response map. Defaults to "count" for count, // else "_". @@ -1267,11 +1272,157 @@ func (x *ListAuditEventTypesResponse) GetTypes() []*AuditEventType { return nil } +// DeliverAuditEventsRequest names one range of the platform journal, (from, to], +// whose audit records the operation appends to the warehouse audit storage. Both +// ends are positions a ModuleReadJournal page returned; from may instead be the +// literal "start", meaning before the oldest retained entry, and is never empty so +// that the first range of a fresh journal has a non-empty value to correlate on. +// The range ends at a position that is final (below the oldest transaction still +// running) and holds at most 500 audit records, or it is refused before anything +// is appended. The effect id is "saas.audit.recorded.v1/", derived by the +// host: one range, one receipt. A runtime task may carry its own transport +// effect identity; the range receipt is independent of that task identity. +type DeliverAuditEventsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + From string `protobuf:"bytes,1,opt,name=from,proto3" json:"from,omitempty"` + To string `protobuf:"bytes,2,opt,name=to,proto3" json:"to,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeliverAuditEventsRequest) Reset() { + *x = DeliverAuditEventsRequest{} + mi := &file_saas_accounts_v1_audit_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeliverAuditEventsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeliverAuditEventsRequest) ProtoMessage() {} + +func (x *DeliverAuditEventsRequest) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_audit_proto_msgTypes[13] + 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 DeliverAuditEventsRequest.ProtoReflect.Descriptor instead. +func (*DeliverAuditEventsRequest) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_audit_proto_rawDescGZIP(), []int{13} +} + +func (x *DeliverAuditEventsRequest) GetFrom() string { + if x != nil { + return x.From + } + return "" +} + +func (x *DeliverAuditEventsRequest) GetTo() string { + if x != nil { + return x.To + } + return "" +} + +type DeliverAuditEventsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + EffectId string `protobuf:"bytes,1,opt,name=effect_id,json=effectId,proto3" json:"effect_id,omitempty"` + // status is "committed" when the range is recorded. An absent lookup + // returns NOT_FOUND: absence is inconclusive, never a committed response. + Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` + // delivered is the number of audit records of the range appended to the + // storage; a range with none still commits a receipt. + Delivered int32 `protobuf:"varint,3,opt,name=delivered,proto3" json:"delivered,omitempty"` + // The exact committed interval is (from, to]. Retention verifies these + // positions against the receipt's effect identity and request digest before + // retiring journal rows. Legacy receipts without both positions prove no + // coverage; delivered alone never proves an interval. + From string `protobuf:"bytes,4,opt,name=from,proto3" json:"from,omitempty"` + To string `protobuf:"bytes,5,opt,name=to,proto3" json:"to,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeliverAuditEventsResponse) Reset() { + *x = DeliverAuditEventsResponse{} + mi := &file_saas_accounts_v1_audit_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeliverAuditEventsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeliverAuditEventsResponse) ProtoMessage() {} + +func (x *DeliverAuditEventsResponse) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_audit_proto_msgTypes[14] + 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 DeliverAuditEventsResponse.ProtoReflect.Descriptor instead. +func (*DeliverAuditEventsResponse) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_audit_proto_rawDescGZIP(), []int{14} +} + +func (x *DeliverAuditEventsResponse) GetEffectId() string { + if x != nil { + return x.EffectId + } + return "" +} + +func (x *DeliverAuditEventsResponse) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *DeliverAuditEventsResponse) GetDelivered() int32 { + if x != nil { + return x.Delivered + } + return 0 +} + +func (x *DeliverAuditEventsResponse) GetFrom() string { + if x != nil { + return x.From + } + return "" +} + +func (x *DeliverAuditEventsResponse) GetTo() string { + if x != nil { + return x.To + } + return "" +} + var File_saas_accounts_v1_audit_proto protoreflect.FileDescriptor const file_saas_accounts_v1_audit_proto_rawDesc = "" + "\n" + - "\x1csaas/accounts/v1/audit.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1csaas/policy/v1/options.proto\"\xdb\x04\n" + + "\x1csaas/accounts/v1/audit.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a!codefly/runnable/v0/options.proto\x1a\x1csaas/policy/v1/options.proto\"\xdb\x04\n" + "\n" + "AuditEvent\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x19\n" + @@ -1406,7 +1557,16 @@ const file_saas_accounts_v1_audit_proto_rawDesc = "" + "\x11marks_user_joined\x18\b \x01(\bR\x0fmarksUserJoined\x12)\n" + "\x10webhook_eligible\x18\t \x01(\bR\x0fwebhookEligible\"U\n" + "\x1bListAuditEventTypesResponse\x126\n" + - "\x05types\x18\x01 \x03(\v2 .saas.accounts.v1.AuditEventTypeR\x05types2\x9d\x06\n" + + "\x05types\x18\x01 \x03(\v2 .saas.accounts.v1.AuditEventTypeR\x05types\"U\n" + + "\x19DeliverAuditEventsRequest\x12\x1d\n" + + "\x04from\x18\x01 \x01(\tB\t\xbaH\x06r\x04\x10\x01\x18@R\x04from\x12\x19\n" + + "\x02to\x18\x02 \x01(\tB\t\xbaH\x06r\x04\x10\x01\x18@R\x02to\"\x93\x01\n" + + "\x1aDeliverAuditEventsResponse\x12\x1b\n" + + "\teffect_id\x18\x01 \x01(\tR\beffectId\x12\x16\n" + + "\x06status\x18\x02 \x01(\tR\x06status\x12\x1c\n" + + "\tdelivered\x18\x03 \x01(\x05R\tdelivered\x12\x12\n" + + "\x04from\x18\x04 \x01(\tR\x04from\x12\x0e\n" + + "\x02to\x18\x05 \x01(\tR\x02to2\xe9\t\n" + "\fAuditService\x12\xb5\x01\n" + "\rQueryAuditLog\x12&.saas.accounts.v1.QueryAuditLogRequest\x1a'.saas.accounts.v1.QueryAuditLogResponse\"S\xc2\xf3\x18:\b\x02\x10\x03\x1a\n" + "audit:read\"\n" + @@ -1422,7 +1582,12 @@ const file_saas_accounts_v1_audit_proto_rawDesc = "" + "\x0eExportAuditLog\x12'.saas.accounts.v1.ExportAuditLogRequest\x1a(.saas.accounts.v1.ExportAuditLogResponse\"]\xc2\xf3\x18:\b\x02\x10\x04\x1a\n" + "audit:read\"\n" + "audit:read*\f\n" + - "\x06org_id\x10\x02\x18\x010\x01:\x02\x10\x01@\x01H\x03P\x03X\x03`\x01\x82\xd3\xe4\x93\x02\x19:\x01*\"\x14/v1/audit-log:exportB\xb2\x01\n" + + "\x06org_id\x10\x02\x18\x010\x01:\x02\x10\x01@\x01H\x03P\x03X\x03`\x01\x82\xd3\xe4\x93\x02\x19:\x01*\"\x14/v1/audit-log:export\x12\xb7\x02\n" + + "\x12DeliverAuditEvents\x12+.saas.accounts.v1.DeliverAuditEventsRequest\x1a,.saas.accounts.v1.DeliverAuditEventsResponse\"\xc5\x01\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x03H\x04P\x02X\x02`\x01\xda\xf3\x18\xa8\x01\n" + + "\x02\b<\x12\x03\b\xd8\x04\x18\x03\"\x02\b\x05*\vUNAVAILABLE*\x12RESOURCE_EXHAUSTED*\aABORTED2\n" + + "saas-auditJ7/saas.accounts.v1.AuditService/LookupDeliverAuditEventsP\x80\x02X\x80\b`\x01r \n" + + "\x0eaudit_delivery\x12\x06invoke\x12\x04read\x18\x01\x12\x8f\x01\n" + + "\x18LookupDeliverAuditEvents\x12+.saas.accounts.v1.DeliverAuditEventsRequest\x1a,.saas.accounts.v1.DeliverAuditEventsResponse\"\x18\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x03P\x02X\x02`\x01B\xb2\x01\n" + "\x14com.saas.accounts.v1B\n" + "AuditProtoP\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" @@ -1438,7 +1603,7 @@ func file_saas_accounts_v1_audit_proto_rawDescGZIP() []byte { return file_saas_accounts_v1_audit_proto_rawDescData } -var file_saas_accounts_v1_audit_proto_msgTypes = make([]protoimpl.MessageInfo, 18) +var file_saas_accounts_v1_audit_proto_msgTypes = make([]protoimpl.MessageInfo, 20) var file_saas_accounts_v1_audit_proto_goTypes = []any{ (*AuditEvent)(nil), // 0: saas.accounts.v1.AuditEvent (*QueryAuditLogRequest)(nil), // 1: saas.accounts.v1.QueryAuditLogRequest @@ -1453,41 +1618,47 @@ var file_saas_accounts_v1_audit_proto_goTypes = []any{ (*ListAuditEventTypesRequest)(nil), // 10: saas.accounts.v1.ListAuditEventTypesRequest (*AuditEventType)(nil), // 11: saas.accounts.v1.AuditEventType (*ListAuditEventTypesResponse)(nil), // 12: saas.accounts.v1.ListAuditEventTypesResponse - nil, // 13: saas.accounts.v1.AuditEvent.MetadataEntry - nil, // 14: saas.accounts.v1.QueryAuditLogRequest.PayloadContainsEntry - nil, // 15: saas.accounts.v1.AggregateAuditLogRequest.PayloadContainsEntry - nil, // 16: saas.accounts.v1.AuditAggregateBucket.MetricsEntry - nil, // 17: saas.accounts.v1.AuditAggregateBucket.SamplesEntry - (*timestamppb.Timestamp)(nil), // 18: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 19: google.protobuf.Struct + (*DeliverAuditEventsRequest)(nil), // 13: saas.accounts.v1.DeliverAuditEventsRequest + (*DeliverAuditEventsResponse)(nil), // 14: saas.accounts.v1.DeliverAuditEventsResponse + nil, // 15: saas.accounts.v1.AuditEvent.MetadataEntry + nil, // 16: saas.accounts.v1.QueryAuditLogRequest.PayloadContainsEntry + nil, // 17: saas.accounts.v1.AggregateAuditLogRequest.PayloadContainsEntry + nil, // 18: saas.accounts.v1.AuditAggregateBucket.MetricsEntry + nil, // 19: saas.accounts.v1.AuditAggregateBucket.SamplesEntry + (*timestamppb.Timestamp)(nil), // 20: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 21: google.protobuf.Struct } var file_saas_accounts_v1_audit_proto_depIdxs = []int32{ - 13, // 0: saas.accounts.v1.AuditEvent.metadata:type_name -> saas.accounts.v1.AuditEvent.MetadataEntry - 18, // 1: saas.accounts.v1.AuditEvent.created_at:type_name -> google.protobuf.Timestamp - 19, // 2: saas.accounts.v1.AuditEvent.payload:type_name -> google.protobuf.Struct - 18, // 3: saas.accounts.v1.QueryAuditLogRequest.from:type_name -> google.protobuf.Timestamp - 18, // 4: saas.accounts.v1.QueryAuditLogRequest.to:type_name -> google.protobuf.Timestamp - 14, // 5: saas.accounts.v1.QueryAuditLogRequest.payload_contains:type_name -> saas.accounts.v1.QueryAuditLogRequest.PayloadContainsEntry + 15, // 0: saas.accounts.v1.AuditEvent.metadata:type_name -> saas.accounts.v1.AuditEvent.MetadataEntry + 20, // 1: saas.accounts.v1.AuditEvent.created_at:type_name -> google.protobuf.Timestamp + 21, // 2: saas.accounts.v1.AuditEvent.payload:type_name -> google.protobuf.Struct + 20, // 3: saas.accounts.v1.QueryAuditLogRequest.from:type_name -> google.protobuf.Timestamp + 20, // 4: saas.accounts.v1.QueryAuditLogRequest.to:type_name -> google.protobuf.Timestamp + 16, // 5: saas.accounts.v1.QueryAuditLogRequest.payload_contains:type_name -> saas.accounts.v1.QueryAuditLogRequest.PayloadContainsEntry 0, // 6: saas.accounts.v1.QueryAuditLogResponse.events:type_name -> saas.accounts.v1.AuditEvent - 18, // 7: saas.accounts.v1.AggregateAuditLogRequest.from:type_name -> google.protobuf.Timestamp - 18, // 8: saas.accounts.v1.AggregateAuditLogRequest.to:type_name -> google.protobuf.Timestamp + 20, // 7: saas.accounts.v1.AggregateAuditLogRequest.from:type_name -> google.protobuf.Timestamp + 20, // 8: saas.accounts.v1.AggregateAuditLogRequest.to:type_name -> google.protobuf.Timestamp 5, // 9: saas.accounts.v1.AggregateAuditLogRequest.metrics:type_name -> saas.accounts.v1.AuditMetric 6, // 10: saas.accounts.v1.AggregateAuditLogRequest.derived:type_name -> saas.accounts.v1.AuditDerivedMetric - 15, // 11: saas.accounts.v1.AggregateAuditLogRequest.payload_contains:type_name -> saas.accounts.v1.AggregateAuditLogRequest.PayloadContainsEntry - 16, // 12: saas.accounts.v1.AuditAggregateBucket.metrics:type_name -> saas.accounts.v1.AuditAggregateBucket.MetricsEntry - 17, // 13: saas.accounts.v1.AuditAggregateBucket.samples:type_name -> saas.accounts.v1.AuditAggregateBucket.SamplesEntry + 17, // 11: saas.accounts.v1.AggregateAuditLogRequest.payload_contains:type_name -> saas.accounts.v1.AggregateAuditLogRequest.PayloadContainsEntry + 18, // 12: saas.accounts.v1.AuditAggregateBucket.metrics:type_name -> saas.accounts.v1.AuditAggregateBucket.MetricsEntry + 19, // 13: saas.accounts.v1.AuditAggregateBucket.samples:type_name -> saas.accounts.v1.AuditAggregateBucket.SamplesEntry 8, // 14: saas.accounts.v1.AggregateAuditLogResponse.buckets:type_name -> saas.accounts.v1.AuditAggregateBucket 11, // 15: saas.accounts.v1.ListAuditEventTypesResponse.types:type_name -> saas.accounts.v1.AuditEventType 1, // 16: saas.accounts.v1.AuditService.QueryAuditLog:input_type -> saas.accounts.v1.QueryAuditLogRequest 7, // 17: saas.accounts.v1.AuditService.AggregateAuditLog:input_type -> saas.accounts.v1.AggregateAuditLogRequest 10, // 18: saas.accounts.v1.AuditService.ListAuditEventTypes:input_type -> saas.accounts.v1.ListAuditEventTypesRequest 3, // 19: saas.accounts.v1.AuditService.ExportAuditLog:input_type -> saas.accounts.v1.ExportAuditLogRequest - 2, // 20: saas.accounts.v1.AuditService.QueryAuditLog:output_type -> saas.accounts.v1.QueryAuditLogResponse - 9, // 21: saas.accounts.v1.AuditService.AggregateAuditLog:output_type -> saas.accounts.v1.AggregateAuditLogResponse - 12, // 22: saas.accounts.v1.AuditService.ListAuditEventTypes:output_type -> saas.accounts.v1.ListAuditEventTypesResponse - 4, // 23: saas.accounts.v1.AuditService.ExportAuditLog:output_type -> saas.accounts.v1.ExportAuditLogResponse - 20, // [20:24] is the sub-list for method output_type - 16, // [16:20] is the sub-list for method input_type + 13, // 20: saas.accounts.v1.AuditService.DeliverAuditEvents:input_type -> saas.accounts.v1.DeliverAuditEventsRequest + 13, // 21: saas.accounts.v1.AuditService.LookupDeliverAuditEvents:input_type -> saas.accounts.v1.DeliverAuditEventsRequest + 2, // 22: saas.accounts.v1.AuditService.QueryAuditLog:output_type -> saas.accounts.v1.QueryAuditLogResponse + 9, // 23: saas.accounts.v1.AuditService.AggregateAuditLog:output_type -> saas.accounts.v1.AggregateAuditLogResponse + 12, // 24: saas.accounts.v1.AuditService.ListAuditEventTypes:output_type -> saas.accounts.v1.ListAuditEventTypesResponse + 4, // 25: saas.accounts.v1.AuditService.ExportAuditLog:output_type -> saas.accounts.v1.ExportAuditLogResponse + 14, // 26: saas.accounts.v1.AuditService.DeliverAuditEvents:output_type -> saas.accounts.v1.DeliverAuditEventsResponse + 14, // 27: saas.accounts.v1.AuditService.LookupDeliverAuditEvents:output_type -> saas.accounts.v1.DeliverAuditEventsResponse + 22, // [22:28] is the sub-list for method output_type + 16, // [16:22] is the sub-list for method input_type 16, // [16:16] is the sub-list for extension type_name 16, // [16:16] is the sub-list for extension extendee 0, // [0:16] is the sub-list for field type_name @@ -1504,7 +1675,7 @@ func file_saas_accounts_v1_audit_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_audit_proto_rawDesc), len(file_saas_accounts_v1_audit_proto_rawDesc)), NumEnums: 0, - NumMessages: 18, + NumMessages: 20, NumExtensions: 0, NumServices: 1, }, diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/audit_grpc.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/audit_grpc.pb.go index 8ce2562e4..8e30d7780 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/audit_grpc.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/audit_grpc.pb.go @@ -20,10 +20,12 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - AuditService_QueryAuditLog_FullMethodName = "/saas.accounts.v1.AuditService/QueryAuditLog" - AuditService_AggregateAuditLog_FullMethodName = "/saas.accounts.v1.AuditService/AggregateAuditLog" - AuditService_ListAuditEventTypes_FullMethodName = "/saas.accounts.v1.AuditService/ListAuditEventTypes" - AuditService_ExportAuditLog_FullMethodName = "/saas.accounts.v1.AuditService/ExportAuditLog" + AuditService_QueryAuditLog_FullMethodName = "/saas.accounts.v1.AuditService/QueryAuditLog" + AuditService_AggregateAuditLog_FullMethodName = "/saas.accounts.v1.AuditService/AggregateAuditLog" + AuditService_ListAuditEventTypes_FullMethodName = "/saas.accounts.v1.AuditService/ListAuditEventTypes" + AuditService_ExportAuditLog_FullMethodName = "/saas.accounts.v1.AuditService/ExportAuditLog" + AuditService_DeliverAuditEvents_FullMethodName = "/saas.accounts.v1.AuditService/DeliverAuditEvents" + AuditService_LookupDeliverAuditEvents_FullMethodName = "/saas.accounts.v1.AuditService/LookupDeliverAuditEvents" ) // AuditServiceClient is the client API for AuditService service. @@ -36,6 +38,17 @@ type AuditServiceClient interface { AggregateAuditLog(ctx context.Context, in *AggregateAuditLogRequest, opts ...grpc.CallOption) (*AggregateAuditLogResponse, error) ListAuditEventTypes(ctx context.Context, in *ListAuditEventTypesRequest, opts ...grpc.CallOption) (*ListAuditEventTypesResponse, error) ExportAuditLog(ctx context.Context, in *ExportAuditLogRequest, opts ...grpc.CallOption) (*ExportAuditLogResponse, error) + // DeliverAuditEvents appends one range of the journal's audit records to the + // warehouse audit storage: the operation module-runtime calls after reading the + // range through ReadJournal. It is an operation, not a sweep. It takes each + // record's organization from the journal row and never from the caller, emits no + // audit of its own, and commits one receipt per range under the installation's + // tenant; delivery is at least once, and every read of the storage returns each + // entry once by id. Its receipt is read back through LookupDeliverAuditEvents. + DeliverAuditEvents(ctx context.Context, in *DeliverAuditEventsRequest, opts ...grpc.CallOption) (*DeliverAuditEventsResponse, error) + // LookupDeliverAuditEvents takes the invocation's same {from, to} body and + // derives the range receipt identity from from. It never delivers. + LookupDeliverAuditEvents(ctx context.Context, in *DeliverAuditEventsRequest, opts ...grpc.CallOption) (*DeliverAuditEventsResponse, error) } type auditServiceClient struct { @@ -86,6 +99,26 @@ func (c *auditServiceClient) ExportAuditLog(ctx context.Context, in *ExportAudit return out, nil } +func (c *auditServiceClient) DeliverAuditEvents(ctx context.Context, in *DeliverAuditEventsRequest, opts ...grpc.CallOption) (*DeliverAuditEventsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeliverAuditEventsResponse) + err := c.cc.Invoke(ctx, AuditService_DeliverAuditEvents_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *auditServiceClient) LookupDeliverAuditEvents(ctx context.Context, in *DeliverAuditEventsRequest, opts ...grpc.CallOption) (*DeliverAuditEventsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeliverAuditEventsResponse) + err := c.cc.Invoke(ctx, AuditService_LookupDeliverAuditEvents_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // AuditServiceServer is the server API for AuditService service. // All implementations must embed UnimplementedAuditServiceServer // for forward compatibility. @@ -96,6 +129,17 @@ type AuditServiceServer interface { AggregateAuditLog(context.Context, *AggregateAuditLogRequest) (*AggregateAuditLogResponse, error) ListAuditEventTypes(context.Context, *ListAuditEventTypesRequest) (*ListAuditEventTypesResponse, error) ExportAuditLog(context.Context, *ExportAuditLogRequest) (*ExportAuditLogResponse, error) + // DeliverAuditEvents appends one range of the journal's audit records to the + // warehouse audit storage: the operation module-runtime calls after reading the + // range through ReadJournal. It is an operation, not a sweep. It takes each + // record's organization from the journal row and never from the caller, emits no + // audit of its own, and commits one receipt per range under the installation's + // tenant; delivery is at least once, and every read of the storage returns each + // entry once by id. Its receipt is read back through LookupDeliverAuditEvents. + DeliverAuditEvents(context.Context, *DeliverAuditEventsRequest) (*DeliverAuditEventsResponse, error) + // LookupDeliverAuditEvents takes the invocation's same {from, to} body and + // derives the range receipt identity from from. It never delivers. + LookupDeliverAuditEvents(context.Context, *DeliverAuditEventsRequest) (*DeliverAuditEventsResponse, error) mustEmbedUnimplementedAuditServiceServer() } @@ -118,6 +162,12 @@ func (UnimplementedAuditServiceServer) ListAuditEventTypes(context.Context, *Lis func (UnimplementedAuditServiceServer) ExportAuditLog(context.Context, *ExportAuditLogRequest) (*ExportAuditLogResponse, error) { return nil, status.Error(codes.Unimplemented, "method ExportAuditLog not implemented") } +func (UnimplementedAuditServiceServer) DeliverAuditEvents(context.Context, *DeliverAuditEventsRequest) (*DeliverAuditEventsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DeliverAuditEvents not implemented") +} +func (UnimplementedAuditServiceServer) LookupDeliverAuditEvents(context.Context, *DeliverAuditEventsRequest) (*DeliverAuditEventsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method LookupDeliverAuditEvents not implemented") +} func (UnimplementedAuditServiceServer) mustEmbedUnimplementedAuditServiceServer() {} func (UnimplementedAuditServiceServer) testEmbeddedByValue() {} @@ -211,6 +261,42 @@ func _AuditService_ExportAuditLog_Handler(srv interface{}, ctx context.Context, return interceptor(ctx, in, info, handler) } +func _AuditService_DeliverAuditEvents_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeliverAuditEventsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuditServiceServer).DeliverAuditEvents(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuditService_DeliverAuditEvents_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuditServiceServer).DeliverAuditEvents(ctx, req.(*DeliverAuditEventsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AuditService_LookupDeliverAuditEvents_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeliverAuditEventsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuditServiceServer).LookupDeliverAuditEvents(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuditService_LookupDeliverAuditEvents_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuditServiceServer).LookupDeliverAuditEvents(ctx, req.(*DeliverAuditEventsRequest)) + } + return interceptor(ctx, in, info, handler) +} + // AuditService_ServiceDesc is the grpc.ServiceDesc for AuditService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -234,6 +320,14 @@ var AuditService_ServiceDesc = grpc.ServiceDesc{ MethodName: "ExportAuditLog", Handler: _AuditService_ExportAuditLog_Handler, }, + { + MethodName: "DeliverAuditEvents", + Handler: _AuditService_DeliverAuditEvents_Handler, + }, + { + MethodName: "LookupDeliverAuditEvents", + Handler: _AuditService_LookupDeliverAuditEvents_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "saas/accounts/v1/audit.proto", diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/module_capabilities.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/module_capabilities.pb.go index d3bc30c7a..c417c8487 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/module_capabilities.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/module_capabilities.pb.go @@ -2039,6 +2039,252 @@ func (x *ModuleEmitAuditEventRequest) GetIdempotencyKey() string { return "" } +// ModuleReadJournalRequest reads the platform journal across every tenant. Only a +// module principal whose grant is cross_tenant may call it, the gate a +// system-scoped EmitAuditEvent has; any other caller is PermissionDenied. +type ModuleReadJournalRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // after is the position the previous page ended at (a page's next_after). The + // literal "start" is a valid position meaning "before the oldest retained + // entry", and the empty string is accepted as an alias of it; so a reader with + // nothing read yet has a non-empty position to correlate its first page on. + // Positions are opaque: store one and hand it back unread. No entry's position + // is ever "start". A value this host did not issue is InvalidArgument. + After string `protobuf:"bytes,1,opt,name=after,proto3" json:"after,omitempty"` + // limit is the page size: 0 is 100, and more than 500 is served as 500. A + // page is also bounded to 2 MiB of event data, though the first entry is always + // served. + Limit int32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` + // types are the event types to read, exactly (no pattern). At least one is + // required, so no caller reads every tenant's every event by leaving the + // filter out. + Types []string `protobuf:"bytes,3,rep,name=types,proto3" json:"types,omitempty"` + // delivery_bindings are the binding ids the caller's own delivery of the journal + // runs under. They decide which entries are marked delivery_machinery. + DeliveryBindings []string `protobuf:"bytes,4,rep,name=delivery_bindings,json=deliveryBindings,proto3" json:"delivery_bindings,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModuleReadJournalRequest) Reset() { + *x = ModuleReadJournalRequest{} + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModuleReadJournalRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModuleReadJournalRequest) ProtoMessage() {} + +func (x *ModuleReadJournalRequest) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[20] + 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 ModuleReadJournalRequest.ProtoReflect.Descriptor instead. +func (*ModuleReadJournalRequest) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{20} +} + +func (x *ModuleReadJournalRequest) GetAfter() string { + if x != nil { + return x.After + } + return "" +} + +func (x *ModuleReadJournalRequest) GetLimit() int32 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *ModuleReadJournalRequest) GetTypes() []string { + if x != nil { + return x.Types + } + return nil +} + +func (x *ModuleReadJournalRequest) GetDeliveryBindings() []string { + if x != nil { + return x.DeliveryBindings + } + return nil +} + +// ModuleJournalEntry is one journal row. Rows are served in the order their +// writing transactions are final (see ModuleReadJournalRequest), so a reader that +// resumes from the last position it was given skips nothing. +type ModuleJournalEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + // position is where the row sits in the read order: opaque, and the value a + // reader passes as after to resume past it. + Position string `protobuf:"bytes,1,opt,name=position,proto3" json:"position,omitempty"` + EventId string `protobuf:"bytes,2,opt,name=event_id,json=eventId,proto3" json:"event_id,omitempty"` + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + // organization_id is the row's tenant, empty for a row that has none. + OrganizationId string `protobuf:"bytes,4,opt,name=organization_id,json=organizationId,proto3" json:"organization_id,omitempty"` + OccurredAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=occurred_at,json=occurredAt,proto3" json:"occurred_at,omitempty"` + // data is the event's JSON object; empty when the event's data is not one. + Data *structpb.Struct `protobuf:"bytes,6,opt,name=data,proto3" json:"data,omitempty"` + // delivery_machinery is true when the entry the row carries is the footprint of + // the caller's own delivery of the journal: the host computes it, so a caller + // never parses this module's payloads. It is true for a + // saas.module.operation_context_minted or + // saas.module.delegated_audience_exchange entry whose binding_id is one of the + // request's delivery_bindings, and for a saas.module.work_context_minted entry + // of the caller's own module principal. + DeliveryMachinery bool `protobuf:"varint,7,opt,name=delivery_machinery,json=deliveryMachinery,proto3" json:"delivery_machinery,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModuleJournalEntry) Reset() { + *x = ModuleJournalEntry{} + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModuleJournalEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModuleJournalEntry) ProtoMessage() {} + +func (x *ModuleJournalEntry) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[21] + 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 ModuleJournalEntry.ProtoReflect.Descriptor instead. +func (*ModuleJournalEntry) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{21} +} + +func (x *ModuleJournalEntry) GetPosition() string { + if x != nil { + return x.Position + } + return "" +} + +func (x *ModuleJournalEntry) GetEventId() string { + if x != nil { + return x.EventId + } + return "" +} + +func (x *ModuleJournalEntry) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *ModuleJournalEntry) GetOrganizationId() string { + if x != nil { + return x.OrganizationId + } + return "" +} + +func (x *ModuleJournalEntry) GetOccurredAt() *timestamppb.Timestamp { + if x != nil { + return x.OccurredAt + } + return nil +} + +func (x *ModuleJournalEntry) GetData() *structpb.Struct { + if x != nil { + return x.Data + } + return nil +} + +func (x *ModuleJournalEntry) GetDeliveryMachinery() bool { + if x != nil { + return x.DeliveryMachinery + } + return false +} + +// ModuleReadJournalResponse is one page and where to resume. +type ModuleReadJournalResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Entries []*ModuleJournalEntry `protobuf:"bytes,1,rep,name=entries,proto3" json:"entries,omitempty"` + // next_after is the last entry's position, or the request's after when the page + // is empty. + NextAfter string `protobuf:"bytes,2,opt,name=next_after,json=nextAfter,proto3" json:"next_after,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModuleReadJournalResponse) Reset() { + *x = ModuleReadJournalResponse{} + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModuleReadJournalResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModuleReadJournalResponse) ProtoMessage() {} + +func (x *ModuleReadJournalResponse) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[22] + 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 ModuleReadJournalResponse.ProtoReflect.Descriptor instead. +func (*ModuleReadJournalResponse) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{22} +} + +func (x *ModuleReadJournalResponse) GetEntries() []*ModuleJournalEntry { + if x != nil { + return x.Entries + } + return nil +} + +func (x *ModuleReadJournalResponse) GetNextAfter() string { + if x != nil { + return x.NextAfter + } + return "" +} + // ModuleAuditFieldDeclaration is one payload field of a declared audit event // type. `solution` is stamped by the host from the emitting scope and may not // be declared. @@ -2058,7 +2304,7 @@ type ModuleAuditFieldDeclaration struct { func (x *ModuleAuditFieldDeclaration) Reset() { *x = ModuleAuditFieldDeclaration{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[20] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2070,7 +2316,7 @@ func (x *ModuleAuditFieldDeclaration) String() string { func (*ModuleAuditFieldDeclaration) ProtoMessage() {} func (x *ModuleAuditFieldDeclaration) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[20] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2083,7 +2329,7 @@ func (x *ModuleAuditFieldDeclaration) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleAuditFieldDeclaration.ProtoReflect.Descriptor instead. func (*ModuleAuditFieldDeclaration) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{20} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{23} } func (x *ModuleAuditFieldDeclaration) GetName() string { @@ -2136,7 +2382,7 @@ type ModuleAuditEventTypeDeclaration struct { func (x *ModuleAuditEventTypeDeclaration) Reset() { *x = ModuleAuditEventTypeDeclaration{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[21] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2148,7 +2394,7 @@ func (x *ModuleAuditEventTypeDeclaration) String() string { func (*ModuleAuditEventTypeDeclaration) ProtoMessage() {} func (x *ModuleAuditEventTypeDeclaration) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[21] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2161,7 +2407,7 @@ func (x *ModuleAuditEventTypeDeclaration) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleAuditEventTypeDeclaration.ProtoReflect.Descriptor instead. func (*ModuleAuditEventTypeDeclaration) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{21} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{24} } func (x *ModuleAuditEventTypeDeclaration) GetType() string { @@ -2211,7 +2457,7 @@ type ModuleDeclareAuditEventTypesRequest struct { func (x *ModuleDeclareAuditEventTypesRequest) Reset() { *x = ModuleDeclareAuditEventTypesRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[22] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2223,7 +2469,7 @@ func (x *ModuleDeclareAuditEventTypesRequest) String() string { func (*ModuleDeclareAuditEventTypesRequest) ProtoMessage() {} func (x *ModuleDeclareAuditEventTypesRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[22] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2236,7 +2482,7 @@ func (x *ModuleDeclareAuditEventTypesRequest) ProtoReflect() protoreflect.Messag // Deprecated: Use ModuleDeclareAuditEventTypesRequest.ProtoReflect.Descriptor instead. func (*ModuleDeclareAuditEventTypesRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{22} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{25} } func (x *ModuleDeclareAuditEventTypesRequest) GetPrefix() string { @@ -2266,7 +2512,7 @@ type ModuleDeclareAuditEventTypesResponse struct { func (x *ModuleDeclareAuditEventTypesResponse) Reset() { *x = ModuleDeclareAuditEventTypesResponse{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[23] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2278,7 +2524,7 @@ func (x *ModuleDeclareAuditEventTypesResponse) String() string { func (*ModuleDeclareAuditEventTypesResponse) ProtoMessage() {} func (x *ModuleDeclareAuditEventTypesResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[23] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2291,7 +2537,7 @@ func (x *ModuleDeclareAuditEventTypesResponse) ProtoReflect() protoreflect.Messa // Deprecated: Use ModuleDeclareAuditEventTypesResponse.ProtoReflect.Descriptor instead. func (*ModuleDeclareAuditEventTypesResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{23} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{26} } func (x *ModuleDeclareAuditEventTypesResponse) GetAdmittedTypes() []string { @@ -2325,7 +2571,7 @@ type SubjectVisibilityGrant struct { func (x *SubjectVisibilityGrant) Reset() { *x = SubjectVisibilityGrant{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[24] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2337,7 +2583,7 @@ func (x *SubjectVisibilityGrant) String() string { func (*SubjectVisibilityGrant) ProtoMessage() {} func (x *SubjectVisibilityGrant) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[24] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2350,7 +2596,7 @@ func (x *SubjectVisibilityGrant) ProtoReflect() protoreflect.Message { // Deprecated: Use SubjectVisibilityGrant.ProtoReflect.Descriptor instead. func (*SubjectVisibilityGrant) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{24} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{27} } func (x *SubjectVisibilityGrant) GetVisibleSubjectId() string { @@ -2388,7 +2634,7 @@ type ModuleListSubjectVisibilityRequest struct { func (x *ModuleListSubjectVisibilityRequest) Reset() { *x = ModuleListSubjectVisibilityRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[25] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2400,7 +2646,7 @@ func (x *ModuleListSubjectVisibilityRequest) String() string { func (*ModuleListSubjectVisibilityRequest) ProtoMessage() {} func (x *ModuleListSubjectVisibilityRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[25] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2413,7 +2659,7 @@ func (x *ModuleListSubjectVisibilityRequest) ProtoReflect() protoreflect.Message // Deprecated: Use ModuleListSubjectVisibilityRequest.ProtoReflect.Descriptor instead. func (*ModuleListSubjectVisibilityRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{25} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{28} } func (x *ModuleListSubjectVisibilityRequest) GetTenant() string { @@ -2452,7 +2698,7 @@ type ModuleListSubjectVisibilityResponse struct { func (x *ModuleListSubjectVisibilityResponse) Reset() { *x = ModuleListSubjectVisibilityResponse{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[26] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2464,7 +2710,7 @@ func (x *ModuleListSubjectVisibilityResponse) String() string { func (*ModuleListSubjectVisibilityResponse) ProtoMessage() {} func (x *ModuleListSubjectVisibilityResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[26] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2477,7 +2723,7 @@ func (x *ModuleListSubjectVisibilityResponse) ProtoReflect() protoreflect.Messag // Deprecated: Use ModuleListSubjectVisibilityResponse.ProtoReflect.Descriptor instead. func (*ModuleListSubjectVisibilityResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{26} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{29} } func (x *ModuleListSubjectVisibilityResponse) GetGrants() []*SubjectVisibilityGrant { @@ -2505,7 +2751,7 @@ type FetchDatasourceBlobRequest struct { func (x *FetchDatasourceBlobRequest) Reset() { *x = FetchDatasourceBlobRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[27] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2517,7 +2763,7 @@ func (x *FetchDatasourceBlobRequest) String() string { func (*FetchDatasourceBlobRequest) ProtoMessage() {} func (x *FetchDatasourceBlobRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[27] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2530,7 +2776,7 @@ func (x *FetchDatasourceBlobRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FetchDatasourceBlobRequest.ProtoReflect.Descriptor instead. func (*FetchDatasourceBlobRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{27} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{30} } func (x *FetchDatasourceBlobRequest) GetSourceId() string { @@ -2562,7 +2808,7 @@ type FetchDatasourceBlobChunk struct { func (x *FetchDatasourceBlobChunk) Reset() { *x = FetchDatasourceBlobChunk{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[28] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2574,7 +2820,7 @@ func (x *FetchDatasourceBlobChunk) String() string { func (*FetchDatasourceBlobChunk) ProtoMessage() {} func (x *FetchDatasourceBlobChunk) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[28] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2587,7 +2833,7 @@ func (x *FetchDatasourceBlobChunk) ProtoReflect() protoreflect.Message { // Deprecated: Use FetchDatasourceBlobChunk.ProtoReflect.Descriptor instead. func (*FetchDatasourceBlobChunk) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{28} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{31} } func (x *FetchDatasourceBlobChunk) GetData() []byte { @@ -2625,7 +2871,7 @@ type DatasourceFileRef struct { func (x *DatasourceFileRef) Reset() { *x = DatasourceFileRef{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[29] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2637,7 +2883,7 @@ func (x *DatasourceFileRef) String() string { func (*DatasourceFileRef) ProtoMessage() {} func (x *DatasourceFileRef) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[29] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2650,7 +2896,7 @@ func (x *DatasourceFileRef) ProtoReflect() protoreflect.Message { // Deprecated: Use DatasourceFileRef.ProtoReflect.Descriptor instead. func (*DatasourceFileRef) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{29} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{32} } func (x *DatasourceFileRef) GetItemId() string { @@ -2686,7 +2932,7 @@ type FetchDatasourceFilesRequest struct { func (x *FetchDatasourceFilesRequest) Reset() { *x = FetchDatasourceFilesRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[30] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2698,7 +2944,7 @@ func (x *FetchDatasourceFilesRequest) String() string { func (*FetchDatasourceFilesRequest) ProtoMessage() {} func (x *FetchDatasourceFilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[30] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2711,7 +2957,7 @@ func (x *FetchDatasourceFilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FetchDatasourceFilesRequest.ProtoReflect.Descriptor instead. func (*FetchDatasourceFilesRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{30} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{33} } func (x *FetchDatasourceFilesRequest) GetSourceId() string { @@ -2753,7 +2999,7 @@ type DatasourceProvenance struct { func (x *DatasourceProvenance) Reset() { *x = DatasourceProvenance{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[31] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2765,7 +3011,7 @@ func (x *DatasourceProvenance) String() string { func (*DatasourceProvenance) ProtoMessage() {} func (x *DatasourceProvenance) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[31] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2778,7 +3024,7 @@ func (x *DatasourceProvenance) ProtoReflect() protoreflect.Message { // Deprecated: Use DatasourceProvenance.ProtoReflect.Descriptor instead. func (*DatasourceProvenance) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{31} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{34} } func (x *DatasourceProvenance) GetSourceId() string { @@ -2844,7 +3090,7 @@ type DatasourceItemReaders struct { func (x *DatasourceItemReaders) Reset() { *x = DatasourceItemReaders{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[32] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2856,7 +3102,7 @@ func (x *DatasourceItemReaders) String() string { func (*DatasourceItemReaders) ProtoMessage() {} func (x *DatasourceItemReaders) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[32] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2869,7 +3115,7 @@ func (x *DatasourceItemReaders) ProtoReflect() protoreflect.Message { // Deprecated: Use DatasourceItemReaders.ProtoReflect.Descriptor instead. func (*DatasourceItemReaders) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{32} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{35} } func (x *DatasourceItemReaders) GetBasis() DatasourceItemReadersBasis { @@ -2930,7 +3176,7 @@ type DatasourceFileHeader struct { func (x *DatasourceFileHeader) Reset() { *x = DatasourceFileHeader{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[33] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2942,7 +3188,7 @@ func (x *DatasourceFileHeader) String() string { func (*DatasourceFileHeader) ProtoMessage() {} func (x *DatasourceFileHeader) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[33] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2955,7 +3201,7 @@ func (x *DatasourceFileHeader) ProtoReflect() protoreflect.Message { // Deprecated: Use DatasourceFileHeader.ProtoReflect.Descriptor instead. func (*DatasourceFileHeader) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{33} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{36} } func (x *DatasourceFileHeader) GetProvenance() *DatasourceProvenance { @@ -3009,7 +3255,7 @@ type FetchDatasourceFilesFrame struct { func (x *FetchDatasourceFilesFrame) Reset() { *x = FetchDatasourceFilesFrame{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[34] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3021,7 +3267,7 @@ func (x *FetchDatasourceFilesFrame) String() string { func (*FetchDatasourceFilesFrame) ProtoMessage() {} func (x *FetchDatasourceFilesFrame) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[34] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3034,7 +3280,7 @@ func (x *FetchDatasourceFilesFrame) ProtoReflect() protoreflect.Message { // Deprecated: Use FetchDatasourceFilesFrame.ProtoReflect.Descriptor instead. func (*FetchDatasourceFilesFrame) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{34} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{37} } func (x *FetchDatasourceFilesFrame) GetFrame() isFetchDatasourceFilesFrame_Frame { @@ -3093,7 +3339,7 @@ type ModulePublishEventRequest struct { func (x *ModulePublishEventRequest) Reset() { *x = ModulePublishEventRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[35] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3105,7 +3351,7 @@ func (x *ModulePublishEventRequest) String() string { func (*ModulePublishEventRequest) ProtoMessage() {} func (x *ModulePublishEventRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[35] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3118,7 +3364,7 @@ func (x *ModulePublishEventRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ModulePublishEventRequest.ProtoReflect.Descriptor instead. func (*ModulePublishEventRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{35} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{38} } func (x *ModulePublishEventRequest) GetTenant() string { @@ -3148,7 +3394,7 @@ type ModulePublishEventResponse struct { func (x *ModulePublishEventResponse) Reset() { *x = ModulePublishEventResponse{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[36] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3160,7 +3406,7 @@ func (x *ModulePublishEventResponse) String() string { func (*ModulePublishEventResponse) ProtoMessage() {} func (x *ModulePublishEventResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[36] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3173,7 +3419,7 @@ func (x *ModulePublishEventResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ModulePublishEventResponse.ProtoReflect.Descriptor instead. func (*ModulePublishEventResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{36} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{39} } func (x *ModulePublishEventResponse) GetEventId() string { @@ -3199,7 +3445,7 @@ type ModuleSubscribeRequest struct { func (x *ModuleSubscribeRequest) Reset() { *x = ModuleSubscribeRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[37] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3211,7 +3457,7 @@ func (x *ModuleSubscribeRequest) String() string { func (*ModuleSubscribeRequest) ProtoMessage() {} func (x *ModuleSubscribeRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[37] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3224,7 +3470,7 @@ func (x *ModuleSubscribeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleSubscribeRequest.ProtoReflect.Descriptor instead. func (*ModuleSubscribeRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{37} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{40} } func (x *ModuleSubscribeRequest) GetTypePattern() string { @@ -3263,7 +3509,7 @@ type ModuleSubscription struct { func (x *ModuleSubscription) Reset() { *x = ModuleSubscription{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[38] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3275,7 +3521,7 @@ func (x *ModuleSubscription) String() string { func (*ModuleSubscription) ProtoMessage() {} func (x *ModuleSubscription) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[38] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3288,7 +3534,7 @@ func (x *ModuleSubscription) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleSubscription.ProtoReflect.Descriptor instead. func (*ModuleSubscription) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{38} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{41} } func (x *ModuleSubscription) GetId() string { @@ -3342,7 +3588,7 @@ type ModuleSubscribeResponse struct { func (x *ModuleSubscribeResponse) Reset() { *x = ModuleSubscribeResponse{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[39] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3354,7 +3600,7 @@ func (x *ModuleSubscribeResponse) String() string { func (*ModuleSubscribeResponse) ProtoMessage() {} func (x *ModuleSubscribeResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[39] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3367,7 +3613,7 @@ func (x *ModuleSubscribeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleSubscribeResponse.ProtoReflect.Descriptor instead. func (*ModuleSubscribeResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{39} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{42} } func (x *ModuleSubscribeResponse) GetSubscription() *ModuleSubscription { @@ -3387,7 +3633,7 @@ type ModuleUnsubscribeRequest struct { func (x *ModuleUnsubscribeRequest) Reset() { *x = ModuleUnsubscribeRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[40] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3399,7 +3645,7 @@ func (x *ModuleUnsubscribeRequest) String() string { func (*ModuleUnsubscribeRequest) ProtoMessage() {} func (x *ModuleUnsubscribeRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[40] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3412,7 +3658,7 @@ func (x *ModuleUnsubscribeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleUnsubscribeRequest.ProtoReflect.Descriptor instead. func (*ModuleUnsubscribeRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{40} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{43} } func (x *ModuleUnsubscribeRequest) GetSubscriptionId() string { @@ -3432,7 +3678,7 @@ type ModuleListSubscriptionsRequest struct { func (x *ModuleListSubscriptionsRequest) Reset() { *x = ModuleListSubscriptionsRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[41] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3444,7 +3690,7 @@ func (x *ModuleListSubscriptionsRequest) String() string { func (*ModuleListSubscriptionsRequest) ProtoMessage() {} func (x *ModuleListSubscriptionsRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[41] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3457,7 +3703,7 @@ func (x *ModuleListSubscriptionsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleListSubscriptionsRequest.ProtoReflect.Descriptor instead. func (*ModuleListSubscriptionsRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{41} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{44} } type ModuleListSubscriptionsResponse struct { @@ -3469,7 +3715,7 @@ type ModuleListSubscriptionsResponse struct { func (x *ModuleListSubscriptionsResponse) Reset() { *x = ModuleListSubscriptionsResponse{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[42] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3481,7 +3727,7 @@ func (x *ModuleListSubscriptionsResponse) String() string { func (*ModuleListSubscriptionsResponse) ProtoMessage() {} func (x *ModuleListSubscriptionsResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[42] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3494,7 +3740,7 @@ func (x *ModuleListSubscriptionsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleListSubscriptionsResponse.ProtoReflect.Descriptor instead. func (*ModuleListSubscriptionsResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{42} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{45} } func (x *ModuleListSubscriptionsResponse) GetSubscriptions() []*ModuleSubscription { @@ -3519,7 +3765,7 @@ type ModuleReplayEventsRequest struct { func (x *ModuleReplayEventsRequest) Reset() { *x = ModuleReplayEventsRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[43] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3531,7 +3777,7 @@ func (x *ModuleReplayEventsRequest) String() string { func (*ModuleReplayEventsRequest) ProtoMessage() {} func (x *ModuleReplayEventsRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[43] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3544,7 +3790,7 @@ func (x *ModuleReplayEventsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleReplayEventsRequest.ProtoReflect.Descriptor instead. func (*ModuleReplayEventsRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{43} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{46} } func (x *ModuleReplayEventsRequest) GetTenant() string { @@ -3579,7 +3825,7 @@ type ModuleReplayEventsResponse struct { func (x *ModuleReplayEventsResponse) Reset() { *x = ModuleReplayEventsResponse{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[44] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3591,7 +3837,7 @@ func (x *ModuleReplayEventsResponse) String() string { func (*ModuleReplayEventsResponse) ProtoMessage() {} func (x *ModuleReplayEventsResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[44] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3604,7 +3850,7 @@ func (x *ModuleReplayEventsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleReplayEventsResponse.ProtoReflect.Descriptor instead. func (*ModuleReplayEventsResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{44} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{47} } func (x *ModuleReplayEventsResponse) GetRedelivered() int32 { @@ -3649,7 +3895,7 @@ type ReadableSourceCollection struct { func (x *ReadableSourceCollection) Reset() { *x = ReadableSourceCollection{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[45] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3661,7 +3907,7 @@ func (x *ReadableSourceCollection) String() string { func (*ReadableSourceCollection) ProtoMessage() {} func (x *ReadableSourceCollection) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[45] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3674,7 +3920,7 @@ func (x *ReadableSourceCollection) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadableSourceCollection.ProtoReflect.Descriptor instead. func (*ReadableSourceCollection) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{45} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{48} } func (x *ReadableSourceCollection) GetSourceId() string { @@ -3768,7 +4014,7 @@ type ReadableCollectionGrant struct { func (x *ReadableCollectionGrant) Reset() { *x = ReadableCollectionGrant{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[46] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3780,7 +4026,7 @@ func (x *ReadableCollectionGrant) String() string { func (*ReadableCollectionGrant) ProtoMessage() {} func (x *ReadableCollectionGrant) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[46] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3793,7 +4039,7 @@ func (x *ReadableCollectionGrant) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadableCollectionGrant.ProtoReflect.Descriptor instead. func (*ReadableCollectionGrant) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{46} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{49} } func (x *ReadableCollectionGrant) GetSubjectLabel() string { @@ -3866,7 +4112,7 @@ type CollectionSyncProvenance struct { func (x *CollectionSyncProvenance) Reset() { *x = CollectionSyncProvenance{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[47] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3878,7 +4124,7 @@ func (x *CollectionSyncProvenance) String() string { func (*CollectionSyncProvenance) ProtoMessage() {} func (x *CollectionSyncProvenance) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[47] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3891,7 +4137,7 @@ func (x *CollectionSyncProvenance) ProtoReflect() protoreflect.Message { // Deprecated: Use CollectionSyncProvenance.ProtoReflect.Descriptor instead. func (*CollectionSyncProvenance) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{47} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{50} } func (x *CollectionSyncProvenance) GetStage() SourceSyncStage { @@ -3958,7 +4204,7 @@ type ListReadableSourceCollectionsRequest struct { func (x *ListReadableSourceCollectionsRequest) Reset() { *x = ListReadableSourceCollectionsRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[48] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3970,7 +4216,7 @@ func (x *ListReadableSourceCollectionsRequest) String() string { func (*ListReadableSourceCollectionsRequest) ProtoMessage() {} func (x *ListReadableSourceCollectionsRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[48] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3983,7 +4229,7 @@ func (x *ListReadableSourceCollectionsRequest) ProtoReflect() protoreflect.Messa // Deprecated: Use ListReadableSourceCollectionsRequest.ProtoReflect.Descriptor instead. func (*ListReadableSourceCollectionsRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{48} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{51} } func (x *ListReadableSourceCollectionsRequest) GetPageSize() int32 { @@ -4017,7 +4263,7 @@ type ListReadableSourceCollectionsResponse struct { func (x *ListReadableSourceCollectionsResponse) Reset() { *x = ListReadableSourceCollectionsResponse{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[49] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4029,7 +4275,7 @@ func (x *ListReadableSourceCollectionsResponse) String() string { func (*ListReadableSourceCollectionsResponse) ProtoMessage() {} func (x *ListReadableSourceCollectionsResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[49] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4042,7 +4288,7 @@ func (x *ListReadableSourceCollectionsResponse) ProtoReflect() protoreflect.Mess // Deprecated: Use ListReadableSourceCollectionsResponse.ProtoReflect.Descriptor instead. func (*ListReadableSourceCollectionsResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{49} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{52} } func (x *ListReadableSourceCollectionsResponse) GetCollections() []*ReadableSourceCollection { @@ -4086,7 +4332,7 @@ type ModulePlaceRecordRequest struct { func (x *ModulePlaceRecordRequest) Reset() { *x = ModulePlaceRecordRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[50] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4098,7 +4344,7 @@ func (x *ModulePlaceRecordRequest) String() string { func (*ModulePlaceRecordRequest) ProtoMessage() {} func (x *ModulePlaceRecordRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[50] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4111,7 +4357,7 @@ func (x *ModulePlaceRecordRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ModulePlaceRecordRequest.ProtoReflect.Descriptor instead. func (*ModulePlaceRecordRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{50} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{53} } func (x *ModulePlaceRecordRequest) GetTenant() string { @@ -4170,7 +4416,7 @@ type ModulePlaceRecordResponse struct { func (x *ModulePlaceRecordResponse) Reset() { *x = ModulePlaceRecordResponse{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[51] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4182,7 +4428,7 @@ func (x *ModulePlaceRecordResponse) String() string { func (*ModulePlaceRecordResponse) ProtoMessage() {} func (x *ModulePlaceRecordResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[51] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4195,7 +4441,7 @@ func (x *ModulePlaceRecordResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ModulePlaceRecordResponse.ProtoReflect.Descriptor instead. func (*ModulePlaceRecordResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{51} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{54} } func (x *ModulePlaceRecordResponse) GetNodeId() string { @@ -4222,7 +4468,7 @@ type CheckWorkContextRecordAccessRequest struct { func (x *CheckWorkContextRecordAccessRequest) Reset() { *x = CheckWorkContextRecordAccessRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[52] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4234,7 +4480,7 @@ func (x *CheckWorkContextRecordAccessRequest) String() string { func (*CheckWorkContextRecordAccessRequest) ProtoMessage() {} func (x *CheckWorkContextRecordAccessRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[52] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[55] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4247,7 +4493,7 @@ func (x *CheckWorkContextRecordAccessRequest) ProtoReflect() protoreflect.Messag // Deprecated: Use CheckWorkContextRecordAccessRequest.ProtoReflect.Descriptor instead. func (*CheckWorkContextRecordAccessRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{52} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{55} } func (x *CheckWorkContextRecordAccessRequest) GetResourceType() string { @@ -4290,7 +4536,7 @@ type CheckWorkContextRecordAccessResponse struct { func (x *CheckWorkContextRecordAccessResponse) Reset() { *x = CheckWorkContextRecordAccessResponse{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[53] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4302,7 +4548,7 @@ func (x *CheckWorkContextRecordAccessResponse) String() string { func (*CheckWorkContextRecordAccessResponse) ProtoMessage() {} func (x *CheckWorkContextRecordAccessResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[53] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4315,7 +4561,7 @@ func (x *CheckWorkContextRecordAccessResponse) ProtoReflect() protoreflect.Messa // Deprecated: Use CheckWorkContextRecordAccessResponse.ProtoReflect.Descriptor instead. func (*CheckWorkContextRecordAccessResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{53} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{56} } func (x *CheckWorkContextRecordAccessResponse) GetAllowed() bool { @@ -4344,7 +4590,7 @@ type ModuleExchangeDelegatedReadAudienceRequest struct { func (x *ModuleExchangeDelegatedReadAudienceRequest) Reset() { *x = ModuleExchangeDelegatedReadAudienceRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[54] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4356,7 +4602,7 @@ func (x *ModuleExchangeDelegatedReadAudienceRequest) String() string { func (*ModuleExchangeDelegatedReadAudienceRequest) ProtoMessage() {} func (x *ModuleExchangeDelegatedReadAudienceRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[54] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4369,7 +4615,7 @@ func (x *ModuleExchangeDelegatedReadAudienceRequest) ProtoReflect() protoreflect // Deprecated: Use ModuleExchangeDelegatedReadAudienceRequest.ProtoReflect.Descriptor instead. func (*ModuleExchangeDelegatedReadAudienceRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{54} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{57} } func (x *ModuleExchangeDelegatedReadAudienceRequest) GetBindingId() string { @@ -4425,7 +4671,7 @@ type ModuleExchangeDelegatedOperationAudienceRequest struct { func (x *ModuleExchangeDelegatedOperationAudienceRequest) Reset() { *x = ModuleExchangeDelegatedOperationAudienceRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[55] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4437,7 +4683,7 @@ func (x *ModuleExchangeDelegatedOperationAudienceRequest) String() string { func (*ModuleExchangeDelegatedOperationAudienceRequest) ProtoMessage() {} func (x *ModuleExchangeDelegatedOperationAudienceRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[55] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4450,7 +4696,7 @@ func (x *ModuleExchangeDelegatedOperationAudienceRequest) ProtoReflect() protore // Deprecated: Use ModuleExchangeDelegatedOperationAudienceRequest.ProtoReflect.Descriptor instead. func (*ModuleExchangeDelegatedOperationAudienceRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{55} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{58} } func (x *ModuleExchangeDelegatedOperationAudienceRequest) GetBindingId() string { @@ -4493,7 +4739,7 @@ type ExecutableArtifactContract struct { func (x *ExecutableArtifactContract) Reset() { *x = ExecutableArtifactContract{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[56] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4505,7 +4751,7 @@ func (x *ExecutableArtifactContract) String() string { func (*ExecutableArtifactContract) ProtoMessage() {} func (x *ExecutableArtifactContract) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[56] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4518,7 +4764,7 @@ func (x *ExecutableArtifactContract) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecutableArtifactContract.ProtoReflect.Descriptor instead. func (*ExecutableArtifactContract) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{56} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{59} } func (x *ExecutableArtifactContract) GetKind() string { @@ -4557,7 +4803,7 @@ type ExecutableArtifactIdentity struct { func (x *ExecutableArtifactIdentity) Reset() { *x = ExecutableArtifactIdentity{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[57] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4569,7 +4815,7 @@ func (x *ExecutableArtifactIdentity) String() string { func (*ExecutableArtifactIdentity) ProtoMessage() {} func (x *ExecutableArtifactIdentity) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[57] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4582,7 +4828,7 @@ func (x *ExecutableArtifactIdentity) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecutableArtifactIdentity.ProtoReflect.Descriptor instead. func (*ExecutableArtifactIdentity) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{57} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{60} } func (x *ExecutableArtifactIdentity) GetSchema() string { @@ -4632,7 +4878,7 @@ type ModuleExecutableArtifactRequest struct { func (x *ModuleExecutableArtifactRequest) Reset() { *x = ModuleExecutableArtifactRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[58] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4644,7 +4890,7 @@ func (x *ModuleExecutableArtifactRequest) String() string { func (*ModuleExecutableArtifactRequest) ProtoMessage() {} func (x *ModuleExecutableArtifactRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[58] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4657,7 +4903,7 @@ func (x *ModuleExecutableArtifactRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleExecutableArtifactRequest.ProtoReflect.Descriptor instead. func (*ModuleExecutableArtifactRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{58} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{61} } func (x *ModuleExecutableArtifactRequest) GetParentWorkContextToken() string { @@ -4699,7 +4945,7 @@ type ModuleRevokeExecutableArtifactRequest struct { func (x *ModuleRevokeExecutableArtifactRequest) Reset() { *x = ModuleRevokeExecutableArtifactRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[59] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4711,7 +4957,7 @@ func (x *ModuleRevokeExecutableArtifactRequest) String() string { func (*ModuleRevokeExecutableArtifactRequest) ProtoMessage() {} func (x *ModuleRevokeExecutableArtifactRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[59] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4724,7 +4970,7 @@ func (x *ModuleRevokeExecutableArtifactRequest) ProtoReflect() protoreflect.Mess // Deprecated: Use ModuleRevokeExecutableArtifactRequest.ProtoReflect.Descriptor instead. func (*ModuleRevokeExecutableArtifactRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{59} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{62} } func (x *ModuleRevokeExecutableArtifactRequest) GetParentWorkContextToken() string { @@ -4760,7 +5006,7 @@ type ModuleExecutableArtifactResponse struct { func (x *ModuleExecutableArtifactResponse) Reset() { *x = ModuleExecutableArtifactResponse{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[60] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4772,7 +5018,7 @@ func (x *ModuleExecutableArtifactResponse) String() string { func (*ModuleExecutableArtifactResponse) ProtoMessage() {} func (x *ModuleExecutableArtifactResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[60] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4785,7 +5031,7 @@ func (x *ModuleExecutableArtifactResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleExecutableArtifactResponse.ProtoReflect.Descriptor instead. func (*ModuleExecutableArtifactResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{60} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{63} } func (x *ModuleExecutableArtifactResponse) GetSchemaVersion() string { @@ -4828,7 +5074,7 @@ type ModuleCurrentInstallationRequest struct { func (x *ModuleCurrentInstallationRequest) Reset() { *x = ModuleCurrentInstallationRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[61] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4840,7 +5086,7 @@ func (x *ModuleCurrentInstallationRequest) String() string { func (*ModuleCurrentInstallationRequest) ProtoMessage() {} func (x *ModuleCurrentInstallationRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[61] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4853,7 +5099,7 @@ func (x *ModuleCurrentInstallationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleCurrentInstallationRequest.ProtoReflect.Descriptor instead. func (*ModuleCurrentInstallationRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{61} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{64} } func (x *ModuleCurrentInstallationRequest) GetParentWorkContextToken() string { @@ -4886,7 +5132,7 @@ type ModuleCurrentInstallationResponse struct { func (x *ModuleCurrentInstallationResponse) Reset() { *x = ModuleCurrentInstallationResponse{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[62] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4898,7 +5144,7 @@ func (x *ModuleCurrentInstallationResponse) String() string { func (*ModuleCurrentInstallationResponse) ProtoMessage() {} func (x *ModuleCurrentInstallationResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[62] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[65] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4911,7 +5157,7 @@ func (x *ModuleCurrentInstallationResponse) ProtoReflect() protoreflect.Message // Deprecated: Use ModuleCurrentInstallationResponse.ProtoReflect.Descriptor instead. func (*ModuleCurrentInstallationResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{62} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{65} } func (x *ModuleCurrentInstallationResponse) GetInstallationId() string { @@ -4960,7 +5206,7 @@ type WorkContextSealQuestion struct { func (x *WorkContextSealQuestion) Reset() { *x = WorkContextSealQuestion{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[63] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4972,7 +5218,7 @@ func (x *WorkContextSealQuestion) String() string { func (*WorkContextSealQuestion) ProtoMessage() {} func (x *WorkContextSealQuestion) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[63] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4985,7 +5231,7 @@ func (x *WorkContextSealQuestion) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkContextSealQuestion.ProtoReflect.Descriptor instead. func (*WorkContextSealQuestion) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{63} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{66} } func (x *WorkContextSealQuestion) GetPrincipalId() string { @@ -5016,7 +5262,7 @@ type WorkContextPrincipalEpochQuestion struct { func (x *WorkContextPrincipalEpochQuestion) Reset() { *x = WorkContextPrincipalEpochQuestion{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[64] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5028,7 +5274,7 @@ func (x *WorkContextPrincipalEpochQuestion) String() string { func (*WorkContextPrincipalEpochQuestion) ProtoMessage() {} func (x *WorkContextPrincipalEpochQuestion) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[64] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5041,7 +5287,7 @@ func (x *WorkContextPrincipalEpochQuestion) ProtoReflect() protoreflect.Message // Deprecated: Use WorkContextPrincipalEpochQuestion.ProtoReflect.Descriptor instead. func (*WorkContextPrincipalEpochQuestion) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{64} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{67} } func (x *WorkContextPrincipalEpochQuestion) GetPrincipalId() string { @@ -5065,7 +5311,7 @@ type WorkContextApprovedBuildQuestion struct { func (x *WorkContextApprovedBuildQuestion) Reset() { *x = WorkContextApprovedBuildQuestion{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[65] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5077,7 +5323,7 @@ func (x *WorkContextApprovedBuildQuestion) String() string { func (*WorkContextApprovedBuildQuestion) ProtoMessage() {} func (x *WorkContextApprovedBuildQuestion) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[65] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5090,7 +5336,7 @@ func (x *WorkContextApprovedBuildQuestion) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkContextApprovedBuildQuestion.ProtoReflect.Descriptor instead. func (*WorkContextApprovedBuildQuestion) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{65} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{68} } func (x *WorkContextApprovedBuildQuestion) GetPrincipalId() string { @@ -5116,7 +5362,7 @@ type WorkContextOperationBindingQuestion struct { func (x *WorkContextOperationBindingQuestion) Reset() { *x = WorkContextOperationBindingQuestion{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[66] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5128,7 +5374,7 @@ func (x *WorkContextOperationBindingQuestion) String() string { func (*WorkContextOperationBindingQuestion) ProtoMessage() {} func (x *WorkContextOperationBindingQuestion) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[66] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5141,7 +5387,7 @@ func (x *WorkContextOperationBindingQuestion) ProtoReflect() protoreflect.Messag // Deprecated: Use WorkContextOperationBindingQuestion.ProtoReflect.Descriptor instead. func (*WorkContextOperationBindingQuestion) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{66} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{69} } func (x *WorkContextOperationBindingQuestion) GetBindingId() string { @@ -5169,7 +5415,7 @@ type WorkContextAuthorizationRevisionQuestion struct { func (x *WorkContextAuthorizationRevisionQuestion) Reset() { *x = WorkContextAuthorizationRevisionQuestion{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[67] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5181,7 +5427,7 @@ func (x *WorkContextAuthorizationRevisionQuestion) String() string { func (*WorkContextAuthorizationRevisionQuestion) ProtoMessage() {} func (x *WorkContextAuthorizationRevisionQuestion) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[67] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5194,7 +5440,7 @@ func (x *WorkContextAuthorizationRevisionQuestion) ProtoReflect() protoreflect.M // Deprecated: Use WorkContextAuthorizationRevisionQuestion.ProtoReflect.Descriptor instead. func (*WorkContextAuthorizationRevisionQuestion) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{67} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{70} } func (x *WorkContextAuthorizationRevisionQuestion) GetOwnerPrincipalId() string { @@ -5216,7 +5462,7 @@ type WorkContextApprovalGrantQuestion struct { func (x *WorkContextApprovalGrantQuestion) Reset() { *x = WorkContextApprovalGrantQuestion{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[68] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5228,7 +5474,7 @@ func (x *WorkContextApprovalGrantQuestion) String() string { func (*WorkContextApprovalGrantQuestion) ProtoMessage() {} func (x *WorkContextApprovalGrantQuestion) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[68] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5241,7 +5487,7 @@ func (x *WorkContextApprovalGrantQuestion) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkContextApprovalGrantQuestion.ProtoReflect.Descriptor instead. func (*WorkContextApprovalGrantQuestion) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{68} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{71} } func (x *WorkContextApprovalGrantQuestion) GetGrantId() string { @@ -5264,7 +5510,7 @@ type WorkContextSealAnswer struct { func (x *WorkContextSealAnswer) Reset() { *x = WorkContextSealAnswer{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[69] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5276,7 +5522,7 @@ func (x *WorkContextSealAnswer) String() string { func (*WorkContextSealAnswer) ProtoMessage() {} func (x *WorkContextSealAnswer) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[69] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5289,7 +5535,7 @@ func (x *WorkContextSealAnswer) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkContextSealAnswer.ProtoReflect.Descriptor instead. func (*WorkContextSealAnswer) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{69} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{72} } func (x *WorkContextSealAnswer) GetState() WorkContextSealState { @@ -5324,7 +5570,7 @@ type WorkContextPrincipalEpochAnswer struct { func (x *WorkContextPrincipalEpochAnswer) Reset() { *x = WorkContextPrincipalEpochAnswer{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[70] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5336,7 +5582,7 @@ func (x *WorkContextPrincipalEpochAnswer) String() string { func (*WorkContextPrincipalEpochAnswer) ProtoMessage() {} func (x *WorkContextPrincipalEpochAnswer) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[70] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5349,7 +5595,7 @@ func (x *WorkContextPrincipalEpochAnswer) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkContextPrincipalEpochAnswer.ProtoReflect.Descriptor instead. func (*WorkContextPrincipalEpochAnswer) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{70} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{73} } func (x *WorkContextPrincipalEpochAnswer) GetState() WorkContextPrincipalState { @@ -5387,7 +5633,7 @@ type WorkContextApprovedBuildAnswer struct { func (x *WorkContextApprovedBuildAnswer) Reset() { *x = WorkContextApprovedBuildAnswer{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[71] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5399,7 +5645,7 @@ func (x *WorkContextApprovedBuildAnswer) String() string { func (*WorkContextApprovedBuildAnswer) ProtoMessage() {} func (x *WorkContextApprovedBuildAnswer) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[71] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5412,7 +5658,7 @@ func (x *WorkContextApprovedBuildAnswer) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkContextApprovedBuildAnswer.ProtoReflect.Descriptor instead. func (*WorkContextApprovedBuildAnswer) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{71} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{74} } func (x *WorkContextApprovedBuildAnswer) GetState() WorkContextApprovedBuildState { @@ -5463,7 +5709,7 @@ type WorkContextOperationBindingAnswer struct { func (x *WorkContextOperationBindingAnswer) Reset() { *x = WorkContextOperationBindingAnswer{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[72] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5475,7 +5721,7 @@ func (x *WorkContextOperationBindingAnswer) String() string { func (*WorkContextOperationBindingAnswer) ProtoMessage() {} func (x *WorkContextOperationBindingAnswer) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[72] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5488,7 +5734,7 @@ func (x *WorkContextOperationBindingAnswer) ProtoReflect() protoreflect.Message // Deprecated: Use WorkContextOperationBindingAnswer.ProtoReflect.Descriptor instead. func (*WorkContextOperationBindingAnswer) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{72} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{75} } func (x *WorkContextOperationBindingAnswer) GetState() WorkContextOperationBindingState { @@ -5550,7 +5796,7 @@ type WorkContextAuthorizationRevisionAnswer struct { func (x *WorkContextAuthorizationRevisionAnswer) Reset() { *x = WorkContextAuthorizationRevisionAnswer{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[73] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5562,7 +5808,7 @@ func (x *WorkContextAuthorizationRevisionAnswer) String() string { func (*WorkContextAuthorizationRevisionAnswer) ProtoMessage() {} func (x *WorkContextAuthorizationRevisionAnswer) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[73] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5575,7 +5821,7 @@ func (x *WorkContextAuthorizationRevisionAnswer) ProtoReflect() protoreflect.Mes // Deprecated: Use WorkContextAuthorizationRevisionAnswer.ProtoReflect.Descriptor instead. func (*WorkContextAuthorizationRevisionAnswer) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{73} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{76} } func (x *WorkContextAuthorizationRevisionAnswer) GetOwnerPrincipalId() string { @@ -5602,7 +5848,7 @@ type WorkContextApprovalGrantAnswer struct { func (x *WorkContextApprovalGrantAnswer) Reset() { *x = WorkContextApprovalGrantAnswer{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[74] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5614,7 +5860,7 @@ func (x *WorkContextApprovalGrantAnswer) String() string { func (*WorkContextApprovalGrantAnswer) ProtoMessage() {} func (x *WorkContextApprovalGrantAnswer) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[74] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5627,7 +5873,7 @@ func (x *WorkContextApprovalGrantAnswer) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkContextApprovalGrantAnswer.ProtoReflect.Descriptor instead. func (*WorkContextApprovalGrantAnswer) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{74} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{77} } func (x *WorkContextApprovalGrantAnswer) GetState() WorkContextApprovalGrantState { @@ -5670,7 +5916,7 @@ type ModuleResolveWorkContextAuthorityRequest struct { func (x *ModuleResolveWorkContextAuthorityRequest) Reset() { *x = ModuleResolveWorkContextAuthorityRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[75] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5682,7 +5928,7 @@ func (x *ModuleResolveWorkContextAuthorityRequest) String() string { func (*ModuleResolveWorkContextAuthorityRequest) ProtoMessage() {} func (x *ModuleResolveWorkContextAuthorityRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[75] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[78] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5695,7 +5941,7 @@ func (x *ModuleResolveWorkContextAuthorityRequest) ProtoReflect() protoreflect.M // Deprecated: Use ModuleResolveWorkContextAuthorityRequest.ProtoReflect.Descriptor instead. func (*ModuleResolveWorkContextAuthorityRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{75} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{78} } func (x *ModuleResolveWorkContextAuthorityRequest) GetWorkContext() string { @@ -5833,7 +6079,7 @@ type ModuleResolveWorkContextAuthorityResponse struct { func (x *ModuleResolveWorkContextAuthorityResponse) Reset() { *x = ModuleResolveWorkContextAuthorityResponse{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[76] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5845,7 +6091,7 @@ func (x *ModuleResolveWorkContextAuthorityResponse) String() string { func (*ModuleResolveWorkContextAuthorityResponse) ProtoMessage() {} func (x *ModuleResolveWorkContextAuthorityResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[76] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[79] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5858,7 +6104,7 @@ func (x *ModuleResolveWorkContextAuthorityResponse) ProtoReflect() protoreflect. // Deprecated: Use ModuleResolveWorkContextAuthorityResponse.ProtoReflect.Descriptor instead. func (*ModuleResolveWorkContextAuthorityResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{76} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{79} } func (x *ModuleResolveWorkContextAuthorityResponse) GetAnswer() isModuleResolveWorkContextAuthorityResponse_Answer { @@ -6093,7 +6339,25 @@ const file_saas_accounts_v1_module_capabilities_proto_rawDesc = "" + "\xbaH\ar\x05\x10\x01\x18\x80\x01R\bsolution\x12#\n" + "\bentry_id\x18\x05 \x01(\tB\b\xbaH\x05r\x03\x18\xff\x01R\aentryId\x12/\n" + "\x06fields\x18\x06 \x01(\v2\x17.google.protobuf.StructR\x06fields\x121\n" + - "\x0fidempotency_key\x18\a \x01(\tB\b\xbaH\x05r\x03\x18\xff\x01R\x0eidempotencyKey\"\xb8\x01\n" + + "\x0fidempotency_key\x18\a \x01(\tB\b\xbaH\x05r\x03\x18\xff\x01R\x0eidempotencyKey\"\xc3\x01\n" + + "\x18ModuleReadJournalRequest\x12\x1d\n" + + "\x05after\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x18@R\x05after\x12\x1d\n" + + "\x05limit\x18\x02 \x01(\x05B\a\xbaH\x04\x1a\x02(\x00R\x05limit\x12)\n" + + "\x05types\x18\x03 \x03(\tB\x13\xbaH\x10\x92\x01\r\b\x01\x10\x10\"\ar\x05\x10\x01\x18\x80\x02R\x05types\x12>\n" + + "\x11delivery_bindings\x18\x04 \x03(\tB\x11\xbaH\x0e\x92\x01\v\x10@\"\ar\x05\x10\x01\x18\x80\x02R\x10deliveryBindings\"\xa1\x02\n" + + "\x12ModuleJournalEntry\x12\x1a\n" + + "\bposition\x18\x01 \x01(\tR\bposition\x12\x19\n" + + "\bevent_id\x18\x02 \x01(\tR\aeventId\x12\x12\n" + + "\x04type\x18\x03 \x01(\tR\x04type\x12'\n" + + "\x0forganization_id\x18\x04 \x01(\tR\x0eorganizationId\x12;\n" + + "\voccurred_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "occurredAt\x12+\n" + + "\x04data\x18\x06 \x01(\v2\x17.google.protobuf.StructR\x04data\x12-\n" + + "\x12delivery_machinery\x18\a \x01(\bR\x11deliveryMachinery\"z\n" + + "\x19ModuleReadJournalResponse\x12>\n" + + "\aentries\x18\x01 \x03(\v2$.saas.accounts.v1.ModuleJournalEntryR\aentries\x12\x1d\n" + + "\n" + + "next_after\x18\x02 \x01(\tR\tnextAfter\"\xb8\x01\n" + "\x1bModuleAuditFieldDeclaration\x12\x1d\n" + "\x04name\x18\x01 \x01(\tB\t\xbaH\x06r\x04\x10\x01\x18@R\x04name\x12F\n" + "\x04kind\x18\x02 \x01(\x0e2&.saas.accounts.v1.ModuleAuditFieldKindB\n" + @@ -6430,7 +6694,7 @@ const file_saas_accounts_v1_module_capabilities_proto_rawDesc = "" + "+WORK_CONTEXT_OPERATION_BINDING_STATE_ABSENT\x10\x02*\x86\x01\n" + "\x1dWorkContextApprovalGrantState\x121\n" + "-WORK_CONTEXT_APPROVAL_GRANT_STATE_UNSPECIFIED\x10\x00\x122\n" + - ".WORK_CONTEXT_APPROVAL_GRANT_STATE_NOT_RECORDED\x10\x012\xe0'\n" + + ".WORK_CONTEXT_APPROVAL_GRANT_STATE_NOT_RECORDED\x10\x012\xe3(\n" + "\x19ModuleCapabilitiesService\x12\x9b\x01\n" + "\x16GetCurrentInstallation\x122.saas.accounts.v1.ModuleCurrentInstallationRequest\x1a3.saas.accounts.v1.ModuleCurrentInstallationResponse\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x04X\x03`\x01\x12\xc6\x01\n" + "\x19ApproveExecutableArtifact\x121.saas.accounts.v1.ModuleExecutableArtifactRequest\x1a2.saas.accounts.v1.ModuleExecutableArtifactResponse\"B\xc2\xf3\x18>\b\x03\x10\x010\x01:,\n" + @@ -6458,7 +6722,8 @@ const file_saas_accounts_v1_module_capabilities_proto_rawDesc = "" + "\x0fRequestApproval\x12..saas.accounts.v1.ModuleRequestApprovalRequest\x1a/.saas.accounts.v1.ModuleRequestApprovalResponse\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01\x12u\n" + "\vGetApproval\x12*.saas.accounts.v1.ModuleGetApprovalRequest\x1a .saas.accounts.v1.ModuleApproval\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01\x12q\n" + "\x0eCancelApproval\x12-.saas.accounts.v1.ModuleCancelApprovalRequest\x1a\x16.google.protobuf.Empty\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01\x12q\n" + - "\x0eEmitAuditEvent\x12-.saas.accounts.v1.ModuleEmitAuditEventRequest\x1a\x16.google.protobuf.Empty\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01\x12\xc3\x01\n" + + "\x0eEmitAuditEvent\x12-.saas.accounts.v1.ModuleEmitAuditEventRequest\x1a\x16.google.protobuf.Empty\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01\x12\x80\x01\n" + + "\vReadJournal\x12*.saas.accounts.v1.ModuleReadJournalRequest\x1a+.saas.accounts.v1.ModuleReadJournalResponse\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01\x12\xc3\x01\n" + "\x16DeclareAuditEventTypes\x125.saas.accounts.v1.ModuleDeclareAuditEventTypesRequest\x1a6.saas.accounts.v1.ModuleDeclareAuditEventTypesResponse\":\xc2\xf3\x186\b\x03\x10\x010\x01:$\n" + " saas.module.audit_types_declared\x10\x02@\x01H\aP\x03X\x03`\x01\x12\x9e\x01\n" + "\x15ListSubjectVisibility\x124.saas.accounts.v1.ModuleListSubjectVisibilityRequest\x1a5.saas.accounts.v1.ModuleListSubjectVisibilityResponse\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01\x12\x8e\x01\n" + @@ -6491,7 +6756,7 @@ func file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP() []byte { } var file_saas_accounts_v1_module_capabilities_proto_enumTypes = make([]protoimpl.EnumInfo, 11) -var file_saas_accounts_v1_module_capabilities_proto_msgTypes = make([]protoimpl.MessageInfo, 77) +var file_saas_accounts_v1_module_capabilities_proto_msgTypes = make([]protoimpl.MessageInfo, 80) var file_saas_accounts_v1_module_capabilities_proto_goTypes = []any{ (ModuleAuditFieldKind)(0), // 0: saas.accounts.v1.ModuleAuditFieldKind (ModuleAuditEventVisibility)(0), // 1: saas.accounts.v1.ModuleAuditEventVisibility @@ -6524,223 +6789,231 @@ var file_saas_accounts_v1_module_capabilities_proto_goTypes = []any{ (*ModuleApproval)(nil), // 28: saas.accounts.v1.ModuleApproval (*ModuleCancelApprovalRequest)(nil), // 29: saas.accounts.v1.ModuleCancelApprovalRequest (*ModuleEmitAuditEventRequest)(nil), // 30: saas.accounts.v1.ModuleEmitAuditEventRequest - (*ModuleAuditFieldDeclaration)(nil), // 31: saas.accounts.v1.ModuleAuditFieldDeclaration - (*ModuleAuditEventTypeDeclaration)(nil), // 32: saas.accounts.v1.ModuleAuditEventTypeDeclaration - (*ModuleDeclareAuditEventTypesRequest)(nil), // 33: saas.accounts.v1.ModuleDeclareAuditEventTypesRequest - (*ModuleDeclareAuditEventTypesResponse)(nil), // 34: saas.accounts.v1.ModuleDeclareAuditEventTypesResponse - (*SubjectVisibilityGrant)(nil), // 35: saas.accounts.v1.SubjectVisibilityGrant - (*ModuleListSubjectVisibilityRequest)(nil), // 36: saas.accounts.v1.ModuleListSubjectVisibilityRequest - (*ModuleListSubjectVisibilityResponse)(nil), // 37: saas.accounts.v1.ModuleListSubjectVisibilityResponse - (*FetchDatasourceBlobRequest)(nil), // 38: saas.accounts.v1.FetchDatasourceBlobRequest - (*FetchDatasourceBlobChunk)(nil), // 39: saas.accounts.v1.FetchDatasourceBlobChunk - (*DatasourceFileRef)(nil), // 40: saas.accounts.v1.DatasourceFileRef - (*FetchDatasourceFilesRequest)(nil), // 41: saas.accounts.v1.FetchDatasourceFilesRequest - (*DatasourceProvenance)(nil), // 42: saas.accounts.v1.DatasourceProvenance - (*DatasourceItemReaders)(nil), // 43: saas.accounts.v1.DatasourceItemReaders - (*DatasourceFileHeader)(nil), // 44: saas.accounts.v1.DatasourceFileHeader - (*FetchDatasourceFilesFrame)(nil), // 45: saas.accounts.v1.FetchDatasourceFilesFrame - (*ModulePublishEventRequest)(nil), // 46: saas.accounts.v1.ModulePublishEventRequest - (*ModulePublishEventResponse)(nil), // 47: saas.accounts.v1.ModulePublishEventResponse - (*ModuleSubscribeRequest)(nil), // 48: saas.accounts.v1.ModuleSubscribeRequest - (*ModuleSubscription)(nil), // 49: saas.accounts.v1.ModuleSubscription - (*ModuleSubscribeResponse)(nil), // 50: saas.accounts.v1.ModuleSubscribeResponse - (*ModuleUnsubscribeRequest)(nil), // 51: saas.accounts.v1.ModuleUnsubscribeRequest - (*ModuleListSubscriptionsRequest)(nil), // 52: saas.accounts.v1.ModuleListSubscriptionsRequest - (*ModuleListSubscriptionsResponse)(nil), // 53: saas.accounts.v1.ModuleListSubscriptionsResponse - (*ModuleReplayEventsRequest)(nil), // 54: saas.accounts.v1.ModuleReplayEventsRequest - (*ModuleReplayEventsResponse)(nil), // 55: saas.accounts.v1.ModuleReplayEventsResponse - (*ReadableSourceCollection)(nil), // 56: saas.accounts.v1.ReadableSourceCollection - (*ReadableCollectionGrant)(nil), // 57: saas.accounts.v1.ReadableCollectionGrant - (*CollectionSyncProvenance)(nil), // 58: saas.accounts.v1.CollectionSyncProvenance - (*ListReadableSourceCollectionsRequest)(nil), // 59: saas.accounts.v1.ListReadableSourceCollectionsRequest - (*ListReadableSourceCollectionsResponse)(nil), // 60: saas.accounts.v1.ListReadableSourceCollectionsResponse - (*ModulePlaceRecordRequest)(nil), // 61: saas.accounts.v1.ModulePlaceRecordRequest - (*ModulePlaceRecordResponse)(nil), // 62: saas.accounts.v1.ModulePlaceRecordResponse - (*CheckWorkContextRecordAccessRequest)(nil), // 63: saas.accounts.v1.CheckWorkContextRecordAccessRequest - (*CheckWorkContextRecordAccessResponse)(nil), // 64: saas.accounts.v1.CheckWorkContextRecordAccessResponse - (*ModuleExchangeDelegatedReadAudienceRequest)(nil), // 65: saas.accounts.v1.ModuleExchangeDelegatedReadAudienceRequest - (*ModuleExchangeDelegatedOperationAudienceRequest)(nil), // 66: saas.accounts.v1.ModuleExchangeDelegatedOperationAudienceRequest - (*ExecutableArtifactContract)(nil), // 67: saas.accounts.v1.ExecutableArtifactContract - (*ExecutableArtifactIdentity)(nil), // 68: saas.accounts.v1.ExecutableArtifactIdentity - (*ModuleExecutableArtifactRequest)(nil), // 69: saas.accounts.v1.ModuleExecutableArtifactRequest - (*ModuleRevokeExecutableArtifactRequest)(nil), // 70: saas.accounts.v1.ModuleRevokeExecutableArtifactRequest - (*ModuleExecutableArtifactResponse)(nil), // 71: saas.accounts.v1.ModuleExecutableArtifactResponse - (*ModuleCurrentInstallationRequest)(nil), // 72: saas.accounts.v1.ModuleCurrentInstallationRequest - (*ModuleCurrentInstallationResponse)(nil), // 73: saas.accounts.v1.ModuleCurrentInstallationResponse - (*WorkContextSealQuestion)(nil), // 74: saas.accounts.v1.WorkContextSealQuestion - (*WorkContextPrincipalEpochQuestion)(nil), // 75: saas.accounts.v1.WorkContextPrincipalEpochQuestion - (*WorkContextApprovedBuildQuestion)(nil), // 76: saas.accounts.v1.WorkContextApprovedBuildQuestion - (*WorkContextOperationBindingQuestion)(nil), // 77: saas.accounts.v1.WorkContextOperationBindingQuestion - (*WorkContextAuthorizationRevisionQuestion)(nil), // 78: saas.accounts.v1.WorkContextAuthorizationRevisionQuestion - (*WorkContextApprovalGrantQuestion)(nil), // 79: saas.accounts.v1.WorkContextApprovalGrantQuestion - (*WorkContextSealAnswer)(nil), // 80: saas.accounts.v1.WorkContextSealAnswer - (*WorkContextPrincipalEpochAnswer)(nil), // 81: saas.accounts.v1.WorkContextPrincipalEpochAnswer - (*WorkContextApprovedBuildAnswer)(nil), // 82: saas.accounts.v1.WorkContextApprovedBuildAnswer - (*WorkContextOperationBindingAnswer)(nil), // 83: saas.accounts.v1.WorkContextOperationBindingAnswer - (*WorkContextAuthorizationRevisionAnswer)(nil), // 84: saas.accounts.v1.WorkContextAuthorizationRevisionAnswer - (*WorkContextApprovalGrantAnswer)(nil), // 85: saas.accounts.v1.WorkContextApprovalGrantAnswer - (*ModuleResolveWorkContextAuthorityRequest)(nil), // 86: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest - (*ModuleResolveWorkContextAuthorityResponse)(nil), // 87: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse - (*v1.NewJob)(nil), // 88: saas.jobs.v1.NewJob - (v1.JobEnqueueDisposition)(0), // 89: saas.jobs.v1.JobEnqueueDisposition - (*durationpb.Duration)(nil), // 90: google.protobuf.Duration - (*v1.JobEnvelope)(nil), // 91: saas.jobs.v1.JobEnvelope - (*v1.JobLeaseReference)(nil), // 92: saas.jobs.v1.JobLeaseReference - (*v1.JobLease)(nil), // 93: saas.jobs.v1.JobLease - (*v1.JobFailure)(nil), // 94: saas.jobs.v1.JobFailure - (*timestamppb.Timestamp)(nil), // 95: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 96: google.protobuf.Struct - (*v11.EventEnvelope)(nil), // 97: saas.events.v1.EventEnvelope - (*ModuleMintWorkContextRequest)(nil), // 98: saas.accounts.v1.ModuleMintWorkContextRequest - (*ModuleMintOperationContextRequest)(nil), // 99: saas.accounts.v1.ModuleMintOperationContextRequest - (*ModuleMintSourceOperationContextRequest)(nil), // 100: saas.accounts.v1.ModuleMintSourceOperationContextRequest - (*IssuedWorkContext)(nil), // 101: saas.accounts.v1.IssuedWorkContext - (*emptypb.Empty)(nil), // 102: google.protobuf.Empty - (*ModuleMintWorkContextResponse)(nil), // 103: saas.accounts.v1.ModuleMintWorkContextResponse - (*ModuleMintOperationContextResponse)(nil), // 104: saas.accounts.v1.ModuleMintOperationContextResponse - (*ModuleMintSourceOperationContextResponse)(nil), // 105: saas.accounts.v1.ModuleMintSourceOperationContextResponse + (*ModuleReadJournalRequest)(nil), // 31: saas.accounts.v1.ModuleReadJournalRequest + (*ModuleJournalEntry)(nil), // 32: saas.accounts.v1.ModuleJournalEntry + (*ModuleReadJournalResponse)(nil), // 33: saas.accounts.v1.ModuleReadJournalResponse + (*ModuleAuditFieldDeclaration)(nil), // 34: saas.accounts.v1.ModuleAuditFieldDeclaration + (*ModuleAuditEventTypeDeclaration)(nil), // 35: saas.accounts.v1.ModuleAuditEventTypeDeclaration + (*ModuleDeclareAuditEventTypesRequest)(nil), // 36: saas.accounts.v1.ModuleDeclareAuditEventTypesRequest + (*ModuleDeclareAuditEventTypesResponse)(nil), // 37: saas.accounts.v1.ModuleDeclareAuditEventTypesResponse + (*SubjectVisibilityGrant)(nil), // 38: saas.accounts.v1.SubjectVisibilityGrant + (*ModuleListSubjectVisibilityRequest)(nil), // 39: saas.accounts.v1.ModuleListSubjectVisibilityRequest + (*ModuleListSubjectVisibilityResponse)(nil), // 40: saas.accounts.v1.ModuleListSubjectVisibilityResponse + (*FetchDatasourceBlobRequest)(nil), // 41: saas.accounts.v1.FetchDatasourceBlobRequest + (*FetchDatasourceBlobChunk)(nil), // 42: saas.accounts.v1.FetchDatasourceBlobChunk + (*DatasourceFileRef)(nil), // 43: saas.accounts.v1.DatasourceFileRef + (*FetchDatasourceFilesRequest)(nil), // 44: saas.accounts.v1.FetchDatasourceFilesRequest + (*DatasourceProvenance)(nil), // 45: saas.accounts.v1.DatasourceProvenance + (*DatasourceItemReaders)(nil), // 46: saas.accounts.v1.DatasourceItemReaders + (*DatasourceFileHeader)(nil), // 47: saas.accounts.v1.DatasourceFileHeader + (*FetchDatasourceFilesFrame)(nil), // 48: saas.accounts.v1.FetchDatasourceFilesFrame + (*ModulePublishEventRequest)(nil), // 49: saas.accounts.v1.ModulePublishEventRequest + (*ModulePublishEventResponse)(nil), // 50: saas.accounts.v1.ModulePublishEventResponse + (*ModuleSubscribeRequest)(nil), // 51: saas.accounts.v1.ModuleSubscribeRequest + (*ModuleSubscription)(nil), // 52: saas.accounts.v1.ModuleSubscription + (*ModuleSubscribeResponse)(nil), // 53: saas.accounts.v1.ModuleSubscribeResponse + (*ModuleUnsubscribeRequest)(nil), // 54: saas.accounts.v1.ModuleUnsubscribeRequest + (*ModuleListSubscriptionsRequest)(nil), // 55: saas.accounts.v1.ModuleListSubscriptionsRequest + (*ModuleListSubscriptionsResponse)(nil), // 56: saas.accounts.v1.ModuleListSubscriptionsResponse + (*ModuleReplayEventsRequest)(nil), // 57: saas.accounts.v1.ModuleReplayEventsRequest + (*ModuleReplayEventsResponse)(nil), // 58: saas.accounts.v1.ModuleReplayEventsResponse + (*ReadableSourceCollection)(nil), // 59: saas.accounts.v1.ReadableSourceCollection + (*ReadableCollectionGrant)(nil), // 60: saas.accounts.v1.ReadableCollectionGrant + (*CollectionSyncProvenance)(nil), // 61: saas.accounts.v1.CollectionSyncProvenance + (*ListReadableSourceCollectionsRequest)(nil), // 62: saas.accounts.v1.ListReadableSourceCollectionsRequest + (*ListReadableSourceCollectionsResponse)(nil), // 63: saas.accounts.v1.ListReadableSourceCollectionsResponse + (*ModulePlaceRecordRequest)(nil), // 64: saas.accounts.v1.ModulePlaceRecordRequest + (*ModulePlaceRecordResponse)(nil), // 65: saas.accounts.v1.ModulePlaceRecordResponse + (*CheckWorkContextRecordAccessRequest)(nil), // 66: saas.accounts.v1.CheckWorkContextRecordAccessRequest + (*CheckWorkContextRecordAccessResponse)(nil), // 67: saas.accounts.v1.CheckWorkContextRecordAccessResponse + (*ModuleExchangeDelegatedReadAudienceRequest)(nil), // 68: saas.accounts.v1.ModuleExchangeDelegatedReadAudienceRequest + (*ModuleExchangeDelegatedOperationAudienceRequest)(nil), // 69: saas.accounts.v1.ModuleExchangeDelegatedOperationAudienceRequest + (*ExecutableArtifactContract)(nil), // 70: saas.accounts.v1.ExecutableArtifactContract + (*ExecutableArtifactIdentity)(nil), // 71: saas.accounts.v1.ExecutableArtifactIdentity + (*ModuleExecutableArtifactRequest)(nil), // 72: saas.accounts.v1.ModuleExecutableArtifactRequest + (*ModuleRevokeExecutableArtifactRequest)(nil), // 73: saas.accounts.v1.ModuleRevokeExecutableArtifactRequest + (*ModuleExecutableArtifactResponse)(nil), // 74: saas.accounts.v1.ModuleExecutableArtifactResponse + (*ModuleCurrentInstallationRequest)(nil), // 75: saas.accounts.v1.ModuleCurrentInstallationRequest + (*ModuleCurrentInstallationResponse)(nil), // 76: saas.accounts.v1.ModuleCurrentInstallationResponse + (*WorkContextSealQuestion)(nil), // 77: saas.accounts.v1.WorkContextSealQuestion + (*WorkContextPrincipalEpochQuestion)(nil), // 78: saas.accounts.v1.WorkContextPrincipalEpochQuestion + (*WorkContextApprovedBuildQuestion)(nil), // 79: saas.accounts.v1.WorkContextApprovedBuildQuestion + (*WorkContextOperationBindingQuestion)(nil), // 80: saas.accounts.v1.WorkContextOperationBindingQuestion + (*WorkContextAuthorizationRevisionQuestion)(nil), // 81: saas.accounts.v1.WorkContextAuthorizationRevisionQuestion + (*WorkContextApprovalGrantQuestion)(nil), // 82: saas.accounts.v1.WorkContextApprovalGrantQuestion + (*WorkContextSealAnswer)(nil), // 83: saas.accounts.v1.WorkContextSealAnswer + (*WorkContextPrincipalEpochAnswer)(nil), // 84: saas.accounts.v1.WorkContextPrincipalEpochAnswer + (*WorkContextApprovedBuildAnswer)(nil), // 85: saas.accounts.v1.WorkContextApprovedBuildAnswer + (*WorkContextOperationBindingAnswer)(nil), // 86: saas.accounts.v1.WorkContextOperationBindingAnswer + (*WorkContextAuthorizationRevisionAnswer)(nil), // 87: saas.accounts.v1.WorkContextAuthorizationRevisionAnswer + (*WorkContextApprovalGrantAnswer)(nil), // 88: saas.accounts.v1.WorkContextApprovalGrantAnswer + (*ModuleResolveWorkContextAuthorityRequest)(nil), // 89: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest + (*ModuleResolveWorkContextAuthorityResponse)(nil), // 90: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse + (*v1.NewJob)(nil), // 91: saas.jobs.v1.NewJob + (v1.JobEnqueueDisposition)(0), // 92: saas.jobs.v1.JobEnqueueDisposition + (*durationpb.Duration)(nil), // 93: google.protobuf.Duration + (*v1.JobEnvelope)(nil), // 94: saas.jobs.v1.JobEnvelope + (*v1.JobLeaseReference)(nil), // 95: saas.jobs.v1.JobLeaseReference + (*v1.JobLease)(nil), // 96: saas.jobs.v1.JobLease + (*v1.JobFailure)(nil), // 97: saas.jobs.v1.JobFailure + (*timestamppb.Timestamp)(nil), // 98: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 99: google.protobuf.Struct + (*v11.EventEnvelope)(nil), // 100: saas.events.v1.EventEnvelope + (*ModuleMintWorkContextRequest)(nil), // 101: saas.accounts.v1.ModuleMintWorkContextRequest + (*ModuleMintOperationContextRequest)(nil), // 102: saas.accounts.v1.ModuleMintOperationContextRequest + (*ModuleMintSourceOperationContextRequest)(nil), // 103: saas.accounts.v1.ModuleMintSourceOperationContextRequest + (*IssuedWorkContext)(nil), // 104: saas.accounts.v1.IssuedWorkContext + (*emptypb.Empty)(nil), // 105: google.protobuf.Empty + (*ModuleMintWorkContextResponse)(nil), // 106: saas.accounts.v1.ModuleMintWorkContextResponse + (*ModuleMintOperationContextResponse)(nil), // 107: saas.accounts.v1.ModuleMintOperationContextResponse + (*ModuleMintSourceOperationContextResponse)(nil), // 108: saas.accounts.v1.ModuleMintSourceOperationContextResponse } var file_saas_accounts_v1_module_capabilities_proto_depIdxs = []int32{ - 88, // 0: saas.accounts.v1.ModuleEnqueueJobRequest.job:type_name -> saas.jobs.v1.NewJob - 89, // 1: saas.accounts.v1.ModuleEnqueueJobResponse.disposition:type_name -> saas.jobs.v1.JobEnqueueDisposition - 90, // 2: saas.accounts.v1.ModuleClaimJobsRequest.lease_duration:type_name -> google.protobuf.Duration - 91, // 3: saas.accounts.v1.ModuleClaimJobsResponse.jobs:type_name -> saas.jobs.v1.JobEnvelope - 92, // 4: saas.accounts.v1.ModuleHeartbeatJobRequest.lease:type_name -> saas.jobs.v1.JobLeaseReference - 90, // 5: saas.accounts.v1.ModuleHeartbeatJobRequest.extension:type_name -> google.protobuf.Duration - 93, // 6: saas.accounts.v1.ModuleHeartbeatJobResponse.lease:type_name -> saas.jobs.v1.JobLease - 92, // 7: saas.accounts.v1.ModuleAckJobRequest.lease:type_name -> saas.jobs.v1.JobLeaseReference - 92, // 8: saas.accounts.v1.ModuleNackJobRequest.lease:type_name -> saas.jobs.v1.JobLeaseReference - 94, // 9: saas.accounts.v1.ModuleNackJobRequest.failure:type_name -> saas.jobs.v1.JobFailure - 95, // 10: saas.accounts.v1.ModuleNackJobRequest.retry_at:type_name -> google.protobuf.Timestamp - 96, // 11: saas.accounts.v1.ModuleResumeRef.payload:type_name -> google.protobuf.Struct - 96, // 12: saas.accounts.v1.ModuleRequestApprovalRequest.subject:type_name -> google.protobuf.Struct + 91, // 0: saas.accounts.v1.ModuleEnqueueJobRequest.job:type_name -> saas.jobs.v1.NewJob + 92, // 1: saas.accounts.v1.ModuleEnqueueJobResponse.disposition:type_name -> saas.jobs.v1.JobEnqueueDisposition + 93, // 2: saas.accounts.v1.ModuleClaimJobsRequest.lease_duration:type_name -> google.protobuf.Duration + 94, // 3: saas.accounts.v1.ModuleClaimJobsResponse.jobs:type_name -> saas.jobs.v1.JobEnvelope + 95, // 4: saas.accounts.v1.ModuleHeartbeatJobRequest.lease:type_name -> saas.jobs.v1.JobLeaseReference + 93, // 5: saas.accounts.v1.ModuleHeartbeatJobRequest.extension:type_name -> google.protobuf.Duration + 96, // 6: saas.accounts.v1.ModuleHeartbeatJobResponse.lease:type_name -> saas.jobs.v1.JobLease + 95, // 7: saas.accounts.v1.ModuleAckJobRequest.lease:type_name -> saas.jobs.v1.JobLeaseReference + 95, // 8: saas.accounts.v1.ModuleNackJobRequest.lease:type_name -> saas.jobs.v1.JobLeaseReference + 97, // 9: saas.accounts.v1.ModuleNackJobRequest.failure:type_name -> saas.jobs.v1.JobFailure + 98, // 10: saas.accounts.v1.ModuleNackJobRequest.retry_at:type_name -> google.protobuf.Timestamp + 99, // 11: saas.accounts.v1.ModuleResumeRef.payload:type_name -> google.protobuf.Struct + 99, // 12: saas.accounts.v1.ModuleRequestApprovalRequest.subject:type_name -> google.protobuf.Struct 23, // 13: saas.accounts.v1.ModuleRequestApprovalRequest.policy:type_name -> saas.accounts.v1.ModuleApprovalPolicy 24, // 14: saas.accounts.v1.ModuleRequestApprovalRequest.resume_ref:type_name -> saas.accounts.v1.ModuleResumeRef - 95, // 15: saas.accounts.v1.ModuleRequestApprovalRequest.expires_at:type_name -> google.protobuf.Timestamp - 95, // 16: saas.accounts.v1.ModuleRequestApprovalRequest.escalate_at:type_name -> google.protobuf.Timestamp - 96, // 17: saas.accounts.v1.ModuleApproval.subject:type_name -> google.protobuf.Struct + 98, // 15: saas.accounts.v1.ModuleRequestApprovalRequest.expires_at:type_name -> google.protobuf.Timestamp + 98, // 16: saas.accounts.v1.ModuleRequestApprovalRequest.escalate_at:type_name -> google.protobuf.Timestamp + 99, // 17: saas.accounts.v1.ModuleApproval.subject:type_name -> google.protobuf.Struct 24, // 18: saas.accounts.v1.ModuleApproval.resume_ref:type_name -> saas.accounts.v1.ModuleResumeRef - 95, // 19: saas.accounts.v1.ModuleApproval.expires_at:type_name -> google.protobuf.Timestamp - 95, // 20: saas.accounts.v1.ModuleApproval.escalate_at:type_name -> google.protobuf.Timestamp - 95, // 21: saas.accounts.v1.ModuleApproval.created_at:type_name -> google.protobuf.Timestamp - 96, // 22: saas.accounts.v1.ModuleEmitAuditEventRequest.fields:type_name -> google.protobuf.Struct - 0, // 23: saas.accounts.v1.ModuleAuditFieldDeclaration.kind:type_name -> saas.accounts.v1.ModuleAuditFieldKind - 31, // 24: saas.accounts.v1.ModuleAuditEventTypeDeclaration.fields:type_name -> saas.accounts.v1.ModuleAuditFieldDeclaration - 1, // 25: saas.accounts.v1.ModuleAuditEventTypeDeclaration.visibility:type_name -> saas.accounts.v1.ModuleAuditEventVisibility - 32, // 26: saas.accounts.v1.ModuleDeclareAuditEventTypesRequest.types:type_name -> saas.accounts.v1.ModuleAuditEventTypeDeclaration - 95, // 27: saas.accounts.v1.SubjectVisibilityGrant.expires_at:type_name -> google.protobuf.Timestamp - 35, // 28: saas.accounts.v1.ModuleListSubjectVisibilityResponse.grants:type_name -> saas.accounts.v1.SubjectVisibilityGrant - 40, // 29: saas.accounts.v1.FetchDatasourceFilesRequest.files:type_name -> saas.accounts.v1.DatasourceFileRef - 2, // 30: saas.accounts.v1.DatasourceItemReaders.basis:type_name -> saas.accounts.v1.DatasourceItemReadersBasis - 42, // 31: saas.accounts.v1.DatasourceFileHeader.provenance:type_name -> saas.accounts.v1.DatasourceProvenance - 43, // 32: saas.accounts.v1.DatasourceFileHeader.readers:type_name -> saas.accounts.v1.DatasourceItemReaders - 44, // 33: saas.accounts.v1.FetchDatasourceFilesFrame.header:type_name -> saas.accounts.v1.DatasourceFileHeader - 97, // 34: saas.accounts.v1.ModulePublishEventRequest.envelope:type_name -> saas.events.v1.EventEnvelope - 3, // 35: saas.accounts.v1.ModuleSubscribeRequest.delivery:type_name -> saas.accounts.v1.EventDelivery - 3, // 36: saas.accounts.v1.ModuleSubscription.delivery:type_name -> saas.accounts.v1.EventDelivery - 95, // 37: saas.accounts.v1.ModuleSubscription.created_at:type_name -> google.protobuf.Timestamp - 49, // 38: saas.accounts.v1.ModuleSubscribeResponse.subscription:type_name -> saas.accounts.v1.ModuleSubscription - 49, // 39: saas.accounts.v1.ModuleListSubscriptionsResponse.subscriptions:type_name -> saas.accounts.v1.ModuleSubscription - 95, // 40: saas.accounts.v1.ModuleReplayEventsRequest.since:type_name -> google.protobuf.Timestamp - 4, // 41: saas.accounts.v1.ReadableSourceCollection.grant_disclosure:type_name -> saas.accounts.v1.MetadataDisclosure - 57, // 42: saas.accounts.v1.ReadableSourceCollection.read_grants:type_name -> saas.accounts.v1.ReadableCollectionGrant - 58, // 43: saas.accounts.v1.ReadableSourceCollection.sync:type_name -> saas.accounts.v1.CollectionSyncProvenance - 95, // 44: saas.accounts.v1.ReadableCollectionGrant.expires_at:type_name -> google.protobuf.Timestamp - 5, // 45: saas.accounts.v1.CollectionSyncProvenance.stage:type_name -> saas.accounts.v1.SourceSyncStage - 95, // 46: saas.accounts.v1.CollectionSyncProvenance.at:type_name -> google.protobuf.Timestamp - 4, // 47: saas.accounts.v1.CollectionSyncProvenance.requester_disclosure:type_name -> saas.accounts.v1.MetadataDisclosure - 95, // 48: saas.accounts.v1.CollectionSyncProvenance.requested_at:type_name -> google.protobuf.Timestamp - 56, // 49: saas.accounts.v1.ListReadableSourceCollectionsResponse.collections:type_name -> saas.accounts.v1.ReadableSourceCollection - 67, // 50: saas.accounts.v1.ExecutableArtifactIdentity.contracts:type_name -> saas.accounts.v1.ExecutableArtifactContract - 68, // 51: saas.accounts.v1.ModuleExecutableArtifactRequest.identity:type_name -> saas.accounts.v1.ExecutableArtifactIdentity - 6, // 52: saas.accounts.v1.WorkContextSealAnswer.state:type_name -> saas.accounts.v1.WorkContextSealState - 7, // 53: saas.accounts.v1.WorkContextPrincipalEpochAnswer.state:type_name -> saas.accounts.v1.WorkContextPrincipalState - 8, // 54: saas.accounts.v1.WorkContextApprovedBuildAnswer.state:type_name -> saas.accounts.v1.WorkContextApprovedBuildState - 9, // 55: saas.accounts.v1.WorkContextOperationBindingAnswer.state:type_name -> saas.accounts.v1.WorkContextOperationBindingState - 10, // 56: saas.accounts.v1.WorkContextApprovalGrantAnswer.state:type_name -> saas.accounts.v1.WorkContextApprovalGrantState - 74, // 57: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest.seal:type_name -> saas.accounts.v1.WorkContextSealQuestion - 75, // 58: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest.principal_epoch:type_name -> saas.accounts.v1.WorkContextPrincipalEpochQuestion - 76, // 59: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest.approved_build:type_name -> saas.accounts.v1.WorkContextApprovedBuildQuestion - 77, // 60: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest.operation_binding:type_name -> saas.accounts.v1.WorkContextOperationBindingQuestion - 78, // 61: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest.authorization_revision:type_name -> saas.accounts.v1.WorkContextAuthorizationRevisionQuestion - 79, // 62: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest.approval_grant:type_name -> saas.accounts.v1.WorkContextApprovalGrantQuestion - 80, // 63: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse.seal:type_name -> saas.accounts.v1.WorkContextSealAnswer - 81, // 64: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse.principal_epoch:type_name -> saas.accounts.v1.WorkContextPrincipalEpochAnswer - 82, // 65: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse.approved_build:type_name -> saas.accounts.v1.WorkContextApprovedBuildAnswer - 83, // 66: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse.operation_binding:type_name -> saas.accounts.v1.WorkContextOperationBindingAnswer - 84, // 67: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse.authorization_revision:type_name -> saas.accounts.v1.WorkContextAuthorizationRevisionAnswer - 85, // 68: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse.approval_grant:type_name -> saas.accounts.v1.WorkContextApprovalGrantAnswer - 72, // 69: saas.accounts.v1.ModuleCapabilitiesService.GetCurrentInstallation:input_type -> saas.accounts.v1.ModuleCurrentInstallationRequest - 69, // 70: saas.accounts.v1.ModuleCapabilitiesService.ApproveExecutableArtifact:input_type -> saas.accounts.v1.ModuleExecutableArtifactRequest - 69, // 71: saas.accounts.v1.ModuleCapabilitiesService.AuthorizeExecutableArtifact:input_type -> saas.accounts.v1.ModuleExecutableArtifactRequest - 70, // 72: saas.accounts.v1.ModuleCapabilitiesService.RevokeExecutableArtifact:input_type -> saas.accounts.v1.ModuleRevokeExecutableArtifactRequest - 65, // 73: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedReadAudience:input_type -> saas.accounts.v1.ModuleExchangeDelegatedReadAudienceRequest - 66, // 74: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedOperationAudience:input_type -> saas.accounts.v1.ModuleExchangeDelegatedOperationAudienceRequest - 63, // 75: saas.accounts.v1.ModuleCapabilitiesService.CheckWorkContextRecordAccess:input_type -> saas.accounts.v1.CheckWorkContextRecordAccessRequest - 59, // 76: saas.accounts.v1.ModuleCapabilitiesService.ListReadableSourceCollections:input_type -> saas.accounts.v1.ListReadableSourceCollectionsRequest - 61, // 77: saas.accounts.v1.ModuleCapabilitiesService.PlaceRecord:input_type -> saas.accounts.v1.ModulePlaceRecordRequest - 11, // 78: saas.accounts.v1.ModuleCapabilitiesService.EnqueueJob:input_type -> saas.accounts.v1.ModuleEnqueueJobRequest - 13, // 79: saas.accounts.v1.ModuleCapabilitiesService.ClaimJobs:input_type -> saas.accounts.v1.ModuleClaimJobsRequest - 15, // 80: saas.accounts.v1.ModuleCapabilitiesService.HeartbeatJob:input_type -> saas.accounts.v1.ModuleHeartbeatJobRequest - 17, // 81: saas.accounts.v1.ModuleCapabilitiesService.AckJob:input_type -> saas.accounts.v1.ModuleAckJobRequest - 18, // 82: saas.accounts.v1.ModuleCapabilitiesService.NackJob:input_type -> saas.accounts.v1.ModuleNackJobRequest - 19, // 83: saas.accounts.v1.ModuleCapabilitiesService.NotifyUser:input_type -> saas.accounts.v1.ModuleNotifyUserRequest - 21, // 84: saas.accounts.v1.ModuleCapabilitiesService.NotifyOrgAdmins:input_type -> saas.accounts.v1.ModuleNotifyOrgAdminsRequest - 25, // 85: saas.accounts.v1.ModuleCapabilitiesService.RequestApproval:input_type -> saas.accounts.v1.ModuleRequestApprovalRequest - 27, // 86: saas.accounts.v1.ModuleCapabilitiesService.GetApproval:input_type -> saas.accounts.v1.ModuleGetApprovalRequest - 29, // 87: saas.accounts.v1.ModuleCapabilitiesService.CancelApproval:input_type -> saas.accounts.v1.ModuleCancelApprovalRequest - 30, // 88: saas.accounts.v1.ModuleCapabilitiesService.EmitAuditEvent:input_type -> saas.accounts.v1.ModuleEmitAuditEventRequest - 33, // 89: saas.accounts.v1.ModuleCapabilitiesService.DeclareAuditEventTypes:input_type -> saas.accounts.v1.ModuleDeclareAuditEventTypesRequest - 36, // 90: saas.accounts.v1.ModuleCapabilitiesService.ListSubjectVisibility:input_type -> saas.accounts.v1.ModuleListSubjectVisibilityRequest - 38, // 91: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceBlob:input_type -> saas.accounts.v1.FetchDatasourceBlobRequest - 41, // 92: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceFiles:input_type -> saas.accounts.v1.FetchDatasourceFilesRequest - 98, // 93: saas.accounts.v1.ModuleCapabilitiesService.MintModuleWorkContext:input_type -> saas.accounts.v1.ModuleMintWorkContextRequest - 99, // 94: saas.accounts.v1.ModuleCapabilitiesService.MintModuleOperationContext:input_type -> saas.accounts.v1.ModuleMintOperationContextRequest - 100, // 95: saas.accounts.v1.ModuleCapabilitiesService.MintSourceOperationContext:input_type -> saas.accounts.v1.ModuleMintSourceOperationContextRequest - 46, // 96: saas.accounts.v1.ModuleCapabilitiesService.PublishEvent:input_type -> saas.accounts.v1.ModulePublishEventRequest - 48, // 97: saas.accounts.v1.ModuleCapabilitiesService.Subscribe:input_type -> saas.accounts.v1.ModuleSubscribeRequest - 51, // 98: saas.accounts.v1.ModuleCapabilitiesService.Unsubscribe:input_type -> saas.accounts.v1.ModuleUnsubscribeRequest - 52, // 99: saas.accounts.v1.ModuleCapabilitiesService.ListSubscriptions:input_type -> saas.accounts.v1.ModuleListSubscriptionsRequest - 54, // 100: saas.accounts.v1.ModuleCapabilitiesService.ReplayEvents:input_type -> saas.accounts.v1.ModuleReplayEventsRequest - 86, // 101: saas.accounts.v1.ModuleCapabilitiesService.ResolveWorkContextAuthority:input_type -> saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest - 73, // 102: saas.accounts.v1.ModuleCapabilitiesService.GetCurrentInstallation:output_type -> saas.accounts.v1.ModuleCurrentInstallationResponse - 71, // 103: saas.accounts.v1.ModuleCapabilitiesService.ApproveExecutableArtifact:output_type -> saas.accounts.v1.ModuleExecutableArtifactResponse - 71, // 104: saas.accounts.v1.ModuleCapabilitiesService.AuthorizeExecutableArtifact:output_type -> saas.accounts.v1.ModuleExecutableArtifactResponse - 71, // 105: saas.accounts.v1.ModuleCapabilitiesService.RevokeExecutableArtifact:output_type -> saas.accounts.v1.ModuleExecutableArtifactResponse - 101, // 106: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedReadAudience:output_type -> saas.accounts.v1.IssuedWorkContext - 101, // 107: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedOperationAudience:output_type -> saas.accounts.v1.IssuedWorkContext - 64, // 108: saas.accounts.v1.ModuleCapabilitiesService.CheckWorkContextRecordAccess:output_type -> saas.accounts.v1.CheckWorkContextRecordAccessResponse - 60, // 109: saas.accounts.v1.ModuleCapabilitiesService.ListReadableSourceCollections:output_type -> saas.accounts.v1.ListReadableSourceCollectionsResponse - 62, // 110: saas.accounts.v1.ModuleCapabilitiesService.PlaceRecord:output_type -> saas.accounts.v1.ModulePlaceRecordResponse - 12, // 111: saas.accounts.v1.ModuleCapabilitiesService.EnqueueJob:output_type -> saas.accounts.v1.ModuleEnqueueJobResponse - 14, // 112: saas.accounts.v1.ModuleCapabilitiesService.ClaimJobs:output_type -> saas.accounts.v1.ModuleClaimJobsResponse - 16, // 113: saas.accounts.v1.ModuleCapabilitiesService.HeartbeatJob:output_type -> saas.accounts.v1.ModuleHeartbeatJobResponse - 102, // 114: saas.accounts.v1.ModuleCapabilitiesService.AckJob:output_type -> google.protobuf.Empty - 102, // 115: saas.accounts.v1.ModuleCapabilitiesService.NackJob:output_type -> google.protobuf.Empty - 20, // 116: saas.accounts.v1.ModuleCapabilitiesService.NotifyUser:output_type -> saas.accounts.v1.ModuleNotifyUserResponse - 22, // 117: saas.accounts.v1.ModuleCapabilitiesService.NotifyOrgAdmins:output_type -> saas.accounts.v1.ModuleNotifyOrgAdminsResponse - 26, // 118: saas.accounts.v1.ModuleCapabilitiesService.RequestApproval:output_type -> saas.accounts.v1.ModuleRequestApprovalResponse - 28, // 119: saas.accounts.v1.ModuleCapabilitiesService.GetApproval:output_type -> saas.accounts.v1.ModuleApproval - 102, // 120: saas.accounts.v1.ModuleCapabilitiesService.CancelApproval:output_type -> google.protobuf.Empty - 102, // 121: saas.accounts.v1.ModuleCapabilitiesService.EmitAuditEvent:output_type -> google.protobuf.Empty - 34, // 122: saas.accounts.v1.ModuleCapabilitiesService.DeclareAuditEventTypes:output_type -> saas.accounts.v1.ModuleDeclareAuditEventTypesResponse - 37, // 123: saas.accounts.v1.ModuleCapabilitiesService.ListSubjectVisibility:output_type -> saas.accounts.v1.ModuleListSubjectVisibilityResponse - 39, // 124: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceBlob:output_type -> saas.accounts.v1.FetchDatasourceBlobChunk - 45, // 125: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceFiles:output_type -> saas.accounts.v1.FetchDatasourceFilesFrame - 103, // 126: saas.accounts.v1.ModuleCapabilitiesService.MintModuleWorkContext:output_type -> saas.accounts.v1.ModuleMintWorkContextResponse - 104, // 127: saas.accounts.v1.ModuleCapabilitiesService.MintModuleOperationContext:output_type -> saas.accounts.v1.ModuleMintOperationContextResponse - 105, // 128: saas.accounts.v1.ModuleCapabilitiesService.MintSourceOperationContext:output_type -> saas.accounts.v1.ModuleMintSourceOperationContextResponse - 47, // 129: saas.accounts.v1.ModuleCapabilitiesService.PublishEvent:output_type -> saas.accounts.v1.ModulePublishEventResponse - 50, // 130: saas.accounts.v1.ModuleCapabilitiesService.Subscribe:output_type -> saas.accounts.v1.ModuleSubscribeResponse - 102, // 131: saas.accounts.v1.ModuleCapabilitiesService.Unsubscribe:output_type -> google.protobuf.Empty - 53, // 132: saas.accounts.v1.ModuleCapabilitiesService.ListSubscriptions:output_type -> saas.accounts.v1.ModuleListSubscriptionsResponse - 55, // 133: saas.accounts.v1.ModuleCapabilitiesService.ReplayEvents:output_type -> saas.accounts.v1.ModuleReplayEventsResponse - 87, // 134: saas.accounts.v1.ModuleCapabilitiesService.ResolveWorkContextAuthority:output_type -> saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse - 102, // [102:135] is the sub-list for method output_type - 69, // [69:102] is the sub-list for method input_type - 69, // [69:69] is the sub-list for extension type_name - 69, // [69:69] is the sub-list for extension extendee - 0, // [0:69] is the sub-list for field type_name + 98, // 19: saas.accounts.v1.ModuleApproval.expires_at:type_name -> google.protobuf.Timestamp + 98, // 20: saas.accounts.v1.ModuleApproval.escalate_at:type_name -> google.protobuf.Timestamp + 98, // 21: saas.accounts.v1.ModuleApproval.created_at:type_name -> google.protobuf.Timestamp + 99, // 22: saas.accounts.v1.ModuleEmitAuditEventRequest.fields:type_name -> google.protobuf.Struct + 98, // 23: saas.accounts.v1.ModuleJournalEntry.occurred_at:type_name -> google.protobuf.Timestamp + 99, // 24: saas.accounts.v1.ModuleJournalEntry.data:type_name -> google.protobuf.Struct + 32, // 25: saas.accounts.v1.ModuleReadJournalResponse.entries:type_name -> saas.accounts.v1.ModuleJournalEntry + 0, // 26: saas.accounts.v1.ModuleAuditFieldDeclaration.kind:type_name -> saas.accounts.v1.ModuleAuditFieldKind + 34, // 27: saas.accounts.v1.ModuleAuditEventTypeDeclaration.fields:type_name -> saas.accounts.v1.ModuleAuditFieldDeclaration + 1, // 28: saas.accounts.v1.ModuleAuditEventTypeDeclaration.visibility:type_name -> saas.accounts.v1.ModuleAuditEventVisibility + 35, // 29: saas.accounts.v1.ModuleDeclareAuditEventTypesRequest.types:type_name -> saas.accounts.v1.ModuleAuditEventTypeDeclaration + 98, // 30: saas.accounts.v1.SubjectVisibilityGrant.expires_at:type_name -> google.protobuf.Timestamp + 38, // 31: saas.accounts.v1.ModuleListSubjectVisibilityResponse.grants:type_name -> saas.accounts.v1.SubjectVisibilityGrant + 43, // 32: saas.accounts.v1.FetchDatasourceFilesRequest.files:type_name -> saas.accounts.v1.DatasourceFileRef + 2, // 33: saas.accounts.v1.DatasourceItemReaders.basis:type_name -> saas.accounts.v1.DatasourceItemReadersBasis + 45, // 34: saas.accounts.v1.DatasourceFileHeader.provenance:type_name -> saas.accounts.v1.DatasourceProvenance + 46, // 35: saas.accounts.v1.DatasourceFileHeader.readers:type_name -> saas.accounts.v1.DatasourceItemReaders + 47, // 36: saas.accounts.v1.FetchDatasourceFilesFrame.header:type_name -> saas.accounts.v1.DatasourceFileHeader + 100, // 37: saas.accounts.v1.ModulePublishEventRequest.envelope:type_name -> saas.events.v1.EventEnvelope + 3, // 38: saas.accounts.v1.ModuleSubscribeRequest.delivery:type_name -> saas.accounts.v1.EventDelivery + 3, // 39: saas.accounts.v1.ModuleSubscription.delivery:type_name -> saas.accounts.v1.EventDelivery + 98, // 40: saas.accounts.v1.ModuleSubscription.created_at:type_name -> google.protobuf.Timestamp + 52, // 41: saas.accounts.v1.ModuleSubscribeResponse.subscription:type_name -> saas.accounts.v1.ModuleSubscription + 52, // 42: saas.accounts.v1.ModuleListSubscriptionsResponse.subscriptions:type_name -> saas.accounts.v1.ModuleSubscription + 98, // 43: saas.accounts.v1.ModuleReplayEventsRequest.since:type_name -> google.protobuf.Timestamp + 4, // 44: saas.accounts.v1.ReadableSourceCollection.grant_disclosure:type_name -> saas.accounts.v1.MetadataDisclosure + 60, // 45: saas.accounts.v1.ReadableSourceCollection.read_grants:type_name -> saas.accounts.v1.ReadableCollectionGrant + 61, // 46: saas.accounts.v1.ReadableSourceCollection.sync:type_name -> saas.accounts.v1.CollectionSyncProvenance + 98, // 47: saas.accounts.v1.ReadableCollectionGrant.expires_at:type_name -> google.protobuf.Timestamp + 5, // 48: saas.accounts.v1.CollectionSyncProvenance.stage:type_name -> saas.accounts.v1.SourceSyncStage + 98, // 49: saas.accounts.v1.CollectionSyncProvenance.at:type_name -> google.protobuf.Timestamp + 4, // 50: saas.accounts.v1.CollectionSyncProvenance.requester_disclosure:type_name -> saas.accounts.v1.MetadataDisclosure + 98, // 51: saas.accounts.v1.CollectionSyncProvenance.requested_at:type_name -> google.protobuf.Timestamp + 59, // 52: saas.accounts.v1.ListReadableSourceCollectionsResponse.collections:type_name -> saas.accounts.v1.ReadableSourceCollection + 70, // 53: saas.accounts.v1.ExecutableArtifactIdentity.contracts:type_name -> saas.accounts.v1.ExecutableArtifactContract + 71, // 54: saas.accounts.v1.ModuleExecutableArtifactRequest.identity:type_name -> saas.accounts.v1.ExecutableArtifactIdentity + 6, // 55: saas.accounts.v1.WorkContextSealAnswer.state:type_name -> saas.accounts.v1.WorkContextSealState + 7, // 56: saas.accounts.v1.WorkContextPrincipalEpochAnswer.state:type_name -> saas.accounts.v1.WorkContextPrincipalState + 8, // 57: saas.accounts.v1.WorkContextApprovedBuildAnswer.state:type_name -> saas.accounts.v1.WorkContextApprovedBuildState + 9, // 58: saas.accounts.v1.WorkContextOperationBindingAnswer.state:type_name -> saas.accounts.v1.WorkContextOperationBindingState + 10, // 59: saas.accounts.v1.WorkContextApprovalGrantAnswer.state:type_name -> saas.accounts.v1.WorkContextApprovalGrantState + 77, // 60: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest.seal:type_name -> saas.accounts.v1.WorkContextSealQuestion + 78, // 61: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest.principal_epoch:type_name -> saas.accounts.v1.WorkContextPrincipalEpochQuestion + 79, // 62: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest.approved_build:type_name -> saas.accounts.v1.WorkContextApprovedBuildQuestion + 80, // 63: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest.operation_binding:type_name -> saas.accounts.v1.WorkContextOperationBindingQuestion + 81, // 64: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest.authorization_revision:type_name -> saas.accounts.v1.WorkContextAuthorizationRevisionQuestion + 82, // 65: saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest.approval_grant:type_name -> saas.accounts.v1.WorkContextApprovalGrantQuestion + 83, // 66: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse.seal:type_name -> saas.accounts.v1.WorkContextSealAnswer + 84, // 67: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse.principal_epoch:type_name -> saas.accounts.v1.WorkContextPrincipalEpochAnswer + 85, // 68: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse.approved_build:type_name -> saas.accounts.v1.WorkContextApprovedBuildAnswer + 86, // 69: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse.operation_binding:type_name -> saas.accounts.v1.WorkContextOperationBindingAnswer + 87, // 70: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse.authorization_revision:type_name -> saas.accounts.v1.WorkContextAuthorizationRevisionAnswer + 88, // 71: saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse.approval_grant:type_name -> saas.accounts.v1.WorkContextApprovalGrantAnswer + 75, // 72: saas.accounts.v1.ModuleCapabilitiesService.GetCurrentInstallation:input_type -> saas.accounts.v1.ModuleCurrentInstallationRequest + 72, // 73: saas.accounts.v1.ModuleCapabilitiesService.ApproveExecutableArtifact:input_type -> saas.accounts.v1.ModuleExecutableArtifactRequest + 72, // 74: saas.accounts.v1.ModuleCapabilitiesService.AuthorizeExecutableArtifact:input_type -> saas.accounts.v1.ModuleExecutableArtifactRequest + 73, // 75: saas.accounts.v1.ModuleCapabilitiesService.RevokeExecutableArtifact:input_type -> saas.accounts.v1.ModuleRevokeExecutableArtifactRequest + 68, // 76: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedReadAudience:input_type -> saas.accounts.v1.ModuleExchangeDelegatedReadAudienceRequest + 69, // 77: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedOperationAudience:input_type -> saas.accounts.v1.ModuleExchangeDelegatedOperationAudienceRequest + 66, // 78: saas.accounts.v1.ModuleCapabilitiesService.CheckWorkContextRecordAccess:input_type -> saas.accounts.v1.CheckWorkContextRecordAccessRequest + 62, // 79: saas.accounts.v1.ModuleCapabilitiesService.ListReadableSourceCollections:input_type -> saas.accounts.v1.ListReadableSourceCollectionsRequest + 64, // 80: saas.accounts.v1.ModuleCapabilitiesService.PlaceRecord:input_type -> saas.accounts.v1.ModulePlaceRecordRequest + 11, // 81: saas.accounts.v1.ModuleCapabilitiesService.EnqueueJob:input_type -> saas.accounts.v1.ModuleEnqueueJobRequest + 13, // 82: saas.accounts.v1.ModuleCapabilitiesService.ClaimJobs:input_type -> saas.accounts.v1.ModuleClaimJobsRequest + 15, // 83: saas.accounts.v1.ModuleCapabilitiesService.HeartbeatJob:input_type -> saas.accounts.v1.ModuleHeartbeatJobRequest + 17, // 84: saas.accounts.v1.ModuleCapabilitiesService.AckJob:input_type -> saas.accounts.v1.ModuleAckJobRequest + 18, // 85: saas.accounts.v1.ModuleCapabilitiesService.NackJob:input_type -> saas.accounts.v1.ModuleNackJobRequest + 19, // 86: saas.accounts.v1.ModuleCapabilitiesService.NotifyUser:input_type -> saas.accounts.v1.ModuleNotifyUserRequest + 21, // 87: saas.accounts.v1.ModuleCapabilitiesService.NotifyOrgAdmins:input_type -> saas.accounts.v1.ModuleNotifyOrgAdminsRequest + 25, // 88: saas.accounts.v1.ModuleCapabilitiesService.RequestApproval:input_type -> saas.accounts.v1.ModuleRequestApprovalRequest + 27, // 89: saas.accounts.v1.ModuleCapabilitiesService.GetApproval:input_type -> saas.accounts.v1.ModuleGetApprovalRequest + 29, // 90: saas.accounts.v1.ModuleCapabilitiesService.CancelApproval:input_type -> saas.accounts.v1.ModuleCancelApprovalRequest + 30, // 91: saas.accounts.v1.ModuleCapabilitiesService.EmitAuditEvent:input_type -> saas.accounts.v1.ModuleEmitAuditEventRequest + 31, // 92: saas.accounts.v1.ModuleCapabilitiesService.ReadJournal:input_type -> saas.accounts.v1.ModuleReadJournalRequest + 36, // 93: saas.accounts.v1.ModuleCapabilitiesService.DeclareAuditEventTypes:input_type -> saas.accounts.v1.ModuleDeclareAuditEventTypesRequest + 39, // 94: saas.accounts.v1.ModuleCapabilitiesService.ListSubjectVisibility:input_type -> saas.accounts.v1.ModuleListSubjectVisibilityRequest + 41, // 95: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceBlob:input_type -> saas.accounts.v1.FetchDatasourceBlobRequest + 44, // 96: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceFiles:input_type -> saas.accounts.v1.FetchDatasourceFilesRequest + 101, // 97: saas.accounts.v1.ModuleCapabilitiesService.MintModuleWorkContext:input_type -> saas.accounts.v1.ModuleMintWorkContextRequest + 102, // 98: saas.accounts.v1.ModuleCapabilitiesService.MintModuleOperationContext:input_type -> saas.accounts.v1.ModuleMintOperationContextRequest + 103, // 99: saas.accounts.v1.ModuleCapabilitiesService.MintSourceOperationContext:input_type -> saas.accounts.v1.ModuleMintSourceOperationContextRequest + 49, // 100: saas.accounts.v1.ModuleCapabilitiesService.PublishEvent:input_type -> saas.accounts.v1.ModulePublishEventRequest + 51, // 101: saas.accounts.v1.ModuleCapabilitiesService.Subscribe:input_type -> saas.accounts.v1.ModuleSubscribeRequest + 54, // 102: saas.accounts.v1.ModuleCapabilitiesService.Unsubscribe:input_type -> saas.accounts.v1.ModuleUnsubscribeRequest + 55, // 103: saas.accounts.v1.ModuleCapabilitiesService.ListSubscriptions:input_type -> saas.accounts.v1.ModuleListSubscriptionsRequest + 57, // 104: saas.accounts.v1.ModuleCapabilitiesService.ReplayEvents:input_type -> saas.accounts.v1.ModuleReplayEventsRequest + 89, // 105: saas.accounts.v1.ModuleCapabilitiesService.ResolveWorkContextAuthority:input_type -> saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest + 76, // 106: saas.accounts.v1.ModuleCapabilitiesService.GetCurrentInstallation:output_type -> saas.accounts.v1.ModuleCurrentInstallationResponse + 74, // 107: saas.accounts.v1.ModuleCapabilitiesService.ApproveExecutableArtifact:output_type -> saas.accounts.v1.ModuleExecutableArtifactResponse + 74, // 108: saas.accounts.v1.ModuleCapabilitiesService.AuthorizeExecutableArtifact:output_type -> saas.accounts.v1.ModuleExecutableArtifactResponse + 74, // 109: saas.accounts.v1.ModuleCapabilitiesService.RevokeExecutableArtifact:output_type -> saas.accounts.v1.ModuleExecutableArtifactResponse + 104, // 110: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedReadAudience:output_type -> saas.accounts.v1.IssuedWorkContext + 104, // 111: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedOperationAudience:output_type -> saas.accounts.v1.IssuedWorkContext + 67, // 112: saas.accounts.v1.ModuleCapabilitiesService.CheckWorkContextRecordAccess:output_type -> saas.accounts.v1.CheckWorkContextRecordAccessResponse + 63, // 113: saas.accounts.v1.ModuleCapabilitiesService.ListReadableSourceCollections:output_type -> saas.accounts.v1.ListReadableSourceCollectionsResponse + 65, // 114: saas.accounts.v1.ModuleCapabilitiesService.PlaceRecord:output_type -> saas.accounts.v1.ModulePlaceRecordResponse + 12, // 115: saas.accounts.v1.ModuleCapabilitiesService.EnqueueJob:output_type -> saas.accounts.v1.ModuleEnqueueJobResponse + 14, // 116: saas.accounts.v1.ModuleCapabilitiesService.ClaimJobs:output_type -> saas.accounts.v1.ModuleClaimJobsResponse + 16, // 117: saas.accounts.v1.ModuleCapabilitiesService.HeartbeatJob:output_type -> saas.accounts.v1.ModuleHeartbeatJobResponse + 105, // 118: saas.accounts.v1.ModuleCapabilitiesService.AckJob:output_type -> google.protobuf.Empty + 105, // 119: saas.accounts.v1.ModuleCapabilitiesService.NackJob:output_type -> google.protobuf.Empty + 20, // 120: saas.accounts.v1.ModuleCapabilitiesService.NotifyUser:output_type -> saas.accounts.v1.ModuleNotifyUserResponse + 22, // 121: saas.accounts.v1.ModuleCapabilitiesService.NotifyOrgAdmins:output_type -> saas.accounts.v1.ModuleNotifyOrgAdminsResponse + 26, // 122: saas.accounts.v1.ModuleCapabilitiesService.RequestApproval:output_type -> saas.accounts.v1.ModuleRequestApprovalResponse + 28, // 123: saas.accounts.v1.ModuleCapabilitiesService.GetApproval:output_type -> saas.accounts.v1.ModuleApproval + 105, // 124: saas.accounts.v1.ModuleCapabilitiesService.CancelApproval:output_type -> google.protobuf.Empty + 105, // 125: saas.accounts.v1.ModuleCapabilitiesService.EmitAuditEvent:output_type -> google.protobuf.Empty + 33, // 126: saas.accounts.v1.ModuleCapabilitiesService.ReadJournal:output_type -> saas.accounts.v1.ModuleReadJournalResponse + 37, // 127: saas.accounts.v1.ModuleCapabilitiesService.DeclareAuditEventTypes:output_type -> saas.accounts.v1.ModuleDeclareAuditEventTypesResponse + 40, // 128: saas.accounts.v1.ModuleCapabilitiesService.ListSubjectVisibility:output_type -> saas.accounts.v1.ModuleListSubjectVisibilityResponse + 42, // 129: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceBlob:output_type -> saas.accounts.v1.FetchDatasourceBlobChunk + 48, // 130: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceFiles:output_type -> saas.accounts.v1.FetchDatasourceFilesFrame + 106, // 131: saas.accounts.v1.ModuleCapabilitiesService.MintModuleWorkContext:output_type -> saas.accounts.v1.ModuleMintWorkContextResponse + 107, // 132: saas.accounts.v1.ModuleCapabilitiesService.MintModuleOperationContext:output_type -> saas.accounts.v1.ModuleMintOperationContextResponse + 108, // 133: saas.accounts.v1.ModuleCapabilitiesService.MintSourceOperationContext:output_type -> saas.accounts.v1.ModuleMintSourceOperationContextResponse + 50, // 134: saas.accounts.v1.ModuleCapabilitiesService.PublishEvent:output_type -> saas.accounts.v1.ModulePublishEventResponse + 53, // 135: saas.accounts.v1.ModuleCapabilitiesService.Subscribe:output_type -> saas.accounts.v1.ModuleSubscribeResponse + 105, // 136: saas.accounts.v1.ModuleCapabilitiesService.Unsubscribe:output_type -> google.protobuf.Empty + 56, // 137: saas.accounts.v1.ModuleCapabilitiesService.ListSubscriptions:output_type -> saas.accounts.v1.ModuleListSubscriptionsResponse + 58, // 138: saas.accounts.v1.ModuleCapabilitiesService.ReplayEvents:output_type -> saas.accounts.v1.ModuleReplayEventsResponse + 90, // 139: saas.accounts.v1.ModuleCapabilitiesService.ResolveWorkContextAuthority:output_type -> saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse + 106, // [106:140] is the sub-list for method output_type + 72, // [72:106] is the sub-list for method input_type + 72, // [72:72] is the sub-list for extension type_name + 72, // [72:72] is the sub-list for extension extendee + 0, // [0:72] is the sub-list for field type_name } func init() { file_saas_accounts_v1_module_capabilities_proto_init() } @@ -6750,11 +7023,11 @@ func file_saas_accounts_v1_module_capabilities_proto_init() { } file_saas_accounts_v1_work_contexts_proto_init() file_saas_accounts_v1_module_registration_proto_init() - file_saas_accounts_v1_module_capabilities_proto_msgTypes[34].OneofWrappers = []any{ + file_saas_accounts_v1_module_capabilities_proto_msgTypes[37].OneofWrappers = []any{ (*FetchDatasourceFilesFrame_Header)(nil), (*FetchDatasourceFilesFrame_Data)(nil), } - file_saas_accounts_v1_module_capabilities_proto_msgTypes[75].OneofWrappers = []any{ + file_saas_accounts_v1_module_capabilities_proto_msgTypes[78].OneofWrappers = []any{ (*ModuleResolveWorkContextAuthorityRequest_Seal)(nil), (*ModuleResolveWorkContextAuthorityRequest_PrincipalEpoch)(nil), (*ModuleResolveWorkContextAuthorityRequest_ApprovedBuild)(nil), @@ -6762,7 +7035,7 @@ func file_saas_accounts_v1_module_capabilities_proto_init() { (*ModuleResolveWorkContextAuthorityRequest_AuthorizationRevision)(nil), (*ModuleResolveWorkContextAuthorityRequest_ApprovalGrant)(nil), } - file_saas_accounts_v1_module_capabilities_proto_msgTypes[76].OneofWrappers = []any{ + file_saas_accounts_v1_module_capabilities_proto_msgTypes[79].OneofWrappers = []any{ (*ModuleResolveWorkContextAuthorityResponse_Seal)(nil), (*ModuleResolveWorkContextAuthorityResponse_PrincipalEpoch)(nil), (*ModuleResolveWorkContextAuthorityResponse_ApprovedBuild)(nil), @@ -6776,7 +7049,7 @@ func file_saas_accounts_v1_module_capabilities_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_module_capabilities_proto_rawDesc), len(file_saas_accounts_v1_module_capabilities_proto_rawDesc)), NumEnums: 11, - NumMessages: 77, + NumMessages: 80, NumExtensions: 0, NumServices: 1, }, diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/module_capabilities_grpc.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/module_capabilities_grpc.pb.go index a0f69f14b..ac1f73323 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/module_capabilities_grpc.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/module_capabilities_grpc.pb.go @@ -41,6 +41,7 @@ const ( ModuleCapabilitiesService_GetApproval_FullMethodName = "/saas.accounts.v1.ModuleCapabilitiesService/GetApproval" ModuleCapabilitiesService_CancelApproval_FullMethodName = "/saas.accounts.v1.ModuleCapabilitiesService/CancelApproval" ModuleCapabilitiesService_EmitAuditEvent_FullMethodName = "/saas.accounts.v1.ModuleCapabilitiesService/EmitAuditEvent" + ModuleCapabilitiesService_ReadJournal_FullMethodName = "/saas.accounts.v1.ModuleCapabilitiesService/ReadJournal" ModuleCapabilitiesService_DeclareAuditEventTypes_FullMethodName = "/saas.accounts.v1.ModuleCapabilitiesService/DeclareAuditEventTypes" ModuleCapabilitiesService_ListSubjectVisibility_FullMethodName = "/saas.accounts.v1.ModuleCapabilitiesService/ListSubjectVisibility" ModuleCapabilitiesService_FetchDatasourceBlob_FullMethodName = "/saas.accounts.v1.ModuleCapabilitiesService/FetchDatasourceBlob" @@ -121,6 +122,11 @@ type ModuleCapabilitiesServiceClient interface { CancelApproval(ctx context.Context, in *ModuleCancelApprovalRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) // EmitAuditEvent records a registered audit event on the tenant's spine. EmitAuditEvent(ctx context.Context, in *ModuleEmitAuditEventRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + // ReadJournal serves the platform journal across every tenant, and the rows with + // no tenant, to a module principal whose grant is cross_tenant. Rows are served + // in the order their writing transactions are final, so a reader that resumes + // from next_after skips nothing; it enqueues nothing and is not a replay. + ReadJournal(ctx context.Context, in *ModuleReadJournalRequest, opts ...grpc.CallOption) (*ModuleReadJournalResponse, error) // DeclareAuditEventTypes admits the audit event types a composed module owns, // into the namespaces the operator bound to it; see the request. DeclareAuditEventTypes(ctx context.Context, in *ModuleDeclareAuditEventTypesRequest, opts ...grpc.CallOption) (*ModuleDeclareAuditEventTypesResponse, error) @@ -398,6 +404,16 @@ func (c *moduleCapabilitiesServiceClient) EmitAuditEvent(ctx context.Context, in return out, nil } +func (c *moduleCapabilitiesServiceClient) ReadJournal(ctx context.Context, in *ModuleReadJournalRequest, opts ...grpc.CallOption) (*ModuleReadJournalResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ModuleReadJournalResponse) + err := c.cc.Invoke(ctx, ModuleCapabilitiesService_ReadJournal_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *moduleCapabilitiesServiceClient) DeclareAuditEventTypes(ctx context.Context, in *ModuleDeclareAuditEventTypesRequest, opts ...grpc.CallOption) (*ModuleDeclareAuditEventTypesResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ModuleDeclareAuditEventTypesResponse) @@ -612,6 +628,11 @@ type ModuleCapabilitiesServiceServer interface { CancelApproval(context.Context, *ModuleCancelApprovalRequest) (*emptypb.Empty, error) // EmitAuditEvent records a registered audit event on the tenant's spine. EmitAuditEvent(context.Context, *ModuleEmitAuditEventRequest) (*emptypb.Empty, error) + // ReadJournal serves the platform journal across every tenant, and the rows with + // no tenant, to a module principal whose grant is cross_tenant. Rows are served + // in the order their writing transactions are final, so a reader that resumes + // from next_after skips nothing; it enqueues nothing and is not a replay. + ReadJournal(context.Context, *ModuleReadJournalRequest) (*ModuleReadJournalResponse, error) // DeclareAuditEventTypes admits the audit event types a composed module owns, // into the namespaces the operator bound to it; see the request. DeclareAuditEventTypes(context.Context, *ModuleDeclareAuditEventTypesRequest) (*ModuleDeclareAuditEventTypesResponse, error) @@ -749,6 +770,9 @@ func (UnimplementedModuleCapabilitiesServiceServer) CancelApproval(context.Conte func (UnimplementedModuleCapabilitiesServiceServer) EmitAuditEvent(context.Context, *ModuleEmitAuditEventRequest) (*emptypb.Empty, error) { return nil, status.Error(codes.Unimplemented, "method EmitAuditEvent not implemented") } +func (UnimplementedModuleCapabilitiesServiceServer) ReadJournal(context.Context, *ModuleReadJournalRequest) (*ModuleReadJournalResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ReadJournal not implemented") +} func (UnimplementedModuleCapabilitiesServiceServer) DeclareAuditEventTypes(context.Context, *ModuleDeclareAuditEventTypesRequest) (*ModuleDeclareAuditEventTypesResponse, error) { return nil, status.Error(codes.Unimplemented, "method DeclareAuditEventTypes not implemented") } @@ -1170,6 +1194,24 @@ func _ModuleCapabilitiesService_EmitAuditEvent_Handler(srv interface{}, ctx cont return interceptor(ctx, in, info, handler) } +func _ModuleCapabilitiesService_ReadJournal_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ModuleReadJournalRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ModuleCapabilitiesServiceServer).ReadJournal(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ModuleCapabilitiesService_ReadJournal_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ModuleCapabilitiesServiceServer).ReadJournal(ctx, req.(*ModuleReadJournalRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _ModuleCapabilitiesService_DeclareAuditEventTypes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ModuleDeclareAuditEventTypesRequest) if err := dec(in); err != nil { @@ -1477,6 +1519,10 @@ var ModuleCapabilitiesService_ServiceDesc = grpc.ServiceDesc{ MethodName: "EmitAuditEvent", Handler: _ModuleCapabilitiesService_EmitAuditEvent_Handler, }, + { + MethodName: "ReadJournal", + Handler: _ModuleCapabilitiesService_ReadJournal_Handler, + }, { MethodName: "DeclareAuditEventTypes", Handler: _ModuleCapabilitiesService_DeclareAuditEventTypes_Handler, diff --git a/module/services/accounts/code/pkg/infra/postgres_audit.go b/module/services/accounts/code/pkg/infra/postgres_audit.go index 990778b48..24cb6bd36 100644 --- a/module/services/accounts/code/pkg/infra/postgres_audit.go +++ b/module/services/accounts/code/pkg/infra/postgres_audit.go @@ -2,7 +2,6 @@ package infra import ( "context" - "encoding/base64" "encoding/json" "fmt" "strings" @@ -84,29 +83,6 @@ func (s *PostgresStore) InsertAuditEvent(ctx context.Context, entry business.Aud return err } -// encodeAuditCursor / decodeAuditCursor carry the keyset position for audit -// pagination. The token is opaque to callers: base64 of "|". -func encodeAuditCursor(ct time.Time, id string) string { - raw := ct.UTC().Format(time.RFC3339Nano) + "|" + id - return base64.RawURLEncoding.EncodeToString([]byte(raw)) -} - -func decodeAuditCursor(token string) (time.Time, string, error) { - raw, err := base64.RawURLEncoding.DecodeString(token) - if err != nil { - return time.Time{}, "", err - } - parts := strings.SplitN(string(raw), "|", 2) - if len(parts) != 2 { - return time.Time{}, "", fmt.Errorf("malformed cursor") - } - ct, err := time.Parse(time.RFC3339Nano, parts[0]) - if err != nil { - return time.Time{}, "", err - } - return ct, parts[1], nil -} - // auditWhere builds the shared WHERE clause for the search and aggregate paths // from an AuditQuery, returning the SQL fragment and its ordered args. func auditWhere(q business.AuditQuery, startArg int) (string, []any, error) { @@ -196,7 +172,7 @@ func (s *PostgresStore) QueryAuditLog(ctx context.Context, q business.AuditQuery // (created_at, id); the compound comparison is stable under the DESC // ordering even when many rows share a created_at. if q.PageToken != "" { - ct, id, err := decodeAuditCursor(q.PageToken) + ct, id, err := business.DecodeAuditPageToken(q.PageToken) if err != nil { return nil, "", 0, fmt.Errorf("invalid page token: %w", err) } @@ -269,7 +245,7 @@ func (s *PostgresStore) QueryAuditLog(ctx context.Context, q business.AuditQuery if len(events) > int(pageSize) { events = events[:pageSize] last := events[len(events)-1] - nextToken = encodeAuditCursor(last.CreatedAt, last.ID) + nextToken = business.EncodeAuditPageToken(last.CreatedAt, last.ID) } return events, nextToken, int32(len(events)), nil diff --git a/module/services/accounts/code/pkg/infra/postgres_audit_delivery_test.go b/module/services/accounts/code/pkg/infra/postgres_audit_delivery_test.go new file mode 100644 index 000000000..a82395ac8 --- /dev/null +++ b/module/services/accounts/code/pkg/infra/postgres_audit_delivery_test.go @@ -0,0 +1,269 @@ +//go:build !pure + +package infra_test + +import ( + "context" + "sort" + "testing" + "time" + + "accounts/pkg/auth" + "accounts/pkg/business" + gen "accounts/pkg/gen/saas/accounts/v1" + "accounts/pkg/infra/storetx" + + "connectrpc.com/connect" + "github.com/codefly-dev/sdk-go/receipts/connecttransport" + + "github.com/codefly-dev/sdk-go/receipts" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// dedupWarehouse is a warehouse that, like the real one, may hold an entry twice +// and returns each entry once, by id. +type dedupWarehouse struct{ appended []business.AuditEntry } + +func (w *dedupWarehouse) Append(_ context.Context, entry business.AuditEntry) error { + w.appended = append(w.appended, entry) + return nil +} + +func (w *dedupWarehouse) List(context.Context, business.AuditRead) ([]business.AuditEntry, string, error) { + once := map[string]business.AuditEntry{} + for _, entry := range w.appended { + once[entry.ID] = entry + } + out := make([]business.AuditEntry, 0, len(once)) + for _, entry := range once { + out = append(out, entry) + } + sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID }) + return out, "", nil +} + +func (w *dedupWarehouse) Aggregate(context.Context, business.AuditRead, business.AuditAggregationSpec) ([]business.AuditAggregateBucket, error) { + return nil, nil +} + +// DeliverAuditEvents over the real journal and the real receipt store: a range is +// appended with each record's organization taken from its row, one receipt is +// written per range under the installation's tenant, a replay of the same range is +// answered from the receipt with no second append, and overlapping ranges append +// at least once while a read of the warehouse returns each entry once. Delivery +// writes no audit of its own. +// +// The generated operation marking and real SDK guard serialize and replay the range. +func TestDeliverAuditEventsWritesOneReceiptPerRangeAndReplaysFromIt(t *testing.T) { + warehouse := &dedupWarehouse{} + audit, err := business.NewAuditService(business.AuditServiceConfig{ + Backend: business.AuditBackendWarehouse, Store: testStore, Producer: testStore, + Transport: auditJournalTransport(t), Warehouse: warehouse, + }) + require.NoError(t, err) + svc, caller := journalReader(t, true) + svc.SetAuditService(audit) + receiptStore, closeReceipts, err := testStore.NewEffectReceipts() + require.NoError(t, err) + defer closeReceipts() + require.NoError(t, svc.ConfigureSourceOperationReceipts(receiptStore)) + + installation := seedOrg(t, seedUser(t)) + orgA, orgB := seedOrg(t, seedUser(t)), seedOrg(t, seedUser(t)) + head := drainJournal(t, svc, caller, business.JournalStart, business.AuditRecordedEventType).next + + // Three audit records: one for an organization, one for none, one for another. + var ids []string + for _, org := range []string{orgA, "", orgB} { + envelope := auditJournalEnvelopeOf(t, seedUser(t), org) + require.NoError(t, testStore.WithControlPlane(testCtx, func(ctx context.Context) error { + return auditJournalTransport(t).Publish(ctx, storetx.Tx(ctx), envelope) + })) + ids = append(ids, envelope.GetId()) + } + records := drainJournal(t, svc, caller, head, business.AuditRecordedEventType) + require.Equal(t, ids, eventIDs(business.ModuleReadJournalResult{Entries: records.entries})) + position := func(i int) string { return records.entries[i].Position } + + handlerRuns := 0 + _, guard := svc.SourceOperationReceipts() + require.True(t, guard.IsOperation(business.DeliverAuditEventsMethod)) + handle := func(from, to string) (*gen.DeliverAuditEventsResponse, error) { + effectID := business.AuditDeliveryEffectID(from) + ctx := auth.WithVerifiedDatabaseIdentity(testCtx, caller.PrincipalID, installation) + ctx = business.WithSourceOperationRecheck(ctx, func(context.Context) error { return nil }) + req := connect.NewRequest(&gen.DeliverAuditEventsRequest{From: from, To: to}) + req.Header().Set(receipts.EffectIDHeaderName, effectID) + response, err := connecttransport.WrapUnary(guard, business.DeliverAuditEventsMethod, + func(ctx context.Context, req *connect.Request[gen.DeliverAuditEventsRequest]) (*connect.Response[gen.DeliverAuditEventsResponse], error) { + handlerRuns++ + var out *gen.DeliverAuditEventsResponse + _, err := svc.DeliverAuditEvents(ctx, req.Msg.From, req.Msg.To, func(ctx context.Context, r business.AuditDeliveryResult) error { + out = &gen.DeliverAuditEventsResponse{EffectId: effectID, Status: "committed", Delivered: int32(r.Delivered), From: req.Msg.From, To: req.Msg.To} + return svc.RecordSourceOperationReceipt(ctx, out) + }) + if err != nil { + return nil, err + } + return connect.NewResponse(out), nil + })(ctx, req) + if err != nil { + return nil, err + } + return response.Msg, nil + } + receiptCount := func(tenant, from string) int { + _, found, err := receiptStore.Lookup(testCtx, tenant, business.AuditDeliveryEffectID(from), business.DeliverAuditEventsMethod) + require.NoError(t, err) + if found { + return 1 + } + return 0 + } + entriesOf := func() map[string]string { // entry id -> organization appended + out := map[string]string{} + for _, entry := range warehouse.appended { + out[business.AuditJournalEventID(entry.ID)] = entry.OrgID + } + return out + } + + // The first range: all three records, from the start. + first, err := handle(business.JournalStart, position(2)) + require.NoError(t, err) + require.EqualValues(t, 3, first.Delivered) + require.Equal(t, business.JournalStart, first.From) + require.Equal(t, position(2), first.To) + require.Len(t, warehouse.appended, 3) + require.Equal(t, map[string]string{ids[0]: orgA, ids[1]: "", ids[2]: orgB}, entriesOf(), + "each record's organization is its row's, and the one with none has none") + require.Equal(t, 1, receiptCount(installation, business.JournalStart), "one receipt, under the installation's tenant") + require.Zero(t, receiptCount(orgA, business.JournalStart), "no receipt is written under a record's organization") + + // A replay of the range is answered from the receipt: no second append. + replay, err := handle(business.JournalStart, position(2)) + require.NoError(t, err) + require.True(t, proto.Equal(first, replay)) + require.Equal(t, 1, handlerRuns) + require.Len(t, warehouse.appended, 3) + + // The same start with another end is another request under one effect id. + _, err = handle(business.JournalStart, position(1)) + require.ErrorIs(t, err, receipts.ErrEffectIDReused) + + // An overlapping range (records 2 and 3 again) appends them a second time under its own receipt, and a + // read of the warehouse still returns each entry once. + overlap, err := handle(position(0), position(2)) + require.NoError(t, err) + require.EqualValues(t, 2, overlap.Delivered) + require.Len(t, warehouse.appended, 5, "at least once") + require.Equal(t, 1, receiptCount(installation, position(0))) + listed, _, err := warehouse.List(testCtx, business.AuditRead{}) + require.NoError(t, err) + require.Len(t, listed, 3, "every read returns each entry once") + + // An end that could still gain rows is refused and writes nothing. + open := openControlPlaneTx(t) + open.do(t, func(ctx context.Context) error { + var xact string + return storetx.Tx(ctx).QueryRow(ctx, `SELECT pg_current_xact_id()::text`).Scan(&xact) + }) + later := auditJournalEnvelopeOf(t, seedUser(t), orgA) + require.NoError(t, testStore.WithControlPlane(testCtx, func(ctx context.Context) error { + return auditJournalTransport(t).Publish(ctx, storetx.Tx(ctx), later) + })) + var laterXact, laterSeq string + require.NoError(t, testStore.WithControlPlane(testCtx, func(ctx context.Context) error { + return storetx.Tx(ctx).QueryRow(ctx, `SELECT xact_id::text, seq::text FROM domain_events WHERE id = $1::uuid`, later.GetId()).Scan(&laterXact, &laterSeq) + })) + beyond := "v1." + laterXact + "." + laterSeq + _, err = handle(position(2), beyond) + require.Equal(t, codes.FailedPrecondition, status.Code(err), "the writer with the earlier transaction id has not finished: %v", err) + require.Equal(t, 0, receiptCount(installation, position(2))) + require.Len(t, warehouse.appended, 5) + open.commit(t) + done, err := handle(position(2), beyond) + require.NoError(t, err) + require.EqualValues(t, 1, done.Delivered) + + // Delivering wrote no audit entry of its own: nothing after the last record + // this test published, but that record. + tail := drainJournal(t, svc, caller, position(2), business.AuditRecordedEventType) + require.Equal(t, []string{later.GetId()}, eventIDs(business.ModuleReadJournalResult{Entries: tail.entries})) + + // Source cleanup can find a matching attempt and still must leave the full + // audit receipt intact: the audit method is outside its two-method filter. + source := uuid.NewString() + effect := business.AuditDeliveryEffectID(business.JournalStart) + cleanupCtx := receipts.WithEffect(testCtx, receipts.Effect{ + ID: effect, Tenant: installation, Method: business.SourceOperationMethod, RequestDigest: []byte("source fixture"), + }) + require.NoError(t, testStore.WithOrgTx(cleanupCtx, installation, func(ctx context.Context) error { + _, err := testStore.CreateSourceOperationAttempt(ctx, business.SourceOperationAttempt{ + OrgID: installation, EffectID: effect, ActorID: caller.PrincipalID, SourceID: source, + Operation: "example", DeclarationDigest: "example", RequestDigest: []byte("source fixture"), + }) + if err != nil { + return err + } + if err := svc.RecordSourceOperationReceipt(ctx, &gen.InvokeSourceOperationResponse{OutputJson: "{}"}); err != nil { + return err + } + removed, err := testStore.PruneSourceOperationReceipts(ctx, installation, source, time.Now().Add(time.Hour)) + require.EqualValues(t, 1, removed, "the expired datasource receipt is removed") + return err + })) + saved, found, err := receiptStore.Lookup(testCtx, installation, effect, business.DeliverAuditEventsMethod) + require.NoError(t, err) + require.True(t, found) + var retained gen.DeliverAuditEventsResponse + require.NoError(t, proto.Unmarshal(saved.Response, &retained)) + require.True(t, proto.Equal(first, &retained), "cleanup preserves coverage and the committed response") + + // Backdate the delivered records and a later, undelivered record. Running + // real warehouse retention retires only the four records proven above. + undelivered := auditJournalEnvelopeOf(t, seedUser(t), orgB) + require.NoError(t, testStore.WithControlPlane(testCtx, func(ctx context.Context) error { + return auditJournalTransport(t).Publish(ctx, storetx.Tx(ctx), undelivered) + })) + ids = append(ids, later.GetId(), undelivered.GetId()) + require.NoError(t, testStore.WithControlPlane(testCtx, func(ctx context.Context) error { + _, err := storetx.Tx(ctx).Exec(ctx, `UPDATE domain_events SET created_at = clock_timestamp() - interval '960 hours' WHERE id::text = ANY($1::text[])`, ids) + return err + })) + postgres, err := business.NewService(testStore) + require.NoError(t, err) + postgresDeleted, err := postgres.RunRetention(testCtx) + require.NoError(t, err) + require.NotContains(t, postgresDeleted, "audit_journal") + require.NoError(t, testStore.WithControlPlane(testCtx, func(ctx context.Context) error { + var remaining int + err := storetx.Tx(ctx).QueryRow(ctx, `SELECT count(*) FROM domain_events WHERE id::text = ANY($1::text[])`, ids).Scan(&remaining) + require.Equal(t, 5, remaining, "Postgres mode leaves every journal entry in place") + return err + })) + deleted, err := svc.RunRetention(testCtx) + require.NoError(t, err) + require.EqualValues(t, 4, deleted["audit_journal"]) + require.NoError(t, testStore.WithControlPlane(testCtx, func(ctx context.Context) error { + var remaining []string + rows, err := storetx.Tx(ctx).Query(ctx, `SELECT id::text FROM domain_events WHERE id::text = ANY($1::text[])`, ids) + if err != nil { + return err + } + defer rows.Close() + for rows.Next() { + var id string + if err := rows.Scan(&id); err != nil { + return err + } + remaining = append(remaining, id) + } + require.Equal(t, []string{undelivered.GetId()}, remaining) + return rows.Err() + })) +} diff --git a/module/services/accounts/code/pkg/infra/postgres_audit_journal_emitters_test.go b/module/services/accounts/code/pkg/infra/postgres_audit_journal_emitters_test.go new file mode 100644 index 000000000..a95534b79 --- /dev/null +++ b/module/services/accounts/code/pkg/infra/postgres_audit_journal_emitters_test.go @@ -0,0 +1,158 @@ +//go:build !pure + +package infra_test + +import ( + "context" + "errors" + "testing" + "time" + + "accounts/pkg/business" + "accounts/pkg/infra/storetx" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" +) + +// warehouseStorage is the AuditStorage of a warehouse the test cannot reach. It +// keeps what it is asked to append, so the test can show the entries went to the +// journal and nowhere else. +type warehouseStorage struct{ appended []business.AuditEntry } + +func (w *warehouseStorage) Append(_ context.Context, entry business.AuditEntry) error { + w.appended = append(w.appended, entry) + return nil +} + +func (w *warehouseStorage) List(context.Context, business.AuditRead) ([]business.AuditEntry, string, error) { + return nil, "", errors.New("not read in this test") +} + +func (w *warehouseStorage) Aggregate(context.Context, business.AuditRead, business.AuditAggregationSpec) ([]business.AuditAggregateBucket, error) { + return nil, errors.New("not read in this test") +} + +// passkeyEngine answers the WebAuthn ceremonies with fixed proofs: the test is +// about what the registration writes, not about the protocol. +type passkeyEngine struct{} + +func (passkeyEngine) BeginRegistration(context.Context, business.WebAuthnUser) ([]byte, []byte, time.Time, error) { + return []byte(`{"challenge":"registration"}`), []byte(`{"state":"registration"}`), time.Now().Add(time.Minute), nil +} + +func (passkeyEngine) FinishRegistration(_ context.Context, _ business.WebAuthnUser, session, response []byte) (*business.WebAuthnCredentialResult, error) { + if string(session) != `{"state":"registration"}` || string(response) != `{"credential":"registration"}` { + return nil, errors.New("invalid registration proof") + } + return &business.WebAuthnCredentialResult{ID: []byte("credential-" + uuid.NewString()), CredentialJSON: []byte(`{"counter":0}`)}, nil +} + +func (passkeyEngine) BeginLogin(context.Context, business.WebAuthnUser) ([]byte, []byte, time.Time, error) { + return nil, nil, time.Time{}, errors.New("not used in this test") +} + +func (passkeyEngine) FinishLogin(context.Context, business.WebAuthnUser, []byte, []byte) (*business.WebAuthnCredentialResult, error) { + return nil, errors.New("not used in this test") +} + +// Under the warehouse backend every audit entry is the journal's, and the ones +// with no organization are written inside a user-scoped transaction, where +// publish_domain_event used to refuse them. The WebAuthn registration and the MFA +// enrolment write theirs there, so this drives the real operations against the +// real function: each entry must commit with its mutation as a journal row for no +// tenant, in the user's name, and reach no storage. +func TestWarehouseBackendJournalsTheUserScopedEntriesWithNoOrganization(t *testing.T) { + warehouse := &warehouseStorage{} + audit, err := business.NewAuditService(business.AuditServiceConfig{ + Backend: business.AuditBackendWarehouse, + Store: testStore, + Producer: testStore, + Transport: auditJournalTransport(t), + Warehouse: warehouse, + }) + require.NoError(t, err) + service, err := business.NewService(testStore) + require.NoError(t, err) + service.SetAuditService(audit) + service.SetMFASecretCipher(webhookProjectionCipher{}) + service.SetWebAuthnEngine(passkeyEngine{}) + require.NoError(t, service.VerifyAuditWiring()) + + user := seedUser(t) + + // WebAuthn registration: webauthn.go begins and finishes it in one + // user-scoped transaction each, and each writes an entry. + ceremony, _, err := service.BeginWebAuthnRegistration(testCtx, user) + require.NoError(t, err) + passkey, err := service.FinishWebAuthnRegistration(testCtx, user, ceremony, `{"credential":"registration"}`, "Passkey") + require.NoError(t, err) + + // MFA: mfa.go enrols, then revokes, a TOTP device and rotates the backup + // codes, each in a user-scoped transaction of its own. + _, _, err = service.SetupTOTP(testCtx, user) + require.NoError(t, err) + devices, err := service.ListMFADevices(testCtx, user) + require.NoError(t, err) + var totp string + for _, device := range devices { + if device.DeviceType == "totp" { + totp = device.ID + } + } + require.NotEmpty(t, totp, "the TOTP device the enrolment created") + require.NoError(t, service.RevokeMFADevice(testCtx, user, totp)) + _, err = service.GenerateBackupCodes(testCtx, user) + require.NoError(t, err) + + want := []business.EventType{ + business.EventMFAWebAuthnRegStarted, + business.EventMFAWebAuthnRegistered, + business.EventMFATOTPSetupStarted, + business.EventMFADeviceRevoked, + business.EventMFABackupGenerated, + } + var got []business.AuditEntry + var tenanted, auditRows int + require.NoError(t, testStore.WithControlPlane(testCtx, func(ctx context.Context) error { + tx := storetx.Tx(ctx) + rows, err := tx.Query(ctx, `SELECT data FROM domain_events + WHERE type = $1 AND actor_principal_id = $2 AND tenant_id IS NULL + ORDER BY seq`, business.AuditRecordedEventType, user) + if err != nil { + return err + } + defer rows.Close() + for rows.Next() { + var data []byte + if err := rows.Scan(&data); err != nil { + return err + } + entry, err := business.AuditEntryFromJournal(data) + if err != nil { + return err + } + got = append(got, entry) + } + if err := rows.Err(); err != nil { + return err + } + if err := tx.QueryRow(ctx, `SELECT count(*) FROM domain_events WHERE actor_principal_id = $1 AND tenant_id IS NOT NULL`, user).Scan(&tenanted); err != nil { + return err + } + return tx.QueryRow(ctx, `SELECT count(*) FROM audit_events WHERE actor_id = $1::uuid`, user).Scan(&auditRows) + })) + + var types []business.EventType + for _, entry := range got { + types = append(types, entry.EventType) + require.Equal(t, user, entry.ActorID) + require.Equal(t, business.ActorTypeUser, entry.ActorType) + require.Empty(t, entry.OrgID) + } + require.Equal(t, want, types, "one journal row per entry, in the order the operations wrote them") + require.Equal(t, passkey.ID, got[1].ResourceID, "the registration entry names the device it registered") + require.Zero(t, tenanted, "no entry of this user was given a tenant") + require.Zero(t, auditRows, "audit_events keeps no history under the warehouse backend") + require.Empty(t, warehouse.appended, "the journal is the entry's only copy until a delivery appends it") +} diff --git a/module/services/accounts/code/pkg/infra/postgres_audit_journal_publish_test.go b/module/services/accounts/code/pkg/infra/postgres_audit_journal_publish_test.go new file mode 100644 index 000000000..f880f4593 --- /dev/null +++ b/module/services/accounts/code/pkg/infra/postgres_audit_journal_publish_test.go @@ -0,0 +1,210 @@ +//go:build !pure + +package infra_test + +import ( + "context" + "errors" + "testing" + "time" + + "accounts/pkg/business" + eventsv1 "accounts/pkg/gen/saas/events/v1" + "accounts/pkg/infra" + "accounts/pkg/infra/storetx" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgconn" + "github.com/stretchr/testify/require" +) + +// These tests hold publish_domain_event's tenant guard to what migration 41 +// leaves of it, against the real function and the real roles: the journal record +// of an audit entry with no organization is admitted from a user-scoped +// transaction for that user's own action, and nothing else tenant-less is. + +func auditJournalTransport(t *testing.T) *infra.PostgresEventTransport { + t.Helper() + return infra.NewPostgresEventTransport(infra.NewPostgresJobStore(testPool), testPool, + "audit-journal-"+uuid.NewString(), time.Minute) +} + +// auditJournalEnvelopeOf is the envelope the emitter publishes for an entry, so +// the tests exercise what is actually sent and not a stand-in for it. +func auditJournalEnvelopeOf(t *testing.T, actorID, orgID string) *eventsv1.EventEnvelope { + t.Helper() + envelope, err := business.NewAuditJournalEnvelope(business.AuditEntry{ + ID: uuid.NewString(), + EventType: business.EventMFATOTPSetupStarted, + ActorID: actorID, ActorType: business.ActorTypeUser, + Resource: "mfa_device", ResourceID: uuid.NewString(), + OrgID: orgID, + CreatedAt: time.Now().UTC(), + }) + require.NoError(t, err) + return envelope +} + +func publishInUserTx(transport *infra.PostgresEventTransport, userID string, envelope *eventsv1.EventEnvelope) error { + return testStore.WithUserTx(testCtx, userID, func(ctx context.Context) error { + return transport.Publish(ctx, storetx.Tx(ctx), envelope) + }) +} + +func requireTenantGuardRefusal(t *testing.T, err error) { + t.Helper() + require.Error(t, err) + var pgErr *pgconn.PgError + require.True(t, errors.As(err, &pgErr), "want a database refusal, got %v", err) + require.Equal(t, "42501", pgErr.Code, "insufficient_privilege") + require.Contains(t, pgErr.Message, "event tenant does not match the signed request scope") +} + +type journalRow struct { + Type, Actor string + TenantNull bool +} + +// journalRowsOf reads the committed journal rows under the control plane, which +// sees every tenant's and the tenant-less ones. +func journalRowsOf(t *testing.T, id string) []journalRow { + t.Helper() + var rows []journalRow + require.NoError(t, testStore.WithControlPlane(testCtx, func(ctx context.Context) error { + found, err := storetx.Tx(ctx).Query(ctx, + `SELECT type, actor_principal_id, tenant_id IS NULL FROM domain_events WHERE id = $1::uuid`, id) + if err != nil { + return err + } + defer found.Close() + for found.Next() { + var row journalRow + if err := found.Scan(&row.Type, &row.Actor, &row.TenantNull); err != nil { + return err + } + rows = append(rows, row) + } + return found.Err() + })) + return rows +} + +func TestUserTransactionPublishesAnAuditEntryWithNoOrganizationForItsOwnUser(t *testing.T) { + transport := auditJournalTransport(t) + user := seedUser(t) + envelope := auditJournalEnvelopeOf(t, user, "") + require.Empty(t, envelope.GetTenantId()) + require.Equal(t, user, envelope.GetActorPrincipalId(), "the emitter names the entry's actor on the envelope") + + require.NoError(t, publishInUserTx(transport, user, envelope)) + require.Equal(t, []journalRow{{Type: business.AuditRecordedEventType, Actor: user, TenantNull: true}}, + journalRowsOf(t, envelope.GetId())) + + // A retry of the same entry is the same event: the function's idempotent + // path reads the stored row back under the owner's authority, and must do so + // for a row with no tenant. + require.NoError(t, publishInUserTx(transport, user, envelope)) + require.Len(t, journalRowsOf(t, envelope.GetId()), 1) +} + +func TestUserTransactionCannotPublishAnAuditEntryOfAnotherActor(t *testing.T) { + transport := auditJournalTransport(t) + user, other := seedUser(t), seedUser(t) + + elsewhere := auditJournalEnvelopeOf(t, other, "") + requireTenantGuardRefusal(t, publishInUserTx(transport, user, elsewhere)) + require.Empty(t, journalRowsOf(t, elsewhere.GetId())) + + // No actor at all is no actor equal to the user. + anonymous := auditJournalEnvelopeOf(t, "", "") + requireTenantGuardRefusal(t, publishInUserTx(transport, user, anonymous)) + require.Empty(t, journalRowsOf(t, anonymous.GetId())) +} + +func TestUserTransactionCannotPublishAnyOtherTypeForNoTenant(t *testing.T) { + transport := auditJournalTransport(t) + user := seedUser(t) + for _, eventType := range []string{ + "documents.entry.version_minted", + "saas.audit.recorded.v2", + "saas.audit.recorded", + "SAAS.AUDIT.RECORDED.V1", + } { + envelope := auditJournalEnvelopeOf(t, user, "") + envelope.Type = eventType + envelope.Id = uuid.NewString() + requireTenantGuardRefusal(t, publishInUserTx(transport, user, envelope)) + require.Empty(t, journalRowsOf(t, envelope.GetId()), eventType) + } +} + +func TestTenantScopedTransactionStillCannotPublishForAnotherTenant(t *testing.T) { + transport := auditJournalTransport(t) + user := seedUser(t) + mine, theirs := uuid.NewString(), uuid.NewString() + + // An organization-scoped entry of the right actor, for a tenant that is not + // the transaction's signed scope. + foreign := auditJournalEnvelopeOf(t, user, theirs) + err := testStore.WithOrgTx(testCtx, mine, func(ctx context.Context) error { + return transport.Publish(ctx, storetx.Tx(ctx), foreign) + }) + requireTenantGuardRefusal(t, err) + require.Empty(t, journalRowsOf(t, foreign.GetId())) + + // A user-scoped transaction has no tenant at all, so it cannot name one. + named := auditJournalEnvelopeOf(t, user, mine) + requireTenantGuardRefusal(t, publishInUserTx(transport, user, named)) + require.Empty(t, journalRowsOf(t, named.GetId())) + + // Its own tenant is still admitted, as it always was. + own := auditJournalEnvelopeOf(t, user, mine) + require.NoError(t, testStore.WithOrgTx(testCtx, mine, func(ctx context.Context) error { + return transport.Publish(ctx, storetx.Tx(ctx), own) + })) + require.Equal(t, []journalRow{{Type: business.AuditRecordedEventType, Actor: user}}, journalRowsOf(t, own.GetId())) +} + +// A tenant-scoped transaction binds no user, so the admission has no user to +// compare the actor with and does not apply: the entry's organization is its +// only way in. +func TestOrganizationTransactionWithNoUserCannotPublishATenantlessAuditEntry(t *testing.T) { + transport := auditJournalTransport(t) + envelope := auditJournalEnvelopeOf(t, uuid.NewString(), "") + err := testStore.WithOrgTx(testCtx, uuid.NewString(), func(ctx context.Context) error { + return transport.Publish(ctx, storetx.Tx(ctx), envelope) + }) + requireTenantGuardRefusal(t, err) + require.Empty(t, journalRowsOf(t, envelope.GetId())) +} + +// The control plane's authority is unchanged: it publishes any type for no +// tenant, which is how Emit writes the entries it opens its own transaction for. +func TestControlPlaneStillPublishesForNoTenant(t *testing.T) { + transport := auditJournalTransport(t) + envelope := auditJournalEnvelopeOf(t, uuid.NewString(), "") + require.NoError(t, testStore.WithControlPlane(testCtx, func(ctx context.Context) error { + return transport.Publish(ctx, storetx.Tx(ctx), envelope) + })) + require.Len(t, journalRowsOf(t, envelope.GetId()), 1) +} + +// Matching envelope authority cannot disguise another actor or tenant inside +// the audit record that the warehouse actually reads. +func TestUserAuditJournalRecordCannotContradictItsEnvelope(t *testing.T) { + transport := auditJournalTransport(t) + user := seedUser(t) + other := seedUser(t) + org := seedOrg(t, user) + for _, forged := range []struct{ name, actor, org string }{ + {"another actor", other, ""}, {"another organization", user, org}, + } { + t.Run(forged.name, func(t *testing.T) { + envelope := auditJournalEnvelopeOf(t, forged.actor, forged.org) + envelope.ActorPrincipalId = user + envelope.TenantId = "" + requireTenantGuardRefusal(t, publishInUserTx(transport, user, envelope)) + require.Empty(t, journalRowsOf(t, envelope.Id)) + }) + } +} diff --git a/module/services/accounts/code/pkg/infra/postgres_audit_journal_retention.go b/module/services/accounts/code/pkg/infra/postgres_audit_journal_retention.go new file mode 100644 index 000000000..f0f8c4b2c --- /dev/null +++ b/module/services/accounts/code/pkg/infra/postgres_audit_journal_retention.go @@ -0,0 +1,102 @@ +package infra + +import ( + "bytes" + "context" + "errors" + "sort" + "time" + + "accounts/pkg/business" + gen "accounts/pkg/gen/saas/accounts/v1" + "accounts/pkg/infra/internal/txbind" + + "github.com/codefly-dev/sdk-go/receipts" + "google.golang.org/protobuf/proto" +) + +type auditJournalRange struct{ from, to business.JournalPosition } + +// auditDeliveryCoverage reads the SDK's committed protobuf response, rather +// than inferring an end from its count or effect id. A legacy or inconsistent +// receipt proves nothing and leaves the journal's only copy in place. +func auditDeliveryCoverage(effect string, digest, response []byte) (auditJournalRange, bool) { + var out gen.DeliverAuditEventsResponse + if proto.Unmarshal(response, &out) != nil || out.Status != "committed" || out.From == "" || out.To == "" || + out.EffectId != effect || effect != business.AuditDeliveryEffectID(out.From) { + return auditJournalRange{}, false + } + from, err := business.ParseJournalPosition(out.From) + if err != nil { + return auditJournalRange{}, false + } + to, err := business.ParseJournalPosition(out.To) + if err != nil || !from.Less(to) { + return auditJournalRange{}, false + } + expected, err := receipts.RequestDigest(&gen.DeliverAuditEventsRequest{From: out.From, To: out.To}) + if err != nil || !bytes.Equal(expected, digest) { + return auditJournalRange{}, false + } + return auditJournalRange{from, to}, true +} + +// DeleteDeliveredAuditJournal runs on the existing retention transaction and +// SDK receipt table. Positions are global across tenants: the receipt's tenant +// is the delivery installation, not the organization on any particular row. +func (s *PostgresStore) DeleteDeliveredAuditJournal(ctx context.Context, before time.Time) (deleted, oldUnproven int64, err error) { + binding, ok := txbind.Lookup(ctx) + if !ok || binding.Pool != txbind.ControlPlane { + return 0, 0, errors.New("audit journal retention requires a control-plane transaction") + } + rows, err := binding.Tx.Query(ctx, `SELECT effect_id, request_digest, response + FROM public.codefly_effect_receipts WHERE method = $1 AND tenant <> ''`, business.DeliverAuditEventsMethod) + if err != nil { + return 0, 0, err + } + defer rows.Close() + var ranges []auditJournalRange + for rows.Next() { + var effect string + var digest, response []byte + if err := rows.Scan(&effect, &digest, &response); err != nil { + return 0, 0, err + } + if span, valid := auditDeliveryCoverage(effect, digest, response); valid { + ranges = append(ranges, span) + } + } + if err := rows.Err(); err != nil { + return 0, 0, err + } + rows.Close() + + // Collapse overlapping or touching intervals so a sequential delivery does + // not cause one DELETE per historical receipt. A gap always stays a gap. + sort.Slice(ranges, func(i, j int) bool { return ranges[i].from.Less(ranges[j].from) }) + merged := ranges[:0] + for _, span := range ranges { + if len(merged) == 0 || merged[len(merged)-1].to.Less(span.from) { + merged = append(merged, span) + } else if merged[len(merged)-1].to.Less(span.to) { + merged[len(merged)-1].to = span.to + } + } + for _, span := range merged { + tag, err := binding.Tx.Exec(ctx, `DELETE FROM public.domain_events e + WHERE e.type = $1 AND e.created_at < $2 + AND (COALESCE(e.xact_id, '0'::xid8), e.seq) > ($3::text::xid8, $4::bigint) + AND (COALESCE(e.xact_id, '0'::xid8), e.seq) <= ($5::text::xid8, $6::bigint)`, + business.AuditRecordedEventType, before, uitoa(span.from.Xact), span.from.Seq, uitoa(span.to.Xact), span.to.Seq) + if err != nil { + return 0, 0, err + } + deleted += tag.RowsAffected() + } + err = binding.Tx.QueryRow(ctx, `SELECT count(*) FROM public.domain_events + WHERE type = $1 AND created_at < $2`, business.AuditRecordedEventType, before).Scan(&oldUnproven) + if err != nil { + return 0, 0, err + } + return deleted, oldUnproven, nil +} diff --git a/module/services/accounts/code/pkg/infra/postgres_audit_journal_retention_test.go b/module/services/accounts/code/pkg/infra/postgres_audit_journal_retention_test.go new file mode 100644 index 000000000..ea9a8e12a --- /dev/null +++ b/module/services/accounts/code/pkg/infra/postgres_audit_journal_retention_test.go @@ -0,0 +1,188 @@ +//go:build !pure + +package infra_test + +import ( + "context" + "strconv" + "testing" + "time" + + "accounts/pkg/business" + gen "accounts/pkg/gen/saas/accounts/v1" + "accounts/pkg/infra" + + "github.com/codefly-dev/sdk-go/receipts" + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +// Each matrix runs in an owner transaction that rolls back. Clearing existing +// rows only inside that transaction gives the global old-unproven count an +// exact expectation without changing another test's committed journal. +func auditRetentionFixture(t *testing.T, fn func(context.Context, pgx.Tx, time.Time)) { + t.Helper() + asRetention(t, func(ctx context.Context, tx pgx.Tx) { + _, err := tx.Exec(ctx, `DELETE FROM public.codefly_effect_receipts; DELETE FROM public.domain_events`) + require.NoError(t, err) + var cutoff time.Time + require.NoError(t, tx.QueryRow(ctx, `SELECT clock_timestamp() - interval '720 hours'`).Scan(&cutoff)) + fn(ctx, tx, cutoff) + }) +} + +func retentionJournalRow(t *testing.T, ctx context.Context, tx pgx.Tx, xact *string, eventType string, created, occurred time.Time, tenant *string) (string, string) { + t.Helper() + id := uuid.NewString() + var seq int64 + require.NoError(t, tx.QueryRow(ctx, `INSERT INTO public.domain_events + (id, type, source, request_fingerprint, xact_id, created_at, event_time, tenant_id) + VALUES ($1, $2, 'saas/test', decode(repeat('ab',32),'hex'), $3::text::xid8, $4, $5, $6::text::uuid) + RETURNING seq`, id, eventType, xact, created, occurred, tenant).Scan(&seq)) + position := business.JournalPosition{Seq: seq} + if xact != nil { + var err error + position.Xact, err = strconv.ParseUint(*xact, 10, 64) + require.NoError(t, err) + } + return id, position.String() +} + +func retentionReceipt(t *testing.T, ctx context.Context, tx pgx.Tx, method, from, to string, change func(*gen.DeliverAuditEventsResponse, *[]byte)) { + t.Helper() + digest, err := receipts.RequestDigest(&gen.DeliverAuditEventsRequest{From: from, To: to}) + require.NoError(t, err) + response := &gen.DeliverAuditEventsResponse{ + EffectId: business.AuditDeliveryEffectID(from), Status: "committed", Delivered: 1, From: from, To: to, + } + if change != nil { + change(response, &digest) + } + wire, err := proto.Marshal(response) + require.NoError(t, err) + _, err = tx.Exec(ctx, `INSERT INTO public.codefly_effect_receipts + (tenant,effect_id,method,request_digest,response,committed_at) + VALUES ($1,$2,$3,$4,$5,clock_timestamp()-interval '960 hours')`, + uuid.NewString(), business.AuditDeliveryEffectID(from), method, digest, wire) + require.NoError(t, err) +} + +func TestAuditJournalRetentionPreservesHolesAndUnprovenEntries(t *testing.T) { + auditRetentionFixture(t, func(ctx context.Context, tx pgx.Tx, cutoff time.Time) { + old := cutoff.Add(-24 * time.Hour) + tenant := uuid.NewString() + var ids, positions []string + for i := 0; i < 7; i++ { + rowXact := strconv.Itoa(10 + i) + var org *string // Both organization and no-organization records. + if i%2 == 0 { + org = &tenant + } + id, position := retentionJournalRow(t, ctx, tx, &rowXact, business.AuditRecordedEventType, old, time.Now(), org) + ids, positions = append(ids, id), append(positions, position) + } + // The committed intervals overlap, but neither proves rows 0, 3 or 6. + for _, interval := range [][2]int{{0, 2}, {1, 2}, {3, 5}} { + retentionReceipt(t, ctx, tx, business.DeliverAuditEventsMethod, positions[interval[0]], positions[interval[1]], nil) + } + retentionReceipt(t, ctx, tx, business.DeliverAuditEventsMethod, positions[5], positions[6], func(r *gen.DeliverAuditEventsResponse, _ *[]byte) { + r.From, r.To, r.Delivered = "", "", 500 // Legacy count cannot prove coverage. + }) + // A young entry with an old producer timestamp, and the exact cutoff, + // survive even though a committed interval covers their positions. + xact := "20" + young, youngPos := retentionJournalRow(t, ctx, tx, &xact, business.AuditRecordedEventType, cutoff.Add(time.Hour), old, nil) + boundary, boundaryPos := retentionJournalRow(t, ctx, tx, &xact, business.AuditRecordedEventType, cutoff, old, nil) + retentionReceipt(t, ctx, tx, business.DeliverAuditEventsMethod, positions[6], boundaryPos, nil) + other, otherPos := retentionJournalRow(t, ctx, tx, &xact, "example.recorded.v1", old, old, nil) + retentionReceipt(t, ctx, tx, business.DeliverAuditEventsMethod, youngPos, otherPos, nil) + _, err := tx.Exec(ctx, `SET LOCAL ROLE app_control_plane`) + require.NoError(t, err) + removed, unproven, err := testStore.DeleteDeliveredAuditJournal(infra.BindControlPlaneTx(ctx, tx), cutoff) + require.NoError(t, err) + require.EqualValues(t, 4, removed) + require.EqualValues(t, 3, unproven) + rows, err := tx.Query(ctx, `SELECT id::text FROM public.domain_events ORDER BY seq`) + require.NoError(t, err) + remaining, err := pgx.CollectRows(rows, pgx.RowTo[string]) + require.NoError(t, err) + require.Equal(t, []string{ids[0], ids[3], ids[6], young, boundary, other}, remaining) + }) +} + +func TestAuditJournalRetentionRequiresConsistentCommittedCoverage(t *testing.T) { + cases := map[string]func(*gen.DeliverAuditEventsResponse, *[]byte){ + "legacy": func(r *gen.DeliverAuditEventsResponse, _ *[]byte) { r.From, r.To = "", "" }, + "uncommitted": func(r *gen.DeliverAuditEventsResponse, _ *[]byte) { r.Status = "pending" }, + "different effect": func(r *gen.DeliverAuditEventsResponse, _ *[]byte) { r.EffectId = "different" }, + "different request": func(_ *gen.DeliverAuditEventsResponse, d *[]byte) { *d = []byte("wrong digest") }, + "invalid position": func(r *gen.DeliverAuditEventsResponse, _ *[]byte) { r.To = "v1.01.2" }, + "reversed interval": func(r *gen.DeliverAuditEventsResponse, _ *[]byte) { r.From, r.To = r.To, r.From }, + "empty interval": func(r *gen.DeliverAuditEventsResponse, _ *[]byte) { r.To = r.From }, + } + for name, change := range cases { + t.Run(name, func(t *testing.T) { + auditRetentionFixture(t, func(ctx context.Context, tx pgx.Tx, cutoff time.Time) { + _, position := retentionJournalRow(t, ctx, tx, nil, business.AuditRecordedEventType, cutoff.Add(-time.Hour), time.Now(), nil) + retentionReceipt(t, ctx, tx, business.DeliverAuditEventsMethod, business.JournalStart, position, change) + _, err := tx.Exec(ctx, `SET LOCAL ROLE app_control_plane`) + require.NoError(t, err) + removed, unproven, err := testStore.DeleteDeliveredAuditJournal(infra.BindControlPlaneTx(ctx, tx), cutoff) + require.NoError(t, err) + require.Zero(t, removed) + require.EqualValues(t, 1, unproven) + }) + }) + } + t.Run("malformed protobuf", func(t *testing.T) { + auditRetentionFixture(t, func(ctx context.Context, tx pgx.Tx, cutoff time.Time) { + _, position := retentionJournalRow(t, ctx, tx, nil, business.AuditRecordedEventType, cutoff.Add(-time.Hour), time.Now(), nil) + retentionReceipt(t, ctx, tx, business.DeliverAuditEventsMethod, business.JournalStart, position, nil) + _, err := tx.Exec(ctx, `UPDATE public.codefly_effect_receipts SET response = decode('ff','hex'); SET LOCAL ROLE app_control_plane`) + require.NoError(t, err) + removed, unproven, err := testStore.DeleteDeliveredAuditJournal(infra.BindControlPlaneTx(ctx, tx), cutoff) + require.NoError(t, err) + require.Zero(t, removed) + require.EqualValues(t, 1, unproven) + }) + }) +} + +func TestAuditJournalRetentionRoleCannotDeleteOtherTypesOrYoungRecords(t *testing.T) { + auditRetentionFixture(t, func(ctx context.Context, tx pgx.Tx, cutoff time.Time) { + old := cutoff.Add(-time.Hour) + _, oldPos := retentionJournalRow(t, ctx, tx, nil, business.AuditRecordedEventType, old, time.Now(), nil) + _, youngPos := retentionJournalRow(t, ctx, tx, nil, business.AuditRecordedEventType, cutoff.Add(time.Hour), old, nil) + _, _ = retentionJournalRow(t, ctx, tx, nil, "example.recorded.v1", old, old, nil) + retentionReceipt(t, ctx, tx, business.DeliverAuditEventsMethod, business.JournalStart, youngPos, nil) + retentionReceipt(t, ctx, tx, business.SourceOperationMethod, business.JournalStart, oldPos, nil) + _, err := tx.Exec(ctx, `SET LOCAL ROLE app_control_plane`) + require.NoError(t, err) + var methods []string + rows, err := tx.Query(ctx, `SELECT method FROM public.codefly_effect_receipts`) + require.NoError(t, err) + methods, err = pgx.CollectRows(rows, pgx.RowTo[string]) + require.NoError(t, err) + require.Equal(t, []string{business.DeliverAuditEventsMethod}, methods, "ordinary source response bytes remain hidden") + tag, err := tx.Exec(ctx, `DELETE FROM public.domain_events WHERE type <> $1 OR created_at >= $2`, business.AuditRecordedEventType, cutoff) + require.NoError(t, err) + require.Zero(t, tag.RowsAffected(), "RLS blocks direct deletion of other types and young audit records") + removed, unproven, err := testStore.DeleteDeliveredAuditJournal(infra.BindControlPlaneTx(ctx, tx), cutoff) + require.NoError(t, err) + require.EqualValues(t, 1, removed, "a committed range can retire pre-xact_id rows from JournalStart") + require.Zero(t, unproven) + }) +} + +func TestAuditJournalRetentionRefusesRequestOrUnboundTransactions(t *testing.T) { + _, _, err := testStore.DeleteDeliveredAuditJournal(testCtx, time.Now()) + require.ErrorContains(t, err, "control-plane transaction") + org := seedOrg(t, seedUser(t)) + require.NoError(t, testStore.WithOrgTx(testCtx, org, func(ctx context.Context) error { + _, _, err := testStore.DeleteDeliveredAuditJournal(ctx, time.Now()) + require.ErrorContains(t, err, "control-plane transaction") + return nil + })) +} diff --git a/module/services/accounts/code/pkg/infra/postgres_journal_read.go b/module/services/accounts/code/pkg/infra/postgres_journal_read.go new file mode 100644 index 000000000..8d253ce1d --- /dev/null +++ b/module/services/accounts/code/pkg/infra/postgres_journal_read.go @@ -0,0 +1,158 @@ +package infra + +import ( + "context" + "errors" + "strconv" + "time" + + "accounts/pkg/business" + "accounts/pkg/infra/storetx" + + "github.com/codefly-dev/core/wool" +) + +// The cross-tenant journal read (business.ModuleReadJournal). It runs on the +// control plane, where domain_events' tenant policy does not apply, so it serves +// every tenant's rows and the rows with no tenant; the caller's gate is the only +// one, and each method refuses to run outside a transaction rather than fall back +// to the request pool, where the tenant policy would make an empty answer look +// like a quiet journal. +// +// A row's key is (COALESCE(xact_id, '0'), seq): the transaction that wrote it +// (migration 42; NULL on a row from before it, read as 0, before every real id), +// then seq. A row is served only while its transaction id is below +// pg_snapshot_xmin(pg_current_snapshot()), the oldest transaction id still +// running. Any transaction that can still commit has an id at or after that +// bound, so a row below it belongs to a finished transaction, and nothing that has +// not appeared yet can sort before it: a cursor that stops at the last row served +// skips nothing. The key is the one idx_domain_events_xact_seq is built on. +// +// The same shape as the audit queue's drain (xact_id < pg_snapshot_xmin), applied +// to the journal. It holds behind a write transaction left open, and behind rows +// whose ids are ahead of the cluster after a logical restore into a new one; both +// stall the reader and lose nothing. + +// journalWhere is the predicate every read of the cross-tenant journal shares: +// after $1/$2, up to $3/$4 when $3 is set, of the types $5, below the bound. +const journalWhere = ` + e.type = ANY($5::text[]) + AND (COALESCE(e.xact_id, '0'::xid8), e.seq) > ($1::text::xid8, $2::bigint) + AND ($3::text IS NULL OR (COALESCE(e.xact_id, '0'::xid8), e.seq) <= ($3::text::xid8, $4::bigint)) + AND COALESCE(e.xact_id, '0'::xid8) < pg_snapshot_xmin(pg_current_snapshot())` + +// readJournalPageSQL reads the first $6 rows in key order, then keeps the rows +// whose cumulative data fits $7 bytes, so one page is bounded in bytes as +// well as in rows; the first row is always kept even when it exceeds the bound. +const readJournalPageSQL = ` + SELECT xact::text, seq, id::text, type, tenant_id::text, occurred_at, datacontenttype, data + FROM ( + SELECT *, sum(octet_length(data)) OVER (ORDER BY xact, seq) AS running, + row_number() OVER (ORDER BY xact, seq) AS row_number + FROM ( + SELECT COALESCE(e.xact_id, '0'::xid8) AS xact, e.seq, e.id, e.type, e.tenant_id, + COALESCE(e.event_time, e.created_at) AS occurred_at, e.datacontenttype, e.data + FROM public.domain_events e + WHERE` + journalWhere + ` + ORDER BY xact, e.seq + LIMIT $6 + ) page + ) sized + WHERE running <= $7 OR row_number = 1 + ORDER BY xact, seq` + +const countJournalRangeSQL = ` + SELECT count(*) FROM ( + SELECT 1 FROM public.domain_events e + WHERE` + journalWhere + ` + LIMIT $6 + ) counted` + +const journalPositionVisibleSQL = ` + SELECT $1::text::xid8 < pg_snapshot_xmin(pg_current_snapshot())` + +var errJournalNeedsControlPlane = errors.New("journal read: the cross-tenant journal is read inside a control-plane transaction") + +// journalRangeArgs are the shared predicate's parameters $1..$5. +func journalRangeArgs(q business.JournalPageQuery) []any { + var through, throughSeq any // NULL: no upper bound + if q.Through != nil { + through = uitoa(q.Through.Xact) + throughSeq = q.Through.Seq + } + return []any{uitoa(q.After.Xact), q.After.Seq, through, throughSeq, q.Types} +} + +func uitoa(v uint64) string { return strconv.FormatUint(v, 10) } + +func (s *PostgresStore) ReadJournalPage(ctx context.Context, q business.JournalPageQuery) ([]business.JournalRow, error) { + w := wool.Get(ctx).In("ReadJournalPage") + if storetx.Tx(ctx) == nil { + return nil, errJournalNeedsControlPlane + } + if q.Limit < 1 { + return nil, errors.New("journal read: limit must be positive") + } + maxBytes := q.MaxBytes + if maxBytes < 1 { + maxBytes = 1<<31 - 1 + } + rows, err := s.getQueryExecutor(ctx).Query(ctx, readJournalPageSQL, + append(journalRangeArgs(q), q.Limit, maxBytes)...) + if err != nil { + return nil, w.Wrapf(err, "failed to read the journal") + } + defer rows.Close() + + var out []business.JournalRow + for rows.Next() { + var ( + row business.JournalRow + xact string + tenant *string + occurred time.Time + contentType string + ) + if err := rows.Scan(&xact, &row.Position.Seq, &row.EventID, &row.Type, &tenant, &occurred, &contentType, &row.Data); err != nil { + return nil, w.Wrapf(err, "failed to scan a journal row") + } + position, err := strconv.ParseUint(xact, 10, 64) + if err != nil { + return nil, w.Wrapf(err, "failed to read a transaction id") + } + row.Position.Xact = position + if tenant != nil { + row.TenantID = *tenant + } + row.OccurredAt, row.ContentType = occurred, contentType + out = append(out, row) + } + if err := rows.Err(); err != nil { + return nil, w.Wrapf(err, "iterating journal rows") + } + return out, nil +} + +func (s *PostgresStore) CountJournalRange(ctx context.Context, q business.JournalPageQuery, limit int) (int, error) { + w := wool.Get(ctx).In("CountJournalRange") + if storetx.Tx(ctx) == nil { + return 0, errJournalNeedsControlPlane + } + var n int + if err := s.getQueryExecutor(ctx).QueryRow(ctx, countJournalRangeSQL, append(journalRangeArgs(q), limit)...).Scan(&n); err != nil { + return 0, w.Wrapf(err, "failed to count the journal range") + } + return n, nil +} + +func (s *PostgresStore) JournalPositionVisible(ctx context.Context, pos business.JournalPosition) (bool, error) { + w := wool.Get(ctx).In("JournalPositionVisible") + if storetx.Tx(ctx) == nil { + return false, errJournalNeedsControlPlane + } + var visible bool + if err := s.getQueryExecutor(ctx).QueryRow(ctx, journalPositionVisibleSQL, uitoa(pos.Xact)).Scan(&visible); err != nil { + return false, w.Wrapf(err, "failed to read the journal frontier") + } + return visible, nil +} diff --git a/module/services/accounts/code/pkg/infra/postgres_journal_read_test.go b/module/services/accounts/code/pkg/infra/postgres_journal_read_test.go new file mode 100644 index 000000000..b5bd31872 --- /dev/null +++ b/module/services/accounts/code/pkg/infra/postgres_journal_read_test.go @@ -0,0 +1,495 @@ +//go:build !pure + +package infra_test + +import ( + "context" + "fmt" + "strings" + "sync" + "testing" + "time" + + "accounts/pkg/business" + eventsv1 "accounts/pkg/gen/saas/events/v1" + "accounts/pkg/infra" + "accounts/pkg/infra/storetx" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// These tests hold the cross-tenant journal read (business.ModuleReadJournal over +// PostgresStore.ReadJournalPage) to its contract on the real database: it serves +// only rows whose writing transaction has finished, in (xact_id, seq) order, so a +// cursor never passes a row that can still appear below it; it serves every +// tenant's rows and the rows with no tenant to a cross-tenant principal and to no +// one else; and it marks the entries that are the caller's own delivery footprint. + +// journalReadAuthority is the live authority read of a test that is not about it: +// the principal's declared grant, in full, current. The capability decision under +// test is the cross-tenant gate. +type journalReadAuthority struct{ service *business.Service } + +func (f journalReadAuthority) LiveModuleAuthority(_ context.Context, principal, _, _ string) (*business.LiveModuleAuthority, error) { + grant := f.service.ModulePrincipals()[principal] + return &business.LiveModuleAuthority{ + InstallationID: "installation-current", RegistryBindingID: grant.PresenceBinding, + Scope: business.FullInstallationScope(grant), + }, nil +} + +func (f journalReadAuthority) BeginModuleAuthorityUse(ctx context.Context) (context.Context, func(), error) { + return ctx, func() {}, nil +} + +// journalReader is a Service over the real store with one module principal. +func journalReader(t *testing.T, crossTenant bool) (*business.Service, business.ModuleCaller) { + t.Helper() + svc, err := business.NewService(testStore) + require.NoError(t, err) + svc.SetModuleAuthorityReads(journalReadAuthority{service: svc}, nil) + tenant := business.NewIDString() + caller := business.ModuleCaller{PrincipalID: business.ModulePrincipalID("journal-reader"), BoundOrg: tenant} + svc.SetModuleCapabilities(nil, nil, business.ModulePrincipalRegistry{ + caller.PrincipalID: {Prefix: "journal-reader", Tenant: tenant, CrossTenant: crossTenant}, + }) + return svc, caller +} + +// freshJournalType is an event type no other test publishes, so a read of it sees +// exactly the rows this test wrote. +func freshJournalType() string { + return "saas.test.journalread_" + strings.ReplaceAll(uuid.NewString()[:8], "-", "") +} + +func publishJournalRow(ctx context.Context, eventType, tenant string) (string, error) { + transport := infra.NewPostgresEventTransport(infra.NewPostgresJobStore(testPool), testPool, "journal-read-"+uuid.NewString(), time.Minute) + envelope := &eventsv1.EventEnvelope{ + Id: uuid.NewString(), Type: eventType, Source: "saas/test", Subject: "subject", Specversion: "1.0", + Datacontenttype: "application/json", Time: timestamppb.Now(), Data: []byte(`{"k":"v"}`), TenantId: tenant, + } + return envelope.Id, transport.Publish(ctx, storetx.Tx(ctx), envelope) +} + +// publishCommitted writes one row in a transaction of its own and commits it. +func publishCommitted(t *testing.T, eventType, tenant string) string { + t.Helper() + var id string + require.NoError(t, testStore.WithControlPlane(testCtx, func(ctx context.Context) error { + var err error + id, err = publishJournalRow(ctx, eventType, tenant) + return err + })) + return id +} + +// openTx is a control-plane transaction held open on its own goroutine until +// commit is called, so a test can interleave other writers with it. +type openTx struct { + steps chan func(context.Context) error + done chan error + once sync.Once +} + +func openControlPlaneTx(t *testing.T) *openTx { + t.Helper() + tx := &openTx{steps: make(chan func(context.Context) error), done: make(chan error, 1)} + started := make(chan struct{}) + go func() { + tx.done <- testStore.WithControlPlane(testCtx, func(ctx context.Context) error { + close(started) + for step := range tx.steps { + if err := step(ctx); err != nil { + return err + } + } + return nil + }) + }() + <-started + t.Cleanup(func() { tx.once.Do(func() { close(tx.steps) }) }) // a test that failed before commit + return tx +} + +// do runs one statement's worth of work inside the open transaction and waits for it. +func (o *openTx) do(t *testing.T, fn func(context.Context) error) { + t.Helper() + failed := make(chan error, 1) + o.steps <- func(ctx context.Context) error { + err := fn(ctx) + failed <- err + return err + } + require.NoError(t, <-failed) +} + +func (o *openTx) commit(t *testing.T) { + t.Helper() + o.once.Do(func() { close(o.steps) }) + require.NoError(t, <-o.done) +} + +func readJournal(t *testing.T, svc *business.Service, caller business.ModuleCaller, after string, types ...string) business.ModuleReadJournalResult { + t.Helper() + page, err := svc.ModuleReadJournal(testCtx, caller, business.ModuleReadJournalInput{After: after, Types: types}) + require.NoError(t, err) + return page +} + +func eventIDs(page business.ModuleReadJournalResult) []string { + ids := make([]string, 0, len(page.Entries)) + for _, entry := range page.Entries { + ids = append(ids, entry.EventID) + } + return ids +} + +// A transaction that has written a row and not committed holds the read back: a +// row with a higher seq that committed first is not served while the lower one is +// open, the cursor stops below both, and once the first commits both are served, +// the lower one first, with nothing skipped. A cursor on seq alone serves the +// higher row at once and then never returns for the lower. +func TestModuleReadJournalHoldsBehindAnOpenTransactionAndSkipsNothing(t *testing.T) { + svc, caller := journalReader(t, true) + eventType := freshJournalType() + + committed := publishCommitted(t, eventType, "") + + var held string + open := openControlPlaneTx(t) + open.do(t, func(ctx context.Context) (err error) { + held, err = publishJournalRow(ctx, eventType, "") + return err + }) + later := publishCommitted(t, eventType, "") // a higher seq, committed first + + var heldSeq, laterSeq int64 + require.NoError(t, testStore.WithControlPlane(testCtx, func(ctx context.Context) error { + if err := storetx.Tx(ctx).QueryRow(ctx, `SELECT seq FROM domain_events WHERE id = $1::uuid`, later).Scan(&laterSeq); err != nil { + return err + } + // The open transaction's own row is invisible from here; its seq is read + // inside it below. + return nil + })) + open.do(t, func(ctx context.Context) error { + return storetx.Tx(ctx).QueryRow(ctx, `SELECT seq FROM domain_events WHERE id = $1::uuid`, held).Scan(&heldSeq) + }) + require.Less(t, heldSeq, laterSeq, "the open transaction's row holds the lower seq") + + first := readJournal(t, svc, caller, "", eventType) + require.Equal(t, []string{committed}, eventIDs(first), "neither the open row nor the row that committed above it is served") + require.Equal(t, first.Entries[0].Position, first.NextAfter, "next_after is the last entry's position") + + // The same read again changes nothing, and an empty read keeps its cursor. + again := readJournal(t, svc, caller, first.NextAfter, eventType) + require.Empty(t, again.Entries) + require.Equal(t, first.NextAfter, again.NextAfter, "an empty page resumes where it was asked from") + + open.commit(t) + + second := readJournal(t, svc, caller, first.NextAfter, eventType) + require.Equal(t, []string{held, later}, eventIDs(second), "after the commit both rows are served, the lower one first") + require.Equal(t, second.Entries[1].Position, second.NextAfter) + + // Everything, from the beginning, is the three rows once each, in order, and + // "start" and "" both mean the beginning. + all := readJournal(t, svc, caller, "", eventType) + require.Equal(t, []string{committed, held, later}, eventIDs(all)) + require.Equal(t, eventIDs(all), eventIDs(readJournal(t, svc, caller, business.JournalStart, eventType))) + require.Equal(t, business.JournalStart, readJournal(t, svc, caller, business.JournalStart, freshJournalType()).NextAfter, + "an empty page from the start resumes at the start") + require.Empty(t, readJournal(t, svc, caller, second.NextAfter, eventType).Entries) +} + +// The order is the writing transaction's, then seq. A transaction that took its id +// first and inserted last sorts before one that took a later id and committed +// sooner, even though its seq is higher; and the later one is held until the +// earlier one is done, because the earlier one can still commit below it. +func TestModuleReadJournalOrdersByTransactionThenSeq(t *testing.T) { + svc, caller := journalReader(t, true) + eventType := freshJournalType() + + open := openControlPlaneTx(t) + open.do(t, func(ctx context.Context) error { // the id is taken here, the row is written later + var xact string + return storetx.Tx(ctx).QueryRow(ctx, `SELECT pg_current_xact_id()::text`).Scan(&xact) + }) + later := publishCommitted(t, eventType, "") // a later id, a lower seq, committed first + require.Empty(t, readJournal(t, svc, caller, "", eventType).Entries, "held behind the transaction that has the earlier id") + + var held string + open.do(t, func(ctx context.Context) (err error) { + held, err = publishJournalRow(ctx, eventType, "") + return err + }) + require.Empty(t, readJournal(t, svc, caller, "", eventType).Entries, "still open") + open.commit(t) + + page := readJournal(t, svc, caller, "", eventType) + require.Equal(t, []string{held, later}, eventIDs(page), "the earlier transaction first, though its seq is higher") + var seqs [2]int64 + require.NoError(t, testStore.WithControlPlane(testCtx, func(ctx context.Context) error { + for i, id := range []string{held, later} { + if err := storetx.Tx(ctx).QueryRow(ctx, `SELECT seq FROM domain_events WHERE id = $1::uuid`, id).Scan(&seqs[i]); err != nil { + return err + } + } + return nil + })) + require.Greater(t, seqs[0], seqs[1]) +} + +// Paging by position over rows written by several transactions neither repeats nor +// skips a row, whatever the page size, and the page bound is held. +func TestModuleReadJournalPagesWithoutRepeatOrSkip(t *testing.T) { + svc, caller := journalReader(t, true) + eventType := freshJournalType() + var want []string + for range 7 { + want = append(want, publishCommitted(t, eventType, "")) + } + var got []string + after := "" + for range 10 { + page, err := svc.ModuleReadJournal(testCtx, caller, business.ModuleReadJournalInput{After: after, Limit: 3, Types: []string{eventType}}) + require.NoError(t, err) + require.LessOrEqual(t, len(page.Entries), 3) + if len(page.Entries) == 0 { + break + } + got = append(got, eventIDs(page)...) + after = page.NextAfter + } + require.Equal(t, want, got) +} + +// The page is bounded in bytes as well as rows: the first row is always served so +// a row larger than the bound still moves the cursor, and a row is served only +// while the bytes before it fit. +func TestReadJournalPageBoundsTheBytesOfAPage(t *testing.T) { + eventType := freshJournalType() + body := `{"pad":"` + strings.Repeat("x", 600) + `"}` + for range 3 { + require.NoError(t, testStore.WithControlPlane(testCtx, func(ctx context.Context) error { + envelope := &eventsv1.EventEnvelope{ + Id: uuid.NewString(), Type: eventType, Source: "saas/test", Specversion: "1.0", + Datacontenttype: "application/json", Time: timestamppb.Now(), Data: []byte(body), + } + return auditJournalTransport(t).Publish(ctx, storetx.Tx(ctx), envelope) + })) + } + read := func(maxBytes int) (rows []business.JournalRow) { + require.NoError(t, testStore.WithControlPlane(testCtx, func(ctx context.Context) (err error) { + rows, err = testStore.ReadJournalPage(ctx, business.JournalPageQuery{Types: []string{eventType}, Limit: 100, MaxBytes: maxBytes}) + return err + })) + return rows + } + require.Len(t, read(1), 1, "the first row is always served") + require.Len(t, read(len(body)), 1, "the next row would exceed the byte bound") + require.Len(t, read(2*len(body)), 2) + require.Len(t, read(0), 3, "no bound") +} + +// The read refuses to run outside a control-plane transaction instead of falling +// back to the request pool, where an empty answer would look like a quiet journal. +func TestJournalReadNeedsAControlPlaneTransaction(t *testing.T) { + _, err := testStore.ReadJournalPage(testCtx, business.JournalPageQuery{Types: []string{freshJournalType()}, Limit: 1}) + require.Error(t, err) + _, err = testStore.CountJournalRange(testCtx, business.JournalPageQuery{Types: []string{freshJournalType()}}, 1) + require.Error(t, err) + _, err = testStore.JournalPositionVisible(testCtx, business.JournalPosition{Xact: 1}) + require.Error(t, err) +} + +// Every tenant's rows and the rows with no tenant are served to a cross-tenant +// principal, in one read; a principal that is not cross-tenant is refused. The +// tenant-scoped read of the same journal, by contrast, sees one tenant. +func TestModuleReadJournalServesEveryTenantAndNoTenant(t *testing.T) { + svc, caller := journalReader(t, true) + eventType := freshJournalType() + orgX, orgY := seedOrg(t, seedUser(t)), seedOrg(t, seedUser(t)) + + x := publishCommitted(t, eventType, orgX) + none := publishCommitted(t, eventType, "") + y := publishCommitted(t, eventType, orgY) + + page := readJournal(t, svc, caller, "", eventType) + require.Equal(t, []string{x, none, y}, eventIDs(page)) + require.Equal(t, []string{orgX, "", orgY}, []string{ + page.Entries[0].OrganizationID, page.Entries[1].OrganizationID, page.Entries[2].OrganizationID, + }, "the row with no tenant is served with an empty organization") + for _, entry := range page.Entries { + require.Equal(t, eventType, entry.Type) + require.Equal(t, map[string]any{"k": "v"}, entry.Data) + require.False(t, entry.DeliveryMachinery) + require.WithinDuration(t, time.Now(), entry.OccurredAt, time.Minute) + require.True(t, strings.HasPrefix(entry.Position, "v1."), entry.Position) + } + + // The same rows through a tenant's own transaction: one tenant's. + var seen int + require.NoError(t, testStore.WithOrgTx(testCtx, orgX, func(ctx context.Context) error { + return storetx.Tx(ctx).QueryRow(ctx, `SELECT count(*) FROM domain_events WHERE type = $1`, eventType).Scan(&seen) + })) + require.Equal(t, 1, seen) + + // Not cross-tenant: refused, and the refusal is the gate's, not an empty page. + bound, boundCaller := journalReader(t, false) + _, err := bound.ModuleReadJournal(testCtx, boundCaller, business.ModuleReadJournalInput{Types: []string{eventType}}) + require.Equal(t, codes.PermissionDenied, status.Code(err), "%v", err) + _, err = svc.ModuleReadJournal(testCtx, business.ModuleCaller{PrincipalID: business.ModulePrincipalID("unregistered")}, business.ModuleReadJournalInput{Types: []string{eventType}}) + require.Equal(t, codes.Unauthenticated, status.Code(err), "%v", err) + require.ErrorContains(t, err, "not a registered module principal") +} + +// The positions the read issues are the ones it accepts, and one it did not issue +// is refused. +func TestModuleReadJournalRefusesAMalformedPosition(t *testing.T) { + svc, caller := journalReader(t, true) + _, err := svc.ModuleReadJournal(testCtx, caller, business.ModuleReadJournalInput{After: "v1.12", Types: []string{freshJournalType()}}) + require.Equal(t, codes.InvalidArgument, status.Code(err), "%v", err) +} + +// delivery_machinery is computed here from the entries the host's own emitters +// write, never from a payload the caller parses: the mint of an operation context +// and the exchange on one of the caller's delivery bindings, and the caller's own +// Work Context mint, and nothing else. +func TestModuleReadJournalMarksTheCallersDeliveryMachinery(t *testing.T) { + warehouse := &warehouseStorage{} + audit, err := business.NewAuditService(business.AuditServiceConfig{ + Backend: business.AuditBackendWarehouse, Store: testStore, Producer: testStore, + Transport: auditJournalTransport(t), Warehouse: warehouse, + }) + require.NoError(t, err) + svc, caller := journalReader(t, true) + svc.SetAuditService(audit) + require.NoError(t, svc.VerifyAuditWiring()) + + org := seedOrg(t, seedUser(t)) + otherModule := business.ModulePrincipalID("some-other-module") + execution := business.ExecutionIdentity{ + Namespace: "ns", ServiceAccount: "sa", PodName: "pod", PodUID: uuid.NewString(), ContainerName: "c", + } + authority := func(principal string) business.ModuleWorkContextAuthority { + identity := execution + identity.PrincipalID = principal + return business.ModuleWorkContextAuthority{PrincipalID: principal, Tenant: org, Execution: identity} + } + operationMint := func(binding string) business.ModuleOperationContextAuthority { + return business.ModuleOperationContextAuthority{ + ModuleWorkContextAuthority: authority(caller.PrincipalID), BindingID: binding, Audience: "saas-audit", + Scopes: []business.ModuleOperationScope{{ResourceKind: "saas.audit-delivery", Actions: []string{"invoke"}}}, + } + } + exchange := func(binding string) business.DelegatedAudienceExchangeObservation { + return business.DelegatedAudienceExchangeObservation{ + Caller: caller, ActorID: uuid.NewString(), Tenant: org, BindingKind: "operation", BindingID: binding, + Audience: "saas-audit", Outcome: business.DelegatedAudienceExchangeIssued, + } + } + + head := drainJournal(t, svc, caller, "", business.AuditRecordedEventType).next + + // In the order written; the wanted flag beside each. + steps := []struct { + name string + event business.EventType + want bool + do func() error + }{ + {"operation context for a delivery binding", business.EventModuleOperationContextMint, true, + func() error { + return svc.RecordModuleOperationContextMint(testCtx, "journal-reader", operationMint("delivery-binding")) + }}, + {"operation context for another binding", business.EventModuleOperationContextMint, false, + func() error { + return svc.RecordModuleOperationContextMint(testCtx, "journal-reader", operationMint("customer-binding")) + }}, + {"exchange on a delivery binding", business.EventDelegatedAudienceExchange, true, + func() error { svc.ObserveDelegatedAudienceExchange(testCtx, exchange("delivery-binding")); return nil }}, + {"exchange on another binding", business.EventDelegatedAudienceExchange, false, + func() error { svc.ObserveDelegatedAudienceExchange(testCtx, exchange("customer-binding")); return nil }}, + {"the caller's own Work Context mint", business.EventModuleWorkContextMint, true, + func() error { + return svc.RecordModuleWorkContextMint(testCtx, "journal-reader", authority(caller.PrincipalID)) + }}, + {"another module's Work Context mint", business.EventModuleWorkContextMint, false, + func() error { + return svc.RecordModuleWorkContextMint(testCtx, "some-other-module", authority(otherModule)) + }}, + // An entry of another type that happens to carry a delivery binding's id is + // not the caller's footprint: the type decides, not a field's value. + {"an ordinary entry that names a delivery binding", business.EventSolutionHostBindingApplied, false, + func() error { + return testStore.WithControlPlane(testCtx, func(ctx context.Context) error { + return audit.EmitTx(ctx, business.AuditEntry{ + ID: uuid.NewString(), EventType: business.EventSolutionHostBindingApplied, ActorID: uuid.NewString(), + ActorType: business.ActorTypeSystem, Resource: "solution", ResourceID: uuid.NewString(), OrgID: org, + Payload: map[string]any{ + "binding_id": "delivery-binding", "solution_id": "example", "generation": int64(1), + "digest": "sha256:" + strings.Repeat("b", 64), "release": "r1", "removed": false, "routes": []any{}, + }, + CreatedAt: time.Now().UTC(), + }) + }) + }}, + } + for _, step := range steps { + require.NoError(t, step.do(), step.name) + } + + read, err := svc.ModuleReadJournal(testCtx, caller, business.ModuleReadJournalInput{ + After: head, Types: []string{business.AuditRecordedEventType}, DeliveryBindings: []string{"delivery-binding"}, + }) + require.NoError(t, err) + var mine []business.ModuleJournalEntry + for _, entry := range read.Entries { + if entry.OrganizationID == org { + mine = append(mine, entry) + } + } + require.Len(t, mine, len(steps)) + for i, step := range steps { + require.Equal(t, string(step.event), mine[i].Data["event_type"], step.name) + require.Equal(t, step.want, mine[i].DeliveryMachinery, step.name) + } + + // With no delivery bindings named, only the caller's own mint is marked. + unbound, err := svc.ModuleReadJournal(testCtx, caller, business.ModuleReadJournalInput{After: head, Types: []string{business.AuditRecordedEventType}}) + require.NoError(t, err) + var flagged []string + for _, entry := range unbound.Entries { + if entry.OrganizationID == org && entry.DeliveryMachinery { + flagged = append(flagged, fmt.Sprint(entry.Data["event_type"])) + } + } + require.Equal(t, []string{string(business.EventModuleWorkContextMint)}, flagged) +} + +type drained struct { + entries []business.ModuleJournalEntry + next string +} + +// drainJournal reads to the end of the visible journal for the types. +func drainJournal(t *testing.T, svc *business.Service, caller business.ModuleCaller, after string, types ...string) drained { + t.Helper() + out := drained{next: after} + for range 1000 { + page, err := svc.ModuleReadJournal(testCtx, caller, business.ModuleReadJournalInput{After: out.next, Limit: business.ModuleJournalMaxPage, Types: types}) + require.NoError(t, err) + if len(page.Entries) == 0 { + return out + } + out.entries = append(out.entries, page.Entries...) + out.next = page.NextAfter + } + t.Fatal("the journal did not drain") + return out +} diff --git a/module/services/accounts/code/pkg/infra/postgres_role_hardening_test.go b/module/services/accounts/code/pkg/infra/postgres_role_hardening_test.go index e1dc94424..d3084f7a9 100644 --- a/module/services/accounts/code/pkg/infra/postgres_role_hardening_test.go +++ b/module/services/accounts/code/pkg/infra/postgres_role_hardening_test.go @@ -381,14 +381,17 @@ func TestControlPlaneRelationGrantsAreExact(t *testing.T) { if relation == "executable_artifact_approvals" { want = relationPrivileges{} } - // Callable-source declarations, OAuth state, attempts, receipts and - // retained object listings are tenant-only. The control plane must not read or mutate them. + // Callable-source declarations, OAuth state, attempts and retained + // object listings are tenant-only. switch relation { case "datasource_source_operations", "datasource_oauth_consumed_states", - "datasource_operation_attempts", "codefly_effect_receipts", - "datasource_object_manifests": + "datasource_operation_attempts", "datasource_object_manifests": want = relationPrivileges{} } + if relation == "codefly_effect_receipts" { + // RLS exposes only committed audit delivery receipts to retention. + want = relationPrivileges{selectRows: true} + } if relation == "feature_flags" { want = relationPrivileges{selectRows: true} } @@ -481,11 +484,9 @@ func TestControlPlaneRelationGrantsAreExact(t *testing.T) { if relation == "work_context_replay" { want = relationPrivileges{selectRows: true, insertRows: true, deleteRows: true} } - // domain_events and event_subscriptions: the control plane publishes - // platform events and maintains subscriptions (materialize / subscribe / - // unsubscribe marks revoked_at), so it reads, inserts, and updates but - // never row-deletes — retention and revocation are soft. - if relation == "domain_events" || relation == "event_subscriptions" { + // Subscriptions are revoked softly. Journal DELETE is separately + // restricted by RLS to audit records older than thirty days. + if relation == "event_subscriptions" { want = relationPrivileges{selectRows: true, insertRows: true, updateRows: true} } // team_membership_quarantine records what migration 127 removed. Only @@ -810,9 +811,10 @@ func TestActivePoliciesDoNotTrustSessionBypassSettings(t *testing.T) { // Scope inventory and policy presence alone cannot support that claim: a // relation keeps forced RLS and a non-zero policy count when a migration adds a // permissive USING (true) policy beside the scoped one. So this checks EVERY -// policy expression individually — each must compare the published scope column -// or admit nothing — rather than asking whether the column appears somewhere in -// the relation's policies. +// permissive request policy individually — each must compare the published scope +// column or admit nothing. PostgreSQL ORs permissive grants and ANDs restrictive +// filters: a restrictive predicate cannot grant authority on its own. Bounded +// control-plane grants and the audit-retention filter are checked exactly below. func TestPublishedRLSPolicyDetailMatchesLivePolicies(t *testing.T) { require.NoError(t, testStore.WithControlPlane(testCtx, func(ctx context.Context) error { tx := storetx.Tx(ctx) @@ -825,21 +827,45 @@ func TestPublishedRLSPolicyDetailMatchesLivePolicies(t *testing.T) { require.NotEmpty(t, authority.PolicyShape, relation) policies := livePolicies(ctx, t, tx, relation) + if relation == "domain_events" { + require.Contains(t, policies, livePolicy{ + name: "audit_journal_retention_delete", permissive: false, + roles: []string{"app_control_plane"}, command: "d", + expressions: []string{"((type = 'saas.audit.recorded.v1'::text) AND (created_at < (statement_timestamp() - '720:00:00'::interval)))"}, + }, "audit retention must remain a control-plane-only restrictive DELETE with the exact type and thirty-day minimum age") + } + if relation == "codefly_effect_receipts" { + require.Contains(t, policies, livePolicy{ + name: "audit_delivery_retention_receipts", permissive: true, + roles: []string{"app_control_plane"}, command: "r", + expressions: []string{"((CURRENT_USER = 'app_control_plane'::name) AND (method = '/saas.accounts.v1.AuditService/DeliverAuditEvents'::text))"}, + }, "retention may read only audit-delivery receipts as the exact active control-plane role") + } // Explicit background policies replace BYPASSRLS only for the exact - // active SQL role. Keep checking every request-visible predicate. + // active SQL role. Keep checking every permissive request predicate. requestPolicies := policies[:0] for _, policy := range policies { if policy.migrationOwner != "" { + require.True(t, policy.permissive, relation) require.Equal(t, "webhook_subscriptions", relation) require.Equal(t, "r", policy.command, relation) require.Equal(t, []string{"(CURRENT_USER = '" + policy.migrationOwner + "'::name)"}, policy.expressions, relation) continue } if policy.backgroundRole != "" { + require.True(t, policy.permissive, relation) require.Equal(t, "*", policy.command, relation) require.Equal(t, []string{"(CURRENT_USER = '" + policy.backgroundRole + "'::name)", "(CURRENT_USER = '" + policy.backgroundRole + "'::name)"}, policy.expressions, relation) continue } + if relation == "codefly_effect_receipts" && policy.name == "audit_delivery_retention_receipts" { + // The exact command, role and predicate were checked above; + // this policy grants no request connection any rows. + continue + } + if !policy.permissive { + continue // Restrictive policies can only narrow existing grants. + } requestPolicies = append(requestPolicies, policy) } policies = requestPolicies @@ -894,6 +920,9 @@ func TestPublishedRLSPolicyDetailMatchesLivePolicies(t *testing.T) { } type livePolicy struct { + name string + permissive bool + roles []string backgroundRole string migrationOwner string command string @@ -903,7 +932,10 @@ type livePolicy struct { func livePolicies(ctx context.Context, t *testing.T, tx pgx.Tx, relation string) []livePolicy { t.Helper() rows, err := tx.Query(ctx, ` - SELECT policy.polcmd::text, + SELECT policy.polname, policy.polpermissive, + ARRAY(SELECT CASE WHEN role_oid = 0 THEN 'PUBLIC' ELSE pg_get_userbyid(role_oid)::text END + FROM unnest(policy.polroles) AS role_oid ORDER BY 1), + policy.polcmd::text, COALESCE(pg_get_expr(policy.polqual, policy.polrelid), ''), COALESCE(pg_get_expr(policy.polwithcheck, policy.polrelid), ''), CASE WHEN cardinality(policy.polroles)=1 AND policy.polname=role.rolname || '_explicit_rows' @@ -923,8 +955,9 @@ func livePolicies(ctx context.Context, t *testing.T, tx pgx.Tx, relation string) var policies []livePolicy for rows.Next() { var command, using, check, backgroundRole, migrationOwner string - require.NoError(t, rows.Scan(&command, &using, &check, &backgroundRole, &migrationOwner), relation) - policy := livePolicy{command: command, backgroundRole: backgroundRole, migrationOwner: migrationOwner} + var policy livePolicy + require.NoError(t, rows.Scan(&policy.name, &policy.permissive, &policy.roles, &command, &using, &check, &backgroundRole, &migrationOwner), relation) + policy.command, policy.backgroundRole, policy.migrationOwner = command, backgroundRole, migrationOwner for _, expression := range []string{using, check} { if expression != "" { policy.expressions = append(policy.expressions, expression) diff --git a/module/services/accounts/code/pkg/infra/testdata/control_plane_call_sites.txt b/module/services/accounts/code/pkg/infra/testdata/control_plane_call_sites.txt index c25965cea..cb7514f40 100644 --- a/module/services/accounts/code/pkg/infra/testdata/control_plane_call_sites.txt +++ b/module/services/accounts/code/pkg/infra/testdata/control_plane_call_sites.txt @@ -27,8 +27,9 @@ pkg/auth/pg/session_store.go: (*SessionStore).FindByRefreshHash: WithControlPlan pkg/auth/pg/session_store.go: (*SessionStore).RevokeFamily: WithControlPlane pkg/auth/pg/session_store.go: (*SessionStore).RotateRefresh: WithControlPlane pkg/business/audit.go: (*DurableAuditEmitter).Emit: WithControlPlane -pkg/business/audit.go: (*Service).AggregateAuditLog: WithControlPlane -pkg/business/audit.go: (*Service).QueryAuditLog: WithControlPlane +pkg/business/audit_delivery.go: (*Service).DeliverAuditEvents: WithControlPlane +pkg/business/audit_delivery.go: (*Service).DeliverAuditEvents: WithControlPlane +pkg/business/audit_storage.go: withinAuditScope: WithControlPlane pkg/business/authority_core_delivery.go: (*Service).composeAppliedDelivery: WithControlPlane pkg/business/authority_delivery.go: (*Service).verifyAppliedAuthorityDocument: WithControlPlane pkg/business/connector_credentials.go: (*Service).SourceSigningSecret: WithControlPlane @@ -73,6 +74,7 @@ pkg/business/invitations.go: (*Service).ResendInvitation: As(System()) pkg/business/invitations.go: (*Service).RevokeInvitation: WithControlPlane pkg/business/invitations.go: (*Service).invitationByToken: As(System()) pkg/business/invitations.go: (*Service).pendingInvitationForSsoJit: As(System()) +pkg/business/journal_read.go: (*Service).ModuleReadJournal: WithControlPlane pkg/business/magic_links.go: (*Service).SendMagicLink: As(System()) pkg/business/magic_links.go: (*Service).VerifyMagicLink: As(System()) pkg/business/magic_links.go: (*Service).VerifyMagicLink: As(System()) @@ -131,6 +133,7 @@ pkg/business/retention.go: (*Service).RunRetention: WithControlPlane pkg/business/retention.go: (*Service).RunRetention: WithControlPlane pkg/business/retention.go: (*Service).RunRetention: WithControlPlane pkg/business/retention.go: (*Service).RunRetention: WithControlPlane +pkg/business/retention.go: (*Service).RunRetention: WithControlPlane pkg/business/service.go: (*Service).AssignRole: WithControlPlane pkg/business/service.go: (*Service).CheckPermission: WithControlPlane pkg/business/service.go: (*Service).CreateRole: WithControlPlane diff --git a/module/services/accounts/code/pkg/relationcatalog/catalog.go b/module/services/accounts/code/pkg/relationcatalog/catalog.go index f7a859592..f50b5a48b 100644 --- a/module/services/accounts/code/pkg/relationcatalog/catalog.go +++ b/module/services/accounts/code/pkg/relationcatalog/catalog.go @@ -7,8 +7,9 @@ // RLS-protected subset into its GetServiceInfo catalog, so a consumer asking // "which relations are RLS-protected?" reads the same list the infrastructure // hardening suite checks a live database against (relation grants, ENABLE / -// FORCE ROW LEVEL SECURITY, policy presence, and the scope column each policy -// actually references). Neither copy can drift from the other, and both drift +// FORCE ROW LEVEL SECURITY, policy presence, and the scope column each permissive +// request policy references). Restrictive policies only narrow those grants. +// Neither copy can drift from the other, and both drift // loudly from the database rather than quietly. package relationcatalog diff --git a/module/services/accounts/code/work.go b/module/services/accounts/code/work.go index 57714e12b..599a5c911 100644 --- a/module/services/accounts/code/work.go +++ b/module/services/accounts/code/work.go @@ -5,6 +5,7 @@ import ( "accounts/pkg/abuse" "accounts/pkg/adapters" "accounts/pkg/analytics" + "accounts/pkg/auditstorage" "accounts/pkg/auth" devvalidator "accounts/pkg/auth/dev" ed25519minter "accounts/pkg/auth/ed25519" @@ -684,19 +685,33 @@ func doWork(ctx context.Context) (clean Clean, err error) { if err != nil { return nil, err } - var auditEmitterOpts []business.DurableAuditEmitterOption - if auditSinkMode == auditSinkBoth { - auditEmitterOpts = append(auditEmitterOpts, business.WithExternalTee()) - } // Every org-scoped audit record publishes its external domain event in the // same transaction; that event is what the relay fans out to the endpoints // subscribed to its type. - auditEmitterOpts = append(auditEmitterOpts, business.WithDomainEventTransport(eventTransport)) - auditEmitter, err := business.NewDurableAuditEmitter(store, store, auditEmitterOpts...) + // + // Where the trail is kept is chosen here, once, from the `audit-storage` + // group: Postgres (audit_events, written in the caller's transaction) or the + // warehouse (journaled in the caller's transaction and appended afterwards). + auditStorageConfig, err := auditstorage.Load(ctx) + if err != nil { + return nil, err + } + auditBackend, warehouseAuditStorage, err := selectAuditStorage(auditStorageConfig, auditSinkMode, buildWarehouseAuditStorage) + if err != nil { + return nil, err + } + auditService, err := business.NewAuditService(business.AuditServiceConfig{ + Backend: auditBackend, + Store: store, + Producer: store, + Transport: eventTransport, + ExternalTee: auditSinkMode == auditSinkBoth, + Warehouse: warehouseAuditStorage, + }) if err != nil { return nil, err } - service.SetAuditEmitter(auditEmitter) + service.SetAuditService(auditService) // Refuse to serve behind an emitter that cannot write on the caller's // transaction: every security mutation commits its audit row and webhook // fan-out inside its own transaction, and an emitter without EmitTx would let @@ -1423,7 +1438,7 @@ func doWork(ctx context.Context) (clean Clean, err error) { sw.Info("stopping retention goroutine") retentionCancel() sw.Info("closing audit emitter") - auditEmitter.Close() + auditService.Close() sw.Info("audit emitter closed") if closeCache != nil { sw.Info("closing redis cache") diff --git a/module/services/accounts/code/work_audit_test.go b/module/services/accounts/code/work_audit_test.go index 9aaa39814..b5cbbf3f5 100644 --- a/module/services/accounts/code/work_audit_test.go +++ b/module/services/accounts/code/work_audit_test.go @@ -1,8 +1,11 @@ package main import ( + "errors" + "strings" "testing" + "accounts/pkg/auditstorage" "accounts/pkg/business" "github.com/stretchr/testify/require" @@ -46,3 +49,74 @@ func TestConfiguredAuditSinkRejectsUnknownMode(t *testing.T) { _, _, err := configuredAuditSink() require.ErrorContains(t, err, "must be postgres or both") } + +// fakeWarehouseAuditStorage stands in for the warehouse AuditStorage, which is +// not built yet: selection and the write path are exercised with it. +type fakeWarehouseAuditStorage struct{ business.AuditStorage } + +func TestSelectAuditStoragePostgresIsTheDefault(t *testing.T) { + built := false + build := func(auditstorage.Config) (business.AuditStorage, error) { built = true; return nil, nil } + for _, sink := range []auditSinkMode{auditSinkPostgres, auditSinkBoth} { + backend, storage, err := selectAuditStorage(auditstorage.Config{Backend: business.AuditBackendPostgres}, sink, build) + if err != nil || backend != business.AuditBackendPostgres || storage != nil { + t.Fatalf("sink %v: %v %v %v", sink, backend, storage, err) + } + } + if built { + t.Fatal("the warehouse storage is not built under the Postgres backend") + } +} + +func TestSelectAuditStorageWarehouseBuildsTheStorage(t *testing.T) { + fake := fakeWarehouseAuditStorage{} + var got auditstorage.Config + config := auditstorage.Config{Backend: business.AuditBackendWarehouse, Dataset: "audit", Table: "entries"} + backend, storage, err := selectAuditStorage(config, auditSinkPostgres, func(c auditstorage.Config) (business.AuditStorage, error) { + got = c + return fake, nil + }) + if err != nil || backend != business.AuditBackendWarehouse || storage != business.AuditStorage(fake) { + t.Fatalf("%v %v %v", backend, storage, err) + } + if got != config { + t.Fatalf("the builder was given %+v, want the group's names %+v", got, config) + } +} + +// The tee copies audit_events rows and the warehouse writes none: the pair is +// refused at startup, by name, before anything is built. +func TestSelectAuditStorageRefusesTheTeeWithTheWarehouse(t *testing.T) { + built := false + _, _, err := selectAuditStorage(auditstorage.Config{Backend: business.AuditBackendWarehouse, Dataset: "d", Table: "t"}, auditSinkBoth, + func(auditstorage.Config) (business.AuditStorage, error) { + built = true + return fakeWarehouseAuditStorage{}, nil + }) + if err == nil || !strings.Contains(err.Error(), "AUDIT_SINK=both") || !strings.Contains(err.Error(), "AUDIT_STORAGE_BACKEND=warehouse") { + t.Fatalf("err = %v, want a refusal naming both settings", err) + } + if built { + t.Fatal("the storage was built for a combination that is refused") + } +} + +// Until the warehouse AuditStorage exists in this build, `warehouse` is refused +// at startup with a message that says why and what to set instead. +func TestWarehouseBackendIsRefusedAtStartupUntilItsStorageLands(t *testing.T) { + _, _, err := selectAuditStorage(auditstorage.Config{Backend: business.AuditBackendWarehouse, Dataset: "d", Table: "t"}, auditSinkPostgres, buildWarehouseAuditStorage) + if !errors.Is(err, errWarehouseAuditStorageUnavailable) { + t.Fatalf("err = %v", err) + } + if !strings.Contains(err.Error(), "AUDIT_STORAGE_BACKEND=warehouse") || !strings.Contains(err.Error(), "postgres") { + t.Fatalf("the refusal must name the setting and what to use instead: %v", err) + } +} + +func TestSelectAuditStorageRefusesABuilderThatReturnsNothing(t *testing.T) { + _, _, err := selectAuditStorage(auditstorage.Config{Backend: business.AuditBackendWarehouse, Dataset: "d", Table: "t"}, auditSinkPostgres, + func(auditstorage.Config) (business.AuditStorage, error) { return nil, nil }) + if err == nil { + t.Fatal("a warehouse backend with no storage is refused") + } +} diff --git a/module/services/accounts/generated/api-contract-surfaces.json b/module/services/accounts/generated/api-contract-surfaces.json index 69272354e..1a8a43947 100644 --- a/module/services/accounts/generated/api-contract-surfaces.json +++ b/module/services/accounts/generated/api-contract-surfaces.json @@ -29,6 +29,7 @@ "/saas.accounts.v1.ModuleCapabilitiesService/NotifyUser", "/saas.accounts.v1.ModuleCapabilitiesService/PlaceRecord", "/saas.accounts.v1.ModuleCapabilitiesService/PublishEvent", + "/saas.accounts.v1.ModuleCapabilitiesService/ReadJournal", "/saas.accounts.v1.ModuleCapabilitiesService/ReplayEvents", "/saas.accounts.v1.ModuleCapabilitiesService/RequestApproval", "/saas.accounts.v1.ModuleCapabilitiesService/ResolveWorkContextAuthority", @@ -132,8 +133,10 @@ "fullName": "saas.accounts.v1.AuditService", "procedures": [ "/saas.accounts.v1.AuditService/AggregateAuditLog", + "/saas.accounts.v1.AuditService/DeliverAuditEvents", "/saas.accounts.v1.AuditService/ExportAuditLog", "/saas.accounts.v1.AuditService/ListAuditEventTypes", + "/saas.accounts.v1.AuditService/LookupDeliverAuditEvents", "/saas.accounts.v1.AuditService/QueryAuditLog" ] }, @@ -350,6 +353,7 @@ "/saas.accounts.v1.ModuleCapabilitiesService/NotifyUser", "/saas.accounts.v1.ModuleCapabilitiesService/PlaceRecord", "/saas.accounts.v1.ModuleCapabilitiesService/PublishEvent", + "/saas.accounts.v1.ModuleCapabilitiesService/ReadJournal", "/saas.accounts.v1.ModuleCapabilitiesService/ReplayEvents", "/saas.accounts.v1.ModuleCapabilitiesService/RequestApproval", "/saas.accounts.v1.ModuleCapabilitiesService/ResolveWorkContextAuthority", diff --git a/module/services/accounts/generated/authz-methods.json b/module/services/accounts/generated/authz-methods.json index 0fdfbb7d7..a38a0ce7a 100644 --- a/module/services/accounts/generated/authz-methods.json +++ b/module/services/accounts/generated/authz-methods.json @@ -1318,6 +1318,43 @@ "policy_sha256": "60a4fdc7035f800d001df484b53294b13256a4e27b7159360c3eac2be30117ce", "rate_limit_backend_failure_mode": "RATE_LIMIT_BACKEND_FAILURE_MODE_FAIL_OPEN" }, + { + "procedure": "/saas.accounts.v1.AuditService/DeliverAuditEvents", + "full_service": "saas.accounts.v1.AuditService", + "method": "DeliverAuditEvents", + "input_type": "saas.accounts.v1.DeliverAuditEventsRequest", + "output_type": "saas.accounts.v1.DeliverAuditEventsResponse", + "client_streaming": false, + "server_streaming": false, + "policy": { + "exposure": "EXPOSURE_AUTHENTICATED", + "tenant": "TENANT_REQUIREMENT_NONE", + "permissions": [], + "scopes": [], + "resource_bindings": [], + "mfa": "MFA_REQUIREMENT_NONE", + "audit": { + "events": [], + "emission": "AUDIT_EMISSION_NONE" + }, + "idempotency": "IDEMPOTENCY_REQUIREMENT_REQUIRED", + "rate_limit": "RATE_LIMIT_CLASS_STANDARD_WRITE", + "request_sensitivity": "SENSITIVITY_INTERNAL", + "response_sensitivity": "SENSITIVITY_INTERNAL", + "platform_role": "PLATFORM_ROLE_REQUIREMENT_NONE", + "authentication_factor_attempt": false, + "conditions": [], + "impersonation": "IMPERSONATION_REQUIREMENT_UNSPECIFIED" + }, + "policy_tier": "auth", + "owner": { + "module": "saas-starter", + "service": "accounts" + }, + "source_proto": "saas/accounts/v1/audit.proto", + "policy_sha256": "4989ebca609c382f966b4ee6878377005329a24c5778f879b61f7b1ccfc1a6bc", + "rate_limit_backend_failure_mode": "RATE_LIMIT_BACKEND_FAILURE_MODE_FAIL_OPEN" + }, { "procedure": "/saas.accounts.v1.AuditService/ExportAuditLog", "full_service": "saas.accounts.v1.AuditService", @@ -1406,6 +1443,43 @@ "policy_sha256": "9af740c1a0dd2124a43732af02a589927b5a3997f0d5cd474905341301684c3d", "rate_limit_backend_failure_mode": "RATE_LIMIT_BACKEND_FAILURE_MODE_FAIL_OPEN" }, + { + "procedure": "/saas.accounts.v1.AuditService/LookupDeliverAuditEvents", + "full_service": "saas.accounts.v1.AuditService", + "method": "LookupDeliverAuditEvents", + "input_type": "saas.accounts.v1.DeliverAuditEventsRequest", + "output_type": "saas.accounts.v1.DeliverAuditEventsResponse", + "client_streaming": false, + "server_streaming": false, + "policy": { + "exposure": "EXPOSURE_AUTHENTICATED", + "tenant": "TENANT_REQUIREMENT_NONE", + "permissions": [], + "scopes": [], + "resource_bindings": [], + "mfa": "MFA_REQUIREMENT_NONE", + "audit": { + "events": [], + "emission": "AUDIT_EMISSION_NONE" + }, + "idempotency": "IDEMPOTENCY_REQUIREMENT_FORBIDDEN", + "rate_limit": "RATE_LIMIT_CLASS_STANDARD_READ", + "request_sensitivity": "SENSITIVITY_INTERNAL", + "response_sensitivity": "SENSITIVITY_INTERNAL", + "platform_role": "PLATFORM_ROLE_REQUIREMENT_NONE", + "authentication_factor_attempt": false, + "conditions": [], + "impersonation": "IMPERSONATION_REQUIREMENT_UNSPECIFIED" + }, + "policy_tier": "auth", + "owner": { + "module": "saas-starter", + "service": "accounts" + }, + "source_proto": "saas/accounts/v1/audit.proto", + "policy_sha256": "cf793418b9894306b7c9ae232675b854a3077cc2f7b56777c682bc83d2127d0e", + "rate_limit_backend_failure_mode": "RATE_LIMIT_BACKEND_FAILURE_MODE_FAIL_OPEN" + }, { "procedure": "/saas.accounts.v1.AuditService/QueryAuditLog", "full_service": "saas.accounts.v1.AuditService", @@ -6662,6 +6736,43 @@ "policy_sha256": "45df71bfa7f313b565b0e8c314557847bab913990d43ba59b97d755412b4a4fd", "rate_limit_backend_failure_mode": "RATE_LIMIT_BACKEND_FAILURE_MODE_FAIL_OPEN" }, + { + "procedure": "/saas.accounts.v1.ModuleCapabilitiesService/ReadJournal", + "full_service": "saas.accounts.v1.ModuleCapabilitiesService", + "method": "ReadJournal", + "input_type": "saas.accounts.v1.ModuleReadJournalRequest", + "output_type": "saas.accounts.v1.ModuleReadJournalResponse", + "client_streaming": false, + "server_streaming": false, + "policy": { + "exposure": "EXPOSURE_INTERNAL", + "tenant": "TENANT_REQUIREMENT_NONE", + "permissions": [], + "scopes": [], + "resource_bindings": [], + "mfa": "MFA_REQUIREMENT_NONE", + "audit": { + "events": [], + "emission": "AUDIT_EMISSION_NONE" + }, + "idempotency": "IDEMPOTENCY_REQUIREMENT_FORBIDDEN", + "rate_limit": "RATE_LIMIT_CLASS_INTERNAL", + "request_sensitivity": "SENSITIVITY_CONFIDENTIAL", + "response_sensitivity": "SENSITIVITY_CONFIDENTIAL", + "platform_role": "PLATFORM_ROLE_REQUIREMENT_NONE", + "authentication_factor_attempt": false, + "conditions": [], + "impersonation": "IMPERSONATION_REQUIREMENT_UNSPECIFIED" + }, + "policy_tier": "internal", + "owner": { + "module": "saas-starter", + "service": "accounts" + }, + "source_proto": "saas/accounts/v1/module_capabilities.proto", + "policy_sha256": "45df71bfa7f313b565b0e8c314557847bab913990d43ba59b97d755412b4a4fd", + "rate_limit_backend_failure_mode": "RATE_LIMIT_BACKEND_FAILURE_MODE_FAIL_OPEN" + }, { "procedure": "/saas.accounts.v1.ModuleCapabilitiesService/ReplayEvents", "full_service": "saas.accounts.v1.ModuleCapabilitiesService", diff --git a/module/services/accounts/generated/gateway-routes.json b/module/services/accounts/generated/gateway-routes.json index faa25c69e..fd0e81309 100644 --- a/module/services/accounts/generated/gateway-routes.json +++ b/module/services/accounts/generated/gateway-routes.json @@ -113,6 +113,22 @@ "rewrite_path": "/saas.accounts.v1.AuditService/AggregateAuditLog", "remove_after": "2026-10-11" }, + { + "protocol": "GATEWAY_PROTOCOL_CONNECT", + "method": "POST", + "path": "/customers.AuditService/DeliverAuditEvents", + "match": "GATEWAY_MATCH_EXACT", + "procedure": "/saas.accounts.v1.AuditService/DeliverAuditEvents", + "owner": { + "module": "saas-starter", + "service": "accounts" + }, + "upstream_endpoint": "connect", + "exposure": "EXPOSURE_AUTHENTICATED", + "source": "GATEWAY_ROUTE_SOURCE_COMPATIBILITY_ALIAS", + "rewrite_path": "/saas.accounts.v1.AuditService/DeliverAuditEvents", + "remove_after": "2026-10-11" + }, { "protocol": "GATEWAY_PROTOCOL_CONNECT", "method": "POST", @@ -145,6 +161,22 @@ "rewrite_path": "/saas.accounts.v1.AuditService/ListAuditEventTypes", "remove_after": "2026-10-11" }, + { + "protocol": "GATEWAY_PROTOCOL_CONNECT", + "method": "POST", + "path": "/customers.AuditService/LookupDeliverAuditEvents", + "match": "GATEWAY_MATCH_EXACT", + "procedure": "/saas.accounts.v1.AuditService/LookupDeliverAuditEvents", + "owner": { + "module": "saas-starter", + "service": "accounts" + }, + "upstream_endpoint": "connect", + "exposure": "EXPOSURE_AUTHENTICATED", + "source": "GATEWAY_ROUTE_SOURCE_COMPATIBILITY_ALIAS", + "rewrite_path": "/saas.accounts.v1.AuditService/LookupDeliverAuditEvents", + "remove_after": "2026-10-11" + }, { "protocol": "GATEWAY_PROTOCOL_CONNECT", "method": "POST", @@ -3889,6 +3921,22 @@ "rewrite_path": "", "remove_after": "" }, + { + "protocol": "GATEWAY_PROTOCOL_CONNECT", + "method": "POST", + "path": "/saas.accounts.v1.AuditService/DeliverAuditEvents", + "match": "GATEWAY_MATCH_EXACT", + "procedure": "/saas.accounts.v1.AuditService/DeliverAuditEvents", + "owner": { + "module": "saas-starter", + "service": "accounts" + }, + "upstream_endpoint": "connect", + "exposure": "EXPOSURE_AUTHENTICATED", + "source": "GATEWAY_ROUTE_SOURCE_DESCRIPTOR", + "rewrite_path": "", + "remove_after": "" + }, { "protocol": "GATEWAY_PROTOCOL_CONNECT", "method": "POST", @@ -3921,6 +3969,22 @@ "rewrite_path": "", "remove_after": "" }, + { + "protocol": "GATEWAY_PROTOCOL_CONNECT", + "method": "POST", + "path": "/saas.accounts.v1.AuditService/LookupDeliverAuditEvents", + "match": "GATEWAY_MATCH_EXACT", + "procedure": "/saas.accounts.v1.AuditService/LookupDeliverAuditEvents", + "owner": { + "module": "saas-starter", + "service": "accounts" + }, + "upstream_endpoint": "connect", + "exposure": "EXPOSURE_AUTHENTICATED", + "source": "GATEWAY_ROUTE_SOURCE_DESCRIPTOR", + "rewrite_path": "", + "remove_after": "" + }, { "protocol": "GATEWAY_PROTOCOL_CONNECT", "method": "POST", diff --git a/module/services/accounts/generated/openapi-raw/api.swagger.json b/module/services/accounts/generated/openapi-raw/api.swagger.json index 64cd4108a..1008acfb0 100644 --- a/module/services/accounts/generated/openapi-raw/api.swagger.json +++ b/module/services/accounts/generated/openapi-raw/api.swagger.json @@ -7484,7 +7484,7 @@ "percentile": { "type": "number", "format": "double", - "description": "percentile ∈ (0,1], used only when op == percentile (0.95 → p95)." + "description": "percentile ∈ (0,1], used only when op == percentile (0.95 → p95). Only the\nPostgres audit backend computes it: the warehouse backend keeps entries\nat-least-once and deduplicates them on read, and a percentile over that is not\nan exact answer, so a deployment on the warehouse backend refuses a query that\nasks for one with InvalidArgument. Every other op is served by both backends." }, "alias": { "type": "string", diff --git a/module/services/accounts/generated/service-catalog.json b/module/services/accounts/generated/service-catalog.json index f3faf12e5..69d39e2b5 100644 --- a/module/services/accounts/generated/service-catalog.json +++ b/module/services/accounts/generated/service-catalog.json @@ -92,8 +92,10 @@ "full_name": "saas.accounts.v1.AuditService", "procedures": [ "/saas.accounts.v1.AuditService/AggregateAuditLog", + "/saas.accounts.v1.AuditService/DeliverAuditEvents", "/saas.accounts.v1.AuditService/ExportAuditLog", "/saas.accounts.v1.AuditService/ListAuditEventTypes", + "/saas.accounts.v1.AuditService/LookupDeliverAuditEvents", "/saas.accounts.v1.AuditService/QueryAuditLog" ] }, @@ -310,6 +312,7 @@ "/saas.accounts.v1.ModuleCapabilitiesService/NotifyUser", "/saas.accounts.v1.ModuleCapabilitiesService/PlaceRecord", "/saas.accounts.v1.ModuleCapabilitiesService/PublishEvent", + "/saas.accounts.v1.ModuleCapabilitiesService/ReadJournal", "/saas.accounts.v1.ModuleCapabilitiesService/ReplayEvents", "/saas.accounts.v1.ModuleCapabilitiesService/RequestApproval", "/saas.accounts.v1.ModuleCapabilitiesService/ResolveWorkContextAuthority", @@ -2158,6 +2161,49 @@ "service": "accounts" } }, + { + "procedure": "/saas.accounts.v1.AuditService/DeliverAuditEvents", + "service": "AuditService", + "full_service": "saas.accounts.v1.AuditService", + "method": "DeliverAuditEvents", + "input_type": "saas.accounts.v1.DeliverAuditEventsRequest", + "output_type": "saas.accounts.v1.DeliverAuditEventsResponse", + "client_streaming": false, + "server_streaming": false, + "deprecated": false, + "source_proto": "saas/accounts/v1/audit.proto", + "protocols": [ + "PROTOCOL_GRPC", + "PROTOCOL_CONNECT" + ], + "http_bindings": [], + "policy": { + "exposure": "EXPOSURE_AUTHENTICATED", + "tenant": "TENANT_REQUIREMENT_NONE", + "permissions": [], + "scopes": [], + "resource_bindings": [], + "mfa": "MFA_REQUIREMENT_NONE", + "audit": { + "events": [], + "emission": "AUDIT_EMISSION_NONE" + }, + "idempotency": "IDEMPOTENCY_REQUIREMENT_REQUIRED", + "rate_limit": "RATE_LIMIT_CLASS_STANDARD_WRITE", + "request_sensitivity": "SENSITIVITY_INTERNAL", + "response_sensitivity": "SENSITIVITY_INTERNAL", + "platform_role": "PLATFORM_ROLE_REQUIREMENT_NONE", + "authentication_factor_attempt": false, + "conditions": [], + "impersonation": "IMPERSONATION_REQUIREMENT_UNSPECIFIED" + }, + "policy_tier": "auth", + "description": "Deliver one final audit journal range under the installed module's delivery authority, committing a receipt.", + "owner": { + "module": "saas-starter", + "service": "accounts" + } + }, { "procedure": "/saas.accounts.v1.AuditService/ExportAuditLog", "service": "AuditService", @@ -2274,6 +2320,49 @@ "service": "accounts" } }, + { + "procedure": "/saas.accounts.v1.AuditService/LookupDeliverAuditEvents", + "service": "AuditService", + "full_service": "saas.accounts.v1.AuditService", + "method": "LookupDeliverAuditEvents", + "input_type": "saas.accounts.v1.DeliverAuditEventsRequest", + "output_type": "saas.accounts.v1.DeliverAuditEventsResponse", + "client_streaming": false, + "server_streaming": false, + "deprecated": false, + "source_proto": "saas/accounts/v1/audit.proto", + "protocols": [ + "PROTOCOL_GRPC", + "PROTOCOL_CONNECT" + ], + "http_bindings": [], + "policy": { + "exposure": "EXPOSURE_AUTHENTICATED", + "tenant": "TENANT_REQUIREMENT_NONE", + "permissions": [], + "scopes": [], + "resource_bindings": [], + "mfa": "MFA_REQUIREMENT_NONE", + "audit": { + "events": [], + "emission": "AUDIT_EMISSION_NONE" + }, + "idempotency": "IDEMPOTENCY_REQUIREMENT_FORBIDDEN", + "rate_limit": "RATE_LIMIT_CLASS_STANDARD_READ", + "request_sensitivity": "SENSITIVITY_INTERNAL", + "response_sensitivity": "SENSITIVITY_INTERNAL", + "platform_role": "PLATFORM_ROLE_REQUIREMENT_NONE", + "authentication_factor_attempt": false, + "conditions": [], + "impersonation": "IMPERSONATION_REQUIREMENT_UNSPECIFIED" + }, + "policy_tier": "auth", + "description": "Read the receipt of one audit delivery without appending any entries.", + "owner": { + "module": "saas-starter", + "service": "accounts" + } + }, { "procedure": "/saas.accounts.v1.AuditService/QueryAuditLog", "service": "AuditService", @@ -8758,6 +8847,49 @@ "service": "accounts" } }, + { + "procedure": "/saas.accounts.v1.ModuleCapabilitiesService/ReadJournal", + "service": "ModuleCapabilitiesService", + "full_service": "saas.accounts.v1.ModuleCapabilitiesService", + "method": "ReadJournal", + "input_type": "saas.accounts.v1.ModuleReadJournalRequest", + "output_type": "saas.accounts.v1.ModuleReadJournalResponse", + "client_streaming": false, + "server_streaming": false, + "deprecated": false, + "source_proto": "saas/accounts/v1/module_capabilities.proto", + "protocols": [ + "PROTOCOL_GRPC", + "PROTOCOL_CONNECT" + ], + "http_bindings": [], + "policy": { + "exposure": "EXPOSURE_INTERNAL", + "tenant": "TENANT_REQUIREMENT_NONE", + "permissions": [], + "scopes": [], + "resource_bindings": [], + "mfa": "MFA_REQUIREMENT_NONE", + "audit": { + "events": [], + "emission": "AUDIT_EMISSION_NONE" + }, + "idempotency": "IDEMPOTENCY_REQUIREMENT_FORBIDDEN", + "rate_limit": "RATE_LIMIT_CLASS_INTERNAL", + "request_sensitivity": "SENSITIVITY_CONFIDENTIAL", + "response_sensitivity": "SENSITIVITY_CONFIDENTIAL", + "platform_role": "PLATFORM_ROLE_REQUIREMENT_NONE", + "authentication_factor_attempt": false, + "conditions": [], + "impersonation": "IMPERSONATION_REQUIREMENT_UNSPECIFIED" + }, + "policy_tier": "internal", + "description": "Read a page of the cross-tenant journal under a cross-tenant module grant, without enqueueing work.", + "owner": { + "module": "saas-starter", + "service": "accounts" + } + }, { "procedure": "/saas.accounts.v1.ModuleCapabilitiesService/ReplayEvents", "service": "ModuleCapabilitiesService", diff --git a/module/services/accounts/proto/saas/accounts/v1/audit.proto b/module/services/accounts/proto/saas/accounts/v1/audit.proto index ee8451f18..e0fbd1e87 100644 --- a/module/services/accounts/proto/saas/accounts/v1/audit.proto +++ b/module/services/accounts/proto/saas/accounts/v1/audit.proto @@ -6,6 +6,7 @@ import "buf/validate/validate.proto"; import "google/api/annotations.proto"; import "google/protobuf/struct.proto"; import "google/protobuf/timestamp.proto"; +import "codefly/runnable/v0/options.proto"; import "saas/policy/v1/options.proto"; // ============================================================================ @@ -115,7 +116,11 @@ message AuditMetric { // percentile) require a "payload:" field; non-numeric payload values are // ignored. string field = 2; - // percentile ∈ (0,1], used only when op == percentile (0.95 → p95). + // percentile ∈ (0,1], used only when op == percentile (0.95 → p95). Only the + // Postgres audit backend computes it: the warehouse backend keeps entries + // at-least-once and deduplicates them on read, and a percentile over that is not + // an exact answer, so a deployment on the warehouse backend refuses a query that + // asks for one with InvalidArgument. Every other op is served by both backends. double percentile = 3; // alias names this metric in the response map. Defaults to "count" for count, // else "_". @@ -238,6 +243,43 @@ message ListAuditEventTypesResponse { repeated AuditEventType types = 1; } +// DeliverAuditEventsRequest names one range of the platform journal, (from, to], +// whose audit records the operation appends to the warehouse audit storage. Both +// ends are positions a ModuleReadJournal page returned; from may instead be the +// literal "start", meaning before the oldest retained entry, and is never empty so +// that the first range of a fresh journal has a non-empty value to correlate on. +// The range ends at a position that is final (below the oldest transaction still +// running) and holds at most 500 audit records, or it is refused before anything +// is appended. The effect id is "saas.audit.recorded.v1/", derived by the +// host: one range, one receipt. A runtime task may carry its own transport +// effect identity; the range receipt is independent of that task identity. +message DeliverAuditEventsRequest { + string from = 1 [(buf.validate.field).string = { + min_len: 1 + max_len: 64 + }]; + string to = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 64 + }]; +} + +message DeliverAuditEventsResponse { + string effect_id = 1; + // status is "committed" when the range is recorded. An absent lookup + // returns NOT_FOUND: absence is inconclusive, never a committed response. + string status = 2; + // delivered is the number of audit records of the range appended to the + // storage; a range with none still commits a receipt. + int32 delivered = 3; + // The exact committed interval is (from, to]. Retention verifies these + // positions against the receipt's effect identity and request digest before + // retiring journal rows. Legacy receipts without both positions prove no + // coverage; delivered alone never proves an interval. + string from = 4; + string to = 5; +} + // AuditService — append-only audit event log service AuditService { rpc QueryAuditLog(QueryAuditLogRequest) returns (QueryAuditLogResponse) { @@ -322,4 +364,68 @@ service AuditService { body: "*" }; } + + // DeliverAuditEvents appends one range of the journal's audit records to the + // warehouse audit storage: the operation module-runtime calls after reading the + // range through ReadJournal. It is an operation, not a sweep. It takes each + // record's organization from the journal row and never from the caller, emits no + // audit of its own, and commits one receipt per range under the installation's + // tenant; delivery is at least once, and every read of the storage returns each + // entry once by id. Its receipt is read back through LookupDeliverAuditEvents. + rpc DeliverAuditEvents(DeliverAuditEventsRequest) returns (DeliverAuditEventsResponse) { + option (saas.policy.v1.method_policy) = { + exposure: EXPOSURE_AUTHENTICATED + // Headless module principals are authorized by their live grant and + // saas.audit-delivery scope, not by a human organization membership. + tenant: TENANT_REQUIREMENT_NONE + mfa: MFA_REQUIREMENT_NONE + audit: {emission: AUDIT_EMISSION_NONE} + idempotency: IDEMPOTENCY_REQUIREMENT_REQUIRED + rate_limit: RATE_LIMIT_CLASS_STANDARD_WRITE + request_sensitivity: SENSITIVITY_INTERNAL + response_sensitivity: SENSITIVITY_INTERNAL + platform_role: PLATFORM_ROLE_REQUIREMENT_NONE + }; + option (codefly.runnable.v0.operation) = { + attempt_timeout: {seconds: 60} + total_timeout: {seconds: 600} + max_attempts: 3 + backoff: {seconds: 5} + retryable_codes: [ + "UNAVAILABLE", + "RESOURCE_EXHAUSTED", + "ABORTED" + ] + audience: "saas-audit" + required_scope_slots: { + name: "audit_delivery" + required_actions: [ + "invoke", + "read" + ] + lookup: true + } + lookup_method: "/saas.accounts.v1.AuditService/LookupDeliverAuditEvents" + max_input_bytes: 256 + max_output_bytes: 1024 + completion: COMPLETION_CALL + }; + } + // LookupDeliverAuditEvents takes the invocation's same {from, to} body and + // derives the range receipt identity from from. It never delivers. + rpc LookupDeliverAuditEvents(DeliverAuditEventsRequest) returns (DeliverAuditEventsResponse) { + option (saas.policy.v1.method_policy) = { + exposure: EXPOSURE_AUTHENTICATED + // Headless module principals are authorized by their live grant and + // saas.audit-delivery scope, not by a human organization membership. + tenant: TENANT_REQUIREMENT_NONE + mfa: MFA_REQUIREMENT_NONE + audit: {emission: AUDIT_EMISSION_NONE} + idempotency: IDEMPOTENCY_REQUIREMENT_FORBIDDEN + rate_limit: RATE_LIMIT_CLASS_STANDARD_READ + request_sensitivity: SENSITIVITY_INTERNAL + response_sensitivity: SENSITIVITY_INTERNAL + platform_role: PLATFORM_ROLE_REQUIREMENT_NONE + }; + } } diff --git a/module/services/accounts/proto/saas/accounts/v1/module_capabilities.proto b/module/services/accounts/proto/saas/accounts/v1/module_capabilities.proto index 1eb2b358e..07ad13bfc 100644 --- a/module/services/accounts/proto/saas/accounts/v1/module_capabilities.proto +++ b/module/services/accounts/proto/saas/accounts/v1/module_capabilities.proto @@ -355,6 +355,69 @@ message ModuleEmitAuditEventRequest { string idempotency_key = 7 [(buf.validate.field).string.max_len = 255]; } +// ModuleReadJournalRequest reads the platform journal across every tenant. Only a +// module principal whose grant is cross_tenant may call it, the gate a +// system-scoped EmitAuditEvent has; any other caller is PermissionDenied. +message ModuleReadJournalRequest { + // after is the position the previous page ended at (a page's next_after). The + // literal "start" is a valid position meaning "before the oldest retained + // entry", and the empty string is accepted as an alias of it; so a reader with + // nothing read yet has a non-empty position to correlate its first page on. + // Positions are opaque: store one and hand it back unread. No entry's position + // is ever "start". A value this host did not issue is InvalidArgument. + string after = 1 [(buf.validate.field).string.max_len = 64]; + // limit is the page size: 0 is 100, and more than 500 is served as 500. A + // page is also bounded to 2 MiB of event data, though the first entry is always + // served. + int32 limit = 2 [(buf.validate.field).int32.gte = 0]; + // types are the event types to read, exactly (no pattern). At least one is + // required, so no caller reads every tenant's every event by leaving the + // filter out. + repeated string types = 3 [(buf.validate.field).repeated = { + min_items: 1 + max_items: 16 + items: {string: {min_len: 1, max_len: 256}} + }]; + // delivery_bindings are the binding ids the caller's own delivery of the journal + // runs under. They decide which entries are marked delivery_machinery. + repeated string delivery_bindings = 4 [(buf.validate.field).repeated = { + max_items: 64 + items: {string: {min_len: 1, max_len: 256}} + }]; +} + +// ModuleJournalEntry is one journal row. Rows are served in the order their +// writing transactions are final (see ModuleReadJournalRequest), so a reader that +// resumes from the last position it was given skips nothing. +message ModuleJournalEntry { + // position is where the row sits in the read order: opaque, and the value a + // reader passes as after to resume past it. + string position = 1; + string event_id = 2; + string type = 3; + // organization_id is the row's tenant, empty for a row that has none. + string organization_id = 4; + google.protobuf.Timestamp occurred_at = 5; + // data is the event's JSON object; empty when the event's data is not one. + google.protobuf.Struct data = 6; + // delivery_machinery is true when the entry the row carries is the footprint of + // the caller's own delivery of the journal: the host computes it, so a caller + // never parses this module's payloads. It is true for a + // saas.module.operation_context_minted or + // saas.module.delegated_audience_exchange entry whose binding_id is one of the + // request's delivery_bindings, and for a saas.module.work_context_minted entry + // of the caller's own module principal. + bool delivery_machinery = 7; +} + +// ModuleReadJournalResponse is one page and where to resume. +message ModuleReadJournalResponse { + repeated ModuleJournalEntry entries = 1; + // next_after is the last entry's position, or the request's after when the page + // is empty. + string next_after = 2; +} + // ModuleAuditFieldKind is the kind of one declared audit payload field: the // same kinds a solution may declare in its manifest. enum ModuleAuditFieldKind { @@ -1656,6 +1719,23 @@ service ModuleCapabilitiesService { platform_role: PLATFORM_ROLE_REQUIREMENT_NONE }; } + // ReadJournal serves the platform journal across every tenant, and the rows with + // no tenant, to a module principal whose grant is cross_tenant. Rows are served + // in the order their writing transactions are final, so a reader that resumes + // from next_after skips nothing; it enqueues nothing and is not a replay. + rpc ReadJournal(ModuleReadJournalRequest) returns (ModuleReadJournalResponse) { + option (saas.policy.v1.method_policy) = { + exposure: EXPOSURE_INTERNAL + tenant: TENANT_REQUIREMENT_NONE + mfa: MFA_REQUIREMENT_NONE + audit: {emission: AUDIT_EMISSION_NONE} + idempotency: IDEMPOTENCY_REQUIREMENT_FORBIDDEN + rate_limit: RATE_LIMIT_CLASS_INTERNAL + request_sensitivity: SENSITIVITY_CONFIDENTIAL + response_sensitivity: SENSITIVITY_CONFIDENTIAL + platform_role: PLATFORM_ROLE_REQUIREMENT_NONE + }; + } // DeclareAuditEventTypes admits the audit event types a composed module owns, // into the namespaces the operator bound to it; see the request. rpc DeclareAuditEventTypes(ModuleDeclareAuditEventTypesRequest) returns (ModuleDeclareAuditEventTypesResponse) { diff --git a/module/services/accounts/service.codefly.yaml b/module/services/accounts/service.codefly.yaml index b8376764f..b18103fe2 100644 --- a/module/services/accounts/service.codefly.yaml +++ b/module/services/accounts/service.codefly.yaml @@ -34,6 +34,7 @@ workspace-configuration-dependencies: - abuse-protection - application - artifact-cache + - audit-storage - billing - datasource-keys - datasource-oauth diff --git a/module/services/auth-gateway/code/authz_catalog_gen.go b/module/services/auth-gateway/code/authz_catalog_gen.go index d7971ce6e..fb059db66 100644 --- a/module/services/auth-gateway/code/authz_catalog_gen.go +++ b/module/services/auth-gateway/code/authz_catalog_gen.go @@ -44,8 +44,10 @@ var generatedAuthorizationByProcedure = map[string]generatedAuthorizationMetadat "/saas.accounts.v1.ApprovalReviewService/DecideApprovalReview": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassStandardWrite, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "d6e8c91358de5158f879cda16b620e5a8aa1f71b252e8e8878234599797ed914"}, "/saas.accounts.v1.ApprovalReviewService/GetApprovalReview": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassStandardRead, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "ad07a0509490d86d9bb64cb28e5db5ef578575d2cbab8ee86d279ffe1392cca8"}, "/saas.accounts.v1.AuditService/AggregateAuditLog": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassStandardRead, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "60a4fdc7035f800d001df484b53294b13256a4e27b7159360c3eac2be30117ce"}, + "/saas.accounts.v1.AuditService/DeliverAuditEvents": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassStandardWrite, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "4989ebca609c382f966b4ee6878377005329a24c5778f879b61f7b1ccfc1a6bc"}, "/saas.accounts.v1.AuditService/ExportAuditLog": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassStandardRead, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "468012453cddebdd9969ce4dd7c962e055b36743f5506c8e75f4749a4440800b"}, "/saas.accounts.v1.AuditService/ListAuditEventTypes": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassStandardRead, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "9af740c1a0dd2124a43732af02a589927b5a3997f0d5cd474905341301684c3d"}, + "/saas.accounts.v1.AuditService/LookupDeliverAuditEvents": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassStandardRead, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "cf793418b9894306b7c9ae232675b854a3077cc2f7b56777c682bc83d2127d0e"}, "/saas.accounts.v1.AuditService/QueryAuditLog": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassStandardRead, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "60a4fdc7035f800d001df484b53294b13256a4e27b7159360c3eac2be30117ce"}, "/saas.accounts.v1.AuthService/Authenticate": {exposure: edgeExposurePublic, rateLimitClass: edgeRateLimitClassAuthentication, rateLimitBackendFailClosed: true, authenticationFactorAttempt: false, policySHA256: "e2e8ecbc2db47561ade3aeec694fe4e7ac3d5cb5ca9876f5c66698ab321dc6fd"}, "/saas.accounts.v1.AuthService/BeginOAuth": {exposure: edgeExposurePublic, rateLimitClass: edgeRateLimitClassAuthentication, rateLimitBackendFailClosed: true, authenticationFactorAttempt: false, policySHA256: "41dd7beadd8c050c3b2e14ca75ec22d630bca83e9578b728aa784de8283e0b3a"}, @@ -172,6 +174,7 @@ var generatedAuthorizationByProcedure = map[string]generatedAuthorizationMetadat "/saas.accounts.v1.ModuleCapabilitiesService/NotifyUser": {exposure: edgeExposureInternal, rateLimitClass: edgeRateLimitClassInternal, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "45df71bfa7f313b565b0e8c314557847bab913990d43ba59b97d755412b4a4fd"}, "/saas.accounts.v1.ModuleCapabilitiesService/PlaceRecord": {exposure: edgeExposureInternal, rateLimitClass: edgeRateLimitClassInternal, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "45df71bfa7f313b565b0e8c314557847bab913990d43ba59b97d755412b4a4fd"}, "/saas.accounts.v1.ModuleCapabilitiesService/PublishEvent": {exposure: edgeExposureInternal, rateLimitClass: edgeRateLimitClassInternal, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "45df71bfa7f313b565b0e8c314557847bab913990d43ba59b97d755412b4a4fd"}, + "/saas.accounts.v1.ModuleCapabilitiesService/ReadJournal": {exposure: edgeExposureInternal, rateLimitClass: edgeRateLimitClassInternal, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "45df71bfa7f313b565b0e8c314557847bab913990d43ba59b97d755412b4a4fd"}, "/saas.accounts.v1.ModuleCapabilitiesService/ReplayEvents": {exposure: edgeExposureInternal, rateLimitClass: edgeRateLimitClassInternal, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "45df71bfa7f313b565b0e8c314557847bab913990d43ba59b97d755412b4a4fd"}, "/saas.accounts.v1.ModuleCapabilitiesService/RequestApproval": {exposure: edgeExposureInternal, rateLimitClass: edgeRateLimitClassInternal, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "45df71bfa7f313b565b0e8c314557847bab913990d43ba59b97d755412b4a4fd"}, "/saas.accounts.v1.ModuleCapabilitiesService/ResolveWorkContextAuthority": {exposure: edgeExposureInternal, rateLimitClass: edgeRateLimitClassInternal, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "45df71bfa7f313b565b0e8c314557847bab913990d43ba59b97d755412b4a4fd"}, diff --git a/module/services/auth-gateway/code/routing_catalog_gen.go b/module/services/auth-gateway/code/routing_catalog_gen.go index 7d41dc208..0fb854439 100644 --- a/module/services/auth-gateway/code/routing_catalog_gen.go +++ b/module/services/auth-gateway/code/routing_catalog_gen.go @@ -13,8 +13,10 @@ func generatedCatalogConnectRoutes() []*RouteEntry { {Service: "accounts_connect", Method: "POST", Path: "/customers.ApprovalReviewService/DecideApprovalReview", UpstreamPath: "/saas.accounts.v1.ApprovalReviewService/DecideApprovalReview", Procedure: "/saas.accounts.v1.ApprovalReviewService/DecideApprovalReview"}, {Service: "accounts_connect", Method: "POST", Path: "/customers.ApprovalReviewService/GetApprovalReview", UpstreamPath: "/saas.accounts.v1.ApprovalReviewService/GetApprovalReview", Procedure: "/saas.accounts.v1.ApprovalReviewService/GetApprovalReview"}, {Service: "accounts_connect", Method: "POST", Path: "/customers.AuditService/AggregateAuditLog", UpstreamPath: "/saas.accounts.v1.AuditService/AggregateAuditLog", Procedure: "/saas.accounts.v1.AuditService/AggregateAuditLog"}, + {Service: "accounts_connect", Method: "POST", Path: "/customers.AuditService/DeliverAuditEvents", UpstreamPath: "/saas.accounts.v1.AuditService/DeliverAuditEvents", Procedure: "/saas.accounts.v1.AuditService/DeliverAuditEvents"}, {Service: "accounts_connect", Method: "POST", Path: "/customers.AuditService/ExportAuditLog", UpstreamPath: "/saas.accounts.v1.AuditService/ExportAuditLog", Procedure: "/saas.accounts.v1.AuditService/ExportAuditLog"}, {Service: "accounts_connect", Method: "POST", Path: "/customers.AuditService/ListAuditEventTypes", UpstreamPath: "/saas.accounts.v1.AuditService/ListAuditEventTypes", Procedure: "/saas.accounts.v1.AuditService/ListAuditEventTypes"}, + {Service: "accounts_connect", Method: "POST", Path: "/customers.AuditService/LookupDeliverAuditEvents", UpstreamPath: "/saas.accounts.v1.AuditService/LookupDeliverAuditEvents", Procedure: "/saas.accounts.v1.AuditService/LookupDeliverAuditEvents"}, {Service: "accounts_connect", Method: "POST", Path: "/customers.AuditService/QueryAuditLog", UpstreamPath: "/saas.accounts.v1.AuditService/QueryAuditLog", Procedure: "/saas.accounts.v1.AuditService/QueryAuditLog"}, {Service: "accounts_connect", Method: "POST", Path: "/customers.AuthService/Authenticate", UpstreamPath: "/saas.accounts.v1.AuthService/Authenticate", Procedure: "/saas.accounts.v1.AuthService/Authenticate"}, {Service: "accounts_connect", Method: "POST", Path: "/customers.AuthService/BeginOAuth", UpstreamPath: "/saas.accounts.v1.AuthService/BeginOAuth", Procedure: "/saas.accounts.v1.AuthService/BeginOAuth"}, @@ -249,8 +251,10 @@ func generatedCatalogConnectRoutes() []*RouteEntry { {Service: "accounts_connect", Method: "POST", Path: "/saas.accounts.v1.ApprovalReviewService/DecideApprovalReview", UpstreamPath: "", Procedure: "/saas.accounts.v1.ApprovalReviewService/DecideApprovalReview"}, {Service: "accounts_connect", Method: "POST", Path: "/saas.accounts.v1.ApprovalReviewService/GetApprovalReview", UpstreamPath: "", Procedure: "/saas.accounts.v1.ApprovalReviewService/GetApprovalReview"}, {Service: "accounts_connect", Method: "POST", Path: "/saas.accounts.v1.AuditService/AggregateAuditLog", UpstreamPath: "", Procedure: "/saas.accounts.v1.AuditService/AggregateAuditLog"}, + {Service: "accounts_connect", Method: "POST", Path: "/saas.accounts.v1.AuditService/DeliverAuditEvents", UpstreamPath: "", Procedure: "/saas.accounts.v1.AuditService/DeliverAuditEvents"}, {Service: "accounts_connect", Method: "POST", Path: "/saas.accounts.v1.AuditService/ExportAuditLog", UpstreamPath: "", Procedure: "/saas.accounts.v1.AuditService/ExportAuditLog"}, {Service: "accounts_connect", Method: "POST", Path: "/saas.accounts.v1.AuditService/ListAuditEventTypes", UpstreamPath: "", Procedure: "/saas.accounts.v1.AuditService/ListAuditEventTypes"}, + {Service: "accounts_connect", Method: "POST", Path: "/saas.accounts.v1.AuditService/LookupDeliverAuditEvents", UpstreamPath: "", Procedure: "/saas.accounts.v1.AuditService/LookupDeliverAuditEvents"}, {Service: "accounts_connect", Method: "POST", Path: "/saas.accounts.v1.AuditService/QueryAuditLog", UpstreamPath: "", Procedure: "/saas.accounts.v1.AuditService/QueryAuditLog"}, {Service: "accounts_connect", Method: "POST", Path: "/saas.accounts.v1.AuthService/Authenticate", UpstreamPath: "", Procedure: "/saas.accounts.v1.AuthService/Authenticate"}, {Service: "accounts_connect", Method: "POST", Path: "/saas.accounts.v1.AuthService/BeginOAuth", UpstreamPath: "", Procedure: "/saas.accounts.v1.AuthService/BeginOAuth"}, diff --git a/module/services/frontend/code/package-lock.json b/module/services/frontend/code/package-lock.json index f843f8fbc..0a8521f1a 100644 --- a/module/services/frontend/code/package-lock.json +++ b/module/services/frontend/code/package-lock.json @@ -15742,7 +15742,7 @@ }, "packages/saas-sdk": { "name": "@codefly-dev/saas-sdk", - "version": "0.3.40", + "version": "0.3.41", "license": "Apache-2.0", "dependencies": { "@bufbuild/protobuf": "^2.11.0", 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 68c3d388e..cc99ee680 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": "", "path": "contract.binpb", - "digest": "sha256:da3be80f4ed222b07d24e2b23d5eb797a36b1334fef560b24aeecf2850ac48d6", + "digest": "sha256:19a9a6803b93239a67538e6cf6111caa5136f982c44942ba21ac00bf7a05ce95", "services": [ { "name": "AccessibleScopeService", @@ -24,8 +24,10 @@ "fullName": "saas.accounts.v1.AuditService", "procedures": [ "/saas.accounts.v1.AuditService/AggregateAuditLog", + "/saas.accounts.v1.AuditService/DeliverAuditEvents", "/saas.accounts.v1.AuditService/ExportAuditLog", "/saas.accounts.v1.AuditService/ListAuditEventTypes", + "/saas.accounts.v1.AuditService/LookupDeliverAuditEvents", "/saas.accounts.v1.AuditService/QueryAuditLog" ] }, 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 4d1a16547775bf37a8db22c84eca85144126722d..885fcda8afa8bb9b2de60035539b5123b74bcdd0 100644 GIT binary patch delta 11420 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 YTyW5Xh|sOm=b8AE7@0mV`K#9d2R$@JPXGV_ delta 7148 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 N`~^cVUlzVu`ak!dz~cY_ 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 5cb12c298..49cbfc2cb 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:743b63bad9a546f105e994c754d845865e189465e6c72816e5785efe38ab7831 + contract-digest: sha256:12c6216596c7460c71bf83e3962a3724c19bd79401459088a51f7f3b3fc1e3d9 services: - AccessibleScopeService - AuditService diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/audit_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/audit_pb.ts index 5284c29c4..8c347e2ce 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/audit_pb.ts +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/audit_pb.ts @@ -8,6 +8,7 @@ import { file_buf_validate_validate } from "../../../buf/validate/validate_pb"; import { file_google_api_annotations } from "../../../google/api/annotations_pb"; import type { Timestamp } from "@bufbuild/protobuf/wkt"; import { file_google_protobuf_struct, file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; +import { file_codefly_runnable_v0_options } from "../../../codefly/runnable/v0/options_pb"; import { file_saas_policy_v1_options } from "../../policy/v1/options_pb"; import type { JsonObject, Message } from "@bufbuild/protobuf"; @@ -15,7 +16,7 @@ import type { JsonObject, Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/audit.proto. */ export const file_saas_accounts_v1_audit: GenFile = /*@__PURE__*/ - fileDesc("ChxzYWFzL2FjY291bnRzL3YxL2F1ZGl0LnByb3RvEhBzYWFzLmFjY291bnRzLnYxIrsDCgpBdWRpdEV2ZW50EgoKAmlkGAEgASgJEhAKCGFjdG9yX2lkGAIgASgJEhIKCmFjdG9yX3R5cGUYAyABKAkSEgoGYWN0aW9uGAQgASgJQgIYARIQCghyZXNvdXJjZRgFIAEoCRITCgtyZXNvdXJjZV9pZBgGIAEoCRIOCgZvcmdfaWQYByABKAkSQAoIbWV0YWRhdGEYCCADKAsyKi5zYWFzLmFjY291bnRzLnYxLkF1ZGl0RXZlbnQuTWV0YWRhdGFFbnRyeUICGAESEgoKaXBfYWRkcmVzcxgJIAEoCRIuCgpjcmVhdGVkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBISCgpldmVudF90eXBlGAsgASgJEhYKDnNjaGVtYV92ZXJzaW9uGAwgASgFEigKB3BheWxvYWQYDSABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EhAKCGNhdGVnb3J5GA4gASgJEhEKCWNsaWVudF9pZBgPIAEoCRovCg1NZXRhZGF0YUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEi+AMKFFF1ZXJ5QXVkaXRMb2dSZXF1ZXN0Eg4KBm9yZ19pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCRISCgZhY3Rpb24YAyABKAlCAhgBEhAKCHJlc291cmNlGAQgASgJEhMKC3Jlc291cmNlX2lkGAUgASgJEigKBGZyb20YBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEiYKAnRvGAcgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIcCglwYWdlX3NpemUYCCABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAkgASgJEhIKCmV2ZW50X3R5cGUYCiABKAkSEAoIY2F0ZWdvcnkYCyABKAkSVQoQcGF5bG9hZF9jb250YWlucxgMIAMoCzI7LnNhYXMuYWNjb3VudHMudjEuUXVlcnlBdWRpdExvZ1JlcXVlc3QuUGF5bG9hZENvbnRhaW5zRW50cnkSEQoJbmFtZXNwYWNlGA0gASgJEhEKCWNsaWVudF9pZBgOIAEoCRIkCgtldmVudF90eXBlcxgPIAMoCUIPukgMkgEJEEAiBXIDGP8BGjYKFFBheWxvYWRDb250YWluc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEicwoVUXVlcnlBdWRpdExvZ1Jlc3BvbnNlEiwKBmV2ZW50cxgBIAMoCzIcLnNhYXMuYWNjb3VudHMudjEuQXVkaXRFdmVudBIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkSEwoLdG90YWxfY291bnQYAyABKAUilwEKFUV4cG9ydEF1ZGl0TG9nUmVxdWVzdBIOCgZvcmdfaWQYASABKAkSDgoGZm9ybWF0GAIgASgJEhAKCGFjdG9yX2lkGAMgASgJEhIKBmFjdGlvbhgEIAEoCUICGAESEgoKZXZlbnRfdHlwZRgFIAEoCRIkCgtldmVudF90eXBlcxgGIAMoCUIPukgMkgEJEEAiBXIDGP8BIk4KFkV4cG9ydEF1ZGl0TG9nUmVzcG9uc2USDAoEZGF0YRgBIAEoDBIUCgxjb250ZW50X3R5cGUYAiABKAkSEAoIZmlsZW5hbWUYAyABKAkiSwoLQXVkaXRNZXRyaWMSCgoCb3AYASABKAkSDQoFZmllbGQYAiABKAkSEgoKcGVyY2VudGlsZRgDIAEoARINCgVhbGlhcxgEIAEoCSJLChJBdWRpdERlcml2ZWRNZXRyaWMSDQoFYWxpYXMYASABKAkSEQoJbnVtZXJhdG9yGAIgASgJEhMKC2Rlbm9taW5hdG9yGAMgASgJIu0EChhBZ2dyZWdhdGVBdWRpdExvZ1JlcXVlc3QSDgoGb3JnX2lkGAEgASgJEhAKCGFjdG9yX2lkGAIgASgJEhIKCmV2ZW50X3R5cGUYAyABKAkSEAoIY2F0ZWdvcnkYBCABKAkSEAoIcmVzb3VyY2UYBSABKAkSKAoEZnJvbRgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASJgoCdG8YByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhAKCGdyb3VwX2J5GAggASgJEg4KBmJ1Y2tldBgJIAEoCRIRCglncm91cF9ieXMYCiADKAkSLgoHbWV0cmljcxgLIAMoCzIdLnNhYXMuYWNjb3VudHMudjEuQXVkaXRNZXRyaWMSNQoHZGVyaXZlZBgMIAMoCzIkLnNhYXMuYWNjb3VudHMudjEuQXVkaXREZXJpdmVkTWV0cmljEhEKCW5hbWVzcGFjZRgNIAEoCRITCgtyZXNvdXJjZV9pZBgOIAEoCRJZChBwYXlsb2FkX2NvbnRhaW5zGA8gAygLMj8uc2Fhcy5hY2NvdW50cy52MS5BZ2dyZWdhdGVBdWRpdExvZ1JlcXVlc3QuUGF5bG9hZENvbnRhaW5zRW50cnkSFQoNY29sbGVjdGlvbl9pZBgQIAEoCRIRCgljbGllbnRfaWQYESABKAkSJAoLZXZlbnRfdHlwZXMYEiADKAlCD7pIDJIBCRBAIgVyAxj/ARo2ChRQYXlsb2FkQ29udGFpbnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIqwCChRBdWRpdEFnZ3JlZ2F0ZUJ1Y2tldBILCgNrZXkYASABKAkSDQoFY291bnQYAiABKAMSDAoEa2V5cxgDIAMoCRJECgdtZXRyaWNzGAQgAygLMjMuc2Fhcy5hY2NvdW50cy52MS5BdWRpdEFnZ3JlZ2F0ZUJ1Y2tldC5NZXRyaWNzRW50cnkSRAoHc2FtcGxlcxgFIAMoCzIzLnNhYXMuYWNjb3VudHMudjEuQXVkaXRBZ2dyZWdhdGVCdWNrZXQuU2FtcGxlc0VudHJ5Gi4KDE1ldHJpY3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAE6AjgBGi4KDFNhbXBsZXNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAM6AjgBInQKGUFnZ3JlZ2F0ZUF1ZGl0TG9nUmVzcG9uc2USNwoHYnVja2V0cxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuQXVkaXRBZ2dyZWdhdGVCdWNrZXQSHgoWc2NvcGVfY29udHJhY3RfdmVyc2lvbhgCIAEoDSIcChpMaXN0QXVkaXRFdmVudFR5cGVzUmVxdWVzdCLBAQoOQXVkaXRFdmVudFR5cGUSDAoEbmFtZRgBIAEoCRIPCgd2ZXJzaW9uGAIgASgFEhAKCGNhdGVnb3J5GAMgASgJEg0KBW93bmVyGAQgASgJEhIKCmRlcHJlY2F0ZWQYBSABKAgSEwoLZGVzY3JpcHRpb24YBiABKAkSEQoJbmFtZXNwYWNlGAcgASgJEhkKEW1hcmtzX3VzZXJfam9pbmVkGAggASgIEhgKEHdlYmhvb2tfZWxpZ2libGUYCSABKAgiTgobTGlzdEF1ZGl0RXZlbnRUeXBlc1Jlc3BvbnNlEi8KBXR5cGVzGAEgAygLMiAuc2Fhcy5hY2NvdW50cy52MS5BdWRpdEV2ZW50VHlwZTKdBgoMQXVkaXRTZXJ2aWNlErUBCg1RdWVyeUF1ZGl0TG9nEiYuc2Fhcy5hY2NvdW50cy52MS5RdWVyeUF1ZGl0TG9nUmVxdWVzdBonLnNhYXMuYWNjb3VudHMudjEuUXVlcnlBdWRpdExvZ1Jlc3BvbnNlIlPC8xg6CAIQAxoKYXVkaXQ6cmVhZCIKYXVkaXQ6cmVhZCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCDxINL3YxL2F1ZGl0LWxvZxLLAQoRQWdncmVnYXRlQXVkaXRMb2cSKi5zYWFzLmFjY291bnRzLnYxLkFnZ3JlZ2F0ZUF1ZGl0TG9nUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuQWdncmVnYXRlQXVkaXRMb2dSZXNwb25zZSJdwvMYOggCEAMaCmF1ZGl0OnJlYWQiCmF1ZGl0OnJlYWQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAhkSFy92MS9hdWRpdC1sb2c6YWdncmVnYXRlEsEBChNMaXN0QXVkaXRFdmVudFR5cGVzEiwuc2Fhcy5hY2NvdW50cy52MS5MaXN0QXVkaXRFdmVudFR5cGVzUmVxdWVzdBotLnNhYXMuYWNjb3VudHMudjEuTGlzdEF1ZGl0RXZlbnRUeXBlc1Jlc3BvbnNlIk3C8xgsCAIQARoKYXVkaXQ6cmVhZCIKYXVkaXQ6cmVhZDABOgIQAUABSANQAlgCYAGC0+STAhcSFS92MS9hdWRpdC1ldmVudC10eXBlcxLCAQoORXhwb3J0QXVkaXRMb2cSJy5zYWFzLmFjY291bnRzLnYxLkV4cG9ydEF1ZGl0TG9nUmVxdWVzdBooLnNhYXMuYWNjb3VudHMudjEuRXhwb3J0QXVkaXRMb2dSZXNwb25zZSJdwvMYOggCEAQaCmF1ZGl0OnJlYWQiCmF1ZGl0OnJlYWQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAhk6ASoiFC92MS9hdWRpdC1sb2c6ZXhwb3J0YgZwcm90bzM", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_struct, file_google_protobuf_timestamp, file_saas_policy_v1_options]); + fileDesc("ChxzYWFzL2FjY291bnRzL3YxL2F1ZGl0LnByb3RvEhBzYWFzLmFjY291bnRzLnYxIrsDCgpBdWRpdEV2ZW50EgoKAmlkGAEgASgJEhAKCGFjdG9yX2lkGAIgASgJEhIKCmFjdG9yX3R5cGUYAyABKAkSEgoGYWN0aW9uGAQgASgJQgIYARIQCghyZXNvdXJjZRgFIAEoCRITCgtyZXNvdXJjZV9pZBgGIAEoCRIOCgZvcmdfaWQYByABKAkSQAoIbWV0YWRhdGEYCCADKAsyKi5zYWFzLmFjY291bnRzLnYxLkF1ZGl0RXZlbnQuTWV0YWRhdGFFbnRyeUICGAESEgoKaXBfYWRkcmVzcxgJIAEoCRIuCgpjcmVhdGVkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBISCgpldmVudF90eXBlGAsgASgJEhYKDnNjaGVtYV92ZXJzaW9uGAwgASgFEigKB3BheWxvYWQYDSABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EhAKCGNhdGVnb3J5GA4gASgJEhEKCWNsaWVudF9pZBgPIAEoCRovCg1NZXRhZGF0YUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEi+AMKFFF1ZXJ5QXVkaXRMb2dSZXF1ZXN0Eg4KBm9yZ19pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCRISCgZhY3Rpb24YAyABKAlCAhgBEhAKCHJlc291cmNlGAQgASgJEhMKC3Jlc291cmNlX2lkGAUgASgJEigKBGZyb20YBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEiYKAnRvGAcgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIcCglwYWdlX3NpemUYCCABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAkgASgJEhIKCmV2ZW50X3R5cGUYCiABKAkSEAoIY2F0ZWdvcnkYCyABKAkSVQoQcGF5bG9hZF9jb250YWlucxgMIAMoCzI7LnNhYXMuYWNjb3VudHMudjEuUXVlcnlBdWRpdExvZ1JlcXVlc3QuUGF5bG9hZENvbnRhaW5zRW50cnkSEQoJbmFtZXNwYWNlGA0gASgJEhEKCWNsaWVudF9pZBgOIAEoCRIkCgtldmVudF90eXBlcxgPIAMoCUIPukgMkgEJEEAiBXIDGP8BGjYKFFBheWxvYWRDb250YWluc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEicwoVUXVlcnlBdWRpdExvZ1Jlc3BvbnNlEiwKBmV2ZW50cxgBIAMoCzIcLnNhYXMuYWNjb3VudHMudjEuQXVkaXRFdmVudBIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkSEwoLdG90YWxfY291bnQYAyABKAUilwEKFUV4cG9ydEF1ZGl0TG9nUmVxdWVzdBIOCgZvcmdfaWQYASABKAkSDgoGZm9ybWF0GAIgASgJEhAKCGFjdG9yX2lkGAMgASgJEhIKBmFjdGlvbhgEIAEoCUICGAESEgoKZXZlbnRfdHlwZRgFIAEoCRIkCgtldmVudF90eXBlcxgGIAMoCUIPukgMkgEJEEAiBXIDGP8BIk4KFkV4cG9ydEF1ZGl0TG9nUmVzcG9uc2USDAoEZGF0YRgBIAEoDBIUCgxjb250ZW50X3R5cGUYAiABKAkSEAoIZmlsZW5hbWUYAyABKAkiSwoLQXVkaXRNZXRyaWMSCgoCb3AYASABKAkSDQoFZmllbGQYAiABKAkSEgoKcGVyY2VudGlsZRgDIAEoARINCgVhbGlhcxgEIAEoCSJLChJBdWRpdERlcml2ZWRNZXRyaWMSDQoFYWxpYXMYASABKAkSEQoJbnVtZXJhdG9yGAIgASgJEhMKC2Rlbm9taW5hdG9yGAMgASgJIu0EChhBZ2dyZWdhdGVBdWRpdExvZ1JlcXVlc3QSDgoGb3JnX2lkGAEgASgJEhAKCGFjdG9yX2lkGAIgASgJEhIKCmV2ZW50X3R5cGUYAyABKAkSEAoIY2F0ZWdvcnkYBCABKAkSEAoIcmVzb3VyY2UYBSABKAkSKAoEZnJvbRgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASJgoCdG8YByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhAKCGdyb3VwX2J5GAggASgJEg4KBmJ1Y2tldBgJIAEoCRIRCglncm91cF9ieXMYCiADKAkSLgoHbWV0cmljcxgLIAMoCzIdLnNhYXMuYWNjb3VudHMudjEuQXVkaXRNZXRyaWMSNQoHZGVyaXZlZBgMIAMoCzIkLnNhYXMuYWNjb3VudHMudjEuQXVkaXREZXJpdmVkTWV0cmljEhEKCW5hbWVzcGFjZRgNIAEoCRITCgtyZXNvdXJjZV9pZBgOIAEoCRJZChBwYXlsb2FkX2NvbnRhaW5zGA8gAygLMj8uc2Fhcy5hY2NvdW50cy52MS5BZ2dyZWdhdGVBdWRpdExvZ1JlcXVlc3QuUGF5bG9hZENvbnRhaW5zRW50cnkSFQoNY29sbGVjdGlvbl9pZBgQIAEoCRIRCgljbGllbnRfaWQYESABKAkSJAoLZXZlbnRfdHlwZXMYEiADKAlCD7pIDJIBCRBAIgVyAxj/ARo2ChRQYXlsb2FkQ29udGFpbnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIqwCChRBdWRpdEFnZ3JlZ2F0ZUJ1Y2tldBILCgNrZXkYASABKAkSDQoFY291bnQYAiABKAMSDAoEa2V5cxgDIAMoCRJECgdtZXRyaWNzGAQgAygLMjMuc2Fhcy5hY2NvdW50cy52MS5BdWRpdEFnZ3JlZ2F0ZUJ1Y2tldC5NZXRyaWNzRW50cnkSRAoHc2FtcGxlcxgFIAMoCzIzLnNhYXMuYWNjb3VudHMudjEuQXVkaXRBZ2dyZWdhdGVCdWNrZXQuU2FtcGxlc0VudHJ5Gi4KDE1ldHJpY3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAE6AjgBGi4KDFNhbXBsZXNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAM6AjgBInQKGUFnZ3JlZ2F0ZUF1ZGl0TG9nUmVzcG9uc2USNwoHYnVja2V0cxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuQXVkaXRBZ2dyZWdhdGVCdWNrZXQSHgoWc2NvcGVfY29udHJhY3RfdmVyc2lvbhgCIAEoDSIcChpMaXN0QXVkaXRFdmVudFR5cGVzUmVxdWVzdCLBAQoOQXVkaXRFdmVudFR5cGUSDAoEbmFtZRgBIAEoCRIPCgd2ZXJzaW9uGAIgASgFEhAKCGNhdGVnb3J5GAMgASgJEg0KBW93bmVyGAQgASgJEhIKCmRlcHJlY2F0ZWQYBSABKAgSEwoLZGVzY3JpcHRpb24YBiABKAkSEQoJbmFtZXNwYWNlGAcgASgJEhkKEW1hcmtzX3VzZXJfam9pbmVkGAggASgIEhgKEHdlYmhvb2tfZWxpZ2libGUYCSABKAgiTgobTGlzdEF1ZGl0RXZlbnRUeXBlc1Jlc3BvbnNlEi8KBXR5cGVzGAEgAygLMiAuc2Fhcy5hY2NvdW50cy52MS5BdWRpdEV2ZW50VHlwZSJLChlEZWxpdmVyQXVkaXRFdmVudHNSZXF1ZXN0EhcKBGZyb20YASABKAlCCbpIBnIEEAEYQBIVCgJ0bxgCIAEoCUIJukgGcgQQARhAImwKGkRlbGl2ZXJBdWRpdEV2ZW50c1Jlc3BvbnNlEhEKCWVmZmVjdF9pZBgBIAEoCRIOCgZzdGF0dXMYAiABKAkSEQoJZGVsaXZlcmVkGAMgASgFEgwKBGZyb20YBCABKAkSCgoCdG8YBSABKAky6QkKDEF1ZGl0U2VydmljZRK1AQoNUXVlcnlBdWRpdExvZxImLnNhYXMuYWNjb3VudHMudjEuUXVlcnlBdWRpdExvZ1JlcXVlc3QaJy5zYWFzLmFjY291bnRzLnYxLlF1ZXJ5QXVkaXRMb2dSZXNwb25zZSJTwvMYOggCEAMaCmF1ZGl0OnJlYWQiCmF1ZGl0OnJlYWQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAg8SDS92MS9hdWRpdC1sb2cSywEKEUFnZ3JlZ2F0ZUF1ZGl0TG9nEiouc2Fhcy5hY2NvdW50cy52MS5BZ2dyZWdhdGVBdWRpdExvZ1JlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLkFnZ3JlZ2F0ZUF1ZGl0TG9nUmVzcG9uc2UiXcLzGDoIAhADGgphdWRpdDpyZWFkIgphdWRpdDpyZWFkKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIZEhcvdjEvYXVkaXQtbG9nOmFnZ3JlZ2F0ZRLBAQoTTGlzdEF1ZGl0RXZlbnRUeXBlcxIsLnNhYXMuYWNjb3VudHMudjEuTGlzdEF1ZGl0RXZlbnRUeXBlc1JlcXVlc3QaLS5zYWFzLmFjY291bnRzLnYxLkxpc3RBdWRpdEV2ZW50VHlwZXNSZXNwb25zZSJNwvMYLAgCEAEaCmF1ZGl0OnJlYWQiCmF1ZGl0OnJlYWQwAToCEAFAAUgDUAJYAmABgtPkkwIXEhUvdjEvYXVkaXQtZXZlbnQtdHlwZXMSwgEKDkV4cG9ydEF1ZGl0TG9nEicuc2Fhcy5hY2NvdW50cy52MS5FeHBvcnRBdWRpdExvZ1JlcXVlc3QaKC5zYWFzLmFjY291bnRzLnYxLkV4cG9ydEF1ZGl0TG9nUmVzcG9uc2UiXcLzGDoIAhAEGgphdWRpdDpyZWFkIgphdWRpdDpyZWFkKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIZOgEqIhQvdjEvYXVkaXQtbG9nOmV4cG9ydBK3AgoSRGVsaXZlckF1ZGl0RXZlbnRzEisuc2Fhcy5hY2NvdW50cy52MS5EZWxpdmVyQXVkaXRFdmVudHNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5EZWxpdmVyQXVkaXRFdmVudHNSZXNwb25zZSLFAcLzGBQIAhABMAE6AhABQANIBFACWAJgAdrzGKgBCgIIPBIDCNgEGAMiAggFKgtVTkFWQUlMQUJMRSoSUkVTT1VSQ0VfRVhIQVVTVEVEKgdBQk9SVEVEMgpzYWFzLWF1ZGl0Sjcvc2Fhcy5hY2NvdW50cy52MS5BdWRpdFNlcnZpY2UvTG9va3VwRGVsaXZlckF1ZGl0RXZlbnRzUIACWIAIYAFyIAoOYXVkaXRfZGVsaXZlcnkSBmludm9rZRIEcmVhZBgBEo8BChhMb29rdXBEZWxpdmVyQXVkaXRFdmVudHMSKy5zYWFzLmFjY291bnRzLnYxLkRlbGl2ZXJBdWRpdEV2ZW50c1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkRlbGl2ZXJBdWRpdEV2ZW50c1Jlc3BvbnNlIhjC8xgUCAIQATABOgIQAUABSANQAlgCYAFiBnByb3RvMw", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_struct, file_google_protobuf_timestamp, file_codefly_runnable_v0_options, file_saas_policy_v1_options]); /** * @generated from message saas.accounts.v1.AuditEvent @@ -360,7 +361,11 @@ export type AuditMetric = Message<"saas.accounts.v1.AuditMetric"> & { field: string; /** - * percentile ∈ (0,1], used only when op == percentile (0.95 → p95). + * percentile ∈ (0,1], used only when op == percentile (0.95 → p95). Only the + * Postgres audit backend computes it: the warehouse backend keeps entries + * at-least-once and deduplicates them on read, and a percentile over that is not + * an exact answer, so a deployment on the warehouse backend refuses a query that + * asks for one with InvalidArgument. Every other op is served by both backends. * * @generated from field: double percentile = 3; */ @@ -733,6 +738,87 @@ export type ListAuditEventTypesResponse = Message<"saas.accounts.v1.ListAuditEve export const ListAuditEventTypesResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_saas_accounts_v1_audit, 12); +/** + * DeliverAuditEventsRequest names one range of the platform journal, (from, to], + * whose audit records the operation appends to the warehouse audit storage. Both + * ends are positions a ModuleReadJournal page returned; from may instead be the + * literal "start", meaning before the oldest retained entry, and is never empty so + * that the first range of a fresh journal has a non-empty value to correlate on. + * The range ends at a position that is final (below the oldest transaction still + * running) and holds at most 500 audit records, or it is refused before anything + * is appended. The effect id is "saas.audit.recorded.v1/", derived by the + * host: one range, one receipt. A runtime task may carry its own transport + * effect identity; the range receipt is independent of that task identity. + * + * @generated from message saas.accounts.v1.DeliverAuditEventsRequest + */ +export type DeliverAuditEventsRequest = Message<"saas.accounts.v1.DeliverAuditEventsRequest"> & { + /** + * @generated from field: string from = 1; + */ + from: string; + + /** + * @generated from field: string to = 2; + */ + to: string; +}; + +/** + * Describes the message saas.accounts.v1.DeliverAuditEventsRequest. + * Use `create(DeliverAuditEventsRequestSchema)` to create a new message. + */ +export const DeliverAuditEventsRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_audit, 13); + +/** + * @generated from message saas.accounts.v1.DeliverAuditEventsResponse + */ +export type DeliverAuditEventsResponse = Message<"saas.accounts.v1.DeliverAuditEventsResponse"> & { + /** + * @generated from field: string effect_id = 1; + */ + effectId: string; + + /** + * status is "committed" when the range is recorded. An absent lookup + * returns NOT_FOUND: absence is inconclusive, never a committed response. + * + * @generated from field: string status = 2; + */ + status: string; + + /** + * delivered is the number of audit records of the range appended to the + * storage; a range with none still commits a receipt. + * + * @generated from field: int32 delivered = 3; + */ + delivered: number; + + /** + * The exact committed interval is (from, to]. Retention verifies these + * positions against the receipt's effect identity and request digest before + * retiring journal rows. Legacy receipts without both positions prove no + * coverage; delivered alone never proves an interval. + * + * @generated from field: string from = 4; + */ + from: string; + + /** + * @generated from field: string to = 5; + */ + to: string; +}; + +/** + * Describes the message saas.accounts.v1.DeliverAuditEventsResponse. + * Use `create(DeliverAuditEventsResponseSchema)` to create a new message. + */ +export const DeliverAuditEventsResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_audit, 14); + /** * AuditService — append-only audit event log * @@ -771,5 +857,32 @@ export const AuditService: GenService<{ input: typeof ExportAuditLogRequestSchema; output: typeof ExportAuditLogResponseSchema; }, + /** + * DeliverAuditEvents appends one range of the journal's audit records to the + * warehouse audit storage: the operation module-runtime calls after reading the + * range through ReadJournal. It is an operation, not a sweep. It takes each + * record's organization from the journal row and never from the caller, emits no + * audit of its own, and commits one receipt per range under the installation's + * tenant; delivery is at least once, and every read of the storage returns each + * entry once by id. Its receipt is read back through LookupDeliverAuditEvents. + * + * @generated from rpc saas.accounts.v1.AuditService.DeliverAuditEvents + */ + deliverAuditEvents: { + methodKind: "unary"; + input: typeof DeliverAuditEventsRequestSchema; + output: typeof DeliverAuditEventsResponseSchema; + }, + /** + * LookupDeliverAuditEvents takes the invocation's same {from, to} body and + * derives the range receipt identity from from. It never delivers. + * + * @generated from rpc saas.accounts.v1.AuditService.LookupDeliverAuditEvents + */ + lookupDeliverAuditEvents: { + methodKind: "unary"; + input: typeof DeliverAuditEventsRequestSchema; + output: typeof DeliverAuditEventsResponseSchema; + }, }> = /*@__PURE__*/ serviceDesc(file_saas_accounts_v1_audit, 0); diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/module_capabilities_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/module_capabilities_pb.ts index 3274731a1..fba47e9d3 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/module_capabilities_pb.ts +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/module_capabilities_pb.ts @@ -22,7 +22,7 @@ import type { JsonObject, Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/module_capabilities.proto. */ export const file_saas_accounts_v1_module_capabilities: GenFile = /*@__PURE__*/ - fileDesc("CipzYWFzL2FjY291bnRzL3YxL21vZHVsZV9jYXBhYmlsaXRpZXMucHJvdG8SEHNhYXMuYWNjb3VudHMudjEiXgoXTW9kdWxlRW5xdWV1ZUpvYlJlcXVlc3QSGAoGdGVuYW50GAEgASgJQgi6SAVyA7ABARIpCgNqb2IYAiABKAsyFC5zYWFzLmpvYnMudjEuTmV3Sm9iQga6SAPIAQEidgoYTW9kdWxlRW5xdWV1ZUpvYlJlc3BvbnNlEhgKBmpvYl9pZBgBIAEoCUIIukgFcgOwAQESQAoLZGlzcG9zaXRpb24YAiABKA4yIy5zYWFzLmpvYnMudjEuSm9iRW5xdWV1ZURpc3Bvc2l0aW9uQga6SAPIAQEiygEKFk1vZHVsZUNsYWltSm9ic1JlcXVlc3QSLgoFcXVldWUYASABKAlCH7pIHHIaEAEYgAEyE15bYS16XVthLXowLTlfLi1dKiQSHQoJd29ya2VyX2lkGAIgASgJQgq6SAdyBRABGP8BEhgKBWxpbWl0GAMgASgNQgm6SAYqBBhkKAESRwoObGVhc2VfZHVyYXRpb24YBCABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25CFLpIEcgBAaoBCyIDCJAcMgQQwIQ9IkIKF01vZHVsZUNsYWltSm9ic1Jlc3BvbnNlEicKBGpvYnMYASADKAsyGS5zYWFzLmpvYnMudjEuSm9iRW52ZWxvcGUilwEKGU1vZHVsZUhlYXJ0YmVhdEpvYlJlcXVlc3QSNgoFbGVhc2UYASABKAsyHy5zYWFzLmpvYnMudjEuSm9iTGVhc2VSZWZlcmVuY2VCBrpIA8gBARJCCglleHRlbnNpb24YAiABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25CFLpIEcgBAaoBCyIDCJAcMgQQwIQ9IksKGk1vZHVsZUhlYXJ0YmVhdEpvYlJlc3BvbnNlEi0KBWxlYXNlGAEgASgLMhYuc2Fhcy5qb2JzLnYxLkpvYkxlYXNlQga6SAPIAQEipgEKE01vZHVsZUFja0pvYlJlcXVlc3QSNgoFbGVhc2UYASABKAsyHy5zYWFzLmpvYnMudjEuSm9iTGVhc2VSZWZlcmVuY2VCBrpIA8gBARI3Cg5leGVjdXRpb25fa2luZBgCIAEoCUIfukgcchoYQDIWXiR8XlthLXpdW2EtejAtOV8uLV0qJBIeCgxleGVjdXRpb25faWQYAyABKAlCCLpIBXIDGP8BIsIBChRNb2R1bGVOYWNrSm9iUmVxdWVzdBI2CgVsZWFzZRgBIAEoCzIfLnNhYXMuam9icy52MS5Kb2JMZWFzZVJlZmVyZW5jZUIGukgDyAEBEjEKB2ZhaWx1cmUYAiABKAsyGC5zYWFzLmpvYnMudjEuSm9iRmFpbHVyZUIGukgDyAEBEhEKCXJldHJ5YWJsZRgDIAEoCBIsCghyZXRyeV9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAi9gEKF01vZHVsZU5vdGlmeVVzZXJSZXF1ZXN0EhgKBnRlbmFudBgBIAEoCUIIukgFcgOwAQESGQoHdXNlcl9pZBgCIAEoCUIIukgFcgOwAQESGQoFdGl0bGUYAyABKAlCCrpIB3IFEAEY5gcSFgoEYm9keRgEIAEoCUIIukgFcgMYgEASFQoEdHlwZRgFIAEoCUIHukgEcgIYQBIcCgphY3Rpb25fdXJsGAYgASgJQgi6SAVyAxiAEBIbCghjYXRlZ29yeRgHIAEoCUIJukgGcgQQARggEiEKD2lkZW1wb3RlbmN5X2tleRgIIAEoCUIIukgFcgMY/wEiRgoYTW9kdWxlTm90aWZ5VXNlclJlc3BvbnNlEhcKD25vdGlmaWNhdGlvbl9pZBgBIAEoCRIRCglkZWxpdmVyZWQYAiABKAgi4AEKHE1vZHVsZU5vdGlmeU9yZ0FkbWluc1JlcXVlc3QSGAoGdGVuYW50GAEgASgJQgi6SAVyA7ABARIZCgV0aXRsZRgCIAEoCUIKukgHcgUQARjmBxIWCgRib2R5GAMgASgJQgi6SAVyAxiAQBIVCgR0eXBlGAQgASgJQge6SARyAhhAEhwKCmFjdGlvbl91cmwYBSABKAlCCLpIBXIDGIAQEhsKCGNhdGVnb3J5GAYgASgJQgm6SAZyBBABGCASIQoPaWRlbXBvdGVuY3lfa2V5GAcgASgJQgi6SAVyAxj/ASIyCh1Nb2R1bGVOb3RpZnlPcmdBZG1pbnNSZXNwb25zZRIRCglkZWxpdmVyZWQYASABKAgiYwoUTW9kdWxlQXBwcm92YWxQb2xpY3kSFwoGcXVvcnVtGAEgASgNQge6SAQqAhhkEh4KDGFwcHJvdmVyX3NldBgCIAMoCUIIukgFkgECEGQSEgoKYWxsb3dfc2VsZhgDIAEoCCKbAQoPTW9kdWxlUmVzdW1lUmVmEi4KBXF1ZXVlGAEgASgJQh+6SBxyGhABGIABMhNeW2Etel1bYS16MC05Xy4tXSokEi4KBXRvcGljGAIgASgJQh+6SBxyGhABGP8BMhNeW2Etel1bYS16MC05Xy4tXSokEigKB3BheWxvYWQYAyABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0IpYDChxNb2R1bGVSZXF1ZXN0QXBwcm92YWxSZXF1ZXN0EhgKBnRlbmFudBgBIAEoCUIIukgFcgOwAQESHAoIcmVzb3VyY2UYAiABKAlCCrpIB3IFEAEYgAESGgoGYWN0aW9uGAMgASgJQgq6SAdyBRABGIABEigKB3N1YmplY3QYBCABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EiAKDHJlcXVlc3RlZF9ieRgFIAEoCUIKukgHcgUQARj/ARI2CgZwb2xpY3kYBiABKAsyJi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUFwcHJvdmFsUG9saWN5Ej0KCnJlc3VtZV9yZWYYByABKAsyIS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVJlc3VtZVJlZkIGukgDyAEBEi4KCmV4cGlyZXNfYXQYCCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC2VzY2FsYXRlX2F0GAkgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCI+Ch1Nb2R1bGVSZXF1ZXN0QXBwcm92YWxSZXNwb25zZRIdCgthcHByb3ZhbF9pZBgBIAEoCUIIukgFcgOwAQEiUwoYTW9kdWxlR2V0QXBwcm92YWxSZXF1ZXN0EhgKBnRlbmFudBgBIAEoCUIIukgFcgOwAQESHQoLYXBwcm92YWxfaWQYAiABKAlCCLpIBXIDsAEBIvUCCg5Nb2R1bGVBcHByb3ZhbBIKCgJpZBgBIAEoCRIOCgZ0ZW5hbnQYAiABKAkSEAoIcmVzb3VyY2UYAyABKAkSDgoGYWN0aW9uGAQgASgJEigKB3N1YmplY3QYBSABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EhQKDHJlcXVlc3RlZF9ieRgGIAEoCRIOCgZxdW9ydW0YByABKA0SDQoFc3RhdGUYCCABKAkSNQoKcmVzdW1lX3JlZhgJIAEoCzIhLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUmVzdW1lUmVmEi4KCmV4cGlyZXNfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC2VzY2FsYXRlX2F0GAsgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIuCgpjcmVhdGVkX2F0GAwgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCJwChtNb2R1bGVDYW5jZWxBcHByb3ZhbFJlcXVlc3QSGAoGdGVuYW50GAEgASgJQgi6SAVyA7ABARIdCgthcHByb3ZhbF9pZBgCIAEoCUIIukgFcgOwAQESGAoGcmVhc29uGAMgASgJQgi6SAVyAxiACCLuAQobTW9kdWxlRW1pdEF1ZGl0RXZlbnRSZXF1ZXN0Eg4KBnRlbmFudBgBIAEoCRIeCgpldmVudF90eXBlGAIgASgJQgq6SAdyBRABGIABEhkKBWFjdG9yGAMgASgJQgq6SAdyBRABGP8BEhwKCHNvbHV0aW9uGAQgASgJQgq6SAdyBRABGIABEhoKCGVudHJ5X2lkGAUgASgJQgi6SAVyAxj/ARInCgZmaWVsZHMYBiABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EiEKD2lkZW1wb3RlbmN5X2tleRgHIAEoCUIIukgFcgMY/wEinwEKG01vZHVsZUF1ZGl0RmllbGREZWNsYXJhdGlvbhIXCgRuYW1lGAEgASgJQgm6SAZyBBABGEASQAoEa2luZBgCIAEoDjImLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlQXVkaXRGaWVsZEtpbmRCCrpIB4IBBBABIAASGAoGdmFsdWVzGAMgAygJQgi6SAWSAQIQQBILCgNwaWkYBCABKAgi7wEKH01vZHVsZUF1ZGl0RXZlbnRUeXBlRGVjbGFyYXRpb24SGAoEdHlwZRgBIAEoCUIKukgHcgUQARiAARIdCgtkZXNjcmlwdGlvbhgCIAEoCUIIukgFcgMYgAgSRwoGZmllbGRzGAMgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVBdWRpdEZpZWxkRGVjbGFyYXRpb25CCLpIBZIBAhAgEkoKCnZpc2liaWxpdHkYBCABKA4yLC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUF1ZGl0RXZlbnRWaXNpYmlsaXR5Qgi6SAWCAQIQASKzAQojTW9kdWxlRGVjbGFyZUF1ZGl0RXZlbnRUeXBlc1JlcXVlc3QSPgoGcHJlZml4GAEgASgJQi66SCtyKRABGIABMiJeW2EtejAtOV0oPzpbYS16MC05Xy1dKlthLXowLTldKT8kEkwKBXR5cGVzGAIgAygLMjEuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVBdWRpdEV2ZW50VHlwZURlY2xhcmF0aW9uQgq6SAeSAQQIARBAIl0KJE1vZHVsZURlY2xhcmVBdWRpdEV2ZW50VHlwZXNSZXNwb25zZRIWCg5hZG1pdHRlZF90eXBlcxgBIAMoCRIdChVuYW1lc3BhY2VzX3Rha2VuX292ZXIYAiADKAkibQoWU3ViamVjdFZpc2liaWxpdHlHcmFudBIjChJ2aXNpYmxlX3N1YmplY3RfaWQYASABKAlCB7pIBHICEAESLgoKZXhwaXJlc19hdBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiYwoiTW9kdWxlTGlzdFN1YmplY3RWaXNpYmlsaXR5UmVxdWVzdBIYCgZ0ZW5hbnQYASABKAlCCLpIBXIDsAEBEiMKEXZpZXdlcl9zdWJqZWN0X2lkGAIgASgJQgi6SAVyA7ABASJfCiNNb2R1bGVMaXN0U3ViamVjdFZpc2liaWxpdHlSZXNwb25zZRI4CgZncmFudHMYASADKAsyKC5zYWFzLmFjY291bnRzLnYxLlN1YmplY3RWaXNpYmlsaXR5R3JhbnQiVwoaRmV0Y2hEYXRhc291cmNlQmxvYlJlcXVlc3QSGwoJc291cmNlX2lkGAEgASgJQgi6SAVyA7ABARIcCghibG9iX3NoYRgCIAEoCUIKukgHcgUQARj/ASJSChhGZXRjaERhdGFzb3VyY2VCbG9iQ2h1bmsSDAoEZGF0YRgBIAEoDBISCgp0b3RhbF9zaXplGAIgASgDEhQKDGNvbnRlbnRfdHlwZRgDIAEoCSJSChFEYXRhc291cmNlRmlsZVJlZhIbCgdpdGVtX2lkGAEgASgJQgq6SAdyBRABGIAgEiAKDGl0ZW1fdmVyc2lvbhgCIAEoCUIKukgHcgUQARj/ASKYAQobRmV0Y2hEYXRhc291cmNlRmlsZXNSZXF1ZXN0EhsKCXNvdXJjZV9pZBgBIAEoCUIIukgFcgOwAQESGwoHdmVyc2lvbhgCIAEoCUIKukgHcgUQARj/ARI/CgVmaWxlcxgDIAMoCzIjLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZUZpbGVSZWZCC7pICJIBBQgBEOgHIosBChREYXRhc291cmNlUHJvdmVuYW5jZRIRCglzb3VyY2VfaWQYASABKAkSDgoGb3JnX2lkGAIgASgJEhgKEGJvdW5kYXJ5X25vZGVfaWQYAyABKAkSDwoHdmVyc2lvbhgEIAEoCRIPCgdpdGVtX2lkGAUgASgJEhQKDGl0ZW1fdmVyc2lvbhgGIAEoCSLAAQoVRGF0YXNvdXJjZUl0ZW1SZWFkZXJzEjsKBWJhc2lzGAEgASgOMiwuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlSXRlbVJlYWRlcnNCYXNpcxIQCgh1c2VyX2lkcxgCIAMoCRIRCglncm91cF9pZHMYAyADKAkSGAoQYm91bmRhcnlfcmVhZGVycxgEIAEoCBIWCg51bm1hcHBlZF9jb3VudBgFIAEoBRITCgthY2xfdmVyc2lvbhgGIAEoCSK+AQoURGF0YXNvdXJjZUZpbGVIZWFkZXISOgoKcHJvdmVuYW5jZRgBIAEoCzImLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVByb3ZlbmFuY2USDAoEcGF0aBgCIAEoCRIUCgxjb250ZW50X3R5cGUYAyABKAkSDAoEc2l6ZRgEIAEoAxI4CgdyZWFkZXJzGAUgASgLMicuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlSXRlbVJlYWRlcnMibgoZRmV0Y2hEYXRhc291cmNlRmlsZXNGcmFtZRI4CgZoZWFkZXIYASABKAsyJi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VGaWxlSGVhZGVySAASDgoEZGF0YRgCIAEoDEgAQgcKBWZyYW1lIm4KGU1vZHVsZVB1Ymxpc2hFdmVudFJlcXVlc3QSGAoGdGVuYW50GAEgASgJQgi6SAVyA7ABARI3CghlbnZlbG9wZRgCIAEoCzIdLnNhYXMuZXZlbnRzLnYxLkV2ZW50RW52ZWxvcGVCBrpIA8gBASI4ChpNb2R1bGVQdWJsaXNoRXZlbnRSZXNwb25zZRIaCghldmVudF9pZBgBIAEoCUIIukgFcgOwAQEiyAEKFk1vZHVsZVN1YnNjcmliZVJlcXVlc3QSSwoMdHlwZV9wYXR0ZXJuGAEgASgJQjW6SDJyMBABGIABMileW2Etel1bYS16MC05XSooPzpcLlthLXowLTldKykqKD86XC5cKik/JBIuCgVxdWV1ZRgCIAEoCUIfukgcchoQARiAATITXlthLXpdW2EtejAtOV8uLV0qJBIxCghkZWxpdmVyeRgDIAEoDjIfLnNhYXMuYWNjb3VudHMudjEuRXZlbnREZWxpdmVyeSLJAQoSTW9kdWxlU3Vic2NyaXB0aW9uEgoKAmlkGAEgASgJEh8KF3N1YnNjcmliZXJfcHJpbmNpcGFsX2lkGAIgASgJEhQKDHR5cGVfcGF0dGVybhgDIAEoCRINCgVxdWV1ZRgEIAEoCRIxCghkZWxpdmVyeRgFIAEoDjIfLnNhYXMuYWNjb3VudHMudjEuRXZlbnREZWxpdmVyeRIuCgpjcmVhdGVkX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCJVChdNb2R1bGVTdWJzY3JpYmVSZXNwb25zZRI6CgxzdWJzY3JpcHRpb24YASABKAsyJC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVN1YnNjcmlwdGlvbiI9ChhNb2R1bGVVbnN1YnNjcmliZVJlcXVlc3QSIQoPc3Vic2NyaXB0aW9uX2lkGAEgASgJQgi6SAVyA7ABASIgCh5Nb2R1bGVMaXN0U3Vic2NyaXB0aW9uc1JlcXVlc3QiXgofTW9kdWxlTGlzdFN1YnNjcmlwdGlvbnNSZXNwb25zZRI7Cg1zdWJzY3JpcHRpb25zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVTdWJzY3JpcHRpb24iegoZTW9kdWxlUmVwbGF5RXZlbnRzUmVxdWVzdBIYCgZ0ZW5hbnQYASABKAlCCLpIBXIDsAEBEhgKBHR5cGUYAiABKAlCCrpIB3IFEAEYgAESKQoFc2luY2UYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIjEKGk1vZHVsZVJlcGxheUV2ZW50c1Jlc3BvbnNlEhMKC3JlZGVsaXZlcmVkGAEgASgFItMCChhSZWFkYWJsZVNvdXJjZUNvbGxlY3Rpb24SEQoJc291cmNlX2lkGAEgASgJEhMKC2JvdW5kYXJ5X2lkGAIgASgJEg4KBm9yaWdpbhgDIAEoCRIRCgljb250YWluZXIYBCABKAkSCwoDcmVmGAUgASgJEg0KBXBhdGhzGAYgAygJEhYKDmJvdW5kYXJ5X2xhYmVsGAcgASgJEj4KEGdyYW50X2Rpc2Nsb3N1cmUYCCABKA4yJC5zYWFzLmFjY291bnRzLnYxLk1ldGFkYXRhRGlzY2xvc3VyZRI+CgtyZWFkX2dyYW50cxgJIAMoCzIpLnNhYXMuYWNjb3VudHMudjEuUmVhZGFibGVDb2xsZWN0aW9uR3JhbnQSOAoEc3luYxgKIAEoCzIqLnNhYXMuYWNjb3VudHMudjEuQ29sbGVjdGlvblN5bmNQcm92ZW5hbmNlIrABChdSZWFkYWJsZUNvbGxlY3Rpb25HcmFudBIVCg1zdWJqZWN0X2xhYmVsGAEgASgJEhQKDHN1YmplY3Rfa2luZBgCIAEoCRIRCglyb2xlX25hbWUYAyABKAkSEgoKc2NvcGVfcGF0aBgEIAEoCRIRCglpbmhlcml0ZWQYBSABKAgSLgoKZXhwaXJlc19hdBgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiqQIKGENvbGxlY3Rpb25TeW5jUHJvdmVuYW5jZRIwCgVzdGFnZRgBIAEoDjIhLnNhYXMuYWNjb3VudHMudjEuU291cmNlU3luY1N0YWdlEiYKAmF0GAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIQCghyZXZpc2lvbhgDIAEoCRIPCgd0cmlnZ2VyGAQgASgJEkIKFHJlcXVlc3Rlcl9kaXNjbG9zdXJlGAUgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5NZXRhZGF0YURpc2Nsb3N1cmUSMAoMcmVxdWVzdGVkX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIaChJyZXF1ZXN0ZWRfYnlfbGFiZWwYByABKAkikwEKJExpc3RSZWFkYWJsZVNvdXJjZUNvbGxlY3Rpb25zUmVxdWVzdBIdCglwYWdlX3NpemUYASABKAVCCrpIBxoFGOgHKAASHAoKcGFnZV90b2tlbhgCIAEoCUIIukgFcgMYgBASLgoZdmlld2VyX3dvcmtfY29udGV4dF90b2tlbhgDIAEoCUILukgIcgYQARiAgAIigQEKJUxpc3RSZWFkYWJsZVNvdXJjZUNvbGxlY3Rpb25zUmVzcG9uc2USPwoLY29sbGVjdGlvbnMYASADKAsyKi5zYWFzLmFjY291bnRzLnYxLlJlYWRhYmxlU291cmNlQ29sbGVjdGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAki6QEKGE1vZHVsZVBsYWNlUmVjb3JkUmVxdWVzdBIYCgZ0ZW5hbnQYASABKAlCCLpIBXIDsAEBEj0KCnNjb3BlX3BhdGgYAiABKAlCKbpIJnIkEAEYgAQyHV5bYS16MC05X10rKD86XC5bYS16MC05X10rKSokEhcKBGtpbmQYAyABKAlCCbpIBnIEEAEYQBIXCgVsYWJlbBgEIAEoCUIIukgFcgMY/wESIQoNcmVzb3VyY2VfdHlwZRgFIAEoCUIKukgHcgUQARiAARIfCgtyZXNvdXJjZV9pZBgGIAEoCUIKukgHcgUQARiABCI2ChlNb2R1bGVQbGFjZVJlY29yZFJlc3BvbnNlEhkKB25vZGVfaWQYASABKAlCCLpIBXIDsAEBIrUBCiNDaGVja1dvcmtDb250ZXh0UmVjb3JkQWNjZXNzUmVxdWVzdBIhCg1yZXNvdXJjZV90eXBlGAEgASgJQgq6SAdyBRABGIACEh8KC3Jlc291cmNlX2lkGAIgASgJQgq6SAdyBRABGIAEEhoKBmFjdGlvbhgDIAEoCUIKukgHcgUQARiAAhIuChl2aWV3ZXJfd29ya19jb250ZXh0X3Rva2VuGAQgASgJQgu6SAhyBhABGICAAiJOCiRDaGVja1dvcmtDb250ZXh0UmVjb3JkQWNjZXNzUmVzcG9uc2USDwoHYWxsb3dlZBgBIAEoCBIVCg1zY29wZV9ub2RlX2lkGAIgASgJInwKKk1vZHVsZUV4Y2hhbmdlRGVsZWdhdGVkUmVhZEF1ZGllbmNlUmVxdWVzdBIeCgpiaW5kaW5nX2lkGAEgASgJQgq6SAdyBRABGIABEi4KGXBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4YAiABKAlCC7pICHIGEAEYgIACIoADCi9Nb2R1bGVFeGNoYW5nZURlbGVnYXRlZE9wZXJhdGlvbkF1ZGllbmNlUmVxdWVzdBIeCgpiaW5kaW5nX2lkGAEgASgJQgq6SAdyBRABGIABEiwKGXBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4YAiABKAlCCbpIBnIEGICAAhIOCgZsb29rdXAYAyABKAgSIgoNZGVsZWdhdGlvbl9pZBgEIAEoCUILukgI2AEBcgOwAQE6ygG6SMYBGsMBCjNleGNoYW5nZV9kZWxlZ2F0ZWRfb3BlcmF0aW9uX2F1ZGllbmNlLm9uZV9hdXRob3JpdHkSRmV4YWN0bHkgb25lIG9mIHBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4gYW5kIGRlbGVnYXRpb25faWQgaXMgcmVxdWlyZWQaRCh0aGlzLnBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4gIT0gJycpICE9ICh0aGlzLmRlbGVnYXRpb25faWQgIT0gJycpIn0KGkV4ZWN1dGFibGVBcnRpZmFjdENvbnRyYWN0EhcKBGtpbmQYASABKAlCCbpIBnIEEAEYQBIYCgRuYW1lGAIgASgJQgq6SAdyBRABGIAEEiwKBmRpZ2VzdBgDIAEoCUIcukgZchcyFV5zaGEyNTY6WzAtOWEtZl17NjR9JCLiAQoaRXhlY3V0YWJsZUFydGlmYWN0SWRlbnRpdHkSGgoGc2NoZW1hGAEgASgJQgq6SAdyBRABGIABEhoKBnNvdXJjZRgCIAEoCUIKukgHcgUQARiABBIcCgdzdWJqZWN0GAMgASgMQgu6SAh6BhABGICABBJKCgljb250cmFjdHMYBCADKAsyLC5zYWFzLmFjY291bnRzLnYxLkV4ZWN1dGFibGVBcnRpZmFjdENvbnRyYWN0Qgm6SAaSAQMQgAISIgoRZXhwZWN0ZWRfcmV2aXNpb24YBSABKANCB7pIBCICKAAi2wEKH01vZHVsZUV4ZWN1dGFibGVBcnRpZmFjdFJlcXVlc3QSLgoZcGFyZW50X3dvcmtfY29udGV4dF90b2tlbhgBIAEoCUILukgIcgYQARiAgAISIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABARIdCglwb2xpY3lfaWQYAyABKAlCCrpIB3IFEAEYgAESRgoIaWRlbnRpdHkYBCABKAsyLC5zYWFzLmFjY291bnRzLnYxLkV4ZWN1dGFibGVBcnRpZmFjdElkZW50aXR5Qga6SAPIAQEimQEKJU1vZHVsZVJldm9rZUV4ZWN1dGFibGVBcnRpZmFjdFJlcXVlc3QSLgoZcGFyZW50X3dvcmtfY29udGV4dF90b2tlbhgBIAEoCUILukgIcgYQARiAgAISIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABARIdCgthcHByb3ZhbF9pZBgDIAEoCUIIukgFcgOwAQEifQogTW9kdWxlRXhlY3V0YWJsZUFydGlmYWN0UmVzcG9uc2USFgoOc2NoZW1hX3ZlcnNpb24YASABKAkSFgoOcXVhbGlmaWVkX25hbWUYAiABKAkSDgoGZGlnZXN0GAMgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAQgASgDInUKIE1vZHVsZUN1cnJlbnRJbnN0YWxsYXRpb25SZXF1ZXN0Ei4KGXBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4YASABKAlCC7pICHIGEAEYgIACEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEimwEKIU1vZHVsZUN1cnJlbnRJbnN0YWxsYXRpb25SZXNwb25zZRIXCg9pbnN0YWxsYXRpb25faWQYASABKAkSEQoJdGVuYW50X2lkGAIgASgJEhsKCXRhcmdldF9pZBgEIAEoCUIIukgFcgOwAQESEgoKYmluZGluZ19pZBgFIAEoCUoECAMQBFITc29sdXRpb25faWRlbnRpZmllciJgChdXb3JrQ29udGV4dFNlYWxRdWVzdGlvbhIgCgxwcmluY2lwYWxfaWQYASABKAlCCrpIB3IFEAEYgAQSIwoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgq6SAdyBRABGIAEIkUKIVdvcmtDb250ZXh0UHJpbmNpcGFsRXBvY2hRdWVzdGlvbhIgCgxwcmluY2lwYWxfaWQYASABKAlCCrpIB3IFEAEYgAQiRAogV29ya0NvbnRleHRBcHByb3ZlZEJ1aWxkUXVlc3Rpb24SIAoMcHJpbmNpcGFsX2lkGAEgASgJQgq6SAdyBRABGIAEIkUKI1dvcmtDb250ZXh0T3BlcmF0aW9uQmluZGluZ1F1ZXN0aW9uEh4KCmJpbmRpbmdfaWQYASABKAlCCrpIB3IFEAEYgAQiUgooV29ya0NvbnRleHRBdXRob3JpemF0aW9uUmV2aXNpb25RdWVzdGlvbhImChJvd25lcl9wcmluY2lwYWxfaWQYASABKAlCCrpIB3IFEAEYgAQiQAogV29ya0NvbnRleHRBcHByb3ZhbEdyYW50UXVlc3Rpb24SHAoIZ3JhbnRfaWQYASABKAlCCrpIB3IFEAEYgAIihgEKFVdvcmtDb250ZXh0U2VhbEFuc3dlchI1CgVzdGF0ZRgBIAEoDjImLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRTZWFsU3RhdGUSFwoPaW5zdGFsbGF0aW9uX2lkGAIgASgJEh0KFWluc3RhbGxhdGlvbl9yZXZpc2lvbhgDIAEoBCKCAQofV29ya0NvbnRleHRQcmluY2lwYWxFcG9jaEFuc3dlchI6CgVzdGF0ZRgBIAEoDjIrLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRQcmluY2lwYWxTdGF0ZRIUCgxwcmluY2lwYWxfaWQYAiABKAkSDQoFZXBvY2gYAyABKAQipwEKHldvcmtDb250ZXh0QXBwcm92ZWRCdWlsZEFuc3dlchI+CgVzdGF0ZRgBIAEoDjIvLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRBcHByb3ZlZEJ1aWxkU3RhdGUSFAoMcHJpbmNpcGFsX2lkGAIgASgJEhQKDGltYWdlX2RpZ2VzdBgDIAEoCRIZChFidWlsZF9pbmNhcm5hdGlvbhgEIAEoBCLhAQohV29ya0NvbnRleHRPcGVyYXRpb25CaW5kaW5nQW5zd2VyEkEKBXN0YXRlGAEgASgOMjIuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dE9wZXJhdGlvbkJpbmRpbmdTdGF0ZRISCgpiaW5kaW5nX2lkGAIgASgJEhQKDHByaW5jaXBhbF9pZBgDIAEoCRIXCg9pbnN0YWxsYXRpb25faWQYBCABKAkSEAoIcmV2aXNpb24YBSABKAQSEwoLaW5jYXJuYXRpb24YBiABKAQSDwoHcmV2b2tlZBgHIAEoCCJWCiZXb3JrQ29udGV4dEF1dGhvcml6YXRpb25SZXZpc2lvbkFuc3dlchIaChJvd25lcl9wcmluY2lwYWxfaWQYASABKAkSEAoIcmV2aXNpb24YAiABKAQicgoeV29ya0NvbnRleHRBcHByb3ZhbEdyYW50QW5zd2VyEj4KBXN0YXRlGAEgASgOMi8uc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dEFwcHJvdmFsR3JhbnRTdGF0ZRIQCghncmFudF9pZBgCIAEoCSLJBAooTW9kdWxlUmVzb2x2ZVdvcmtDb250ZXh0QXV0aG9yaXR5UmVxdWVzdBIhCgx3b3JrX2NvbnRleHQYASABKAlCC7pICHIGEAEYgIACEjkKBHNlYWwYAyABKAsyKS5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0U2VhbFF1ZXN0aW9uSAASTgoPcHJpbmNpcGFsX2Vwb2NoGAQgASgLMjMuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFByaW5jaXBhbEVwb2NoUXVlc3Rpb25IABJMCg5hcHByb3ZlZF9idWlsZBgFIAEoCzIyLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRBcHByb3ZlZEJ1aWxkUXVlc3Rpb25IABJSChFvcGVyYXRpb25fYmluZGluZxgGIAEoCzI1LnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRPcGVyYXRpb25CaW5kaW5nUXVlc3Rpb25IABJcChZhdXRob3JpemF0aW9uX3JldmlzaW9uGAcgASgLMjouc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dEF1dGhvcml6YXRpb25SZXZpc2lvblF1ZXN0aW9uSAASTAoOYXBwcm92YWxfZ3JhbnQYCCABKAsyMi5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0QXBwcm92YWxHcmFudFF1ZXN0aW9uSABCEQoIcXVlc3Rpb24SBbpIAggBSgQIAhADUghhdWRpZW5jZSKCBAopTW9kdWxlUmVzb2x2ZVdvcmtDb250ZXh0QXV0aG9yaXR5UmVzcG9uc2USNwoEc2VhbBgBIAEoCzInLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRTZWFsQW5zd2VySAASTAoPcHJpbmNpcGFsX2Vwb2NoGAIgASgLMjEuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFByaW5jaXBhbEVwb2NoQW5zd2VySAASSgoOYXBwcm92ZWRfYnVpbGQYAyABKAsyMC5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0QXBwcm92ZWRCdWlsZEFuc3dlckgAElAKEW9wZXJhdGlvbl9iaW5kaW5nGAQgASgLMjMuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dE9wZXJhdGlvbkJpbmRpbmdBbnN3ZXJIABJaChZhdXRob3JpemF0aW9uX3JldmlzaW9uGAUgASgLMjguc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dEF1dGhvcml6YXRpb25SZXZpc2lvbkFuc3dlckgAEkoKDmFwcHJvdmFsX2dyYW50GAYgASgLMjAuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dEFwcHJvdmFsR3JhbnRBbnN3ZXJIAEIICgZhbnN3ZXIquAIKFE1vZHVsZUF1ZGl0RmllbGRLaW5kEicKI01PRFVMRV9BVURJVF9GSUVMRF9LSU5EX1VOU1BFQ0lGSUVEEAASIgoeTU9EVUxFX0FVRElUX0ZJRUxEX0tJTkRfU1RSSU5HEAESIAocTU9EVUxFX0FVRElUX0ZJRUxEX0tJTkRfVVVJRBACEh8KG01PRFVMRV9BVURJVF9GSUVMRF9LSU5EX0lOVBADEiIKHk1PRFVMRV9BVURJVF9GSUVMRF9LSU5EX05VTUJFUhAEEiAKHE1PRFVMRV9BVURJVF9GSUVMRF9LSU5EX0JPT0wQBRIgChxNT0RVTEVfQVVESVRfRklFTERfS0lORF9FTlVNEAYSKAokTU9EVUxFX0FVRElUX0ZJRUxEX0tJTkRfU1RSSU5HX0FSUkFZEAcqoQEKGk1vZHVsZUF1ZGl0RXZlbnRWaXNpYmlsaXR5Ei0KKU1PRFVMRV9BVURJVF9FVkVOVF9WSVNJQklMSVRZX1VOU1BFQ0lGSUVEEAASKAokTU9EVUxFX0FVRElUX0VWRU5UX1ZJU0lCSUxJVFlfVEVOQU5UEAESKgomTU9EVUxFX0FVRElUX0VWRU5UX1ZJU0lCSUxJVFlfRVhURVJOQUwQAircAQoaRGF0YXNvdXJjZUl0ZW1SZWFkZXJzQmFzaXMSLQopREFUQVNPVVJDRV9JVEVNX1JFQURFUlNfQkFTSVNfVU5TUEVDSUZJRUQQABIsCihEQVRBU09VUkNFX0lURU1fUkVBREVSU19CQVNJU19UUkFOU0xBVEVEEAESLworREFUQVNPVVJDRV9JVEVNX1JFQURFUlNfQkFTSVNfU09VUkNFX1NDT1BFRBACEjAKLERBVEFTT1VSQ0VfSVRFTV9SRUFERVJTX0JBU0lTX1VOVFJBTlNMQVRBQkxFEAMqaQoNRXZlbnREZWxpdmVyeRIeChpFVkVOVF9ERUxJVkVSWV9VTlNQRUNJRklFRBAAEhwKGEVWRU5UX0RFTElWRVJZX1VOT1JERVJFRBABEhoKFkVWRU5UX0RFTElWRVJZX09SREVSRUQQAip+ChJNZXRhZGF0YURpc2Nsb3N1cmUSIwofTUVUQURBVEFfRElTQ0xPU1VSRV9VTlNQRUNJRklFRBAAEiEKHU1FVEFEQVRBX0RJU0NMT1NVUkVfRElTQ0xPU0VEEAESIAocTUVUQURBVEFfRElTQ0xPU1VSRV9XSVRISEVMRBACKlwKD1NvdXJjZVN5bmNTdGFnZRIhCh1TT1VSQ0VfU1lOQ19TVEFHRV9VTlNQRUNJRklFRBAAEiYKIlNPVVJDRV9TWU5DX1NUQUdFX0NIQU5HRVNfRU5RVUVVRUQQASqFAQoUV29ya0NvbnRleHRTZWFsU3RhdGUSJwojV09SS19DT05URVhUX1NFQUxfU1RBVEVfVU5TUEVDSUZJRUQQABIgChxXT1JLX0NPTlRFWFRfU0VBTF9TVEFURV9IRUxEEAESIgoeV09SS19DT05URVhUX1NFQUxfU1RBVEVfVU5IRUxEEAIqmwEKGVdvcmtDb250ZXh0UHJpbmNpcGFsU3RhdGUSLAooV09SS19DT05URVhUX1BSSU5DSVBBTF9TVEFURV9VTlNQRUNJRklFRBAAEiYKIldPUktfQ09OVEVYVF9QUklOQ0lQQUxfU1RBVEVfS05PV04QARIoCiRXT1JLX0NPTlRFWFRfUFJJTkNJUEFMX1NUQVRFX1VOS05PV04QAiq8AQodV29ya0NvbnRleHRBcHByb3ZlZEJ1aWxkU3RhdGUSMQotV09SS19DT05URVhUX0FQUFJPVkVEX0JVSUxEX1NUQVRFX1VOU1BFQ0lGSUVEEAASLgoqV09SS19DT05URVhUX0FQUFJPVkVEX0JVSUxEX1NUQVRFX0FQUFJPVkVEEAESOAo0V09SS19DT05URVhUX0FQUFJPVkVEX0JVSUxEX1NUQVRFX0JFQVJTX05PX0VYRUNVVElPThACKrwBCiBXb3JrQ29udGV4dE9wZXJhdGlvbkJpbmRpbmdTdGF0ZRI0CjBXT1JLX0NPTlRFWFRfT1BFUkFUSU9OX0JJTkRJTkdfU1RBVEVfVU5TUEVDSUZJRUQQABIxCi1XT1JLX0NPTlRFWFRfT1BFUkFUSU9OX0JJTkRJTkdfU1RBVEVfUkVTT0xWRUQQARIvCitXT1JLX0NPTlRFWFRfT1BFUkFUSU9OX0JJTkRJTkdfU1RBVEVfQUJTRU5UEAIqhgEKHVdvcmtDb250ZXh0QXBwcm92YWxHcmFudFN0YXRlEjEKLVdPUktfQ09OVEVYVF9BUFBST1ZBTF9HUkFOVF9TVEFURV9VTlNQRUNJRklFRBAAEjIKLldPUktfQ09OVEVYVF9BUFBST1ZBTF9HUkFOVF9TVEFURV9OT1RfUkVDT1JERUQQATLgJwoZTW9kdWxlQ2FwYWJpbGl0aWVzU2VydmljZRKbAQoWR2V0Q3VycmVudEluc3RhbGxhdGlvbhIyLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlQ3VycmVudEluc3RhbGxhdGlvblJlcXVlc3QaMy5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUN1cnJlbnRJbnN0YWxsYXRpb25SZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUARYA2ABEsYBChlBcHByb3ZlRXhlY3V0YWJsZUFydGlmYWN0EjEuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFeGVjdXRhYmxlQXJ0aWZhY3RSZXF1ZXN0GjIuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFeGVjdXRhYmxlQXJ0aWZhY3RSZXNwb25zZSJCwvMYPggDEAEwATosCihzYWFzLm1vZHVsZS5leGVjdXRhYmxlX2FydGlmYWN0X2RlY2lzaW9uEAJAAUgHUARYAmABEp4BChtBdXRob3JpemVFeGVjdXRhYmxlQXJ0aWZhY3QSMS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUV4ZWN1dGFibGVBcnRpZmFjdFJlcXVlc3QaMi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUV4ZWN1dGFibGVBcnRpZmFjdFJlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQBFgCYAESywEKGFJldm9rZUV4ZWN1dGFibGVBcnRpZmFjdBI3LnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUmV2b2tlRXhlY3V0YWJsZUFydGlmYWN0UmVxdWVzdBoyLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlRXhlY3V0YWJsZUFydGlmYWN0UmVzcG9uc2UiQsLzGD4IAxABMAE6LAooc2Fhcy5tb2R1bGUuZXhlY3V0YWJsZV9hcnRpZmFjdF9kZWNpc2lvbhACQAFIB1AEWAJgARLFAQodRXhjaGFuZ2VEZWxlZ2F0ZWRSZWFkQXVkaWVuY2USPC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUV4Y2hhbmdlRGVsZWdhdGVkUmVhZEF1ZGllbmNlUmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuSXNzdWVkV29ya0NvbnRleHQiQcLzGD0IAxABMAE6Kwonc2Fhcy5tb2R1bGUuZGVsZWdhdGVkX2F1ZGllbmNlX2V4Y2hhbmdlEARAAUgHUARYBGABEs8BCiJFeGNoYW5nZURlbGVnYXRlZE9wZXJhdGlvbkF1ZGllbmNlEkEuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFeGNoYW5nZURlbGVnYXRlZE9wZXJhdGlvbkF1ZGllbmNlUmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuSXNzdWVkV29ya0NvbnRleHQiQcLzGD0IAxABMAE6Kwonc2Fhcy5tb2R1bGUuZGVsZWdhdGVkX2F1ZGllbmNlX2V4Y2hhbmdlEARAAUgHUARYBGABEqcBChxDaGVja1dvcmtDb250ZXh0UmVjb3JkQWNjZXNzEjUuc2Fhcy5hY2NvdW50cy52MS5DaGVja1dvcmtDb250ZXh0UmVjb3JkQWNjZXNzUmVxdWVzdBo2LnNhYXMuYWNjb3VudHMudjEuQ2hlY2tXb3JrQ29udGV4dFJlY29yZEFjY2Vzc1Jlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESqgEKHUxpc3RSZWFkYWJsZVNvdXJjZUNvbGxlY3Rpb25zEjYuc2Fhcy5hY2NvdW50cy52MS5MaXN0UmVhZGFibGVTb3VyY2VDb2xsZWN0aW9uc1JlcXVlc3QaNy5zYWFzLmFjY291bnRzLnYxLkxpc3RSZWFkYWJsZVNvdXJjZUNvbGxlY3Rpb25zUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARKAAQoLUGxhY2VSZWNvcmQSKi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVBsYWNlUmVjb3JkUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUGxhY2VSZWNvcmRSZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEn0KCkVucXVldWVKb2ISKS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUVucXVldWVKb2JSZXF1ZXN0Giouc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFbnF1ZXVlSm9iUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARJ6CglDbGFpbUpvYnMSKC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUNsYWltSm9ic1JlcXVlc3QaKS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUNsYWltSm9ic1Jlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESgwEKDEhlYXJ0YmVhdEpvYhIrLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlSGVhcnRiZWF0Sm9iUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlSGVhcnRiZWF0Sm9iUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARJhCgZBY2tKb2ISJS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUFja0pvYlJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARJjCgdOYWNrSm9iEiYuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVOYWNrSm9iUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEn0KCk5vdGlmeVVzZXISKS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZU5vdGlmeVVzZXJSZXF1ZXN0Giouc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVOb3RpZnlVc2VyUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARKtAQoPTm90aWZ5T3JnQWRtaW5zEi4uc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVOb3RpZnlPcmdBZG1pbnNSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVOb3RpZnlPcmdBZG1pbnNSZXNwb25zZSI5wvMYNQgDEAEwATojCh9zYWFzLm1vZHVsZS5vcmdfYWRtaW5zX25vdGlmaWVkEAJAAUgHUANYA2ABEowBCg9SZXF1ZXN0QXBwcm92YWwSLi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVJlcXVlc3RBcHByb3ZhbFJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVJlcXVlc3RBcHByb3ZhbFJlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESdQoLR2V0QXBwcm92YWwSKi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUdldEFwcHJvdmFsUmVxdWVzdBogLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlQXBwcm92YWwiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARJxCg5DYW5jZWxBcHByb3ZhbBItLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlQ2FuY2VsQXBwcm92YWxSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAEScQoORW1pdEF1ZGl0RXZlbnQSLS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUVtaXRBdWRpdEV2ZW50UmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEsMBChZEZWNsYXJlQXVkaXRFdmVudFR5cGVzEjUuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVEZWNsYXJlQXVkaXRFdmVudFR5cGVzUmVxdWVzdBo2LnNhYXMuYWNjb3VudHMudjEuTW9kdWxlRGVjbGFyZUF1ZGl0RXZlbnRUeXBlc1Jlc3BvbnNlIjrC8xg2CAMQATABOiQKIHNhYXMubW9kdWxlLmF1ZGl0X3R5cGVzX2RlY2xhcmVkEAJAAUgHUANYA2ABEp4BChVMaXN0U3ViamVjdFZpc2liaWxpdHkSNC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUxpc3RTdWJqZWN0VmlzaWJpbGl0eVJlcXVlc3QaNS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUxpc3RTdWJqZWN0VmlzaWJpbGl0eVJlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESjgEKE0ZldGNoRGF0YXNvdXJjZUJsb2ISLC5zYWFzLmFjY291bnRzLnYxLkZldGNoRGF0YXNvdXJjZUJsb2JSZXF1ZXN0Giouc2Fhcy5hY2NvdW50cy52MS5GZXRjaERhdGFzb3VyY2VCbG9iQ2h1bmsiG4gCAcLzGBQIAxABMAE6AhABQAFIB1ADWANgATABEo4BChRGZXRjaERhdGFzb3VyY2VGaWxlcxItLnNhYXMuYWNjb3VudHMudjEuRmV0Y2hEYXRhc291cmNlRmlsZXNSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5GZXRjaERhdGFzb3VyY2VGaWxlc0ZyYW1lIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAEwARKzAQoVTWludE1vZHVsZVdvcmtDb250ZXh0Ei4uc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVNaW50V29ya0NvbnRleHRSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVNaW50V29ya0NvbnRleHRSZXNwb25zZSI5wvMYNQgDEAEwATojCh9zYWFzLm1vZHVsZS53b3JrX2NvbnRleHRfbWludGVkEAJAAUgHUARYBGABEscBChpNaW50TW9kdWxlT3BlcmF0aW9uQ29udGV4dBIzLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlTWludE9wZXJhdGlvbkNvbnRleHRSZXF1ZXN0GjQuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVNaW50T3BlcmF0aW9uQ29udGV4dFJlc3BvbnNlIj7C8xg6CAMQATABOigKJHNhYXMubW9kdWxlLm9wZXJhdGlvbl9jb250ZXh0X21pbnRlZBACQAFIB1AEWARgARLOAQoaTWludFNvdXJjZU9wZXJhdGlvbkNvbnRleHQSOS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZU1pbnRTb3VyY2VPcGVyYXRpb25Db250ZXh0UmVxdWVzdBo6LnNhYXMuYWNjb3VudHMudjEuTW9kdWxlTWludFNvdXJjZU9wZXJhdGlvbkNvbnRleHRSZXNwb25zZSI5wvMYNQgDEAEwATojCh9zYWFzLmRhdGFzb3VyY2UuZGVsZWdhdGlvbi51c2VkEAJAAUgHUARYBGABEoMBCgxQdWJsaXNoRXZlbnQSKy5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVB1Ymxpc2hFdmVudFJlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVB1Ymxpc2hFdmVudFJlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESegoJU3Vic2NyaWJlEiguc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVTdWJzY3JpYmVSZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVTdWJzY3JpYmVSZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEmsKC1Vuc3Vic2NyaWJlEiouc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVVbnN1YnNjcmliZVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARKSAQoRTGlzdFN1YnNjcmlwdGlvbnMSMC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUxpc3RTdWJzY3JpcHRpb25zUmVxdWVzdBoxLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlTGlzdFN1YnNjcmlwdGlvbnNSZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEoMBCgxSZXBsYXlFdmVudHMSKy5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVJlcGxheUV2ZW50c1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVJlcGxheUV2ZW50c1Jlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESsAEKG1Jlc29sdmVXb3JrQ29udGV4dEF1dGhvcml0eRI6LnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUmVzb2x2ZVdvcmtDb250ZXh0QXV0aG9yaXR5UmVxdWVzdBo7LnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUmVzb2x2ZVdvcmtDb250ZXh0QXV0aG9yaXR5UmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgAWIGcHJvdG8z", [file_saas_accounts_v1_work_contexts, file_buf_validate_validate, file_google_protobuf_empty, file_google_protobuf_duration, file_google_protobuf_struct, file_google_protobuf_timestamp, file_saas_accounts_v1_module_registration, file_saas_events_v1_events, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); + fileDesc("CipzYWFzL2FjY291bnRzL3YxL21vZHVsZV9jYXBhYmlsaXRpZXMucHJvdG8SEHNhYXMuYWNjb3VudHMudjEiXgoXTW9kdWxlRW5xdWV1ZUpvYlJlcXVlc3QSGAoGdGVuYW50GAEgASgJQgi6SAVyA7ABARIpCgNqb2IYAiABKAsyFC5zYWFzLmpvYnMudjEuTmV3Sm9iQga6SAPIAQEidgoYTW9kdWxlRW5xdWV1ZUpvYlJlc3BvbnNlEhgKBmpvYl9pZBgBIAEoCUIIukgFcgOwAQESQAoLZGlzcG9zaXRpb24YAiABKA4yIy5zYWFzLmpvYnMudjEuSm9iRW5xdWV1ZURpc3Bvc2l0aW9uQga6SAPIAQEiygEKFk1vZHVsZUNsYWltSm9ic1JlcXVlc3QSLgoFcXVldWUYASABKAlCH7pIHHIaEAEYgAEyE15bYS16XVthLXowLTlfLi1dKiQSHQoJd29ya2VyX2lkGAIgASgJQgq6SAdyBRABGP8BEhgKBWxpbWl0GAMgASgNQgm6SAYqBBhkKAESRwoObGVhc2VfZHVyYXRpb24YBCABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25CFLpIEcgBAaoBCyIDCJAcMgQQwIQ9IkIKF01vZHVsZUNsYWltSm9ic1Jlc3BvbnNlEicKBGpvYnMYASADKAsyGS5zYWFzLmpvYnMudjEuSm9iRW52ZWxvcGUilwEKGU1vZHVsZUhlYXJ0YmVhdEpvYlJlcXVlc3QSNgoFbGVhc2UYASABKAsyHy5zYWFzLmpvYnMudjEuSm9iTGVhc2VSZWZlcmVuY2VCBrpIA8gBARJCCglleHRlbnNpb24YAiABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25CFLpIEcgBAaoBCyIDCJAcMgQQwIQ9IksKGk1vZHVsZUhlYXJ0YmVhdEpvYlJlc3BvbnNlEi0KBWxlYXNlGAEgASgLMhYuc2Fhcy5qb2JzLnYxLkpvYkxlYXNlQga6SAPIAQEipgEKE01vZHVsZUFja0pvYlJlcXVlc3QSNgoFbGVhc2UYASABKAsyHy5zYWFzLmpvYnMudjEuSm9iTGVhc2VSZWZlcmVuY2VCBrpIA8gBARI3Cg5leGVjdXRpb25fa2luZBgCIAEoCUIfukgcchoYQDIWXiR8XlthLXpdW2EtejAtOV8uLV0qJBIeCgxleGVjdXRpb25faWQYAyABKAlCCLpIBXIDGP8BIsIBChRNb2R1bGVOYWNrSm9iUmVxdWVzdBI2CgVsZWFzZRgBIAEoCzIfLnNhYXMuam9icy52MS5Kb2JMZWFzZVJlZmVyZW5jZUIGukgDyAEBEjEKB2ZhaWx1cmUYAiABKAsyGC5zYWFzLmpvYnMudjEuSm9iRmFpbHVyZUIGukgDyAEBEhEKCXJldHJ5YWJsZRgDIAEoCBIsCghyZXRyeV9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAi9gEKF01vZHVsZU5vdGlmeVVzZXJSZXF1ZXN0EhgKBnRlbmFudBgBIAEoCUIIukgFcgOwAQESGQoHdXNlcl9pZBgCIAEoCUIIukgFcgOwAQESGQoFdGl0bGUYAyABKAlCCrpIB3IFEAEY5gcSFgoEYm9keRgEIAEoCUIIukgFcgMYgEASFQoEdHlwZRgFIAEoCUIHukgEcgIYQBIcCgphY3Rpb25fdXJsGAYgASgJQgi6SAVyAxiAEBIbCghjYXRlZ29yeRgHIAEoCUIJukgGcgQQARggEiEKD2lkZW1wb3RlbmN5X2tleRgIIAEoCUIIukgFcgMY/wEiRgoYTW9kdWxlTm90aWZ5VXNlclJlc3BvbnNlEhcKD25vdGlmaWNhdGlvbl9pZBgBIAEoCRIRCglkZWxpdmVyZWQYAiABKAgi4AEKHE1vZHVsZU5vdGlmeU9yZ0FkbWluc1JlcXVlc3QSGAoGdGVuYW50GAEgASgJQgi6SAVyA7ABARIZCgV0aXRsZRgCIAEoCUIKukgHcgUQARjmBxIWCgRib2R5GAMgASgJQgi6SAVyAxiAQBIVCgR0eXBlGAQgASgJQge6SARyAhhAEhwKCmFjdGlvbl91cmwYBSABKAlCCLpIBXIDGIAQEhsKCGNhdGVnb3J5GAYgASgJQgm6SAZyBBABGCASIQoPaWRlbXBvdGVuY3lfa2V5GAcgASgJQgi6SAVyAxj/ASIyCh1Nb2R1bGVOb3RpZnlPcmdBZG1pbnNSZXNwb25zZRIRCglkZWxpdmVyZWQYASABKAgiYwoUTW9kdWxlQXBwcm92YWxQb2xpY3kSFwoGcXVvcnVtGAEgASgNQge6SAQqAhhkEh4KDGFwcHJvdmVyX3NldBgCIAMoCUIIukgFkgECEGQSEgoKYWxsb3dfc2VsZhgDIAEoCCKbAQoPTW9kdWxlUmVzdW1lUmVmEi4KBXF1ZXVlGAEgASgJQh+6SBxyGhABGIABMhNeW2Etel1bYS16MC05Xy4tXSokEi4KBXRvcGljGAIgASgJQh+6SBxyGhABGP8BMhNeW2Etel1bYS16MC05Xy4tXSokEigKB3BheWxvYWQYAyABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0IpYDChxNb2R1bGVSZXF1ZXN0QXBwcm92YWxSZXF1ZXN0EhgKBnRlbmFudBgBIAEoCUIIukgFcgOwAQESHAoIcmVzb3VyY2UYAiABKAlCCrpIB3IFEAEYgAESGgoGYWN0aW9uGAMgASgJQgq6SAdyBRABGIABEigKB3N1YmplY3QYBCABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EiAKDHJlcXVlc3RlZF9ieRgFIAEoCUIKukgHcgUQARj/ARI2CgZwb2xpY3kYBiABKAsyJi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUFwcHJvdmFsUG9saWN5Ej0KCnJlc3VtZV9yZWYYByABKAsyIS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVJlc3VtZVJlZkIGukgDyAEBEi4KCmV4cGlyZXNfYXQYCCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC2VzY2FsYXRlX2F0GAkgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCI+Ch1Nb2R1bGVSZXF1ZXN0QXBwcm92YWxSZXNwb25zZRIdCgthcHByb3ZhbF9pZBgBIAEoCUIIukgFcgOwAQEiUwoYTW9kdWxlR2V0QXBwcm92YWxSZXF1ZXN0EhgKBnRlbmFudBgBIAEoCUIIukgFcgOwAQESHQoLYXBwcm92YWxfaWQYAiABKAlCCLpIBXIDsAEBIvUCCg5Nb2R1bGVBcHByb3ZhbBIKCgJpZBgBIAEoCRIOCgZ0ZW5hbnQYAiABKAkSEAoIcmVzb3VyY2UYAyABKAkSDgoGYWN0aW9uGAQgASgJEigKB3N1YmplY3QYBSABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EhQKDHJlcXVlc3RlZF9ieRgGIAEoCRIOCgZxdW9ydW0YByABKA0SDQoFc3RhdGUYCCABKAkSNQoKcmVzdW1lX3JlZhgJIAEoCzIhLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUmVzdW1lUmVmEi4KCmV4cGlyZXNfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC2VzY2FsYXRlX2F0GAsgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIuCgpjcmVhdGVkX2F0GAwgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCJwChtNb2R1bGVDYW5jZWxBcHByb3ZhbFJlcXVlc3QSGAoGdGVuYW50GAEgASgJQgi6SAVyA7ABARIdCgthcHByb3ZhbF9pZBgCIAEoCUIIukgFcgOwAQESGAoGcmVhc29uGAMgASgJQgi6SAVyAxiACCLuAQobTW9kdWxlRW1pdEF1ZGl0RXZlbnRSZXF1ZXN0Eg4KBnRlbmFudBgBIAEoCRIeCgpldmVudF90eXBlGAIgASgJQgq6SAdyBRABGIABEhkKBWFjdG9yGAMgASgJQgq6SAdyBRABGP8BEhwKCHNvbHV0aW9uGAQgASgJQgq6SAdyBRABGIABEhoKCGVudHJ5X2lkGAUgASgJQgi6SAVyAxj/ARInCgZmaWVsZHMYBiABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EiEKD2lkZW1wb3RlbmN5X2tleRgHIAEoCUIIukgFcgMY/wEinAEKGE1vZHVsZVJlYWRKb3VybmFsUmVxdWVzdBIWCgVhZnRlchgBIAEoCUIHukgEcgIYQBIWCgVsaW1pdBgCIAEoBUIHukgEGgIoABIiCgV0eXBlcxgDIAMoCUITukgQkgENCAEQECIHcgUQARiAAhIsChFkZWxpdmVyeV9iaW5kaW5ncxgEIAMoCUIRukgOkgELEEAiB3IFEAEYgAIi0wEKEk1vZHVsZUpvdXJuYWxFbnRyeRIQCghwb3NpdGlvbhgBIAEoCRIQCghldmVudF9pZBgCIAEoCRIMCgR0eXBlGAMgASgJEhcKD29yZ2FuaXphdGlvbl9pZBgEIAEoCRIvCgtvY2N1cnJlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASJQoEZGF0YRgGIAEoCzIXLmdvb2dsZS5wcm90b2J1Zi5TdHJ1Y3QSGgoSZGVsaXZlcnlfbWFjaGluZXJ5GAcgASgIImYKGU1vZHVsZVJlYWRKb3VybmFsUmVzcG9uc2USNQoHZW50cmllcxgBIAMoCzIkLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlSm91cm5hbEVudHJ5EhIKCm5leHRfYWZ0ZXIYAiABKAkinwEKG01vZHVsZUF1ZGl0RmllbGREZWNsYXJhdGlvbhIXCgRuYW1lGAEgASgJQgm6SAZyBBABGEASQAoEa2luZBgCIAEoDjImLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlQXVkaXRGaWVsZEtpbmRCCrpIB4IBBBABIAASGAoGdmFsdWVzGAMgAygJQgi6SAWSAQIQQBILCgNwaWkYBCABKAgi7wEKH01vZHVsZUF1ZGl0RXZlbnRUeXBlRGVjbGFyYXRpb24SGAoEdHlwZRgBIAEoCUIKukgHcgUQARiAARIdCgtkZXNjcmlwdGlvbhgCIAEoCUIIukgFcgMYgAgSRwoGZmllbGRzGAMgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVBdWRpdEZpZWxkRGVjbGFyYXRpb25CCLpIBZIBAhAgEkoKCnZpc2liaWxpdHkYBCABKA4yLC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUF1ZGl0RXZlbnRWaXNpYmlsaXR5Qgi6SAWCAQIQASKzAQojTW9kdWxlRGVjbGFyZUF1ZGl0RXZlbnRUeXBlc1JlcXVlc3QSPgoGcHJlZml4GAEgASgJQi66SCtyKRABGIABMiJeW2EtejAtOV0oPzpbYS16MC05Xy1dKlthLXowLTldKT8kEkwKBXR5cGVzGAIgAygLMjEuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVBdWRpdEV2ZW50VHlwZURlY2xhcmF0aW9uQgq6SAeSAQQIARBAIl0KJE1vZHVsZURlY2xhcmVBdWRpdEV2ZW50VHlwZXNSZXNwb25zZRIWCg5hZG1pdHRlZF90eXBlcxgBIAMoCRIdChVuYW1lc3BhY2VzX3Rha2VuX292ZXIYAiADKAkibQoWU3ViamVjdFZpc2liaWxpdHlHcmFudBIjChJ2aXNpYmxlX3N1YmplY3RfaWQYASABKAlCB7pIBHICEAESLgoKZXhwaXJlc19hdBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiYwoiTW9kdWxlTGlzdFN1YmplY3RWaXNpYmlsaXR5UmVxdWVzdBIYCgZ0ZW5hbnQYASABKAlCCLpIBXIDsAEBEiMKEXZpZXdlcl9zdWJqZWN0X2lkGAIgASgJQgi6SAVyA7ABASJfCiNNb2R1bGVMaXN0U3ViamVjdFZpc2liaWxpdHlSZXNwb25zZRI4CgZncmFudHMYASADKAsyKC5zYWFzLmFjY291bnRzLnYxLlN1YmplY3RWaXNpYmlsaXR5R3JhbnQiVwoaRmV0Y2hEYXRhc291cmNlQmxvYlJlcXVlc3QSGwoJc291cmNlX2lkGAEgASgJQgi6SAVyA7ABARIcCghibG9iX3NoYRgCIAEoCUIKukgHcgUQARj/ASJSChhGZXRjaERhdGFzb3VyY2VCbG9iQ2h1bmsSDAoEZGF0YRgBIAEoDBISCgp0b3RhbF9zaXplGAIgASgDEhQKDGNvbnRlbnRfdHlwZRgDIAEoCSJSChFEYXRhc291cmNlRmlsZVJlZhIbCgdpdGVtX2lkGAEgASgJQgq6SAdyBRABGIAgEiAKDGl0ZW1fdmVyc2lvbhgCIAEoCUIKukgHcgUQARj/ASKYAQobRmV0Y2hEYXRhc291cmNlRmlsZXNSZXF1ZXN0EhsKCXNvdXJjZV9pZBgBIAEoCUIIukgFcgOwAQESGwoHdmVyc2lvbhgCIAEoCUIKukgHcgUQARj/ARI/CgVmaWxlcxgDIAMoCzIjLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZUZpbGVSZWZCC7pICJIBBQgBEOgHIosBChREYXRhc291cmNlUHJvdmVuYW5jZRIRCglzb3VyY2VfaWQYASABKAkSDgoGb3JnX2lkGAIgASgJEhgKEGJvdW5kYXJ5X25vZGVfaWQYAyABKAkSDwoHdmVyc2lvbhgEIAEoCRIPCgdpdGVtX2lkGAUgASgJEhQKDGl0ZW1fdmVyc2lvbhgGIAEoCSLAAQoVRGF0YXNvdXJjZUl0ZW1SZWFkZXJzEjsKBWJhc2lzGAEgASgOMiwuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlSXRlbVJlYWRlcnNCYXNpcxIQCgh1c2VyX2lkcxgCIAMoCRIRCglncm91cF9pZHMYAyADKAkSGAoQYm91bmRhcnlfcmVhZGVycxgEIAEoCBIWCg51bm1hcHBlZF9jb3VudBgFIAEoBRITCgthY2xfdmVyc2lvbhgGIAEoCSK+AQoURGF0YXNvdXJjZUZpbGVIZWFkZXISOgoKcHJvdmVuYW5jZRgBIAEoCzImLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVByb3ZlbmFuY2USDAoEcGF0aBgCIAEoCRIUCgxjb250ZW50X3R5cGUYAyABKAkSDAoEc2l6ZRgEIAEoAxI4CgdyZWFkZXJzGAUgASgLMicuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlSXRlbVJlYWRlcnMibgoZRmV0Y2hEYXRhc291cmNlRmlsZXNGcmFtZRI4CgZoZWFkZXIYASABKAsyJi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VGaWxlSGVhZGVySAASDgoEZGF0YRgCIAEoDEgAQgcKBWZyYW1lIm4KGU1vZHVsZVB1Ymxpc2hFdmVudFJlcXVlc3QSGAoGdGVuYW50GAEgASgJQgi6SAVyA7ABARI3CghlbnZlbG9wZRgCIAEoCzIdLnNhYXMuZXZlbnRzLnYxLkV2ZW50RW52ZWxvcGVCBrpIA8gBASI4ChpNb2R1bGVQdWJsaXNoRXZlbnRSZXNwb25zZRIaCghldmVudF9pZBgBIAEoCUIIukgFcgOwAQEiyAEKFk1vZHVsZVN1YnNjcmliZVJlcXVlc3QSSwoMdHlwZV9wYXR0ZXJuGAEgASgJQjW6SDJyMBABGIABMileW2Etel1bYS16MC05XSooPzpcLlthLXowLTldKykqKD86XC5cKik/JBIuCgVxdWV1ZRgCIAEoCUIfukgcchoQARiAATITXlthLXpdW2EtejAtOV8uLV0qJBIxCghkZWxpdmVyeRgDIAEoDjIfLnNhYXMuYWNjb3VudHMudjEuRXZlbnREZWxpdmVyeSLJAQoSTW9kdWxlU3Vic2NyaXB0aW9uEgoKAmlkGAEgASgJEh8KF3N1YnNjcmliZXJfcHJpbmNpcGFsX2lkGAIgASgJEhQKDHR5cGVfcGF0dGVybhgDIAEoCRINCgVxdWV1ZRgEIAEoCRIxCghkZWxpdmVyeRgFIAEoDjIfLnNhYXMuYWNjb3VudHMudjEuRXZlbnREZWxpdmVyeRIuCgpjcmVhdGVkX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCJVChdNb2R1bGVTdWJzY3JpYmVSZXNwb25zZRI6CgxzdWJzY3JpcHRpb24YASABKAsyJC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVN1YnNjcmlwdGlvbiI9ChhNb2R1bGVVbnN1YnNjcmliZVJlcXVlc3QSIQoPc3Vic2NyaXB0aW9uX2lkGAEgASgJQgi6SAVyA7ABASIgCh5Nb2R1bGVMaXN0U3Vic2NyaXB0aW9uc1JlcXVlc3QiXgofTW9kdWxlTGlzdFN1YnNjcmlwdGlvbnNSZXNwb25zZRI7Cg1zdWJzY3JpcHRpb25zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVTdWJzY3JpcHRpb24iegoZTW9kdWxlUmVwbGF5RXZlbnRzUmVxdWVzdBIYCgZ0ZW5hbnQYASABKAlCCLpIBXIDsAEBEhgKBHR5cGUYAiABKAlCCrpIB3IFEAEYgAESKQoFc2luY2UYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIjEKGk1vZHVsZVJlcGxheUV2ZW50c1Jlc3BvbnNlEhMKC3JlZGVsaXZlcmVkGAEgASgFItMCChhSZWFkYWJsZVNvdXJjZUNvbGxlY3Rpb24SEQoJc291cmNlX2lkGAEgASgJEhMKC2JvdW5kYXJ5X2lkGAIgASgJEg4KBm9yaWdpbhgDIAEoCRIRCgljb250YWluZXIYBCABKAkSCwoDcmVmGAUgASgJEg0KBXBhdGhzGAYgAygJEhYKDmJvdW5kYXJ5X2xhYmVsGAcgASgJEj4KEGdyYW50X2Rpc2Nsb3N1cmUYCCABKA4yJC5zYWFzLmFjY291bnRzLnYxLk1ldGFkYXRhRGlzY2xvc3VyZRI+CgtyZWFkX2dyYW50cxgJIAMoCzIpLnNhYXMuYWNjb3VudHMudjEuUmVhZGFibGVDb2xsZWN0aW9uR3JhbnQSOAoEc3luYxgKIAEoCzIqLnNhYXMuYWNjb3VudHMudjEuQ29sbGVjdGlvblN5bmNQcm92ZW5hbmNlIrABChdSZWFkYWJsZUNvbGxlY3Rpb25HcmFudBIVCg1zdWJqZWN0X2xhYmVsGAEgASgJEhQKDHN1YmplY3Rfa2luZBgCIAEoCRIRCglyb2xlX25hbWUYAyABKAkSEgoKc2NvcGVfcGF0aBgEIAEoCRIRCglpbmhlcml0ZWQYBSABKAgSLgoKZXhwaXJlc19hdBgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiqQIKGENvbGxlY3Rpb25TeW5jUHJvdmVuYW5jZRIwCgVzdGFnZRgBIAEoDjIhLnNhYXMuYWNjb3VudHMudjEuU291cmNlU3luY1N0YWdlEiYKAmF0GAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIQCghyZXZpc2lvbhgDIAEoCRIPCgd0cmlnZ2VyGAQgASgJEkIKFHJlcXVlc3Rlcl9kaXNjbG9zdXJlGAUgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5NZXRhZGF0YURpc2Nsb3N1cmUSMAoMcmVxdWVzdGVkX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIaChJyZXF1ZXN0ZWRfYnlfbGFiZWwYByABKAkikwEKJExpc3RSZWFkYWJsZVNvdXJjZUNvbGxlY3Rpb25zUmVxdWVzdBIdCglwYWdlX3NpemUYASABKAVCCrpIBxoFGOgHKAASHAoKcGFnZV90b2tlbhgCIAEoCUIIukgFcgMYgBASLgoZdmlld2VyX3dvcmtfY29udGV4dF90b2tlbhgDIAEoCUILukgIcgYQARiAgAIigQEKJUxpc3RSZWFkYWJsZVNvdXJjZUNvbGxlY3Rpb25zUmVzcG9uc2USPwoLY29sbGVjdGlvbnMYASADKAsyKi5zYWFzLmFjY291bnRzLnYxLlJlYWRhYmxlU291cmNlQ29sbGVjdGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAki6QEKGE1vZHVsZVBsYWNlUmVjb3JkUmVxdWVzdBIYCgZ0ZW5hbnQYASABKAlCCLpIBXIDsAEBEj0KCnNjb3BlX3BhdGgYAiABKAlCKbpIJnIkEAEYgAQyHV5bYS16MC05X10rKD86XC5bYS16MC05X10rKSokEhcKBGtpbmQYAyABKAlCCbpIBnIEEAEYQBIXCgVsYWJlbBgEIAEoCUIIukgFcgMY/wESIQoNcmVzb3VyY2VfdHlwZRgFIAEoCUIKukgHcgUQARiAARIfCgtyZXNvdXJjZV9pZBgGIAEoCUIKukgHcgUQARiABCI2ChlNb2R1bGVQbGFjZVJlY29yZFJlc3BvbnNlEhkKB25vZGVfaWQYASABKAlCCLpIBXIDsAEBIrUBCiNDaGVja1dvcmtDb250ZXh0UmVjb3JkQWNjZXNzUmVxdWVzdBIhCg1yZXNvdXJjZV90eXBlGAEgASgJQgq6SAdyBRABGIACEh8KC3Jlc291cmNlX2lkGAIgASgJQgq6SAdyBRABGIAEEhoKBmFjdGlvbhgDIAEoCUIKukgHcgUQARiAAhIuChl2aWV3ZXJfd29ya19jb250ZXh0X3Rva2VuGAQgASgJQgu6SAhyBhABGICAAiJOCiRDaGVja1dvcmtDb250ZXh0UmVjb3JkQWNjZXNzUmVzcG9uc2USDwoHYWxsb3dlZBgBIAEoCBIVCg1zY29wZV9ub2RlX2lkGAIgASgJInwKKk1vZHVsZUV4Y2hhbmdlRGVsZWdhdGVkUmVhZEF1ZGllbmNlUmVxdWVzdBIeCgpiaW5kaW5nX2lkGAEgASgJQgq6SAdyBRABGIABEi4KGXBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4YAiABKAlCC7pICHIGEAEYgIACIoADCi9Nb2R1bGVFeGNoYW5nZURlbGVnYXRlZE9wZXJhdGlvbkF1ZGllbmNlUmVxdWVzdBIeCgpiaW5kaW5nX2lkGAEgASgJQgq6SAdyBRABGIABEiwKGXBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4YAiABKAlCCbpIBnIEGICAAhIOCgZsb29rdXAYAyABKAgSIgoNZGVsZWdhdGlvbl9pZBgEIAEoCUILukgI2AEBcgOwAQE6ygG6SMYBGsMBCjNleGNoYW5nZV9kZWxlZ2F0ZWRfb3BlcmF0aW9uX2F1ZGllbmNlLm9uZV9hdXRob3JpdHkSRmV4YWN0bHkgb25lIG9mIHBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4gYW5kIGRlbGVnYXRpb25faWQgaXMgcmVxdWlyZWQaRCh0aGlzLnBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4gIT0gJycpICE9ICh0aGlzLmRlbGVnYXRpb25faWQgIT0gJycpIn0KGkV4ZWN1dGFibGVBcnRpZmFjdENvbnRyYWN0EhcKBGtpbmQYASABKAlCCbpIBnIEEAEYQBIYCgRuYW1lGAIgASgJQgq6SAdyBRABGIAEEiwKBmRpZ2VzdBgDIAEoCUIcukgZchcyFV5zaGEyNTY6WzAtOWEtZl17NjR9JCLiAQoaRXhlY3V0YWJsZUFydGlmYWN0SWRlbnRpdHkSGgoGc2NoZW1hGAEgASgJQgq6SAdyBRABGIABEhoKBnNvdXJjZRgCIAEoCUIKukgHcgUQARiABBIcCgdzdWJqZWN0GAMgASgMQgu6SAh6BhABGICABBJKCgljb250cmFjdHMYBCADKAsyLC5zYWFzLmFjY291bnRzLnYxLkV4ZWN1dGFibGVBcnRpZmFjdENvbnRyYWN0Qgm6SAaSAQMQgAISIgoRZXhwZWN0ZWRfcmV2aXNpb24YBSABKANCB7pIBCICKAAi2wEKH01vZHVsZUV4ZWN1dGFibGVBcnRpZmFjdFJlcXVlc3QSLgoZcGFyZW50X3dvcmtfY29udGV4dF90b2tlbhgBIAEoCUILukgIcgYQARiAgAISIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABARIdCglwb2xpY3lfaWQYAyABKAlCCrpIB3IFEAEYgAESRgoIaWRlbnRpdHkYBCABKAsyLC5zYWFzLmFjY291bnRzLnYxLkV4ZWN1dGFibGVBcnRpZmFjdElkZW50aXR5Qga6SAPIAQEimQEKJU1vZHVsZVJldm9rZUV4ZWN1dGFibGVBcnRpZmFjdFJlcXVlc3QSLgoZcGFyZW50X3dvcmtfY29udGV4dF90b2tlbhgBIAEoCUILukgIcgYQARiAgAISIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABARIdCgthcHByb3ZhbF9pZBgDIAEoCUIIukgFcgOwAQEifQogTW9kdWxlRXhlY3V0YWJsZUFydGlmYWN0UmVzcG9uc2USFgoOc2NoZW1hX3ZlcnNpb24YASABKAkSFgoOcXVhbGlmaWVkX25hbWUYAiABKAkSDgoGZGlnZXN0GAMgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAQgASgDInUKIE1vZHVsZUN1cnJlbnRJbnN0YWxsYXRpb25SZXF1ZXN0Ei4KGXBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4YASABKAlCC7pICHIGEAEYgIACEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEimwEKIU1vZHVsZUN1cnJlbnRJbnN0YWxsYXRpb25SZXNwb25zZRIXCg9pbnN0YWxsYXRpb25faWQYASABKAkSEQoJdGVuYW50X2lkGAIgASgJEhsKCXRhcmdldF9pZBgEIAEoCUIIukgFcgOwAQESEgoKYmluZGluZ19pZBgFIAEoCUoECAMQBFITc29sdXRpb25faWRlbnRpZmllciJgChdXb3JrQ29udGV4dFNlYWxRdWVzdGlvbhIgCgxwcmluY2lwYWxfaWQYASABKAlCCrpIB3IFEAEYgAQSIwoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgq6SAdyBRABGIAEIkUKIVdvcmtDb250ZXh0UHJpbmNpcGFsRXBvY2hRdWVzdGlvbhIgCgxwcmluY2lwYWxfaWQYASABKAlCCrpIB3IFEAEYgAQiRAogV29ya0NvbnRleHRBcHByb3ZlZEJ1aWxkUXVlc3Rpb24SIAoMcHJpbmNpcGFsX2lkGAEgASgJQgq6SAdyBRABGIAEIkUKI1dvcmtDb250ZXh0T3BlcmF0aW9uQmluZGluZ1F1ZXN0aW9uEh4KCmJpbmRpbmdfaWQYASABKAlCCrpIB3IFEAEYgAQiUgooV29ya0NvbnRleHRBdXRob3JpemF0aW9uUmV2aXNpb25RdWVzdGlvbhImChJvd25lcl9wcmluY2lwYWxfaWQYASABKAlCCrpIB3IFEAEYgAQiQAogV29ya0NvbnRleHRBcHByb3ZhbEdyYW50UXVlc3Rpb24SHAoIZ3JhbnRfaWQYASABKAlCCrpIB3IFEAEYgAIihgEKFVdvcmtDb250ZXh0U2VhbEFuc3dlchI1CgVzdGF0ZRgBIAEoDjImLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRTZWFsU3RhdGUSFwoPaW5zdGFsbGF0aW9uX2lkGAIgASgJEh0KFWluc3RhbGxhdGlvbl9yZXZpc2lvbhgDIAEoBCKCAQofV29ya0NvbnRleHRQcmluY2lwYWxFcG9jaEFuc3dlchI6CgVzdGF0ZRgBIAEoDjIrLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRQcmluY2lwYWxTdGF0ZRIUCgxwcmluY2lwYWxfaWQYAiABKAkSDQoFZXBvY2gYAyABKAQipwEKHldvcmtDb250ZXh0QXBwcm92ZWRCdWlsZEFuc3dlchI+CgVzdGF0ZRgBIAEoDjIvLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRBcHByb3ZlZEJ1aWxkU3RhdGUSFAoMcHJpbmNpcGFsX2lkGAIgASgJEhQKDGltYWdlX2RpZ2VzdBgDIAEoCRIZChFidWlsZF9pbmNhcm5hdGlvbhgEIAEoBCLhAQohV29ya0NvbnRleHRPcGVyYXRpb25CaW5kaW5nQW5zd2VyEkEKBXN0YXRlGAEgASgOMjIuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dE9wZXJhdGlvbkJpbmRpbmdTdGF0ZRISCgpiaW5kaW5nX2lkGAIgASgJEhQKDHByaW5jaXBhbF9pZBgDIAEoCRIXCg9pbnN0YWxsYXRpb25faWQYBCABKAkSEAoIcmV2aXNpb24YBSABKAQSEwoLaW5jYXJuYXRpb24YBiABKAQSDwoHcmV2b2tlZBgHIAEoCCJWCiZXb3JrQ29udGV4dEF1dGhvcml6YXRpb25SZXZpc2lvbkFuc3dlchIaChJvd25lcl9wcmluY2lwYWxfaWQYASABKAkSEAoIcmV2aXNpb24YAiABKAQicgoeV29ya0NvbnRleHRBcHByb3ZhbEdyYW50QW5zd2VyEj4KBXN0YXRlGAEgASgOMi8uc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dEFwcHJvdmFsR3JhbnRTdGF0ZRIQCghncmFudF9pZBgCIAEoCSLJBAooTW9kdWxlUmVzb2x2ZVdvcmtDb250ZXh0QXV0aG9yaXR5UmVxdWVzdBIhCgx3b3JrX2NvbnRleHQYASABKAlCC7pICHIGEAEYgIACEjkKBHNlYWwYAyABKAsyKS5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0U2VhbFF1ZXN0aW9uSAASTgoPcHJpbmNpcGFsX2Vwb2NoGAQgASgLMjMuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFByaW5jaXBhbEVwb2NoUXVlc3Rpb25IABJMCg5hcHByb3ZlZF9idWlsZBgFIAEoCzIyLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRBcHByb3ZlZEJ1aWxkUXVlc3Rpb25IABJSChFvcGVyYXRpb25fYmluZGluZxgGIAEoCzI1LnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRPcGVyYXRpb25CaW5kaW5nUXVlc3Rpb25IABJcChZhdXRob3JpemF0aW9uX3JldmlzaW9uGAcgASgLMjouc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dEF1dGhvcml6YXRpb25SZXZpc2lvblF1ZXN0aW9uSAASTAoOYXBwcm92YWxfZ3JhbnQYCCABKAsyMi5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0QXBwcm92YWxHcmFudFF1ZXN0aW9uSABCEQoIcXVlc3Rpb24SBbpIAggBSgQIAhADUghhdWRpZW5jZSKCBAopTW9kdWxlUmVzb2x2ZVdvcmtDb250ZXh0QXV0aG9yaXR5UmVzcG9uc2USNwoEc2VhbBgBIAEoCzInLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRTZWFsQW5zd2VySAASTAoPcHJpbmNpcGFsX2Vwb2NoGAIgASgLMjEuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFByaW5jaXBhbEVwb2NoQW5zd2VySAASSgoOYXBwcm92ZWRfYnVpbGQYAyABKAsyMC5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0QXBwcm92ZWRCdWlsZEFuc3dlckgAElAKEW9wZXJhdGlvbl9iaW5kaW5nGAQgASgLMjMuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dE9wZXJhdGlvbkJpbmRpbmdBbnN3ZXJIABJaChZhdXRob3JpemF0aW9uX3JldmlzaW9uGAUgASgLMjguc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dEF1dGhvcml6YXRpb25SZXZpc2lvbkFuc3dlckgAEkoKDmFwcHJvdmFsX2dyYW50GAYgASgLMjAuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dEFwcHJvdmFsR3JhbnRBbnN3ZXJIAEIICgZhbnN3ZXIquAIKFE1vZHVsZUF1ZGl0RmllbGRLaW5kEicKI01PRFVMRV9BVURJVF9GSUVMRF9LSU5EX1VOU1BFQ0lGSUVEEAASIgoeTU9EVUxFX0FVRElUX0ZJRUxEX0tJTkRfU1RSSU5HEAESIAocTU9EVUxFX0FVRElUX0ZJRUxEX0tJTkRfVVVJRBACEh8KG01PRFVMRV9BVURJVF9GSUVMRF9LSU5EX0lOVBADEiIKHk1PRFVMRV9BVURJVF9GSUVMRF9LSU5EX05VTUJFUhAEEiAKHE1PRFVMRV9BVURJVF9GSUVMRF9LSU5EX0JPT0wQBRIgChxNT0RVTEVfQVVESVRfRklFTERfS0lORF9FTlVNEAYSKAokTU9EVUxFX0FVRElUX0ZJRUxEX0tJTkRfU1RSSU5HX0FSUkFZEAcqoQEKGk1vZHVsZUF1ZGl0RXZlbnRWaXNpYmlsaXR5Ei0KKU1PRFVMRV9BVURJVF9FVkVOVF9WSVNJQklMSVRZX1VOU1BFQ0lGSUVEEAASKAokTU9EVUxFX0FVRElUX0VWRU5UX1ZJU0lCSUxJVFlfVEVOQU5UEAESKgomTU9EVUxFX0FVRElUX0VWRU5UX1ZJU0lCSUxJVFlfRVhURVJOQUwQAircAQoaRGF0YXNvdXJjZUl0ZW1SZWFkZXJzQmFzaXMSLQopREFUQVNPVVJDRV9JVEVNX1JFQURFUlNfQkFTSVNfVU5TUEVDSUZJRUQQABIsCihEQVRBU09VUkNFX0lURU1fUkVBREVSU19CQVNJU19UUkFOU0xBVEVEEAESLworREFUQVNPVVJDRV9JVEVNX1JFQURFUlNfQkFTSVNfU09VUkNFX1NDT1BFRBACEjAKLERBVEFTT1VSQ0VfSVRFTV9SRUFERVJTX0JBU0lTX1VOVFJBTlNMQVRBQkxFEAMqaQoNRXZlbnREZWxpdmVyeRIeChpFVkVOVF9ERUxJVkVSWV9VTlNQRUNJRklFRBAAEhwKGEVWRU5UX0RFTElWRVJZX1VOT1JERVJFRBABEhoKFkVWRU5UX0RFTElWRVJZX09SREVSRUQQAip+ChJNZXRhZGF0YURpc2Nsb3N1cmUSIwofTUVUQURBVEFfRElTQ0xPU1VSRV9VTlNQRUNJRklFRBAAEiEKHU1FVEFEQVRBX0RJU0NMT1NVUkVfRElTQ0xPU0VEEAESIAocTUVUQURBVEFfRElTQ0xPU1VSRV9XSVRISEVMRBACKlwKD1NvdXJjZVN5bmNTdGFnZRIhCh1TT1VSQ0VfU1lOQ19TVEFHRV9VTlNQRUNJRklFRBAAEiYKIlNPVVJDRV9TWU5DX1NUQUdFX0NIQU5HRVNfRU5RVUVVRUQQASqFAQoUV29ya0NvbnRleHRTZWFsU3RhdGUSJwojV09SS19DT05URVhUX1NFQUxfU1RBVEVfVU5TUEVDSUZJRUQQABIgChxXT1JLX0NPTlRFWFRfU0VBTF9TVEFURV9IRUxEEAESIgoeV09SS19DT05URVhUX1NFQUxfU1RBVEVfVU5IRUxEEAIqmwEKGVdvcmtDb250ZXh0UHJpbmNpcGFsU3RhdGUSLAooV09SS19DT05URVhUX1BSSU5DSVBBTF9TVEFURV9VTlNQRUNJRklFRBAAEiYKIldPUktfQ09OVEVYVF9QUklOQ0lQQUxfU1RBVEVfS05PV04QARIoCiRXT1JLX0NPTlRFWFRfUFJJTkNJUEFMX1NUQVRFX1VOS05PV04QAiq8AQodV29ya0NvbnRleHRBcHByb3ZlZEJ1aWxkU3RhdGUSMQotV09SS19DT05URVhUX0FQUFJPVkVEX0JVSUxEX1NUQVRFX1VOU1BFQ0lGSUVEEAASLgoqV09SS19DT05URVhUX0FQUFJPVkVEX0JVSUxEX1NUQVRFX0FQUFJPVkVEEAESOAo0V09SS19DT05URVhUX0FQUFJPVkVEX0JVSUxEX1NUQVRFX0JFQVJTX05PX0VYRUNVVElPThACKrwBCiBXb3JrQ29udGV4dE9wZXJhdGlvbkJpbmRpbmdTdGF0ZRI0CjBXT1JLX0NPTlRFWFRfT1BFUkFUSU9OX0JJTkRJTkdfU1RBVEVfVU5TUEVDSUZJRUQQABIxCi1XT1JLX0NPTlRFWFRfT1BFUkFUSU9OX0JJTkRJTkdfU1RBVEVfUkVTT0xWRUQQARIvCitXT1JLX0NPTlRFWFRfT1BFUkFUSU9OX0JJTkRJTkdfU1RBVEVfQUJTRU5UEAIqhgEKHVdvcmtDb250ZXh0QXBwcm92YWxHcmFudFN0YXRlEjEKLVdPUktfQ09OVEVYVF9BUFBST1ZBTF9HUkFOVF9TVEFURV9VTlNQRUNJRklFRBAAEjIKLldPUktfQ09OVEVYVF9BUFBST1ZBTF9HUkFOVF9TVEFURV9OT1RfUkVDT1JERUQQATLjKAoZTW9kdWxlQ2FwYWJpbGl0aWVzU2VydmljZRKbAQoWR2V0Q3VycmVudEluc3RhbGxhdGlvbhIyLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlQ3VycmVudEluc3RhbGxhdGlvblJlcXVlc3QaMy5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUN1cnJlbnRJbnN0YWxsYXRpb25SZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUARYA2ABEsYBChlBcHByb3ZlRXhlY3V0YWJsZUFydGlmYWN0EjEuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFeGVjdXRhYmxlQXJ0aWZhY3RSZXF1ZXN0GjIuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFeGVjdXRhYmxlQXJ0aWZhY3RSZXNwb25zZSJCwvMYPggDEAEwATosCihzYWFzLm1vZHVsZS5leGVjdXRhYmxlX2FydGlmYWN0X2RlY2lzaW9uEAJAAUgHUARYAmABEp4BChtBdXRob3JpemVFeGVjdXRhYmxlQXJ0aWZhY3QSMS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUV4ZWN1dGFibGVBcnRpZmFjdFJlcXVlc3QaMi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUV4ZWN1dGFibGVBcnRpZmFjdFJlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQBFgCYAESywEKGFJldm9rZUV4ZWN1dGFibGVBcnRpZmFjdBI3LnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUmV2b2tlRXhlY3V0YWJsZUFydGlmYWN0UmVxdWVzdBoyLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlRXhlY3V0YWJsZUFydGlmYWN0UmVzcG9uc2UiQsLzGD4IAxABMAE6LAooc2Fhcy5tb2R1bGUuZXhlY3V0YWJsZV9hcnRpZmFjdF9kZWNpc2lvbhACQAFIB1AEWAJgARLFAQodRXhjaGFuZ2VEZWxlZ2F0ZWRSZWFkQXVkaWVuY2USPC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUV4Y2hhbmdlRGVsZWdhdGVkUmVhZEF1ZGllbmNlUmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuSXNzdWVkV29ya0NvbnRleHQiQcLzGD0IAxABMAE6Kwonc2Fhcy5tb2R1bGUuZGVsZWdhdGVkX2F1ZGllbmNlX2V4Y2hhbmdlEARAAUgHUARYBGABEs8BCiJFeGNoYW5nZURlbGVnYXRlZE9wZXJhdGlvbkF1ZGllbmNlEkEuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFeGNoYW5nZURlbGVnYXRlZE9wZXJhdGlvbkF1ZGllbmNlUmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuSXNzdWVkV29ya0NvbnRleHQiQcLzGD0IAxABMAE6Kwonc2Fhcy5tb2R1bGUuZGVsZWdhdGVkX2F1ZGllbmNlX2V4Y2hhbmdlEARAAUgHUARYBGABEqcBChxDaGVja1dvcmtDb250ZXh0UmVjb3JkQWNjZXNzEjUuc2Fhcy5hY2NvdW50cy52MS5DaGVja1dvcmtDb250ZXh0UmVjb3JkQWNjZXNzUmVxdWVzdBo2LnNhYXMuYWNjb3VudHMudjEuQ2hlY2tXb3JrQ29udGV4dFJlY29yZEFjY2Vzc1Jlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESqgEKHUxpc3RSZWFkYWJsZVNvdXJjZUNvbGxlY3Rpb25zEjYuc2Fhcy5hY2NvdW50cy52MS5MaXN0UmVhZGFibGVTb3VyY2VDb2xsZWN0aW9uc1JlcXVlc3QaNy5zYWFzLmFjY291bnRzLnYxLkxpc3RSZWFkYWJsZVNvdXJjZUNvbGxlY3Rpb25zUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARKAAQoLUGxhY2VSZWNvcmQSKi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVBsYWNlUmVjb3JkUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUGxhY2VSZWNvcmRSZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEn0KCkVucXVldWVKb2ISKS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUVucXVldWVKb2JSZXF1ZXN0Giouc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFbnF1ZXVlSm9iUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARJ6CglDbGFpbUpvYnMSKC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUNsYWltSm9ic1JlcXVlc3QaKS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUNsYWltSm9ic1Jlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESgwEKDEhlYXJ0YmVhdEpvYhIrLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlSGVhcnRiZWF0Sm9iUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlSGVhcnRiZWF0Sm9iUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARJhCgZBY2tKb2ISJS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUFja0pvYlJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARJjCgdOYWNrSm9iEiYuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVOYWNrSm9iUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEn0KCk5vdGlmeVVzZXISKS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZU5vdGlmeVVzZXJSZXF1ZXN0Giouc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVOb3RpZnlVc2VyUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARKtAQoPTm90aWZ5T3JnQWRtaW5zEi4uc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVOb3RpZnlPcmdBZG1pbnNSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVOb3RpZnlPcmdBZG1pbnNSZXNwb25zZSI5wvMYNQgDEAEwATojCh9zYWFzLm1vZHVsZS5vcmdfYWRtaW5zX25vdGlmaWVkEAJAAUgHUANYA2ABEowBCg9SZXF1ZXN0QXBwcm92YWwSLi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVJlcXVlc3RBcHByb3ZhbFJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVJlcXVlc3RBcHByb3ZhbFJlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESdQoLR2V0QXBwcm92YWwSKi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUdldEFwcHJvdmFsUmVxdWVzdBogLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlQXBwcm92YWwiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARJxCg5DYW5jZWxBcHByb3ZhbBItLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlQ2FuY2VsQXBwcm92YWxSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAEScQoORW1pdEF1ZGl0RXZlbnQSLS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUVtaXRBdWRpdEV2ZW50UmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEoABCgtSZWFkSm91cm5hbBIqLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUmVhZEpvdXJuYWxSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVSZWFkSm91cm5hbFJlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESwwEKFkRlY2xhcmVBdWRpdEV2ZW50VHlwZXMSNS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZURlY2xhcmVBdWRpdEV2ZW50VHlwZXNSZXF1ZXN0GjYuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVEZWNsYXJlQXVkaXRFdmVudFR5cGVzUmVzcG9uc2UiOsLzGDYIAxABMAE6JAogc2Fhcy5tb2R1bGUuYXVkaXRfdHlwZXNfZGVjbGFyZWQQAkABSAdQA1gDYAESngEKFUxpc3RTdWJqZWN0VmlzaWJpbGl0eRI0LnNhYXMuYWNjb3VudHMudjEuTW9kdWxlTGlzdFN1YmplY3RWaXNpYmlsaXR5UmVxdWVzdBo1LnNhYXMuYWNjb3VudHMudjEuTW9kdWxlTGlzdFN1YmplY3RWaXNpYmlsaXR5UmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARKOAQoTRmV0Y2hEYXRhc291cmNlQmxvYhIsLnNhYXMuYWNjb3VudHMudjEuRmV0Y2hEYXRhc291cmNlQmxvYlJlcXVlc3QaKi5zYWFzLmFjY291bnRzLnYxLkZldGNoRGF0YXNvdXJjZUJsb2JDaHVuayIbiAIBwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABMAESjgEKFEZldGNoRGF0YXNvdXJjZUZpbGVzEi0uc2Fhcy5hY2NvdW50cy52MS5GZXRjaERhdGFzb3VyY2VGaWxlc1JlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLkZldGNoRGF0YXNvdXJjZUZpbGVzRnJhbWUiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgATABErMBChVNaW50TW9kdWxlV29ya0NvbnRleHQSLi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZU1pbnRXb3JrQ29udGV4dFJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLk1vZHVsZU1pbnRXb3JrQ29udGV4dFJlc3BvbnNlIjnC8xg1CAMQATABOiMKH3NhYXMubW9kdWxlLndvcmtfY29udGV4dF9taW50ZWQQAkABSAdQBFgEYAESxwEKGk1pbnRNb2R1bGVPcGVyYXRpb25Db250ZXh0EjMuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVNaW50T3BlcmF0aW9uQ29udGV4dFJlcXVlc3QaNC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZU1pbnRPcGVyYXRpb25Db250ZXh0UmVzcG9uc2UiPsLzGDoIAxABMAE6KAokc2Fhcy5tb2R1bGUub3BlcmF0aW9uX2NvbnRleHRfbWludGVkEAJAAUgHUARYBGABEs4BChpNaW50U291cmNlT3BlcmF0aW9uQ29udGV4dBI5LnNhYXMuYWNjb3VudHMudjEuTW9kdWxlTWludFNvdXJjZU9wZXJhdGlvbkNvbnRleHRSZXF1ZXN0Gjouc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVNaW50U291cmNlT3BlcmF0aW9uQ29udGV4dFJlc3BvbnNlIjnC8xg1CAMQATABOiMKH3NhYXMuZGF0YXNvdXJjZS5kZWxlZ2F0aW9uLnVzZWQQAkABSAdQBFgEYAESgwEKDFB1Ymxpc2hFdmVudBIrLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUHVibGlzaEV2ZW50UmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUHVibGlzaEV2ZW50UmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARJ6CglTdWJzY3JpYmUSKC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVN1YnNjcmliZVJlcXVlc3QaKS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVN1YnNjcmliZVJlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESawoLVW5zdWJzY3JpYmUSKi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVVuc3Vic2NyaWJlUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEpIBChFMaXN0U3Vic2NyaXB0aW9ucxIwLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlTGlzdFN1YnNjcmlwdGlvbnNSZXF1ZXN0GjEuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVMaXN0U3Vic2NyaXB0aW9uc1Jlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESgwEKDFJlcGxheUV2ZW50cxIrLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUmVwbGF5RXZlbnRzUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUmVwbGF5RXZlbnRzUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARKwAQobUmVzb2x2ZVdvcmtDb250ZXh0QXV0aG9yaXR5Ejouc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVSZXNvbHZlV29ya0NvbnRleHRBdXRob3JpdHlSZXF1ZXN0Gjsuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVSZXNvbHZlV29ya0NvbnRleHRBdXRob3JpdHlSZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABYgZwcm90bzM", [file_saas_accounts_v1_work_contexts, file_buf_validate_validate, file_google_protobuf_empty, file_google_protobuf_duration, file_google_protobuf_struct, file_google_protobuf_timestamp, file_saas_accounts_v1_module_registration, file_saas_events_v1_events, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); /** * ModuleEnqueueJobRequest names the tenant the work belongs to and carries the @@ -715,6 +715,153 @@ export type ModuleEmitAuditEventRequest = Message<"saas.accounts.v1.ModuleEmitAu export const ModuleEmitAuditEventRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_saas_accounts_v1_module_capabilities, 19); +/** + * ModuleReadJournalRequest reads the platform journal across every tenant. Only a + * module principal whose grant is cross_tenant may call it, the gate a + * system-scoped EmitAuditEvent has; any other caller is PermissionDenied. + * + * @generated from message saas.accounts.v1.ModuleReadJournalRequest + */ +export type ModuleReadJournalRequest = Message<"saas.accounts.v1.ModuleReadJournalRequest"> & { + /** + * after is the position the previous page ended at (a page's next_after). The + * literal "start" is a valid position meaning "before the oldest retained + * entry", and the empty string is accepted as an alias of it; so a reader with + * nothing read yet has a non-empty position to correlate its first page on. + * Positions are opaque: store one and hand it back unread. No entry's position + * is ever "start". A value this host did not issue is InvalidArgument. + * + * @generated from field: string after = 1; + */ + after: string; + + /** + * limit is the page size: 0 is 100, and more than 500 is served as 500. A + * page is also bounded to 2 MiB of event data, though the first entry is always + * served. + * + * @generated from field: int32 limit = 2; + */ + limit: number; + + /** + * types are the event types to read, exactly (no pattern). At least one is + * required, so no caller reads every tenant's every event by leaving the + * filter out. + * + * @generated from field: repeated string types = 3; + */ + types: string[]; + + /** + * delivery_bindings are the binding ids the caller's own delivery of the journal + * runs under. They decide which entries are marked delivery_machinery. + * + * @generated from field: repeated string delivery_bindings = 4; + */ + deliveryBindings: string[]; +}; + +/** + * Describes the message saas.accounts.v1.ModuleReadJournalRequest. + * Use `create(ModuleReadJournalRequestSchema)` to create a new message. + */ +export const ModuleReadJournalRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_module_capabilities, 20); + +/** + * ModuleJournalEntry is one journal row. Rows are served in the order their + * writing transactions are final (see ModuleReadJournalRequest), so a reader that + * resumes from the last position it was given skips nothing. + * + * @generated from message saas.accounts.v1.ModuleJournalEntry + */ +export type ModuleJournalEntry = Message<"saas.accounts.v1.ModuleJournalEntry"> & { + /** + * position is where the row sits in the read order: opaque, and the value a + * reader passes as after to resume past it. + * + * @generated from field: string position = 1; + */ + position: string; + + /** + * @generated from field: string event_id = 2; + */ + eventId: string; + + /** + * @generated from field: string type = 3; + */ + type: string; + + /** + * organization_id is the row's tenant, empty for a row that has none. + * + * @generated from field: string organization_id = 4; + */ + organizationId: string; + + /** + * @generated from field: google.protobuf.Timestamp occurred_at = 5; + */ + occurredAt?: Timestamp; + + /** + * data is the event's JSON object; empty when the event's data is not one. + * + * @generated from field: google.protobuf.Struct data = 6; + */ + data?: JsonObject; + + /** + * delivery_machinery is true when the entry the row carries is the footprint of + * the caller's own delivery of the journal: the host computes it, so a caller + * never parses this module's payloads. It is true for a + * saas.module.operation_context_minted or + * saas.module.delegated_audience_exchange entry whose binding_id is one of the + * request's delivery_bindings, and for a saas.module.work_context_minted entry + * of the caller's own module principal. + * + * @generated from field: bool delivery_machinery = 7; + */ + deliveryMachinery: boolean; +}; + +/** + * Describes the message saas.accounts.v1.ModuleJournalEntry. + * Use `create(ModuleJournalEntrySchema)` to create a new message. + */ +export const ModuleJournalEntrySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_module_capabilities, 21); + +/** + * ModuleReadJournalResponse is one page and where to resume. + * + * @generated from message saas.accounts.v1.ModuleReadJournalResponse + */ +export type ModuleReadJournalResponse = Message<"saas.accounts.v1.ModuleReadJournalResponse"> & { + /** + * @generated from field: repeated saas.accounts.v1.ModuleJournalEntry entries = 1; + */ + entries: ModuleJournalEntry[]; + + /** + * next_after is the last entry's position, or the request's after when the page + * is empty. + * + * @generated from field: string next_after = 2; + */ + nextAfter: string; +}; + +/** + * Describes the message saas.accounts.v1.ModuleReadJournalResponse. + * Use `create(ModuleReadJournalResponseSchema)` to create a new message. + */ +export const ModuleReadJournalResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_module_capabilities, 22); + /** * ModuleAuditFieldDeclaration is one payload field of a declared audit event * type. `solution` is stamped by the host from the emitting scope and may not @@ -756,7 +903,7 @@ export type ModuleAuditFieldDeclaration = Message<"saas.accounts.v1.ModuleAuditF * Use `create(ModuleAuditFieldDeclarationSchema)` to create a new message. */ export const ModuleAuditFieldDeclarationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 20); + messageDesc(file_saas_accounts_v1_module_capabilities, 23); /** * ModuleAuditEventTypeDeclaration is one audit event type a composed module @@ -801,7 +948,7 @@ export type ModuleAuditEventTypeDeclaration = Message<"saas.accounts.v1.ModuleAu * Use `create(ModuleAuditEventTypeDeclarationSchema)` to create a new message. */ export const ModuleAuditEventTypeDeclarationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 21); + messageDesc(file_saas_accounts_v1_module_capabilities, 24); /** * ModuleDeclareAuditEventTypesRequest declares the audit event types a composed @@ -834,7 +981,7 @@ export type ModuleDeclareAuditEventTypesRequest = Message<"saas.accounts.v1.Modu * Use `create(ModuleDeclareAuditEventTypesRequestSchema)` to create a new message. */ export const ModuleDeclareAuditEventTypesRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 22); + messageDesc(file_saas_accounts_v1_module_capabilities, 25); /** * ModuleDeclareAuditEventTypesResponse names what the call changed: the types @@ -860,7 +1007,7 @@ export type ModuleDeclareAuditEventTypesResponse = Message<"saas.accounts.v1.Mod * Use `create(ModuleDeclareAuditEventTypesResponseSchema)` to create a new message. */ export const ModuleDeclareAuditEventTypesResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 23); + messageDesc(file_saas_accounts_v1_module_capabilities, 26); /** * SubjectVisibilityGrant is one entry of a viewer's visible-subject set: another @@ -890,7 +1037,7 @@ export type SubjectVisibilityGrant = Message<"saas.accounts.v1.SubjectVisibility * Use `create(SubjectVisibilityGrantSchema)` to create a new message. */ export const SubjectVisibilityGrantSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 24); + messageDesc(file_saas_accounts_v1_module_capabilities, 27); /** * ModuleListSubjectVisibilityRequest asks for one viewer's whole visible-subject @@ -924,7 +1071,7 @@ export type ModuleListSubjectVisibilityRequest = Message<"saas.accounts.v1.Modul * Use `create(ModuleListSubjectVisibilityRequestSchema)` to create a new message. */ export const ModuleListSubjectVisibilityRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 25); + messageDesc(file_saas_accounts_v1_module_capabilities, 28); /** * ModuleListSubjectVisibilityResponse carries the viewer's WHOLE set, read in @@ -955,7 +1102,7 @@ export type ModuleListSubjectVisibilityResponse = Message<"saas.accounts.v1.Modu * Use `create(ModuleListSubjectVisibilityResponseSchema)` to create a new message. */ export const ModuleListSubjectVisibilityResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 26); + messageDesc(file_saas_accounts_v1_module_capabilities, 29); /** * FetchDatasourceBlobRequest asks accounts to re-fetch one file blob from the @@ -986,7 +1133,7 @@ export type FetchDatasourceBlobRequest = Message<"saas.accounts.v1.FetchDatasour * Use `create(FetchDatasourceBlobRequestSchema)` to create a new message. */ export const FetchDatasourceBlobRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 27); + messageDesc(file_saas_accounts_v1_module_capabilities, 30); /** * FetchDatasourceBlobChunk is one frame of the streamed blob. total_size and @@ -1018,7 +1165,7 @@ export type FetchDatasourceBlobChunk = Message<"saas.accounts.v1.FetchDatasource * Use `create(FetchDatasourceBlobChunkSchema)` to create a new message. */ export const FetchDatasourceBlobChunkSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 28); + messageDesc(file_saas_accounts_v1_module_capabilities, 31); /** * DatasourceFileRef names one file of a files-interface datasource at the @@ -1045,7 +1192,7 @@ export type DatasourceFileRef = Message<"saas.accounts.v1.DatasourceFileRef"> & * Use `create(DatasourceFileRefSchema)` to create a new message. */ export const DatasourceFileRefSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 29); + messageDesc(file_saas_accounts_v1_module_capabilities, 32); /** * FetchDatasourceFilesRequest asks accounts for a batch of files of one source @@ -1081,7 +1228,7 @@ export type FetchDatasourceFilesRequest = Message<"saas.accounts.v1.FetchDatasou * Use `create(FetchDatasourceFilesRequestSchema)` to create a new message. */ export const FetchDatasourceFilesRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 30); + messageDesc(file_saas_accounts_v1_module_capabilities, 33); /** * DatasourceProvenance is the envelope every item a datasource serves carries, @@ -1128,7 +1275,7 @@ export type DatasourceProvenance = Message<"saas.accounts.v1.DatasourceProvenanc * Use `create(DatasourceProvenanceSchema)` to create a new message. */ export const DatasourceProvenanceSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 31); + messageDesc(file_saas_accounts_v1_module_capabilities, 34); /** * DatasourceItemReaders is the envelope's per-item permission slot: who may read @@ -1181,7 +1328,7 @@ export type DatasourceItemReaders = Message<"saas.accounts.v1.DatasourceItemRead * Use `create(DatasourceItemReadersSchema)` to create a new message. */ export const DatasourceItemReadersSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 32); + messageDesc(file_saas_accounts_v1_module_capabilities, 35); /** * DatasourceFileHeader opens one file of a FetchDatasourceFiles stream. size is @@ -1222,7 +1369,7 @@ export type DatasourceFileHeader = Message<"saas.accounts.v1.DatasourceFileHeade * Use `create(DatasourceFileHeaderSchema)` to create a new message. */ export const DatasourceFileHeaderSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 33); + messageDesc(file_saas_accounts_v1_module_capabilities, 36); /** * FetchDatasourceFilesFrame is one frame of a FetchDatasourceFiles stream. The @@ -1255,7 +1402,7 @@ export type FetchDatasourceFilesFrame = Message<"saas.accounts.v1.FetchDatasourc * Use `create(FetchDatasourceFilesFrameSchema)` to create a new message. */ export const FetchDatasourceFilesFrameSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 34); + messageDesc(file_saas_accounts_v1_module_capabilities, 37); /** * ModulePublishEventRequest publishes one domain event for the caller's bound @@ -1283,7 +1430,7 @@ export type ModulePublishEventRequest = Message<"saas.accounts.v1.ModulePublishE * Use `create(ModulePublishEventRequestSchema)` to create a new message. */ export const ModulePublishEventRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 35); + messageDesc(file_saas_accounts_v1_module_capabilities, 38); /** * @generated from message saas.accounts.v1.ModulePublishEventResponse @@ -1305,7 +1452,7 @@ export type ModulePublishEventResponse = Message<"saas.accounts.v1.ModulePublish * Use `create(ModulePublishEventResponseSchema)` to create a new message. */ export const ModulePublishEventResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 36); + messageDesc(file_saas_accounts_v1_module_capabilities, 39); /** * ModuleSubscribeRequest creates, or idempotently re-affirms, a durable @@ -1338,7 +1485,7 @@ export type ModuleSubscribeRequest = Message<"saas.accounts.v1.ModuleSubscribeRe * Use `create(ModuleSubscribeRequestSchema)` to create a new message. */ export const ModuleSubscribeRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 37); + messageDesc(file_saas_accounts_v1_module_capabilities, 40); /** * ModuleSubscription is one durable subscription row as the module sees it. @@ -1382,7 +1529,7 @@ export type ModuleSubscription = Message<"saas.accounts.v1.ModuleSubscription"> * Use `create(ModuleSubscriptionSchema)` to create a new message. */ export const ModuleSubscriptionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 38); + messageDesc(file_saas_accounts_v1_module_capabilities, 41); /** * @generated from message saas.accounts.v1.ModuleSubscribeResponse @@ -1399,7 +1546,7 @@ export type ModuleSubscribeResponse = Message<"saas.accounts.v1.ModuleSubscribeR * Use `create(ModuleSubscribeResponseSchema)` to create a new message. */ export const ModuleSubscribeResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 39); + messageDesc(file_saas_accounts_v1_module_capabilities, 42); /** * ModuleUnsubscribeRequest revokes one of the caller's own subscriptions. @@ -1418,7 +1565,7 @@ export type ModuleUnsubscribeRequest = Message<"saas.accounts.v1.ModuleUnsubscri * Use `create(ModuleUnsubscribeRequestSchema)` to create a new message. */ export const ModuleUnsubscribeRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 40); + messageDesc(file_saas_accounts_v1_module_capabilities, 43); /** * ModuleListSubscriptionsRequest lists the calling principal's live @@ -1434,7 +1581,7 @@ export type ModuleListSubscriptionsRequest = Message<"saas.accounts.v1.ModuleLis * Use `create(ModuleListSubscriptionsRequestSchema)` to create a new message. */ export const ModuleListSubscriptionsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 41); + messageDesc(file_saas_accounts_v1_module_capabilities, 44); /** * @generated from message saas.accounts.v1.ModuleListSubscriptionsResponse @@ -1451,7 +1598,7 @@ export type ModuleListSubscriptionsResponse = Message<"saas.accounts.v1.ModuleLi * Use `create(ModuleListSubscriptionsResponseSchema)` to create a new message. */ export const ModuleListSubscriptionsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 42); + messageDesc(file_saas_accounts_v1_module_capabilities, 45); /** * ModuleReplayEventsRequest re-delivers durable events of one type for the @@ -1483,7 +1630,7 @@ export type ModuleReplayEventsRequest = Message<"saas.accounts.v1.ModuleReplayEv * Use `create(ModuleReplayEventsRequestSchema)` to create a new message. */ export const ModuleReplayEventsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 43); + messageDesc(file_saas_accounts_v1_module_capabilities, 46); /** * @generated from message saas.accounts.v1.ModuleReplayEventsResponse @@ -1503,7 +1650,7 @@ export type ModuleReplayEventsResponse = Message<"saas.accounts.v1.ModuleReplayE * Use `create(ModuleReplayEventsResponseSchema)` to create a new message. */ export const ModuleReplayEventsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 44); + messageDesc(file_saas_accounts_v1_module_capabilities, 47); /** * ModuleCapabilitiesService is the module-facing platform surface. Every RPC is @@ -1586,7 +1733,7 @@ export type ReadableSourceCollection = Message<"saas.accounts.v1.ReadableSourceC * Use `create(ReadableSourceCollectionSchema)` to create a new message. */ export const ReadableSourceCollectionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 45); + messageDesc(file_saas_accounts_v1_module_capabilities, 48); /** * ReadableCollectionGrant is one active grant conferring read on a collection's @@ -1637,7 +1784,7 @@ export type ReadableCollectionGrant = Message<"saas.accounts.v1.ReadableCollecti * Use `create(ReadableCollectionGrantSchema)` to create a new message. */ export const ReadableCollectionGrantSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 46); + messageDesc(file_saas_accounts_v1_module_capabilities, 49); /** * CollectionSyncProvenance is the host's record of how one feeding source last @@ -1701,7 +1848,7 @@ export type CollectionSyncProvenance = Message<"saas.accounts.v1.CollectionSyncP * Use `create(CollectionSyncProvenanceSchema)` to create a new message. */ export const CollectionSyncProvenanceSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 47); + messageDesc(file_saas_accounts_v1_module_capabilities, 50); /** * The calling module authenticates with its own module Work Context in the @@ -1733,7 +1880,7 @@ export type ListReadableSourceCollectionsRequest = Message<"saas.accounts.v1.Lis * Use `create(ListReadableSourceCollectionsRequestSchema)` to create a new message. */ export const ListReadableSourceCollectionsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 48); + messageDesc(file_saas_accounts_v1_module_capabilities, 51); /** * @generated from message saas.accounts.v1.ListReadableSourceCollectionsResponse @@ -1755,7 +1902,7 @@ export type ListReadableSourceCollectionsResponse = Message<"saas.accounts.v1.Li * Use `create(ListReadableSourceCollectionsResponseSchema)` to create a new message. */ export const ListReadableSourceCollectionsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 49); + messageDesc(file_saas_accounts_v1_module_capabilities, 52); /** * ModulePlaceRecordRequest binds one of the calling module's own records to a @@ -1812,7 +1959,7 @@ export type ModulePlaceRecordRequest = Message<"saas.accounts.v1.ModulePlaceReco * Use `create(ModulePlaceRecordRequestSchema)` to create a new message. */ export const ModulePlaceRecordRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 50); + messageDesc(file_saas_accounts_v1_module_capabilities, 53); /** * @generated from message saas.accounts.v1.ModulePlaceRecordResponse @@ -1835,7 +1982,7 @@ export type ModulePlaceRecordResponse = Message<"saas.accounts.v1.ModulePlaceRec * Use `create(ModulePlaceRecordResponseSchema)` to create a new message. */ export const ModulePlaceRecordResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 51); + messageDesc(file_saas_accounts_v1_module_capabilities, 54); /** * Exact-record delegated oracle. The calling module authenticates with its @@ -1873,7 +2020,7 @@ export type CheckWorkContextRecordAccessRequest = Message<"saas.accounts.v1.Chec * Use `create(CheckWorkContextRecordAccessRequestSchema)` to create a new message. */ export const CheckWorkContextRecordAccessRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 52); + messageDesc(file_saas_accounts_v1_module_capabilities, 55); /** * @generated from message saas.accounts.v1.CheckWorkContextRecordAccessResponse @@ -1899,7 +2046,7 @@ export type CheckWorkContextRecordAccessResponse = Message<"saas.accounts.v1.Che * Use `create(CheckWorkContextRecordAccessResponseSchema)` to create a new message. */ export const CheckWorkContextRecordAccessResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 53); + messageDesc(file_saas_accounts_v1_module_capabilities, 56); /** * ModuleExchangeDelegatedReadAudienceRequest selects only an installed binding. @@ -1924,7 +2071,7 @@ export type ModuleExchangeDelegatedReadAudienceRequest = Message<"saas.accounts. * Use `create(ModuleExchangeDelegatedReadAudienceRequestSchema)` to create a new message. */ export const ModuleExchangeDelegatedReadAudienceRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 54); + messageDesc(file_saas_accounts_v1_module_capabilities, 57); /** * Selects one immutable installed operation binding. The caller chooses only @@ -1985,7 +2132,7 @@ export type ModuleExchangeDelegatedOperationAudienceRequest = Message<"saas.acco * Use `create(ModuleExchangeDelegatedOperationAudienceRequestSchema)` to create a new message. */ export const ModuleExchangeDelegatedOperationAudienceRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 55); + messageDesc(file_saas_accounts_v1_module_capabilities, 58); /** * Exact dependency identity as asserted by the authenticated compiler owner. @@ -2014,7 +2161,7 @@ export type ExecutableArtifactContract = Message<"saas.accounts.v1.ExecutableArt * Use `create(ExecutableArtifactContractSchema)` to create a new message. */ export const ExecutableArtifactContractSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 56); + messageDesc(file_saas_accounts_v1_module_capabilities, 59); /** * @generated from message saas.accounts.v1.ExecutableArtifactIdentity @@ -2054,7 +2201,7 @@ export type ExecutableArtifactIdentity = Message<"saas.accounts.v1.ExecutableArt * Use `create(ExecutableArtifactIdentitySchema)` to create a new message. */ export const ExecutableArtifactIdentitySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 57); + messageDesc(file_saas_accounts_v1_module_capabilities, 60); /** * @generated from message saas.accounts.v1.ModuleExecutableArtifactRequest @@ -2086,7 +2233,7 @@ export type ModuleExecutableArtifactRequest = Message<"saas.accounts.v1.ModuleEx * Use `create(ModuleExecutableArtifactRequestSchema)` to create a new message. */ export const ModuleExecutableArtifactRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 58); + messageDesc(file_saas_accounts_v1_module_capabilities, 61); /** * @generated from message saas.accounts.v1.ModuleRevokeExecutableArtifactRequest @@ -2113,7 +2260,7 @@ export type ModuleRevokeExecutableArtifactRequest = Message<"saas.accounts.v1.Mo * Use `create(ModuleRevokeExecutableArtifactRequestSchema)` to create a new message. */ export const ModuleRevokeExecutableArtifactRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 59); + messageDesc(file_saas_accounts_v1_module_capabilities, 62); /** * @generated from message saas.accounts.v1.ModuleExecutableArtifactResponse @@ -2145,7 +2292,7 @@ export type ModuleExecutableArtifactResponse = Message<"saas.accounts.v1.ModuleE * Use `create(ModuleExecutableArtifactResponseSchema)` to create a new message. */ export const ModuleExecutableArtifactResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 60); + messageDesc(file_saas_accounts_v1_module_capabilities, 63); /** * A current observation under independently authenticated module and parent @@ -2170,7 +2317,7 @@ export type ModuleCurrentInstallationRequest = Message<"saas.accounts.v1.ModuleC * Use `create(ModuleCurrentInstallationRequestSchema)` to create a new message. */ export const ModuleCurrentInstallationRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 61); + messageDesc(file_saas_accounts_v1_module_capabilities, 64); /** * @generated from message saas.accounts.v1.ModuleCurrentInstallationResponse @@ -2208,7 +2355,7 @@ export type ModuleCurrentInstallationResponse = Message<"saas.accounts.v1.Module * Use `create(ModuleCurrentInstallationResponseSchema)` to create a new message. */ export const ModuleCurrentInstallationResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 62); + messageDesc(file_saas_accounts_v1_module_capabilities, 65); /** * WorkContextSealQuestion asks for the live installation one principal holds. @@ -2240,7 +2387,7 @@ export type WorkContextSealQuestion = Message<"saas.accounts.v1.WorkContextSealQ * Use `create(WorkContextSealQuestionSchema)` to create a new message. */ export const WorkContextSealQuestionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 63); + messageDesc(file_saas_accounts_v1_module_capabilities, 66); /** * WorkContextPrincipalEpochQuestion asks for one principal's live epoch, @@ -2263,7 +2410,7 @@ export type WorkContextPrincipalEpochQuestion = Message<"saas.accounts.v1.WorkCo * Use `create(WorkContextPrincipalEpochQuestionSchema)` to create a new message. */ export const WorkContextPrincipalEpochQuestionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 64); + messageDesc(file_saas_accounts_v1_module_capabilities, 67); /** * WorkContextApprovedBuildQuestion asks what execution the issuer APPROVES for @@ -2286,7 +2433,7 @@ export type WorkContextApprovedBuildQuestion = Message<"saas.accounts.v1.WorkCon * Use `create(WorkContextApprovedBuildQuestionSchema)` to create a new message. */ export const WorkContextApprovedBuildQuestionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 65); + messageDesc(file_saas_accounts_v1_module_capabilities, 68); /** * WorkContextOperationBindingQuestion resolves one operation binding by its @@ -2311,7 +2458,7 @@ export type WorkContextOperationBindingQuestion = Message<"saas.accounts.v1.Work * Use `create(WorkContextOperationBindingQuestionSchema)` to create a new message. */ export const WorkContextOperationBindingQuestionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 66); + messageDesc(file_saas_accounts_v1_module_capabilities, 69); /** * WorkContextAuthorizationRevisionQuestion asks the authorization revision a @@ -2338,7 +2485,7 @@ export type WorkContextAuthorizationRevisionQuestion = Message<"saas.accounts.v1 * Use `create(WorkContextAuthorizationRevisionQuestionSchema)` to create a new message. */ export const WorkContextAuthorizationRevisionQuestionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 67); + messageDesc(file_saas_accounts_v1_module_capabilities, 70); /** * WorkContextApprovalGrantQuestion resolves the approval a capability's grant @@ -2359,7 +2506,7 @@ export type WorkContextApprovalGrantQuestion = Message<"saas.accounts.v1.WorkCon * Use `create(WorkContextApprovalGrantQuestionSchema)` to create a new message. */ export const WorkContextApprovalGrantQuestionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 68); + messageDesc(file_saas_accounts_v1_module_capabilities, 71); /** * @generated from message saas.accounts.v1.WorkContextSealAnswer @@ -2389,7 +2536,7 @@ export type WorkContextSealAnswer = Message<"saas.accounts.v1.WorkContextSealAns * Use `create(WorkContextSealAnswerSchema)` to create a new message. */ export const WorkContextSealAnswerSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 69); + messageDesc(file_saas_accounts_v1_module_capabilities, 72); /** * @generated from message saas.accounts.v1.WorkContextPrincipalEpochAnswer @@ -2416,7 +2563,7 @@ export type WorkContextPrincipalEpochAnswer = Message<"saas.accounts.v1.WorkCont * Use `create(WorkContextPrincipalEpochAnswerSchema)` to create a new message. */ export const WorkContextPrincipalEpochAnswerSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 70); + messageDesc(file_saas_accounts_v1_module_capabilities, 73); /** * @generated from message saas.accounts.v1.WorkContextApprovedBuildAnswer @@ -2451,7 +2598,7 @@ export type WorkContextApprovedBuildAnswer = Message<"saas.accounts.v1.WorkConte * Use `create(WorkContextApprovedBuildAnswerSchema)` to create a new message. */ export const WorkContextApprovedBuildAnswerSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 71); + messageDesc(file_saas_accounts_v1_module_capabilities, 74); /** * @generated from message saas.accounts.v1.WorkContextOperationBindingAnswer @@ -2505,7 +2652,7 @@ export type WorkContextOperationBindingAnswer = Message<"saas.accounts.v1.WorkCo * Use `create(WorkContextOperationBindingAnswerSchema)` to create a new message. */ export const WorkContextOperationBindingAnswerSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 72); + messageDesc(file_saas_accounts_v1_module_capabilities, 75); /** * @generated from message saas.accounts.v1.WorkContextAuthorizationRevisionAnswer @@ -2527,7 +2674,7 @@ export type WorkContextAuthorizationRevisionAnswer = Message<"saas.accounts.v1.W * Use `create(WorkContextAuthorizationRevisionAnswerSchema)` to create a new message. */ export const WorkContextAuthorizationRevisionAnswerSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 73); + messageDesc(file_saas_accounts_v1_module_capabilities, 76); /** * @generated from message saas.accounts.v1.WorkContextApprovalGrantAnswer @@ -2549,7 +2696,7 @@ export type WorkContextApprovalGrantAnswer = Message<"saas.accounts.v1.WorkConte * Use `create(WorkContextApprovalGrantAnswerSchema)` to create a new message. */ export const WorkContextApprovalGrantAnswerSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 74); + messageDesc(file_saas_accounts_v1_module_capabilities, 77); /** * @generated from message saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest @@ -2616,7 +2763,7 @@ export type ModuleResolveWorkContextAuthorityRequest = Message<"saas.accounts.v1 * Use `create(ModuleResolveWorkContextAuthorityRequestSchema)` to create a new message. */ export const ModuleResolveWorkContextAuthorityRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 75); + messageDesc(file_saas_accounts_v1_module_capabilities, 78); /** * @generated from message saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse @@ -2673,7 +2820,7 @@ export type ModuleResolveWorkContextAuthorityResponse = Message<"saas.accounts.v * Use `create(ModuleResolveWorkContextAuthorityResponseSchema)` to create a new message. */ export const ModuleResolveWorkContextAuthorityResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 76); + messageDesc(file_saas_accounts_v1_module_capabilities, 79); /** * ModuleAuditFieldKind is the kind of one declared audit payload field: the @@ -3303,6 +3450,19 @@ export const ModuleCapabilitiesService: GenService<{ input: typeof ModuleEmitAuditEventRequestSchema; output: typeof EmptySchema; }, + /** + * ReadJournal serves the platform journal across every tenant, and the rows with + * no tenant, to a module principal whose grant is cross_tenant. Rows are served + * in the order their writing transactions are final, so a reader that resumes + * from next_after skips nothing; it enqueues nothing and is not a replay. + * + * @generated from rpc saas.accounts.v1.ModuleCapabilitiesService.ReadJournal + */ + readJournal: { + methodKind: "unary"; + input: typeof ModuleReadJournalRequestSchema; + output: typeof ModuleReadJournalResponseSchema; + }, /** * DeclareAuditEventTypes admits the audit event types a composed module owns, * into the namespaces the operator bound to it; see the request. diff --git a/module/services/frontend/code/packages/saas-sdk/package.json b/module/services/frontend/code/packages/saas-sdk/package.json index 1a723c22a..15472f8a5 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.40", + "version": "0.3.41", "description": "Typed Connect client, gateway-bound facades, and audit data-graph tooling for the saas-starter public API.", "repository": { "type": "git", diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/audit_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/audit_pb.ts index 8541e08c0..d874d245a 100644 --- a/module/services/frontend/code/src/gen/saas/accounts/v1/audit_pb.ts +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/audit_pb.ts @@ -8,6 +8,7 @@ import { file_buf_validate_validate } from "../../../buf/validate/validate_pb"; import { file_google_api_annotations } from "../../../google/api/annotations_pb"; import type { Timestamp } from "@bufbuild/protobuf/wkt"; import { file_google_protobuf_struct, file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; +import { file_codefly_runnable_v0_options } from "../../../codefly/runnable/v0/options_pb"; import { file_saas_policy_v1_options } from "../../policy/v1/options_pb"; import type { JsonObject, Message } from "@bufbuild/protobuf"; @@ -15,7 +16,7 @@ import type { JsonObject, Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/audit.proto. */ export const file_saas_accounts_v1_audit: GenFile = /*@__PURE__*/ - fileDesc("ChxzYWFzL2FjY291bnRzL3YxL2F1ZGl0LnByb3RvEhBzYWFzLmFjY291bnRzLnYxIrsDCgpBdWRpdEV2ZW50EgoKAmlkGAEgASgJEhAKCGFjdG9yX2lkGAIgASgJEhIKCmFjdG9yX3R5cGUYAyABKAkSEgoGYWN0aW9uGAQgASgJQgIYARIQCghyZXNvdXJjZRgFIAEoCRITCgtyZXNvdXJjZV9pZBgGIAEoCRIOCgZvcmdfaWQYByABKAkSQAoIbWV0YWRhdGEYCCADKAsyKi5zYWFzLmFjY291bnRzLnYxLkF1ZGl0RXZlbnQuTWV0YWRhdGFFbnRyeUICGAESEgoKaXBfYWRkcmVzcxgJIAEoCRIuCgpjcmVhdGVkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBISCgpldmVudF90eXBlGAsgASgJEhYKDnNjaGVtYV92ZXJzaW9uGAwgASgFEigKB3BheWxvYWQYDSABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EhAKCGNhdGVnb3J5GA4gASgJEhEKCWNsaWVudF9pZBgPIAEoCRovCg1NZXRhZGF0YUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEi+AMKFFF1ZXJ5QXVkaXRMb2dSZXF1ZXN0Eg4KBm9yZ19pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCRISCgZhY3Rpb24YAyABKAlCAhgBEhAKCHJlc291cmNlGAQgASgJEhMKC3Jlc291cmNlX2lkGAUgASgJEigKBGZyb20YBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEiYKAnRvGAcgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIcCglwYWdlX3NpemUYCCABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAkgASgJEhIKCmV2ZW50X3R5cGUYCiABKAkSEAoIY2F0ZWdvcnkYCyABKAkSVQoQcGF5bG9hZF9jb250YWlucxgMIAMoCzI7LnNhYXMuYWNjb3VudHMudjEuUXVlcnlBdWRpdExvZ1JlcXVlc3QuUGF5bG9hZENvbnRhaW5zRW50cnkSEQoJbmFtZXNwYWNlGA0gASgJEhEKCWNsaWVudF9pZBgOIAEoCRIkCgtldmVudF90eXBlcxgPIAMoCUIPukgMkgEJEEAiBXIDGP8BGjYKFFBheWxvYWRDb250YWluc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEicwoVUXVlcnlBdWRpdExvZ1Jlc3BvbnNlEiwKBmV2ZW50cxgBIAMoCzIcLnNhYXMuYWNjb3VudHMudjEuQXVkaXRFdmVudBIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkSEwoLdG90YWxfY291bnQYAyABKAUilwEKFUV4cG9ydEF1ZGl0TG9nUmVxdWVzdBIOCgZvcmdfaWQYASABKAkSDgoGZm9ybWF0GAIgASgJEhAKCGFjdG9yX2lkGAMgASgJEhIKBmFjdGlvbhgEIAEoCUICGAESEgoKZXZlbnRfdHlwZRgFIAEoCRIkCgtldmVudF90eXBlcxgGIAMoCUIPukgMkgEJEEAiBXIDGP8BIk4KFkV4cG9ydEF1ZGl0TG9nUmVzcG9uc2USDAoEZGF0YRgBIAEoDBIUCgxjb250ZW50X3R5cGUYAiABKAkSEAoIZmlsZW5hbWUYAyABKAkiSwoLQXVkaXRNZXRyaWMSCgoCb3AYASABKAkSDQoFZmllbGQYAiABKAkSEgoKcGVyY2VudGlsZRgDIAEoARINCgVhbGlhcxgEIAEoCSJLChJBdWRpdERlcml2ZWRNZXRyaWMSDQoFYWxpYXMYASABKAkSEQoJbnVtZXJhdG9yGAIgASgJEhMKC2Rlbm9taW5hdG9yGAMgASgJIu0EChhBZ2dyZWdhdGVBdWRpdExvZ1JlcXVlc3QSDgoGb3JnX2lkGAEgASgJEhAKCGFjdG9yX2lkGAIgASgJEhIKCmV2ZW50X3R5cGUYAyABKAkSEAoIY2F0ZWdvcnkYBCABKAkSEAoIcmVzb3VyY2UYBSABKAkSKAoEZnJvbRgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASJgoCdG8YByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhAKCGdyb3VwX2J5GAggASgJEg4KBmJ1Y2tldBgJIAEoCRIRCglncm91cF9ieXMYCiADKAkSLgoHbWV0cmljcxgLIAMoCzIdLnNhYXMuYWNjb3VudHMudjEuQXVkaXRNZXRyaWMSNQoHZGVyaXZlZBgMIAMoCzIkLnNhYXMuYWNjb3VudHMudjEuQXVkaXREZXJpdmVkTWV0cmljEhEKCW5hbWVzcGFjZRgNIAEoCRITCgtyZXNvdXJjZV9pZBgOIAEoCRJZChBwYXlsb2FkX2NvbnRhaW5zGA8gAygLMj8uc2Fhcy5hY2NvdW50cy52MS5BZ2dyZWdhdGVBdWRpdExvZ1JlcXVlc3QuUGF5bG9hZENvbnRhaW5zRW50cnkSFQoNY29sbGVjdGlvbl9pZBgQIAEoCRIRCgljbGllbnRfaWQYESABKAkSJAoLZXZlbnRfdHlwZXMYEiADKAlCD7pIDJIBCRBAIgVyAxj/ARo2ChRQYXlsb2FkQ29udGFpbnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIqwCChRBdWRpdEFnZ3JlZ2F0ZUJ1Y2tldBILCgNrZXkYASABKAkSDQoFY291bnQYAiABKAMSDAoEa2V5cxgDIAMoCRJECgdtZXRyaWNzGAQgAygLMjMuc2Fhcy5hY2NvdW50cy52MS5BdWRpdEFnZ3JlZ2F0ZUJ1Y2tldC5NZXRyaWNzRW50cnkSRAoHc2FtcGxlcxgFIAMoCzIzLnNhYXMuYWNjb3VudHMudjEuQXVkaXRBZ2dyZWdhdGVCdWNrZXQuU2FtcGxlc0VudHJ5Gi4KDE1ldHJpY3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAE6AjgBGi4KDFNhbXBsZXNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAM6AjgBInQKGUFnZ3JlZ2F0ZUF1ZGl0TG9nUmVzcG9uc2USNwoHYnVja2V0cxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuQXVkaXRBZ2dyZWdhdGVCdWNrZXQSHgoWc2NvcGVfY29udHJhY3RfdmVyc2lvbhgCIAEoDSIcChpMaXN0QXVkaXRFdmVudFR5cGVzUmVxdWVzdCLBAQoOQXVkaXRFdmVudFR5cGUSDAoEbmFtZRgBIAEoCRIPCgd2ZXJzaW9uGAIgASgFEhAKCGNhdGVnb3J5GAMgASgJEg0KBW93bmVyGAQgASgJEhIKCmRlcHJlY2F0ZWQYBSABKAgSEwoLZGVzY3JpcHRpb24YBiABKAkSEQoJbmFtZXNwYWNlGAcgASgJEhkKEW1hcmtzX3VzZXJfam9pbmVkGAggASgIEhgKEHdlYmhvb2tfZWxpZ2libGUYCSABKAgiTgobTGlzdEF1ZGl0RXZlbnRUeXBlc1Jlc3BvbnNlEi8KBXR5cGVzGAEgAygLMiAuc2Fhcy5hY2NvdW50cy52MS5BdWRpdEV2ZW50VHlwZTKdBgoMQXVkaXRTZXJ2aWNlErUBCg1RdWVyeUF1ZGl0TG9nEiYuc2Fhcy5hY2NvdW50cy52MS5RdWVyeUF1ZGl0TG9nUmVxdWVzdBonLnNhYXMuYWNjb3VudHMudjEuUXVlcnlBdWRpdExvZ1Jlc3BvbnNlIlPC8xg6CAIQAxoKYXVkaXQ6cmVhZCIKYXVkaXQ6cmVhZCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCDxINL3YxL2F1ZGl0LWxvZxLLAQoRQWdncmVnYXRlQXVkaXRMb2cSKi5zYWFzLmFjY291bnRzLnYxLkFnZ3JlZ2F0ZUF1ZGl0TG9nUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuQWdncmVnYXRlQXVkaXRMb2dSZXNwb25zZSJdwvMYOggCEAMaCmF1ZGl0OnJlYWQiCmF1ZGl0OnJlYWQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAhkSFy92MS9hdWRpdC1sb2c6YWdncmVnYXRlEsEBChNMaXN0QXVkaXRFdmVudFR5cGVzEiwuc2Fhcy5hY2NvdW50cy52MS5MaXN0QXVkaXRFdmVudFR5cGVzUmVxdWVzdBotLnNhYXMuYWNjb3VudHMudjEuTGlzdEF1ZGl0RXZlbnRUeXBlc1Jlc3BvbnNlIk3C8xgsCAIQARoKYXVkaXQ6cmVhZCIKYXVkaXQ6cmVhZDABOgIQAUABSANQAlgCYAGC0+STAhcSFS92MS9hdWRpdC1ldmVudC10eXBlcxLCAQoORXhwb3J0QXVkaXRMb2cSJy5zYWFzLmFjY291bnRzLnYxLkV4cG9ydEF1ZGl0TG9nUmVxdWVzdBooLnNhYXMuYWNjb3VudHMudjEuRXhwb3J0QXVkaXRMb2dSZXNwb25zZSJdwvMYOggCEAQaCmF1ZGl0OnJlYWQiCmF1ZGl0OnJlYWQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAhk6ASoiFC92MS9hdWRpdC1sb2c6ZXhwb3J0QrIBChRjb20uc2Fhcy5hY2NvdW50cy52MUIKQXVkaXRQcm90b1ABWixhY2NvdW50cy9wa2cvZ2VuL3NhYXMvYWNjb3VudHMvdjE7YWNjb3VudHN2MaICA1NBWKoCEFNhYXMuQWNjb3VudHMuVjHKAhBTYWFzXEFjY291bnRzXFYx4gIcU2Fhc1xBY2NvdW50c1xWMVxHUEJNZXRhZGF0YeoCElNhYXM6OkFjY291bnRzOjpWMWIGcHJvdG8z", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_struct, file_google_protobuf_timestamp, file_saas_policy_v1_options]); + fileDesc("ChxzYWFzL2FjY291bnRzL3YxL2F1ZGl0LnByb3RvEhBzYWFzLmFjY291bnRzLnYxIrsDCgpBdWRpdEV2ZW50EgoKAmlkGAEgASgJEhAKCGFjdG9yX2lkGAIgASgJEhIKCmFjdG9yX3R5cGUYAyABKAkSEgoGYWN0aW9uGAQgASgJQgIYARIQCghyZXNvdXJjZRgFIAEoCRITCgtyZXNvdXJjZV9pZBgGIAEoCRIOCgZvcmdfaWQYByABKAkSQAoIbWV0YWRhdGEYCCADKAsyKi5zYWFzLmFjY291bnRzLnYxLkF1ZGl0RXZlbnQuTWV0YWRhdGFFbnRyeUICGAESEgoKaXBfYWRkcmVzcxgJIAEoCRIuCgpjcmVhdGVkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBISCgpldmVudF90eXBlGAsgASgJEhYKDnNjaGVtYV92ZXJzaW9uGAwgASgFEigKB3BheWxvYWQYDSABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EhAKCGNhdGVnb3J5GA4gASgJEhEKCWNsaWVudF9pZBgPIAEoCRovCg1NZXRhZGF0YUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEi+AMKFFF1ZXJ5QXVkaXRMb2dSZXF1ZXN0Eg4KBm9yZ19pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCRISCgZhY3Rpb24YAyABKAlCAhgBEhAKCHJlc291cmNlGAQgASgJEhMKC3Jlc291cmNlX2lkGAUgASgJEigKBGZyb20YBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEiYKAnRvGAcgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIcCglwYWdlX3NpemUYCCABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAkgASgJEhIKCmV2ZW50X3R5cGUYCiABKAkSEAoIY2F0ZWdvcnkYCyABKAkSVQoQcGF5bG9hZF9jb250YWlucxgMIAMoCzI7LnNhYXMuYWNjb3VudHMudjEuUXVlcnlBdWRpdExvZ1JlcXVlc3QuUGF5bG9hZENvbnRhaW5zRW50cnkSEQoJbmFtZXNwYWNlGA0gASgJEhEKCWNsaWVudF9pZBgOIAEoCRIkCgtldmVudF90eXBlcxgPIAMoCUIPukgMkgEJEEAiBXIDGP8BGjYKFFBheWxvYWRDb250YWluc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEicwoVUXVlcnlBdWRpdExvZ1Jlc3BvbnNlEiwKBmV2ZW50cxgBIAMoCzIcLnNhYXMuYWNjb3VudHMudjEuQXVkaXRFdmVudBIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkSEwoLdG90YWxfY291bnQYAyABKAUilwEKFUV4cG9ydEF1ZGl0TG9nUmVxdWVzdBIOCgZvcmdfaWQYASABKAkSDgoGZm9ybWF0GAIgASgJEhAKCGFjdG9yX2lkGAMgASgJEhIKBmFjdGlvbhgEIAEoCUICGAESEgoKZXZlbnRfdHlwZRgFIAEoCRIkCgtldmVudF90eXBlcxgGIAMoCUIPukgMkgEJEEAiBXIDGP8BIk4KFkV4cG9ydEF1ZGl0TG9nUmVzcG9uc2USDAoEZGF0YRgBIAEoDBIUCgxjb250ZW50X3R5cGUYAiABKAkSEAoIZmlsZW5hbWUYAyABKAkiSwoLQXVkaXRNZXRyaWMSCgoCb3AYASABKAkSDQoFZmllbGQYAiABKAkSEgoKcGVyY2VudGlsZRgDIAEoARINCgVhbGlhcxgEIAEoCSJLChJBdWRpdERlcml2ZWRNZXRyaWMSDQoFYWxpYXMYASABKAkSEQoJbnVtZXJhdG9yGAIgASgJEhMKC2Rlbm9taW5hdG9yGAMgASgJIu0EChhBZ2dyZWdhdGVBdWRpdExvZ1JlcXVlc3QSDgoGb3JnX2lkGAEgASgJEhAKCGFjdG9yX2lkGAIgASgJEhIKCmV2ZW50X3R5cGUYAyABKAkSEAoIY2F0ZWdvcnkYBCABKAkSEAoIcmVzb3VyY2UYBSABKAkSKAoEZnJvbRgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASJgoCdG8YByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhAKCGdyb3VwX2J5GAggASgJEg4KBmJ1Y2tldBgJIAEoCRIRCglncm91cF9ieXMYCiADKAkSLgoHbWV0cmljcxgLIAMoCzIdLnNhYXMuYWNjb3VudHMudjEuQXVkaXRNZXRyaWMSNQoHZGVyaXZlZBgMIAMoCzIkLnNhYXMuYWNjb3VudHMudjEuQXVkaXREZXJpdmVkTWV0cmljEhEKCW5hbWVzcGFjZRgNIAEoCRITCgtyZXNvdXJjZV9pZBgOIAEoCRJZChBwYXlsb2FkX2NvbnRhaW5zGA8gAygLMj8uc2Fhcy5hY2NvdW50cy52MS5BZ2dyZWdhdGVBdWRpdExvZ1JlcXVlc3QuUGF5bG9hZENvbnRhaW5zRW50cnkSFQoNY29sbGVjdGlvbl9pZBgQIAEoCRIRCgljbGllbnRfaWQYESABKAkSJAoLZXZlbnRfdHlwZXMYEiADKAlCD7pIDJIBCRBAIgVyAxj/ARo2ChRQYXlsb2FkQ29udGFpbnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIqwCChRBdWRpdEFnZ3JlZ2F0ZUJ1Y2tldBILCgNrZXkYASABKAkSDQoFY291bnQYAiABKAMSDAoEa2V5cxgDIAMoCRJECgdtZXRyaWNzGAQgAygLMjMuc2Fhcy5hY2NvdW50cy52MS5BdWRpdEFnZ3JlZ2F0ZUJ1Y2tldC5NZXRyaWNzRW50cnkSRAoHc2FtcGxlcxgFIAMoCzIzLnNhYXMuYWNjb3VudHMudjEuQXVkaXRBZ2dyZWdhdGVCdWNrZXQuU2FtcGxlc0VudHJ5Gi4KDE1ldHJpY3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAE6AjgBGi4KDFNhbXBsZXNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAM6AjgBInQKGUFnZ3JlZ2F0ZUF1ZGl0TG9nUmVzcG9uc2USNwoHYnVja2V0cxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuQXVkaXRBZ2dyZWdhdGVCdWNrZXQSHgoWc2NvcGVfY29udHJhY3RfdmVyc2lvbhgCIAEoDSIcChpMaXN0QXVkaXRFdmVudFR5cGVzUmVxdWVzdCLBAQoOQXVkaXRFdmVudFR5cGUSDAoEbmFtZRgBIAEoCRIPCgd2ZXJzaW9uGAIgASgFEhAKCGNhdGVnb3J5GAMgASgJEg0KBW93bmVyGAQgASgJEhIKCmRlcHJlY2F0ZWQYBSABKAgSEwoLZGVzY3JpcHRpb24YBiABKAkSEQoJbmFtZXNwYWNlGAcgASgJEhkKEW1hcmtzX3VzZXJfam9pbmVkGAggASgIEhgKEHdlYmhvb2tfZWxpZ2libGUYCSABKAgiTgobTGlzdEF1ZGl0RXZlbnRUeXBlc1Jlc3BvbnNlEi8KBXR5cGVzGAEgAygLMiAuc2Fhcy5hY2NvdW50cy52MS5BdWRpdEV2ZW50VHlwZSJLChlEZWxpdmVyQXVkaXRFdmVudHNSZXF1ZXN0EhcKBGZyb20YASABKAlCCbpIBnIEEAEYQBIVCgJ0bxgCIAEoCUIJukgGcgQQARhAImwKGkRlbGl2ZXJBdWRpdEV2ZW50c1Jlc3BvbnNlEhEKCWVmZmVjdF9pZBgBIAEoCRIOCgZzdGF0dXMYAiABKAkSEQoJZGVsaXZlcmVkGAMgASgFEgwKBGZyb20YBCABKAkSCgoCdG8YBSABKAky6QkKDEF1ZGl0U2VydmljZRK1AQoNUXVlcnlBdWRpdExvZxImLnNhYXMuYWNjb3VudHMudjEuUXVlcnlBdWRpdExvZ1JlcXVlc3QaJy5zYWFzLmFjY291bnRzLnYxLlF1ZXJ5QXVkaXRMb2dSZXNwb25zZSJTwvMYOggCEAMaCmF1ZGl0OnJlYWQiCmF1ZGl0OnJlYWQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAg8SDS92MS9hdWRpdC1sb2cSywEKEUFnZ3JlZ2F0ZUF1ZGl0TG9nEiouc2Fhcy5hY2NvdW50cy52MS5BZ2dyZWdhdGVBdWRpdExvZ1JlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLkFnZ3JlZ2F0ZUF1ZGl0TG9nUmVzcG9uc2UiXcLzGDoIAhADGgphdWRpdDpyZWFkIgphdWRpdDpyZWFkKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIZEhcvdjEvYXVkaXQtbG9nOmFnZ3JlZ2F0ZRLBAQoTTGlzdEF1ZGl0RXZlbnRUeXBlcxIsLnNhYXMuYWNjb3VudHMudjEuTGlzdEF1ZGl0RXZlbnRUeXBlc1JlcXVlc3QaLS5zYWFzLmFjY291bnRzLnYxLkxpc3RBdWRpdEV2ZW50VHlwZXNSZXNwb25zZSJNwvMYLAgCEAEaCmF1ZGl0OnJlYWQiCmF1ZGl0OnJlYWQwAToCEAFAAUgDUAJYAmABgtPkkwIXEhUvdjEvYXVkaXQtZXZlbnQtdHlwZXMSwgEKDkV4cG9ydEF1ZGl0TG9nEicuc2Fhcy5hY2NvdW50cy52MS5FeHBvcnRBdWRpdExvZ1JlcXVlc3QaKC5zYWFzLmFjY291bnRzLnYxLkV4cG9ydEF1ZGl0TG9nUmVzcG9uc2UiXcLzGDoIAhAEGgphdWRpdDpyZWFkIgphdWRpdDpyZWFkKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIZOgEqIhQvdjEvYXVkaXQtbG9nOmV4cG9ydBK3AgoSRGVsaXZlckF1ZGl0RXZlbnRzEisuc2Fhcy5hY2NvdW50cy52MS5EZWxpdmVyQXVkaXRFdmVudHNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5EZWxpdmVyQXVkaXRFdmVudHNSZXNwb25zZSLFAcLzGBQIAhABMAE6AhABQANIBFACWAJgAdrzGKgBCgIIPBIDCNgEGAMiAggFKgtVTkFWQUlMQUJMRSoSUkVTT1VSQ0VfRVhIQVVTVEVEKgdBQk9SVEVEMgpzYWFzLWF1ZGl0Sjcvc2Fhcy5hY2NvdW50cy52MS5BdWRpdFNlcnZpY2UvTG9va3VwRGVsaXZlckF1ZGl0RXZlbnRzUIACWIAIYAFyIAoOYXVkaXRfZGVsaXZlcnkSBmludm9rZRIEcmVhZBgBEo8BChhMb29rdXBEZWxpdmVyQXVkaXRFdmVudHMSKy5zYWFzLmFjY291bnRzLnYxLkRlbGl2ZXJBdWRpdEV2ZW50c1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkRlbGl2ZXJBdWRpdEV2ZW50c1Jlc3BvbnNlIhjC8xgUCAIQATABOgIQAUABSANQAlgCYAFCsgEKFGNvbS5zYWFzLmFjY291bnRzLnYxQgpBdWRpdFByb3RvUAFaLGFjY291bnRzL3BrZy9nZW4vc2Fhcy9hY2NvdW50cy92MTthY2NvdW50c3YxogIDU0FYqgIQU2Fhcy5BY2NvdW50cy5WMcoCEFNhYXNcQWNjb3VudHNcVjHiAhxTYWFzXEFjY291bnRzXFYxXEdQQk1ldGFkYXRh6gISU2Fhczo6QWNjb3VudHM6OlYxYgZwcm90bzM", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_struct, file_google_protobuf_timestamp, file_codefly_runnable_v0_options, file_saas_policy_v1_options]); /** * @generated from message saas.accounts.v1.AuditEvent @@ -360,7 +361,11 @@ export type AuditMetric = Message<"saas.accounts.v1.AuditMetric"> & { field: string; /** - * percentile ∈ (0,1], used only when op == percentile (0.95 → p95). + * percentile ∈ (0,1], used only when op == percentile (0.95 → p95). Only the + * Postgres audit backend computes it: the warehouse backend keeps entries + * at-least-once and deduplicates them on read, and a percentile over that is not + * an exact answer, so a deployment on the warehouse backend refuses a query that + * asks for one with InvalidArgument. Every other op is served by both backends. * * @generated from field: double percentile = 3; */ @@ -733,6 +738,87 @@ export type ListAuditEventTypesResponse = Message<"saas.accounts.v1.ListAuditEve export const ListAuditEventTypesResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_saas_accounts_v1_audit, 12); +/** + * DeliverAuditEventsRequest names one range of the platform journal, (from, to], + * whose audit records the operation appends to the warehouse audit storage. Both + * ends are positions a ModuleReadJournal page returned; from may instead be the + * literal "start", meaning before the oldest retained entry, and is never empty so + * that the first range of a fresh journal has a non-empty value to correlate on. + * The range ends at a position that is final (below the oldest transaction still + * running) and holds at most 500 audit records, or it is refused before anything + * is appended. The effect id is "saas.audit.recorded.v1/", derived by the + * host: one range, one receipt. A runtime task may carry its own transport + * effect identity; the range receipt is independent of that task identity. + * + * @generated from message saas.accounts.v1.DeliverAuditEventsRequest + */ +export type DeliverAuditEventsRequest = Message<"saas.accounts.v1.DeliverAuditEventsRequest"> & { + /** + * @generated from field: string from = 1; + */ + from: string; + + /** + * @generated from field: string to = 2; + */ + to: string; +}; + +/** + * Describes the message saas.accounts.v1.DeliverAuditEventsRequest. + * Use `create(DeliverAuditEventsRequestSchema)` to create a new message. + */ +export const DeliverAuditEventsRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_audit, 13); + +/** + * @generated from message saas.accounts.v1.DeliverAuditEventsResponse + */ +export type DeliverAuditEventsResponse = Message<"saas.accounts.v1.DeliverAuditEventsResponse"> & { + /** + * @generated from field: string effect_id = 1; + */ + effectId: string; + + /** + * status is "committed" when the range is recorded. An absent lookup + * returns NOT_FOUND: absence is inconclusive, never a committed response. + * + * @generated from field: string status = 2; + */ + status: string; + + /** + * delivered is the number of audit records of the range appended to the + * storage; a range with none still commits a receipt. + * + * @generated from field: int32 delivered = 3; + */ + delivered: number; + + /** + * The exact committed interval is (from, to]. Retention verifies these + * positions against the receipt's effect identity and request digest before + * retiring journal rows. Legacy receipts without both positions prove no + * coverage; delivered alone never proves an interval. + * + * @generated from field: string from = 4; + */ + from: string; + + /** + * @generated from field: string to = 5; + */ + to: string; +}; + +/** + * Describes the message saas.accounts.v1.DeliverAuditEventsResponse. + * Use `create(DeliverAuditEventsResponseSchema)` to create a new message. + */ +export const DeliverAuditEventsResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_audit, 14); + /** * AuditService — append-only audit event log * @@ -771,6 +857,33 @@ export const AuditService: GenService<{ input: typeof ExportAuditLogRequestSchema; output: typeof ExportAuditLogResponseSchema; }, + /** + * DeliverAuditEvents appends one range of the journal's audit records to the + * warehouse audit storage: the operation module-runtime calls after reading the + * range through ReadJournal. It is an operation, not a sweep. It takes each + * record's organization from the journal row and never from the caller, emits no + * audit of its own, and commits one receipt per range under the installation's + * tenant; delivery is at least once, and every read of the storage returns each + * entry once by id. Its receipt is read back through LookupDeliverAuditEvents. + * + * @generated from rpc saas.accounts.v1.AuditService.DeliverAuditEvents + */ + deliverAuditEvents: { + methodKind: "unary"; + input: typeof DeliverAuditEventsRequestSchema; + output: typeof DeliverAuditEventsResponseSchema; + }, + /** + * LookupDeliverAuditEvents takes the invocation's same {from, to} body and + * derives the range receipt identity from from. It never delivers. + * + * @generated from rpc saas.accounts.v1.AuditService.LookupDeliverAuditEvents + */ + lookupDeliverAuditEvents: { + methodKind: "unary"; + input: typeof DeliverAuditEventsRequestSchema; + output: typeof DeliverAuditEventsResponseSchema; + }, }> = /*@__PURE__*/ serviceDesc(file_saas_accounts_v1_audit, 0); diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/module_capabilities_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/module_capabilities_pb.ts index dfabb2c50..e6b336294 100644 --- a/module/services/frontend/code/src/gen/saas/accounts/v1/module_capabilities_pb.ts +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/module_capabilities_pb.ts @@ -22,7 +22,7 @@ import type { JsonObject, Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/module_capabilities.proto. */ export const file_saas_accounts_v1_module_capabilities: GenFile = /*@__PURE__*/ - fileDesc("CipzYWFzL2FjY291bnRzL3YxL21vZHVsZV9jYXBhYmlsaXRpZXMucHJvdG8SEHNhYXMuYWNjb3VudHMudjEiXgoXTW9kdWxlRW5xdWV1ZUpvYlJlcXVlc3QSGAoGdGVuYW50GAEgASgJQgi6SAVyA7ABARIpCgNqb2IYAiABKAsyFC5zYWFzLmpvYnMudjEuTmV3Sm9iQga6SAPIAQEidgoYTW9kdWxlRW5xdWV1ZUpvYlJlc3BvbnNlEhgKBmpvYl9pZBgBIAEoCUIIukgFcgOwAQESQAoLZGlzcG9zaXRpb24YAiABKA4yIy5zYWFzLmpvYnMudjEuSm9iRW5xdWV1ZURpc3Bvc2l0aW9uQga6SAPIAQEiygEKFk1vZHVsZUNsYWltSm9ic1JlcXVlc3QSLgoFcXVldWUYASABKAlCH7pIHHIaEAEYgAEyE15bYS16XVthLXowLTlfLi1dKiQSHQoJd29ya2VyX2lkGAIgASgJQgq6SAdyBRABGP8BEhgKBWxpbWl0GAMgASgNQgm6SAYqBBhkKAESRwoObGVhc2VfZHVyYXRpb24YBCABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25CFLpIEcgBAaoBCyIDCJAcMgQQwIQ9IkIKF01vZHVsZUNsYWltSm9ic1Jlc3BvbnNlEicKBGpvYnMYASADKAsyGS5zYWFzLmpvYnMudjEuSm9iRW52ZWxvcGUilwEKGU1vZHVsZUhlYXJ0YmVhdEpvYlJlcXVlc3QSNgoFbGVhc2UYASABKAsyHy5zYWFzLmpvYnMudjEuSm9iTGVhc2VSZWZlcmVuY2VCBrpIA8gBARJCCglleHRlbnNpb24YAiABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25CFLpIEcgBAaoBCyIDCJAcMgQQwIQ9IksKGk1vZHVsZUhlYXJ0YmVhdEpvYlJlc3BvbnNlEi0KBWxlYXNlGAEgASgLMhYuc2Fhcy5qb2JzLnYxLkpvYkxlYXNlQga6SAPIAQEipgEKE01vZHVsZUFja0pvYlJlcXVlc3QSNgoFbGVhc2UYASABKAsyHy5zYWFzLmpvYnMudjEuSm9iTGVhc2VSZWZlcmVuY2VCBrpIA8gBARI3Cg5leGVjdXRpb25fa2luZBgCIAEoCUIfukgcchoYQDIWXiR8XlthLXpdW2EtejAtOV8uLV0qJBIeCgxleGVjdXRpb25faWQYAyABKAlCCLpIBXIDGP8BIsIBChRNb2R1bGVOYWNrSm9iUmVxdWVzdBI2CgVsZWFzZRgBIAEoCzIfLnNhYXMuam9icy52MS5Kb2JMZWFzZVJlZmVyZW5jZUIGukgDyAEBEjEKB2ZhaWx1cmUYAiABKAsyGC5zYWFzLmpvYnMudjEuSm9iRmFpbHVyZUIGukgDyAEBEhEKCXJldHJ5YWJsZRgDIAEoCBIsCghyZXRyeV9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAi9gEKF01vZHVsZU5vdGlmeVVzZXJSZXF1ZXN0EhgKBnRlbmFudBgBIAEoCUIIukgFcgOwAQESGQoHdXNlcl9pZBgCIAEoCUIIukgFcgOwAQESGQoFdGl0bGUYAyABKAlCCrpIB3IFEAEY5gcSFgoEYm9keRgEIAEoCUIIukgFcgMYgEASFQoEdHlwZRgFIAEoCUIHukgEcgIYQBIcCgphY3Rpb25fdXJsGAYgASgJQgi6SAVyAxiAEBIbCghjYXRlZ29yeRgHIAEoCUIJukgGcgQQARggEiEKD2lkZW1wb3RlbmN5X2tleRgIIAEoCUIIukgFcgMY/wEiRgoYTW9kdWxlTm90aWZ5VXNlclJlc3BvbnNlEhcKD25vdGlmaWNhdGlvbl9pZBgBIAEoCRIRCglkZWxpdmVyZWQYAiABKAgi4AEKHE1vZHVsZU5vdGlmeU9yZ0FkbWluc1JlcXVlc3QSGAoGdGVuYW50GAEgASgJQgi6SAVyA7ABARIZCgV0aXRsZRgCIAEoCUIKukgHcgUQARjmBxIWCgRib2R5GAMgASgJQgi6SAVyAxiAQBIVCgR0eXBlGAQgASgJQge6SARyAhhAEhwKCmFjdGlvbl91cmwYBSABKAlCCLpIBXIDGIAQEhsKCGNhdGVnb3J5GAYgASgJQgm6SAZyBBABGCASIQoPaWRlbXBvdGVuY3lfa2V5GAcgASgJQgi6SAVyAxj/ASIyCh1Nb2R1bGVOb3RpZnlPcmdBZG1pbnNSZXNwb25zZRIRCglkZWxpdmVyZWQYASABKAgiYwoUTW9kdWxlQXBwcm92YWxQb2xpY3kSFwoGcXVvcnVtGAEgASgNQge6SAQqAhhkEh4KDGFwcHJvdmVyX3NldBgCIAMoCUIIukgFkgECEGQSEgoKYWxsb3dfc2VsZhgDIAEoCCKbAQoPTW9kdWxlUmVzdW1lUmVmEi4KBXF1ZXVlGAEgASgJQh+6SBxyGhABGIABMhNeW2Etel1bYS16MC05Xy4tXSokEi4KBXRvcGljGAIgASgJQh+6SBxyGhABGP8BMhNeW2Etel1bYS16MC05Xy4tXSokEigKB3BheWxvYWQYAyABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0IpYDChxNb2R1bGVSZXF1ZXN0QXBwcm92YWxSZXF1ZXN0EhgKBnRlbmFudBgBIAEoCUIIukgFcgOwAQESHAoIcmVzb3VyY2UYAiABKAlCCrpIB3IFEAEYgAESGgoGYWN0aW9uGAMgASgJQgq6SAdyBRABGIABEigKB3N1YmplY3QYBCABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EiAKDHJlcXVlc3RlZF9ieRgFIAEoCUIKukgHcgUQARj/ARI2CgZwb2xpY3kYBiABKAsyJi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUFwcHJvdmFsUG9saWN5Ej0KCnJlc3VtZV9yZWYYByABKAsyIS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVJlc3VtZVJlZkIGukgDyAEBEi4KCmV4cGlyZXNfYXQYCCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC2VzY2FsYXRlX2F0GAkgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCI+Ch1Nb2R1bGVSZXF1ZXN0QXBwcm92YWxSZXNwb25zZRIdCgthcHByb3ZhbF9pZBgBIAEoCUIIukgFcgOwAQEiUwoYTW9kdWxlR2V0QXBwcm92YWxSZXF1ZXN0EhgKBnRlbmFudBgBIAEoCUIIukgFcgOwAQESHQoLYXBwcm92YWxfaWQYAiABKAlCCLpIBXIDsAEBIvUCCg5Nb2R1bGVBcHByb3ZhbBIKCgJpZBgBIAEoCRIOCgZ0ZW5hbnQYAiABKAkSEAoIcmVzb3VyY2UYAyABKAkSDgoGYWN0aW9uGAQgASgJEigKB3N1YmplY3QYBSABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EhQKDHJlcXVlc3RlZF9ieRgGIAEoCRIOCgZxdW9ydW0YByABKA0SDQoFc3RhdGUYCCABKAkSNQoKcmVzdW1lX3JlZhgJIAEoCzIhLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUmVzdW1lUmVmEi4KCmV4cGlyZXNfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC2VzY2FsYXRlX2F0GAsgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIuCgpjcmVhdGVkX2F0GAwgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCJwChtNb2R1bGVDYW5jZWxBcHByb3ZhbFJlcXVlc3QSGAoGdGVuYW50GAEgASgJQgi6SAVyA7ABARIdCgthcHByb3ZhbF9pZBgCIAEoCUIIukgFcgOwAQESGAoGcmVhc29uGAMgASgJQgi6SAVyAxiACCLuAQobTW9kdWxlRW1pdEF1ZGl0RXZlbnRSZXF1ZXN0Eg4KBnRlbmFudBgBIAEoCRIeCgpldmVudF90eXBlGAIgASgJQgq6SAdyBRABGIABEhkKBWFjdG9yGAMgASgJQgq6SAdyBRABGP8BEhwKCHNvbHV0aW9uGAQgASgJQgq6SAdyBRABGIABEhoKCGVudHJ5X2lkGAUgASgJQgi6SAVyAxj/ARInCgZmaWVsZHMYBiABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EiEKD2lkZW1wb3RlbmN5X2tleRgHIAEoCUIIukgFcgMY/wEinwEKG01vZHVsZUF1ZGl0RmllbGREZWNsYXJhdGlvbhIXCgRuYW1lGAEgASgJQgm6SAZyBBABGEASQAoEa2luZBgCIAEoDjImLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlQXVkaXRGaWVsZEtpbmRCCrpIB4IBBBABIAASGAoGdmFsdWVzGAMgAygJQgi6SAWSAQIQQBILCgNwaWkYBCABKAgi7wEKH01vZHVsZUF1ZGl0RXZlbnRUeXBlRGVjbGFyYXRpb24SGAoEdHlwZRgBIAEoCUIKukgHcgUQARiAARIdCgtkZXNjcmlwdGlvbhgCIAEoCUIIukgFcgMYgAgSRwoGZmllbGRzGAMgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVBdWRpdEZpZWxkRGVjbGFyYXRpb25CCLpIBZIBAhAgEkoKCnZpc2liaWxpdHkYBCABKA4yLC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUF1ZGl0RXZlbnRWaXNpYmlsaXR5Qgi6SAWCAQIQASKzAQojTW9kdWxlRGVjbGFyZUF1ZGl0RXZlbnRUeXBlc1JlcXVlc3QSPgoGcHJlZml4GAEgASgJQi66SCtyKRABGIABMiJeW2EtejAtOV0oPzpbYS16MC05Xy1dKlthLXowLTldKT8kEkwKBXR5cGVzGAIgAygLMjEuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVBdWRpdEV2ZW50VHlwZURlY2xhcmF0aW9uQgq6SAeSAQQIARBAIl0KJE1vZHVsZURlY2xhcmVBdWRpdEV2ZW50VHlwZXNSZXNwb25zZRIWCg5hZG1pdHRlZF90eXBlcxgBIAMoCRIdChVuYW1lc3BhY2VzX3Rha2VuX292ZXIYAiADKAkibQoWU3ViamVjdFZpc2liaWxpdHlHcmFudBIjChJ2aXNpYmxlX3N1YmplY3RfaWQYASABKAlCB7pIBHICEAESLgoKZXhwaXJlc19hdBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiYwoiTW9kdWxlTGlzdFN1YmplY3RWaXNpYmlsaXR5UmVxdWVzdBIYCgZ0ZW5hbnQYASABKAlCCLpIBXIDsAEBEiMKEXZpZXdlcl9zdWJqZWN0X2lkGAIgASgJQgi6SAVyA7ABASJfCiNNb2R1bGVMaXN0U3ViamVjdFZpc2liaWxpdHlSZXNwb25zZRI4CgZncmFudHMYASADKAsyKC5zYWFzLmFjY291bnRzLnYxLlN1YmplY3RWaXNpYmlsaXR5R3JhbnQiVwoaRmV0Y2hEYXRhc291cmNlQmxvYlJlcXVlc3QSGwoJc291cmNlX2lkGAEgASgJQgi6SAVyA7ABARIcCghibG9iX3NoYRgCIAEoCUIKukgHcgUQARj/ASJSChhGZXRjaERhdGFzb3VyY2VCbG9iQ2h1bmsSDAoEZGF0YRgBIAEoDBISCgp0b3RhbF9zaXplGAIgASgDEhQKDGNvbnRlbnRfdHlwZRgDIAEoCSJSChFEYXRhc291cmNlRmlsZVJlZhIbCgdpdGVtX2lkGAEgASgJQgq6SAdyBRABGIAgEiAKDGl0ZW1fdmVyc2lvbhgCIAEoCUIKukgHcgUQARj/ASKYAQobRmV0Y2hEYXRhc291cmNlRmlsZXNSZXF1ZXN0EhsKCXNvdXJjZV9pZBgBIAEoCUIIukgFcgOwAQESGwoHdmVyc2lvbhgCIAEoCUIKukgHcgUQARj/ARI/CgVmaWxlcxgDIAMoCzIjLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZUZpbGVSZWZCC7pICJIBBQgBEOgHIosBChREYXRhc291cmNlUHJvdmVuYW5jZRIRCglzb3VyY2VfaWQYASABKAkSDgoGb3JnX2lkGAIgASgJEhgKEGJvdW5kYXJ5X25vZGVfaWQYAyABKAkSDwoHdmVyc2lvbhgEIAEoCRIPCgdpdGVtX2lkGAUgASgJEhQKDGl0ZW1fdmVyc2lvbhgGIAEoCSLAAQoVRGF0YXNvdXJjZUl0ZW1SZWFkZXJzEjsKBWJhc2lzGAEgASgOMiwuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlSXRlbVJlYWRlcnNCYXNpcxIQCgh1c2VyX2lkcxgCIAMoCRIRCglncm91cF9pZHMYAyADKAkSGAoQYm91bmRhcnlfcmVhZGVycxgEIAEoCBIWCg51bm1hcHBlZF9jb3VudBgFIAEoBRITCgthY2xfdmVyc2lvbhgGIAEoCSK+AQoURGF0YXNvdXJjZUZpbGVIZWFkZXISOgoKcHJvdmVuYW5jZRgBIAEoCzImLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVByb3ZlbmFuY2USDAoEcGF0aBgCIAEoCRIUCgxjb250ZW50X3R5cGUYAyABKAkSDAoEc2l6ZRgEIAEoAxI4CgdyZWFkZXJzGAUgASgLMicuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlSXRlbVJlYWRlcnMibgoZRmV0Y2hEYXRhc291cmNlRmlsZXNGcmFtZRI4CgZoZWFkZXIYASABKAsyJi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VGaWxlSGVhZGVySAASDgoEZGF0YRgCIAEoDEgAQgcKBWZyYW1lIm4KGU1vZHVsZVB1Ymxpc2hFdmVudFJlcXVlc3QSGAoGdGVuYW50GAEgASgJQgi6SAVyA7ABARI3CghlbnZlbG9wZRgCIAEoCzIdLnNhYXMuZXZlbnRzLnYxLkV2ZW50RW52ZWxvcGVCBrpIA8gBASI4ChpNb2R1bGVQdWJsaXNoRXZlbnRSZXNwb25zZRIaCghldmVudF9pZBgBIAEoCUIIukgFcgOwAQEiyAEKFk1vZHVsZVN1YnNjcmliZVJlcXVlc3QSSwoMdHlwZV9wYXR0ZXJuGAEgASgJQjW6SDJyMBABGIABMileW2Etel1bYS16MC05XSooPzpcLlthLXowLTldKykqKD86XC5cKik/JBIuCgVxdWV1ZRgCIAEoCUIfukgcchoQARiAATITXlthLXpdW2EtejAtOV8uLV0qJBIxCghkZWxpdmVyeRgDIAEoDjIfLnNhYXMuYWNjb3VudHMudjEuRXZlbnREZWxpdmVyeSLJAQoSTW9kdWxlU3Vic2NyaXB0aW9uEgoKAmlkGAEgASgJEh8KF3N1YnNjcmliZXJfcHJpbmNpcGFsX2lkGAIgASgJEhQKDHR5cGVfcGF0dGVybhgDIAEoCRINCgVxdWV1ZRgEIAEoCRIxCghkZWxpdmVyeRgFIAEoDjIfLnNhYXMuYWNjb3VudHMudjEuRXZlbnREZWxpdmVyeRIuCgpjcmVhdGVkX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCJVChdNb2R1bGVTdWJzY3JpYmVSZXNwb25zZRI6CgxzdWJzY3JpcHRpb24YASABKAsyJC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVN1YnNjcmlwdGlvbiI9ChhNb2R1bGVVbnN1YnNjcmliZVJlcXVlc3QSIQoPc3Vic2NyaXB0aW9uX2lkGAEgASgJQgi6SAVyA7ABASIgCh5Nb2R1bGVMaXN0U3Vic2NyaXB0aW9uc1JlcXVlc3QiXgofTW9kdWxlTGlzdFN1YnNjcmlwdGlvbnNSZXNwb25zZRI7Cg1zdWJzY3JpcHRpb25zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVTdWJzY3JpcHRpb24iegoZTW9kdWxlUmVwbGF5RXZlbnRzUmVxdWVzdBIYCgZ0ZW5hbnQYASABKAlCCLpIBXIDsAEBEhgKBHR5cGUYAiABKAlCCrpIB3IFEAEYgAESKQoFc2luY2UYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIjEKGk1vZHVsZVJlcGxheUV2ZW50c1Jlc3BvbnNlEhMKC3JlZGVsaXZlcmVkGAEgASgFItMCChhSZWFkYWJsZVNvdXJjZUNvbGxlY3Rpb24SEQoJc291cmNlX2lkGAEgASgJEhMKC2JvdW5kYXJ5X2lkGAIgASgJEg4KBm9yaWdpbhgDIAEoCRIRCgljb250YWluZXIYBCABKAkSCwoDcmVmGAUgASgJEg0KBXBhdGhzGAYgAygJEhYKDmJvdW5kYXJ5X2xhYmVsGAcgASgJEj4KEGdyYW50X2Rpc2Nsb3N1cmUYCCABKA4yJC5zYWFzLmFjY291bnRzLnYxLk1ldGFkYXRhRGlzY2xvc3VyZRI+CgtyZWFkX2dyYW50cxgJIAMoCzIpLnNhYXMuYWNjb3VudHMudjEuUmVhZGFibGVDb2xsZWN0aW9uR3JhbnQSOAoEc3luYxgKIAEoCzIqLnNhYXMuYWNjb3VudHMudjEuQ29sbGVjdGlvblN5bmNQcm92ZW5hbmNlIrABChdSZWFkYWJsZUNvbGxlY3Rpb25HcmFudBIVCg1zdWJqZWN0X2xhYmVsGAEgASgJEhQKDHN1YmplY3Rfa2luZBgCIAEoCRIRCglyb2xlX25hbWUYAyABKAkSEgoKc2NvcGVfcGF0aBgEIAEoCRIRCglpbmhlcml0ZWQYBSABKAgSLgoKZXhwaXJlc19hdBgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiqQIKGENvbGxlY3Rpb25TeW5jUHJvdmVuYW5jZRIwCgVzdGFnZRgBIAEoDjIhLnNhYXMuYWNjb3VudHMudjEuU291cmNlU3luY1N0YWdlEiYKAmF0GAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIQCghyZXZpc2lvbhgDIAEoCRIPCgd0cmlnZ2VyGAQgASgJEkIKFHJlcXVlc3Rlcl9kaXNjbG9zdXJlGAUgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5NZXRhZGF0YURpc2Nsb3N1cmUSMAoMcmVxdWVzdGVkX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIaChJyZXF1ZXN0ZWRfYnlfbGFiZWwYByABKAkikwEKJExpc3RSZWFkYWJsZVNvdXJjZUNvbGxlY3Rpb25zUmVxdWVzdBIdCglwYWdlX3NpemUYASABKAVCCrpIBxoFGOgHKAASHAoKcGFnZV90b2tlbhgCIAEoCUIIukgFcgMYgBASLgoZdmlld2VyX3dvcmtfY29udGV4dF90b2tlbhgDIAEoCUILukgIcgYQARiAgAIigQEKJUxpc3RSZWFkYWJsZVNvdXJjZUNvbGxlY3Rpb25zUmVzcG9uc2USPwoLY29sbGVjdGlvbnMYASADKAsyKi5zYWFzLmFjY291bnRzLnYxLlJlYWRhYmxlU291cmNlQ29sbGVjdGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAki6QEKGE1vZHVsZVBsYWNlUmVjb3JkUmVxdWVzdBIYCgZ0ZW5hbnQYASABKAlCCLpIBXIDsAEBEj0KCnNjb3BlX3BhdGgYAiABKAlCKbpIJnIkEAEYgAQyHV5bYS16MC05X10rKD86XC5bYS16MC05X10rKSokEhcKBGtpbmQYAyABKAlCCbpIBnIEEAEYQBIXCgVsYWJlbBgEIAEoCUIIukgFcgMY/wESIQoNcmVzb3VyY2VfdHlwZRgFIAEoCUIKukgHcgUQARiAARIfCgtyZXNvdXJjZV9pZBgGIAEoCUIKukgHcgUQARiABCI2ChlNb2R1bGVQbGFjZVJlY29yZFJlc3BvbnNlEhkKB25vZGVfaWQYASABKAlCCLpIBXIDsAEBIrUBCiNDaGVja1dvcmtDb250ZXh0UmVjb3JkQWNjZXNzUmVxdWVzdBIhCg1yZXNvdXJjZV90eXBlGAEgASgJQgq6SAdyBRABGIACEh8KC3Jlc291cmNlX2lkGAIgASgJQgq6SAdyBRABGIAEEhoKBmFjdGlvbhgDIAEoCUIKukgHcgUQARiAAhIuChl2aWV3ZXJfd29ya19jb250ZXh0X3Rva2VuGAQgASgJQgu6SAhyBhABGICAAiJOCiRDaGVja1dvcmtDb250ZXh0UmVjb3JkQWNjZXNzUmVzcG9uc2USDwoHYWxsb3dlZBgBIAEoCBIVCg1zY29wZV9ub2RlX2lkGAIgASgJInwKKk1vZHVsZUV4Y2hhbmdlRGVsZWdhdGVkUmVhZEF1ZGllbmNlUmVxdWVzdBIeCgpiaW5kaW5nX2lkGAEgASgJQgq6SAdyBRABGIABEi4KGXBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4YAiABKAlCC7pICHIGEAEYgIACIoADCi9Nb2R1bGVFeGNoYW5nZURlbGVnYXRlZE9wZXJhdGlvbkF1ZGllbmNlUmVxdWVzdBIeCgpiaW5kaW5nX2lkGAEgASgJQgq6SAdyBRABGIABEiwKGXBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4YAiABKAlCCbpIBnIEGICAAhIOCgZsb29rdXAYAyABKAgSIgoNZGVsZWdhdGlvbl9pZBgEIAEoCUILukgI2AEBcgOwAQE6ygG6SMYBGsMBCjNleGNoYW5nZV9kZWxlZ2F0ZWRfb3BlcmF0aW9uX2F1ZGllbmNlLm9uZV9hdXRob3JpdHkSRmV4YWN0bHkgb25lIG9mIHBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4gYW5kIGRlbGVnYXRpb25faWQgaXMgcmVxdWlyZWQaRCh0aGlzLnBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4gIT0gJycpICE9ICh0aGlzLmRlbGVnYXRpb25faWQgIT0gJycpIn0KGkV4ZWN1dGFibGVBcnRpZmFjdENvbnRyYWN0EhcKBGtpbmQYASABKAlCCbpIBnIEEAEYQBIYCgRuYW1lGAIgASgJQgq6SAdyBRABGIAEEiwKBmRpZ2VzdBgDIAEoCUIcukgZchcyFV5zaGEyNTY6WzAtOWEtZl17NjR9JCLiAQoaRXhlY3V0YWJsZUFydGlmYWN0SWRlbnRpdHkSGgoGc2NoZW1hGAEgASgJQgq6SAdyBRABGIABEhoKBnNvdXJjZRgCIAEoCUIKukgHcgUQARiABBIcCgdzdWJqZWN0GAMgASgMQgu6SAh6BhABGICABBJKCgljb250cmFjdHMYBCADKAsyLC5zYWFzLmFjY291bnRzLnYxLkV4ZWN1dGFibGVBcnRpZmFjdENvbnRyYWN0Qgm6SAaSAQMQgAISIgoRZXhwZWN0ZWRfcmV2aXNpb24YBSABKANCB7pIBCICKAAi2wEKH01vZHVsZUV4ZWN1dGFibGVBcnRpZmFjdFJlcXVlc3QSLgoZcGFyZW50X3dvcmtfY29udGV4dF90b2tlbhgBIAEoCUILukgIcgYQARiAgAISIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABARIdCglwb2xpY3lfaWQYAyABKAlCCrpIB3IFEAEYgAESRgoIaWRlbnRpdHkYBCABKAsyLC5zYWFzLmFjY291bnRzLnYxLkV4ZWN1dGFibGVBcnRpZmFjdElkZW50aXR5Qga6SAPIAQEimQEKJU1vZHVsZVJldm9rZUV4ZWN1dGFibGVBcnRpZmFjdFJlcXVlc3QSLgoZcGFyZW50X3dvcmtfY29udGV4dF90b2tlbhgBIAEoCUILukgIcgYQARiAgAISIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABARIdCgthcHByb3ZhbF9pZBgDIAEoCUIIukgFcgOwAQEifQogTW9kdWxlRXhlY3V0YWJsZUFydGlmYWN0UmVzcG9uc2USFgoOc2NoZW1hX3ZlcnNpb24YASABKAkSFgoOcXVhbGlmaWVkX25hbWUYAiABKAkSDgoGZGlnZXN0GAMgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAQgASgDInUKIE1vZHVsZUN1cnJlbnRJbnN0YWxsYXRpb25SZXF1ZXN0Ei4KGXBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4YASABKAlCC7pICHIGEAEYgIACEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEimwEKIU1vZHVsZUN1cnJlbnRJbnN0YWxsYXRpb25SZXNwb25zZRIXCg9pbnN0YWxsYXRpb25faWQYASABKAkSEQoJdGVuYW50X2lkGAIgASgJEhsKCXRhcmdldF9pZBgEIAEoCUIIukgFcgOwAQESEgoKYmluZGluZ19pZBgFIAEoCUoECAMQBFITc29sdXRpb25faWRlbnRpZmllciJgChdXb3JrQ29udGV4dFNlYWxRdWVzdGlvbhIgCgxwcmluY2lwYWxfaWQYASABKAlCCrpIB3IFEAEYgAQSIwoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgq6SAdyBRABGIAEIkUKIVdvcmtDb250ZXh0UHJpbmNpcGFsRXBvY2hRdWVzdGlvbhIgCgxwcmluY2lwYWxfaWQYASABKAlCCrpIB3IFEAEYgAQiRAogV29ya0NvbnRleHRBcHByb3ZlZEJ1aWxkUXVlc3Rpb24SIAoMcHJpbmNpcGFsX2lkGAEgASgJQgq6SAdyBRABGIAEIkUKI1dvcmtDb250ZXh0T3BlcmF0aW9uQmluZGluZ1F1ZXN0aW9uEh4KCmJpbmRpbmdfaWQYASABKAlCCrpIB3IFEAEYgAQiUgooV29ya0NvbnRleHRBdXRob3JpemF0aW9uUmV2aXNpb25RdWVzdGlvbhImChJvd25lcl9wcmluY2lwYWxfaWQYASABKAlCCrpIB3IFEAEYgAQiQAogV29ya0NvbnRleHRBcHByb3ZhbEdyYW50UXVlc3Rpb24SHAoIZ3JhbnRfaWQYASABKAlCCrpIB3IFEAEYgAIihgEKFVdvcmtDb250ZXh0U2VhbEFuc3dlchI1CgVzdGF0ZRgBIAEoDjImLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRTZWFsU3RhdGUSFwoPaW5zdGFsbGF0aW9uX2lkGAIgASgJEh0KFWluc3RhbGxhdGlvbl9yZXZpc2lvbhgDIAEoBCKCAQofV29ya0NvbnRleHRQcmluY2lwYWxFcG9jaEFuc3dlchI6CgVzdGF0ZRgBIAEoDjIrLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRQcmluY2lwYWxTdGF0ZRIUCgxwcmluY2lwYWxfaWQYAiABKAkSDQoFZXBvY2gYAyABKAQipwEKHldvcmtDb250ZXh0QXBwcm92ZWRCdWlsZEFuc3dlchI+CgVzdGF0ZRgBIAEoDjIvLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRBcHByb3ZlZEJ1aWxkU3RhdGUSFAoMcHJpbmNpcGFsX2lkGAIgASgJEhQKDGltYWdlX2RpZ2VzdBgDIAEoCRIZChFidWlsZF9pbmNhcm5hdGlvbhgEIAEoBCLhAQohV29ya0NvbnRleHRPcGVyYXRpb25CaW5kaW5nQW5zd2VyEkEKBXN0YXRlGAEgASgOMjIuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dE9wZXJhdGlvbkJpbmRpbmdTdGF0ZRISCgpiaW5kaW5nX2lkGAIgASgJEhQKDHByaW5jaXBhbF9pZBgDIAEoCRIXCg9pbnN0YWxsYXRpb25faWQYBCABKAkSEAoIcmV2aXNpb24YBSABKAQSEwoLaW5jYXJuYXRpb24YBiABKAQSDwoHcmV2b2tlZBgHIAEoCCJWCiZXb3JrQ29udGV4dEF1dGhvcml6YXRpb25SZXZpc2lvbkFuc3dlchIaChJvd25lcl9wcmluY2lwYWxfaWQYASABKAkSEAoIcmV2aXNpb24YAiABKAQicgoeV29ya0NvbnRleHRBcHByb3ZhbEdyYW50QW5zd2VyEj4KBXN0YXRlGAEgASgOMi8uc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dEFwcHJvdmFsR3JhbnRTdGF0ZRIQCghncmFudF9pZBgCIAEoCSLJBAooTW9kdWxlUmVzb2x2ZVdvcmtDb250ZXh0QXV0aG9yaXR5UmVxdWVzdBIhCgx3b3JrX2NvbnRleHQYASABKAlCC7pICHIGEAEYgIACEjkKBHNlYWwYAyABKAsyKS5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0U2VhbFF1ZXN0aW9uSAASTgoPcHJpbmNpcGFsX2Vwb2NoGAQgASgLMjMuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFByaW5jaXBhbEVwb2NoUXVlc3Rpb25IABJMCg5hcHByb3ZlZF9idWlsZBgFIAEoCzIyLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRBcHByb3ZlZEJ1aWxkUXVlc3Rpb25IABJSChFvcGVyYXRpb25fYmluZGluZxgGIAEoCzI1LnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRPcGVyYXRpb25CaW5kaW5nUXVlc3Rpb25IABJcChZhdXRob3JpemF0aW9uX3JldmlzaW9uGAcgASgLMjouc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dEF1dGhvcml6YXRpb25SZXZpc2lvblF1ZXN0aW9uSAASTAoOYXBwcm92YWxfZ3JhbnQYCCABKAsyMi5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0QXBwcm92YWxHcmFudFF1ZXN0aW9uSABCEQoIcXVlc3Rpb24SBbpIAggBSgQIAhADUghhdWRpZW5jZSKCBAopTW9kdWxlUmVzb2x2ZVdvcmtDb250ZXh0QXV0aG9yaXR5UmVzcG9uc2USNwoEc2VhbBgBIAEoCzInLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRTZWFsQW5zd2VySAASTAoPcHJpbmNpcGFsX2Vwb2NoGAIgASgLMjEuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFByaW5jaXBhbEVwb2NoQW5zd2VySAASSgoOYXBwcm92ZWRfYnVpbGQYAyABKAsyMC5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0QXBwcm92ZWRCdWlsZEFuc3dlckgAElAKEW9wZXJhdGlvbl9iaW5kaW5nGAQgASgLMjMuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dE9wZXJhdGlvbkJpbmRpbmdBbnN3ZXJIABJaChZhdXRob3JpemF0aW9uX3JldmlzaW9uGAUgASgLMjguc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dEF1dGhvcml6YXRpb25SZXZpc2lvbkFuc3dlckgAEkoKDmFwcHJvdmFsX2dyYW50GAYgASgLMjAuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dEFwcHJvdmFsR3JhbnRBbnN3ZXJIAEIICgZhbnN3ZXIquAIKFE1vZHVsZUF1ZGl0RmllbGRLaW5kEicKI01PRFVMRV9BVURJVF9GSUVMRF9LSU5EX1VOU1BFQ0lGSUVEEAASIgoeTU9EVUxFX0FVRElUX0ZJRUxEX0tJTkRfU1RSSU5HEAESIAocTU9EVUxFX0FVRElUX0ZJRUxEX0tJTkRfVVVJRBACEh8KG01PRFVMRV9BVURJVF9GSUVMRF9LSU5EX0lOVBADEiIKHk1PRFVMRV9BVURJVF9GSUVMRF9LSU5EX05VTUJFUhAEEiAKHE1PRFVMRV9BVURJVF9GSUVMRF9LSU5EX0JPT0wQBRIgChxNT0RVTEVfQVVESVRfRklFTERfS0lORF9FTlVNEAYSKAokTU9EVUxFX0FVRElUX0ZJRUxEX0tJTkRfU1RSSU5HX0FSUkFZEAcqoQEKGk1vZHVsZUF1ZGl0RXZlbnRWaXNpYmlsaXR5Ei0KKU1PRFVMRV9BVURJVF9FVkVOVF9WSVNJQklMSVRZX1VOU1BFQ0lGSUVEEAASKAokTU9EVUxFX0FVRElUX0VWRU5UX1ZJU0lCSUxJVFlfVEVOQU5UEAESKgomTU9EVUxFX0FVRElUX0VWRU5UX1ZJU0lCSUxJVFlfRVhURVJOQUwQAircAQoaRGF0YXNvdXJjZUl0ZW1SZWFkZXJzQmFzaXMSLQopREFUQVNPVVJDRV9JVEVNX1JFQURFUlNfQkFTSVNfVU5TUEVDSUZJRUQQABIsCihEQVRBU09VUkNFX0lURU1fUkVBREVSU19CQVNJU19UUkFOU0xBVEVEEAESLworREFUQVNPVVJDRV9JVEVNX1JFQURFUlNfQkFTSVNfU09VUkNFX1NDT1BFRBACEjAKLERBVEFTT1VSQ0VfSVRFTV9SRUFERVJTX0JBU0lTX1VOVFJBTlNMQVRBQkxFEAMqaQoNRXZlbnREZWxpdmVyeRIeChpFVkVOVF9ERUxJVkVSWV9VTlNQRUNJRklFRBAAEhwKGEVWRU5UX0RFTElWRVJZX1VOT1JERVJFRBABEhoKFkVWRU5UX0RFTElWRVJZX09SREVSRUQQAip+ChJNZXRhZGF0YURpc2Nsb3N1cmUSIwofTUVUQURBVEFfRElTQ0xPU1VSRV9VTlNQRUNJRklFRBAAEiEKHU1FVEFEQVRBX0RJU0NMT1NVUkVfRElTQ0xPU0VEEAESIAocTUVUQURBVEFfRElTQ0xPU1VSRV9XSVRISEVMRBACKlwKD1NvdXJjZVN5bmNTdGFnZRIhCh1TT1VSQ0VfU1lOQ19TVEFHRV9VTlNQRUNJRklFRBAAEiYKIlNPVVJDRV9TWU5DX1NUQUdFX0NIQU5HRVNfRU5RVUVVRUQQASqFAQoUV29ya0NvbnRleHRTZWFsU3RhdGUSJwojV09SS19DT05URVhUX1NFQUxfU1RBVEVfVU5TUEVDSUZJRUQQABIgChxXT1JLX0NPTlRFWFRfU0VBTF9TVEFURV9IRUxEEAESIgoeV09SS19DT05URVhUX1NFQUxfU1RBVEVfVU5IRUxEEAIqmwEKGVdvcmtDb250ZXh0UHJpbmNpcGFsU3RhdGUSLAooV09SS19DT05URVhUX1BSSU5DSVBBTF9TVEFURV9VTlNQRUNJRklFRBAAEiYKIldPUktfQ09OVEVYVF9QUklOQ0lQQUxfU1RBVEVfS05PV04QARIoCiRXT1JLX0NPTlRFWFRfUFJJTkNJUEFMX1NUQVRFX1VOS05PV04QAiq8AQodV29ya0NvbnRleHRBcHByb3ZlZEJ1aWxkU3RhdGUSMQotV09SS19DT05URVhUX0FQUFJPVkVEX0JVSUxEX1NUQVRFX1VOU1BFQ0lGSUVEEAASLgoqV09SS19DT05URVhUX0FQUFJPVkVEX0JVSUxEX1NUQVRFX0FQUFJPVkVEEAESOAo0V09SS19DT05URVhUX0FQUFJPVkVEX0JVSUxEX1NUQVRFX0JFQVJTX05PX0VYRUNVVElPThACKrwBCiBXb3JrQ29udGV4dE9wZXJhdGlvbkJpbmRpbmdTdGF0ZRI0CjBXT1JLX0NPTlRFWFRfT1BFUkFUSU9OX0JJTkRJTkdfU1RBVEVfVU5TUEVDSUZJRUQQABIxCi1XT1JLX0NPTlRFWFRfT1BFUkFUSU9OX0JJTkRJTkdfU1RBVEVfUkVTT0xWRUQQARIvCitXT1JLX0NPTlRFWFRfT1BFUkFUSU9OX0JJTkRJTkdfU1RBVEVfQUJTRU5UEAIqhgEKHVdvcmtDb250ZXh0QXBwcm92YWxHcmFudFN0YXRlEjEKLVdPUktfQ09OVEVYVF9BUFBST1ZBTF9HUkFOVF9TVEFURV9VTlNQRUNJRklFRBAAEjIKLldPUktfQ09OVEVYVF9BUFBST1ZBTF9HUkFOVF9TVEFURV9OT1RfUkVDT1JERUQQATLgJwoZTW9kdWxlQ2FwYWJpbGl0aWVzU2VydmljZRKbAQoWR2V0Q3VycmVudEluc3RhbGxhdGlvbhIyLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlQ3VycmVudEluc3RhbGxhdGlvblJlcXVlc3QaMy5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUN1cnJlbnRJbnN0YWxsYXRpb25SZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUARYA2ABEsYBChlBcHByb3ZlRXhlY3V0YWJsZUFydGlmYWN0EjEuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFeGVjdXRhYmxlQXJ0aWZhY3RSZXF1ZXN0GjIuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFeGVjdXRhYmxlQXJ0aWZhY3RSZXNwb25zZSJCwvMYPggDEAEwATosCihzYWFzLm1vZHVsZS5leGVjdXRhYmxlX2FydGlmYWN0X2RlY2lzaW9uEAJAAUgHUARYAmABEp4BChtBdXRob3JpemVFeGVjdXRhYmxlQXJ0aWZhY3QSMS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUV4ZWN1dGFibGVBcnRpZmFjdFJlcXVlc3QaMi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUV4ZWN1dGFibGVBcnRpZmFjdFJlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQBFgCYAESywEKGFJldm9rZUV4ZWN1dGFibGVBcnRpZmFjdBI3LnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUmV2b2tlRXhlY3V0YWJsZUFydGlmYWN0UmVxdWVzdBoyLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlRXhlY3V0YWJsZUFydGlmYWN0UmVzcG9uc2UiQsLzGD4IAxABMAE6LAooc2Fhcy5tb2R1bGUuZXhlY3V0YWJsZV9hcnRpZmFjdF9kZWNpc2lvbhACQAFIB1AEWAJgARLFAQodRXhjaGFuZ2VEZWxlZ2F0ZWRSZWFkQXVkaWVuY2USPC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUV4Y2hhbmdlRGVsZWdhdGVkUmVhZEF1ZGllbmNlUmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuSXNzdWVkV29ya0NvbnRleHQiQcLzGD0IAxABMAE6Kwonc2Fhcy5tb2R1bGUuZGVsZWdhdGVkX2F1ZGllbmNlX2V4Y2hhbmdlEARAAUgHUARYBGABEs8BCiJFeGNoYW5nZURlbGVnYXRlZE9wZXJhdGlvbkF1ZGllbmNlEkEuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFeGNoYW5nZURlbGVnYXRlZE9wZXJhdGlvbkF1ZGllbmNlUmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuSXNzdWVkV29ya0NvbnRleHQiQcLzGD0IAxABMAE6Kwonc2Fhcy5tb2R1bGUuZGVsZWdhdGVkX2F1ZGllbmNlX2V4Y2hhbmdlEARAAUgHUARYBGABEqcBChxDaGVja1dvcmtDb250ZXh0UmVjb3JkQWNjZXNzEjUuc2Fhcy5hY2NvdW50cy52MS5DaGVja1dvcmtDb250ZXh0UmVjb3JkQWNjZXNzUmVxdWVzdBo2LnNhYXMuYWNjb3VudHMudjEuQ2hlY2tXb3JrQ29udGV4dFJlY29yZEFjY2Vzc1Jlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESqgEKHUxpc3RSZWFkYWJsZVNvdXJjZUNvbGxlY3Rpb25zEjYuc2Fhcy5hY2NvdW50cy52MS5MaXN0UmVhZGFibGVTb3VyY2VDb2xsZWN0aW9uc1JlcXVlc3QaNy5zYWFzLmFjY291bnRzLnYxLkxpc3RSZWFkYWJsZVNvdXJjZUNvbGxlY3Rpb25zUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARKAAQoLUGxhY2VSZWNvcmQSKi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVBsYWNlUmVjb3JkUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUGxhY2VSZWNvcmRSZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEn0KCkVucXVldWVKb2ISKS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUVucXVldWVKb2JSZXF1ZXN0Giouc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFbnF1ZXVlSm9iUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARJ6CglDbGFpbUpvYnMSKC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUNsYWltSm9ic1JlcXVlc3QaKS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUNsYWltSm9ic1Jlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESgwEKDEhlYXJ0YmVhdEpvYhIrLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlSGVhcnRiZWF0Sm9iUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlSGVhcnRiZWF0Sm9iUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARJhCgZBY2tKb2ISJS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUFja0pvYlJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARJjCgdOYWNrSm9iEiYuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVOYWNrSm9iUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEn0KCk5vdGlmeVVzZXISKS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZU5vdGlmeVVzZXJSZXF1ZXN0Giouc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVOb3RpZnlVc2VyUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARKtAQoPTm90aWZ5T3JnQWRtaW5zEi4uc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVOb3RpZnlPcmdBZG1pbnNSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVOb3RpZnlPcmdBZG1pbnNSZXNwb25zZSI5wvMYNQgDEAEwATojCh9zYWFzLm1vZHVsZS5vcmdfYWRtaW5zX25vdGlmaWVkEAJAAUgHUANYA2ABEowBCg9SZXF1ZXN0QXBwcm92YWwSLi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVJlcXVlc3RBcHByb3ZhbFJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVJlcXVlc3RBcHByb3ZhbFJlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESdQoLR2V0QXBwcm92YWwSKi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUdldEFwcHJvdmFsUmVxdWVzdBogLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlQXBwcm92YWwiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARJxCg5DYW5jZWxBcHByb3ZhbBItLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlQ2FuY2VsQXBwcm92YWxSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAEScQoORW1pdEF1ZGl0RXZlbnQSLS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUVtaXRBdWRpdEV2ZW50UmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEsMBChZEZWNsYXJlQXVkaXRFdmVudFR5cGVzEjUuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVEZWNsYXJlQXVkaXRFdmVudFR5cGVzUmVxdWVzdBo2LnNhYXMuYWNjb3VudHMudjEuTW9kdWxlRGVjbGFyZUF1ZGl0RXZlbnRUeXBlc1Jlc3BvbnNlIjrC8xg2CAMQATABOiQKIHNhYXMubW9kdWxlLmF1ZGl0X3R5cGVzX2RlY2xhcmVkEAJAAUgHUANYA2ABEp4BChVMaXN0U3ViamVjdFZpc2liaWxpdHkSNC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUxpc3RTdWJqZWN0VmlzaWJpbGl0eVJlcXVlc3QaNS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUxpc3RTdWJqZWN0VmlzaWJpbGl0eVJlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESjgEKE0ZldGNoRGF0YXNvdXJjZUJsb2ISLC5zYWFzLmFjY291bnRzLnYxLkZldGNoRGF0YXNvdXJjZUJsb2JSZXF1ZXN0Giouc2Fhcy5hY2NvdW50cy52MS5GZXRjaERhdGFzb3VyY2VCbG9iQ2h1bmsiG4gCAcLzGBQIAxABMAE6AhABQAFIB1ADWANgATABEo4BChRGZXRjaERhdGFzb3VyY2VGaWxlcxItLnNhYXMuYWNjb3VudHMudjEuRmV0Y2hEYXRhc291cmNlRmlsZXNSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5GZXRjaERhdGFzb3VyY2VGaWxlc0ZyYW1lIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAEwARKzAQoVTWludE1vZHVsZVdvcmtDb250ZXh0Ei4uc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVNaW50V29ya0NvbnRleHRSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVNaW50V29ya0NvbnRleHRSZXNwb25zZSI5wvMYNQgDEAEwATojCh9zYWFzLm1vZHVsZS53b3JrX2NvbnRleHRfbWludGVkEAJAAUgHUARYBGABEscBChpNaW50TW9kdWxlT3BlcmF0aW9uQ29udGV4dBIzLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlTWludE9wZXJhdGlvbkNvbnRleHRSZXF1ZXN0GjQuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVNaW50T3BlcmF0aW9uQ29udGV4dFJlc3BvbnNlIj7C8xg6CAMQATABOigKJHNhYXMubW9kdWxlLm9wZXJhdGlvbl9jb250ZXh0X21pbnRlZBACQAFIB1AEWARgARLOAQoaTWludFNvdXJjZU9wZXJhdGlvbkNvbnRleHQSOS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZU1pbnRTb3VyY2VPcGVyYXRpb25Db250ZXh0UmVxdWVzdBo6LnNhYXMuYWNjb3VudHMudjEuTW9kdWxlTWludFNvdXJjZU9wZXJhdGlvbkNvbnRleHRSZXNwb25zZSI5wvMYNQgDEAEwATojCh9zYWFzLmRhdGFzb3VyY2UuZGVsZWdhdGlvbi51c2VkEAJAAUgHUARYBGABEoMBCgxQdWJsaXNoRXZlbnQSKy5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVB1Ymxpc2hFdmVudFJlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVB1Ymxpc2hFdmVudFJlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESegoJU3Vic2NyaWJlEiguc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVTdWJzY3JpYmVSZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVTdWJzY3JpYmVSZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEmsKC1Vuc3Vic2NyaWJlEiouc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVVbnN1YnNjcmliZVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARKSAQoRTGlzdFN1YnNjcmlwdGlvbnMSMC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUxpc3RTdWJzY3JpcHRpb25zUmVxdWVzdBoxLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlTGlzdFN1YnNjcmlwdGlvbnNSZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEoMBCgxSZXBsYXlFdmVudHMSKy5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVJlcGxheUV2ZW50c1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVJlcGxheUV2ZW50c1Jlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESsAEKG1Jlc29sdmVXb3JrQ29udGV4dEF1dGhvcml0eRI6LnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUmVzb2x2ZVdvcmtDb250ZXh0QXV0aG9yaXR5UmVxdWVzdBo7LnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUmVzb2x2ZVdvcmtDb250ZXh0QXV0aG9yaXR5UmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgAUK/AQoUY29tLnNhYXMuYWNjb3VudHMudjFCF01vZHVsZUNhcGFiaWxpdGllc1Byb3RvUAFaLGFjY291bnRzL3BrZy9nZW4vc2Fhcy9hY2NvdW50cy92MTthY2NvdW50c3YxogIDU0FYqgIQU2Fhcy5BY2NvdW50cy5WMcoCEFNhYXNcQWNjb3VudHNcVjHiAhxTYWFzXEFjY291bnRzXFYxXEdQQk1ldGFkYXRh6gISU2Fhczo6QWNjb3VudHM6OlYxYgZwcm90bzM", [file_saas_accounts_v1_work_contexts, file_buf_validate_validate, file_google_protobuf_empty, file_google_protobuf_duration, file_google_protobuf_struct, file_google_protobuf_timestamp, file_saas_accounts_v1_module_registration, file_saas_events_v1_events, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); + fileDesc("CipzYWFzL2FjY291bnRzL3YxL21vZHVsZV9jYXBhYmlsaXRpZXMucHJvdG8SEHNhYXMuYWNjb3VudHMudjEiXgoXTW9kdWxlRW5xdWV1ZUpvYlJlcXVlc3QSGAoGdGVuYW50GAEgASgJQgi6SAVyA7ABARIpCgNqb2IYAiABKAsyFC5zYWFzLmpvYnMudjEuTmV3Sm9iQga6SAPIAQEidgoYTW9kdWxlRW5xdWV1ZUpvYlJlc3BvbnNlEhgKBmpvYl9pZBgBIAEoCUIIukgFcgOwAQESQAoLZGlzcG9zaXRpb24YAiABKA4yIy5zYWFzLmpvYnMudjEuSm9iRW5xdWV1ZURpc3Bvc2l0aW9uQga6SAPIAQEiygEKFk1vZHVsZUNsYWltSm9ic1JlcXVlc3QSLgoFcXVldWUYASABKAlCH7pIHHIaEAEYgAEyE15bYS16XVthLXowLTlfLi1dKiQSHQoJd29ya2VyX2lkGAIgASgJQgq6SAdyBRABGP8BEhgKBWxpbWl0GAMgASgNQgm6SAYqBBhkKAESRwoObGVhc2VfZHVyYXRpb24YBCABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25CFLpIEcgBAaoBCyIDCJAcMgQQwIQ9IkIKF01vZHVsZUNsYWltSm9ic1Jlc3BvbnNlEicKBGpvYnMYASADKAsyGS5zYWFzLmpvYnMudjEuSm9iRW52ZWxvcGUilwEKGU1vZHVsZUhlYXJ0YmVhdEpvYlJlcXVlc3QSNgoFbGVhc2UYASABKAsyHy5zYWFzLmpvYnMudjEuSm9iTGVhc2VSZWZlcmVuY2VCBrpIA8gBARJCCglleHRlbnNpb24YAiABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25CFLpIEcgBAaoBCyIDCJAcMgQQwIQ9IksKGk1vZHVsZUhlYXJ0YmVhdEpvYlJlc3BvbnNlEi0KBWxlYXNlGAEgASgLMhYuc2Fhcy5qb2JzLnYxLkpvYkxlYXNlQga6SAPIAQEipgEKE01vZHVsZUFja0pvYlJlcXVlc3QSNgoFbGVhc2UYASABKAsyHy5zYWFzLmpvYnMudjEuSm9iTGVhc2VSZWZlcmVuY2VCBrpIA8gBARI3Cg5leGVjdXRpb25fa2luZBgCIAEoCUIfukgcchoYQDIWXiR8XlthLXpdW2EtejAtOV8uLV0qJBIeCgxleGVjdXRpb25faWQYAyABKAlCCLpIBXIDGP8BIsIBChRNb2R1bGVOYWNrSm9iUmVxdWVzdBI2CgVsZWFzZRgBIAEoCzIfLnNhYXMuam9icy52MS5Kb2JMZWFzZVJlZmVyZW5jZUIGukgDyAEBEjEKB2ZhaWx1cmUYAiABKAsyGC5zYWFzLmpvYnMudjEuSm9iRmFpbHVyZUIGukgDyAEBEhEKCXJldHJ5YWJsZRgDIAEoCBIsCghyZXRyeV9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAi9gEKF01vZHVsZU5vdGlmeVVzZXJSZXF1ZXN0EhgKBnRlbmFudBgBIAEoCUIIukgFcgOwAQESGQoHdXNlcl9pZBgCIAEoCUIIukgFcgOwAQESGQoFdGl0bGUYAyABKAlCCrpIB3IFEAEY5gcSFgoEYm9keRgEIAEoCUIIukgFcgMYgEASFQoEdHlwZRgFIAEoCUIHukgEcgIYQBIcCgphY3Rpb25fdXJsGAYgASgJQgi6SAVyAxiAEBIbCghjYXRlZ29yeRgHIAEoCUIJukgGcgQQARggEiEKD2lkZW1wb3RlbmN5X2tleRgIIAEoCUIIukgFcgMY/wEiRgoYTW9kdWxlTm90aWZ5VXNlclJlc3BvbnNlEhcKD25vdGlmaWNhdGlvbl9pZBgBIAEoCRIRCglkZWxpdmVyZWQYAiABKAgi4AEKHE1vZHVsZU5vdGlmeU9yZ0FkbWluc1JlcXVlc3QSGAoGdGVuYW50GAEgASgJQgi6SAVyA7ABARIZCgV0aXRsZRgCIAEoCUIKukgHcgUQARjmBxIWCgRib2R5GAMgASgJQgi6SAVyAxiAQBIVCgR0eXBlGAQgASgJQge6SARyAhhAEhwKCmFjdGlvbl91cmwYBSABKAlCCLpIBXIDGIAQEhsKCGNhdGVnb3J5GAYgASgJQgm6SAZyBBABGCASIQoPaWRlbXBvdGVuY3lfa2V5GAcgASgJQgi6SAVyAxj/ASIyCh1Nb2R1bGVOb3RpZnlPcmdBZG1pbnNSZXNwb25zZRIRCglkZWxpdmVyZWQYASABKAgiYwoUTW9kdWxlQXBwcm92YWxQb2xpY3kSFwoGcXVvcnVtGAEgASgNQge6SAQqAhhkEh4KDGFwcHJvdmVyX3NldBgCIAMoCUIIukgFkgECEGQSEgoKYWxsb3dfc2VsZhgDIAEoCCKbAQoPTW9kdWxlUmVzdW1lUmVmEi4KBXF1ZXVlGAEgASgJQh+6SBxyGhABGIABMhNeW2Etel1bYS16MC05Xy4tXSokEi4KBXRvcGljGAIgASgJQh+6SBxyGhABGP8BMhNeW2Etel1bYS16MC05Xy4tXSokEigKB3BheWxvYWQYAyABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0IpYDChxNb2R1bGVSZXF1ZXN0QXBwcm92YWxSZXF1ZXN0EhgKBnRlbmFudBgBIAEoCUIIukgFcgOwAQESHAoIcmVzb3VyY2UYAiABKAlCCrpIB3IFEAEYgAESGgoGYWN0aW9uGAMgASgJQgq6SAdyBRABGIABEigKB3N1YmplY3QYBCABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EiAKDHJlcXVlc3RlZF9ieRgFIAEoCUIKukgHcgUQARj/ARI2CgZwb2xpY3kYBiABKAsyJi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUFwcHJvdmFsUG9saWN5Ej0KCnJlc3VtZV9yZWYYByABKAsyIS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVJlc3VtZVJlZkIGukgDyAEBEi4KCmV4cGlyZXNfYXQYCCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC2VzY2FsYXRlX2F0GAkgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCI+Ch1Nb2R1bGVSZXF1ZXN0QXBwcm92YWxSZXNwb25zZRIdCgthcHByb3ZhbF9pZBgBIAEoCUIIukgFcgOwAQEiUwoYTW9kdWxlR2V0QXBwcm92YWxSZXF1ZXN0EhgKBnRlbmFudBgBIAEoCUIIukgFcgOwAQESHQoLYXBwcm92YWxfaWQYAiABKAlCCLpIBXIDsAEBIvUCCg5Nb2R1bGVBcHByb3ZhbBIKCgJpZBgBIAEoCRIOCgZ0ZW5hbnQYAiABKAkSEAoIcmVzb3VyY2UYAyABKAkSDgoGYWN0aW9uGAQgASgJEigKB3N1YmplY3QYBSABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EhQKDHJlcXVlc3RlZF9ieRgGIAEoCRIOCgZxdW9ydW0YByABKA0SDQoFc3RhdGUYCCABKAkSNQoKcmVzdW1lX3JlZhgJIAEoCzIhLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUmVzdW1lUmVmEi4KCmV4cGlyZXNfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC2VzY2FsYXRlX2F0GAsgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIuCgpjcmVhdGVkX2F0GAwgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCJwChtNb2R1bGVDYW5jZWxBcHByb3ZhbFJlcXVlc3QSGAoGdGVuYW50GAEgASgJQgi6SAVyA7ABARIdCgthcHByb3ZhbF9pZBgCIAEoCUIIukgFcgOwAQESGAoGcmVhc29uGAMgASgJQgi6SAVyAxiACCLuAQobTW9kdWxlRW1pdEF1ZGl0RXZlbnRSZXF1ZXN0Eg4KBnRlbmFudBgBIAEoCRIeCgpldmVudF90eXBlGAIgASgJQgq6SAdyBRABGIABEhkKBWFjdG9yGAMgASgJQgq6SAdyBRABGP8BEhwKCHNvbHV0aW9uGAQgASgJQgq6SAdyBRABGIABEhoKCGVudHJ5X2lkGAUgASgJQgi6SAVyAxj/ARInCgZmaWVsZHMYBiABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EiEKD2lkZW1wb3RlbmN5X2tleRgHIAEoCUIIukgFcgMY/wEinAEKGE1vZHVsZVJlYWRKb3VybmFsUmVxdWVzdBIWCgVhZnRlchgBIAEoCUIHukgEcgIYQBIWCgVsaW1pdBgCIAEoBUIHukgEGgIoABIiCgV0eXBlcxgDIAMoCUITukgQkgENCAEQECIHcgUQARiAAhIsChFkZWxpdmVyeV9iaW5kaW5ncxgEIAMoCUIRukgOkgELEEAiB3IFEAEYgAIi0wEKEk1vZHVsZUpvdXJuYWxFbnRyeRIQCghwb3NpdGlvbhgBIAEoCRIQCghldmVudF9pZBgCIAEoCRIMCgR0eXBlGAMgASgJEhcKD29yZ2FuaXphdGlvbl9pZBgEIAEoCRIvCgtvY2N1cnJlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASJQoEZGF0YRgGIAEoCzIXLmdvb2dsZS5wcm90b2J1Zi5TdHJ1Y3QSGgoSZGVsaXZlcnlfbWFjaGluZXJ5GAcgASgIImYKGU1vZHVsZVJlYWRKb3VybmFsUmVzcG9uc2USNQoHZW50cmllcxgBIAMoCzIkLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlSm91cm5hbEVudHJ5EhIKCm5leHRfYWZ0ZXIYAiABKAkinwEKG01vZHVsZUF1ZGl0RmllbGREZWNsYXJhdGlvbhIXCgRuYW1lGAEgASgJQgm6SAZyBBABGEASQAoEa2luZBgCIAEoDjImLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlQXVkaXRGaWVsZEtpbmRCCrpIB4IBBBABIAASGAoGdmFsdWVzGAMgAygJQgi6SAWSAQIQQBILCgNwaWkYBCABKAgi7wEKH01vZHVsZUF1ZGl0RXZlbnRUeXBlRGVjbGFyYXRpb24SGAoEdHlwZRgBIAEoCUIKukgHcgUQARiAARIdCgtkZXNjcmlwdGlvbhgCIAEoCUIIukgFcgMYgAgSRwoGZmllbGRzGAMgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVBdWRpdEZpZWxkRGVjbGFyYXRpb25CCLpIBZIBAhAgEkoKCnZpc2liaWxpdHkYBCABKA4yLC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUF1ZGl0RXZlbnRWaXNpYmlsaXR5Qgi6SAWCAQIQASKzAQojTW9kdWxlRGVjbGFyZUF1ZGl0RXZlbnRUeXBlc1JlcXVlc3QSPgoGcHJlZml4GAEgASgJQi66SCtyKRABGIABMiJeW2EtejAtOV0oPzpbYS16MC05Xy1dKlthLXowLTldKT8kEkwKBXR5cGVzGAIgAygLMjEuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVBdWRpdEV2ZW50VHlwZURlY2xhcmF0aW9uQgq6SAeSAQQIARBAIl0KJE1vZHVsZURlY2xhcmVBdWRpdEV2ZW50VHlwZXNSZXNwb25zZRIWCg5hZG1pdHRlZF90eXBlcxgBIAMoCRIdChVuYW1lc3BhY2VzX3Rha2VuX292ZXIYAiADKAkibQoWU3ViamVjdFZpc2liaWxpdHlHcmFudBIjChJ2aXNpYmxlX3N1YmplY3RfaWQYASABKAlCB7pIBHICEAESLgoKZXhwaXJlc19hdBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiYwoiTW9kdWxlTGlzdFN1YmplY3RWaXNpYmlsaXR5UmVxdWVzdBIYCgZ0ZW5hbnQYASABKAlCCLpIBXIDsAEBEiMKEXZpZXdlcl9zdWJqZWN0X2lkGAIgASgJQgi6SAVyA7ABASJfCiNNb2R1bGVMaXN0U3ViamVjdFZpc2liaWxpdHlSZXNwb25zZRI4CgZncmFudHMYASADKAsyKC5zYWFzLmFjY291bnRzLnYxLlN1YmplY3RWaXNpYmlsaXR5R3JhbnQiVwoaRmV0Y2hEYXRhc291cmNlQmxvYlJlcXVlc3QSGwoJc291cmNlX2lkGAEgASgJQgi6SAVyA7ABARIcCghibG9iX3NoYRgCIAEoCUIKukgHcgUQARj/ASJSChhGZXRjaERhdGFzb3VyY2VCbG9iQ2h1bmsSDAoEZGF0YRgBIAEoDBISCgp0b3RhbF9zaXplGAIgASgDEhQKDGNvbnRlbnRfdHlwZRgDIAEoCSJSChFEYXRhc291cmNlRmlsZVJlZhIbCgdpdGVtX2lkGAEgASgJQgq6SAdyBRABGIAgEiAKDGl0ZW1fdmVyc2lvbhgCIAEoCUIKukgHcgUQARj/ASKYAQobRmV0Y2hEYXRhc291cmNlRmlsZXNSZXF1ZXN0EhsKCXNvdXJjZV9pZBgBIAEoCUIIukgFcgOwAQESGwoHdmVyc2lvbhgCIAEoCUIKukgHcgUQARj/ARI/CgVmaWxlcxgDIAMoCzIjLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZUZpbGVSZWZCC7pICJIBBQgBEOgHIosBChREYXRhc291cmNlUHJvdmVuYW5jZRIRCglzb3VyY2VfaWQYASABKAkSDgoGb3JnX2lkGAIgASgJEhgKEGJvdW5kYXJ5X25vZGVfaWQYAyABKAkSDwoHdmVyc2lvbhgEIAEoCRIPCgdpdGVtX2lkGAUgASgJEhQKDGl0ZW1fdmVyc2lvbhgGIAEoCSLAAQoVRGF0YXNvdXJjZUl0ZW1SZWFkZXJzEjsKBWJhc2lzGAEgASgOMiwuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlSXRlbVJlYWRlcnNCYXNpcxIQCgh1c2VyX2lkcxgCIAMoCRIRCglncm91cF9pZHMYAyADKAkSGAoQYm91bmRhcnlfcmVhZGVycxgEIAEoCBIWCg51bm1hcHBlZF9jb3VudBgFIAEoBRITCgthY2xfdmVyc2lvbhgGIAEoCSK+AQoURGF0YXNvdXJjZUZpbGVIZWFkZXISOgoKcHJvdmVuYW5jZRgBIAEoCzImLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVByb3ZlbmFuY2USDAoEcGF0aBgCIAEoCRIUCgxjb250ZW50X3R5cGUYAyABKAkSDAoEc2l6ZRgEIAEoAxI4CgdyZWFkZXJzGAUgASgLMicuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlSXRlbVJlYWRlcnMibgoZRmV0Y2hEYXRhc291cmNlRmlsZXNGcmFtZRI4CgZoZWFkZXIYASABKAsyJi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VGaWxlSGVhZGVySAASDgoEZGF0YRgCIAEoDEgAQgcKBWZyYW1lIm4KGU1vZHVsZVB1Ymxpc2hFdmVudFJlcXVlc3QSGAoGdGVuYW50GAEgASgJQgi6SAVyA7ABARI3CghlbnZlbG9wZRgCIAEoCzIdLnNhYXMuZXZlbnRzLnYxLkV2ZW50RW52ZWxvcGVCBrpIA8gBASI4ChpNb2R1bGVQdWJsaXNoRXZlbnRSZXNwb25zZRIaCghldmVudF9pZBgBIAEoCUIIukgFcgOwAQEiyAEKFk1vZHVsZVN1YnNjcmliZVJlcXVlc3QSSwoMdHlwZV9wYXR0ZXJuGAEgASgJQjW6SDJyMBABGIABMileW2Etel1bYS16MC05XSooPzpcLlthLXowLTldKykqKD86XC5cKik/JBIuCgVxdWV1ZRgCIAEoCUIfukgcchoQARiAATITXlthLXpdW2EtejAtOV8uLV0qJBIxCghkZWxpdmVyeRgDIAEoDjIfLnNhYXMuYWNjb3VudHMudjEuRXZlbnREZWxpdmVyeSLJAQoSTW9kdWxlU3Vic2NyaXB0aW9uEgoKAmlkGAEgASgJEh8KF3N1YnNjcmliZXJfcHJpbmNpcGFsX2lkGAIgASgJEhQKDHR5cGVfcGF0dGVybhgDIAEoCRINCgVxdWV1ZRgEIAEoCRIxCghkZWxpdmVyeRgFIAEoDjIfLnNhYXMuYWNjb3VudHMudjEuRXZlbnREZWxpdmVyeRIuCgpjcmVhdGVkX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCJVChdNb2R1bGVTdWJzY3JpYmVSZXNwb25zZRI6CgxzdWJzY3JpcHRpb24YASABKAsyJC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVN1YnNjcmlwdGlvbiI9ChhNb2R1bGVVbnN1YnNjcmliZVJlcXVlc3QSIQoPc3Vic2NyaXB0aW9uX2lkGAEgASgJQgi6SAVyA7ABASIgCh5Nb2R1bGVMaXN0U3Vic2NyaXB0aW9uc1JlcXVlc3QiXgofTW9kdWxlTGlzdFN1YnNjcmlwdGlvbnNSZXNwb25zZRI7Cg1zdWJzY3JpcHRpb25zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVTdWJzY3JpcHRpb24iegoZTW9kdWxlUmVwbGF5RXZlbnRzUmVxdWVzdBIYCgZ0ZW5hbnQYASABKAlCCLpIBXIDsAEBEhgKBHR5cGUYAiABKAlCCrpIB3IFEAEYgAESKQoFc2luY2UYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIjEKGk1vZHVsZVJlcGxheUV2ZW50c1Jlc3BvbnNlEhMKC3JlZGVsaXZlcmVkGAEgASgFItMCChhSZWFkYWJsZVNvdXJjZUNvbGxlY3Rpb24SEQoJc291cmNlX2lkGAEgASgJEhMKC2JvdW5kYXJ5X2lkGAIgASgJEg4KBm9yaWdpbhgDIAEoCRIRCgljb250YWluZXIYBCABKAkSCwoDcmVmGAUgASgJEg0KBXBhdGhzGAYgAygJEhYKDmJvdW5kYXJ5X2xhYmVsGAcgASgJEj4KEGdyYW50X2Rpc2Nsb3N1cmUYCCABKA4yJC5zYWFzLmFjY291bnRzLnYxLk1ldGFkYXRhRGlzY2xvc3VyZRI+CgtyZWFkX2dyYW50cxgJIAMoCzIpLnNhYXMuYWNjb3VudHMudjEuUmVhZGFibGVDb2xsZWN0aW9uR3JhbnQSOAoEc3luYxgKIAEoCzIqLnNhYXMuYWNjb3VudHMudjEuQ29sbGVjdGlvblN5bmNQcm92ZW5hbmNlIrABChdSZWFkYWJsZUNvbGxlY3Rpb25HcmFudBIVCg1zdWJqZWN0X2xhYmVsGAEgASgJEhQKDHN1YmplY3Rfa2luZBgCIAEoCRIRCglyb2xlX25hbWUYAyABKAkSEgoKc2NvcGVfcGF0aBgEIAEoCRIRCglpbmhlcml0ZWQYBSABKAgSLgoKZXhwaXJlc19hdBgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiqQIKGENvbGxlY3Rpb25TeW5jUHJvdmVuYW5jZRIwCgVzdGFnZRgBIAEoDjIhLnNhYXMuYWNjb3VudHMudjEuU291cmNlU3luY1N0YWdlEiYKAmF0GAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIQCghyZXZpc2lvbhgDIAEoCRIPCgd0cmlnZ2VyGAQgASgJEkIKFHJlcXVlc3Rlcl9kaXNjbG9zdXJlGAUgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5NZXRhZGF0YURpc2Nsb3N1cmUSMAoMcmVxdWVzdGVkX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIaChJyZXF1ZXN0ZWRfYnlfbGFiZWwYByABKAkikwEKJExpc3RSZWFkYWJsZVNvdXJjZUNvbGxlY3Rpb25zUmVxdWVzdBIdCglwYWdlX3NpemUYASABKAVCCrpIBxoFGOgHKAASHAoKcGFnZV90b2tlbhgCIAEoCUIIukgFcgMYgBASLgoZdmlld2VyX3dvcmtfY29udGV4dF90b2tlbhgDIAEoCUILukgIcgYQARiAgAIigQEKJUxpc3RSZWFkYWJsZVNvdXJjZUNvbGxlY3Rpb25zUmVzcG9uc2USPwoLY29sbGVjdGlvbnMYASADKAsyKi5zYWFzLmFjY291bnRzLnYxLlJlYWRhYmxlU291cmNlQ29sbGVjdGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAki6QEKGE1vZHVsZVBsYWNlUmVjb3JkUmVxdWVzdBIYCgZ0ZW5hbnQYASABKAlCCLpIBXIDsAEBEj0KCnNjb3BlX3BhdGgYAiABKAlCKbpIJnIkEAEYgAQyHV5bYS16MC05X10rKD86XC5bYS16MC05X10rKSokEhcKBGtpbmQYAyABKAlCCbpIBnIEEAEYQBIXCgVsYWJlbBgEIAEoCUIIukgFcgMY/wESIQoNcmVzb3VyY2VfdHlwZRgFIAEoCUIKukgHcgUQARiAARIfCgtyZXNvdXJjZV9pZBgGIAEoCUIKukgHcgUQARiABCI2ChlNb2R1bGVQbGFjZVJlY29yZFJlc3BvbnNlEhkKB25vZGVfaWQYASABKAlCCLpIBXIDsAEBIrUBCiNDaGVja1dvcmtDb250ZXh0UmVjb3JkQWNjZXNzUmVxdWVzdBIhCg1yZXNvdXJjZV90eXBlGAEgASgJQgq6SAdyBRABGIACEh8KC3Jlc291cmNlX2lkGAIgASgJQgq6SAdyBRABGIAEEhoKBmFjdGlvbhgDIAEoCUIKukgHcgUQARiAAhIuChl2aWV3ZXJfd29ya19jb250ZXh0X3Rva2VuGAQgASgJQgu6SAhyBhABGICAAiJOCiRDaGVja1dvcmtDb250ZXh0UmVjb3JkQWNjZXNzUmVzcG9uc2USDwoHYWxsb3dlZBgBIAEoCBIVCg1zY29wZV9ub2RlX2lkGAIgASgJInwKKk1vZHVsZUV4Y2hhbmdlRGVsZWdhdGVkUmVhZEF1ZGllbmNlUmVxdWVzdBIeCgpiaW5kaW5nX2lkGAEgASgJQgq6SAdyBRABGIABEi4KGXBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4YAiABKAlCC7pICHIGEAEYgIACIoADCi9Nb2R1bGVFeGNoYW5nZURlbGVnYXRlZE9wZXJhdGlvbkF1ZGllbmNlUmVxdWVzdBIeCgpiaW5kaW5nX2lkGAEgASgJQgq6SAdyBRABGIABEiwKGXBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4YAiABKAlCCbpIBnIEGICAAhIOCgZsb29rdXAYAyABKAgSIgoNZGVsZWdhdGlvbl9pZBgEIAEoCUILukgI2AEBcgOwAQE6ygG6SMYBGsMBCjNleGNoYW5nZV9kZWxlZ2F0ZWRfb3BlcmF0aW9uX2F1ZGllbmNlLm9uZV9hdXRob3JpdHkSRmV4YWN0bHkgb25lIG9mIHBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4gYW5kIGRlbGVnYXRpb25faWQgaXMgcmVxdWlyZWQaRCh0aGlzLnBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4gIT0gJycpICE9ICh0aGlzLmRlbGVnYXRpb25faWQgIT0gJycpIn0KGkV4ZWN1dGFibGVBcnRpZmFjdENvbnRyYWN0EhcKBGtpbmQYASABKAlCCbpIBnIEEAEYQBIYCgRuYW1lGAIgASgJQgq6SAdyBRABGIAEEiwKBmRpZ2VzdBgDIAEoCUIcukgZchcyFV5zaGEyNTY6WzAtOWEtZl17NjR9JCLiAQoaRXhlY3V0YWJsZUFydGlmYWN0SWRlbnRpdHkSGgoGc2NoZW1hGAEgASgJQgq6SAdyBRABGIABEhoKBnNvdXJjZRgCIAEoCUIKukgHcgUQARiABBIcCgdzdWJqZWN0GAMgASgMQgu6SAh6BhABGICABBJKCgljb250cmFjdHMYBCADKAsyLC5zYWFzLmFjY291bnRzLnYxLkV4ZWN1dGFibGVBcnRpZmFjdENvbnRyYWN0Qgm6SAaSAQMQgAISIgoRZXhwZWN0ZWRfcmV2aXNpb24YBSABKANCB7pIBCICKAAi2wEKH01vZHVsZUV4ZWN1dGFibGVBcnRpZmFjdFJlcXVlc3QSLgoZcGFyZW50X3dvcmtfY29udGV4dF90b2tlbhgBIAEoCUILukgIcgYQARiAgAISIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABARIdCglwb2xpY3lfaWQYAyABKAlCCrpIB3IFEAEYgAESRgoIaWRlbnRpdHkYBCABKAsyLC5zYWFzLmFjY291bnRzLnYxLkV4ZWN1dGFibGVBcnRpZmFjdElkZW50aXR5Qga6SAPIAQEimQEKJU1vZHVsZVJldm9rZUV4ZWN1dGFibGVBcnRpZmFjdFJlcXVlc3QSLgoZcGFyZW50X3dvcmtfY29udGV4dF90b2tlbhgBIAEoCUILukgIcgYQARiAgAISIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABARIdCgthcHByb3ZhbF9pZBgDIAEoCUIIukgFcgOwAQEifQogTW9kdWxlRXhlY3V0YWJsZUFydGlmYWN0UmVzcG9uc2USFgoOc2NoZW1hX3ZlcnNpb24YASABKAkSFgoOcXVhbGlmaWVkX25hbWUYAiABKAkSDgoGZGlnZXN0GAMgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAQgASgDInUKIE1vZHVsZUN1cnJlbnRJbnN0YWxsYXRpb25SZXF1ZXN0Ei4KGXBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4YASABKAlCC7pICHIGEAEYgIACEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEimwEKIU1vZHVsZUN1cnJlbnRJbnN0YWxsYXRpb25SZXNwb25zZRIXCg9pbnN0YWxsYXRpb25faWQYASABKAkSEQoJdGVuYW50X2lkGAIgASgJEhsKCXRhcmdldF9pZBgEIAEoCUIIukgFcgOwAQESEgoKYmluZGluZ19pZBgFIAEoCUoECAMQBFITc29sdXRpb25faWRlbnRpZmllciJgChdXb3JrQ29udGV4dFNlYWxRdWVzdGlvbhIgCgxwcmluY2lwYWxfaWQYASABKAlCCrpIB3IFEAEYgAQSIwoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgq6SAdyBRABGIAEIkUKIVdvcmtDb250ZXh0UHJpbmNpcGFsRXBvY2hRdWVzdGlvbhIgCgxwcmluY2lwYWxfaWQYASABKAlCCrpIB3IFEAEYgAQiRAogV29ya0NvbnRleHRBcHByb3ZlZEJ1aWxkUXVlc3Rpb24SIAoMcHJpbmNpcGFsX2lkGAEgASgJQgq6SAdyBRABGIAEIkUKI1dvcmtDb250ZXh0T3BlcmF0aW9uQmluZGluZ1F1ZXN0aW9uEh4KCmJpbmRpbmdfaWQYASABKAlCCrpIB3IFEAEYgAQiUgooV29ya0NvbnRleHRBdXRob3JpemF0aW9uUmV2aXNpb25RdWVzdGlvbhImChJvd25lcl9wcmluY2lwYWxfaWQYASABKAlCCrpIB3IFEAEYgAQiQAogV29ya0NvbnRleHRBcHByb3ZhbEdyYW50UXVlc3Rpb24SHAoIZ3JhbnRfaWQYASABKAlCCrpIB3IFEAEYgAIihgEKFVdvcmtDb250ZXh0U2VhbEFuc3dlchI1CgVzdGF0ZRgBIAEoDjImLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRTZWFsU3RhdGUSFwoPaW5zdGFsbGF0aW9uX2lkGAIgASgJEh0KFWluc3RhbGxhdGlvbl9yZXZpc2lvbhgDIAEoBCKCAQofV29ya0NvbnRleHRQcmluY2lwYWxFcG9jaEFuc3dlchI6CgVzdGF0ZRgBIAEoDjIrLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRQcmluY2lwYWxTdGF0ZRIUCgxwcmluY2lwYWxfaWQYAiABKAkSDQoFZXBvY2gYAyABKAQipwEKHldvcmtDb250ZXh0QXBwcm92ZWRCdWlsZEFuc3dlchI+CgVzdGF0ZRgBIAEoDjIvLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRBcHByb3ZlZEJ1aWxkU3RhdGUSFAoMcHJpbmNpcGFsX2lkGAIgASgJEhQKDGltYWdlX2RpZ2VzdBgDIAEoCRIZChFidWlsZF9pbmNhcm5hdGlvbhgEIAEoBCLhAQohV29ya0NvbnRleHRPcGVyYXRpb25CaW5kaW5nQW5zd2VyEkEKBXN0YXRlGAEgASgOMjIuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dE9wZXJhdGlvbkJpbmRpbmdTdGF0ZRISCgpiaW5kaW5nX2lkGAIgASgJEhQKDHByaW5jaXBhbF9pZBgDIAEoCRIXCg9pbnN0YWxsYXRpb25faWQYBCABKAkSEAoIcmV2aXNpb24YBSABKAQSEwoLaW5jYXJuYXRpb24YBiABKAQSDwoHcmV2b2tlZBgHIAEoCCJWCiZXb3JrQ29udGV4dEF1dGhvcml6YXRpb25SZXZpc2lvbkFuc3dlchIaChJvd25lcl9wcmluY2lwYWxfaWQYASABKAkSEAoIcmV2aXNpb24YAiABKAQicgoeV29ya0NvbnRleHRBcHByb3ZhbEdyYW50QW5zd2VyEj4KBXN0YXRlGAEgASgOMi8uc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dEFwcHJvdmFsR3JhbnRTdGF0ZRIQCghncmFudF9pZBgCIAEoCSLJBAooTW9kdWxlUmVzb2x2ZVdvcmtDb250ZXh0QXV0aG9yaXR5UmVxdWVzdBIhCgx3b3JrX2NvbnRleHQYASABKAlCC7pICHIGEAEYgIACEjkKBHNlYWwYAyABKAsyKS5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0U2VhbFF1ZXN0aW9uSAASTgoPcHJpbmNpcGFsX2Vwb2NoGAQgASgLMjMuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFByaW5jaXBhbEVwb2NoUXVlc3Rpb25IABJMCg5hcHByb3ZlZF9idWlsZBgFIAEoCzIyLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRBcHByb3ZlZEJ1aWxkUXVlc3Rpb25IABJSChFvcGVyYXRpb25fYmluZGluZxgGIAEoCzI1LnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRPcGVyYXRpb25CaW5kaW5nUXVlc3Rpb25IABJcChZhdXRob3JpemF0aW9uX3JldmlzaW9uGAcgASgLMjouc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dEF1dGhvcml6YXRpb25SZXZpc2lvblF1ZXN0aW9uSAASTAoOYXBwcm92YWxfZ3JhbnQYCCABKAsyMi5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0QXBwcm92YWxHcmFudFF1ZXN0aW9uSABCEQoIcXVlc3Rpb24SBbpIAggBSgQIAhADUghhdWRpZW5jZSKCBAopTW9kdWxlUmVzb2x2ZVdvcmtDb250ZXh0QXV0aG9yaXR5UmVzcG9uc2USNwoEc2VhbBgBIAEoCzInLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRTZWFsQW5zd2VySAASTAoPcHJpbmNpcGFsX2Vwb2NoGAIgASgLMjEuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFByaW5jaXBhbEVwb2NoQW5zd2VySAASSgoOYXBwcm92ZWRfYnVpbGQYAyABKAsyMC5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0QXBwcm92ZWRCdWlsZEFuc3dlckgAElAKEW9wZXJhdGlvbl9iaW5kaW5nGAQgASgLMjMuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dE9wZXJhdGlvbkJpbmRpbmdBbnN3ZXJIABJaChZhdXRob3JpemF0aW9uX3JldmlzaW9uGAUgASgLMjguc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dEF1dGhvcml6YXRpb25SZXZpc2lvbkFuc3dlckgAEkoKDmFwcHJvdmFsX2dyYW50GAYgASgLMjAuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dEFwcHJvdmFsR3JhbnRBbnN3ZXJIAEIICgZhbnN3ZXIquAIKFE1vZHVsZUF1ZGl0RmllbGRLaW5kEicKI01PRFVMRV9BVURJVF9GSUVMRF9LSU5EX1VOU1BFQ0lGSUVEEAASIgoeTU9EVUxFX0FVRElUX0ZJRUxEX0tJTkRfU1RSSU5HEAESIAocTU9EVUxFX0FVRElUX0ZJRUxEX0tJTkRfVVVJRBACEh8KG01PRFVMRV9BVURJVF9GSUVMRF9LSU5EX0lOVBADEiIKHk1PRFVMRV9BVURJVF9GSUVMRF9LSU5EX05VTUJFUhAEEiAKHE1PRFVMRV9BVURJVF9GSUVMRF9LSU5EX0JPT0wQBRIgChxNT0RVTEVfQVVESVRfRklFTERfS0lORF9FTlVNEAYSKAokTU9EVUxFX0FVRElUX0ZJRUxEX0tJTkRfU1RSSU5HX0FSUkFZEAcqoQEKGk1vZHVsZUF1ZGl0RXZlbnRWaXNpYmlsaXR5Ei0KKU1PRFVMRV9BVURJVF9FVkVOVF9WSVNJQklMSVRZX1VOU1BFQ0lGSUVEEAASKAokTU9EVUxFX0FVRElUX0VWRU5UX1ZJU0lCSUxJVFlfVEVOQU5UEAESKgomTU9EVUxFX0FVRElUX0VWRU5UX1ZJU0lCSUxJVFlfRVhURVJOQUwQAircAQoaRGF0YXNvdXJjZUl0ZW1SZWFkZXJzQmFzaXMSLQopREFUQVNPVVJDRV9JVEVNX1JFQURFUlNfQkFTSVNfVU5TUEVDSUZJRUQQABIsCihEQVRBU09VUkNFX0lURU1fUkVBREVSU19CQVNJU19UUkFOU0xBVEVEEAESLworREFUQVNPVVJDRV9JVEVNX1JFQURFUlNfQkFTSVNfU09VUkNFX1NDT1BFRBACEjAKLERBVEFTT1VSQ0VfSVRFTV9SRUFERVJTX0JBU0lTX1VOVFJBTlNMQVRBQkxFEAMqaQoNRXZlbnREZWxpdmVyeRIeChpFVkVOVF9ERUxJVkVSWV9VTlNQRUNJRklFRBAAEhwKGEVWRU5UX0RFTElWRVJZX1VOT1JERVJFRBABEhoKFkVWRU5UX0RFTElWRVJZX09SREVSRUQQAip+ChJNZXRhZGF0YURpc2Nsb3N1cmUSIwofTUVUQURBVEFfRElTQ0xPU1VSRV9VTlNQRUNJRklFRBAAEiEKHU1FVEFEQVRBX0RJU0NMT1NVUkVfRElTQ0xPU0VEEAESIAocTUVUQURBVEFfRElTQ0xPU1VSRV9XSVRISEVMRBACKlwKD1NvdXJjZVN5bmNTdGFnZRIhCh1TT1VSQ0VfU1lOQ19TVEFHRV9VTlNQRUNJRklFRBAAEiYKIlNPVVJDRV9TWU5DX1NUQUdFX0NIQU5HRVNfRU5RVUVVRUQQASqFAQoUV29ya0NvbnRleHRTZWFsU3RhdGUSJwojV09SS19DT05URVhUX1NFQUxfU1RBVEVfVU5TUEVDSUZJRUQQABIgChxXT1JLX0NPTlRFWFRfU0VBTF9TVEFURV9IRUxEEAESIgoeV09SS19DT05URVhUX1NFQUxfU1RBVEVfVU5IRUxEEAIqmwEKGVdvcmtDb250ZXh0UHJpbmNpcGFsU3RhdGUSLAooV09SS19DT05URVhUX1BSSU5DSVBBTF9TVEFURV9VTlNQRUNJRklFRBAAEiYKIldPUktfQ09OVEVYVF9QUklOQ0lQQUxfU1RBVEVfS05PV04QARIoCiRXT1JLX0NPTlRFWFRfUFJJTkNJUEFMX1NUQVRFX1VOS05PV04QAiq8AQodV29ya0NvbnRleHRBcHByb3ZlZEJ1aWxkU3RhdGUSMQotV09SS19DT05URVhUX0FQUFJPVkVEX0JVSUxEX1NUQVRFX1VOU1BFQ0lGSUVEEAASLgoqV09SS19DT05URVhUX0FQUFJPVkVEX0JVSUxEX1NUQVRFX0FQUFJPVkVEEAESOAo0V09SS19DT05URVhUX0FQUFJPVkVEX0JVSUxEX1NUQVRFX0JFQVJTX05PX0VYRUNVVElPThACKrwBCiBXb3JrQ29udGV4dE9wZXJhdGlvbkJpbmRpbmdTdGF0ZRI0CjBXT1JLX0NPTlRFWFRfT1BFUkFUSU9OX0JJTkRJTkdfU1RBVEVfVU5TUEVDSUZJRUQQABIxCi1XT1JLX0NPTlRFWFRfT1BFUkFUSU9OX0JJTkRJTkdfU1RBVEVfUkVTT0xWRUQQARIvCitXT1JLX0NPTlRFWFRfT1BFUkFUSU9OX0JJTkRJTkdfU1RBVEVfQUJTRU5UEAIqhgEKHVdvcmtDb250ZXh0QXBwcm92YWxHcmFudFN0YXRlEjEKLVdPUktfQ09OVEVYVF9BUFBST1ZBTF9HUkFOVF9TVEFURV9VTlNQRUNJRklFRBAAEjIKLldPUktfQ09OVEVYVF9BUFBST1ZBTF9HUkFOVF9TVEFURV9OT1RfUkVDT1JERUQQATLjKAoZTW9kdWxlQ2FwYWJpbGl0aWVzU2VydmljZRKbAQoWR2V0Q3VycmVudEluc3RhbGxhdGlvbhIyLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlQ3VycmVudEluc3RhbGxhdGlvblJlcXVlc3QaMy5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUN1cnJlbnRJbnN0YWxsYXRpb25SZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUARYA2ABEsYBChlBcHByb3ZlRXhlY3V0YWJsZUFydGlmYWN0EjEuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFeGVjdXRhYmxlQXJ0aWZhY3RSZXF1ZXN0GjIuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFeGVjdXRhYmxlQXJ0aWZhY3RSZXNwb25zZSJCwvMYPggDEAEwATosCihzYWFzLm1vZHVsZS5leGVjdXRhYmxlX2FydGlmYWN0X2RlY2lzaW9uEAJAAUgHUARYAmABEp4BChtBdXRob3JpemVFeGVjdXRhYmxlQXJ0aWZhY3QSMS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUV4ZWN1dGFibGVBcnRpZmFjdFJlcXVlc3QaMi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUV4ZWN1dGFibGVBcnRpZmFjdFJlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQBFgCYAESywEKGFJldm9rZUV4ZWN1dGFibGVBcnRpZmFjdBI3LnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUmV2b2tlRXhlY3V0YWJsZUFydGlmYWN0UmVxdWVzdBoyLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlRXhlY3V0YWJsZUFydGlmYWN0UmVzcG9uc2UiQsLzGD4IAxABMAE6LAooc2Fhcy5tb2R1bGUuZXhlY3V0YWJsZV9hcnRpZmFjdF9kZWNpc2lvbhACQAFIB1AEWAJgARLFAQodRXhjaGFuZ2VEZWxlZ2F0ZWRSZWFkQXVkaWVuY2USPC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUV4Y2hhbmdlRGVsZWdhdGVkUmVhZEF1ZGllbmNlUmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuSXNzdWVkV29ya0NvbnRleHQiQcLzGD0IAxABMAE6Kwonc2Fhcy5tb2R1bGUuZGVsZWdhdGVkX2F1ZGllbmNlX2V4Y2hhbmdlEARAAUgHUARYBGABEs8BCiJFeGNoYW5nZURlbGVnYXRlZE9wZXJhdGlvbkF1ZGllbmNlEkEuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFeGNoYW5nZURlbGVnYXRlZE9wZXJhdGlvbkF1ZGllbmNlUmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuSXNzdWVkV29ya0NvbnRleHQiQcLzGD0IAxABMAE6Kwonc2Fhcy5tb2R1bGUuZGVsZWdhdGVkX2F1ZGllbmNlX2V4Y2hhbmdlEARAAUgHUARYBGABEqcBChxDaGVja1dvcmtDb250ZXh0UmVjb3JkQWNjZXNzEjUuc2Fhcy5hY2NvdW50cy52MS5DaGVja1dvcmtDb250ZXh0UmVjb3JkQWNjZXNzUmVxdWVzdBo2LnNhYXMuYWNjb3VudHMudjEuQ2hlY2tXb3JrQ29udGV4dFJlY29yZEFjY2Vzc1Jlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESqgEKHUxpc3RSZWFkYWJsZVNvdXJjZUNvbGxlY3Rpb25zEjYuc2Fhcy5hY2NvdW50cy52MS5MaXN0UmVhZGFibGVTb3VyY2VDb2xsZWN0aW9uc1JlcXVlc3QaNy5zYWFzLmFjY291bnRzLnYxLkxpc3RSZWFkYWJsZVNvdXJjZUNvbGxlY3Rpb25zUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARKAAQoLUGxhY2VSZWNvcmQSKi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVBsYWNlUmVjb3JkUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUGxhY2VSZWNvcmRSZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEn0KCkVucXVldWVKb2ISKS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUVucXVldWVKb2JSZXF1ZXN0Giouc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFbnF1ZXVlSm9iUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARJ6CglDbGFpbUpvYnMSKC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUNsYWltSm9ic1JlcXVlc3QaKS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUNsYWltSm9ic1Jlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESgwEKDEhlYXJ0YmVhdEpvYhIrLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlSGVhcnRiZWF0Sm9iUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlSGVhcnRiZWF0Sm9iUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARJhCgZBY2tKb2ISJS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUFja0pvYlJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARJjCgdOYWNrSm9iEiYuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVOYWNrSm9iUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEn0KCk5vdGlmeVVzZXISKS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZU5vdGlmeVVzZXJSZXF1ZXN0Giouc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVOb3RpZnlVc2VyUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARKtAQoPTm90aWZ5T3JnQWRtaW5zEi4uc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVOb3RpZnlPcmdBZG1pbnNSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVOb3RpZnlPcmdBZG1pbnNSZXNwb25zZSI5wvMYNQgDEAEwATojCh9zYWFzLm1vZHVsZS5vcmdfYWRtaW5zX25vdGlmaWVkEAJAAUgHUANYA2ABEowBCg9SZXF1ZXN0QXBwcm92YWwSLi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVJlcXVlc3RBcHByb3ZhbFJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVJlcXVlc3RBcHByb3ZhbFJlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESdQoLR2V0QXBwcm92YWwSKi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUdldEFwcHJvdmFsUmVxdWVzdBogLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlQXBwcm92YWwiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARJxCg5DYW5jZWxBcHByb3ZhbBItLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlQ2FuY2VsQXBwcm92YWxSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAEScQoORW1pdEF1ZGl0RXZlbnQSLS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUVtaXRBdWRpdEV2ZW50UmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEoABCgtSZWFkSm91cm5hbBIqLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUmVhZEpvdXJuYWxSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVSZWFkSm91cm5hbFJlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESwwEKFkRlY2xhcmVBdWRpdEV2ZW50VHlwZXMSNS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZURlY2xhcmVBdWRpdEV2ZW50VHlwZXNSZXF1ZXN0GjYuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVEZWNsYXJlQXVkaXRFdmVudFR5cGVzUmVzcG9uc2UiOsLzGDYIAxABMAE6JAogc2Fhcy5tb2R1bGUuYXVkaXRfdHlwZXNfZGVjbGFyZWQQAkABSAdQA1gDYAESngEKFUxpc3RTdWJqZWN0VmlzaWJpbGl0eRI0LnNhYXMuYWNjb3VudHMudjEuTW9kdWxlTGlzdFN1YmplY3RWaXNpYmlsaXR5UmVxdWVzdBo1LnNhYXMuYWNjb3VudHMudjEuTW9kdWxlTGlzdFN1YmplY3RWaXNpYmlsaXR5UmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARKOAQoTRmV0Y2hEYXRhc291cmNlQmxvYhIsLnNhYXMuYWNjb3VudHMudjEuRmV0Y2hEYXRhc291cmNlQmxvYlJlcXVlc3QaKi5zYWFzLmFjY291bnRzLnYxLkZldGNoRGF0YXNvdXJjZUJsb2JDaHVuayIbiAIBwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABMAESjgEKFEZldGNoRGF0YXNvdXJjZUZpbGVzEi0uc2Fhcy5hY2NvdW50cy52MS5GZXRjaERhdGFzb3VyY2VGaWxlc1JlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLkZldGNoRGF0YXNvdXJjZUZpbGVzRnJhbWUiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgATABErMBChVNaW50TW9kdWxlV29ya0NvbnRleHQSLi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZU1pbnRXb3JrQ29udGV4dFJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLk1vZHVsZU1pbnRXb3JrQ29udGV4dFJlc3BvbnNlIjnC8xg1CAMQATABOiMKH3NhYXMubW9kdWxlLndvcmtfY29udGV4dF9taW50ZWQQAkABSAdQBFgEYAESxwEKGk1pbnRNb2R1bGVPcGVyYXRpb25Db250ZXh0EjMuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVNaW50T3BlcmF0aW9uQ29udGV4dFJlcXVlc3QaNC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZU1pbnRPcGVyYXRpb25Db250ZXh0UmVzcG9uc2UiPsLzGDoIAxABMAE6KAokc2Fhcy5tb2R1bGUub3BlcmF0aW9uX2NvbnRleHRfbWludGVkEAJAAUgHUARYBGABEs4BChpNaW50U291cmNlT3BlcmF0aW9uQ29udGV4dBI5LnNhYXMuYWNjb3VudHMudjEuTW9kdWxlTWludFNvdXJjZU9wZXJhdGlvbkNvbnRleHRSZXF1ZXN0Gjouc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVNaW50U291cmNlT3BlcmF0aW9uQ29udGV4dFJlc3BvbnNlIjnC8xg1CAMQATABOiMKH3NhYXMuZGF0YXNvdXJjZS5kZWxlZ2F0aW9uLnVzZWQQAkABSAdQBFgEYAESgwEKDFB1Ymxpc2hFdmVudBIrLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUHVibGlzaEV2ZW50UmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUHVibGlzaEV2ZW50UmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARJ6CglTdWJzY3JpYmUSKC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVN1YnNjcmliZVJlcXVlc3QaKS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVN1YnNjcmliZVJlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESawoLVW5zdWJzY3JpYmUSKi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVVuc3Vic2NyaWJlUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEpIBChFMaXN0U3Vic2NyaXB0aW9ucxIwLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlTGlzdFN1YnNjcmlwdGlvbnNSZXF1ZXN0GjEuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVMaXN0U3Vic2NyaXB0aW9uc1Jlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESgwEKDFJlcGxheUV2ZW50cxIrLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUmVwbGF5RXZlbnRzUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUmVwbGF5RXZlbnRzUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARKwAQobUmVzb2x2ZVdvcmtDb250ZXh0QXV0aG9yaXR5Ejouc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVSZXNvbHZlV29ya0NvbnRleHRBdXRob3JpdHlSZXF1ZXN0Gjsuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVSZXNvbHZlV29ya0NvbnRleHRBdXRob3JpdHlSZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABQr8BChRjb20uc2Fhcy5hY2NvdW50cy52MUIXTW9kdWxlQ2FwYWJpbGl0aWVzUHJvdG9QAVosYWNjb3VudHMvcGtnL2dlbi9zYWFzL2FjY291bnRzL3YxO2FjY291bnRzdjGiAgNTQViqAhBTYWFzLkFjY291bnRzLlYxygIQU2Fhc1xBY2NvdW50c1xWMeICHFNhYXNcQWNjb3VudHNcVjFcR1BCTWV0YWRhdGHqAhJTYWFzOjpBY2NvdW50czo6VjFiBnByb3RvMw", [file_saas_accounts_v1_work_contexts, file_buf_validate_validate, file_google_protobuf_empty, file_google_protobuf_duration, file_google_protobuf_struct, file_google_protobuf_timestamp, file_saas_accounts_v1_module_registration, file_saas_events_v1_events, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); /** * ModuleEnqueueJobRequest names the tenant the work belongs to and carries the @@ -715,6 +715,153 @@ export type ModuleEmitAuditEventRequest = Message<"saas.accounts.v1.ModuleEmitAu export const ModuleEmitAuditEventRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_saas_accounts_v1_module_capabilities, 19); +/** + * ModuleReadJournalRequest reads the platform journal across every tenant. Only a + * module principal whose grant is cross_tenant may call it, the gate a + * system-scoped EmitAuditEvent has; any other caller is PermissionDenied. + * + * @generated from message saas.accounts.v1.ModuleReadJournalRequest + */ +export type ModuleReadJournalRequest = Message<"saas.accounts.v1.ModuleReadJournalRequest"> & { + /** + * after is the position the previous page ended at (a page's next_after). The + * literal "start" is a valid position meaning "before the oldest retained + * entry", and the empty string is accepted as an alias of it; so a reader with + * nothing read yet has a non-empty position to correlate its first page on. + * Positions are opaque: store one and hand it back unread. No entry's position + * is ever "start". A value this host did not issue is InvalidArgument. + * + * @generated from field: string after = 1; + */ + after: string; + + /** + * limit is the page size: 0 is 100, and more than 500 is served as 500. A + * page is also bounded to 2 MiB of event data, though the first entry is always + * served. + * + * @generated from field: int32 limit = 2; + */ + limit: number; + + /** + * types are the event types to read, exactly (no pattern). At least one is + * required, so no caller reads every tenant's every event by leaving the + * filter out. + * + * @generated from field: repeated string types = 3; + */ + types: string[]; + + /** + * delivery_bindings are the binding ids the caller's own delivery of the journal + * runs under. They decide which entries are marked delivery_machinery. + * + * @generated from field: repeated string delivery_bindings = 4; + */ + deliveryBindings: string[]; +}; + +/** + * Describes the message saas.accounts.v1.ModuleReadJournalRequest. + * Use `create(ModuleReadJournalRequestSchema)` to create a new message. + */ +export const ModuleReadJournalRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_module_capabilities, 20); + +/** + * ModuleJournalEntry is one journal row. Rows are served in the order their + * writing transactions are final (see ModuleReadJournalRequest), so a reader that + * resumes from the last position it was given skips nothing. + * + * @generated from message saas.accounts.v1.ModuleJournalEntry + */ +export type ModuleJournalEntry = Message<"saas.accounts.v1.ModuleJournalEntry"> & { + /** + * position is where the row sits in the read order: opaque, and the value a + * reader passes as after to resume past it. + * + * @generated from field: string position = 1; + */ + position: string; + + /** + * @generated from field: string event_id = 2; + */ + eventId: string; + + /** + * @generated from field: string type = 3; + */ + type: string; + + /** + * organization_id is the row's tenant, empty for a row that has none. + * + * @generated from field: string organization_id = 4; + */ + organizationId: string; + + /** + * @generated from field: google.protobuf.Timestamp occurred_at = 5; + */ + occurredAt?: Timestamp; + + /** + * data is the event's JSON object; empty when the event's data is not one. + * + * @generated from field: google.protobuf.Struct data = 6; + */ + data?: JsonObject; + + /** + * delivery_machinery is true when the entry the row carries is the footprint of + * the caller's own delivery of the journal: the host computes it, so a caller + * never parses this module's payloads. It is true for a + * saas.module.operation_context_minted or + * saas.module.delegated_audience_exchange entry whose binding_id is one of the + * request's delivery_bindings, and for a saas.module.work_context_minted entry + * of the caller's own module principal. + * + * @generated from field: bool delivery_machinery = 7; + */ + deliveryMachinery: boolean; +}; + +/** + * Describes the message saas.accounts.v1.ModuleJournalEntry. + * Use `create(ModuleJournalEntrySchema)` to create a new message. + */ +export const ModuleJournalEntrySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_module_capabilities, 21); + +/** + * ModuleReadJournalResponse is one page and where to resume. + * + * @generated from message saas.accounts.v1.ModuleReadJournalResponse + */ +export type ModuleReadJournalResponse = Message<"saas.accounts.v1.ModuleReadJournalResponse"> & { + /** + * @generated from field: repeated saas.accounts.v1.ModuleJournalEntry entries = 1; + */ + entries: ModuleJournalEntry[]; + + /** + * next_after is the last entry's position, or the request's after when the page + * is empty. + * + * @generated from field: string next_after = 2; + */ + nextAfter: string; +}; + +/** + * Describes the message saas.accounts.v1.ModuleReadJournalResponse. + * Use `create(ModuleReadJournalResponseSchema)` to create a new message. + */ +export const ModuleReadJournalResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_module_capabilities, 22); + /** * ModuleAuditFieldDeclaration is one payload field of a declared audit event * type. `solution` is stamped by the host from the emitting scope and may not @@ -756,7 +903,7 @@ export type ModuleAuditFieldDeclaration = Message<"saas.accounts.v1.ModuleAuditF * Use `create(ModuleAuditFieldDeclarationSchema)` to create a new message. */ export const ModuleAuditFieldDeclarationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 20); + messageDesc(file_saas_accounts_v1_module_capabilities, 23); /** * ModuleAuditEventTypeDeclaration is one audit event type a composed module @@ -801,7 +948,7 @@ export type ModuleAuditEventTypeDeclaration = Message<"saas.accounts.v1.ModuleAu * Use `create(ModuleAuditEventTypeDeclarationSchema)` to create a new message. */ export const ModuleAuditEventTypeDeclarationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 21); + messageDesc(file_saas_accounts_v1_module_capabilities, 24); /** * ModuleDeclareAuditEventTypesRequest declares the audit event types a composed @@ -834,7 +981,7 @@ export type ModuleDeclareAuditEventTypesRequest = Message<"saas.accounts.v1.Modu * Use `create(ModuleDeclareAuditEventTypesRequestSchema)` to create a new message. */ export const ModuleDeclareAuditEventTypesRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 22); + messageDesc(file_saas_accounts_v1_module_capabilities, 25); /** * ModuleDeclareAuditEventTypesResponse names what the call changed: the types @@ -860,7 +1007,7 @@ export type ModuleDeclareAuditEventTypesResponse = Message<"saas.accounts.v1.Mod * Use `create(ModuleDeclareAuditEventTypesResponseSchema)` to create a new message. */ export const ModuleDeclareAuditEventTypesResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 23); + messageDesc(file_saas_accounts_v1_module_capabilities, 26); /** * SubjectVisibilityGrant is one entry of a viewer's visible-subject set: another @@ -890,7 +1037,7 @@ export type SubjectVisibilityGrant = Message<"saas.accounts.v1.SubjectVisibility * Use `create(SubjectVisibilityGrantSchema)` to create a new message. */ export const SubjectVisibilityGrantSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 24); + messageDesc(file_saas_accounts_v1_module_capabilities, 27); /** * ModuleListSubjectVisibilityRequest asks for one viewer's whole visible-subject @@ -924,7 +1071,7 @@ export type ModuleListSubjectVisibilityRequest = Message<"saas.accounts.v1.Modul * Use `create(ModuleListSubjectVisibilityRequestSchema)` to create a new message. */ export const ModuleListSubjectVisibilityRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 25); + messageDesc(file_saas_accounts_v1_module_capabilities, 28); /** * ModuleListSubjectVisibilityResponse carries the viewer's WHOLE set, read in @@ -955,7 +1102,7 @@ export type ModuleListSubjectVisibilityResponse = Message<"saas.accounts.v1.Modu * Use `create(ModuleListSubjectVisibilityResponseSchema)` to create a new message. */ export const ModuleListSubjectVisibilityResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 26); + messageDesc(file_saas_accounts_v1_module_capabilities, 29); /** * FetchDatasourceBlobRequest asks accounts to re-fetch one file blob from the @@ -986,7 +1133,7 @@ export type FetchDatasourceBlobRequest = Message<"saas.accounts.v1.FetchDatasour * Use `create(FetchDatasourceBlobRequestSchema)` to create a new message. */ export const FetchDatasourceBlobRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 27); + messageDesc(file_saas_accounts_v1_module_capabilities, 30); /** * FetchDatasourceBlobChunk is one frame of the streamed blob. total_size and @@ -1018,7 +1165,7 @@ export type FetchDatasourceBlobChunk = Message<"saas.accounts.v1.FetchDatasource * Use `create(FetchDatasourceBlobChunkSchema)` to create a new message. */ export const FetchDatasourceBlobChunkSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 28); + messageDesc(file_saas_accounts_v1_module_capabilities, 31); /** * DatasourceFileRef names one file of a files-interface datasource at the @@ -1045,7 +1192,7 @@ export type DatasourceFileRef = Message<"saas.accounts.v1.DatasourceFileRef"> & * Use `create(DatasourceFileRefSchema)` to create a new message. */ export const DatasourceFileRefSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 29); + messageDesc(file_saas_accounts_v1_module_capabilities, 32); /** * FetchDatasourceFilesRequest asks accounts for a batch of files of one source @@ -1081,7 +1228,7 @@ export type FetchDatasourceFilesRequest = Message<"saas.accounts.v1.FetchDatasou * Use `create(FetchDatasourceFilesRequestSchema)` to create a new message. */ export const FetchDatasourceFilesRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 30); + messageDesc(file_saas_accounts_v1_module_capabilities, 33); /** * DatasourceProvenance is the envelope every item a datasource serves carries, @@ -1128,7 +1275,7 @@ export type DatasourceProvenance = Message<"saas.accounts.v1.DatasourceProvenanc * Use `create(DatasourceProvenanceSchema)` to create a new message. */ export const DatasourceProvenanceSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 31); + messageDesc(file_saas_accounts_v1_module_capabilities, 34); /** * DatasourceItemReaders is the envelope's per-item permission slot: who may read @@ -1181,7 +1328,7 @@ export type DatasourceItemReaders = Message<"saas.accounts.v1.DatasourceItemRead * Use `create(DatasourceItemReadersSchema)` to create a new message. */ export const DatasourceItemReadersSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 32); + messageDesc(file_saas_accounts_v1_module_capabilities, 35); /** * DatasourceFileHeader opens one file of a FetchDatasourceFiles stream. size is @@ -1222,7 +1369,7 @@ export type DatasourceFileHeader = Message<"saas.accounts.v1.DatasourceFileHeade * Use `create(DatasourceFileHeaderSchema)` to create a new message. */ export const DatasourceFileHeaderSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 33); + messageDesc(file_saas_accounts_v1_module_capabilities, 36); /** * FetchDatasourceFilesFrame is one frame of a FetchDatasourceFiles stream. The @@ -1255,7 +1402,7 @@ export type FetchDatasourceFilesFrame = Message<"saas.accounts.v1.FetchDatasourc * Use `create(FetchDatasourceFilesFrameSchema)` to create a new message. */ export const FetchDatasourceFilesFrameSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 34); + messageDesc(file_saas_accounts_v1_module_capabilities, 37); /** * ModulePublishEventRequest publishes one domain event for the caller's bound @@ -1283,7 +1430,7 @@ export type ModulePublishEventRequest = Message<"saas.accounts.v1.ModulePublishE * Use `create(ModulePublishEventRequestSchema)` to create a new message. */ export const ModulePublishEventRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 35); + messageDesc(file_saas_accounts_v1_module_capabilities, 38); /** * @generated from message saas.accounts.v1.ModulePublishEventResponse @@ -1305,7 +1452,7 @@ export type ModulePublishEventResponse = Message<"saas.accounts.v1.ModulePublish * Use `create(ModulePublishEventResponseSchema)` to create a new message. */ export const ModulePublishEventResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 36); + messageDesc(file_saas_accounts_v1_module_capabilities, 39); /** * ModuleSubscribeRequest creates, or idempotently re-affirms, a durable @@ -1338,7 +1485,7 @@ export type ModuleSubscribeRequest = Message<"saas.accounts.v1.ModuleSubscribeRe * Use `create(ModuleSubscribeRequestSchema)` to create a new message. */ export const ModuleSubscribeRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 37); + messageDesc(file_saas_accounts_v1_module_capabilities, 40); /** * ModuleSubscription is one durable subscription row as the module sees it. @@ -1382,7 +1529,7 @@ export type ModuleSubscription = Message<"saas.accounts.v1.ModuleSubscription"> * Use `create(ModuleSubscriptionSchema)` to create a new message. */ export const ModuleSubscriptionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 38); + messageDesc(file_saas_accounts_v1_module_capabilities, 41); /** * @generated from message saas.accounts.v1.ModuleSubscribeResponse @@ -1399,7 +1546,7 @@ export type ModuleSubscribeResponse = Message<"saas.accounts.v1.ModuleSubscribeR * Use `create(ModuleSubscribeResponseSchema)` to create a new message. */ export const ModuleSubscribeResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 39); + messageDesc(file_saas_accounts_v1_module_capabilities, 42); /** * ModuleUnsubscribeRequest revokes one of the caller's own subscriptions. @@ -1418,7 +1565,7 @@ export type ModuleUnsubscribeRequest = Message<"saas.accounts.v1.ModuleUnsubscri * Use `create(ModuleUnsubscribeRequestSchema)` to create a new message. */ export const ModuleUnsubscribeRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 40); + messageDesc(file_saas_accounts_v1_module_capabilities, 43); /** * ModuleListSubscriptionsRequest lists the calling principal's live @@ -1434,7 +1581,7 @@ export type ModuleListSubscriptionsRequest = Message<"saas.accounts.v1.ModuleLis * Use `create(ModuleListSubscriptionsRequestSchema)` to create a new message. */ export const ModuleListSubscriptionsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 41); + messageDesc(file_saas_accounts_v1_module_capabilities, 44); /** * @generated from message saas.accounts.v1.ModuleListSubscriptionsResponse @@ -1451,7 +1598,7 @@ export type ModuleListSubscriptionsResponse = Message<"saas.accounts.v1.ModuleLi * Use `create(ModuleListSubscriptionsResponseSchema)` to create a new message. */ export const ModuleListSubscriptionsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 42); + messageDesc(file_saas_accounts_v1_module_capabilities, 45); /** * ModuleReplayEventsRequest re-delivers durable events of one type for the @@ -1483,7 +1630,7 @@ export type ModuleReplayEventsRequest = Message<"saas.accounts.v1.ModuleReplayEv * Use `create(ModuleReplayEventsRequestSchema)` to create a new message. */ export const ModuleReplayEventsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 43); + messageDesc(file_saas_accounts_v1_module_capabilities, 46); /** * @generated from message saas.accounts.v1.ModuleReplayEventsResponse @@ -1503,7 +1650,7 @@ export type ModuleReplayEventsResponse = Message<"saas.accounts.v1.ModuleReplayE * Use `create(ModuleReplayEventsResponseSchema)` to create a new message. */ export const ModuleReplayEventsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 44); + messageDesc(file_saas_accounts_v1_module_capabilities, 47); /** * ModuleCapabilitiesService is the module-facing platform surface. Every RPC is @@ -1586,7 +1733,7 @@ export type ReadableSourceCollection = Message<"saas.accounts.v1.ReadableSourceC * Use `create(ReadableSourceCollectionSchema)` to create a new message. */ export const ReadableSourceCollectionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 45); + messageDesc(file_saas_accounts_v1_module_capabilities, 48); /** * ReadableCollectionGrant is one active grant conferring read on a collection's @@ -1637,7 +1784,7 @@ export type ReadableCollectionGrant = Message<"saas.accounts.v1.ReadableCollecti * Use `create(ReadableCollectionGrantSchema)` to create a new message. */ export const ReadableCollectionGrantSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 46); + messageDesc(file_saas_accounts_v1_module_capabilities, 49); /** * CollectionSyncProvenance is the host's record of how one feeding source last @@ -1701,7 +1848,7 @@ export type CollectionSyncProvenance = Message<"saas.accounts.v1.CollectionSyncP * Use `create(CollectionSyncProvenanceSchema)` to create a new message. */ export const CollectionSyncProvenanceSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 47); + messageDesc(file_saas_accounts_v1_module_capabilities, 50); /** * The calling module authenticates with its own module Work Context in the @@ -1733,7 +1880,7 @@ export type ListReadableSourceCollectionsRequest = Message<"saas.accounts.v1.Lis * Use `create(ListReadableSourceCollectionsRequestSchema)` to create a new message. */ export const ListReadableSourceCollectionsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 48); + messageDesc(file_saas_accounts_v1_module_capabilities, 51); /** * @generated from message saas.accounts.v1.ListReadableSourceCollectionsResponse @@ -1755,7 +1902,7 @@ export type ListReadableSourceCollectionsResponse = Message<"saas.accounts.v1.Li * Use `create(ListReadableSourceCollectionsResponseSchema)` to create a new message. */ export const ListReadableSourceCollectionsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 49); + messageDesc(file_saas_accounts_v1_module_capabilities, 52); /** * ModulePlaceRecordRequest binds one of the calling module's own records to a @@ -1812,7 +1959,7 @@ export type ModulePlaceRecordRequest = Message<"saas.accounts.v1.ModulePlaceReco * Use `create(ModulePlaceRecordRequestSchema)` to create a new message. */ export const ModulePlaceRecordRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 50); + messageDesc(file_saas_accounts_v1_module_capabilities, 53); /** * @generated from message saas.accounts.v1.ModulePlaceRecordResponse @@ -1835,7 +1982,7 @@ export type ModulePlaceRecordResponse = Message<"saas.accounts.v1.ModulePlaceRec * Use `create(ModulePlaceRecordResponseSchema)` to create a new message. */ export const ModulePlaceRecordResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 51); + messageDesc(file_saas_accounts_v1_module_capabilities, 54); /** * Exact-record delegated oracle. The calling module authenticates with its @@ -1873,7 +2020,7 @@ export type CheckWorkContextRecordAccessRequest = Message<"saas.accounts.v1.Chec * Use `create(CheckWorkContextRecordAccessRequestSchema)` to create a new message. */ export const CheckWorkContextRecordAccessRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 52); + messageDesc(file_saas_accounts_v1_module_capabilities, 55); /** * @generated from message saas.accounts.v1.CheckWorkContextRecordAccessResponse @@ -1899,7 +2046,7 @@ export type CheckWorkContextRecordAccessResponse = Message<"saas.accounts.v1.Che * Use `create(CheckWorkContextRecordAccessResponseSchema)` to create a new message. */ export const CheckWorkContextRecordAccessResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 53); + messageDesc(file_saas_accounts_v1_module_capabilities, 56); /** * ModuleExchangeDelegatedReadAudienceRequest selects only an installed binding. @@ -1924,7 +2071,7 @@ export type ModuleExchangeDelegatedReadAudienceRequest = Message<"saas.accounts. * Use `create(ModuleExchangeDelegatedReadAudienceRequestSchema)` to create a new message. */ export const ModuleExchangeDelegatedReadAudienceRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 54); + messageDesc(file_saas_accounts_v1_module_capabilities, 57); /** * Selects one immutable installed operation binding. The caller chooses only @@ -1985,7 +2132,7 @@ export type ModuleExchangeDelegatedOperationAudienceRequest = Message<"saas.acco * Use `create(ModuleExchangeDelegatedOperationAudienceRequestSchema)` to create a new message. */ export const ModuleExchangeDelegatedOperationAudienceRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 55); + messageDesc(file_saas_accounts_v1_module_capabilities, 58); /** * Exact dependency identity as asserted by the authenticated compiler owner. @@ -2014,7 +2161,7 @@ export type ExecutableArtifactContract = Message<"saas.accounts.v1.ExecutableArt * Use `create(ExecutableArtifactContractSchema)` to create a new message. */ export const ExecutableArtifactContractSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 56); + messageDesc(file_saas_accounts_v1_module_capabilities, 59); /** * @generated from message saas.accounts.v1.ExecutableArtifactIdentity @@ -2054,7 +2201,7 @@ export type ExecutableArtifactIdentity = Message<"saas.accounts.v1.ExecutableArt * Use `create(ExecutableArtifactIdentitySchema)` to create a new message. */ export const ExecutableArtifactIdentitySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 57); + messageDesc(file_saas_accounts_v1_module_capabilities, 60); /** * @generated from message saas.accounts.v1.ModuleExecutableArtifactRequest @@ -2086,7 +2233,7 @@ export type ModuleExecutableArtifactRequest = Message<"saas.accounts.v1.ModuleEx * Use `create(ModuleExecutableArtifactRequestSchema)` to create a new message. */ export const ModuleExecutableArtifactRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 58); + messageDesc(file_saas_accounts_v1_module_capabilities, 61); /** * @generated from message saas.accounts.v1.ModuleRevokeExecutableArtifactRequest @@ -2113,7 +2260,7 @@ export type ModuleRevokeExecutableArtifactRequest = Message<"saas.accounts.v1.Mo * Use `create(ModuleRevokeExecutableArtifactRequestSchema)` to create a new message. */ export const ModuleRevokeExecutableArtifactRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 59); + messageDesc(file_saas_accounts_v1_module_capabilities, 62); /** * @generated from message saas.accounts.v1.ModuleExecutableArtifactResponse @@ -2145,7 +2292,7 @@ export type ModuleExecutableArtifactResponse = Message<"saas.accounts.v1.ModuleE * Use `create(ModuleExecutableArtifactResponseSchema)` to create a new message. */ export const ModuleExecutableArtifactResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 60); + messageDesc(file_saas_accounts_v1_module_capabilities, 63); /** * A current observation under independently authenticated module and parent @@ -2170,7 +2317,7 @@ export type ModuleCurrentInstallationRequest = Message<"saas.accounts.v1.ModuleC * Use `create(ModuleCurrentInstallationRequestSchema)` to create a new message. */ export const ModuleCurrentInstallationRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 61); + messageDesc(file_saas_accounts_v1_module_capabilities, 64); /** * @generated from message saas.accounts.v1.ModuleCurrentInstallationResponse @@ -2208,7 +2355,7 @@ export type ModuleCurrentInstallationResponse = Message<"saas.accounts.v1.Module * Use `create(ModuleCurrentInstallationResponseSchema)` to create a new message. */ export const ModuleCurrentInstallationResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 62); + messageDesc(file_saas_accounts_v1_module_capabilities, 65); /** * WorkContextSealQuestion asks for the live installation one principal holds. @@ -2240,7 +2387,7 @@ export type WorkContextSealQuestion = Message<"saas.accounts.v1.WorkContextSealQ * Use `create(WorkContextSealQuestionSchema)` to create a new message. */ export const WorkContextSealQuestionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 63); + messageDesc(file_saas_accounts_v1_module_capabilities, 66); /** * WorkContextPrincipalEpochQuestion asks for one principal's live epoch, @@ -2263,7 +2410,7 @@ export type WorkContextPrincipalEpochQuestion = Message<"saas.accounts.v1.WorkCo * Use `create(WorkContextPrincipalEpochQuestionSchema)` to create a new message. */ export const WorkContextPrincipalEpochQuestionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 64); + messageDesc(file_saas_accounts_v1_module_capabilities, 67); /** * WorkContextApprovedBuildQuestion asks what execution the issuer APPROVES for @@ -2286,7 +2433,7 @@ export type WorkContextApprovedBuildQuestion = Message<"saas.accounts.v1.WorkCon * Use `create(WorkContextApprovedBuildQuestionSchema)` to create a new message. */ export const WorkContextApprovedBuildQuestionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 65); + messageDesc(file_saas_accounts_v1_module_capabilities, 68); /** * WorkContextOperationBindingQuestion resolves one operation binding by its @@ -2311,7 +2458,7 @@ export type WorkContextOperationBindingQuestion = Message<"saas.accounts.v1.Work * Use `create(WorkContextOperationBindingQuestionSchema)` to create a new message. */ export const WorkContextOperationBindingQuestionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 66); + messageDesc(file_saas_accounts_v1_module_capabilities, 69); /** * WorkContextAuthorizationRevisionQuestion asks the authorization revision a @@ -2338,7 +2485,7 @@ export type WorkContextAuthorizationRevisionQuestion = Message<"saas.accounts.v1 * Use `create(WorkContextAuthorizationRevisionQuestionSchema)` to create a new message. */ export const WorkContextAuthorizationRevisionQuestionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 67); + messageDesc(file_saas_accounts_v1_module_capabilities, 70); /** * WorkContextApprovalGrantQuestion resolves the approval a capability's grant @@ -2359,7 +2506,7 @@ export type WorkContextApprovalGrantQuestion = Message<"saas.accounts.v1.WorkCon * Use `create(WorkContextApprovalGrantQuestionSchema)` to create a new message. */ export const WorkContextApprovalGrantQuestionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 68); + messageDesc(file_saas_accounts_v1_module_capabilities, 71); /** * @generated from message saas.accounts.v1.WorkContextSealAnswer @@ -2389,7 +2536,7 @@ export type WorkContextSealAnswer = Message<"saas.accounts.v1.WorkContextSealAns * Use `create(WorkContextSealAnswerSchema)` to create a new message. */ export const WorkContextSealAnswerSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 69); + messageDesc(file_saas_accounts_v1_module_capabilities, 72); /** * @generated from message saas.accounts.v1.WorkContextPrincipalEpochAnswer @@ -2416,7 +2563,7 @@ export type WorkContextPrincipalEpochAnswer = Message<"saas.accounts.v1.WorkCont * Use `create(WorkContextPrincipalEpochAnswerSchema)` to create a new message. */ export const WorkContextPrincipalEpochAnswerSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 70); + messageDesc(file_saas_accounts_v1_module_capabilities, 73); /** * @generated from message saas.accounts.v1.WorkContextApprovedBuildAnswer @@ -2451,7 +2598,7 @@ export type WorkContextApprovedBuildAnswer = Message<"saas.accounts.v1.WorkConte * Use `create(WorkContextApprovedBuildAnswerSchema)` to create a new message. */ export const WorkContextApprovedBuildAnswerSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 71); + messageDesc(file_saas_accounts_v1_module_capabilities, 74); /** * @generated from message saas.accounts.v1.WorkContextOperationBindingAnswer @@ -2505,7 +2652,7 @@ export type WorkContextOperationBindingAnswer = Message<"saas.accounts.v1.WorkCo * Use `create(WorkContextOperationBindingAnswerSchema)` to create a new message. */ export const WorkContextOperationBindingAnswerSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 72); + messageDesc(file_saas_accounts_v1_module_capabilities, 75); /** * @generated from message saas.accounts.v1.WorkContextAuthorizationRevisionAnswer @@ -2527,7 +2674,7 @@ export type WorkContextAuthorizationRevisionAnswer = Message<"saas.accounts.v1.W * Use `create(WorkContextAuthorizationRevisionAnswerSchema)` to create a new message. */ export const WorkContextAuthorizationRevisionAnswerSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 73); + messageDesc(file_saas_accounts_v1_module_capabilities, 76); /** * @generated from message saas.accounts.v1.WorkContextApprovalGrantAnswer @@ -2549,7 +2696,7 @@ export type WorkContextApprovalGrantAnswer = Message<"saas.accounts.v1.WorkConte * Use `create(WorkContextApprovalGrantAnswerSchema)` to create a new message. */ export const WorkContextApprovalGrantAnswerSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 74); + messageDesc(file_saas_accounts_v1_module_capabilities, 77); /** * @generated from message saas.accounts.v1.ModuleResolveWorkContextAuthorityRequest @@ -2616,7 +2763,7 @@ export type ModuleResolveWorkContextAuthorityRequest = Message<"saas.accounts.v1 * Use `create(ModuleResolveWorkContextAuthorityRequestSchema)` to create a new message. */ export const ModuleResolveWorkContextAuthorityRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 75); + messageDesc(file_saas_accounts_v1_module_capabilities, 78); /** * @generated from message saas.accounts.v1.ModuleResolveWorkContextAuthorityResponse @@ -2673,7 +2820,7 @@ export type ModuleResolveWorkContextAuthorityResponse = Message<"saas.accounts.v * Use `create(ModuleResolveWorkContextAuthorityResponseSchema)` to create a new message. */ export const ModuleResolveWorkContextAuthorityResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 76); + messageDesc(file_saas_accounts_v1_module_capabilities, 79); /** * ModuleAuditFieldKind is the kind of one declared audit payload field: the @@ -3303,6 +3450,19 @@ export const ModuleCapabilitiesService: GenService<{ input: typeof ModuleEmitAuditEventRequestSchema; output: typeof EmptySchema; }, + /** + * ReadJournal serves the platform journal across every tenant, and the rows with + * no tenant, to a module principal whose grant is cross_tenant. Rows are served + * in the order their writing transactions are final, so a reader that resumes + * from next_after skips nothing; it enqueues nothing and is not a replay. + * + * @generated from rpc saas.accounts.v1.ModuleCapabilitiesService.ReadJournal + */ + readJournal: { + methodKind: "unary"; + input: typeof ModuleReadJournalRequestSchema; + output: typeof ModuleReadJournalResponseSchema; + }, /** * DeclareAuditEventTypes admits the audit event types a composed module owns, * into the namespaces the operator bound to it; see the request. diff --git a/module/services/store/code/migration_61_publish_domain_event_test.go b/module/services/store/code/migration_61_publish_domain_event_test.go new file mode 100644 index 000000000..144a3b09d --- /dev/null +++ b/module/services/store/code/migration_61_publish_domain_event_test.go @@ -0,0 +1,103 @@ +package main + +import ( + "database/sql" + "strings" + "testing" + + "github.com/golang-migrate/migrate/v4" +) + +// publishDomainEventFacts is everything about the function that a migration can +// change: its body, who owns it, whether it runs with its owner's authority, the +// settings pinned on it and who may execute it. +type publishDomainEventFacts struct { + body, owner, definer, config, acl string +} + +func readPublishDomainEvent(t *testing.T, db *sql.DB) publishDomainEventFacts { + t.Helper() + var f publishDomainEventFacts + err := db.QueryRow(`SELECT prosrc, pg_get_userbyid(proowner), prosecdef::text, + coalesce(proconfig::text, ''), coalesce(proacl::text, '') + FROM pg_proc + WHERE oid = 'public.publish_domain_event(uuid,text,text,text,timestamp with time zone,text,text,text,bytea,uuid,text,text,text,text,text,text,text,integer,bytea)'::regprocedure`). + Scan(&f.body, &f.owner, &f.definer, &f.config, &f.acl) + if err != nil { + t.Fatalf("read publish_domain_event: %v", err) + } + return f +} + +// Migration 61 replaces publish_domain_event to admit one tenant-less event from +// a tenant-scoped caller. The function's owner, authority, pinned search_path and +// grants must come through the replacement untouched, the owner must be left with +// no CREATE on the schema it borrowed for it, and the down migration must put +// back the body the function had before, byte for byte. Install the ledger below +// 61, take the function's facts, apply 61, step back, and compare; then apply 61 +// again, because a down that leaves the ledger unable to go forward is no down. +// +// The canonical migration replay gate supplies the disposable PostgreSQL 16 +// cluster through the existing throwawayPostgres helper. +func TestMigration61PublishDomainEventDownRestoresTheBody(t *testing.T) { + db, databaseURL := throwawayPostgres(t) + + if err := migrateStoreFrom("file://"+ledgerUpTo(t, 60), databaseURL); err != nil { + t.Fatalf("apply the ledger below migration 61: %v", err) + } + before := readPublishDomainEvent(t, db) + + assertOwnerHoldsNoCreate := func(stage string) { + t.Helper() + var holds bool + if err := db.QueryRow(`SELECT has_schema_privilege('app_control_plane', 'public', 'CREATE')`).Scan(&holds); err != nil { + t.Fatal(err) + } + if holds { + t.Fatalf("%s: app_control_plane still holds CREATE on schema public", stage) + } + } + assertOwnerHoldsNoCreate("below migration 61") + + withUp := "file://" + ledgerUpTo(t, 61) + if err := migrateStoreFrom(withUp, databaseURL); err != nil { + t.Fatalf("apply migration 61: %v", err) + } + after := readPublishDomainEvent(t, db) + if after.body == before.body { + t.Fatal("migration 61 left the function body unchanged") + } + if !strings.Contains(after.body, "saas.audit.recorded.v1") { + t.Fatal("migration 61 does not name the audit journal type in the function body") + } + if after.owner != before.owner || after.definer != before.definer || after.config != before.config || after.acl != before.acl { + t.Fatalf("migration 61 changed more than the body:\n before %+v\n after %+v", before, after) + } + if after.owner != "app_control_plane" || after.definer != "true" || + after.config != `{"search_path=pg_catalog, public, pg_temp"}` { + t.Fatalf("the replacement lost the function's owner, authority or search_path: %+v", after) + } + assertOwnerHoldsNoCreate("after migration 61") + + runner, err := migrate.New(withUp, databaseURL) + if err != nil { + t.Fatal(err) + } + stepErr := runner.Steps(-1) + sourceErr, databaseErr := runner.Close() + if stepErr != nil || sourceErr != nil || databaseErr != nil { + t.Fatalf("revert migration 61: step %v, close source %v, close database %v", stepErr, sourceErr, databaseErr) + } + if reverted := readPublishDomainEvent(t, db); reverted != before { + t.Fatalf("the down migration did not restore the function:\n got %+v\nwant %+v", reverted, before) + } + assertOwnerHoldsNoCreate("after the down migration") + + if err := migrateStoreFrom(withUp, databaseURL); err != nil { + t.Fatalf("apply migration 61 again after its down: %v", err) + } + if again := readPublishDomainEvent(t, db); again != after { + t.Fatal("migration 61 applied a second time produced a different body") + } + t.Log("migration 61 replaced publish_domain_event keeping owner, definer authority, search_path and grants; its down restored the previous body exactly") +} diff --git a/module/services/store/code/migration_62_domain_events_xact_id_test.go b/module/services/store/code/migration_62_domain_events_xact_id_test.go new file mode 100644 index 000000000..0c3e95c6e --- /dev/null +++ b/module/services/store/code/migration_62_domain_events_xact_id_test.go @@ -0,0 +1,200 @@ +package main + +import ( + "context" + "database/sql" + "strings" + "testing" + + "github.com/golang-migrate/migrate/v4" +) + +// publishAsControlPlane writes one journal row through the real +// publish_domain_event, as the role that publishes on behalf of a tenant, and +// returns the id of the transaction that wrote it as the database reports it. +// eventID must be a UUID. +func publishAsControlPlane(t *testing.T, db *sql.DB, eventID, eventType string) string { + t.Helper() + tx, err := db.BeginTx(context.Background(), nil) + if err != nil { + t.Fatal(err) + } + defer func() { _ = tx.Rollback() }() + if _, err := tx.Exec(`SET LOCAL ROLE app_control_plane`); err != nil { + t.Fatalf("assume app_control_plane: %v", err) + } + var xact string + if err := tx.QueryRow(`SELECT pg_current_xact_id()::text`).Scan(&xact); err != nil { + t.Fatal(err) + } + if _, err := tx.Exec(`SELECT * FROM public.publish_domain_event( + $1::uuid, $2, 'saas/test', '', now(), '1.0', 'application/json', '', '{}'::bytea, + NULL, '', '', '', '', '', '', '', 1, decode(repeat('ab', 32), 'hex'))`, + eventID, eventType); err != nil { + t.Fatalf("publish %s: %v", eventType, err) + } + if err := tx.Commit(); err != nil { + t.Fatal(err) + } + return xact +} + +// Migration 62 adds domain_events.xact_id, the writing transaction's id, as a +// column default, so the journal read can serve only rows whose transaction has +// finished. It must change nothing about publish_domain_event, leave the rows +// that existed NULL and still readable in seq order under the key the read uses, +// stamp every new row with its real transaction id, and revert cleanly. Install +// the ledger below 62, write rows, apply 62, write more, step back, and forward +// again. +// +// The canonical migration replay gate supplies the disposable PostgreSQL 16 +// cluster through the existing throwawayPostgres helper. +func TestMigration62DomainEventsXactID(t *testing.T) { + db, databaseURL := throwawayPostgres(t) + + if err := migrateStoreFrom("file://"+ledgerUpTo(t, 61), databaseURL); err != nil { + t.Fatalf("apply the ledger below migration 62: %v", err) + } + var hasColumn bool + hasXactColumn := func() bool { + t.Helper() + if err := db.QueryRow(`SELECT EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema='public' AND table_name='domain_events' AND column_name='xact_id')`).Scan(&hasColumn); err != nil { + t.Fatal(err) + } + return hasColumn + } + if hasXactColumn() { + t.Fatal("xact_id exists below migration 62") + } + function := readPublishDomainEvent(t, db) + + // Rows that exist before the migration: committed, with no transaction id + // recorded for them. + const legacyA, legacyB = "00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002" + publishAsControlPlane(t, db, legacyA, "saas.test.legacy") + publishAsControlPlane(t, db, legacyB, "saas.test.legacy") + + withUp := "file://" + ledgerUpTo(t, 62) + if err := migrateStoreFrom(withUp, databaseURL); err != nil { + t.Fatalf("apply migration 62: %v", err) + } + if !hasXactColumn() { + t.Fatal("migration 62 added no xact_id column") + } + var dataType, columnDefault string + if err := db.QueryRow(`SELECT format_type(a.atttypid, a.atttypmod), coalesce(pg_get_expr(d.adbin, d.adrelid), '') + FROM pg_attribute a LEFT JOIN pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum + WHERE a.attrelid = 'public.domain_events'::regclass AND a.attname = 'xact_id' AND NOT a.attisdropped`). + Scan(&dataType, &columnDefault); err != nil { + t.Fatal(err) + } + if dataType != "xid8" || columnDefault != "pg_current_xact_id()" { + t.Fatalf("xact_id is %s default %q, want xid8 default pg_current_xact_id()", dataType, columnDefault) + } + + // The function is not touched: same body, owner, authority, settings, grants. + if after := readPublishDomainEvent(t, db); after != function { + t.Fatalf("migration 62 changed publish_domain_event:\n got %+v\nwant %+v", after, function) + } + + // Legacy rows stay NULL and are not rewritten into a real transaction id. + var legacyNull int + if err := db.QueryRow(`SELECT count(*) FROM public.domain_events WHERE type = 'saas.test.legacy' AND xact_id IS NULL`).Scan(&legacyNull); err != nil { + t.Fatal(err) + } + if legacyNull != 2 { + t.Fatalf("%d of the 2 rows that existed are NULL, want both", legacyNull) + } + + // New rows carry the transaction that wrote them, through the unchanged + // function. + const newA, newB = "00000000-0000-4000-8000-000000000003", "00000000-0000-4000-8000-000000000004" + xactA := publishAsControlPlane(t, db, newA, "saas.test.new") + xactB := publishAsControlPlane(t, db, newB, "saas.test.new") + for id, want := range map[string]string{newA: xactA, newB: xactB} { + var got sql.NullString + if err := db.QueryRow(`SELECT xact_id::text FROM public.domain_events WHERE id = $1::uuid`, id).Scan(&got); err != nil { + t.Fatal(err) + } + if !got.Valid || got.String != want { + t.Fatalf("row %s has xact_id %v, want the writing transaction %s", id, got, want) + } + } + + // The read's key puts the legacy block first, in seq order, then the new rows + // in transaction order, and the index is on exactly that key. + var order []string + rows, err := db.Query(`SELECT id::text FROM public.domain_events + ORDER BY COALESCE(xact_id, '0'::xid8), seq`) + if err != nil { + t.Fatal(err) + } + for rows.Next() { + var id string + if err := rows.Scan(&id); err != nil { + t.Fatal(err) + } + order = append(order, id) + } + if err := rows.Err(); err != nil { + t.Fatal(err) + } + rows.Close() + if got, want := strings.Join(order, ","), strings.Join([]string{legacyA, legacyB, newA, newB}, ","); got != want { + t.Fatalf("read order\n got %s\nwant %s", got, want) + } + var indexDef string + if err := db.QueryRow(`SELECT indexdef FROM pg_indexes WHERE schemaname='public' AND indexname='idx_domain_events_xact_seq'`).Scan(&indexDef); err != nil { + t.Fatalf("migration 62 did not create idx_domain_events_xact_seq: %v", err) + } + if !strings.Contains(indexDef, "COALESCE(xact_id, '0'::xid8)") || !strings.Contains(indexDef, "seq") { + t.Fatalf("index is not on the read's key: %s", indexDef) + } + + runner, err := migrate.New(withUp, databaseURL) + if err != nil { + t.Fatal(err) + } + stepErr := runner.Steps(-1) + sourceErr, databaseErr := runner.Close() + if stepErr != nil || sourceErr != nil || databaseErr != nil { + t.Fatalf("revert migration 62: step %v, close source %v, close database %v", stepErr, sourceErr, databaseErr) + } + if hasXactColumn() { + t.Fatal("the down migration left xact_id behind") + } + var leftIndex int + if err := db.QueryRow(`SELECT count(*) FROM pg_indexes WHERE schemaname='public' AND indexname='idx_domain_events_xact_seq'`).Scan(&leftIndex); err != nil { + t.Fatal(err) + } + if leftIndex != 0 { + t.Fatal("the down migration left the index behind") + } + var kept int + if err := db.QueryRow(`SELECT count(*) FROM public.domain_events`).Scan(&kept); err != nil { + t.Fatal(err) + } + if kept != 4 { + t.Fatalf("the down migration left %d rows, want all 4", kept) + } + if reverted := readPublishDomainEvent(t, db); reverted != function { + t.Fatalf("the down migration changed publish_domain_event:\n got %+v\nwant %+v", reverted, function) + } + // The function still publishes without the column. + publishAsControlPlane(t, db, "00000000-0000-4000-8000-000000000005", "saas.test.after_down") + + if err := migrateStoreFrom(withUp, databaseURL); err != nil { + t.Fatalf("apply migration 62 again after its down: %v", err) + } + // Every row that exists when the column comes back is NULL again, the one + // published while it was gone included, and still reads in seq order. + var nullAgain int + if err := db.QueryRow(`SELECT count(*) FROM public.domain_events WHERE xact_id IS NULL`).Scan(&nullAgain); err != nil { + t.Fatal(err) + } + if nullAgain != 5 { + t.Fatalf("%d rows are NULL after re-applying, want 5", nullAgain) + } + t.Log("migration 62 added xact_id as a default without touching publish_domain_event; existing rows stayed NULL and ordered by seq; its down removed the column and index and kept every row") +} diff --git a/module/services/store/migrations/61_audit_journal_user_scoped_publish.down.sql b/module/services/store/migrations/61_audit_journal_user_scoped_publish.down.sql new file mode 100644 index 000000000..2f83b0df6 --- /dev/null +++ b/module/services/store/migrations/61_audit_journal_user_scoped_publish.down.sql @@ -0,0 +1,99 @@ +-- Reverts migration 41: publish_domain_event again refuses every NULL tenant from +-- app_tenant, so a user-scoped transaction cannot publish the journal record of an +-- audit entry that has no organization. The function is restored as migration 12 +-- left it: the baseline body, with pg_temp last in its search_path. The owner and +-- the grants are kept by CREATE OR REPLACE. The owner's authority is assumed for +-- the replacement as in the up migration. + +GRANT CREATE ON SCHEMA public TO app_control_plane; +SET LOCAL ROLE app_control_plane; + +CREATE OR REPLACE FUNCTION public.publish_domain_event(p_id uuid, p_type text, p_source text, p_subject text, p_event_time timestamp with time zone, p_specversion text, p_datacontenttype text, p_dataschema text, p_data bytea, p_tenant_id uuid, p_boundary_id text, p_partition_key text, p_correlation_id text, p_causation_id text, p_actor_principal_id text, p_owner_principal_id text, p_traceparent text, p_schema_version integer, p_request_fingerprint bytea) RETURNS TABLE(event_id uuid, stored_fingerprint bytea, inserted boolean) + LANGUAGE plpgsql SECURITY DEFINER + SET search_path TO 'pg_catalog', 'public', 'pg_temp' + AS $$ +DECLARE + caller_role TEXT; + inserted_id UUID; +BEGIN + caller_role := COALESCE( + NULLIF(current_setting('role', true), 'none'), + session_user::text + ); + + IF caller_role = 'app_tenant' THEN + -- IS DISTINCT FROM, not <>: a transaction that never set app.current_org_id + -- (a user-scoped one, which runs as app_tenant and sets only + -- app.current_user_id) leaves the setting NULL, and `p_tenant_id <> NULL` + -- is NULL, not TRUE — so a plain comparison lets that transaction publish + -- for any tenant it names. The guard has to fail closed on an absent scope. + IF p_tenant_id IS NULL + OR p_tenant_id IS DISTINCT FROM NULLIF(current_setting('app.current_org_id', true), '')::uuid THEN + RAISE EXCEPTION 'event tenant does not match the signed request scope' + USING ERRCODE = 'insufficient_privilege'; + END IF; + ELSIF caller_role = 'app_control_plane' THEN + -- The control plane publishes on behalf of a tenant. A platform-admin + -- mutation writes its audit record with RLS bypassed, and that record's + -- external domain event is what an outbound webhook subscription is + -- fanned out from, so refusing a tenant here would silently stop + -- delivering every privileged action to the endpoints subscribed to it. + -- The role already writes any tenant's audit rows; publishing the + -- matching event is the same authority, and the relay applies the tenant + -- gate again when it resolves subscriptions. + NULL; + ELSIF caller_role <> 'app_job_worker' THEN + RAISE EXCEPTION 'role % cannot publish domain events', caller_role + USING ERRCODE = 'insufficient_privilege'; + END IF; + + -- Serialize concurrent publishes within one partition so that the identity + -- seq assigned below is handed out in commit order, not merely in insert + -- order. The lock is transaction-scoped: it is held until the producer's + -- transaction commits or rolls back, which is exactly the window in which a + -- competing producer could otherwise take a higher seq and commit first. + -- Ordering is a per-partition guarantee, so the lock is per-partition; a + -- platform event with no partition key is unordered by construction + -- (eventOrdering returns no ordering key for it) and takes no lock, so it + -- never serializes against anything. + IF COALESCE(p_partition_key, '') <> '' THEN + PERFORM pg_advisory_xact_lock(hashtextextended('events:' || p_partition_key, 0)); + END IF; + + INSERT INTO public.domain_events ( + id, type, source, subject, event_time, specversion, datacontenttype, + dataschema, data, tenant_id, boundary_id, partition_key, correlation_id, + causation_id, actor_principal_id, owner_principal_id, traceparent, + schema_version, request_fingerprint + ) VALUES ( + p_id, p_type, p_source, COALESCE(p_subject, ''), p_event_time, + COALESCE(p_specversion, ''), COALESCE(p_datacontenttype, ''), + COALESCE(p_dataschema, ''), COALESCE(p_data, ''::bytea), p_tenant_id, + COALESCE(p_boundary_id, ''), COALESCE(p_partition_key, ''), + COALESCE(p_correlation_id, ''), COALESCE(p_causation_id, ''), + COALESCE(p_actor_principal_id, ''), COALESCE(p_owner_principal_id, ''), + COALESCE(p_traceparent, ''), COALESCE(p_schema_version, 1), + p_request_fingerprint + ) + ON CONFLICT (id) DO NOTHING + RETURNING id INTO inserted_id; + + IF inserted_id IS NOT NULL THEN + RETURN QUERY SELECT inserted_id, p_request_fingerprint, TRUE; + RETURN; + END IF; + + RETURN QUERY + SELECT existing.id, existing.request_fingerprint, FALSE + FROM public.domain_events AS existing + WHERE existing.id = p_id; + + IF NOT FOUND THEN + RAISE EXCEPTION 'domain event idempotency conflict could not be resolved' + USING ERRCODE = 'serialization_failure'; + END IF; +END +$$; + +RESET ROLE; +REVOKE CREATE ON SCHEMA public FROM app_control_plane; diff --git a/module/services/store/migrations/61_audit_journal_user_scoped_publish.up.sql b/module/services/store/migrations/61_audit_journal_user_scoped_publish.up.sql new file mode 100644 index 000000000..607f22b9f --- /dev/null +++ b/module/services/store/migrations/61_audit_journal_user_scoped_publish.up.sql @@ -0,0 +1,146 @@ +-- publish_domain_event admits one tenant-less event from a tenant-scoped caller: +-- the journal record of an audit entry that has no organization, written by a +-- user-scoped transaction for that user's own action. +-- +-- Under the warehouse audit backend every audit entry is published to the +-- journal as the internal type saas.audit.recorded.v1 in the caller's +-- transaction (ADR 0009). A user-scoped transaction runs as app_tenant and sets +-- only app.current_user_id, and the function refused every NULL tenant from +-- app_tenant, so an entry with no organization written there (a WebAuthn +-- registration, an MFA enrolment) could not be published. audit_events already +-- admits exactly that row (policy audit_events_tenant, WITH CHECK: org_id IS NULL +-- AND actor_id equal to app.current_user_id); the journal now admits the same +-- one, and nothing wider. The app_tenant branch admits a NULL tenant only when +-- the type is saas.audit.recorded.v1, app.current_user_id is set and non-empty, +-- and p_actor_principal_id is that user. The audit record in p_data must also +-- name that actor and no organization. A person's principal id is their user +-- id (sync_human_principal), and the emitter sets the envelope's actor to the +-- audit entry's actor id, the value audit_events.actor_id records. +-- +-- Every other refusal, the advisory lock, the insert and the return are the +-- function's own, unchanged. CREATE OR REPLACE keeps the owner and the grants and +-- replaces every attribute, so SECURITY DEFINER and the search_path migration 12 +-- set are stated again. Replacing a function needs its owner's authority and +-- CREATE on the schema: the migration principal grants the owner CREATE for the +-- replacement and takes it back, assumes the owner for it, and hands the +-- transaction back before the ledger write, as migrations 12 and 35 do. + +GRANT CREATE ON SCHEMA public TO app_control_plane; +SET LOCAL ROLE app_control_plane; + +CREATE OR REPLACE FUNCTION public.publish_domain_event(p_id uuid, p_type text, p_source text, p_subject text, p_event_time timestamp with time zone, p_specversion text, p_datacontenttype text, p_dataschema text, p_data bytea, p_tenant_id uuid, p_boundary_id text, p_partition_key text, p_correlation_id text, p_causation_id text, p_actor_principal_id text, p_owner_principal_id text, p_traceparent text, p_schema_version integer, p_request_fingerprint bytea) RETURNS TABLE(event_id uuid, stored_fingerprint bytea, inserted boolean) + LANGUAGE plpgsql SECURITY DEFINER + SET search_path TO 'pg_catalog', 'public', 'pg_temp' + AS $$ +DECLARE + caller_role TEXT; + inserted_id UUID; + audit_record JSONB; +BEGIN + caller_role := COALESCE( + NULLIF(current_setting('role', true), 'none'), + session_user::text + ); + + IF caller_role = 'app_tenant' THEN + -- IS DISTINCT FROM, not <>: a transaction that never set app.current_org_id + -- (a user-scoped one, which runs as app_tenant and sets only + -- app.current_user_id) leaves the setting NULL, and `p_tenant_id <> NULL` + -- is NULL, not TRUE — so a plain comparison lets that transaction publish + -- for any tenant it names. The guard has to fail closed on an absent scope. + IF p_tenant_id IS NULL + OR p_tenant_id IS DISTINCT FROM NULLIF(current_setting('app.current_org_id', true), '')::uuid THEN + -- The one tenant-less event a tenant-scoped caller may publish: the + -- journal record of an audit entry that has no organization, written + -- by a user-scoped transaction for that user's own action. It + -- publishes for no tenant, and only the internal audit type + -- saas.audit.recorded.v1 may do so. The rule mirrors the + -- audit_events_tenant policy that admits the same row into + -- audit_events (org_id IS NULL and actor_id equal to + -- app.current_user_id): app.current_user_id must be set and + -- non-empty, and the event's actor must be that user. The type is + -- internal, so the relay never fans it out and no subscription can + -- match it. Anything else that reaches here is refused: a tenant that + -- is not the request's, and a tenant-less event of any other type or + -- for any other actor. + IF p_tenant_id IS NOT NULL + OR p_type IS DISTINCT FROM 'saas.audit.recorded.v1' + OR NULLIF(current_setting('app.current_user_id', true), '') IS NULL + OR p_actor_principal_id IS DISTINCT FROM NULLIF(current_setting('app.current_user_id', true), '') THEN + RAISE EXCEPTION 'event tenant does not match the signed request scope' + USING ERRCODE = 'insufficient_privilege'; + END IF; + -- The audit entry lives in data, so an envelope naming the current + -- user must not smuggle in another actor or organization. + audit_record := convert_from(p_data, 'UTF8')::jsonb; + IF audit_record->>'actor_id' IS DISTINCT FROM p_actor_principal_id + OR COALESCE(audit_record->>'organization_id', '') <> '' THEN + RAISE EXCEPTION 'event tenant does not match the signed request scope' + USING ERRCODE = 'insufficient_privilege'; + END IF; + END IF; + ELSIF caller_role = 'app_control_plane' THEN + -- The control plane publishes on behalf of a tenant. A platform-admin + -- mutation writes its audit record with RLS bypassed, and that record's + -- external domain event is what an outbound webhook subscription is + -- fanned out from, so refusing a tenant here would silently stop + -- delivering every privileged action to the endpoints subscribed to it. + -- The role already writes any tenant's audit rows; publishing the + -- matching event is the same authority, and the relay applies the tenant + -- gate again when it resolves subscriptions. + NULL; + ELSIF caller_role <> 'app_job_worker' THEN + RAISE EXCEPTION 'role % cannot publish domain events', caller_role + USING ERRCODE = 'insufficient_privilege'; + END IF; + + -- Serialize concurrent publishes within one partition so that the identity + -- seq assigned below is handed out in commit order, not merely in insert + -- order. The lock is transaction-scoped: it is held until the producer's + -- transaction commits or rolls back, which is exactly the window in which a + -- competing producer could otherwise take a higher seq and commit first. + -- Ordering is a per-partition guarantee, so the lock is per-partition; a + -- platform event with no partition key is unordered by construction + -- (eventOrdering returns no ordering key for it) and takes no lock, so it + -- never serializes against anything. + IF COALESCE(p_partition_key, '') <> '' THEN + PERFORM pg_advisory_xact_lock(hashtextextended('events:' || p_partition_key, 0)); + END IF; + + INSERT INTO public.domain_events ( + id, type, source, subject, event_time, specversion, datacontenttype, + dataschema, data, tenant_id, boundary_id, partition_key, correlation_id, + causation_id, actor_principal_id, owner_principal_id, traceparent, + schema_version, request_fingerprint + ) VALUES ( + p_id, p_type, p_source, COALESCE(p_subject, ''), p_event_time, + COALESCE(p_specversion, ''), COALESCE(p_datacontenttype, ''), + COALESCE(p_dataschema, ''), COALESCE(p_data, ''::bytea), p_tenant_id, + COALESCE(p_boundary_id, ''), COALESCE(p_partition_key, ''), + COALESCE(p_correlation_id, ''), COALESCE(p_causation_id, ''), + COALESCE(p_actor_principal_id, ''), COALESCE(p_owner_principal_id, ''), + COALESCE(p_traceparent, ''), COALESCE(p_schema_version, 1), + p_request_fingerprint + ) + ON CONFLICT (id) DO NOTHING + RETURNING id INTO inserted_id; + + IF inserted_id IS NOT NULL THEN + RETURN QUERY SELECT inserted_id, p_request_fingerprint, TRUE; + RETURN; + END IF; + + RETURN QUERY + SELECT existing.id, existing.request_fingerprint, FALSE + FROM public.domain_events AS existing + WHERE existing.id = p_id; + + IF NOT FOUND THEN + RAISE EXCEPTION 'domain event idempotency conflict could not be resolved' + USING ERRCODE = 'serialization_failure'; + END IF; +END +$$; + +RESET ROLE; +REVOKE CREATE ON SCHEMA public FROM app_control_plane; diff --git a/module/services/store/migrations/62_domain_events_xact_id.down.sql b/module/services/store/migrations/62_domain_events_xact_id.down.sql new file mode 100644 index 000000000..0d2ba14f6 --- /dev/null +++ b/module/services/store/migrations/62_domain_events_xact_id.down.sql @@ -0,0 +1,8 @@ +-- Reverts migration 42: domain_events no longer records the transaction that wrote +-- a row. Dropping the column drops its default and the index on its expression +-- with it; the index is dropped first and by name so that the revert says what it +-- removes. Nothing else reads xact_id, so no other object changes. The ids are +-- not recoverable once dropped: a later migration 42 leaves every existing row +-- NULL, which reads as transaction 0. +DROP INDEX IF EXISTS public.idx_domain_events_xact_seq; +ALTER TABLE public.domain_events DROP COLUMN IF EXISTS xact_id; diff --git a/module/services/store/migrations/62_domain_events_xact_id.up.sql b/module/services/store/migrations/62_domain_events_xact_id.up.sql new file mode 100644 index 000000000..4d76fe408 --- /dev/null +++ b/module/services/store/migrations/62_domain_events_xact_id.up.sql @@ -0,0 +1,59 @@ +-- domain_events records the transaction that wrote each row, so a reader can tell +-- which rows can no longer change place. +-- +-- WHY. seq is the order rows were inserted, not the order their transactions +-- commit: a transaction that inserted seq 10 and is still open does not stop one +-- that inserted seq 11 from committing first, so a reader that has seen seq 11 may +-- still be owed seq 10. A cursor on seq therefore skips a row whenever it advances +-- past an open writer (EVENTS.md describes the re-read window a consumer needs for +-- that). The journal read for the platform's own reader (ReadJournal, ADR 0009) +-- needs a cursor that cannot skip, and that is exact when it is gated on the +-- writing transaction: every transaction that can still commit took its id at or +-- after pg_snapshot_xmin(pg_current_snapshot()), so a row whose xact_id is below +-- that bound belongs to a transaction that has finished, and a cursor that only +-- serves such rows, in (xact_id, seq) order, never passes a row that can still +-- appear below it. +-- +-- THE COLUMN. xact_id is the writing transaction's top-level id, set at insert by +-- a column default, pg_current_xact_id(). A default, not a change to +-- publish_domain_event: the only INSERT into this table is that function's, the +-- default fires there with the function's body, guard, advisory lock and grants +-- exactly as migration 41 left them, and nothing is replaced. pg_current_xact_id() +-- returns the top-level id even inside a subtransaction, which is the one +-- pg_snapshot_xmin compares against. The default is attached after the column is +-- added, not in the same statement: a volatile default on ADD COLUMN rewrites the +-- whole table under an exclusive lock, and the plain ADD COLUMN and SET DEFAULT +-- are catalog-only. +-- +-- EXISTING ROWS ARE LEFT NULL, AND NULL IS READ AS TRANSACTION 0. A backfill would +-- rewrite every row of a table that has no enforced retention, only to invent a +-- value: the writing transactions are long gone and their ids are not recorded. +-- None is needed. Adding the column takes an ACCESS EXCLUSIVE lock, which waits +-- for every transaction that has written to the table to finish, so each row that +-- exists when the column appears is committed; and every row written afterwards +-- takes a real id. The NULL rows are therefore one finished block that sorts +-- before every row with an id, ordered among themselves by seq, which is how they +-- have always been ordered. A reader keys on COALESCE(xact_id, '0'), and the index +-- is on that same expression, so the legacy block and the new rows read as one +-- ordered sequence with no special case. Position 0 is not a transaction id, so +-- no row ever collides with it. +-- +-- THE INDEX serves the read's shape: WHERE key > cursor AND key-below-the-frontier +-- ORDER BY key LIMIT n, where key is (COALESCE(xact_id, '0'), seq). It is built +-- without CONCURRENTLY, as migration 3's index on this table was: the runner +-- applies a migration in a transaction, where CONCURRENTLY is not allowed, and +-- the build blocks writers to the table for its duration. +-- +-- STALLS. The frontier holds behind a write transaction that stays open (a stuck +-- session, a long migration), and behind rows whose ids are ahead of the cluster +-- after a logical restore into a new cluster. Both stall the reader without +-- losing anything. + +ALTER TABLE public.domain_events ADD COLUMN xact_id xid8; +ALTER TABLE public.domain_events ALTER COLUMN xact_id SET DEFAULT pg_current_xact_id(); + +COMMENT ON COLUMN public.domain_events.xact_id IS + 'The top-level transaction id that wrote the row (pg_current_xact_id() at insert). NULL on a row that existed when this column was added; read as 0, before every real id. A row below pg_snapshot_xmin(pg_current_snapshot()) belongs to a finished transaction.'; + +CREATE INDEX idx_domain_events_xact_seq + ON public.domain_events USING btree ((COALESCE(xact_id, '0'::xid8)), seq); diff --git a/module/services/store/migrations/63_audit_journal_retention.down.sql b/module/services/store/migrations/63_audit_journal_retention.down.sql new file mode 100644 index 000000000..7cd3777ec --- /dev/null +++ b/module/services/store/migrations/63_audit_journal_retention.down.sql @@ -0,0 +1,4 @@ +REVOKE DELETE ON public.domain_events FROM app_control_plane; +DROP POLICY audit_journal_retention_delete ON public.domain_events; +REVOKE SELECT ON public.codefly_effect_receipts FROM app_control_plane; +DROP POLICY audit_delivery_retention_receipts ON public.codefly_effect_receipts; diff --git a/module/services/store/migrations/63_audit_journal_retention.up.sql b/module/services/store/migrations/63_audit_journal_retention.up.sql new file mode 100644 index 000000000..b39a26bab --- /dev/null +++ b/module/services/store/migrations/63_audit_journal_retention.up.sql @@ -0,0 +1,17 @@ +-- Retention reads only committed audit delivery receipts. Source-operation +-- responses remain tenant-only, and no control-plane receipt mutation is added. +GRANT SELECT ON public.codefly_effect_receipts TO app_control_plane; +CREATE POLICY audit_delivery_retention_receipts ON public.codefly_effect_receipts + FOR SELECT TO app_control_plane + USING (CURRENT_USER = 'app_control_plane'::name + AND method = '/saas.accounts.v1.AuditService/DeliverAuditEvents'); + +-- The existing permissive control-plane policy still governs its other +-- commands. A restrictive DELETE policy limits this new grant to old internal +-- audit rows even when another permissive policy also matches the caller. +-- Hours keep the thirty-day age independent of the session's time zone/DST. +GRANT DELETE ON public.domain_events TO app_control_plane; +CREATE POLICY audit_journal_retention_delete ON public.domain_events + AS RESTRICTIVE FOR DELETE TO app_control_plane + USING (type = 'saas.audit.recorded.v1' + AND created_at < statement_timestamp() - interval '720 hours'); diff --git a/module/tools/interface-docs-gate.mjs b/module/tools/interface-docs-gate.mjs index 3b5ced55c..4f6a0fb07 100644 --- a/module/tools/interface-docs-gate.mjs +++ b/module/tools/interface-docs-gate.mjs @@ -125,6 +125,7 @@ const METHOD_CONTEXTS = { "ModuleCapabilitiesService/PlaceRecord": "authorization", "ModuleCapabilitiesService/PublishEvent": "events", "ModuleCapabilitiesService/ReplayEvents": "events", + "ModuleCapabilitiesService/ReadJournal": "events", "ModuleCapabilitiesService/RequestApproval": "approvals", "ModuleCapabilitiesService/ResolveWorkContextAuthority": "authorization", "ModuleCapabilitiesService/Subscribe": "events", From bb66a84cfff053bcccf3f0842bc6ffa6b2e0d159 Mon Sep 17 00:00:00 2001 From: crisfiorini-oss <301773486+crisfiorini-oss@users.noreply.github.com> Date: Tue, 27 Oct 2026 16:45:55 +0000 Subject: [PATCH 2/4] fix(accounts): reuse shared member installation read --- .../source-read-sdk/library.codefly.yaml | 2 +- .../api/accounts/authority/contract.binpb | Bin 1310942 -> 1312473 bytes .../authority/proto/host/DIGESTS.sha256 | 2 +- .../host/installation/v1/installation.proto | 34 ++- .../api/accounts/connect/contract.binpb | Bin 1310942 -> 1312473 bytes .../connect/proto/host/DIGESTS.sha256 | 2 +- .../host/installation/v1/installation.proto | 34 ++- module/contracts/api/catalog.codefly.json | 5 +- module/docs/adr/0009-audit-store-swap.md | 3 +- module/module.package.codefly.yaml | 4 +- module/services/accounts/AUTHZ_MATRIX.md | 5 +- module/services/accounts/code/go.mod | 10 +- module/services/accounts/code/go.sum | 20 +- .../connect_handlers_host_interface.go | 4 + .../code/pkg/adapters/host_interface.go | 22 ++ .../code/pkg/business/interface_policies.go | 5 +- .../host/installation/v1/installation.pb.go | 222 +++++++++++++++--- .../installation/v1/installation_grpc.pb.go | 60 ++++- .../installation.connect.go | 51 +++- .../generated/api-contract-surfaces.json | 1 + .../accounts/generated/authz-methods.json | 37 +++ .../accounts/generated/gateway-routes.json | 16 ++ .../accounts/generated/service-catalog.json | 44 ++++ .../accounts/proto/host/DIGESTS.sha256 | 2 +- .../host/installation/v1/installation.proto | 34 ++- .../auth-gateway/code/authz_catalog_gen.go | 1 + .../auth-gateway/code/routing_catalog_gen.go | 1 + .../saas-sdk/generated/library.codefly.yaml | 2 +- .../host/installation/v1/installation_pb.ts | 94 +++++++- .../host/installation/v1/installation_pb.ts | 94 +++++++- 30 files changed, 725 insertions(+), 86 deletions(-) diff --git a/libraries/source-read-sdk/library.codefly.yaml b/libraries/source-read-sdk/library.codefly.yaml index d588416cd..c9735a84a 100644 --- a/libraries/source-read-sdk/library.codefly.yaml +++ b/libraries/source-read-sdk/library.codefly.yaml @@ -12,7 +12,7 @@ sources: module: saas-starter service: accounts endpoint: authority - contract-digest: sha256:12c6216596c7460c71bf83e3962a3724c19bd79401459088a51f7f3b3fc1e3d9 + contract-digest: sha256:a6dfc46790b2193c93c7952f1f6e57a0825c223fed428bff2adc10c59fa03221 services: - ModuleCapabilitiesService generated-by: diff --git a/module/contracts/api/accounts/authority/contract.binpb b/module/contracts/api/accounts/authority/contract.binpb index 4546de754e8ab5d76598fed0450582ad88512445..a6dfa546fb9ddb64f1d7800457c5f1b153755d36 100644 GIT binary patch delta 6814 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 TmE%`|UnPDr|LEwL12g^)ASwIu31Z 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 TmE%`|UnPDr|LEwL12g^)ASwIu31Z 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 Generated from protobuf service descriptors and `saas.policy.v1.method_policy`; only the prose description is joined from `pkg/business/introspection.go`. Do not edit by hand. Run `go generate ./pkg/business` from `module/services/accounts/code`. -Inventory: **299 RPCs** across **41 services**. Missing, invalid, or unclassified procedures are denied by both Connect and gRPC interceptors. +Inventory: **300 RPCs** across **41 services**. Missing, invalid, or unclassified procedures are denied by both Connect and gRPC interceptors. The compact tier is retained for compatibility. Guards, resource bindings, audit events, limiter class, and data sensitivity are descriptor-authoritative. Domain handlers may enforce stronger state-dependent rules but may not weaken this floor. @@ -27,6 +27,7 @@ The compact tier is retained for compatibility. Guards, resource bindings, audit | `/host.installation.v1.InstallationService/GrantRole` | unary | `—` | `auth` | exposure=AUTHENTICATED; tenant=NONE; impersonation=FORBIDDEN | — | — | SUCCESS: saas.authority.role_granted | FORBIDDEN / STANDARD_WRITE | CONFIDENTIAL → CONFIDENTIAL | Grant a person or a team one of an installed solution's roles. | | `/host.installation.v1.InstallationService/Install` | unary | `—` | `org_admin` | exposure=AUTHENTICATED; tenant=ORG_ADMIN; impersonation=FORBIDDEN | — | — | SUCCESS: saas.authority.installation_requested, saas.authority.installation_activated | FORBIDDEN / STANDARD_WRITE | CONFIDENTIAL → CONFIDENTIAL | Install one solution at one organisation under the consent its administrator reviewed; pending until its install step converges. | | `/host.installation.v1.InstallationService/List` | unary | `—` | `org_admin` | exposure=AUTHENTICATED; tenant=ORG_ADMIN; impersonation=FORBIDDEN | — | — | — | OPTIONAL / STANDARD_READ | CONFIDENTIAL → CONFIDENTIAL | List what one organisation has installed. | +| `/host.installation.v1.InstallationService/ListHeld` | unary | `—` | `auth` | exposure=AUTHENTICATED; tenant=NONE; impersonation=FORBIDDEN | — | — | — | OPTIONAL / STANDARD_READ | CONFIDENTIAL → CONFIDENTIAL | List only the signed-in member's current installation roles, including team grants. | | `/host.installation.v1.InstallationService/ListRoleGrants` | unary | `—` | `auth` | exposure=AUTHENTICATED; tenant=NONE; impersonation=FORBIDDEN | — | — | — | OPTIONAL / STANDARD_READ | CONFIDENTIAL → CONFIDENTIAL | List who holds which role on one installation. | | `/host.installation.v1.InstallationService/Review` | unary | `—` | `org_admin` | exposure=AUTHENTICATED; tenant=ORG_ADMIN; impersonation=FORBIDDEN | — | — | — | OPTIONAL / STANDARD_READ | CONFIDENTIAL → CONFIDENTIAL | Read the exact authority offer an administrator consents to, without recording consent. | | `/host.installation.v1.InstallationService/RevokeRole` | unary | `—` | `auth` | exposure=AUTHENTICATED; tenant=NONE; impersonation=FORBIDDEN | — | — | SUCCESS: saas.authority.role_revoked | FORBIDDEN / STANDARD_WRITE | CONFIDENTIAL → CONFIDENTIAL | Revoke one role grant; its subject's capabilities through the installation end. | @@ -310,7 +311,7 @@ The compact tier is retained for compatibility. Guards, resource bindings, audit ## Tier totals -- `auth`: 52 +- `auth`: 53 - `internal`: 56 - `mfa`: 5 - `org_admin`: 68 diff --git a/module/services/accounts/code/go.mod b/module/services/accounts/code/go.mod index d320900bf..96f7df6b0 100644 --- a/module/services/accounts/code/go.mod +++ b/module/services/accounts/code/go.mod @@ -7,11 +7,11 @@ require ( buf.build/go/protovalidate v1.4.0 connectrpc.com/connect v1.21.0 connectrpc.com/otelconnect v0.9.0 - github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261027132425-c3568f113db1 - github.com/codefly-dev/authority/libraries/authority-core/go v0.0.0-20261027132425-c3568f113db1 - github.com/codefly-dev/authority/libraries/conformance/go v0.0.0-20261027132425-c3568f113db1 - github.com/codefly-dev/authority/libraries/delivery/go v0.0.0-20261027132425-c3568f113db1 - github.com/codefly-dev/authority/libraries/resources/go v0.0.0-20261027132425-c3568f113db1 + github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261027160913-bfe4876a8b96 + github.com/codefly-dev/authority/libraries/authority-core/go v0.0.0-20261027160913-bfe4876a8b96 + github.com/codefly-dev/authority/libraries/conformance/go v0.0.0-20261027160913-bfe4876a8b96 + github.com/codefly-dev/authority/libraries/delivery/go v0.0.0-20261027160913-bfe4876a8b96 + github.com/codefly-dev/authority/libraries/resources/go v0.0.0-20261027160913-bfe4876a8b96 github.com/codefly-dev/cli/contracts/deployment v0.0.0-20261006151622-5824cf5342fc github.com/codefly-dev/core v0.19.7-0.20261026092055-b23dc13543b2 github.com/codefly-dev/interface-cache/go/cache v0.4.1 diff --git a/module/services/accounts/code/go.sum b/module/services/accounts/code/go.sum index 9f99cf9b9..77d9fc6d6 100644 --- a/module/services/accounts/code/go.sum +++ b/module/services/accounts/code/go.sum @@ -56,18 +56,18 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= -github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261027132425-c3568f113db1 h1:y8J/hDtidFnBvfdRoApLx5qsbWxtpxXjAO9OyhuXXT4= -github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261027132425-c3568f113db1/go.mod h1:4hRjiWuJOKzrEk8AS87pbWCbpiEmhEvQBGQaJo5Hmkg= -github.com/codefly-dev/authority/libraries/authority-core/go v0.0.0-20261027132425-c3568f113db1 h1:nszx0AoJd9w08MwUWr380g1fVEJ4V8KiDQQ+hTfGYes= -github.com/codefly-dev/authority/libraries/authority-core/go v0.0.0-20261027132425-c3568f113db1/go.mod h1:kTjeuNl03XSCkH3UhGKuHAKamkIzNsC2RPFu/g0fqSk= -github.com/codefly-dev/authority/libraries/conformance/go v0.0.0-20261027132425-c3568f113db1 h1:3KYJZ1TSC7nFyPls9PM6zXgt6QcnsQcPjRm2s+QwRSg= -github.com/codefly-dev/authority/libraries/conformance/go v0.0.0-20261027132425-c3568f113db1/go.mod h1:or3dd9bqacXWMjke/n81l/QxRaLwq2lq/hL4V2qtAZo= -github.com/codefly-dev/authority/libraries/delivery/go v0.0.0-20261027132425-c3568f113db1 h1:5Lm7ZEss7Y/pJS6owYdYNNqfVR+cNv31dc2HWtW3x+I= -github.com/codefly-dev/authority/libraries/delivery/go v0.0.0-20261027132425-c3568f113db1/go.mod h1:XsOFnO2MQdr37vM3ULa0Uwi8ZWptyVx7hRSUyIdaZ1Q= +github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261027160913-bfe4876a8b96 h1:N8Gb3M8Ep9Mrqlhs/3+J1R4DZSb+aO2XvOqKODIOsms= +github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261027160913-bfe4876a8b96/go.mod h1:4hRjiWuJOKzrEk8AS87pbWCbpiEmhEvQBGQaJo5Hmkg= +github.com/codefly-dev/authority/libraries/authority-core/go v0.0.0-20261027160913-bfe4876a8b96 h1:F3Pluk6x4lw3sjuTWBpFBTWCx8d7QgLkxNiibNRLAVU= +github.com/codefly-dev/authority/libraries/authority-core/go v0.0.0-20261027160913-bfe4876a8b96/go.mod h1:kTjeuNl03XSCkH3UhGKuHAKamkIzNsC2RPFu/g0fqSk= +github.com/codefly-dev/authority/libraries/conformance/go v0.0.0-20261027160913-bfe4876a8b96 h1:JAwG62/l+wluGw3hok6FFs0qUUjVnnoOH2AYFnjkUXA= +github.com/codefly-dev/authority/libraries/conformance/go v0.0.0-20261027160913-bfe4876a8b96/go.mod h1:or3dd9bqacXWMjke/n81l/QxRaLwq2lq/hL4V2qtAZo= +github.com/codefly-dev/authority/libraries/delivery/go v0.0.0-20261027160913-bfe4876a8b96 h1:gd28HWTEPfBU5z9UIw6GeeCo1nK0wEc268wOhccrIGg= +github.com/codefly-dev/authority/libraries/delivery/go v0.0.0-20261027160913-bfe4876a8b96/go.mod h1:XsOFnO2MQdr37vM3ULa0Uwi8ZWptyVx7hRSUyIdaZ1Q= github.com/codefly-dev/authority/libraries/hop/go v0.0.0-20261027063450-22b86ccbf2a5 h1:pHywzlszDDjNJIOg52YhmDCpW+7N4c/Ftma/qx5HxxE= github.com/codefly-dev/authority/libraries/hop/go v0.0.0-20261027063450-22b86ccbf2a5/go.mod h1:9WF8dd98HYX9g/49uhrTgaTbWXo3hBvYzS3UVWYiZHw= -github.com/codefly-dev/authority/libraries/resources/go v0.0.0-20261027132425-c3568f113db1 h1:xjWJja3adt61lpYvwtz3VYekrxU4r1sHvOmW9YQ0K0Q= -github.com/codefly-dev/authority/libraries/resources/go v0.0.0-20261027132425-c3568f113db1/go.mod h1:1rmjrVyr0h7zOuwOHS9i+BoeS9dr3XNvtupLZFwY6As= +github.com/codefly-dev/authority/libraries/resources/go v0.0.0-20261027160913-bfe4876a8b96 h1:Tf208FcTIlJHfjuhJJF/bshqRwye5aL5wflvbiR3Z3U= +github.com/codefly-dev/authority/libraries/resources/go v0.0.0-20261027160913-bfe4876a8b96/go.mod h1:1rmjrVyr0h7zOuwOHS9i+BoeS9dr3XNvtupLZFwY6As= github.com/codefly-dev/cli/contracts/deployment v0.0.0-20261006151622-5824cf5342fc h1:olDnVJ1A1Retn0z9A59YMwDRmDLgkV2y1lbl+HzNrj8= github.com/codefly-dev/cli/contracts/deployment v0.0.0-20261006151622-5824cf5342fc/go.mod h1:VWPYY3FH4te7SzdN2xl9odNpWidJ1Grl/Hjs1SNoEF8= github.com/codefly-dev/core v0.19.7-0.20261026092055-b23dc13543b2 h1:I1wL0ANXhTdQuEAnxXF+s4+rKILlBu/QHILq4Skyi3I= diff --git a/module/services/accounts/code/pkg/adapters/connect_handlers_host_interface.go b/module/services/accounts/code/pkg/adapters/connect_handlers_host_interface.go index 1c672b012..ef069be17 100644 --- a/module/services/accounts/code/pkg/adapters/connect_handlers_host_interface.go +++ b/module/services/accounts/code/pkg/adapters/connect_handlers_host_interface.go @@ -96,6 +96,10 @@ func (h *hostInstallationConnectHandler) List(ctx context.Context, req *connect. return unary(ctx, req, h.server().List) } +func (h *hostInstallationConnectHandler) ListHeld(ctx context.Context, req *connect.Request[installationv1.ListHeldRequest]) (*connect.Response[installationv1.ListHeldResponse], error) { + return unary(ctx, req, h.server().ListHeld) +} + type hostSessionConnectHandler struct{ svc *business.Service } func (h *hostSessionConnectHandler) SignIn(ctx context.Context, req *connect.Request[sessionv1.SignInRequest]) (*connect.Response[sessionv1.Session], error) { diff --git a/module/services/accounts/code/pkg/adapters/host_interface.go b/module/services/accounts/code/pkg/adapters/host_interface.go index db83bf604..f2149fb49 100644 --- a/module/services/accounts/code/pkg/adapters/host_interface.go +++ b/module/services/accounts/code/pkg/adapters/host_interface.go @@ -557,6 +557,28 @@ func (s *HostInstallationServer) List(ctx context.Context, req *installationv1.L return out, nil } +// ListHeld reads the signed-in member's current installation roles through +// the shared Authority core, including team grants and their live revocation. +func (s *HostInstallationServer) ListHeld(ctx context.Context, req *installationv1.ListHeldRequest) (*installationv1.ListHeldResponse, error) { + core, err := authorityCore() + if err != nil { + return nil, err + } + caller, err := signedInPerson(ctx, "list held installations") + if err != nil { + return nil, err + } + held, err := core.HeldInstallations(ctx, caller, req.GetOrganizationId()) + if err != nil { + return nil, coreStatus(err) + } + out := &installationv1.ListHeldResponse{} + for _, h := range held { + out.Installations = append(out.Installations, &installationv1.HeldInstallation{Installation: installationToWire(core, ctx, h.Installation), Roles: h.Roles}) + } + return out, nil +} + // installationToWire is an installation as the interface states it, with the // roles its solution declares. func installationToWire(core *authority.Service, ctx context.Context, i domain.Installation) *installationv1.Installation { diff --git a/module/services/accounts/code/pkg/business/interface_policies.go b/module/services/accounts/code/pkg/business/interface_policies.go index 377f715ed..fec1704b9 100644 --- a/module/services/accounts/code/pkg/business/interface_policies.go +++ b/module/services/accounts/code/pkg/business/interface_policies.go @@ -229,7 +229,9 @@ func InterfaceMethodPolicies() map[string]*policyv1.MethodPolicy { "/host.installation.v1.InstallationService/GrantRole": roleChange(EventAuthorityRoleGranted), "/host.installation.v1.InstallationService/RevokeRole": roleChange(EventAuthorityRoleRevoked), "/host.installation.v1.InstallationService/ListRoleGrants": read(policyv1.Exposure_EXPOSURE_AUTHENTICATED, policyv1.TenantRequirement_TENANT_REQUIREMENT_NONE, policyv1.Sensitivity_SENSITIVITY_CONFIDENTIAL), - "/host.installation.v1.InstallationService/List": read(policyv1.Exposure_EXPOSURE_AUTHENTICATED, policyv1.TenantRequirement_TENANT_REQUIREMENT_ORG_ADMIN, policyv1.Sensitivity_SENSITIVITY_CONFIDENTIAL), // an administrator's read; a member's view is the entitlement listing + "/host.installation.v1.InstallationService/List": read(policyv1.Exposure_EXPOSURE_AUTHENTICATED, policyv1.TenantRequirement_TENANT_REQUIREMENT_ORG_ADMIN, policyv1.Sensitivity_SENSITIVITY_CONFIDENTIAL), // an administrator's read; a member's own holdings are ListHeld + // The core restricts this read to the signed-in member's own holdings. + "/host.installation.v1.InstallationService/ListHeld": read(policyv1.Exposure_EXPOSURE_AUTHENTICATED, policyv1.TenantRequirement_TENANT_REQUIREMENT_NONE, policyv1.Sensitivity_SENSITIVITY_CONFIDENTIAL), // Current is the caller's own session: authenticated by the bearer it // describes, organisation-less like the session itself may be. "/host.session.v1.SessionService/Current": read(policyv1.Exposure_EXPOSURE_AUTHENTICATED, policyv1.TenantRequirement_TENANT_REQUIREMENT_NONE, policyv1.Sensitivity_SENSITIVITY_CONFIDENTIAL), @@ -314,6 +316,7 @@ var interfaceDescriptions = map[string]string{ "InstallationService/GrantRole": "Grant a person or a team one of an installed solution's roles.", "InstallationService/RevokeRole": "Revoke one role grant; its subject's capabilities through the installation end.", "InstallationService/ListRoleGrants": "List who holds which role on one installation.", + "InstallationService/ListHeld": "List only the signed-in member's current installation roles, including team grants.", "InstallationService/List": "List what one organisation has installed.", "AuthorityService/ListResources": "Answer which resources of one kind a checked capability covers for one action, from its owner's current grants.", "AuthorityService/Delegate": "Record a signed-in person's grant to a module, within what they hold.", diff --git a/module/services/accounts/code/pkg/gen/host/installation/v1/installation.pb.go b/module/services/accounts/code/pkg/gen/host/installation/v1/installation.pb.go index 9bc9cbdd4..d7a3b8b8d 100644 --- a/module/services/accounts/code/pkg/gen/host/installation/v1/installation.pb.go +++ b/module/services/accounts/code/pkg/gen/host/installation/v1/installation.pb.go @@ -1633,6 +1633,155 @@ func (x *OfferTool) GetWrite() bool { return false } +// ListHeldRequest names the organisation the person reads at: the one their +// session was opened at. +type ListHeldRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The organisation. + OrganizationId string `protobuf:"bytes,1,opt,name=organization_id,json=organizationId,proto3" json:"organization_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListHeldRequest) Reset() { + *x = ListHeldRequest{} + mi := &file_host_installation_v1_installation_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListHeldRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListHeldRequest) ProtoMessage() {} + +func (x *ListHeldRequest) ProtoReflect() protoreflect.Message { + mi := &file_host_installation_v1_installation_proto_msgTypes[24] + 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 ListHeldRequest.ProtoReflect.Descriptor instead. +func (*ListHeldRequest) Descriptor() ([]byte, []int) { + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{24} +} + +func (x *ListHeldRequest) GetOrganizationId() string { + if x != nil { + return x.OrganizationId + } + return "" +} + +// ListHeldResponse is every installation the person holds a role on, sorted +// by installation id. +type ListHeldResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The installations. + Installations []*HeldInstallation `protobuf:"bytes,1,rep,name=installations,proto3" json:"installations,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListHeldResponse) Reset() { + *x = ListHeldResponse{} + mi := &file_host_installation_v1_installation_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListHeldResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListHeldResponse) ProtoMessage() {} + +func (x *ListHeldResponse) ProtoReflect() protoreflect.Message { + mi := &file_host_installation_v1_installation_proto_msgTypes[25] + 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 ListHeldResponse.ProtoReflect.Descriptor instead. +func (*ListHeldResponse) Descriptor() ([]byte, []int) { + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{25} +} + +func (x *ListHeldResponse) GetInstallations() []*HeldInstallation { + if x != nil { + return x.Installations + } + return nil +} + +// HeldInstallation is one installation the person may use. +type HeldInstallation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The installation. + Installation *Installation `protobuf:"bytes,1,opt,name=installation,proto3" json:"installation,omitempty"` + // The roles the person holds on it, directly or through a team, sorted. + Roles []string `protobuf:"bytes,2,rep,name=roles,proto3" json:"roles,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HeldInstallation) Reset() { + *x = HeldInstallation{} + mi := &file_host_installation_v1_installation_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HeldInstallation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HeldInstallation) ProtoMessage() {} + +func (x *HeldInstallation) ProtoReflect() protoreflect.Message { + mi := &file_host_installation_v1_installation_proto_msgTypes[26] + 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 HeldInstallation.ProtoReflect.Descriptor instead. +func (*HeldInstallation) Descriptor() ([]byte, []int) { + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{26} +} + +func (x *HeldInstallation) GetInstallation() *Installation { + if x != nil { + return x.Installation + } + return nil +} + +func (x *HeldInstallation) GetRoles() []string { + if x != nil { + return x.Roles + } + return nil +} + var File_host_installation_v1_installation_proto protoreflect.FileDescriptor const file_host_installation_v1_installation_proto_rawDesc = "" + @@ -1746,7 +1895,14 @@ const file_host_installation_v1_installation_proto_rawDesc = "" + "\tOfferTool\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n" + "\abinding\x18\x02 \x01(\tR\abinding\x12\x14\n" + - "\x05write\x18\x03 \x01(\bR\x05write2\x98\x05\n" + + "\x05write\x18\x03 \x01(\bR\x05write\":\n" + + "\x0fListHeldRequest\x12'\n" + + "\x0forganization_id\x18\x01 \x01(\tR\x0eorganizationId\"`\n" + + "\x10ListHeldResponse\x12L\n" + + "\rinstallations\x18\x01 \x03(\v2&.host.installation.v1.HeldInstallationR\rinstallations\"p\n" + + "\x10HeldInstallation\x12F\n" + + "\finstallation\x18\x01 \x01(\v2\".host.installation.v1.InstallationR\finstallation\x12\x14\n" + + "\x05roles\x18\x02 \x03(\tR\x05roles2\xf3\x05\n" + "\x13InstallationService\x12f\n" + "\aCurrent\x120.host.installation.v1.CurrentInstallationRequest\x1a).host.installation.v1.CurrentInstallation\x12S\n" + "\aInstall\x12$.host.installation.v1.InstallRequest\x1a\".host.installation.v1.Installation\x12T\n" + @@ -1755,7 +1911,8 @@ const file_host_installation_v1_installation_proto_rawDesc = "" + "\n" + "RevokeRole\x12'.host.installation.v1.RevokeRoleRequest\x1a\".host.installation.v1.Installation\x12k\n" + "\x0eListRoleGrants\x12+.host.installation.v1.ListRoleGrantsRequest\x1a,.host.installation.v1.ListRoleGrantsResponse\x12M\n" + - "\x04List\x12!.host.installation.v1.ListRequest\x1a\".host.installation.v1.ListResponse2p\n" + + "\x04List\x12!.host.installation.v1.ListRequest\x1a\".host.installation.v1.ListResponse\x12Y\n" + + "\bListHeld\x12%.host.installation.v1.ListHeldRequest\x1a&.host.installation.v1.ListHeldResponse2p\n" + "\x12InstallableService\x12Z\n" + "\aInstall\x12(.host.installation.v1.InstallationNotice\x1a%.host.installation.v1.InstallationAck2\xc8\x01\n" + "\fOfferService\x12_\n" + @@ -1777,7 +1934,7 @@ func file_host_installation_v1_installation_proto_rawDescGZIP() []byte { } var file_host_installation_v1_installation_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_host_installation_v1_installation_proto_msgTypes = make([]protoimpl.MessageInfo, 24) +var file_host_installation_v1_installation_proto_msgTypes = make([]protoimpl.MessageInfo, 27) var file_host_installation_v1_installation_proto_goTypes = []any{ (Installation_Status)(0), // 0: host.installation.v1.Installation.Status (*CurrentInstallationRequest)(nil), // 1: host.installation.v1.CurrentInstallationRequest @@ -1804,6 +1961,9 @@ var file_host_installation_v1_installation_proto_goTypes = []any{ (*OfferScope)(nil), // 22: host.installation.v1.OfferScope (*OfferCapability)(nil), // 23: host.installation.v1.OfferCapability (*OfferTool)(nil), // 24: host.installation.v1.OfferTool + (*ListHeldRequest)(nil), // 25: host.installation.v1.ListHeldRequest + (*ListHeldResponse)(nil), // 26: host.installation.v1.ListHeldResponse + (*HeldInstallation)(nil), // 27: host.installation.v1.HeldInstallation } var file_host_installation_v1_installation_proto_depIdxs = []int32{ 5, // 0: host.installation.v1.InstallRequest.approval:type_name -> host.installation.v1.ConsentApproval @@ -1821,31 +1981,35 @@ var file_host_installation_v1_installation_proto_depIdxs = []int32{ 23, // 12: host.installation.v1.OfferRole.capabilities:type_name -> host.installation.v1.OfferCapability 22, // 13: host.installation.v1.OfferCapability.requires:type_name -> host.installation.v1.OfferScope 24, // 14: host.installation.v1.OfferCapability.tools:type_name -> host.installation.v1.OfferTool - 1, // 15: host.installation.v1.InstallationService.Current:input_type -> host.installation.v1.CurrentInstallationRequest - 3, // 16: host.installation.v1.InstallationService.Install:input_type -> host.installation.v1.InstallRequest - 4, // 17: host.installation.v1.InstallationService.Review:input_type -> host.installation.v1.ReviewRequest - 6, // 18: host.installation.v1.InstallationService.GrantRole:input_type -> host.installation.v1.GrantRoleRequest - 7, // 19: host.installation.v1.InstallationService.RevokeRole:input_type -> host.installation.v1.RevokeRoleRequest - 8, // 20: host.installation.v1.InstallationService.ListRoleGrants:input_type -> host.installation.v1.ListRoleGrantsRequest - 11, // 21: host.installation.v1.InstallationService.List:input_type -> host.installation.v1.ListRequest - 14, // 22: host.installation.v1.InstallableService.Install:input_type -> host.installation.v1.InstallationNotice - 16, // 23: host.installation.v1.OfferService.ListOffers:input_type -> host.installation.v1.ListOffersRequest - 4, // 24: host.installation.v1.OfferService.ReviewOffer:input_type -> host.installation.v1.ReviewRequest - 2, // 25: host.installation.v1.InstallationService.Current:output_type -> host.installation.v1.CurrentInstallation - 13, // 26: host.installation.v1.InstallationService.Install:output_type -> host.installation.v1.Installation - 5, // 27: host.installation.v1.InstallationService.Review:output_type -> host.installation.v1.ConsentApproval - 13, // 28: host.installation.v1.InstallationService.GrantRole:output_type -> host.installation.v1.Installation - 13, // 29: host.installation.v1.InstallationService.RevokeRole:output_type -> host.installation.v1.Installation - 9, // 30: host.installation.v1.InstallationService.ListRoleGrants:output_type -> host.installation.v1.ListRoleGrantsResponse - 12, // 31: host.installation.v1.InstallationService.List:output_type -> host.installation.v1.ListResponse - 15, // 32: host.installation.v1.InstallableService.Install:output_type -> host.installation.v1.InstallationAck - 17, // 33: host.installation.v1.OfferService.ListOffers:output_type -> host.installation.v1.ListOffersResponse - 19, // 34: host.installation.v1.OfferService.ReviewOffer:output_type -> host.installation.v1.ReviewedOffer - 25, // [25:35] is the sub-list for method output_type - 15, // [15:25] is the sub-list for method input_type - 15, // [15:15] is the sub-list for extension type_name - 15, // [15:15] is the sub-list for extension extendee - 0, // [0:15] is the sub-list for field type_name + 27, // 15: host.installation.v1.ListHeldResponse.installations:type_name -> host.installation.v1.HeldInstallation + 13, // 16: host.installation.v1.HeldInstallation.installation:type_name -> host.installation.v1.Installation + 1, // 17: host.installation.v1.InstallationService.Current:input_type -> host.installation.v1.CurrentInstallationRequest + 3, // 18: host.installation.v1.InstallationService.Install:input_type -> host.installation.v1.InstallRequest + 4, // 19: host.installation.v1.InstallationService.Review:input_type -> host.installation.v1.ReviewRequest + 6, // 20: host.installation.v1.InstallationService.GrantRole:input_type -> host.installation.v1.GrantRoleRequest + 7, // 21: host.installation.v1.InstallationService.RevokeRole:input_type -> host.installation.v1.RevokeRoleRequest + 8, // 22: host.installation.v1.InstallationService.ListRoleGrants:input_type -> host.installation.v1.ListRoleGrantsRequest + 11, // 23: host.installation.v1.InstallationService.List:input_type -> host.installation.v1.ListRequest + 25, // 24: host.installation.v1.InstallationService.ListHeld:input_type -> host.installation.v1.ListHeldRequest + 14, // 25: host.installation.v1.InstallableService.Install:input_type -> host.installation.v1.InstallationNotice + 16, // 26: host.installation.v1.OfferService.ListOffers:input_type -> host.installation.v1.ListOffersRequest + 4, // 27: host.installation.v1.OfferService.ReviewOffer:input_type -> host.installation.v1.ReviewRequest + 2, // 28: host.installation.v1.InstallationService.Current:output_type -> host.installation.v1.CurrentInstallation + 13, // 29: host.installation.v1.InstallationService.Install:output_type -> host.installation.v1.Installation + 5, // 30: host.installation.v1.InstallationService.Review:output_type -> host.installation.v1.ConsentApproval + 13, // 31: host.installation.v1.InstallationService.GrantRole:output_type -> host.installation.v1.Installation + 13, // 32: host.installation.v1.InstallationService.RevokeRole:output_type -> host.installation.v1.Installation + 9, // 33: host.installation.v1.InstallationService.ListRoleGrants:output_type -> host.installation.v1.ListRoleGrantsResponse + 12, // 34: host.installation.v1.InstallationService.List:output_type -> host.installation.v1.ListResponse + 26, // 35: host.installation.v1.InstallationService.ListHeld:output_type -> host.installation.v1.ListHeldResponse + 15, // 36: host.installation.v1.InstallableService.Install:output_type -> host.installation.v1.InstallationAck + 17, // 37: host.installation.v1.OfferService.ListOffers:output_type -> host.installation.v1.ListOffersResponse + 19, // 38: host.installation.v1.OfferService.ReviewOffer:output_type -> host.installation.v1.ReviewedOffer + 28, // [28:39] is the sub-list for method output_type + 17, // [17:28] is the sub-list for method input_type + 17, // [17:17] is the sub-list for extension type_name + 17, // [17:17] is the sub-list for extension extendee + 0, // [0:17] is the sub-list for field type_name } func init() { file_host_installation_v1_installation_proto_init() } @@ -1859,7 +2023,7 @@ func file_host_installation_v1_installation_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_host_installation_v1_installation_proto_rawDesc), len(file_host_installation_v1_installation_proto_rawDesc)), NumEnums: 1, - NumMessages: 24, + NumMessages: 27, NumExtensions: 0, NumServices: 3, }, diff --git a/module/services/accounts/code/pkg/gen/host/installation/v1/installation_grpc.pb.go b/module/services/accounts/code/pkg/gen/host/installation/v1/installation_grpc.pb.go index 0fdefdf19..492488672 100644 --- a/module/services/accounts/code/pkg/gen/host/installation/v1/installation_grpc.pb.go +++ b/module/services/accounts/code/pkg/gen/host/installation/v1/installation_grpc.pb.go @@ -33,6 +33,7 @@ const ( InstallationService_RevokeRole_FullMethodName = "/host.installation.v1.InstallationService/RevokeRole" InstallationService_ListRoleGrants_FullMethodName = "/host.installation.v1.InstallationService/ListRoleGrants" InstallationService_List_FullMethodName = "/host.installation.v1.InstallationService/List" + InstallationService_ListHeld_FullMethodName = "/host.installation.v1.InstallationService/ListHeld" ) // InstallationServiceClient is the client API for InstallationService service. @@ -61,10 +62,16 @@ type InstallationServiceClient interface { // ListRoleGrants answers who holds which role on the installation; the same // callers as GrantRole. ListRoleGrants(ctx context.Context, in *ListRoleGrantsRequest, opts ...grpc.CallOption) (*ListRoleGrantsResponse, error) - // List answers what one organisation has installed: an administrator's read. A - // member's view, which installed solutions they may use, is their navigation and - // belongs to the entitlement read, not here. + // List answers what one organisation has installed: an administrator's read. + // A member's view, which installed solutions they may use, is ListHeld. List(ctx context.Context, in *ListRequest, opts ...grpc.CallOption) (*ListResponse, error) + // ListHeld answers the signed-in person which installations of their + // organisation they may use: each active installation on which they hold a + // role, granted to them or to a team of theirs, with the roles they hold. + // It is their navigation and answers their own holdings alone, never who + // else holds what. A member holding nothing reads an empty list; a caller + // acting at another organisation is refused naming it. + ListHeld(ctx context.Context, in *ListHeldRequest, opts ...grpc.CallOption) (*ListHeldResponse, error) } type installationServiceClient struct { @@ -145,6 +152,16 @@ func (c *installationServiceClient) List(ctx context.Context, in *ListRequest, o return out, nil } +func (c *installationServiceClient) ListHeld(ctx context.Context, in *ListHeldRequest, opts ...grpc.CallOption) (*ListHeldResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListHeldResponse) + err := c.cc.Invoke(ctx, InstallationService_ListHeld_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // InstallationServiceServer is the server API for InstallationService service. // All implementations must embed UnimplementedInstallationServiceServer // for forward compatibility. @@ -171,10 +188,16 @@ type InstallationServiceServer interface { // ListRoleGrants answers who holds which role on the installation; the same // callers as GrantRole. ListRoleGrants(context.Context, *ListRoleGrantsRequest) (*ListRoleGrantsResponse, error) - // List answers what one organisation has installed: an administrator's read. A - // member's view, which installed solutions they may use, is their navigation and - // belongs to the entitlement read, not here. + // List answers what one organisation has installed: an administrator's read. + // A member's view, which installed solutions they may use, is ListHeld. List(context.Context, *ListRequest) (*ListResponse, error) + // ListHeld answers the signed-in person which installations of their + // organisation they may use: each active installation on which they hold a + // role, granted to them or to a team of theirs, with the roles they hold. + // It is their navigation and answers their own holdings alone, never who + // else holds what. A member holding nothing reads an empty list; a caller + // acting at another organisation is refused naming it. + ListHeld(context.Context, *ListHeldRequest) (*ListHeldResponse, error) mustEmbedUnimplementedInstallationServiceServer() } @@ -206,6 +229,9 @@ func (UnimplementedInstallationServiceServer) ListRoleGrants(context.Context, *L func (UnimplementedInstallationServiceServer) List(context.Context, *ListRequest) (*ListResponse, error) { return nil, status.Error(codes.Unimplemented, "method List not implemented") } +func (UnimplementedInstallationServiceServer) ListHeld(context.Context, *ListHeldRequest) (*ListHeldResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListHeld not implemented") +} func (UnimplementedInstallationServiceServer) mustEmbedUnimplementedInstallationServiceServer() {} func (UnimplementedInstallationServiceServer) testEmbeddedByValue() {} @@ -353,6 +379,24 @@ func _InstallationService_List_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _InstallationService_ListHeld_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListHeldRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(InstallationServiceServer).ListHeld(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: InstallationService_ListHeld_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(InstallationServiceServer).ListHeld(ctx, req.(*ListHeldRequest)) + } + return interceptor(ctx, in, info, handler) +} + // InstallationService_ServiceDesc is the grpc.ServiceDesc for InstallationService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -388,6 +432,10 @@ var InstallationService_ServiceDesc = grpc.ServiceDesc{ MethodName: "List", Handler: _InstallationService_List_Handler, }, + { + MethodName: "ListHeld", + Handler: _InstallationService_ListHeld_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "host/installation/v1/installation.proto", diff --git a/module/services/accounts/code/pkg/gen/host/installation/v1/installationv1connect/installation.connect.go b/module/services/accounts/code/pkg/gen/host/installation/v1/installationv1connect/installation.connect.go index f5cb5ade7..42d72ee8e 100644 --- a/module/services/accounts/code/pkg/gen/host/installation/v1/installationv1connect/installation.connect.go +++ b/module/services/accounts/code/pkg/gen/host/installation/v1/installationv1connect/installation.connect.go @@ -64,6 +64,9 @@ const ( // InstallationServiceListProcedure is the fully-qualified name of the InstallationService's List // RPC. InstallationServiceListProcedure = "/host.installation.v1.InstallationService/List" + // InstallationServiceListHeldProcedure is the fully-qualified name of the InstallationService's + // ListHeld RPC. + InstallationServiceListHeldProcedure = "/host.installation.v1.InstallationService/ListHeld" // InstallableServiceInstallProcedure is the fully-qualified name of the InstallableService's // Install RPC. InstallableServiceInstallProcedure = "/host.installation.v1.InstallableService/Install" @@ -96,10 +99,16 @@ type InstallationServiceClient interface { // ListRoleGrants answers who holds which role on the installation; the same // callers as GrantRole. ListRoleGrants(context.Context, *connect.Request[v1.ListRoleGrantsRequest]) (*connect.Response[v1.ListRoleGrantsResponse], error) - // List answers what one organisation has installed: an administrator's read. A - // member's view, which installed solutions they may use, is their navigation and - // belongs to the entitlement read, not here. + // List answers what one organisation has installed: an administrator's read. + // A member's view, which installed solutions they may use, is ListHeld. List(context.Context, *connect.Request[v1.ListRequest]) (*connect.Response[v1.ListResponse], error) + // ListHeld answers the signed-in person which installations of their + // organisation they may use: each active installation on which they hold a + // role, granted to them or to a team of theirs, with the roles they hold. + // It is their navigation and answers their own holdings alone, never who + // else holds what. A member holding nothing reads an empty list; a caller + // acting at another organisation is refused naming it. + ListHeld(context.Context, *connect.Request[v1.ListHeldRequest]) (*connect.Response[v1.ListHeldResponse], error) } // NewInstallationServiceClient constructs a client for the host.installation.v1.InstallationService @@ -155,6 +164,12 @@ func NewInstallationServiceClient(httpClient connect.HTTPClient, baseURL string, connect.WithSchema(installationServiceMethods.ByName("List")), connect.WithClientOptions(opts...), ), + listHeld: connect.NewClient[v1.ListHeldRequest, v1.ListHeldResponse]( + httpClient, + baseURL+InstallationServiceListHeldProcedure, + connect.WithSchema(installationServiceMethods.ByName("ListHeld")), + connect.WithClientOptions(opts...), + ), } } @@ -167,6 +182,7 @@ type installationServiceClient struct { revokeRole *connect.Client[v1.RevokeRoleRequest, v1.Installation] listRoleGrants *connect.Client[v1.ListRoleGrantsRequest, v1.ListRoleGrantsResponse] list *connect.Client[v1.ListRequest, v1.ListResponse] + listHeld *connect.Client[v1.ListHeldRequest, v1.ListHeldResponse] } // Current calls host.installation.v1.InstallationService.Current. @@ -204,6 +220,11 @@ func (c *installationServiceClient) List(ctx context.Context, req *connect.Reque return c.list.CallUnary(ctx, req) } +// ListHeld calls host.installation.v1.InstallationService.ListHeld. +func (c *installationServiceClient) ListHeld(ctx context.Context, req *connect.Request[v1.ListHeldRequest]) (*connect.Response[v1.ListHeldResponse], error) { + return c.listHeld.CallUnary(ctx, req) +} + // InstallationServiceHandler is an implementation of the host.installation.v1.InstallationService // service. type InstallationServiceHandler interface { @@ -227,10 +248,16 @@ type InstallationServiceHandler interface { // ListRoleGrants answers who holds which role on the installation; the same // callers as GrantRole. ListRoleGrants(context.Context, *connect.Request[v1.ListRoleGrantsRequest]) (*connect.Response[v1.ListRoleGrantsResponse], error) - // List answers what one organisation has installed: an administrator's read. A - // member's view, which installed solutions they may use, is their navigation and - // belongs to the entitlement read, not here. + // List answers what one organisation has installed: an administrator's read. + // A member's view, which installed solutions they may use, is ListHeld. List(context.Context, *connect.Request[v1.ListRequest]) (*connect.Response[v1.ListResponse], error) + // ListHeld answers the signed-in person which installations of their + // organisation they may use: each active installation on which they hold a + // role, granted to them or to a team of theirs, with the roles they hold. + // It is their navigation and answers their own holdings alone, never who + // else holds what. A member holding nothing reads an empty list; a caller + // acting at another organisation is refused naming it. + ListHeld(context.Context, *connect.Request[v1.ListHeldRequest]) (*connect.Response[v1.ListHeldResponse], error) } // NewInstallationServiceHandler builds an HTTP handler from the service implementation. It returns @@ -282,6 +309,12 @@ func NewInstallationServiceHandler(svc InstallationServiceHandler, opts ...conne connect.WithSchema(installationServiceMethods.ByName("List")), connect.WithHandlerOptions(opts...), ) + installationServiceListHeldHandler := connect.NewUnaryHandler( + InstallationServiceListHeldProcedure, + svc.ListHeld, + connect.WithSchema(installationServiceMethods.ByName("ListHeld")), + connect.WithHandlerOptions(opts...), + ) return "/host.installation.v1.InstallationService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case InstallationServiceCurrentProcedure: @@ -298,6 +331,8 @@ func NewInstallationServiceHandler(svc InstallationServiceHandler, opts ...conne installationServiceListRoleGrantsHandler.ServeHTTP(w, r) case InstallationServiceListProcedure: installationServiceListHandler.ServeHTTP(w, r) + case InstallationServiceListHeldProcedure: + installationServiceListHeldHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -335,6 +370,10 @@ func (UnimplementedInstallationServiceHandler) List(context.Context, *connect.Re return nil, connect.NewError(connect.CodeUnimplemented, errors.New("host.installation.v1.InstallationService.List is not implemented")) } +func (UnimplementedInstallationServiceHandler) ListHeld(context.Context, *connect.Request[v1.ListHeldRequest]) (*connect.Response[v1.ListHeldResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("host.installation.v1.InstallationService.ListHeld is not implemented")) +} + // InstallableServiceClient is a client for the host.installation.v1.InstallableService service. type InstallableServiceClient interface { // Install sets up the solution for one installation; it must converge. diff --git a/module/services/accounts/generated/api-contract-surfaces.json b/module/services/accounts/generated/api-contract-surfaces.json index 1a8a43947..1b314db0b 100644 --- a/module/services/accounts/generated/api-contract-surfaces.json +++ b/module/services/accounts/generated/api-contract-surfaces.json @@ -89,6 +89,7 @@ "/host.installation.v1.InstallationService/GrantRole", "/host.installation.v1.InstallationService/Install", "/host.installation.v1.InstallationService/List", + "/host.installation.v1.InstallationService/ListHeld", "/host.installation.v1.InstallationService/ListRoleGrants", "/host.installation.v1.InstallationService/Review", "/host.installation.v1.InstallationService/RevokeRole" diff --git a/module/services/accounts/generated/authz-methods.json b/module/services/accounts/generated/authz-methods.json index a38a0ce7a..3402ccfc4 100644 --- a/module/services/accounts/generated/authz-methods.json +++ b/module/services/accounts/generated/authz-methods.json @@ -732,6 +732,43 @@ "policy_sha256": "b387739192840b24f771349cb2cded72d5f7eabd9d56c433b5b5937e6f4e58f0", "rate_limit_backend_failure_mode": "RATE_LIMIT_BACKEND_FAILURE_MODE_FAIL_OPEN" }, + { + "procedure": "/host.installation.v1.InstallationService/ListHeld", + "full_service": "host.installation.v1.InstallationService", + "method": "ListHeld", + "input_type": "host.installation.v1.ListHeldRequest", + "output_type": "host.installation.v1.ListHeldResponse", + "client_streaming": false, + "server_streaming": false, + "policy": { + "exposure": "EXPOSURE_AUTHENTICATED", + "tenant": "TENANT_REQUIREMENT_NONE", + "permissions": [], + "scopes": [], + "resource_bindings": [], + "mfa": "MFA_REQUIREMENT_NONE", + "audit": { + "events": [], + "emission": "AUDIT_EMISSION_NONE" + }, + "idempotency": "IDEMPOTENCY_REQUIREMENT_OPTIONAL", + "rate_limit": "RATE_LIMIT_CLASS_STANDARD_READ", + "request_sensitivity": "SENSITIVITY_CONFIDENTIAL", + "response_sensitivity": "SENSITIVITY_CONFIDENTIAL", + "platform_role": "PLATFORM_ROLE_REQUIREMENT_NONE", + "authentication_factor_attempt": false, + "conditions": [], + "impersonation": "IMPERSONATION_REQUIREMENT_FORBIDDEN" + }, + "policy_tier": "auth", + "owner": { + "module": "saas-starter", + "service": "accounts" + }, + "source_proto": "host/installation/v1/installation.proto", + "policy_sha256": "79921072468c5f99274839371a7ac834d857df6e5f0708942165c4b31084de26", + "rate_limit_backend_failure_mode": "RATE_LIMIT_BACKEND_FAILURE_MODE_FAIL_OPEN" + }, { "procedure": "/host.installation.v1.InstallationService/ListRoleGrants", "full_service": "host.installation.v1.InstallationService", diff --git a/module/services/accounts/generated/gateway-routes.json b/module/services/accounts/generated/gateway-routes.json index fd0e81309..893a49485 100644 --- a/module/services/accounts/generated/gateway-routes.json +++ b/module/services/accounts/generated/gateway-routes.json @@ -3713,6 +3713,22 @@ "rewrite_path": "", "remove_after": "" }, + { + "protocol": "GATEWAY_PROTOCOL_CONNECT", + "method": "POST", + "path": "/host.installation.v1.InstallationService/ListHeld", + "match": "GATEWAY_MATCH_EXACT", + "procedure": "/host.installation.v1.InstallationService/ListHeld", + "owner": { + "module": "saas-starter", + "service": "accounts" + }, + "upstream_endpoint": "connect", + "exposure": "EXPOSURE_AUTHENTICATED", + "source": "GATEWAY_ROUTE_SOURCE_DESCRIPTOR", + "rewrite_path": "", + "remove_after": "" + }, { "protocol": "GATEWAY_PROTOCOL_CONNECT", "method": "POST", diff --git a/module/services/accounts/generated/service-catalog.json b/module/services/accounts/generated/service-catalog.json index 69d39e2b5..be670e1ab 100644 --- a/module/services/accounts/generated/service-catalog.json +++ b/module/services/accounts/generated/service-catalog.json @@ -48,6 +48,7 @@ "/host.installation.v1.InstallationService/GrantRole", "/host.installation.v1.InstallationService/Install", "/host.installation.v1.InstallationService/List", + "/host.installation.v1.InstallationService/ListHeld", "/host.installation.v1.InstallationService/ListRoleGrants", "/host.installation.v1.InstallationService/Review", "/host.installation.v1.InstallationService/RevokeRole" @@ -1411,6 +1412,49 @@ "service": "accounts" } }, + { + "procedure": "/host.installation.v1.InstallationService/ListHeld", + "service": "InstallationService", + "full_service": "host.installation.v1.InstallationService", + "method": "ListHeld", + "input_type": "host.installation.v1.ListHeldRequest", + "output_type": "host.installation.v1.ListHeldResponse", + "client_streaming": false, + "server_streaming": false, + "deprecated": false, + "source_proto": "host/installation/v1/installation.proto", + "protocols": [ + "PROTOCOL_GRPC", + "PROTOCOL_CONNECT" + ], + "http_bindings": [], + "policy": { + "exposure": "EXPOSURE_AUTHENTICATED", + "tenant": "TENANT_REQUIREMENT_NONE", + "permissions": [], + "scopes": [], + "resource_bindings": [], + "mfa": "MFA_REQUIREMENT_NONE", + "audit": { + "events": [], + "emission": "AUDIT_EMISSION_NONE" + }, + "idempotency": "IDEMPOTENCY_REQUIREMENT_OPTIONAL", + "rate_limit": "RATE_LIMIT_CLASS_STANDARD_READ", + "request_sensitivity": "SENSITIVITY_CONFIDENTIAL", + "response_sensitivity": "SENSITIVITY_CONFIDENTIAL", + "platform_role": "PLATFORM_ROLE_REQUIREMENT_NONE", + "authentication_factor_attempt": false, + "conditions": [], + "impersonation": "IMPERSONATION_REQUIREMENT_FORBIDDEN" + }, + "policy_tier": "auth", + "description": "List only the signed-in member's current installation roles, including team grants.", + "owner": { + "module": "saas-starter", + "service": "accounts" + } + }, { "procedure": "/host.installation.v1.InstallationService/ListRoleGrants", "service": "InstallationService", diff --git a/module/services/accounts/proto/host/DIGESTS.sha256 b/module/services/accounts/proto/host/DIGESTS.sha256 index 1d31bafed..4fcaadff4 100644 --- a/module/services/accounts/proto/host/DIGESTS.sha256 +++ b/module/services/accounts/proto/host/DIGESTS.sha256 @@ -2,5 +2,5 @@ 5f743acb2e21e23eb916092313f54b0737742fe73c77d3f43208987a4aabf4c4 authority/v1/authority.proto f1ea4a9113527f8ed972660c3d2134a39135e96cf212b780ec04d14321ddcab1 authority/v1/work_context.proto 65353da82976f3f00d0a87792b93a723adbfc9deec48a5f7e337ff9526be53f5 composition/v1/composition.proto -d69c70528987ef59180b0ac683ac3c30de9b18077dacc3eece1342465953314e installation/v1/installation.proto +5de7bd8b5f68d5bdf081dab59352473a04c13c6bb5cecae0ec32beb6f5a45305 installation/v1/installation.proto 5d1e2f8fb2e7c3a7b8e027d8de23e26e53fa21212831e23fac492372ac53f28b session/v1/session.proto diff --git a/module/services/accounts/proto/host/installation/v1/installation.proto b/module/services/accounts/proto/host/installation/v1/installation.proto index c1ebefd92..f3f3f1397 100644 --- a/module/services/accounts/proto/host/installation/v1/installation.proto +++ b/module/services/accounts/proto/host/installation/v1/installation.proto @@ -30,10 +30,16 @@ service InstallationService { // ListRoleGrants answers who holds which role on the installation; the same // callers as GrantRole. rpc ListRoleGrants(ListRoleGrantsRequest) returns (ListRoleGrantsResponse); - // List answers what one organisation has installed: an administrator's read. A - // member's view, which installed solutions they may use, is their navigation and - // belongs to the entitlement read, not here. + // List answers what one organisation has installed: an administrator's read. + // A member's view, which installed solutions they may use, is ListHeld. rpc List(ListRequest) returns (ListResponse); + // ListHeld answers the signed-in person which installations of their + // organisation they may use: each active installation on which they hold a + // role, granted to them or to a team of theirs, with the roles they hold. + // It is their navigation and answers their own holdings alone, never who + // else holds what. A member holding nothing reads an empty list; a caller + // acting at another organisation is refused naming it. + rpc ListHeld(ListHeldRequest) returns (ListHeldResponse); } message CurrentInstallationRequest { @@ -309,3 +315,25 @@ message OfferTool { // True when the tool changes data. bool write = 3; } + +// ListHeldRequest names the organisation the person reads at: the one their +// session was opened at. +message ListHeldRequest { + // The organisation. + string organization_id = 1; +} + +// ListHeldResponse is every installation the person holds a role on, sorted +// by installation id. +message ListHeldResponse { + // The installations. + repeated HeldInstallation installations = 1; +} + +// HeldInstallation is one installation the person may use. +message HeldInstallation { + // The installation. + Installation installation = 1; + // The roles the person holds on it, directly or through a team, sorted. + repeated string roles = 2; +} diff --git a/module/services/auth-gateway/code/authz_catalog_gen.go b/module/services/auth-gateway/code/authz_catalog_gen.go index fb059db66..3e99e5a59 100644 --- a/module/services/auth-gateway/code/authz_catalog_gen.go +++ b/module/services/auth-gateway/code/authz_catalog_gen.go @@ -30,6 +30,7 @@ var generatedAuthorizationByProcedure = map[string]generatedAuthorizationMetadat "/host.installation.v1.InstallationService/GrantRole": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassStandardWrite, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "fc01eebe90648f5e096a7d4bbbd8f2aeb3d755bbd808c962352ed78936b12a4a"}, "/host.installation.v1.InstallationService/Install": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassStandardWrite, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "9848918817951cb839354fccd74257a45731b1ff0ae86aaa3af89dadc0f32a1b"}, "/host.installation.v1.InstallationService/List": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassStandardRead, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "b387739192840b24f771349cb2cded72d5f7eabd9d56c433b5b5937e6f4e58f0"}, + "/host.installation.v1.InstallationService/ListHeld": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassStandardRead, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "79921072468c5f99274839371a7ac834d857df6e5f0708942165c4b31084de26"}, "/host.installation.v1.InstallationService/ListRoleGrants": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassStandardRead, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "79921072468c5f99274839371a7ac834d857df6e5f0708942165c4b31084de26"}, "/host.installation.v1.InstallationService/Review": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassStandardRead, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "b387739192840b24f771349cb2cded72d5f7eabd9d56c433b5b5937e6f4e58f0"}, "/host.installation.v1.InstallationService/RevokeRole": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassStandardWrite, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "912f1c45666c1a2c3089918be4723973ca4fadf2dc78e5e80253d93499e74c92"}, diff --git a/module/services/auth-gateway/code/routing_catalog_gen.go b/module/services/auth-gateway/code/routing_catalog_gen.go index 0fb854439..ff4ecdfe4 100644 --- a/module/services/auth-gateway/code/routing_catalog_gen.go +++ b/module/services/auth-gateway/code/routing_catalog_gen.go @@ -238,6 +238,7 @@ func generatedCatalogConnectRoutes() []*RouteEntry { {Service: "accounts_connect", Method: "POST", Path: "/host.installation.v1.InstallationService/GrantRole", UpstreamPath: "", Procedure: "/host.installation.v1.InstallationService/GrantRole"}, {Service: "accounts_connect", Method: "POST", Path: "/host.installation.v1.InstallationService/Install", UpstreamPath: "", Procedure: "/host.installation.v1.InstallationService/Install"}, {Service: "accounts_connect", Method: "POST", Path: "/host.installation.v1.InstallationService/List", UpstreamPath: "", Procedure: "/host.installation.v1.InstallationService/List"}, + {Service: "accounts_connect", Method: "POST", Path: "/host.installation.v1.InstallationService/ListHeld", UpstreamPath: "", Procedure: "/host.installation.v1.InstallationService/ListHeld"}, {Service: "accounts_connect", Method: "POST", Path: "/host.installation.v1.InstallationService/ListRoleGrants", UpstreamPath: "", Procedure: "/host.installation.v1.InstallationService/ListRoleGrants"}, {Service: "accounts_connect", Method: "POST", Path: "/host.installation.v1.InstallationService/Review", UpstreamPath: "", Procedure: "/host.installation.v1.InstallationService/Review"}, {Service: "accounts_connect", Method: "POST", Path: "/host.installation.v1.InstallationService/RevokeRole", UpstreamPath: "", Procedure: "/host.installation.v1.InstallationService/RevokeRole"}, 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 49cbfc2cb..238b0e906 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:12c6216596c7460c71bf83e3962a3724c19bd79401459088a51f7f3b3fc1e3d9 + contract-digest: sha256:a6dfc46790b2193c93c7952f1f6e57a0825c223fed428bff2adc10c59fa03221 services: - AccessibleScopeService - AuditService diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/host/installation/v1/installation_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/host/installation/v1/installation_pb.ts index 3ef395d61..7349ddf76 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/host/installation/v1/installation_pb.ts +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/host/installation/v1/installation_pb.ts @@ -16,7 +16,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file host/installation/v1/installation.proto. */ export const file_host_installation_v1_installation: GenFile = /*@__PURE__*/ - fileDesc("Cidob3N0L2luc3RhbGxhdGlvbi92MS9pbnN0YWxsYXRpb24ucHJvdG8SFGhvc3QuaW5zdGFsbGF0aW9uLnYxIkwKGkN1cnJlbnRJbnN0YWxsYXRpb25SZXF1ZXN0EhQKDHBhcmVudF90b2tlbhgBIAEoCRIYChBhY3Rvcl9jcmVkZW50aWFsGAIgASgJIm8KE0N1cnJlbnRJbnN0YWxsYXRpb24SCgoCaWQYASABKAkSFwoPb3JnYW5pemF0aW9uX2lkGAIgASgJEg4KBnRhcmdldBgDIAEoCRIQCghyZXZpc2lvbhgEIAEoBBIRCgluYW1lc3BhY2UYBSABKAkimQEKDkluc3RhbGxSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIOCgZ0YXJnZXQYAiABKAkSDAoEcm9sZRgDIAEoCRIXCg9vd25lcl9wZXJzb25faWQYBCABKAkSNwoIYXBwcm92YWwYBSABKAsyJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5Db25zZW50QXBwcm92YWwiOAoNUmV2aWV3UmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkSDgoGdGFyZ2V0GAIgASgJInkKD0NvbnNlbnRBcHByb3ZhbBIRCgl0YXJnZXRfaWQYASABKAkSEgoKZ2VuZXJhdGlvbhgCIAEoBBIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoBBIUCgxvcGVyYXRpb25faWQYBCABKAkSDgoGZGlnZXN0GAUgASgJInYKEEdyYW50Um9sZVJlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhcKD2luc3RhbGxhdGlvbl9pZBgCIAEoCRIMCgRyb2xlGAMgASgJEhEKCXBlcnNvbl9pZBgEIAEoCRIPCgd0ZWFtX2lkGAUgASgJIncKEVJldm9rZVJvbGVSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIXCg9pbnN0YWxsYXRpb25faWQYAiABKAkSDAoEcm9sZRgDIAEoCRIRCglwZXJzb25faWQYBCABKAkSDwoHdGVhbV9pZBgFIAEoCSJJChVMaXN0Um9sZUdyYW50c1JlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhcKD2luc3RhbGxhdGlvbl9pZBgCIAEoCSJJChZMaXN0Um9sZUdyYW50c1Jlc3BvbnNlEi8KBmdyYW50cxgBIAMoCzIfLmhvc3QuaW5zdGFsbGF0aW9uLnYxLlJvbGVHcmFudCI9CglSb2xlR3JhbnQSDAoEcm9sZRgBIAEoCRIRCglwZXJzb25faWQYAiABKAkSDwoHdGVhbV9pZBgDIAEoCSImCgtMaXN0UmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkiSQoMTGlzdFJlc3BvbnNlEjkKDWluc3RhbGxhdGlvbnMYASADKAsyIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24i3QIKDEluc3RhbGxhdGlvbhIKCgJpZBgBIAEoCRIXCg9vcmdhbml6YXRpb25faWQYAiABKAkSDgoGdGFyZ2V0GAMgASgJEjkKBnN0YXR1cxgEIAEoDjIpLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbi5TdGF0dXMSEAoIcmV2aXNpb24YBSABKAQSDAoEcm9sZRgGIAEoCRIRCgluYW1lc3BhY2UYCSABKAkSFAoMcHJpbmNpcGFsX2lkGAogASgJEg0KBXJvbGVzGAsgAygJIlsKBlN0YXR1cxIWChJTVEFUVVNfVU5TUEVDSUZJRUQQABISCg5TVEFUVVNfUEVORElORxABEhEKDVNUQVRVU19BQ1RJVkUQAhISCg5TVEFUVVNfUkVWT0tFRBADSgQIBxAISgQICBAJUgh0ZWFtX2lkc1ISYWdlbnRfcHJpbmNpcGFsX2lkIk4KEkluc3RhbGxhdGlvbk5vdGljZRI4CgxpbnN0YWxsYXRpb24YASABKAsyIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24iEQoPSW5zdGFsbGF0aW9uQWNrIiwKEUxpc3RPZmZlcnNSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCSJBChJMaXN0T2ZmZXJzUmVzcG9uc2USKwoGb2ZmZXJzGAEgAygLMhsuaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXIiKwoFT2ZmZXISDgoGdGFyZ2V0GAEgASgJEhIKCmdlbmVyYXRpb24YAiABKAQijwIKDVJldmlld2VkT2ZmZXISNwoIYXBwcm92YWwYASABKAsyJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5Db25zZW50QXBwcm92YWwSDgoGdGFyZ2V0GAIgASgJEi4KBXJvbGVzGAMgAygLMh8uaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXJSb2xlEjgKDGluc3RhbGxhdGlvbhgEIAEoCzIiLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbhIzCgdjZWlsaW5nGAUgAygLMiIuaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXJCaW5kaW5nEhYKDnRvb2xzX2NvbXBvc2VkGAYgASgIImEKDE9mZmVyQmluZGluZxIPCgdiaW5kaW5nGAEgASgJEg4KBm1vZHVsZRgCIAEoCRIwCgZzY29wZXMYAyADKAsyIC5ob3N0Lmluc3RhbGxhdGlvbi52MS5PZmZlclNjb3BlIogBCglPZmZlclJvbGUSDAoEbmFtZRgBIAEoCRIwCgZzY29wZXMYAiADKAsyIC5ob3N0Lmluc3RhbGxhdGlvbi52MS5PZmZlclNjb3BlEjsKDGNhcGFiaWxpdGllcxgDIAMoCzIlLmhvc3QuaW5zdGFsbGF0aW9uLnYxLk9mZmVyQ2FwYWJpbGl0eSJhCgpPZmZlclNjb3BlEhUKDXJlc291cmNlX2tpbmQYASABKAkSDwoHYWN0aW9ucxgCIAMoCRIVCg1hdHRhY2hlZF9vbmx5GAMgASgIEhQKDHJlc291cmNlX2lkcxgEIAMoCSKYAQoPT2ZmZXJDYXBhYmlsaXR5Eg4KBm1vZHVsZRgBIAEoCRIRCglvcGVyYXRpb24YAiABKAkSMgoIcmVxdWlyZXMYAyADKAsyIC5ob3N0Lmluc3RhbGxhdGlvbi52MS5PZmZlclNjb3BlEi4KBXRvb2xzGAQgAygLMh8uaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXJUb29sIjkKCU9mZmVyVG9vbBIMCgRuYW1lGAEgASgJEg8KB2JpbmRpbmcYAiABKAkSDQoFd3JpdGUYAyABKAgymAUKE0luc3RhbGxhdGlvblNlcnZpY2USZgoHQ3VycmVudBIwLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkN1cnJlbnRJbnN0YWxsYXRpb25SZXF1ZXN0GikuaG9zdC5pbnN0YWxsYXRpb24udjEuQ3VycmVudEluc3RhbGxhdGlvbhJTCgdJbnN0YWxsEiQuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbFJlcXVlc3QaIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24SVAoGUmV2aWV3EiMuaG9zdC5pbnN0YWxsYXRpb24udjEuUmV2aWV3UmVxdWVzdBolLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkNvbnNlbnRBcHByb3ZhbBJXCglHcmFudFJvbGUSJi5ob3N0Lmluc3RhbGxhdGlvbi52MS5HcmFudFJvbGVSZXF1ZXN0GiIuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uElkKClJldm9rZVJvbGUSJy5ob3N0Lmluc3RhbGxhdGlvbi52MS5SZXZva2VSb2xlUmVxdWVzdBoiLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbhJrCg5MaXN0Um9sZUdyYW50cxIrLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RSb2xlR3JhbnRzUmVxdWVzdBosLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RSb2xlR3JhbnRzUmVzcG9uc2USTQoETGlzdBIhLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RSZXF1ZXN0GiIuaG9zdC5pbnN0YWxsYXRpb24udjEuTGlzdFJlc3BvbnNlMnAKEkluc3RhbGxhYmxlU2VydmljZRJaCgdJbnN0YWxsEiguaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uTm90aWNlGiUuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uQWNrMsgBCgxPZmZlclNlcnZpY2USXwoKTGlzdE9mZmVycxInLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RPZmZlcnNSZXF1ZXN0GiguaG9zdC5pbnN0YWxsYXRpb24udjEuTGlzdE9mZmVyc1Jlc3BvbnNlElcKC1Jldmlld09mZmVyEiMuaG9zdC5pbnN0YWxsYXRpb24udjEuUmV2aWV3UmVxdWVzdBojLmhvc3QuaW5zdGFsbGF0aW9uLnYxLlJldmlld2VkT2ZmZXJCNVozZ2F0ZXdheS9wa2cvZ2VuL2hvc3QvaW5zdGFsbGF0aW9uL3YxO2luc3RhbGxhdGlvbnYxYgZwcm90bzM"); + fileDesc("Cidob3N0L2luc3RhbGxhdGlvbi92MS9pbnN0YWxsYXRpb24ucHJvdG8SFGhvc3QuaW5zdGFsbGF0aW9uLnYxIkwKGkN1cnJlbnRJbnN0YWxsYXRpb25SZXF1ZXN0EhQKDHBhcmVudF90b2tlbhgBIAEoCRIYChBhY3Rvcl9jcmVkZW50aWFsGAIgASgJIm8KE0N1cnJlbnRJbnN0YWxsYXRpb24SCgoCaWQYASABKAkSFwoPb3JnYW5pemF0aW9uX2lkGAIgASgJEg4KBnRhcmdldBgDIAEoCRIQCghyZXZpc2lvbhgEIAEoBBIRCgluYW1lc3BhY2UYBSABKAkimQEKDkluc3RhbGxSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIOCgZ0YXJnZXQYAiABKAkSDAoEcm9sZRgDIAEoCRIXCg9vd25lcl9wZXJzb25faWQYBCABKAkSNwoIYXBwcm92YWwYBSABKAsyJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5Db25zZW50QXBwcm92YWwiOAoNUmV2aWV3UmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkSDgoGdGFyZ2V0GAIgASgJInkKD0NvbnNlbnRBcHByb3ZhbBIRCgl0YXJnZXRfaWQYASABKAkSEgoKZ2VuZXJhdGlvbhgCIAEoBBIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoBBIUCgxvcGVyYXRpb25faWQYBCABKAkSDgoGZGlnZXN0GAUgASgJInYKEEdyYW50Um9sZVJlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhcKD2luc3RhbGxhdGlvbl9pZBgCIAEoCRIMCgRyb2xlGAMgASgJEhEKCXBlcnNvbl9pZBgEIAEoCRIPCgd0ZWFtX2lkGAUgASgJIncKEVJldm9rZVJvbGVSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIXCg9pbnN0YWxsYXRpb25faWQYAiABKAkSDAoEcm9sZRgDIAEoCRIRCglwZXJzb25faWQYBCABKAkSDwoHdGVhbV9pZBgFIAEoCSJJChVMaXN0Um9sZUdyYW50c1JlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhcKD2luc3RhbGxhdGlvbl9pZBgCIAEoCSJJChZMaXN0Um9sZUdyYW50c1Jlc3BvbnNlEi8KBmdyYW50cxgBIAMoCzIfLmhvc3QuaW5zdGFsbGF0aW9uLnYxLlJvbGVHcmFudCI9CglSb2xlR3JhbnQSDAoEcm9sZRgBIAEoCRIRCglwZXJzb25faWQYAiABKAkSDwoHdGVhbV9pZBgDIAEoCSImCgtMaXN0UmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkiSQoMTGlzdFJlc3BvbnNlEjkKDWluc3RhbGxhdGlvbnMYASADKAsyIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24i3QIKDEluc3RhbGxhdGlvbhIKCgJpZBgBIAEoCRIXCg9vcmdhbml6YXRpb25faWQYAiABKAkSDgoGdGFyZ2V0GAMgASgJEjkKBnN0YXR1cxgEIAEoDjIpLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbi5TdGF0dXMSEAoIcmV2aXNpb24YBSABKAQSDAoEcm9sZRgGIAEoCRIRCgluYW1lc3BhY2UYCSABKAkSFAoMcHJpbmNpcGFsX2lkGAogASgJEg0KBXJvbGVzGAsgAygJIlsKBlN0YXR1cxIWChJTVEFUVVNfVU5TUEVDSUZJRUQQABISCg5TVEFUVVNfUEVORElORxABEhEKDVNUQVRVU19BQ1RJVkUQAhISCg5TVEFUVVNfUkVWT0tFRBADSgQIBxAISgQICBAJUgh0ZWFtX2lkc1ISYWdlbnRfcHJpbmNpcGFsX2lkIk4KEkluc3RhbGxhdGlvbk5vdGljZRI4CgxpbnN0YWxsYXRpb24YASABKAsyIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24iEQoPSW5zdGFsbGF0aW9uQWNrIiwKEUxpc3RPZmZlcnNSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCSJBChJMaXN0T2ZmZXJzUmVzcG9uc2USKwoGb2ZmZXJzGAEgAygLMhsuaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXIiKwoFT2ZmZXISDgoGdGFyZ2V0GAEgASgJEhIKCmdlbmVyYXRpb24YAiABKAQijwIKDVJldmlld2VkT2ZmZXISNwoIYXBwcm92YWwYASABKAsyJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5Db25zZW50QXBwcm92YWwSDgoGdGFyZ2V0GAIgASgJEi4KBXJvbGVzGAMgAygLMh8uaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXJSb2xlEjgKDGluc3RhbGxhdGlvbhgEIAEoCzIiLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbhIzCgdjZWlsaW5nGAUgAygLMiIuaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXJCaW5kaW5nEhYKDnRvb2xzX2NvbXBvc2VkGAYgASgIImEKDE9mZmVyQmluZGluZxIPCgdiaW5kaW5nGAEgASgJEg4KBm1vZHVsZRgCIAEoCRIwCgZzY29wZXMYAyADKAsyIC5ob3N0Lmluc3RhbGxhdGlvbi52MS5PZmZlclNjb3BlIogBCglPZmZlclJvbGUSDAoEbmFtZRgBIAEoCRIwCgZzY29wZXMYAiADKAsyIC5ob3N0Lmluc3RhbGxhdGlvbi52MS5PZmZlclNjb3BlEjsKDGNhcGFiaWxpdGllcxgDIAMoCzIlLmhvc3QuaW5zdGFsbGF0aW9uLnYxLk9mZmVyQ2FwYWJpbGl0eSJhCgpPZmZlclNjb3BlEhUKDXJlc291cmNlX2tpbmQYASABKAkSDwoHYWN0aW9ucxgCIAMoCRIVCg1hdHRhY2hlZF9vbmx5GAMgASgIEhQKDHJlc291cmNlX2lkcxgEIAMoCSKYAQoPT2ZmZXJDYXBhYmlsaXR5Eg4KBm1vZHVsZRgBIAEoCRIRCglvcGVyYXRpb24YAiABKAkSMgoIcmVxdWlyZXMYAyADKAsyIC5ob3N0Lmluc3RhbGxhdGlvbi52MS5PZmZlclNjb3BlEi4KBXRvb2xzGAQgAygLMh8uaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXJUb29sIjkKCU9mZmVyVG9vbBIMCgRuYW1lGAEgASgJEg8KB2JpbmRpbmcYAiABKAkSDQoFd3JpdGUYAyABKAgiKgoPTGlzdEhlbGRSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCSJRChBMaXN0SGVsZFJlc3BvbnNlEj0KDWluc3RhbGxhdGlvbnMYASADKAsyJi5ob3N0Lmluc3RhbGxhdGlvbi52MS5IZWxkSW5zdGFsbGF0aW9uIlsKEEhlbGRJbnN0YWxsYXRpb24SOAoMaW5zdGFsbGF0aW9uGAEgASgLMiIuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uEg0KBXJvbGVzGAIgAygJMvMFChNJbnN0YWxsYXRpb25TZXJ2aWNlEmYKB0N1cnJlbnQSMC5ob3N0Lmluc3RhbGxhdGlvbi52MS5DdXJyZW50SW5zdGFsbGF0aW9uUmVxdWVzdBopLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkN1cnJlbnRJbnN0YWxsYXRpb24SUwoHSW5zdGFsbBIkLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxSZXF1ZXN0GiIuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uElQKBlJldmlldxIjLmhvc3QuaW5zdGFsbGF0aW9uLnYxLlJldmlld1JlcXVlc3QaJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5Db25zZW50QXBwcm92YWwSVwoJR3JhbnRSb2xlEiYuaG9zdC5pbnN0YWxsYXRpb24udjEuR3JhbnRSb2xlUmVxdWVzdBoiLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbhJZCgpSZXZva2VSb2xlEicuaG9zdC5pbnN0YWxsYXRpb24udjEuUmV2b2tlUm9sZVJlcXVlc3QaIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24SawoOTGlzdFJvbGVHcmFudHMSKy5ob3N0Lmluc3RhbGxhdGlvbi52MS5MaXN0Um9sZUdyYW50c1JlcXVlc3QaLC5ob3N0Lmluc3RhbGxhdGlvbi52MS5MaXN0Um9sZUdyYW50c1Jlc3BvbnNlEk0KBExpc3QSIS5ob3N0Lmluc3RhbGxhdGlvbi52MS5MaXN0UmVxdWVzdBoiLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RSZXNwb25zZRJZCghMaXN0SGVsZBIlLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RIZWxkUmVxdWVzdBomLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RIZWxkUmVzcG9uc2UycAoSSW5zdGFsbGFibGVTZXJ2aWNlEloKB0luc3RhbGwSKC5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb25Ob3RpY2UaJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb25BY2syyAEKDE9mZmVyU2VydmljZRJfCgpMaXN0T2ZmZXJzEicuaG9zdC5pbnN0YWxsYXRpb24udjEuTGlzdE9mZmVyc1JlcXVlc3QaKC5ob3N0Lmluc3RhbGxhdGlvbi52MS5MaXN0T2ZmZXJzUmVzcG9uc2USVwoLUmV2aWV3T2ZmZXISIy5ob3N0Lmluc3RhbGxhdGlvbi52MS5SZXZpZXdSZXF1ZXN0GiMuaG9zdC5pbnN0YWxsYXRpb24udjEuUmV2aWV3ZWRPZmZlckI1WjNnYXRld2F5L3BrZy9nZW4vaG9zdC9pbnN0YWxsYXRpb24vdjE7aW5zdGFsbGF0aW9udjFiBnByb3RvMw"); /** * @generated from message host.installation.v1.CurrentInstallationRequest @@ -884,6 +884,78 @@ export type OfferTool = Message<"host.installation.v1.OfferTool"> & { export const OfferToolSchema: GenMessage = /*@__PURE__*/ messageDesc(file_host_installation_v1_installation, 23); +/** + * ListHeldRequest names the organisation the person reads at: the one their + * session was opened at. + * + * @generated from message host.installation.v1.ListHeldRequest + */ +export type ListHeldRequest = Message<"host.installation.v1.ListHeldRequest"> & { + /** + * The organisation. + * + * @generated from field: string organization_id = 1; + */ + organizationId: string; +}; + +/** + * Describes the message host.installation.v1.ListHeldRequest. + * Use `create(ListHeldRequestSchema)` to create a new message. + */ +export const ListHeldRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_installation_v1_installation, 24); + +/** + * ListHeldResponse is every installation the person holds a role on, sorted + * by installation id. + * + * @generated from message host.installation.v1.ListHeldResponse + */ +export type ListHeldResponse = Message<"host.installation.v1.ListHeldResponse"> & { + /** + * The installations. + * + * @generated from field: repeated host.installation.v1.HeldInstallation installations = 1; + */ + installations: HeldInstallation[]; +}; + +/** + * Describes the message host.installation.v1.ListHeldResponse. + * Use `create(ListHeldResponseSchema)` to create a new message. + */ +export const ListHeldResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_installation_v1_installation, 25); + +/** + * HeldInstallation is one installation the person may use. + * + * @generated from message host.installation.v1.HeldInstallation + */ +export type HeldInstallation = Message<"host.installation.v1.HeldInstallation"> & { + /** + * The installation. + * + * @generated from field: host.installation.v1.Installation installation = 1; + */ + installation?: Installation; + + /** + * The roles the person holds on it, directly or through a team, sorted. + * + * @generated from field: repeated string roles = 2; + */ + roles: string[]; +}; + +/** + * Describes the message host.installation.v1.HeldInstallation. + * Use `create(HeldInstallationSchema)` to create a new message. + */ +export const HeldInstallationSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_installation_v1_installation, 26); + /** * InstallationService is implemented by the host and by the local authority. * @@ -959,9 +1031,8 @@ export const InstallationService: GenService<{ output: typeof ListRoleGrantsResponseSchema; }, /** - * List answers what one organisation has installed: an administrator's read. A - * member's view, which installed solutions they may use, is their navigation and - * belongs to the entitlement read, not here. + * List answers what one organisation has installed: an administrator's read. + * A member's view, which installed solutions they may use, is ListHeld. * * @generated from rpc host.installation.v1.InstallationService.List */ @@ -970,6 +1041,21 @@ export const InstallationService: GenService<{ input: typeof ListRequestSchema; output: typeof ListResponseSchema; }, + /** + * ListHeld answers the signed-in person which installations of their + * organisation they may use: each active installation on which they hold a + * role, granted to them or to a team of theirs, with the roles they hold. + * It is their navigation and answers their own holdings alone, never who + * else holds what. A member holding nothing reads an empty list; a caller + * acting at another organisation is refused naming it. + * + * @generated from rpc host.installation.v1.InstallationService.ListHeld + */ + listHeld: { + methodKind: "unary"; + input: typeof ListHeldRequestSchema; + output: typeof ListHeldResponseSchema; + }, }> = /*@__PURE__*/ serviceDesc(file_host_installation_v1_installation, 0); diff --git a/module/services/frontend/code/src/gen/host/installation/v1/installation_pb.ts b/module/services/frontend/code/src/gen/host/installation/v1/installation_pb.ts index f2604fb4c..16e21e465 100644 --- a/module/services/frontend/code/src/gen/host/installation/v1/installation_pb.ts +++ b/module/services/frontend/code/src/gen/host/installation/v1/installation_pb.ts @@ -16,7 +16,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file host/installation/v1/installation.proto. */ export const file_host_installation_v1_installation: GenFile = /*@__PURE__*/ - fileDesc("Cidob3N0L2luc3RhbGxhdGlvbi92MS9pbnN0YWxsYXRpb24ucHJvdG8SFGhvc3QuaW5zdGFsbGF0aW9uLnYxIkwKGkN1cnJlbnRJbnN0YWxsYXRpb25SZXF1ZXN0EhQKDHBhcmVudF90b2tlbhgBIAEoCRIYChBhY3Rvcl9jcmVkZW50aWFsGAIgASgJIm8KE0N1cnJlbnRJbnN0YWxsYXRpb24SCgoCaWQYASABKAkSFwoPb3JnYW5pemF0aW9uX2lkGAIgASgJEg4KBnRhcmdldBgDIAEoCRIQCghyZXZpc2lvbhgEIAEoBBIRCgluYW1lc3BhY2UYBSABKAkimQEKDkluc3RhbGxSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIOCgZ0YXJnZXQYAiABKAkSDAoEcm9sZRgDIAEoCRIXCg9vd25lcl9wZXJzb25faWQYBCABKAkSNwoIYXBwcm92YWwYBSABKAsyJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5Db25zZW50QXBwcm92YWwiOAoNUmV2aWV3UmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkSDgoGdGFyZ2V0GAIgASgJInkKD0NvbnNlbnRBcHByb3ZhbBIRCgl0YXJnZXRfaWQYASABKAkSEgoKZ2VuZXJhdGlvbhgCIAEoBBIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoBBIUCgxvcGVyYXRpb25faWQYBCABKAkSDgoGZGlnZXN0GAUgASgJInYKEEdyYW50Um9sZVJlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhcKD2luc3RhbGxhdGlvbl9pZBgCIAEoCRIMCgRyb2xlGAMgASgJEhEKCXBlcnNvbl9pZBgEIAEoCRIPCgd0ZWFtX2lkGAUgASgJIncKEVJldm9rZVJvbGVSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIXCg9pbnN0YWxsYXRpb25faWQYAiABKAkSDAoEcm9sZRgDIAEoCRIRCglwZXJzb25faWQYBCABKAkSDwoHdGVhbV9pZBgFIAEoCSJJChVMaXN0Um9sZUdyYW50c1JlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhcKD2luc3RhbGxhdGlvbl9pZBgCIAEoCSJJChZMaXN0Um9sZUdyYW50c1Jlc3BvbnNlEi8KBmdyYW50cxgBIAMoCzIfLmhvc3QuaW5zdGFsbGF0aW9uLnYxLlJvbGVHcmFudCI9CglSb2xlR3JhbnQSDAoEcm9sZRgBIAEoCRIRCglwZXJzb25faWQYAiABKAkSDwoHdGVhbV9pZBgDIAEoCSImCgtMaXN0UmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkiSQoMTGlzdFJlc3BvbnNlEjkKDWluc3RhbGxhdGlvbnMYASADKAsyIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24i3QIKDEluc3RhbGxhdGlvbhIKCgJpZBgBIAEoCRIXCg9vcmdhbml6YXRpb25faWQYAiABKAkSDgoGdGFyZ2V0GAMgASgJEjkKBnN0YXR1cxgEIAEoDjIpLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbi5TdGF0dXMSEAoIcmV2aXNpb24YBSABKAQSDAoEcm9sZRgGIAEoCRIRCgluYW1lc3BhY2UYCSABKAkSFAoMcHJpbmNpcGFsX2lkGAogASgJEg0KBXJvbGVzGAsgAygJIlsKBlN0YXR1cxIWChJTVEFUVVNfVU5TUEVDSUZJRUQQABISCg5TVEFUVVNfUEVORElORxABEhEKDVNUQVRVU19BQ1RJVkUQAhISCg5TVEFUVVNfUkVWT0tFRBADSgQIBxAISgQICBAJUgh0ZWFtX2lkc1ISYWdlbnRfcHJpbmNpcGFsX2lkIk4KEkluc3RhbGxhdGlvbk5vdGljZRI4CgxpbnN0YWxsYXRpb24YASABKAsyIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24iEQoPSW5zdGFsbGF0aW9uQWNrIiwKEUxpc3RPZmZlcnNSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCSJBChJMaXN0T2ZmZXJzUmVzcG9uc2USKwoGb2ZmZXJzGAEgAygLMhsuaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXIiKwoFT2ZmZXISDgoGdGFyZ2V0GAEgASgJEhIKCmdlbmVyYXRpb24YAiABKAQijwIKDVJldmlld2VkT2ZmZXISNwoIYXBwcm92YWwYASABKAsyJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5Db25zZW50QXBwcm92YWwSDgoGdGFyZ2V0GAIgASgJEi4KBXJvbGVzGAMgAygLMh8uaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXJSb2xlEjgKDGluc3RhbGxhdGlvbhgEIAEoCzIiLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbhIzCgdjZWlsaW5nGAUgAygLMiIuaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXJCaW5kaW5nEhYKDnRvb2xzX2NvbXBvc2VkGAYgASgIImEKDE9mZmVyQmluZGluZxIPCgdiaW5kaW5nGAEgASgJEg4KBm1vZHVsZRgCIAEoCRIwCgZzY29wZXMYAyADKAsyIC5ob3N0Lmluc3RhbGxhdGlvbi52MS5PZmZlclNjb3BlIogBCglPZmZlclJvbGUSDAoEbmFtZRgBIAEoCRIwCgZzY29wZXMYAiADKAsyIC5ob3N0Lmluc3RhbGxhdGlvbi52MS5PZmZlclNjb3BlEjsKDGNhcGFiaWxpdGllcxgDIAMoCzIlLmhvc3QuaW5zdGFsbGF0aW9uLnYxLk9mZmVyQ2FwYWJpbGl0eSJhCgpPZmZlclNjb3BlEhUKDXJlc291cmNlX2tpbmQYASABKAkSDwoHYWN0aW9ucxgCIAMoCRIVCg1hdHRhY2hlZF9vbmx5GAMgASgIEhQKDHJlc291cmNlX2lkcxgEIAMoCSKYAQoPT2ZmZXJDYXBhYmlsaXR5Eg4KBm1vZHVsZRgBIAEoCRIRCglvcGVyYXRpb24YAiABKAkSMgoIcmVxdWlyZXMYAyADKAsyIC5ob3N0Lmluc3RhbGxhdGlvbi52MS5PZmZlclNjb3BlEi4KBXRvb2xzGAQgAygLMh8uaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXJUb29sIjkKCU9mZmVyVG9vbBIMCgRuYW1lGAEgASgJEg8KB2JpbmRpbmcYAiABKAkSDQoFd3JpdGUYAyABKAgymAUKE0luc3RhbGxhdGlvblNlcnZpY2USZgoHQ3VycmVudBIwLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkN1cnJlbnRJbnN0YWxsYXRpb25SZXF1ZXN0GikuaG9zdC5pbnN0YWxsYXRpb24udjEuQ3VycmVudEluc3RhbGxhdGlvbhJTCgdJbnN0YWxsEiQuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbFJlcXVlc3QaIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24SVAoGUmV2aWV3EiMuaG9zdC5pbnN0YWxsYXRpb24udjEuUmV2aWV3UmVxdWVzdBolLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkNvbnNlbnRBcHByb3ZhbBJXCglHcmFudFJvbGUSJi5ob3N0Lmluc3RhbGxhdGlvbi52MS5HcmFudFJvbGVSZXF1ZXN0GiIuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uElkKClJldm9rZVJvbGUSJy5ob3N0Lmluc3RhbGxhdGlvbi52MS5SZXZva2VSb2xlUmVxdWVzdBoiLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbhJrCg5MaXN0Um9sZUdyYW50cxIrLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RSb2xlR3JhbnRzUmVxdWVzdBosLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RSb2xlR3JhbnRzUmVzcG9uc2USTQoETGlzdBIhLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RSZXF1ZXN0GiIuaG9zdC5pbnN0YWxsYXRpb24udjEuTGlzdFJlc3BvbnNlMnAKEkluc3RhbGxhYmxlU2VydmljZRJaCgdJbnN0YWxsEiguaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uTm90aWNlGiUuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uQWNrMsgBCgxPZmZlclNlcnZpY2USXwoKTGlzdE9mZmVycxInLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RPZmZlcnNSZXF1ZXN0GiguaG9zdC5pbnN0YWxsYXRpb24udjEuTGlzdE9mZmVyc1Jlc3BvbnNlElcKC1Jldmlld09mZmVyEiMuaG9zdC5pbnN0YWxsYXRpb24udjEuUmV2aWV3UmVxdWVzdBojLmhvc3QuaW5zdGFsbGF0aW9uLnYxLlJldmlld2VkT2ZmZXJC1QEKGGNvbS5ob3N0Lmluc3RhbGxhdGlvbi52MUIRSW5zdGFsbGF0aW9uUHJvdG9QAVo0YWNjb3VudHMvcGtnL2dlbi9ob3N0L2luc3RhbGxhdGlvbi92MTtpbnN0YWxsYXRpb252MaICA0hJWKoCFEhvc3QuSW5zdGFsbGF0aW9uLlYxygIUSG9zdFxJbnN0YWxsYXRpb25cVjHiAiBIb3N0XEluc3RhbGxhdGlvblxWMVxHUEJNZXRhZGF0YeoCFkhvc3Q6Okluc3RhbGxhdGlvbjo6VjFiBnByb3RvMw"); + fileDesc("Cidob3N0L2luc3RhbGxhdGlvbi92MS9pbnN0YWxsYXRpb24ucHJvdG8SFGhvc3QuaW5zdGFsbGF0aW9uLnYxIkwKGkN1cnJlbnRJbnN0YWxsYXRpb25SZXF1ZXN0EhQKDHBhcmVudF90b2tlbhgBIAEoCRIYChBhY3Rvcl9jcmVkZW50aWFsGAIgASgJIm8KE0N1cnJlbnRJbnN0YWxsYXRpb24SCgoCaWQYASABKAkSFwoPb3JnYW5pemF0aW9uX2lkGAIgASgJEg4KBnRhcmdldBgDIAEoCRIQCghyZXZpc2lvbhgEIAEoBBIRCgluYW1lc3BhY2UYBSABKAkimQEKDkluc3RhbGxSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIOCgZ0YXJnZXQYAiABKAkSDAoEcm9sZRgDIAEoCRIXCg9vd25lcl9wZXJzb25faWQYBCABKAkSNwoIYXBwcm92YWwYBSABKAsyJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5Db25zZW50QXBwcm92YWwiOAoNUmV2aWV3UmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkSDgoGdGFyZ2V0GAIgASgJInkKD0NvbnNlbnRBcHByb3ZhbBIRCgl0YXJnZXRfaWQYASABKAkSEgoKZ2VuZXJhdGlvbhgCIAEoBBIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoBBIUCgxvcGVyYXRpb25faWQYBCABKAkSDgoGZGlnZXN0GAUgASgJInYKEEdyYW50Um9sZVJlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhcKD2luc3RhbGxhdGlvbl9pZBgCIAEoCRIMCgRyb2xlGAMgASgJEhEKCXBlcnNvbl9pZBgEIAEoCRIPCgd0ZWFtX2lkGAUgASgJIncKEVJldm9rZVJvbGVSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIXCg9pbnN0YWxsYXRpb25faWQYAiABKAkSDAoEcm9sZRgDIAEoCRIRCglwZXJzb25faWQYBCABKAkSDwoHdGVhbV9pZBgFIAEoCSJJChVMaXN0Um9sZUdyYW50c1JlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhcKD2luc3RhbGxhdGlvbl9pZBgCIAEoCSJJChZMaXN0Um9sZUdyYW50c1Jlc3BvbnNlEi8KBmdyYW50cxgBIAMoCzIfLmhvc3QuaW5zdGFsbGF0aW9uLnYxLlJvbGVHcmFudCI9CglSb2xlR3JhbnQSDAoEcm9sZRgBIAEoCRIRCglwZXJzb25faWQYAiABKAkSDwoHdGVhbV9pZBgDIAEoCSImCgtMaXN0UmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkiSQoMTGlzdFJlc3BvbnNlEjkKDWluc3RhbGxhdGlvbnMYASADKAsyIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24i3QIKDEluc3RhbGxhdGlvbhIKCgJpZBgBIAEoCRIXCg9vcmdhbml6YXRpb25faWQYAiABKAkSDgoGdGFyZ2V0GAMgASgJEjkKBnN0YXR1cxgEIAEoDjIpLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbi5TdGF0dXMSEAoIcmV2aXNpb24YBSABKAQSDAoEcm9sZRgGIAEoCRIRCgluYW1lc3BhY2UYCSABKAkSFAoMcHJpbmNpcGFsX2lkGAogASgJEg0KBXJvbGVzGAsgAygJIlsKBlN0YXR1cxIWChJTVEFUVVNfVU5TUEVDSUZJRUQQABISCg5TVEFUVVNfUEVORElORxABEhEKDVNUQVRVU19BQ1RJVkUQAhISCg5TVEFUVVNfUkVWT0tFRBADSgQIBxAISgQICBAJUgh0ZWFtX2lkc1ISYWdlbnRfcHJpbmNpcGFsX2lkIk4KEkluc3RhbGxhdGlvbk5vdGljZRI4CgxpbnN0YWxsYXRpb24YASABKAsyIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24iEQoPSW5zdGFsbGF0aW9uQWNrIiwKEUxpc3RPZmZlcnNSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCSJBChJMaXN0T2ZmZXJzUmVzcG9uc2USKwoGb2ZmZXJzGAEgAygLMhsuaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXIiKwoFT2ZmZXISDgoGdGFyZ2V0GAEgASgJEhIKCmdlbmVyYXRpb24YAiABKAQijwIKDVJldmlld2VkT2ZmZXISNwoIYXBwcm92YWwYASABKAsyJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5Db25zZW50QXBwcm92YWwSDgoGdGFyZ2V0GAIgASgJEi4KBXJvbGVzGAMgAygLMh8uaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXJSb2xlEjgKDGluc3RhbGxhdGlvbhgEIAEoCzIiLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbhIzCgdjZWlsaW5nGAUgAygLMiIuaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXJCaW5kaW5nEhYKDnRvb2xzX2NvbXBvc2VkGAYgASgIImEKDE9mZmVyQmluZGluZxIPCgdiaW5kaW5nGAEgASgJEg4KBm1vZHVsZRgCIAEoCRIwCgZzY29wZXMYAyADKAsyIC5ob3N0Lmluc3RhbGxhdGlvbi52MS5PZmZlclNjb3BlIogBCglPZmZlclJvbGUSDAoEbmFtZRgBIAEoCRIwCgZzY29wZXMYAiADKAsyIC5ob3N0Lmluc3RhbGxhdGlvbi52MS5PZmZlclNjb3BlEjsKDGNhcGFiaWxpdGllcxgDIAMoCzIlLmhvc3QuaW5zdGFsbGF0aW9uLnYxLk9mZmVyQ2FwYWJpbGl0eSJhCgpPZmZlclNjb3BlEhUKDXJlc291cmNlX2tpbmQYASABKAkSDwoHYWN0aW9ucxgCIAMoCRIVCg1hdHRhY2hlZF9vbmx5GAMgASgIEhQKDHJlc291cmNlX2lkcxgEIAMoCSKYAQoPT2ZmZXJDYXBhYmlsaXR5Eg4KBm1vZHVsZRgBIAEoCRIRCglvcGVyYXRpb24YAiABKAkSMgoIcmVxdWlyZXMYAyADKAsyIC5ob3N0Lmluc3RhbGxhdGlvbi52MS5PZmZlclNjb3BlEi4KBXRvb2xzGAQgAygLMh8uaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXJUb29sIjkKCU9mZmVyVG9vbBIMCgRuYW1lGAEgASgJEg8KB2JpbmRpbmcYAiABKAkSDQoFd3JpdGUYAyABKAgiKgoPTGlzdEhlbGRSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCSJRChBMaXN0SGVsZFJlc3BvbnNlEj0KDWluc3RhbGxhdGlvbnMYASADKAsyJi5ob3N0Lmluc3RhbGxhdGlvbi52MS5IZWxkSW5zdGFsbGF0aW9uIlsKEEhlbGRJbnN0YWxsYXRpb24SOAoMaW5zdGFsbGF0aW9uGAEgASgLMiIuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uEg0KBXJvbGVzGAIgAygJMvMFChNJbnN0YWxsYXRpb25TZXJ2aWNlEmYKB0N1cnJlbnQSMC5ob3N0Lmluc3RhbGxhdGlvbi52MS5DdXJyZW50SW5zdGFsbGF0aW9uUmVxdWVzdBopLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkN1cnJlbnRJbnN0YWxsYXRpb24SUwoHSW5zdGFsbBIkLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxSZXF1ZXN0GiIuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uElQKBlJldmlldxIjLmhvc3QuaW5zdGFsbGF0aW9uLnYxLlJldmlld1JlcXVlc3QaJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5Db25zZW50QXBwcm92YWwSVwoJR3JhbnRSb2xlEiYuaG9zdC5pbnN0YWxsYXRpb24udjEuR3JhbnRSb2xlUmVxdWVzdBoiLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbhJZCgpSZXZva2VSb2xlEicuaG9zdC5pbnN0YWxsYXRpb24udjEuUmV2b2tlUm9sZVJlcXVlc3QaIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24SawoOTGlzdFJvbGVHcmFudHMSKy5ob3N0Lmluc3RhbGxhdGlvbi52MS5MaXN0Um9sZUdyYW50c1JlcXVlc3QaLC5ob3N0Lmluc3RhbGxhdGlvbi52MS5MaXN0Um9sZUdyYW50c1Jlc3BvbnNlEk0KBExpc3QSIS5ob3N0Lmluc3RhbGxhdGlvbi52MS5MaXN0UmVxdWVzdBoiLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RSZXNwb25zZRJZCghMaXN0SGVsZBIlLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RIZWxkUmVxdWVzdBomLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RIZWxkUmVzcG9uc2UycAoSSW5zdGFsbGFibGVTZXJ2aWNlEloKB0luc3RhbGwSKC5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb25Ob3RpY2UaJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb25BY2syyAEKDE9mZmVyU2VydmljZRJfCgpMaXN0T2ZmZXJzEicuaG9zdC5pbnN0YWxsYXRpb24udjEuTGlzdE9mZmVyc1JlcXVlc3QaKC5ob3N0Lmluc3RhbGxhdGlvbi52MS5MaXN0T2ZmZXJzUmVzcG9uc2USVwoLUmV2aWV3T2ZmZXISIy5ob3N0Lmluc3RhbGxhdGlvbi52MS5SZXZpZXdSZXF1ZXN0GiMuaG9zdC5pbnN0YWxsYXRpb24udjEuUmV2aWV3ZWRPZmZlckLVAQoYY29tLmhvc3QuaW5zdGFsbGF0aW9uLnYxQhFJbnN0YWxsYXRpb25Qcm90b1ABWjRhY2NvdW50cy9wa2cvZ2VuL2hvc3QvaW5zdGFsbGF0aW9uL3YxO2luc3RhbGxhdGlvbnYxogIDSElYqgIUSG9zdC5JbnN0YWxsYXRpb24uVjHKAhRIb3N0XEluc3RhbGxhdGlvblxWMeICIEhvc3RcSW5zdGFsbGF0aW9uXFYxXEdQQk1ldGFkYXRh6gIWSG9zdDo6SW5zdGFsbGF0aW9uOjpWMWIGcHJvdG8z"); /** * @generated from message host.installation.v1.CurrentInstallationRequest @@ -884,6 +884,78 @@ export type OfferTool = Message<"host.installation.v1.OfferTool"> & { export const OfferToolSchema: GenMessage = /*@__PURE__*/ messageDesc(file_host_installation_v1_installation, 23); +/** + * ListHeldRequest names the organisation the person reads at: the one their + * session was opened at. + * + * @generated from message host.installation.v1.ListHeldRequest + */ +export type ListHeldRequest = Message<"host.installation.v1.ListHeldRequest"> & { + /** + * The organisation. + * + * @generated from field: string organization_id = 1; + */ + organizationId: string; +}; + +/** + * Describes the message host.installation.v1.ListHeldRequest. + * Use `create(ListHeldRequestSchema)` to create a new message. + */ +export const ListHeldRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_installation_v1_installation, 24); + +/** + * ListHeldResponse is every installation the person holds a role on, sorted + * by installation id. + * + * @generated from message host.installation.v1.ListHeldResponse + */ +export type ListHeldResponse = Message<"host.installation.v1.ListHeldResponse"> & { + /** + * The installations. + * + * @generated from field: repeated host.installation.v1.HeldInstallation installations = 1; + */ + installations: HeldInstallation[]; +}; + +/** + * Describes the message host.installation.v1.ListHeldResponse. + * Use `create(ListHeldResponseSchema)` to create a new message. + */ +export const ListHeldResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_installation_v1_installation, 25); + +/** + * HeldInstallation is one installation the person may use. + * + * @generated from message host.installation.v1.HeldInstallation + */ +export type HeldInstallation = Message<"host.installation.v1.HeldInstallation"> & { + /** + * The installation. + * + * @generated from field: host.installation.v1.Installation installation = 1; + */ + installation?: Installation; + + /** + * The roles the person holds on it, directly or through a team, sorted. + * + * @generated from field: repeated string roles = 2; + */ + roles: string[]; +}; + +/** + * Describes the message host.installation.v1.HeldInstallation. + * Use `create(HeldInstallationSchema)` to create a new message. + */ +export const HeldInstallationSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_installation_v1_installation, 26); + /** * InstallationService is implemented by the host and by the local authority. * @@ -959,9 +1031,8 @@ export const InstallationService: GenService<{ output: typeof ListRoleGrantsResponseSchema; }, /** - * List answers what one organisation has installed: an administrator's read. A - * member's view, which installed solutions they may use, is their navigation and - * belongs to the entitlement read, not here. + * List answers what one organisation has installed: an administrator's read. + * A member's view, which installed solutions they may use, is ListHeld. * * @generated from rpc host.installation.v1.InstallationService.List */ @@ -970,6 +1041,21 @@ export const InstallationService: GenService<{ input: typeof ListRequestSchema; output: typeof ListResponseSchema; }, + /** + * ListHeld answers the signed-in person which installations of their + * organisation they may use: each active installation on which they hold a + * role, granted to them or to a team of theirs, with the roles they hold. + * It is their navigation and answers their own holdings alone, never who + * else holds what. A member holding nothing reads an empty list; a caller + * acting at another organisation is refused naming it. + * + * @generated from rpc host.installation.v1.InstallationService.ListHeld + */ + listHeld: { + methodKind: "unary"; + input: typeof ListHeldRequestSchema; + output: typeof ListHeldResponseSchema; + }, }> = /*@__PURE__*/ serviceDesc(file_host_installation_v1_installation, 0); From d2ea83a2179e5d45a8a69cfeb2bcddd50ad1b00e Mon Sep 17 00:00:00 2001 From: crisfiorini-oss <301773486+crisfiorini-oss@users.noreply.github.com> Date: Tue, 27 Oct 2026 16:58:32 +0000 Subject: [PATCH 3/4] test(accounts): retain impersonation refusal for member installation reads --- .../accounts/code/pkg/cataloggen/authz_methods_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/module/services/accounts/code/pkg/cataloggen/authz_methods_test.go b/module/services/accounts/code/pkg/cataloggen/authz_methods_test.go index 48cd1c83f..a753554cc 100644 --- a/module/services/accounts/code/pkg/cataloggen/authz_methods_test.go +++ b/module/services/accounts/code/pkg/cataloggen/authz_methods_test.go @@ -177,5 +177,6 @@ func TestImpersonationRestrictionsMatchTheDescriptorsTheInterceptorReads(t *test } require.Equal(t, catalogued, enforced) - require.Len(t, enforced, 68) // 57 of the API package + Issue, Delegate, RevokeDelegation, GetDelegation, Install, Review, GrantRole, RevokeRole, ListRoleGrants, List, Current of the implemented interfaces; Check and ListResources are public, authenticated by the token they check + require.Contains(t, enforced, "/host.installation.v1.InstallationService/ListHeld") + require.Len(t, enforced, 69) // 57 of the API package + Issue, Delegate, RevokeDelegation, GetDelegation, Install, Review, GrantRole, RevokeRole, ListRoleGrants, List, ListHeld, Current of the implemented interfaces; Check and ListResources are public, authenticated by the token they check } From c1d3b1be0510377f6e909866cd2c82d6a8418c61 Mon Sep 17 00:00:00 2001 From: crisfiorini-oss <301773486+crisfiorini-oss@users.noreply.github.com> Date: Tue, 27 Oct 2026 17:44:24 +0000 Subject: [PATCH 4/4] fix(accounts): mark shared Authority delivery issuance in the journal --- module/EVENTS.md | 5 ++++- module/docs/adr/0009-audit-store-swap.md | 1 + .../accounts/code/pkg/adapters/host_interface.go | 11 ++++++++--- .../impersonation_method_restriction_test.go | 3 ++- .../accounts/code/pkg/business/audit_registry.go | 4 ++-- .../code/pkg/business/authority_core_audit.go | 13 +++++++++++-- .../pkg/business/authority_core_audit_test.go | 16 ++++++++++++++++ .../accounts/code/pkg/business/journal_read.go | 9 +++++++-- 8 files changed, 51 insertions(+), 11 deletions(-) diff --git a/module/EVENTS.md b/module/EVENTS.md index 0c4f06b23..366cf9fe4 100644 --- a/module/EVENTS.md +++ b/module/EVENTS.md @@ -347,7 +347,10 @@ Each entry says whether it is `delivery_machinery`, computed here so a caller ne parses this module's payloads: the entry the row carries is a `saas.module.operation_context_minted` or `saas.module.delegated_audience_exchange` on one of the `delivery_bindings` the caller names (both carry `binding_id`), or a -`saas.module.work_context_minted` for the caller's own module principal. It is the +`saas.module.work_context_minted` for the caller's own module principal. Shared +Authority task/child-session issues also carry the authenticated module principal, +operation binding kind and binding id; only that module's listed delivery bindings +are marked. Ordinary person issues carry no such metadata. It is the caller's own delivery footprint, which delivered as ordinary work would produce more of itself. diff --git a/module/docs/adr/0009-audit-store-swap.md b/module/docs/adr/0009-audit-store-swap.md index 414ed3fbc..57c2946d8 100644 --- a/module/docs/adr/0009-audit-store-swap.md +++ b/module/docs/adr/0009-audit-store-swap.md @@ -191,6 +191,7 @@ tenant, and an `internal` type in it is never fanned out. | Audit journal retention | Read `RunRetention`, journal position SQL, `PruneSourceOperationReceipts`, SDK `receipts` protobuf/digest storage and PostgreSQL receipt tests; searched shared `core`/`sdk-go` implementations and used Go's standard sorting. Source cleanup already preserves audit receipts, but the old committed response omitted the interval end. | Extend the existing committed response with `from`/`to` and reuse its effect identity, request digest, receipt table and control-plane transaction. Preserve legacy coverage as unproven; add only the bounded role permissions needed to retire delivered rows. | | Integration with delivered module authority | Read the current envelope registry, `BeginModuleAuthorityUse`, `AuthorizeModuleCapability`, SDK receipt storage, the migration replay gate and its `throwawayPostgres` helper, and all shipped migrations before reconciling the branch. The existing authority checks already capture the current registry and hold the installation lock. Existing dedicated generated directories cover contract, binding, catalog and deployment snapshots. | Reuse those current checks and generators; use the existing disposable PostgreSQL helper so the new migration tests run in the canonical replay gate. Preserve shipped migrations through 60 and move only the unshipped audit migrations to 61–63. Mark dedicated generated outputs for review while keeping authored inputs and dependency locks visible. | | Service compiler security update | Read the existing service compiler and runtime constants/templates, checked released `service-go-grpc` 0.1.58 and its successful release assets, and used the CLI version/protocol admission before moving the manifests. The shared owner already supplies Go 1.27.3. | Adopt the released owner through `codefly update service`; regenerate its complete outputs. Preserve each service minimum and avoid a local renderer or a second compiler/image mechanism. | +| Shared Authority delivery footprint | Read the current Authority Issue handler, RecordAuthorityIssue and legacy journal marker. Shared issuance uses task/child-session audit types, whose payload previously omitted the binding. | Extend the existing transactional recording with authenticated module/binding metadata, using the existing ModulePrincipalID mapping, and reuse the same caller/binding marker. Ordinary person issues remain unmarked; no new event type, queue or ledger. | | Shared installation interface compatibility | Searched the host adapters and imported interface, and verified [Authority #41](https://github.com/codefly-dev/authority/pull/41) already implements member-held installation reads in the shared core and conformance suite. | Reuse `HeldInstallations` from the shared core through a thin host `ListHeld` adapter; import the exact canonical schema and regenerate complete bindings. No new role filter or storage query. | ## Compiler qualification during merge preparation diff --git a/module/services/accounts/code/pkg/adapters/host_interface.go b/module/services/accounts/code/pkg/adapters/host_interface.go index f2149fb49..6cae84611 100644 --- a/module/services/accounts/code/pkg/adapters/host_interface.go +++ b/module/services/accounts/code/pkg/adapters/host_interface.go @@ -202,6 +202,7 @@ func (s *HostAuthorityServer) Issue(ctx context.Context, req *authorityv1.IssueR Organization: req.GetOrganizationId(), Scopes: scopesFromWire(req.GetScopes()), TTL: time.Duration(req.GetTtlSeconds()) * time.Second} var out authority.Issued child := false + modulePrefix := "" switch as := req.GetAs().(type) { case *authorityv1.IssueRequest_Person: person, perr := signedInPerson(ctx, "be issued a capability") @@ -217,18 +218,21 @@ func (s *HostAuthorityServer) Issue(ctx context.Context, req *authorityv1.IssueR if werr != nil { return nil, werr } + modulePrefix = w.Module out, err = core.IssueForWorkload(ctx, w, r) case *authorityv1.IssueRequest_Installation: w, werr := workload(ctx, req.GetActor().GetCredential(), "actor") if werr != nil { return nil, werr } + modulePrefix = w.Module out, err = core.IssueForInstallation(ctx, w, as.Installation.GetInstallationId(), r) case *authorityv1.IssueRequest_ParentToken: w, werr := workload(ctx, req.GetActor().GetCredential(), "actor") if werr != nil { return nil, werr } + modulePrefix = w.Module out, err = core.IssueChild(ctx, as.ParentToken, w, r) child = true case *authorityv1.IssueRequest_Delegation: @@ -236,6 +240,7 @@ func (s *HostAuthorityServer) Issue(ctx context.Context, req *authorityv1.IssueR if werr != nil { return nil, werr } + modulePrefix = w.Module out, err = core.IssueForDelegation(ctx, as.Delegation.GetDelegationId(), w, r) default: return nil, status.Error(codes.InvalidArgument, "an issue names whom it is for: a person, a workload, an installation, a parent token or a delegation") @@ -243,17 +248,17 @@ func (s *HostAuthorityServer) Issue(ctx context.Context, req *authorityv1.IssueR if err != nil { return nil, coreStatus(err) } - return s.issued(ctx, out, child) + return s.issued(ctx, out, child, modulePrefix, req.GetOperationBindingId()) } // issued records the issue in the organization it was issued in and answers it. -func (s *HostAuthorityServer) issued(ctx context.Context, out authority.Issued, child bool) (*authorityv1.IssueResponse, error) { +func (s *HostAuthorityServer) issued(ctx context.Context, out authority.Issued, child bool, modulePrefix, binding string) (*authorityv1.IssueResponse, error) { wc := out.Context actor := wc.GetOwnerPrincipalId() if chain := wc.GetActorChain(); len(chain) > 0 { actor = chain[len(chain)-1].GetPrincipalId() } - if err := service.RecordAuthorityIssue(ctx, wc.GetTenantId(), actor, child, wc.GetTaskId()); err != nil { + if err := service.RecordAuthorityIssue(ctx, wc.GetTenantId(), actor, child, wc.GetTaskId(), modulePrefix, binding); err != nil { return nil, status.Errorf(codes.Unavailable, "the issue could not be recorded, so it is not handed out: %v", err) } wire, err := workContextToWire(wc) diff --git a/module/services/accounts/code/pkg/adapters/impersonation_method_restriction_test.go b/module/services/accounts/code/pkg/adapters/impersonation_method_restriction_test.go index d113b16fe..99aea596b 100644 --- a/module/services/accounts/code/pkg/adapters/impersonation_method_restriction_test.go +++ b/module/services/accounts/code/pkg/adapters/impersonation_method_restriction_test.go @@ -99,7 +99,8 @@ func TestRestrictedProceduresAreDeniedOnlyWhileImpersonating(t *testing.T) { withoutCentralEnforcement(t) restricted := restrictedProcedures(t) - require.Len(t, restricted, 68, "the declared restriction set changed; confirm it against AUTHZ_MATRIX.md") // +11: Issue, Delegate, RevokeDelegation, GetDelegation, Install, Review, GrantRole, RevokeRole, ListRoleGrants, List, Current of the implemented interfaces; Check and ListResources are public, authenticated by the token they check + require.Contains(t, restricted, "/host.installation.v1.InstallationService/ListHeld") + require.Len(t, restricted, 69, "the declared restriction set changed; confirm it against AUTHZ_MATRIX.md") // +12: Issue, Delegate, RevokeDelegation, GetDelegation, Install, Review, GrantRole, RevokeRole, ListRoleGrants, List, ListHeld, Current of the implemented interfaces; Check and ListResources are public, authenticated by the token they check for _, procedure := range restricted { t.Run(procedure, func(t *testing.T) { diff --git a/module/services/accounts/code/pkg/business/audit_registry.go b/module/services/accounts/code/pkg/business/audit_registry.go index a6d047c27..908baf03a 100644 --- a/module/services/accounts/code/pkg/business/audit_registry.go +++ b/module/services/accounts/code/pkg/business/audit_registry.go @@ -713,9 +713,9 @@ var auditEventCatalog = []AuditEventDefinition{ mutation(EventAuthorityResourceAttached, CategoryAccess, "A resource created through an installation was attached to it by the kind's serving module.", authorityChangeFields()...), mutation(EventAuthorityResourceDetached, CategoryAccess, "A deleted resource was released from the installation that owned it.", authorityChangeFields()...), mutation(EventAuthorityResourceTransferred, CategoryAccess, "An existing resource joined an installed solution's principal, authorized by a person administering it.", authorityChangeFields()...), - mutation(EventWorkContextTaskStarted, CategoryAccess, "A signed Work Context was issued for a new agent task and root session."), + mutation(EventWorkContextTaskStarted, CategoryAccess, "A signed Work Context was issued for a new agent task and root session.", str("module_principal_id"), str("binding_kind"), str("binding_id")), mutation(EventWorkContextRootSession, CategoryAccess, "A new root agent session was started under an existing task."), - mutation(EventWorkContextChildSession, CategoryAccess, "An attenuated child agent session was started."), + mutation(EventWorkContextChildSession, CategoryAccess, "An attenuated child agent session was started.", str("module_principal_id"), str("binding_kind"), str("binding_id")), mutation(EventWorkContextAudienceExch, CategoryAccess, "A Work Context task and session lineage was reissued for another audience."), mutation(EventWorkContextRenewed, CategoryAccess, "A delegated actor renewed its Work Context past the signing TTL cap."), diff --git a/module/services/accounts/code/pkg/business/authority_core_audit.go b/module/services/accounts/code/pkg/business/authority_core_audit.go index b70435fa4..ba1aee8b1 100644 --- a/module/services/accounts/code/pkg/business/authority_core_audit.go +++ b/module/services/accounts/code/pkg/business/authority_core_audit.go @@ -111,7 +111,7 @@ func authorityActor(actor string) (id, actorType string) { // of the organization it was issued in, before it is handed out: a child hop // is a child session, every other shape a task started. An issue whose record // cannot be written is refused. -func (s *Service) RecordAuthorityIssue(ctx context.Context, organization, actor string, child bool, task string) error { +func (s *Service) RecordAuthorityIssue(ctx context.Context, organization, actor string, child bool, task, modulePrefix, binding string) error { event := EventWorkContextTaskStarted if child { event = EventWorkContextChildSession @@ -121,6 +121,15 @@ func (s *Service) RecordAuthorityIssue(ctx context.Context, organization, actor return fmt.Errorf("record %s: organization %q is not one of this host's", event, organization) } return s.store.WithOrgTx(ctx, organization, func(ctx context.Context) error { - return s.emitTx(ctx, actorID, actorType, event, "work_context", task, organization) + return s.emitTx(ctx, actorID, actorType, event, "work_context", task, organization, authorityIssueAuditPayload(modulePrefix, binding)) }) } + +// authorityIssueAuditPayload preserves the authenticated module and the binding +// the shared core just authorized. An ordinary person's issue carries neither. +func authorityIssueAuditPayload(modulePrefix, binding string) map[string]any { + if modulePrefix == "" || binding == "" { + return nil + } + return map[string]any{"module_principal_id": ModulePrincipalID(modulePrefix), "binding_kind": "operation", "binding_id": binding} +} diff --git a/module/services/accounts/code/pkg/business/authority_core_audit_test.go b/module/services/accounts/code/pkg/business/authority_core_audit_test.go index 203b0c6f4..18d066f44 100644 --- a/module/services/accounts/code/pkg/business/authority_core_audit_test.go +++ b/module/services/accounts/code/pkg/business/authority_core_audit_test.go @@ -36,3 +36,19 @@ func TestAuthorityChangesAreRecordedAsRegisteredMutations(t *testing.T) { require.Empty(t, id) require.Equal(t, ActorTypeSystem, actorType) } + +// The actual shared-issuer payload must be understood by the journal marker; +// an unmarked delivery issue would make the delivery tail itself forever. +func TestAuthorityIssuePayloadMarksOnlyItsModulesDeliveryBinding(t *testing.T) { + for _, kind := range []EventType{EventWorkContextTaskStarted, EventWorkContextChildSession} { + entry := AuditEntry{EventType: kind, Payload: authorityIssueAuditPayload("runtime", "delivery")} + bindings := map[string]struct{}{"delivery": {}} + require.True(t, AuditEntryIsDeliveryMachinery(entry, ModulePrincipalID("runtime"), bindings)) + require.False(t, AuditEntryIsDeliveryMachinery(entry, ModulePrincipalID("other"), bindings)) + require.False(t, AuditEntryIsDeliveryMachinery(entry, ModulePrincipalID("runtime"), map[string]struct{}{"another": {}})) + entry.Payload = authorityIssueAuditPayload("", "delivery") + require.False(t, AuditEntryIsDeliveryMachinery(entry, ModulePrincipalID("runtime"), bindings)) + entry.Payload = authorityIssueAuditPayload("runtime", "") + require.False(t, AuditEntryIsDeliveryMachinery(entry, ModulePrincipalID("runtime"), bindings)) + } +} diff --git a/module/services/accounts/code/pkg/business/journal_read.go b/module/services/accounts/code/pkg/business/journal_read.go index d66dc32a6..e6a16e6f7 100644 --- a/module/services/accounts/code/pkg/business/journal_read.go +++ b/module/services/accounts/code/pkg/business/journal_read.go @@ -338,12 +338,17 @@ func journalDataObject(contentType string, data []byte) map[string]any { // - saas.module.operation_context_minted, whose binding_id is one of // deliveryBindings: the headless Work Context a delivery runs under, issued // for one of the caller's delivery bindings. +// // - saas.module.delegated_audience_exchange, whose binding_id is one of // deliveryBindings: an exchange on one of the caller's delivery bindings. +// // - saas.module.work_context_minted whose actor is the caller's own module // principal: the caller's own refresh of its Work Context. Another module's // mint is that module's, not the caller's. // +// - shared Authority task/child-session issues, whose authenticated module +// and operation binding are recorded by that issuer, for a delivery binding. +// // Everything else is false, including a row of another type and a record that // cannot be read: unmarked work is delivered, never skipped. func JournalRowIsDeliveryMachinery(row JournalRow, callerPrincipal string, deliveryBindings map[string]struct{}) bool { @@ -361,9 +366,9 @@ func JournalRowIsDeliveryMachinery(row JournalRow, callerPrincipal string, deliv // the audit entry itself. func AuditEntryIsDeliveryMachinery(entry AuditEntry, callerPrincipal string, deliveryBindings map[string]struct{}) bool { switch entry.EventType { - case EventModuleOperationContextMint, EventDelegatedAudienceExchange: + case EventModuleOperationContextMint, EventDelegatedAudienceExchange, EventWorkContextTaskStarted, EventWorkContextChildSession: principal := entry.ActorID - if entry.EventType == EventDelegatedAudienceExchange { + if entry.EventType != EventModuleOperationContextMint { principal, _ = entry.Payload["module_principal_id"].(string) kind, _ := entry.Payload["binding_kind"].(string) if kind != "operation" {