diff --git a/module/MODULE_INSTALLATION.md b/module/MODULE_INSTALLATION.md index 27c4bb050..4c46690a0 100644 --- a/module/MODULE_INSTALLATION.md +++ b/module/MODULE_INSTALLATION.md @@ -279,3 +279,26 @@ receipts. New consumers require the supported authority response and refuse a missing or unknown version; old servers reject the new request field. Adopting this response does not itself migrate an existing consumer, validate software compatibility or qualify an installed rollout. + +## Recovering an audit emission + +A module calls `ModuleCapabilitiesService.LookupAuditEvent` with the complete +original `ModuleEmitAuditEventRequest`, including a nonempty idempotency key, +under its current module Work Context. The host rechecks the same tenant, +registered event scope and actor rules as emission. This is not an alternative +human audit-query permission. + +A nonempty `event_id` names the committed event for that exact intent. An empty +ID means no receipt was visible at the read; it does not prove that nothing +happened and does not authorize an effect retry. A changed intent under the key, +or a historical key-only reservation without recorded intent, returns +`FAILED_PRECONDITION`. The host does not backfill historical intent from a retry. +Lookup does not create an event or reserve a key. Revoking the module's grant +also revokes receipt access. + +Keyed emissions now bind actor, tenant, resource, payload, impersonation and +client attribution alongside the event identity in the audit transaction. +Attempt timestamps, generated IDs, IP addresses and catalog versions do not +change the intent. Apply migration19 before starting a binary that uses the new +binding; producers must treat an unverifiable historical key as reconciliation +work rather than silently choosing another key and duplicating the effect. diff --git a/module/configurations/local/gateway.env b/module/configurations/local/gateway.env index 648f9c8a6..dd68dbfb4 100644 --- a/module/configurations/local/gateway.env +++ b/module/configurations/local/gateway.env @@ -1,3 +1,8 @@ # Comma-separated exact IPs or CIDRs allowed to supply X-Forwarded-For and # X-Real-IP. Empty means the gateway trusts no forwarding headers. TRUSTED_PROXY_CIDRS= + +# Explicit opt-in for registered /v1/ routes accepting signed Work +# Contexts without a bearer. Enable only targets that independently enforce +# Work Context scopes and current authority. Empty array keeps bearer-only access. +WORK_CONTEXT_MODULE_PREFIXES=[] diff --git a/module/services/accounts/AUTHZ_MATRIX.md b/module/services/accounts/AUTHZ_MATRIX.md index a2b19d4de..e2720f6b1 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: **246 RPCs** across **35 services**. Missing, invalid, or unclassified procedures are denied by both Connect and gRPC interceptors. +Inventory: **248 RPCs** across **35 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. @@ -115,6 +115,7 @@ The compact tier is retained for compatibility. Guards, resource bindings, audit | `/saas.accounts.v1.ModuleCapabilitiesService/ListReadableSourceCollections` | unary | `—` | `internal` | exposure=INTERNAL; tenant=NONE | — | — | — | FORBIDDEN / INTERNAL | CONFIDENTIAL → CONFIDENTIAL | List source collections the verified viewer may currently read. | | `/saas.accounts.v1.ModuleCapabilitiesService/ListSubjectVisibility` | unary | `—` | `internal` | exposure=INTERNAL; tenant=NONE | — | — | — | FORBIDDEN / INTERNAL | CONFIDENTIAL → CONFIDENTIAL | List the subjects whose rows a viewer may read in a tenant. | | `/saas.accounts.v1.ModuleCapabilitiesService/ListSubscriptions` | unary | `—` | `internal` | exposure=INTERNAL; tenant=NONE | — | — | — | FORBIDDEN / INTERNAL | CONFIDENTIAL → CONFIDENTIAL | List the calling principal's live event subscriptions. | +| `/saas.accounts.v1.ModuleCapabilitiesService/LookupAuditEvent` | unary | `—` | `internal` | exposure=INTERNAL; tenant=NONE | — | — | — | FORBIDDEN / INTERNAL | CONFIDENTIAL → CONFIDENTIAL | Recover a committed audit event identity for matching intent under current emission authority. | | `/saas.accounts.v1.ModuleCapabilitiesService/MintModuleOperationContext` | unary | `—` | `internal` | exposure=INTERNAL; tenant=NONE | — | — | SUCCESS: saas.module.operation_context_minted | FORBIDDEN / INTERNAL | SECRET → SECRET | Issue a composed module, with no person present, a Work Context for one of its installed operation audiences. | | `/saas.accounts.v1.ModuleCapabilitiesService/MintModuleRegistration` | unary | `—` | `internal` | exposure=INTERNAL; tenant=NONE | — | — | SUCCESS: saas.module.registration_minted | FORBIDDEN / INTERNAL | SECRET → SECRET | Issue a composed module the signed credential it registers its gateway REST prefix with. | | `/saas.accounts.v1.ModuleCapabilitiesService/MintModuleWorkContext` | unary | `—` | `internal` | exposure=INTERNAL; tenant=NONE | — | — | SUCCESS: saas.module.work_context_minted | FORBIDDEN / INTERNAL | SECRET → SECRET | Issue a composed module the Work Context its service principal calls this surface with. | @@ -129,6 +130,7 @@ The compact tier is retained for compatibility. Guards, resource bindings, audit | `/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/Subscribe` | unary | `—` | `internal` | exposure=INTERNAL; tenant=NONE | — | — | — | FORBIDDEN / INTERNAL | CONFIDENTIAL → CONFIDENTIAL | Create or re-affirm a durable event subscription for the caller. | | `/saas.accounts.v1.ModuleCapabilitiesService/Unsubscribe` | unary | `—` | `internal` | exposure=INTERNAL; tenant=NONE | — | — | — | FORBIDDEN / INTERNAL | CONFIDENTIAL → CONFIDENTIAL | Revoke one of the caller's own event subscriptions. | +| `/saas.accounts.v1.ModuleCapabilitiesService/VerifyWorkContextRuntimeBoundary` | unary | `—` | `internal` | exposure=INTERNAL; tenant=NONE | — | — | — | FORBIDDEN / INTERNAL | SECRET → CONFIDENTIAL | Attest current capability membership in an active host-owned runtime boundary. | | `/saas.accounts.v1.NotificationService/DeleteNotification` | unary | `DELETE /v1/notifications/{id}` | `auth` | exposure=AUTHENTICATED; tenant=USER | — | — | — | FORBIDDEN / STANDARD_WRITE | CONFIDENTIAL → CONFIDENTIAL | Delete one of the caller's notifications. | | `/saas.accounts.v1.NotificationService/GetUnreadCount` | unary | `GET /v1/notifications/unread-count` | `auth` | exposure=AUTHENTICATED; tenant=USER | — | — | — | FORBIDDEN / STANDARD_READ | CONFIDENTIAL → CONFIDENTIAL | Count the caller's unread notifications. | | `/saas.accounts.v1.NotificationService/ListNotifications` | unary | `GET /v1/notifications` | `auth` | exposure=AUTHENTICATED; tenant=USER | — | — | — | FORBIDDEN / STANDARD_READ | CONFIDENTIAL → CONFIDENTIAL | List the caller's notifications. | @@ -258,7 +260,7 @@ The compact tier is retained for compatibility. Guards, resource bindings, audit ## Tier totals - `auth`: 43 -- `internal`: 49 +- `internal`: 51 - `mfa`: 4 - `org_admin`: 56 - `org_member`: 49 diff --git a/module/services/accounts/code/pkg/adapters/contributed_catalog_test.go b/module/services/accounts/code/pkg/adapters/contributed_catalog_test.go new file mode 100644 index 000000000..8c85045bf --- /dev/null +++ b/module/services/accounts/code/pkg/adapters/contributed_catalog_test.go @@ -0,0 +1,44 @@ +package adapters + +import ( + "accounts/pkg/business" + catalogv1 "accounts/pkg/gen/saas/catalog/v1" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "os" + "sort" + "testing" +) + +// The catalog also carries permissions admitted by the module composer, whose +// namespace-qualified logical IDs are wider than built-in descriptor scopes. +func TestServiceCatalogContributedPermissionVocabulary(t *testing.T) { + raw, err := os.ReadFile("../../../generated/service-catalog.json") + require.NoError(t, err) + baseline := &catalogv1.ServiceCatalog{} + require.NoError(t, protojson.Unmarshal(raw, baseline)) + for _, tc := range []struct { + resource, action string + valid bool + }{ + {"example.rows", "read", true}, + {"example-suite.row_items", "update-item", true}, + {"example..rows", "read", false}, + {"Example.rows", "read", false}, + {"example.rows", "read/any", false}, + {"example.rows", "read:", false}, + } { + t.Run(tc.resource+":"+tc.action, func(t *testing.T) { + catalog := proto.Clone(baseline).(*catalogv1.ServiceCatalog) + catalog.Permissions = append(catalog.Permissions, &catalogv1.PermissionDefinition{Permission: tc.resource + ":" + tc.action, Resource: tc.resource, Action: tc.action, Description: "Example contributed permission"}) + sort.Slice(catalog.Permissions, func(i, j int) bool { return catalog.Permissions[i].Permission < catalog.Permissions[j].Permission }) + err := business.ValidateServiceCatalog(catalog) + if tc.valid { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, "not canonical") + } + }) + } +} 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 8499c23c9..8fa77bc83 100644 --- a/module/services/accounts/code/pkg/adapters/module_capabilities_connect.go +++ b/module/services/accounts/code/pkg/adapters/module_capabilities_connect.go @@ -115,3 +115,7 @@ func (h *moduleCapabilitiesConnectHandler) ListSubscriptions(ctx context.Context func (h *moduleCapabilitiesConnectHandler) ReplayEvents(ctx context.Context, req *connect.Request[gen.ModuleReplayEventsRequest]) (*connect.Response[gen.ModuleReplayEventsResponse], error) { return unary(ctx, req, h.inner.ReplayEvents) } + +func (h *moduleCapabilitiesConnectHandler) LookupAuditEvent(ctx context.Context, req *connect.Request[gen.ModuleEmitAuditEventRequest]) (*connect.Response[gen.ModuleLookupAuditEventResponse], error) { + return unary(ctx, req, h.inner.LookupAuditEvent) +} 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 016c6ba95..f153b107c 100644 --- a/module/services/accounts/code/pkg/adapters/module_capabilities_server.go +++ b/module/services/accounts/code/pkg/adapters/module_capabilities_server.go @@ -818,3 +818,19 @@ func moduleApprovalProto(a *business.ApprovalRequest) *gen.ModuleApproval { CreatedAt: timestamppb.New(a.CreatedAt), } } + +func (s *ModuleCapabilitiesServer) LookupAuditEvent(ctx context.Context, req *gen.ModuleEmitAuditEventRequest) (*gen.ModuleLookupAuditEventResponse, error) { + if err := Validate(req); err != nil { + return nil, err + } + caller, err := moduleCaller(ctx) + if err != nil { + return nil, err + } + eventID, err := service.ModuleLookupAuditEvent(ctx, caller, + req.GetTenant(), req.GetEventType(), req.GetActor(), req.GetSolution(), req.GetEntryId(), req.GetIdempotencyKey(), req.GetFields()) + if err != nil { + return nil, err + } + return &gen.ModuleLookupAuditEventResponse{EventId: eventID}, nil +} diff --git a/module/services/accounts/code/pkg/adapters/module_capabilities_server_test.go b/module/services/accounts/code/pkg/adapters/module_capabilities_server_test.go index da9d58d49..968343bc7 100644 --- a/module/services/accounts/code/pkg/adapters/module_capabilities_server_test.go +++ b/module/services/accounts/code/pkg/adapters/module_capabilities_server_test.go @@ -302,3 +302,16 @@ func TestFetchDatasourceBlob_StreamsAuthorizedBlobEndToEnd(t *testing.T) { t.Fatal("streamed frames carried no content type") } } + +// Receipt knowledge is not authority: even a fully specified lookup must pass +// module authentication before reaching the service or any stored audit binding. +func TestAuditReceiptLookupRequiresModuleAuthentication(t *testing.T) { + server := &ModuleCapabilitiesServer{} + _, err := server.LookupAuditEvent(context.Background(), &gen.ModuleEmitAuditEventRequest{ + Tenant: blobStreamOrgID, EventType: "example.changed", Actor: blobStreamCallerID, + Solution: "example", EntryId: "entry", IdempotencyKey: "known-effect-key", + }) + if status.Code(err) != codes.Unauthenticated { + t.Fatalf("unauthenticated lookup: %v", err) + } +} diff --git a/module/services/accounts/code/pkg/adapters/refresh_status_test.go b/module/services/accounts/code/pkg/adapters/refresh_status_test.go new file mode 100644 index 000000000..4be74a07e --- /dev/null +++ b/module/services/accounts/code/pkg/adapters/refresh_status_test.go @@ -0,0 +1,53 @@ +package adapters + +import ( + "context" + "errors" + "fmt" + "testing" + + "accounts/pkg/auth" + "accounts/pkg/business" + gen "accounts/pkg/gen/saas/accounts/v1" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type refreshOutcomeMinter struct { + auth.JWTMinter + err error +} + +func (m refreshOutcomeMinter) VerifyRefresh(context.Context, string) (*auth.TokenPair, error) { + return nil, m.err +} + +// Exercise the actual business and RPC path: rejected credentials must tell +// clients to sign in, while infrastructure failures must remain retryable. +func TestRefreshRejectionIsUnauthenticated(t *testing.T) { + previous := service + t.Cleanup(func() { service = previous }) + for _, tc := range []struct { + name string + err error + want codes.Code + }{ + {"revoked", auth.ErrRefreshRevoked, codes.Unauthenticated}, + {"reused", auth.ErrRefreshReuse, codes.Unauthenticated}, + {"wrapped", fmt.Errorf("rotation: %w", auth.ErrRefreshRevoked), codes.Unauthenticated}, + {"policy", auth.RejectRefresh(auth.RefreshRejectionIdleTimeout), codes.Unauthenticated}, + {"storage unavailable", errors.New("storage unavailable"), codes.Unknown}, + } { + t.Run(tc.name, func(t *testing.T) { + service = &business.Service{} + service.SetJWTMinter(refreshOutcomeMinter{err: tc.err}) + _, err := (&AuthServer{}).RefreshToken(context.Background(), &gen.RefreshTokenRequest{RefreshToken: "test-refresh-token"}) + if status.Code(err) != tc.want { + t.Fatalf("refresh status=%s; want %s", status.Code(err), tc.want) + } + if tc.want == codes.Unauthenticated && status.Convert(err).Message() != "session refresh rejected" { + t.Fatal("refresh rejection must not distinguish token existence or rejection reason") + } + }) + } +} diff --git a/module/services/accounts/code/pkg/adapters/rpcs.go b/module/services/accounts/code/pkg/adapters/rpcs.go index 8739f8f7c..b9c30c375 100644 --- a/module/services/accounts/code/pkg/adapters/rpcs.go +++ b/module/services/accounts/code/pkg/adapters/rpcs.go @@ -1075,7 +1075,13 @@ func (s *AuthServer) RefreshToken(ctx context.Context, req *gen.RefreshTokenRequ if err := Validate(req); err != nil { return nil, err } - return service.RefreshToken(ctx, req) + response, err := service.RefreshToken(ctx, req) + if errors.Is(err, auth.ErrRefreshRevoked) || errors.Is(err, auth.ErrRefreshReuse) { + // Terminal session rejection must reach REST as 401, not an Unknown + // error (500) that clients correctly treat as a transient outage. + return nil, status.Error(codes.Unauthenticated, "session refresh rejected") + } + return response, err } func (s *AuthServer) SwitchOrganization(ctx context.Context, req *gen.SwitchOrganizationRequest) (*gen.SwitchOrganizationResponse, error) { diff --git a/module/services/accounts/code/pkg/adapters/runtime_boundary_verification.go b/module/services/accounts/code/pkg/adapters/runtime_boundary_verification.go new file mode 100644 index 000000000..31f85a52c --- /dev/null +++ b/module/services/accounts/code/pkg/adapters/runtime_boundary_verification.go @@ -0,0 +1,69 @@ +package adapters + +import ( + "context" + + "accounts/pkg/auth" + gen "accounts/pkg/gen/saas/accounts/v1" + "connectrpc.com/connect" + "github.com/codefly-dev/sdk-go/workcontext" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +func (s *ModuleCapabilitiesServer) VerifyWorkContextRuntimeBoundary(ctx context.Context, req *gen.VerifyWorkContextRuntimeBoundaryRequest) (*gen.VerifyWorkContextRuntimeBoundaryResponse, error) { + if err := Validate(req); err != nil { + return nil, err + } + if err := requireInternalCredential(ctx); err != nil { + return nil, err + } + md, _ := metadata.FromIncomingContext(ctx) + if len(md.Get(workcontext.WorkContextHeaderName)) != 1 { + return nil, status.Error(codes.Unauthenticated, "one module Work Context required") + } + authority := WorkContextSingleton() + if service == nil || authority == nil || authority.verifier == nil || authority.configureErr != nil || authority.authority == nil { + return nil, status.Error(codes.Unavailable, "work context authority unavailable") + } + caller, err := moduleCaller(ctx) + if err != nil { + return nil, err + } + token, err := workcontext.ParseWorkContextToken(req.ForwardedWorkContextToken) + if err != nil { + return nil, status.Error(codes.PermissionDenied, "invalid forwarded Work Context") + } + claims, err := authority.verifier.Verify(token, workcontext.WorkContextExpectations{Issuer: authority.issuer}) + if err != nil || claims.GetTenantId() == "" || claims.GetOwnerPrincipalId() == "" || claims.GetTaskId() == "" { + return nil, status.Error(codes.PermissionDenied, "invalid forwarded Work Context") + } + if len(claims.ActorChain) > 0 && authority.journal == nil { + return nil, status.Error(codes.Unavailable, "delegation journal unavailable") + } + ctx = auth.WithVerifiedDatabaseIdentity(ctx, claims.OwnerPrincipalId, claims.TenantId) + if _, err := authority.requireCurrentAuthority(ctx, claims.TenantId, claims); err != nil { + return nil, err + } + if err := service.VerifyModuleRuntimeBoundary(ctx, caller, claims.TenantId, claims.Audience, claims.TaskId); err != nil { + return nil, err + } + // Recheck authority after the registry read, as on the placed-record oracle. + if _, err := authority.requireCurrentAuthority(ctx, claims.TenantId, claims); err != nil { + return nil, err + } + return &gen.VerifyWorkContextRuntimeBoundaryResponse{TenantId: claims.TenantId, BoundaryId: claims.TaskId}, nil +} + +func (h *moduleCapabilitiesConnectHandler) VerifyWorkContextRuntimeBoundary(ctx context.Context, req *connect.Request[gen.VerifyWorkContextRuntimeBoundaryRequest]) (*connect.Response[gen.VerifyWorkContextRuntimeBoundaryResponse], error) { + ctx = connectCtx(ctx, req.Header()) + md, _ := metadata.FromIncomingContext(ctx) + md = md.Copy() + md.Set(workcontext.WorkContextHeaderName, req.Header().Values(workcontext.WorkContextHeaderName)...) + out, err := h.inner.VerifyWorkContextRuntimeBoundary(metadata.NewIncomingContext(ctx, md), req.Msg) + if err != nil { + return nil, translateGRPCError(err) + } + return connect.NewResponse(out), nil +} diff --git a/module/services/accounts/code/pkg/adapters/runtime_boundary_verification_test.go b/module/services/accounts/code/pkg/adapters/runtime_boundary_verification_test.go new file mode 100644 index 000000000..5e8ff9e3f --- /dev/null +++ b/module/services/accounts/code/pkg/adapters/runtime_boundary_verification_test.go @@ -0,0 +1,131 @@ +package adapters + +import ( + "context" + "crypto/ed25519" + "crypto/rand" + "testing" + "time" + + "accounts/pkg/business" + gen "accounts/pkg/gen/saas/accounts/v1" + "connectrpc.com/connect" + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" + "github.com/codefly-dev/sdk-go/workcontext" + "github.com/stretchr/testify/require" +) + +func TestVerifyWorkContextRuntimeBoundarySignedRead(t *testing.T) { + _, facts, client, _ := sourceReadFixture(t) + _, key, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + workContextSingleton.Configure(WorkContextAuthorityConfiguration{Issuer: "accounts.test", KeyID: "test-key", PrivateKey: key, Authority: sourceReadIdentityAuthority{facts}}) + record := &business.SolutionRegistration{RuntimeBoundary: boundaryStoredSeed, + Frontend: &business.SolutionFrontendHalf{LeaseExpiresAt: time.Now().Add(time.Hour), ContractVersion: "v1"}, + Backend: &business.SolutionBackendHalf{LeaseExpiresAt: time.Now().Add(time.Hour), ContractVersion: "v1"}} + store := &boundaryRegistryStore{record: record} + service, err = business.NewService(store) + require.NoError(t, err) + service.SetModuleCapabilities(nil, nil, business.ModulePrincipalRegistry{business.ModulePrincipalID("rows"): {Prefix: "rows", Tenant: readOrg}}) + boundary, err := business.SolutionRuntimeBoundary(boundaryStoredSeed, readOrg) + require.NoError(t, err) + moduleToken, _, err := workContextSingleton.StartModuleTask(business.ModuleWorkContextAuthority{PrincipalID: business.ModulePrincipalID("rows"), Tenant: readOrg}) + require.NoError(t, err) + mint := func(task, tenant, audience string) string { + token, _, err := workContextSingleton.signer.StartTask(workcontext.StartTaskInput{Audience: audience, TenantID: tenant, OwnerPrincipalID: readOwner, TaskID: task, SessionID: "019f6bf7-2222-7222-8222-222222222222", AuthorizationRevision: facts.facts.EffectiveRevision(), ReplayPolicy: workcontext.WorkContextReplayIdempotent}) + require.NoError(t, err) + return token.Encoded() + } + token := mint(boundary, readOrg, "rows") + request := func(token string) *connect.Request[gen.VerifyWorkContextRuntimeBoundaryRequest] { + req := connect.NewRequest(&gen.VerifyWorkContextRuntimeBoundaryRequest{ForwardedWorkContextToken: token}) + req.Header().Set(workcontext.WorkContextHeaderName, moduleToken.Encoded()) + req.Header().Set("x-codefly-internal-token", "source-read-test-perimeter") + return req + } + ctx := context.Background() + result, err := client.VerifyWorkContextRuntimeBoundary(ctx, request(token)) + require.NoError(t, err) + require.Equal(t, readOrg, result.Msg.TenantId) + require.Equal(t, boundary, result.Msg.BoundaryId) + require.NotEqual(t, boundaryStoredSeed, result.Msg.BoundaryId) + require.Nil(t, store.saved, "attestation must not write registration") + for _, tc := range []struct{ name, token string }{ + {"ordinary task", mint("019f6bf7-1111-7111-8111-111111111111", readOrg, "rows")}, + {"seed itself", mint(boundaryStoredSeed, readOrg, "rows")}, + {"wrong audience", mint(boundary, readOrg, "documents")}, + {"wrong tenant", mint(boundary, "019f6bf7-4444-7444-8444-444444444444", "rows")}, + {"invalid signature", token[:len(token)-8] + "invalid!"}, + } { + t.Run(tc.name, func(t *testing.T) { + _, err := client.VerifyWorkContextRuntimeBoundary(ctx, request(tc.token)) + require.Error(t, err) + }) + } + for _, header := range []string{workcontext.WorkContextHeaderName, "x-codefly-internal-token"} { + req := request(token) + req.Header().Del(header) + _, err := client.VerifyWorkContextRuntimeBoundary(ctx, req) + require.Error(t, err) + } + // A valid signature does not rescue an expired forwarded capability. + oldSigner, err := workcontext.NewWorkContextSigner(workcontext.WorkContextSignerOptions{Issuer: "accounts.test", KeyID: "test-key", PrivateKey: key, Now: func() time.Time { return time.Now().Add(-time.Hour) }}) + require.NoError(t, err) + expired, _, err := oldSigner.StartTask(workcontext.StartTaskInput{Audience: "rows", TenantID: readOrg, OwnerPrincipalID: readOwner, TaskID: boundary, SessionID: "old-session", AuthorizationRevision: facts.facts.EffectiveRevision()}) + require.NoError(t, err) + _, err = client.VerifyWorkContextRuntimeBoundary(ctx, request(expired.Encoded())) + require.Error(t, err) + // Every delegated hop remains subject to live revocation, with no new actors. + workContextSingleton.authority = &readExchangeAuthority{facts: facts.facts} + journal := &readExchangeJournal{} + workContextSingleton.journal = journal + actors := []*basev0.WorkActorV1{{PrincipalId: "actor-1", PrincipalKind: "service", DelegationId: "hop-1"}, {PrincipalId: "actor-2", PrincipalKind: "service", DelegationId: "hop-2"}} + delegated, _, err := workContextSingleton.signer.StartTask(workcontext.StartTaskInput{Audience: "rows", TenantID: readOrg, OwnerPrincipalID: readOwner, TaskID: boundary, SessionID: "delegated-session", AuthorizationRevision: facts.facts.EffectiveRevision(), ActorChain: actors}) + require.NoError(t, err) + _, err = client.VerifyWorkContextRuntimeBoundary(ctx, request(delegated.Encoded())) + require.NoError(t, err) + require.Equal(t, []string{"hop-1", "hop-2"}, journal.ids) + for _, hop := range []string{"hop-1", "hop-2"} { + journal.revoked = hop + _, err = client.VerifyWorkContextRuntimeBoundary(ctx, request(delegated.Encoded())) + require.Error(t, err) + } + workContextSingleton.journal = nil + _, err = client.VerifyWorkContextRuntimeBoundary(ctx, request(delegated.Encoded())) + require.Error(t, err) + workContextSingleton.authority = sourceReadIdentityAuthority{facts} + duplicate := request(token) + duplicate.Header().Add(workcontext.WorkContextHeaderName, moduleToken.Encoded()) + _, err = client.VerifyWorkContextRuntimeBoundary(ctx, duplicate) + require.Error(t, err) + // Cross-tenant access needs an explicit current module grant and the other + // tenant's derived boundary; neither tenant nor boundary comes from the body. + otherTenant := "019f6bf7-4444-7444-8444-444444444444" + otherBoundary, err := business.SolutionRuntimeBoundary(boundaryStoredSeed, otherTenant) + require.NoError(t, err) + otherToken := mint(otherBoundary, otherTenant, "rows") + _, err = client.VerifyWorkContextRuntimeBoundary(ctx, request(otherToken)) + require.Error(t, err) + service.SetModuleCapabilities(nil, nil, business.ModulePrincipalRegistry{business.ModulePrincipalID("rows"): {Prefix: "rows", Tenant: readOrg, CrossTenant: true}}) + other, err := client.VerifyWorkContextRuntimeBoundary(ctx, request(otherToken)) + require.NoError(t, err) + require.Equal(t, otherTenant, other.Msg.TenantId) + service.SetModuleCapabilities(nil, nil, business.ModulePrincipalRegistry{business.ModulePrincipalID("rows"): {Prefix: "rows", Tenant: readOrg}}) + facts.facts.OrganizationRevision++ + _, err = client.VerifyWorkContextRuntimeBoundary(ctx, request(token)) + require.Error(t, err) + facts.facts.OrganizationRevision-- + now := time.Now() + for _, mutate := range []func(){ + func() { record.TombstonedAt = &now }, + func() { record.TombstonedAt = nil; record.Backend.LeaseExpiresAt = now.Add(-time.Minute) }, + func() { record.Backend.LeaseExpiresAt = now.Add(time.Hour); record.Frontend.ContractVersion = "v2" }, + func() { record.Frontend.ContractVersion = "v1"; record.Frontend.LeaseExpiresAt = now.Add(-time.Minute) }, + func() { record.Frontend = nil }, + } { + mutate() + _, err = client.VerifyWorkContextRuntimeBoundary(ctx, request(token)) + require.Error(t, err) + } + require.Nil(t, store.saved) +} diff --git a/module/services/accounts/code/pkg/business/audit.go b/module/services/accounts/code/pkg/business/audit.go index 317b173d0..d5f2967fb 100644 --- a/module/services/accounts/code/pkg/business/audit.go +++ b/module/services/accounts/code/pkg/business/audit.go @@ -43,9 +43,10 @@ type AuditEntry struct { CreatedAt time.Time // IdempotencyKey, when set, deduplicates retried emits: the emitter reserves // (OrgID, EventType, IdempotencyKey) in a guard table inside the same - // transaction as the audit write, and a duplicate emit is a no-op success (no - // second row, no second webhook fan-out). Empty disables dedup. See - // audit_event_idempotency (migration 118). + // transaction as the audit write. Only an identical semantic intent is a + // no-op success (no second row or webhook fan-out). Changed intent conflicts; + // historical key-only guards are unverifiable. Empty disables dedup. See + // audit_event_idempotency and its effect receipt binding. IdempotencyKey string } @@ -212,14 +213,14 @@ func (e *DurableAuditEmitter) write(ctx context.Context, entry AuditEntry) error return err } if entry.IdempotencyKey != "" { - reserved, err := e.store.ReserveAuditIdempotency(ctx, entry.OrgID, string(entry.EventType), entry.IdempotencyKey) + reserved, err := e.store.ReserveAuditEffect(ctx, entry) if err != nil { return err } if !reserved { // A prior emit of this (org, event_type, idempotency_key) already wrote - // the event in a committed transaction. Skip the insert and the webhook - // fan-out and report success: the intended effect (exactly one audit row + // the same semantic event in a committed transaction. Skip the insert + // and webhook fan-out and report success: the intended effect (exactly one audit row // and one set of deliveries) is already in place. Reserving inside the // caller's tx keeps the guard row and the audit row atomic, so a rolled // back audit write also frees the key for a genuine retry. diff --git a/module/services/accounts/code/pkg/business/audit_effect.go b/module/services/accounts/code/pkg/business/audit_effect.go new file mode 100644 index 000000000..421259d3a --- /dev/null +++ b/module/services/accounts/code/pkg/business/audit_effect.go @@ -0,0 +1,49 @@ +package business + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +var ErrAuditIdempotencyConflict = errors.New("audit key already names a different intent") +var ErrAuditIdempotencyUnverifiable = errors.New("audit key predates verifiable effect receipts") + +// AuditEffectFingerprint names the semantic effect, not the particular attempt. +// Generated event identity, delivery time, IP and catalog schema version are not +// caller intent. Attribution and the complete payload are. JSON map keys are +// sorted by encoding/json; unsupported payload values fail rather than hashing +// a lossy substitute. The version prefix freezes this interpretation for retries. +func AuditEffectFingerprint(entry AuditEntry) (string, error) { + // The audit writer stores absent payloads as an empty JSON object. + if entry.Payload == nil { + entry.Payload = map[string]any{} + } + intent := struct { + OrgID, EventType, ActorID, ActorType, Resource, ResourceID string + ImpersonatedBy, ClientID string + IsImpersonated bool + Payload map[string]any + }{entry.OrgID, string(entry.EventType), entry.ActorID, entry.ActorType, + entry.Resource, entry.ResourceID, entry.ImpersonatedBy, entry.ClientID, + entry.IsImpersonated, entry.Payload} + raw, err := json.Marshal(intent) + if err != nil { + return "", err + } + sum := sha256.Sum256(raw) + return "v1:" + hex.EncodeToString(sum[:]), nil +} + +// A conflicting or historically unverifiable key cannot be retried as though +// the host merely lost a response. Preserve that distinction at the module API. +func moduleAuditEffectError(err error) error { + if errors.Is(err, ErrAuditIdempotencyConflict) || errors.Is(err, ErrAuditIdempotencyUnverifiable) { + return status.Error(codes.FailedPrecondition, err.Error()) + } + return status.Error(codes.Internal, err.Error()) +} diff --git a/module/services/accounts/code/pkg/business/audit_effect_test.go b/module/services/accounts/code/pkg/business/audit_effect_test.go new file mode 100644 index 000000000..18c7bb615 --- /dev/null +++ b/module/services/accounts/code/pkg/business/audit_effect_test.go @@ -0,0 +1,60 @@ +package business + +import ( + "math" + "testing" + "time" +) + +func TestAuditEffectFingerprint(t *testing.T) { + original := AuditEntry{OrgID: "tenant", ActorID: "actor", ActorType: ActorTypeUser, + EventType: "example.changed", Resource: "example", ResourceID: "entry", + Payload: map[string]any{"a": 1, "b": "value"}} + fingerprint, err := AuditEffectFingerprint(original) + if err != nil { + t.Fatal(err) + } + retry := original + retry.ID, retry.IPAddress, retry.IdempotencyKey = "new-attempt", "new-address", "other-key" + retry.CreatedAt, retry.SchemaVersion = time.Now(), 2 + retry.Payload = map[string]any{"b": "value", "a": 1} + same, err := AuditEffectFingerprint(retry) + if err != nil || fingerprint != same { + t.Fatal("attempt metadata or map order changed semantic intent") + } + for name, change := range map[string]func(*AuditEntry){ + "tenant": func(e *AuditEntry) { e.OrgID = "other" }, + "actor": func(e *AuditEntry) { e.ActorID = "other" }, + "actor kind": func(e *AuditEntry) { e.ActorType = ActorTypeAgent }, + "event": func(e *AuditEntry) { e.EventType = "example.deleted" }, + "resource": func(e *AuditEntry) { e.Resource = "other" }, + "entry": func(e *AuditEntry) { e.ResourceID = "other" }, + "impersonator": func(e *AuditEntry) { e.ImpersonatedBy = "other" }, + "impersonation": func(e *AuditEntry) { e.IsImpersonated = true }, + "client": func(e *AuditEntry) { e.ClientID = "other" }, + "payload": func(e *AuditEntry) { e.Payload = map[string]any{"a": 2, "b": "value"} }, + } { + t.Run(name, func(t *testing.T) { + changed := original + change(&changed) + got, err := AuditEffectFingerprint(changed) + if err != nil || got == fingerprint { + t.Fatalf("changed intent not distinguished: %v", err) + } + }) + } + original.Payload = nil + empty, err := AuditEffectFingerprint(original) + if err != nil { + t.Fatal(err) + } + original.Payload = map[string]any{} + explicitEmpty, err := AuditEffectFingerprint(original) + if err != nil || empty != explicitEmpty { + t.Fatal("absent and empty stored payloads differ") + } + original.Payload = map[string]any{"unsupported": math.NaN()} + if _, err := AuditEffectFingerprint(original); err == nil { + t.Fatal("unsupported payload accepted") + } +} diff --git a/module/services/accounts/code/pkg/business/introspection.go b/module/services/accounts/code/pkg/business/introspection.go index a5d826481..1076cd81a 100644 --- a/module/services/accounts/code/pkg/business/introspection.go +++ b/module/services/accounts/code/pkg/business/introspection.go @@ -173,6 +173,7 @@ var rpcDescriptions = withWorkContextConsumerDescriptions(map[string]string{ "ModuleCapabilitiesService/RequestApproval": "Open a pending approval whose resume job the module claims.", "ModuleCapabilitiesService/GetApproval": "Read one approval request on the caller's tenant.", "ModuleCapabilitiesService/CancelApproval": "Withdraw a still-open approval request.", + "ModuleCapabilitiesService/VerifyWorkContextRuntimeBoundary": "Attest current capability membership in an active host-owned runtime boundary.", "ModuleCapabilitiesService/CheckWorkContextRecordAccess": "Check exact placed-record access under current delegated viewer authority.", "ModuleCapabilitiesService/ExchangeDelegatedReadAudience": "Exchange a current signed parent through an installed read-only module binding.", "ModuleCapabilitiesService/ExchangeDelegatedOperationAudience": "Exchange a current signed parent through one installed operation binding.", @@ -180,6 +181,7 @@ var rpcDescriptions = withWorkContextConsumerDescriptions(map[string]string{ "ModuleCapabilitiesService/PlaceRecord": "Place one of the caller's own records at a scope node.", "ModuleCapabilitiesService/DeclareAuditEventTypes": "Declare the audit event types a composed module owns, in the namespaces bound to it.", "ModuleCapabilitiesService/EmitAuditEvent": "Emit a registered audit event on the tenant's spine.", + "ModuleCapabilitiesService/LookupAuditEvent": "Recover a committed audit event identity for matching intent under current emission authority.", "ModuleCapabilitiesService/ListSubjectVisibility": "List the subjects whose rows a viewer may read in a tenant.", "ModuleCapabilitiesService/FetchDatasourceBlob": "Stream a datasource file blob referenced by a change set.", "ModuleCapabilitiesService/FetchDatasourceFiles": "Stream a batch of a datasource's files at one pinned version.", 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 e41992e7e..2c309ff22 100644 --- a/module/services/accounts/code/pkg/business/module_authority_endpoint.go +++ b/module/services/accounts/code/pkg/business/module_authority_endpoint.go @@ -87,12 +87,14 @@ var moduleAuthorityReadOracles = map[string]struct{}{ // ValidateModuleAuthorityProcedures and the test beside this file hold each // entry to the rules above. var moduleAuthorityProcedures = []string{ + "/saas.accounts.v1.ModuleCapabilitiesService/VerifyWorkContextRuntimeBoundary", "/saas.accounts.v1.ModuleCapabilitiesService/AckJob", "/saas.accounts.v1.ModuleCapabilitiesService/CancelApproval", "/saas.accounts.v1.ModuleCapabilitiesService/CheckWorkContextRecordAccess", "/saas.accounts.v1.ModuleCapabilitiesService/ClaimJobs", "/saas.accounts.v1.ModuleCapabilitiesService/DeclareAuditEventTypes", "/saas.accounts.v1.ModuleCapabilitiesService/EmitAuditEvent", + "/saas.accounts.v1.ModuleCapabilitiesService/LookupAuditEvent", "/saas.accounts.v1.ModuleCapabilitiesService/EnqueueJob", "/saas.accounts.v1.ModuleCapabilitiesService/ExchangeDelegatedOperationAudience", "/saas.accounts.v1.ModuleCapabilitiesService/ExchangeDelegatedReadAudience", diff --git a/module/services/accounts/code/pkg/business/module_capabilities.go b/module/services/accounts/code/pkg/business/module_capabilities.go index 3c168efb5..0d25b9f2a 100644 --- a/module/services/accounts/code/pkg/business/module_capabilities.go +++ b/module/services/accounts/code/pkg/business/module_capabilities.go @@ -892,16 +892,60 @@ func (s *Service) enqueueApprovalResume(ctx context.Context, req *ApprovalReques // entryID must be an opaque identifier; one that is a locator is dropped rather // than stored, for the reasons opaqueAuditEntryID gives. func (s *Service) ModuleEmitAuditEvent(ctx context.Context, caller ModuleCaller, tenant, eventType, actor, solution, entryID, idempotencyKey string, fields *structpb.Struct) error { - grant, err := s.moduleGrant(caller) + entry, err := s.prepareModuleAuditEvent(ctx, caller, tenant, eventType, actor, solution, entryID, idempotencyKey, fields) + if err != nil { + return err + } + write := func(ctx context.Context) error { return s.emitEntryTx(ctx, entry) } + if tenant == "" { + err = s.store.WithControlPlane(ctx, write) + } else { + err = s.store.WithOrgTx(ctx, tenant, write) + } + if err != nil { + return moduleAuditEffectError(err) + } + return nil +} + +// ModuleLookupAuditEvent checks current emission authority and exact intent without +// writing. An absent receipt is inconclusive; it never authorizes reinvocation. +func (s *Service) ModuleLookupAuditEvent(ctx context.Context, caller ModuleCaller, tenant, eventType, actor, solution, entryID, idempotencyKey string, fields *structpb.Struct) (string, error) { + if idempotencyKey == "" { + return "", status.Error(codes.InvalidArgument, "audit receipt requires an idempotency key") + } + entry, err := s.prepareModuleAuditEvent(ctx, caller, tenant, eventType, actor, solution, entryID, idempotencyKey, fields) if err != nil { + return "", err + } + var eventID string + read := func(ctx context.Context) error { + var err error + eventID, err = s.store.LookupAuditEffect(ctx, entry) return err } + if tenant == "" { + err = s.store.WithControlPlane(ctx, read) + } else { + err = s.store.WithOrgTx(ctx, tenant, read) + } + if err != nil { + return "", moduleAuditEffectError(err) + } + return eventID, nil +} + +func (s *Service) prepareModuleAuditEvent(ctx context.Context, caller ModuleCaller, tenant, eventType, actor, solution, entryID, idempotencyKey string, fields *structpb.Struct) (AuditEntry, error) { + grant, err := s.moduleGrant(caller) + if err != nil { + return AuditEntry{}, err + } if tenant == "" { if !grant.CrossTenant { - return status.Errorf(codes.PermissionDenied, "principal %s may not emit system-scoped audit events", caller.PrincipalID) + return AuditEntry{}, status.Errorf(codes.PermissionDenied, "principal %s may not emit system-scoped audit events", caller.PrincipalID) } } else if err := authorizeTenant(caller, grant, tenant); err != nil { - return err + return AuditEntry{}, err } payload := make(map[string]any) for k, v := range fields.AsMap() { @@ -915,7 +959,7 @@ func (s *Service) ModuleEmitAuditEvent(ctx context.Context, caller ModuleCaller, // registry validation is only advisory; this is where the module's typed- // fields contract is actually enforced.) if err := s.validateModuleAuditEvent(ctx, grant, EventType(eventType), solution, payload); err != nil { - return err + return AuditEntry{}, err } // entry_id is optional on the request because the surface is shared: types // that record something about a whole solution have no entry to name. A type @@ -923,13 +967,13 @@ func (s *Service) ModuleEmitAuditEvent(ctx context.Context, caller ModuleCaller, // than at the transport, where a min_len would impose the requirement on // every type that shares the field. if entryID == "" && AuditEventRequiresEntry(EventType(eventType)) { - return status.Errorf(codes.InvalidArgument, "audit: event %q records what happened to a resource and requires entry_id", eventType) + return AuditEntry{}, status.Errorf(codes.InvalidArgument, "audit: event %q records what happened to a resource and requires entry_id", eventType) } // An empty key disables deduplication, so for a type whose emitter reports // per item and retries, accepting one guarantees a duplicate row on every // replayed response rather than risking one. if idempotencyKey == "" && AuditEventRequiresIdempotencyKey(EventType(eventType)) { - return status.Errorf(codes.InvalidArgument, "audit: event %q is emitted per item and requires idempotency_key naming the operation, so a retry collapses", eventType) + return AuditEntry{}, status.Errorf(codes.InvalidArgument, "audit: event %q is emitted per item and requires idempotency_key naming the operation, so a retry collapses", eventType) } // Resolved after the registry has accepted the event: the subject case needs a // membership read, so an emit that is going to be rejected on the request alone @@ -937,27 +981,12 @@ func (s *Service) ModuleEmitAuditEvent(ctx context.Context, caller ModuleCaller, // does so before this surface touches the store at all. actorID, actorType, err := s.resolveModuleAuditActor(ctx, caller, tenant, actor) if err != nil { - return err + return AuditEntry{}, err } entryID = opaqueAuditEntryID(ctx, entryID) - // The emission IS the operation the module requested, so a failed write must - // surface as an error — not the fire-and-forget emit(), which swallows the - // error and would report success while the event was silently lost. - emit := func(ctx context.Context) error { - entry := s.buildAuditEntry(ctx, actorID, actorType, EventType(eventType), solution, entryID, tenant, payload) - entry.IdempotencyKey = idempotencyKey - return s.emitEntryTx(ctx, entry) - } - if tenant == "" { - if err := s.store.WithControlPlane(ctx, emit); err != nil { - return status.Error(codes.Internal, err.Error()) - } - return nil - } - if err := s.store.WithOrgTx(ctx, tenant, emit); err != nil { - return status.Error(codes.Internal, err.Error()) - } - return nil + entry := s.buildAuditEntry(ctx, actorID, actorType, EventType(eventType), solution, entryID, tenant, payload) + entry.IdempotencyKey = idempotencyKey + return entry, nil } // ErrModuleAuditActorUnresolved reports that a module-attributed audit actor diff --git a/module/services/accounts/code/pkg/business/module_capabilities_idempotency_test.go b/module/services/accounts/code/pkg/business/module_capabilities_idempotency_test.go index 4d3494b7e..bd0e3d042 100644 --- a/module/services/accounts/code/pkg/business/module_capabilities_idempotency_test.go +++ b/module/services/accounts/code/pkg/business/module_capabilities_idempotency_test.go @@ -3,11 +3,14 @@ package business_test import ( + "context" "testing" "accounts/pkg/business" "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) // TestModuleEmitAuditEventDedupsRetriedTenantEmit exercises the issue #511 @@ -56,6 +59,23 @@ func TestModuleEmitAuditEventDedupsRetriedTenantEmit(t *testing.T) { require.NoError(t, emit(key), "first emit of a key must succeed") require.NoError(t, emit(key), "a retried emit of the same key must still report success") + // A key is bound to the complete effect, not just its event type. A different + // resource under that key must be refused and must not add a row. + err = svc.ModuleEmitAuditEvent(testCtx, caller, org, "saas.document.ingested", + modulePrincSvc, "sol", "entry-2", key, nil) + require.Equal(t, codes.FailedPrecondition, status.Code(err)) + require.NoError(t, emit(key), "a rejected collision must preserve the original receipt") + + // A key-only reservation from an older deployment has no provable payload. + // Retrying must not fabricate a binding or claim that this event was recorded. + legacyKey := "legacy-" + business.NewIDString() + require.NoError(t, testStore.WithOrgTx(testCtx, org, func(ctx context.Context) error { + inserted, err := testStore.ReserveAuditIdempotency(ctx, org, "saas.document.ingested", legacyKey) + require.True(t, inserted) + return err + })) + require.Equal(t, codes.FailedPrecondition, status.Code(emit(legacyKey))) + // A distinct key under the same (org, event_type) is a genuinely different // event and must be written, proving the guard dedups a retry without // suppressing real traffic. @@ -71,6 +91,43 @@ func TestModuleEmitAuditEventDedupsRetriedTenantEmit(t *testing.T) { require.Len(t, buckets, 1, "one event_type group is expected") require.EqualValues(t, 2, buckets[0].Count, "the retried emit must collapse to one row, leaving two document.ingested rows") + + lookup := func(c business.ModuleCaller, tenant, entry, key string) (string, error) { + return svc.ModuleLookupAuditEvent(testCtx, c, tenant, "saas.document.ingested", modulePrincSvc, "sol", entry, key, nil) + } + eventID, err := lookup(caller, org, "entry-1", key) + require.NoError(t, err) + require.NotEmpty(t, eventID) + rows, _, _, err := svc.QueryAuditLog(testCtx, business.AuditQuery{OrgID: org, EventType: "saas.document.ingested", PageSize: 10}) + require.NoError(t, err) + var matches int + for _, row := range rows { + if row.ID == eventID { + matches++ + } + } + require.Equal(t, 1, matches, "receipt names the committed audit row") + missing, err := lookup(caller, org, "entry-1", "missing-"+business.NewIDString()) + require.NoError(t, err) + require.Empty(t, missing) + _, err = lookup(caller, org, "entry-2", key) + require.Equal(t, codes.FailedPrecondition, status.Code(err)) + _, err = lookup(caller, org, "entry-1", legacyKey) + require.Equal(t, codes.FailedPrecondition, status.Code(err)) + _, err = lookup(caller, org, "entry-1", "") + require.Equal(t, codes.InvalidArgument, status.Code(err)) + _, err = lookup(caller, business.NewIDString(), "entry-1", key) + require.Equal(t, codes.PermissionDenied, status.Code(err)) + // Read-only lookup must not add events, even when no receipt exists. + buckets, err = svc.AggregateAuditLog(testCtx, business.AuditQuery{OrgID: org, EventType: "saas.document.ingested"}, business.AuditAggregationSpec{GroupBy: []string{"event_type"}}) + require.NoError(t, err) + require.Len(t, buckets, 1) + require.EqualValues(t, 2, buckets[0].Count) + // Narrowing the module grant takes effect before an old receipt is returned. + svc.SetModuleCapabilities(backend, backend, business.ModulePrincipalRegistry{}) + _, err = lookup(caller, org, "entry-1", key) + require.Equal(t, codes.PermissionDenied, status.Code(err)) + } // TestModuleEmitAuditEventRowKeepsItsActorAndEntry is the round trip the 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 92e249251..0c3efcdf6 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 @@ -39,6 +39,7 @@ func TestRPCPolicyInventoryIsCompleteAndClassified(t *testing.T) { "/saas.accounts.v1.IdentityService/ResolveIdentity", "/saas.accounts.v1.ModuleCapabilitiesService/ListReadableSourceCollections", "/saas.accounts.v1.ModuleCapabilitiesService/CheckWorkContextRecordAccess", + "/saas.accounts.v1.ModuleCapabilitiesService/VerifyWorkContextRuntimeBoundary", "/saas.accounts.v1.ModuleCapabilitiesService/ExchangeDelegatedReadAudience", "/saas.accounts.v1.ModuleCapabilitiesService/ExchangeDelegatedOperationAudience", "/saas.accounts.v1.ModuleCapabilitiesService/PlaceRecord", @@ -54,6 +55,7 @@ func TestRPCPolicyInventoryIsCompleteAndClassified(t *testing.T) { "/saas.accounts.v1.ModuleCapabilitiesService/CancelApproval", "/saas.accounts.v1.ModuleCapabilitiesService/DeclareAuditEventTypes", "/saas.accounts.v1.ModuleCapabilitiesService/EmitAuditEvent", + "/saas.accounts.v1.ModuleCapabilitiesService/LookupAuditEvent", "/saas.accounts.v1.ModuleCapabilitiesService/ListSubjectVisibility", "/saas.accounts.v1.ModuleCapabilitiesService/FetchDatasourceBlob", "/saas.accounts.v1.ModuleCapabilitiesService/FetchDatasourceFiles", diff --git a/module/services/accounts/code/pkg/business/runtime_boundary_verification.go b/module/services/accounts/code/pkg/business/runtime_boundary_verification.go new file mode 100644 index 000000000..a4d9aeae2 --- /dev/null +++ b/module/services/accounts/code/pkg/business/runtime_boundary_verification.go @@ -0,0 +1,48 @@ +package business + +import ( + "context" + "time" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// VerifyModuleRuntimeBoundary only reads host-owned registration state. Its +// arguments must come from a verified capability, never caller-supplied IDs. +// Both registration halves must be live and compatible, matching serving status; +// backend-only task minting does not yet make a registration attestable. +// Scope authorization remains the consuming module's responsibility. +func (s *Service) VerifyModuleRuntimeBoundary(ctx context.Context, caller ModuleCaller, tenant, audience, boundary string) error { + grant, err := s.moduleGrant(caller) + if err != nil { + return err + } + denied := status.Error(codes.PermissionDenied, "active runtime boundary required") + if tenant == "" || boundary == "" || audience == "" || audience != grant.Prefix { + return denied + } + if grant.Tenant != "" && grant.Tenant != caller.BoundOrg { + return denied + } + if err := authorizeTenant(caller, grant, tenant); err != nil { + return err + } + return s.store.WithControlPlane(ctx, func(ctx context.Context) error { + registrations, _, err := s.store.ListSolutionRegistrations(ctx, false) + if err != nil { + return status.Error(codes.Unavailable, "runtime boundary registry unavailable") + } + now := time.Now().UTC() + for _, registration := range registrations { + if registration == nil || registration.Status(now) != SolutionRegistrationActive { + continue + } + derived, err := SolutionRuntimeBoundary(registration.RuntimeBoundary, tenant) + if err == nil && derived == boundary { + return nil + } + } + return denied + }) +} diff --git a/module/services/accounts/code/pkg/business/service_catalog.go b/module/services/accounts/code/pkg/business/service_catalog.go index 53bdf524f..641689c8d 100644 --- a/module/services/accounts/code/pkg/business/service_catalog.go +++ b/module/services/accounts/code/pkg/business/service_catalog.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "regexp" "sort" "strings" @@ -14,6 +15,10 @@ import ( policyv1 "accounts/pkg/gen/saas/policy/v1" ) +// Contributions use namespace-qualified logical identifiers, as defined by the +// module composer. Descriptor policy vocabulary alone excludes those names. +var contributedPermissionVocabulary = regexp.MustCompile(`^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*:[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$`) + const ( // ServiceCatalogSchemaVersion identifies the normalized catalog contract. ServiceCatalogSchemaVersion = "saas.catalog.v1" @@ -195,7 +200,7 @@ func ValidateServiceCatalog(catalog *catalogv1.ServiceCatalog) error { return fmt.Errorf("permissions are not strictly sorted at %q", permission.GetPermission()) } previousPermission = permission.GetPermission() - if !policyVocabulary.MatchString(permission.GetPermission()) || + if (!policyVocabulary.MatchString(permission.GetPermission()) && !contributedPermissionVocabulary.MatchString(permission.GetPermission())) || permission.GetPermission() != permission.GetResource()+":"+permission.GetAction() { return fmt.Errorf("permission definition %q is not canonical", permission.GetPermission()) } diff --git a/module/services/accounts/code/pkg/business/service_catalog_test.go b/module/services/accounts/code/pkg/business/service_catalog_test.go index 5b286372f..ab6f33855 100644 --- a/module/services/accounts/code/pkg/business/service_catalog_test.go +++ b/module/services/accounts/code/pkg/business/service_catalog_test.go @@ -23,7 +23,7 @@ func TestServiceCatalogCompilation(t *testing.T) { require.Equal(t, "saas.accounts.v1", catalog.GetApiPackage()) require.Equal(t, business.ServiceVersion, catalog.GetApiVersion()) require.Len(t, catalog.GetServices(), 35) - require.Len(t, catalog.GetMethods(), 246) + require.Len(t, catalog.GetMethods(), 248) require.Len(t, catalog.GetPermissions(), 24) require.Len(t, catalog.GetEntitlements(), 5) require.Equal(t, "*:*", catalog.GetPermissions()[0].GetPermission()) @@ -40,6 +40,12 @@ func TestServiceCatalogCompilation(t *testing.T) { require.NotNil(t, method.GetPolicy()) } + for _, name := range []string{"VerifyWorkContextRuntimeBoundary", "LookupAuditEvent"} { + method := methods["/saas.accounts.v1.ModuleCapabilitiesService/"+name] + require.NotNil(t, method, name) + require.Equal(t, "internal", method.GetPolicyTier(), name) + } + readableSources := methods["/saas.accounts.v1.ModuleCapabilitiesService/ListReadableSourceCollections"] require.NotNil(t, readableSources) require.Equal(t, policyv1.Exposure_EXPOSURE_INTERNAL, readableSources.GetPolicy().GetExposure()) diff --git a/module/services/accounts/code/pkg/business/store.go b/module/services/accounts/code/pkg/business/store.go index 4832a9009..d8a3fffb4 100644 --- a/module/services/accounts/code/pkg/business/store.go +++ b/module/services/accounts/code/pkg/business/store.go @@ -538,6 +538,14 @@ type Store interface { // orgID (system-scoped emit) maps to a sentinel org so system events still // dedup. See audit_event_idempotency (migration 118). ReserveAuditIdempotency(ctx context.Context, orgID, eventType, idempotencyKey string) (bool, error) + // ReserveAuditEffect binds a stable key to its complete intent and event ID. + // It runs in the same transaction as InsertAuditEvent. A prior unbound key + // is unverifiable, not proof of the requested effect. No legacy row is filled + // from retry input. The event ID must already be assigned by the emitter. + ReserveAuditEffect(ctx context.Context, entry AuditEntry) (bool, error) + // LookupAuditEffect returns the committed event ID for matching intent, or + // empty when no binding is visible. Empty is not proof of no effect. + LookupAuditEffect(ctx context.Context, entry AuditEntry) (string, error) QueryAuditLog(ctx context.Context, q AuditQuery) ([]AuditEntry, string, int32, error) AggregateAuditLog(ctx context.Context, q AuditQuery, spec AuditAggregationSpec) ([]AuditAggregateBucket, error) SyncAuditEventTypes(ctx context.Context, defs []AuditEventDefinition) 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 cc8a9dbdc..4bfa268df 100644 --- a/module/services/accounts/code/pkg/cataloggen/authz_methods_test.go +++ b/module/services/accounts/code/pkg/cataloggen/authz_methods_test.go @@ -40,7 +40,7 @@ func TestAuthorizationCatalogCompilationAndPolicyProjection(t *testing.T) { require.Contains(t, method.GetProcedure(), "Complete") } } - require.Equal(t, 49, internalCount) + require.Equal(t, 51, internalCount) require.Equal(t, 17, failClosedCount) require.Equal(t, 2, factorAttemptCount) require.Equal( diff --git a/module/services/accounts/code/pkg/cataloggen/testdata/mesh-policy.golden.yaml b/module/services/accounts/code/pkg/cataloggen/testdata/mesh-policy.golden.yaml index 68bd38708..7d3842fb8 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 @@ -47,6 +47,7 @@ spec: - /saas.accounts.v1.ModuleCapabilitiesService/ListReadableSourceCollections - /saas.accounts.v1.ModuleCapabilitiesService/ListSubjectVisibility - /saas.accounts.v1.ModuleCapabilitiesService/ListSubscriptions + - /saas.accounts.v1.ModuleCapabilitiesService/LookupAuditEvent - /saas.accounts.v1.ModuleCapabilitiesService/MintModuleOperationContext - /saas.accounts.v1.ModuleCapabilitiesService/MintModuleRegistration - /saas.accounts.v1.ModuleCapabilitiesService/MintModuleWorkContext @@ -61,6 +62,7 @@ spec: - /saas.accounts.v1.ModuleCapabilitiesService/RequestApproval - /saas.accounts.v1.ModuleCapabilitiesService/Subscribe - /saas.accounts.v1.ModuleCapabilitiesService/Unsubscribe + - /saas.accounts.v1.ModuleCapabilitiesService/VerifyWorkContextRuntimeBoundary - /saas.accounts.v1.PermissionService/CheckAccess - /saas.accounts.v1.PermissionService/CheckPermission - /saas.accounts.v1.PermissionService/Decide 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 0952b5b9f..edeb9bb0e 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 @@ -42,6 +42,9 @@ const ( // ModuleCapabilitiesServiceExchangeDelegatedOperationAudienceProcedure is the fully-qualified name // of the ModuleCapabilitiesService's ExchangeDelegatedOperationAudience RPC. ModuleCapabilitiesServiceExchangeDelegatedOperationAudienceProcedure = "/saas.accounts.v1.ModuleCapabilitiesService/ExchangeDelegatedOperationAudience" + // ModuleCapabilitiesServiceVerifyWorkContextRuntimeBoundaryProcedure is the fully-qualified name of + // the ModuleCapabilitiesService's VerifyWorkContextRuntimeBoundary RPC. + ModuleCapabilitiesServiceVerifyWorkContextRuntimeBoundaryProcedure = "/saas.accounts.v1.ModuleCapabilitiesService/VerifyWorkContextRuntimeBoundary" // ModuleCapabilitiesServiceCheckWorkContextRecordAccessProcedure is the fully-qualified name of the // ModuleCapabilitiesService's CheckWorkContextRecordAccess RPC. ModuleCapabilitiesServiceCheckWorkContextRecordAccessProcedure = "/saas.accounts.v1.ModuleCapabilitiesService/CheckWorkContextRecordAccess" @@ -84,6 +87,9 @@ const ( // ModuleCapabilitiesServiceEmitAuditEventProcedure is the fully-qualified name of the // ModuleCapabilitiesService's EmitAuditEvent RPC. ModuleCapabilitiesServiceEmitAuditEventProcedure = "/saas.accounts.v1.ModuleCapabilitiesService/EmitAuditEvent" + // ModuleCapabilitiesServiceLookupAuditEventProcedure is the fully-qualified name of the + // ModuleCapabilitiesService's LookupAuditEvent RPC. + ModuleCapabilitiesServiceLookupAuditEventProcedure = "/saas.accounts.v1.ModuleCapabilitiesService/LookupAuditEvent" // ModuleCapabilitiesServiceDeclareAuditEventTypesProcedure is the fully-qualified name of the // ModuleCapabilitiesService's DeclareAuditEventTypes RPC. ModuleCapabilitiesServiceDeclareAuditEventTypesProcedure = "/saas.accounts.v1.ModuleCapabilitiesService/DeclareAuditEventTypes" @@ -138,6 +144,9 @@ type ModuleCapabilitiesServiceClient interface { // of a current parent context and exchanges only an installed operation's // invoke scopes, or its read-only receipt-lookup subset. ExchangeDelegatedOperationAudience(context.Context, *connect.Request[v1.ModuleExchangeDelegatedOperationAudienceRequest]) (*connect.Response[v1.IssuedWorkContext], error) + // Attests a current capability under an active host-owned solution boundary. + // This read grants no authority and does not expose the registration seed. + VerifyWorkContextRuntimeBoundary(context.Context, *connect.Request[v1.VerifyWorkContextRuntimeBoundaryRequest]) (*connect.Response[v1.VerifyWorkContextRuntimeBoundaryResponse], error) // Checks current owner and every delegated actor against true record placement, // intersected with the verified capability's attenuated resource/action scope. CheckWorkContextRecordAccess(context.Context, *connect.Request[v1.CheckWorkContextRecordAccessRequest]) (*connect.Response[v1.CheckWorkContextRecordAccessResponse], error) @@ -173,6 +182,10 @@ 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) + // Read-only recovery for the complete original intent. Requires a nonempty + // idempotency_key; same authority as EmitAuditEvent. No retry authorization is + // implied by an absent receipt. Old unbound keys return FAILED_PRECONDITION. + LookupAuditEvent(context.Context, *connect.Request[v1.ModuleEmitAuditEventRequest]) (*connect.Response[v1.ModuleLookupAuditEventResponse], 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) @@ -263,6 +276,12 @@ func NewModuleCapabilitiesServiceClient(httpClient connect.HTTPClient, baseURL s connect.WithSchema(moduleCapabilitiesServiceMethods.ByName("ExchangeDelegatedOperationAudience")), connect.WithClientOptions(opts...), ), + verifyWorkContextRuntimeBoundary: connect.NewClient[v1.VerifyWorkContextRuntimeBoundaryRequest, v1.VerifyWorkContextRuntimeBoundaryResponse]( + httpClient, + baseURL+ModuleCapabilitiesServiceVerifyWorkContextRuntimeBoundaryProcedure, + connect.WithSchema(moduleCapabilitiesServiceMethods.ByName("VerifyWorkContextRuntimeBoundary")), + connect.WithClientOptions(opts...), + ), checkWorkContextRecordAccess: connect.NewClient[v1.CheckWorkContextRecordAccessRequest, v1.CheckWorkContextRecordAccessResponse]( httpClient, baseURL+ModuleCapabilitiesServiceCheckWorkContextRecordAccessProcedure, @@ -347,6 +366,12 @@ func NewModuleCapabilitiesServiceClient(httpClient connect.HTTPClient, baseURL s connect.WithSchema(moduleCapabilitiesServiceMethods.ByName("EmitAuditEvent")), connect.WithClientOptions(opts...), ), + lookupAuditEvent: connect.NewClient[v1.ModuleEmitAuditEventRequest, v1.ModuleLookupAuditEventResponse]( + httpClient, + baseURL+ModuleCapabilitiesServiceLookupAuditEventProcedure, + connect.WithSchema(moduleCapabilitiesServiceMethods.ByName("LookupAuditEvent")), + connect.WithClientOptions(opts...), + ), declareAuditEventTypes: connect.NewClient[v1.ModuleDeclareAuditEventTypesRequest, v1.ModuleDeclareAuditEventTypesResponse]( httpClient, baseURL+ModuleCapabilitiesServiceDeclareAuditEventTypesProcedure, @@ -438,6 +463,7 @@ func NewModuleCapabilitiesServiceClient(httpClient connect.HTTPClient, baseURL s type moduleCapabilitiesServiceClient struct { exchangeDelegatedReadAudience *connect.Client[v1.ModuleExchangeDelegatedReadAudienceRequest, v1.IssuedWorkContext] exchangeDelegatedOperationAudience *connect.Client[v1.ModuleExchangeDelegatedOperationAudienceRequest, v1.IssuedWorkContext] + verifyWorkContextRuntimeBoundary *connect.Client[v1.VerifyWorkContextRuntimeBoundaryRequest, v1.VerifyWorkContextRuntimeBoundaryResponse] checkWorkContextRecordAccess *connect.Client[v1.CheckWorkContextRecordAccessRequest, v1.CheckWorkContextRecordAccessResponse] listReadableSourceCollections *connect.Client[v1.ListReadableSourceCollectionsRequest, v1.ListReadableSourceCollectionsResponse] placeRecord *connect.Client[v1.ModulePlaceRecordRequest, v1.ModulePlaceRecordResponse] @@ -452,6 +478,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] + lookupAuditEvent *connect.Client[v1.ModuleEmitAuditEventRequest, v1.ModuleLookupAuditEventResponse] declareAuditEventTypes *connect.Client[v1.ModuleDeclareAuditEventTypesRequest, v1.ModuleDeclareAuditEventTypesResponse] listSubjectVisibility *connect.Client[v1.ModuleListSubjectVisibilityRequest, v1.ModuleListSubjectVisibilityResponse] fetchDatasourceBlob *connect.Client[v1.FetchDatasourceBlobRequest, v1.FetchDatasourceBlobChunk] @@ -480,6 +507,12 @@ func (c *moduleCapabilitiesServiceClient) ExchangeDelegatedOperationAudience(ctx return c.exchangeDelegatedOperationAudience.CallUnary(ctx, req) } +// VerifyWorkContextRuntimeBoundary calls +// saas.accounts.v1.ModuleCapabilitiesService.VerifyWorkContextRuntimeBoundary. +func (c *moduleCapabilitiesServiceClient) VerifyWorkContextRuntimeBoundary(ctx context.Context, req *connect.Request[v1.VerifyWorkContextRuntimeBoundaryRequest]) (*connect.Response[v1.VerifyWorkContextRuntimeBoundaryResponse], error) { + return c.verifyWorkContextRuntimeBoundary.CallUnary(ctx, req) +} + // CheckWorkContextRecordAccess calls // saas.accounts.v1.ModuleCapabilitiesService.CheckWorkContextRecordAccess. func (c *moduleCapabilitiesServiceClient) CheckWorkContextRecordAccess(ctx context.Context, req *connect.Request[v1.CheckWorkContextRecordAccessRequest]) (*connect.Response[v1.CheckWorkContextRecordAccessResponse], error) { @@ -552,6 +585,11 @@ func (c *moduleCapabilitiesServiceClient) EmitAuditEvent(ctx context.Context, re return c.emitAuditEvent.CallUnary(ctx, req) } +// LookupAuditEvent calls saas.accounts.v1.ModuleCapabilitiesService.LookupAuditEvent. +func (c *moduleCapabilitiesServiceClient) LookupAuditEvent(ctx context.Context, req *connect.Request[v1.ModuleEmitAuditEventRequest]) (*connect.Response[v1.ModuleLookupAuditEventResponse], error) { + return c.lookupAuditEvent.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) @@ -637,6 +675,9 @@ type ModuleCapabilitiesServiceHandler interface { // of a current parent context and exchanges only an installed operation's // invoke scopes, or its read-only receipt-lookup subset. ExchangeDelegatedOperationAudience(context.Context, *connect.Request[v1.ModuleExchangeDelegatedOperationAudienceRequest]) (*connect.Response[v1.IssuedWorkContext], error) + // Attests a current capability under an active host-owned solution boundary. + // This read grants no authority and does not expose the registration seed. + VerifyWorkContextRuntimeBoundary(context.Context, *connect.Request[v1.VerifyWorkContextRuntimeBoundaryRequest]) (*connect.Response[v1.VerifyWorkContextRuntimeBoundaryResponse], error) // Checks current owner and every delegated actor against true record placement, // intersected with the verified capability's attenuated resource/action scope. CheckWorkContextRecordAccess(context.Context, *connect.Request[v1.CheckWorkContextRecordAccessRequest]) (*connect.Response[v1.CheckWorkContextRecordAccessResponse], error) @@ -672,6 +713,10 @@ 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) + // Read-only recovery for the complete original intent. Requires a nonempty + // idempotency_key; same authority as EmitAuditEvent. No retry authorization is + // implied by an absent receipt. Old unbound keys return FAILED_PRECONDITION. + LookupAuditEvent(context.Context, *connect.Request[v1.ModuleEmitAuditEventRequest]) (*connect.Response[v1.ModuleLookupAuditEventResponse], 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) @@ -758,6 +803,12 @@ func NewModuleCapabilitiesServiceHandler(svc ModuleCapabilitiesServiceHandler, o connect.WithSchema(moduleCapabilitiesServiceMethods.ByName("ExchangeDelegatedOperationAudience")), connect.WithHandlerOptions(opts...), ) + moduleCapabilitiesServiceVerifyWorkContextRuntimeBoundaryHandler := connect.NewUnaryHandler( + ModuleCapabilitiesServiceVerifyWorkContextRuntimeBoundaryProcedure, + svc.VerifyWorkContextRuntimeBoundary, + connect.WithSchema(moduleCapabilitiesServiceMethods.ByName("VerifyWorkContextRuntimeBoundary")), + connect.WithHandlerOptions(opts...), + ) moduleCapabilitiesServiceCheckWorkContextRecordAccessHandler := connect.NewUnaryHandler( ModuleCapabilitiesServiceCheckWorkContextRecordAccessProcedure, svc.CheckWorkContextRecordAccess, @@ -842,6 +893,12 @@ func NewModuleCapabilitiesServiceHandler(svc ModuleCapabilitiesServiceHandler, o connect.WithSchema(moduleCapabilitiesServiceMethods.ByName("EmitAuditEvent")), connect.WithHandlerOptions(opts...), ) + moduleCapabilitiesServiceLookupAuditEventHandler := connect.NewUnaryHandler( + ModuleCapabilitiesServiceLookupAuditEventProcedure, + svc.LookupAuditEvent, + connect.WithSchema(moduleCapabilitiesServiceMethods.ByName("LookupAuditEvent")), + connect.WithHandlerOptions(opts...), + ) moduleCapabilitiesServiceDeclareAuditEventTypesHandler := connect.NewUnaryHandler( ModuleCapabilitiesServiceDeclareAuditEventTypesProcedure, svc.DeclareAuditEventTypes, @@ -932,6 +989,8 @@ func NewModuleCapabilitiesServiceHandler(svc ModuleCapabilitiesServiceHandler, o moduleCapabilitiesServiceExchangeDelegatedReadAudienceHandler.ServeHTTP(w, r) case ModuleCapabilitiesServiceExchangeDelegatedOperationAudienceProcedure: moduleCapabilitiesServiceExchangeDelegatedOperationAudienceHandler.ServeHTTP(w, r) + case ModuleCapabilitiesServiceVerifyWorkContextRuntimeBoundaryProcedure: + moduleCapabilitiesServiceVerifyWorkContextRuntimeBoundaryHandler.ServeHTTP(w, r) case ModuleCapabilitiesServiceCheckWorkContextRecordAccessProcedure: moduleCapabilitiesServiceCheckWorkContextRecordAccessHandler.ServeHTTP(w, r) case ModuleCapabilitiesServiceListReadableSourceCollectionsProcedure: @@ -960,6 +1019,8 @@ func NewModuleCapabilitiesServiceHandler(svc ModuleCapabilitiesServiceHandler, o moduleCapabilitiesServiceCancelApprovalHandler.ServeHTTP(w, r) case ModuleCapabilitiesServiceEmitAuditEventProcedure: moduleCapabilitiesServiceEmitAuditEventHandler.ServeHTTP(w, r) + case ModuleCapabilitiesServiceLookupAuditEventProcedure: + moduleCapabilitiesServiceLookupAuditEventHandler.ServeHTTP(w, r) case ModuleCapabilitiesServiceDeclareAuditEventTypesProcedure: moduleCapabilitiesServiceDeclareAuditEventTypesHandler.ServeHTTP(w, r) case ModuleCapabilitiesServiceListSubjectVisibilityProcedure: @@ -1005,6 +1066,10 @@ func (UnimplementedModuleCapabilitiesServiceHandler) ExchangeDelegatedOperationA return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedOperationAudience is not implemented")) } +func (UnimplementedModuleCapabilitiesServiceHandler) VerifyWorkContextRuntimeBoundary(context.Context, *connect.Request[v1.VerifyWorkContextRuntimeBoundaryRequest]) (*connect.Response[v1.VerifyWorkContextRuntimeBoundaryResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.ModuleCapabilitiesService.VerifyWorkContextRuntimeBoundary is not implemented")) +} + func (UnimplementedModuleCapabilitiesServiceHandler) CheckWorkContextRecordAccess(context.Context, *connect.Request[v1.CheckWorkContextRecordAccessRequest]) (*connect.Response[v1.CheckWorkContextRecordAccessResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.ModuleCapabilitiesService.CheckWorkContextRecordAccess is not implemented")) } @@ -1061,6 +1126,10 @@ func (UnimplementedModuleCapabilitiesServiceHandler) EmitAuditEvent(context.Cont return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.ModuleCapabilitiesService.EmitAuditEvent is not implemented")) } +func (UnimplementedModuleCapabilitiesServiceHandler) LookupAuditEvent(context.Context, *connect.Request[v1.ModuleEmitAuditEventRequest]) (*connect.Response[v1.ModuleLookupAuditEventResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.ModuleCapabilitiesService.LookupAuditEvent 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/module_capabilities.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/module_capabilities.pb.go index 31668aeb0..7b419f610 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 @@ -1664,6 +1664,52 @@ func (x *ModuleCancelApprovalRequest) GetReason() string { return "" } +// Receipt for the exact requested audit intent, under current emission authority. +// Empty event_id means no receipt was visible, not proof that nothing happened. +type ModuleLookupAuditEventResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + EventId string `protobuf:"bytes,1,opt,name=event_id,json=eventId,proto3" json:"event_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModuleLookupAuditEventResponse) Reset() { + *x = ModuleLookupAuditEventResponse{} + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModuleLookupAuditEventResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModuleLookupAuditEventResponse) ProtoMessage() {} + +func (x *ModuleLookupAuditEventResponse) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[19] + 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 ModuleLookupAuditEventResponse.ProtoReflect.Descriptor instead. +func (*ModuleLookupAuditEventResponse) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{19} +} + +func (x *ModuleLookupAuditEventResponse) GetEventId() string { + if x != nil { + return x.EventId + } + return "" +} + // ModuleEmitAuditEventRequest emits a registered audit event onto the tenant's // single audit spine. event_type must be registered in the code-owned catalog // (unregistered types are rejected, not stored free-form). solution names the @@ -1679,7 +1725,8 @@ type ModuleEmitAuditEventRequest struct { Fields *structpb.Struct `protobuf:"bytes,6,opt,name=fields,proto3" json:"fields,omitempty"` // idempotency_key deduplicates retried emits: two emits with the same // (tenant, event_type, idempotency_key) collapse to a single audit row, and - // the duplicate returns success without writing again. Empty disables + // an identical intent returns success without writing again. Changed intent or + // an older key without recorded intent returns FAILED_PRECONDITION. Empty disables // deduplication (every emit is a distinct event). Scoped by tenant so one // tenant's keys can never suppress another's events. IdempotencyKey string `protobuf:"bytes,7,opt,name=idempotency_key,json=idempotencyKey,proto3" json:"idempotency_key,omitempty"` @@ -1689,7 +1736,7 @@ type ModuleEmitAuditEventRequest struct { func (x *ModuleEmitAuditEventRequest) Reset() { *x = ModuleEmitAuditEventRequest{} - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[19] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1701,7 +1748,7 @@ func (x *ModuleEmitAuditEventRequest) String() string { func (*ModuleEmitAuditEventRequest) ProtoMessage() {} func (x *ModuleEmitAuditEventRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[19] + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1714,7 +1761,7 @@ func (x *ModuleEmitAuditEventRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleEmitAuditEventRequest.ProtoReflect.Descriptor instead. func (*ModuleEmitAuditEventRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{19} + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{20} } func (x *ModuleEmitAuditEventRequest) GetTenant() string { @@ -1785,7 +1832,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[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1797,7 +1844,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[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1810,7 +1857,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{21} } func (x *ModuleAuditFieldDeclaration) GetName() string { @@ -1863,7 +1910,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[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1875,7 +1922,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[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1888,7 +1935,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{22} } func (x *ModuleAuditEventTypeDeclaration) GetType() string { @@ -1938,7 +1985,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[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1950,7 +1997,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[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1963,7 +2010,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{23} } func (x *ModuleDeclareAuditEventTypesRequest) GetPrefix() string { @@ -1993,7 +2040,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[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2005,7 +2052,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[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2018,7 +2065,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{24} } func (x *ModuleDeclareAuditEventTypesResponse) GetAdmittedTypes() []string { @@ -2052,7 +2099,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[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2064,7 +2111,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[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2077,7 +2124,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{25} } func (x *SubjectVisibilityGrant) GetVisibleSubjectId() string { @@ -2115,7 +2162,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[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2127,7 +2174,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[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2140,7 +2187,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{26} } func (x *ModuleListSubjectVisibilityRequest) GetTenant() string { @@ -2179,7 +2226,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[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2191,7 +2238,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[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2204,7 +2251,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{27} } func (x *ModuleListSubjectVisibilityResponse) GetGrants() []*SubjectVisibilityGrant { @@ -2232,7 +2279,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[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2244,7 +2291,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[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2257,7 +2304,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{28} } func (x *FetchDatasourceBlobRequest) GetSourceId() string { @@ -2289,7 +2336,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[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2301,7 +2348,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[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2314,7 +2361,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{29} } func (x *FetchDatasourceBlobChunk) GetData() []byte { @@ -2352,7 +2399,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[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2364,7 +2411,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[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2377,7 +2424,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{30} } func (x *DatasourceFileRef) GetItemId() string { @@ -2413,7 +2460,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[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2425,7 +2472,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[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2438,7 +2485,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{31} } func (x *FetchDatasourceFilesRequest) GetSourceId() string { @@ -2480,7 +2527,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[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2492,7 +2539,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[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2505,7 +2552,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{32} } func (x *DatasourceProvenance) GetSourceId() string { @@ -2571,7 +2618,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[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2583,7 +2630,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[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2596,7 +2643,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{33} } func (x *DatasourceItemReaders) GetBasis() DatasourceItemReadersBasis { @@ -2657,7 +2704,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[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2669,7 +2716,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[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2682,7 +2729,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{34} } func (x *DatasourceFileHeader) GetProvenance() *DatasourceProvenance { @@ -2736,7 +2783,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[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2748,7 +2795,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[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2761,7 +2808,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{35} } func (x *FetchDatasourceFilesFrame) GetFrame() isFetchDatasourceFilesFrame_Frame { @@ -2820,7 +2867,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[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2832,7 +2879,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[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2845,7 +2892,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{36} } func (x *ModulePublishEventRequest) GetTenant() string { @@ -2875,7 +2922,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[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2887,7 +2934,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[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2900,7 +2947,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{37} } func (x *ModulePublishEventResponse) GetEventId() string { @@ -2926,7 +2973,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[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2938,7 +2985,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[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2951,7 +2998,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{38} } func (x *ModuleSubscribeRequest) GetTypePattern() string { @@ -2990,7 +3037,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[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3002,7 +3049,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[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3015,7 +3062,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{39} } func (x *ModuleSubscription) GetId() string { @@ -3069,7 +3116,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[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3081,7 +3128,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[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3094,7 +3141,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{40} } func (x *ModuleSubscribeResponse) GetSubscription() *ModuleSubscription { @@ -3114,7 +3161,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[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3126,7 +3173,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[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3139,7 +3186,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{41} } func (x *ModuleUnsubscribeRequest) GetSubscriptionId() string { @@ -3159,7 +3206,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[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3171,7 +3218,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[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3184,7 +3231,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{42} } type ModuleListSubscriptionsResponse struct { @@ -3196,7 +3243,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[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3208,7 +3255,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[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3221,7 +3268,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{43} } func (x *ModuleListSubscriptionsResponse) GetSubscriptions() []*ModuleSubscription { @@ -3246,7 +3293,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[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3258,7 +3305,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[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3271,7 +3318,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{44} } func (x *ModuleReplayEventsRequest) GetTenant() string { @@ -3306,7 +3353,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[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3318,7 +3365,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[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3331,7 +3378,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{45} } func (x *ModuleReplayEventsResponse) GetRedelivered() int32 { @@ -3375,7 +3422,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[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3387,7 +3434,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[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3400,7 +3447,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{46} } func (x *ReadableSourceCollection) GetSourceId() string { @@ -3494,7 +3541,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[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3506,7 +3553,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[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3519,7 +3566,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{47} } func (x *ReadableCollectionGrant) GetSubjectLabel() string { @@ -3592,7 +3639,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[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3604,7 +3651,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[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3617,7 +3664,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{48} } func (x *CollectionSyncProvenance) GetStage() SourceSyncStage { @@ -3680,7 +3727,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[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3692,7 +3739,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[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3705,7 +3752,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{49} } func (x *ListReadableSourceCollectionsRequest) GetPageSize() int32 { @@ -3732,7 +3779,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[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3744,7 +3791,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[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3757,7 +3804,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{50} } func (x *ListReadableSourceCollectionsResponse) GetCollections() []*ReadableSourceCollection { @@ -3801,7 +3848,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[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3813,7 +3860,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[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3826,7 +3873,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{51} } func (x *ModulePlaceRecordRequest) GetTenant() string { @@ -3885,7 +3932,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[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3897,7 +3944,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[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3910,7 +3957,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{52} } func (x *ModulePlaceRecordResponse) GetNodeId() string { @@ -3920,6 +3967,104 @@ func (x *ModulePlaceRecordResponse) GetNodeId() string { return "" } +// Read-only attestation; identity and boundary come exclusively from this signed +// context. The caller separately authenticates as its consuming module. +type VerifyWorkContextRuntimeBoundaryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ForwardedWorkContextToken string `protobuf:"bytes,1,opt,name=forwarded_work_context_token,json=forwardedWorkContextToken,proto3" json:"forwarded_work_context_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VerifyWorkContextRuntimeBoundaryRequest) Reset() { + *x = VerifyWorkContextRuntimeBoundaryRequest{} + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[53] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VerifyWorkContextRuntimeBoundaryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VerifyWorkContextRuntimeBoundaryRequest) ProtoMessage() {} + +func (x *VerifyWorkContextRuntimeBoundaryRequest) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[53] + 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 VerifyWorkContextRuntimeBoundaryRequest.ProtoReflect.Descriptor instead. +func (*VerifyWorkContextRuntimeBoundaryRequest) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{53} +} + +func (x *VerifyWorkContextRuntimeBoundaryRequest) GetForwardedWorkContextToken() string { + if x != nil { + return x.ForwardedWorkContextToken + } + return "" +} + +type VerifyWorkContextRuntimeBoundaryResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + TenantId string `protobuf:"bytes,1,opt,name=tenant_id,json=tenantId,proto3" json:"tenant_id,omitempty"` + BoundaryId string `protobuf:"bytes,2,opt,name=boundary_id,json=boundaryId,proto3" json:"boundary_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VerifyWorkContextRuntimeBoundaryResponse) Reset() { + *x = VerifyWorkContextRuntimeBoundaryResponse{} + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[54] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VerifyWorkContextRuntimeBoundaryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VerifyWorkContextRuntimeBoundaryResponse) ProtoMessage() {} + +func (x *VerifyWorkContextRuntimeBoundaryResponse) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_module_capabilities_proto_msgTypes[54] + 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 VerifyWorkContextRuntimeBoundaryResponse.ProtoReflect.Descriptor instead. +func (*VerifyWorkContextRuntimeBoundaryResponse) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP(), []int{54} +} + +func (x *VerifyWorkContextRuntimeBoundaryResponse) GetTenantId() string { + if x != nil { + return x.TenantId + } + return "" +} + +func (x *VerifyWorkContextRuntimeBoundaryResponse) GetBoundaryId() string { + if x != nil { + return x.BoundaryId + } + return "" +} + // Exact-record delegated oracle. Tenant and subjects come only from the signed // forwarded Work Context; a caller cannot provide a scope path or principal. type CheckWorkContextRecordAccessRequest struct { @@ -3933,7 +4078,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) } @@ -3945,7 +4090,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 { @@ -3958,7 +4103,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 { @@ -3994,7 +4139,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) } @@ -4006,7 +4151,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 { @@ -4019,7 +4164,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 { @@ -4048,7 +4193,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) } @@ -4060,7 +4205,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 { @@ -4073,7 +4218,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 { @@ -4129,7 +4274,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) } @@ -4141,7 +4286,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 { @@ -4154,7 +4299,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 { @@ -4298,7 +4443,9 @@ const file_saas_accounts_v1_module_capabilities_proto_rawDesc = "" + "\x06tenant\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x06tenant\x12)\n" + "\vapproval_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\n" + "approvalId\x12 \n" + - "\x06reason\x18\x03 \x01(\tB\b\xbaH\x05r\x03\x18\x80\bR\x06reason\"\xb3\x02\n" + + "\x06reason\x18\x03 \x01(\tB\b\xbaH\x05r\x03\x18\x80\bR\x06reason\";\n" + + "\x1eModuleLookupAuditEventResponse\x12\x19\n" + + "\bevent_id\x18\x01 \x01(\tR\aeventId\"\xb3\x02\n" + "\x1bModuleEmitAuditEventRequest\x12\x16\n" + "\x06tenant\x18\x01 \x01(\tR\x06tenant\x12)\n" + "\n" + @@ -4469,7 +4616,13 @@ const file_saas_accounts_v1_module_capabilities_proto_rawDesc = "" + "\xbaH\ar\x05\x10\x01\x18\x80\x04R\n" + "resourceId\">\n" + "\x19ModulePlaceRecordResponse\x12!\n" + - "\anode_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x06nodeId\"\xa7\x01\n" + + "\anode_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x06nodeId\"w\n" + + "'VerifyWorkContextRuntimeBoundaryRequest\x12L\n" + + "\x1cforwarded_work_context_token\x18\x01 \x01(\tB\v\xbaH\br\x06\x10\x01\x18\x80\x80\x02R\x19forwardedWorkContextToken\"h\n" + + "(VerifyWorkContextRuntimeBoundaryResponse\x12\x1b\n" + + "\ttenant_id\x18\x01 \x01(\tR\btenantId\x12\x1f\n" + + "\vboundary_id\x18\x02 \x01(\tR\n" + + "boundaryId\"\xa7\x01\n" + "#CheckWorkContextRecordAccessRequest\x12/\n" + "\rresource_type\x18\x01 \x01(\tB\n" + "\xbaH\ar\x05\x10\x01\x18\x80\x02R\fresourceType\x12+\n" + @@ -4522,12 +4675,13 @@ const file_saas_accounts_v1_module_capabilities_proto_rawDesc = "" + "\x1cMETADATA_DISCLOSURE_WITHHELD\x10\x02*\\\n" + "\x0fSourceSyncStage\x12!\n" + "\x1dSOURCE_SYNC_STAGE_UNSPECIFIED\x10\x00\x12&\n" + - "\"SOURCE_SYNC_STAGE_CHANGES_ENQUEUED\x10\x012\xd1#\n" + + "\"SOURCE_SYNC_STAGE_CHANGES_ENQUEUED\x10\x012\x97&\n" + "\x19ModuleCapabilitiesService\x12\xc5\x01\n" + "\x1dExchangeDelegatedReadAudience\x12<.saas.accounts.v1.ModuleExchangeDelegatedReadAudienceRequest\x1a#.saas.accounts.v1.IssuedWorkContext\"A\xc2\xf3\x18=\b\x03\x10\x010\x01:+\n" + "'saas.module.delegated_audience_exchange\x10\x04@\x01H\aP\x04X\x04`\x01\x12\xcf\x01\n" + "\"ExchangeDelegatedOperationAudience\x12A.saas.accounts.v1.ModuleExchangeDelegatedOperationAudienceRequest\x1a#.saas.accounts.v1.IssuedWorkContext\"A\xc2\xf3\x18=\b\x03\x10\x010\x01:+\n" + - "'saas.module.delegated_audience_exchange\x10\x04@\x01H\aP\x04X\x04`\x01\x12\xa7\x01\n" + + "'saas.module.delegated_audience_exchange\x10\x04@\x01H\aP\x04X\x04`\x01\x12\xb3\x01\n" + + " VerifyWorkContextRuntimeBoundary\x129.saas.accounts.v1.VerifyWorkContextRuntimeBoundaryRequest\x1a:.saas.accounts.v1.VerifyWorkContextRuntimeBoundaryResponse\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x04X\x03`\x01\x12\xa7\x01\n" + "\x1cCheckWorkContextRecordAccess\x125.saas.accounts.v1.CheckWorkContextRecordAccessRequest\x1a6.saas.accounts.v1.CheckWorkContextRecordAccessResponse\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01\x12\xaa\x01\n" + "\x1dListReadableSourceCollections\x126.saas.accounts.v1.ListReadableSourceCollectionsRequest\x1a7.saas.accounts.v1.ListReadableSourceCollectionsResponse\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01\x12\x80\x01\n" + "\vPlaceRecord\x12*.saas.accounts.v1.ModulePlaceRecordRequest\x1a+.saas.accounts.v1.ModulePlaceRecordResponse\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01\x12}\n" + @@ -4544,7 +4698,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\x8d\x01\n" + + "\x10LookupAuditEvent\x12-.saas.accounts.v1.ModuleEmitAuditEventRequest\x1a0.saas.accounts.v1.ModuleLookupAuditEventResponse\"\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" + @@ -4580,7 +4735,7 @@ func file_saas_accounts_v1_module_capabilities_proto_rawDescGZIP() []byte { } var file_saas_accounts_v1_module_capabilities_proto_enumTypes = make([]protoimpl.EnumInfo, 6) -var file_saas_accounts_v1_module_capabilities_proto_msgTypes = make([]protoimpl.MessageInfo, 56) +var file_saas_accounts_v1_module_capabilities_proto_msgTypes = make([]protoimpl.MessageInfo, 59) var file_saas_accounts_v1_module_capabilities_proto_goTypes = []any{ (ModuleAuditFieldKind)(0), // 0: saas.accounts.v1.ModuleAuditFieldKind (ModuleAuditEventVisibility)(0), // 1: saas.accounts.v1.ModuleAuditEventVisibility @@ -4607,179 +4762,186 @@ var file_saas_accounts_v1_module_capabilities_proto_goTypes = []any{ (*ModuleGetApprovalRequest)(nil), // 22: saas.accounts.v1.ModuleGetApprovalRequest (*ModuleApproval)(nil), // 23: saas.accounts.v1.ModuleApproval (*ModuleCancelApprovalRequest)(nil), // 24: saas.accounts.v1.ModuleCancelApprovalRequest - (*ModuleEmitAuditEventRequest)(nil), // 25: saas.accounts.v1.ModuleEmitAuditEventRequest - (*ModuleAuditFieldDeclaration)(nil), // 26: saas.accounts.v1.ModuleAuditFieldDeclaration - (*ModuleAuditEventTypeDeclaration)(nil), // 27: saas.accounts.v1.ModuleAuditEventTypeDeclaration - (*ModuleDeclareAuditEventTypesRequest)(nil), // 28: saas.accounts.v1.ModuleDeclareAuditEventTypesRequest - (*ModuleDeclareAuditEventTypesResponse)(nil), // 29: saas.accounts.v1.ModuleDeclareAuditEventTypesResponse - (*SubjectVisibilityGrant)(nil), // 30: saas.accounts.v1.SubjectVisibilityGrant - (*ModuleListSubjectVisibilityRequest)(nil), // 31: saas.accounts.v1.ModuleListSubjectVisibilityRequest - (*ModuleListSubjectVisibilityResponse)(nil), // 32: saas.accounts.v1.ModuleListSubjectVisibilityResponse - (*FetchDatasourceBlobRequest)(nil), // 33: saas.accounts.v1.FetchDatasourceBlobRequest - (*FetchDatasourceBlobChunk)(nil), // 34: saas.accounts.v1.FetchDatasourceBlobChunk - (*DatasourceFileRef)(nil), // 35: saas.accounts.v1.DatasourceFileRef - (*FetchDatasourceFilesRequest)(nil), // 36: saas.accounts.v1.FetchDatasourceFilesRequest - (*DatasourceProvenance)(nil), // 37: saas.accounts.v1.DatasourceProvenance - (*DatasourceItemReaders)(nil), // 38: saas.accounts.v1.DatasourceItemReaders - (*DatasourceFileHeader)(nil), // 39: saas.accounts.v1.DatasourceFileHeader - (*FetchDatasourceFilesFrame)(nil), // 40: saas.accounts.v1.FetchDatasourceFilesFrame - (*ModulePublishEventRequest)(nil), // 41: saas.accounts.v1.ModulePublishEventRequest - (*ModulePublishEventResponse)(nil), // 42: saas.accounts.v1.ModulePublishEventResponse - (*ModuleSubscribeRequest)(nil), // 43: saas.accounts.v1.ModuleSubscribeRequest - (*ModuleSubscription)(nil), // 44: saas.accounts.v1.ModuleSubscription - (*ModuleSubscribeResponse)(nil), // 45: saas.accounts.v1.ModuleSubscribeResponse - (*ModuleUnsubscribeRequest)(nil), // 46: saas.accounts.v1.ModuleUnsubscribeRequest - (*ModuleListSubscriptionsRequest)(nil), // 47: saas.accounts.v1.ModuleListSubscriptionsRequest - (*ModuleListSubscriptionsResponse)(nil), // 48: saas.accounts.v1.ModuleListSubscriptionsResponse - (*ModuleReplayEventsRequest)(nil), // 49: saas.accounts.v1.ModuleReplayEventsRequest - (*ModuleReplayEventsResponse)(nil), // 50: saas.accounts.v1.ModuleReplayEventsResponse - (*ReadableSourceCollection)(nil), // 51: saas.accounts.v1.ReadableSourceCollection - (*ReadableCollectionGrant)(nil), // 52: saas.accounts.v1.ReadableCollectionGrant - (*CollectionSyncProvenance)(nil), // 53: saas.accounts.v1.CollectionSyncProvenance - (*ListReadableSourceCollectionsRequest)(nil), // 54: saas.accounts.v1.ListReadableSourceCollectionsRequest - (*ListReadableSourceCollectionsResponse)(nil), // 55: saas.accounts.v1.ListReadableSourceCollectionsResponse - (*ModulePlaceRecordRequest)(nil), // 56: saas.accounts.v1.ModulePlaceRecordRequest - (*ModulePlaceRecordResponse)(nil), // 57: saas.accounts.v1.ModulePlaceRecordResponse - (*CheckWorkContextRecordAccessRequest)(nil), // 58: saas.accounts.v1.CheckWorkContextRecordAccessRequest - (*CheckWorkContextRecordAccessResponse)(nil), // 59: saas.accounts.v1.CheckWorkContextRecordAccessResponse - (*ModuleExchangeDelegatedReadAudienceRequest)(nil), // 60: saas.accounts.v1.ModuleExchangeDelegatedReadAudienceRequest - (*ModuleExchangeDelegatedOperationAudienceRequest)(nil), // 61: saas.accounts.v1.ModuleExchangeDelegatedOperationAudienceRequest - (*v1.NewJob)(nil), // 62: saas.jobs.v1.NewJob - (v1.JobEnqueueDisposition)(0), // 63: saas.jobs.v1.JobEnqueueDisposition - (*durationpb.Duration)(nil), // 64: google.protobuf.Duration - (*v1.JobEnvelope)(nil), // 65: saas.jobs.v1.JobEnvelope - (*v1.JobLeaseReference)(nil), // 66: saas.jobs.v1.JobLeaseReference - (*v1.JobLease)(nil), // 67: saas.jobs.v1.JobLease - (*v1.JobFailure)(nil), // 68: saas.jobs.v1.JobFailure - (*timestamppb.Timestamp)(nil), // 69: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 70: google.protobuf.Struct - (*v11.EventEnvelope)(nil), // 71: saas.events.v1.EventEnvelope - (*ModuleMintRegistrationRequest)(nil), // 72: saas.accounts.v1.ModuleMintRegistrationRequest - (*SolutionMintRegistrationRequest)(nil), // 73: saas.accounts.v1.SolutionMintRegistrationRequest - (*ModuleMintWorkContextRequest)(nil), // 74: saas.accounts.v1.ModuleMintWorkContextRequest - (*ModuleMintOperationContextRequest)(nil), // 75: saas.accounts.v1.ModuleMintOperationContextRequest - (*ModuleMintSourceOperationContextRequest)(nil), // 76: saas.accounts.v1.ModuleMintSourceOperationContextRequest - (*IssuedWorkContext)(nil), // 77: saas.accounts.v1.IssuedWorkContext - (*emptypb.Empty)(nil), // 78: google.protobuf.Empty - (*ModuleMintRegistrationResponse)(nil), // 79: saas.accounts.v1.ModuleMintRegistrationResponse - (*SolutionMintRegistrationResponse)(nil), // 80: saas.accounts.v1.SolutionMintRegistrationResponse - (*ModuleMintWorkContextResponse)(nil), // 81: saas.accounts.v1.ModuleMintWorkContextResponse - (*ModuleMintOperationContextResponse)(nil), // 82: saas.accounts.v1.ModuleMintOperationContextResponse - (*ModuleMintSourceOperationContextResponse)(nil), // 83: saas.accounts.v1.ModuleMintSourceOperationContextResponse + (*ModuleLookupAuditEventResponse)(nil), // 25: saas.accounts.v1.ModuleLookupAuditEventResponse + (*ModuleEmitAuditEventRequest)(nil), // 26: saas.accounts.v1.ModuleEmitAuditEventRequest + (*ModuleAuditFieldDeclaration)(nil), // 27: saas.accounts.v1.ModuleAuditFieldDeclaration + (*ModuleAuditEventTypeDeclaration)(nil), // 28: saas.accounts.v1.ModuleAuditEventTypeDeclaration + (*ModuleDeclareAuditEventTypesRequest)(nil), // 29: saas.accounts.v1.ModuleDeclareAuditEventTypesRequest + (*ModuleDeclareAuditEventTypesResponse)(nil), // 30: saas.accounts.v1.ModuleDeclareAuditEventTypesResponse + (*SubjectVisibilityGrant)(nil), // 31: saas.accounts.v1.SubjectVisibilityGrant + (*ModuleListSubjectVisibilityRequest)(nil), // 32: saas.accounts.v1.ModuleListSubjectVisibilityRequest + (*ModuleListSubjectVisibilityResponse)(nil), // 33: saas.accounts.v1.ModuleListSubjectVisibilityResponse + (*FetchDatasourceBlobRequest)(nil), // 34: saas.accounts.v1.FetchDatasourceBlobRequest + (*FetchDatasourceBlobChunk)(nil), // 35: saas.accounts.v1.FetchDatasourceBlobChunk + (*DatasourceFileRef)(nil), // 36: saas.accounts.v1.DatasourceFileRef + (*FetchDatasourceFilesRequest)(nil), // 37: saas.accounts.v1.FetchDatasourceFilesRequest + (*DatasourceProvenance)(nil), // 38: saas.accounts.v1.DatasourceProvenance + (*DatasourceItemReaders)(nil), // 39: saas.accounts.v1.DatasourceItemReaders + (*DatasourceFileHeader)(nil), // 40: saas.accounts.v1.DatasourceFileHeader + (*FetchDatasourceFilesFrame)(nil), // 41: saas.accounts.v1.FetchDatasourceFilesFrame + (*ModulePublishEventRequest)(nil), // 42: saas.accounts.v1.ModulePublishEventRequest + (*ModulePublishEventResponse)(nil), // 43: saas.accounts.v1.ModulePublishEventResponse + (*ModuleSubscribeRequest)(nil), // 44: saas.accounts.v1.ModuleSubscribeRequest + (*ModuleSubscription)(nil), // 45: saas.accounts.v1.ModuleSubscription + (*ModuleSubscribeResponse)(nil), // 46: saas.accounts.v1.ModuleSubscribeResponse + (*ModuleUnsubscribeRequest)(nil), // 47: saas.accounts.v1.ModuleUnsubscribeRequest + (*ModuleListSubscriptionsRequest)(nil), // 48: saas.accounts.v1.ModuleListSubscriptionsRequest + (*ModuleListSubscriptionsResponse)(nil), // 49: saas.accounts.v1.ModuleListSubscriptionsResponse + (*ModuleReplayEventsRequest)(nil), // 50: saas.accounts.v1.ModuleReplayEventsRequest + (*ModuleReplayEventsResponse)(nil), // 51: saas.accounts.v1.ModuleReplayEventsResponse + (*ReadableSourceCollection)(nil), // 52: saas.accounts.v1.ReadableSourceCollection + (*ReadableCollectionGrant)(nil), // 53: saas.accounts.v1.ReadableCollectionGrant + (*CollectionSyncProvenance)(nil), // 54: saas.accounts.v1.CollectionSyncProvenance + (*ListReadableSourceCollectionsRequest)(nil), // 55: saas.accounts.v1.ListReadableSourceCollectionsRequest + (*ListReadableSourceCollectionsResponse)(nil), // 56: saas.accounts.v1.ListReadableSourceCollectionsResponse + (*ModulePlaceRecordRequest)(nil), // 57: saas.accounts.v1.ModulePlaceRecordRequest + (*ModulePlaceRecordResponse)(nil), // 58: saas.accounts.v1.ModulePlaceRecordResponse + (*VerifyWorkContextRuntimeBoundaryRequest)(nil), // 59: saas.accounts.v1.VerifyWorkContextRuntimeBoundaryRequest + (*VerifyWorkContextRuntimeBoundaryResponse)(nil), // 60: saas.accounts.v1.VerifyWorkContextRuntimeBoundaryResponse + (*CheckWorkContextRecordAccessRequest)(nil), // 61: saas.accounts.v1.CheckWorkContextRecordAccessRequest + (*CheckWorkContextRecordAccessResponse)(nil), // 62: saas.accounts.v1.CheckWorkContextRecordAccessResponse + (*ModuleExchangeDelegatedReadAudienceRequest)(nil), // 63: saas.accounts.v1.ModuleExchangeDelegatedReadAudienceRequest + (*ModuleExchangeDelegatedOperationAudienceRequest)(nil), // 64: saas.accounts.v1.ModuleExchangeDelegatedOperationAudienceRequest + (*v1.NewJob)(nil), // 65: saas.jobs.v1.NewJob + (v1.JobEnqueueDisposition)(0), // 66: saas.jobs.v1.JobEnqueueDisposition + (*durationpb.Duration)(nil), // 67: google.protobuf.Duration + (*v1.JobEnvelope)(nil), // 68: saas.jobs.v1.JobEnvelope + (*v1.JobLeaseReference)(nil), // 69: saas.jobs.v1.JobLeaseReference + (*v1.JobLease)(nil), // 70: saas.jobs.v1.JobLease + (*v1.JobFailure)(nil), // 71: saas.jobs.v1.JobFailure + (*timestamppb.Timestamp)(nil), // 72: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 73: google.protobuf.Struct + (*v11.EventEnvelope)(nil), // 74: saas.events.v1.EventEnvelope + (*ModuleMintRegistrationRequest)(nil), // 75: saas.accounts.v1.ModuleMintRegistrationRequest + (*SolutionMintRegistrationRequest)(nil), // 76: saas.accounts.v1.SolutionMintRegistrationRequest + (*ModuleMintWorkContextRequest)(nil), // 77: saas.accounts.v1.ModuleMintWorkContextRequest + (*ModuleMintOperationContextRequest)(nil), // 78: saas.accounts.v1.ModuleMintOperationContextRequest + (*ModuleMintSourceOperationContextRequest)(nil), // 79: saas.accounts.v1.ModuleMintSourceOperationContextRequest + (*IssuedWorkContext)(nil), // 80: saas.accounts.v1.IssuedWorkContext + (*emptypb.Empty)(nil), // 81: google.protobuf.Empty + (*ModuleMintRegistrationResponse)(nil), // 82: saas.accounts.v1.ModuleMintRegistrationResponse + (*SolutionMintRegistrationResponse)(nil), // 83: saas.accounts.v1.SolutionMintRegistrationResponse + (*ModuleMintWorkContextResponse)(nil), // 84: saas.accounts.v1.ModuleMintWorkContextResponse + (*ModuleMintOperationContextResponse)(nil), // 85: saas.accounts.v1.ModuleMintOperationContextResponse + (*ModuleMintSourceOperationContextResponse)(nil), // 86: saas.accounts.v1.ModuleMintSourceOperationContextResponse } var file_saas_accounts_v1_module_capabilities_proto_depIdxs = []int32{ - 62, // 0: saas.accounts.v1.ModuleEnqueueJobRequest.job:type_name -> saas.jobs.v1.NewJob - 63, // 1: saas.accounts.v1.ModuleEnqueueJobResponse.disposition:type_name -> saas.jobs.v1.JobEnqueueDisposition - 64, // 2: saas.accounts.v1.ModuleClaimJobsRequest.lease_duration:type_name -> google.protobuf.Duration - 65, // 3: saas.accounts.v1.ModuleClaimJobsResponse.jobs:type_name -> saas.jobs.v1.JobEnvelope - 66, // 4: saas.accounts.v1.ModuleHeartbeatJobRequest.lease:type_name -> saas.jobs.v1.JobLeaseReference - 64, // 5: saas.accounts.v1.ModuleHeartbeatJobRequest.extension:type_name -> google.protobuf.Duration - 67, // 6: saas.accounts.v1.ModuleHeartbeatJobResponse.lease:type_name -> saas.jobs.v1.JobLease - 66, // 7: saas.accounts.v1.ModuleAckJobRequest.lease:type_name -> saas.jobs.v1.JobLeaseReference - 66, // 8: saas.accounts.v1.ModuleNackJobRequest.lease:type_name -> saas.jobs.v1.JobLeaseReference - 68, // 9: saas.accounts.v1.ModuleNackJobRequest.failure:type_name -> saas.jobs.v1.JobFailure - 69, // 10: saas.accounts.v1.ModuleNackJobRequest.retry_at:type_name -> google.protobuf.Timestamp - 70, // 11: saas.accounts.v1.ModuleResumeRef.payload:type_name -> google.protobuf.Struct - 70, // 12: saas.accounts.v1.ModuleRequestApprovalRequest.subject:type_name -> google.protobuf.Struct + 65, // 0: saas.accounts.v1.ModuleEnqueueJobRequest.job:type_name -> saas.jobs.v1.NewJob + 66, // 1: saas.accounts.v1.ModuleEnqueueJobResponse.disposition:type_name -> saas.jobs.v1.JobEnqueueDisposition + 67, // 2: saas.accounts.v1.ModuleClaimJobsRequest.lease_duration:type_name -> google.protobuf.Duration + 68, // 3: saas.accounts.v1.ModuleClaimJobsResponse.jobs:type_name -> saas.jobs.v1.JobEnvelope + 69, // 4: saas.accounts.v1.ModuleHeartbeatJobRequest.lease:type_name -> saas.jobs.v1.JobLeaseReference + 67, // 5: saas.accounts.v1.ModuleHeartbeatJobRequest.extension:type_name -> google.protobuf.Duration + 70, // 6: saas.accounts.v1.ModuleHeartbeatJobResponse.lease:type_name -> saas.jobs.v1.JobLease + 69, // 7: saas.accounts.v1.ModuleAckJobRequest.lease:type_name -> saas.jobs.v1.JobLeaseReference + 69, // 8: saas.accounts.v1.ModuleNackJobRequest.lease:type_name -> saas.jobs.v1.JobLeaseReference + 71, // 9: saas.accounts.v1.ModuleNackJobRequest.failure:type_name -> saas.jobs.v1.JobFailure + 72, // 10: saas.accounts.v1.ModuleNackJobRequest.retry_at:type_name -> google.protobuf.Timestamp + 73, // 11: saas.accounts.v1.ModuleResumeRef.payload:type_name -> google.protobuf.Struct + 73, // 12: saas.accounts.v1.ModuleRequestApprovalRequest.subject:type_name -> google.protobuf.Struct 18, // 13: saas.accounts.v1.ModuleRequestApprovalRequest.policy:type_name -> saas.accounts.v1.ModuleApprovalPolicy 19, // 14: saas.accounts.v1.ModuleRequestApprovalRequest.resume_ref:type_name -> saas.accounts.v1.ModuleResumeRef - 69, // 15: saas.accounts.v1.ModuleRequestApprovalRequest.expires_at:type_name -> google.protobuf.Timestamp - 69, // 16: saas.accounts.v1.ModuleRequestApprovalRequest.escalate_at:type_name -> google.protobuf.Timestamp - 70, // 17: saas.accounts.v1.ModuleApproval.subject:type_name -> google.protobuf.Struct + 72, // 15: saas.accounts.v1.ModuleRequestApprovalRequest.expires_at:type_name -> google.protobuf.Timestamp + 72, // 16: saas.accounts.v1.ModuleRequestApprovalRequest.escalate_at:type_name -> google.protobuf.Timestamp + 73, // 17: saas.accounts.v1.ModuleApproval.subject:type_name -> google.protobuf.Struct 19, // 18: saas.accounts.v1.ModuleApproval.resume_ref:type_name -> saas.accounts.v1.ModuleResumeRef - 69, // 19: saas.accounts.v1.ModuleApproval.expires_at:type_name -> google.protobuf.Timestamp - 69, // 20: saas.accounts.v1.ModuleApproval.escalate_at:type_name -> google.protobuf.Timestamp - 69, // 21: saas.accounts.v1.ModuleApproval.created_at:type_name -> google.protobuf.Timestamp - 70, // 22: saas.accounts.v1.ModuleEmitAuditEventRequest.fields:type_name -> google.protobuf.Struct + 72, // 19: saas.accounts.v1.ModuleApproval.expires_at:type_name -> google.protobuf.Timestamp + 72, // 20: saas.accounts.v1.ModuleApproval.escalate_at:type_name -> google.protobuf.Timestamp + 72, // 21: saas.accounts.v1.ModuleApproval.created_at:type_name -> google.protobuf.Timestamp + 73, // 22: saas.accounts.v1.ModuleEmitAuditEventRequest.fields:type_name -> google.protobuf.Struct 0, // 23: saas.accounts.v1.ModuleAuditFieldDeclaration.kind:type_name -> saas.accounts.v1.ModuleAuditFieldKind - 26, // 24: saas.accounts.v1.ModuleAuditEventTypeDeclaration.fields:type_name -> saas.accounts.v1.ModuleAuditFieldDeclaration + 27, // 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 - 27, // 26: saas.accounts.v1.ModuleDeclareAuditEventTypesRequest.types:type_name -> saas.accounts.v1.ModuleAuditEventTypeDeclaration - 69, // 27: saas.accounts.v1.SubjectVisibilityGrant.expires_at:type_name -> google.protobuf.Timestamp - 30, // 28: saas.accounts.v1.ModuleListSubjectVisibilityResponse.grants:type_name -> saas.accounts.v1.SubjectVisibilityGrant - 35, // 29: saas.accounts.v1.FetchDatasourceFilesRequest.files:type_name -> saas.accounts.v1.DatasourceFileRef + 28, // 26: saas.accounts.v1.ModuleDeclareAuditEventTypesRequest.types:type_name -> saas.accounts.v1.ModuleAuditEventTypeDeclaration + 72, // 27: saas.accounts.v1.SubjectVisibilityGrant.expires_at:type_name -> google.protobuf.Timestamp + 31, // 28: saas.accounts.v1.ModuleListSubjectVisibilityResponse.grants:type_name -> saas.accounts.v1.SubjectVisibilityGrant + 36, // 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 - 37, // 31: saas.accounts.v1.DatasourceFileHeader.provenance:type_name -> saas.accounts.v1.DatasourceProvenance - 38, // 32: saas.accounts.v1.DatasourceFileHeader.readers:type_name -> saas.accounts.v1.DatasourceItemReaders - 39, // 33: saas.accounts.v1.FetchDatasourceFilesFrame.header:type_name -> saas.accounts.v1.DatasourceFileHeader - 71, // 34: saas.accounts.v1.ModulePublishEventRequest.envelope:type_name -> saas.events.v1.EventEnvelope + 38, // 31: saas.accounts.v1.DatasourceFileHeader.provenance:type_name -> saas.accounts.v1.DatasourceProvenance + 39, // 32: saas.accounts.v1.DatasourceFileHeader.readers:type_name -> saas.accounts.v1.DatasourceItemReaders + 40, // 33: saas.accounts.v1.FetchDatasourceFilesFrame.header:type_name -> saas.accounts.v1.DatasourceFileHeader + 74, // 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 - 69, // 37: saas.accounts.v1.ModuleSubscription.created_at:type_name -> google.protobuf.Timestamp - 44, // 38: saas.accounts.v1.ModuleSubscribeResponse.subscription:type_name -> saas.accounts.v1.ModuleSubscription - 44, // 39: saas.accounts.v1.ModuleListSubscriptionsResponse.subscriptions:type_name -> saas.accounts.v1.ModuleSubscription - 69, // 40: saas.accounts.v1.ModuleReplayEventsRequest.since:type_name -> google.protobuf.Timestamp + 72, // 37: saas.accounts.v1.ModuleSubscription.created_at:type_name -> google.protobuf.Timestamp + 45, // 38: saas.accounts.v1.ModuleSubscribeResponse.subscription:type_name -> saas.accounts.v1.ModuleSubscription + 45, // 39: saas.accounts.v1.ModuleListSubscriptionsResponse.subscriptions:type_name -> saas.accounts.v1.ModuleSubscription + 72, // 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 - 52, // 42: saas.accounts.v1.ReadableSourceCollection.read_grants:type_name -> saas.accounts.v1.ReadableCollectionGrant - 53, // 43: saas.accounts.v1.ReadableSourceCollection.sync:type_name -> saas.accounts.v1.CollectionSyncProvenance - 69, // 44: saas.accounts.v1.ReadableCollectionGrant.expires_at:type_name -> google.protobuf.Timestamp + 53, // 42: saas.accounts.v1.ReadableSourceCollection.read_grants:type_name -> saas.accounts.v1.ReadableCollectionGrant + 54, // 43: saas.accounts.v1.ReadableSourceCollection.sync:type_name -> saas.accounts.v1.CollectionSyncProvenance + 72, // 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 - 69, // 46: saas.accounts.v1.CollectionSyncProvenance.at:type_name -> google.protobuf.Timestamp + 72, // 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 - 69, // 48: saas.accounts.v1.CollectionSyncProvenance.requested_at:type_name -> google.protobuf.Timestamp - 51, // 49: saas.accounts.v1.ListReadableSourceCollectionsResponse.collections:type_name -> saas.accounts.v1.ReadableSourceCollection - 60, // 50: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedReadAudience:input_type -> saas.accounts.v1.ModuleExchangeDelegatedReadAudienceRequest - 61, // 51: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedOperationAudience:input_type -> saas.accounts.v1.ModuleExchangeDelegatedOperationAudienceRequest - 58, // 52: saas.accounts.v1.ModuleCapabilitiesService.CheckWorkContextRecordAccess:input_type -> saas.accounts.v1.CheckWorkContextRecordAccessRequest - 54, // 53: saas.accounts.v1.ModuleCapabilitiesService.ListReadableSourceCollections:input_type -> saas.accounts.v1.ListReadableSourceCollectionsRequest - 56, // 54: saas.accounts.v1.ModuleCapabilitiesService.PlaceRecord:input_type -> saas.accounts.v1.ModulePlaceRecordRequest - 6, // 55: saas.accounts.v1.ModuleCapabilitiesService.EnqueueJob:input_type -> saas.accounts.v1.ModuleEnqueueJobRequest - 8, // 56: saas.accounts.v1.ModuleCapabilitiesService.ClaimJobs:input_type -> saas.accounts.v1.ModuleClaimJobsRequest - 10, // 57: saas.accounts.v1.ModuleCapabilitiesService.HeartbeatJob:input_type -> saas.accounts.v1.ModuleHeartbeatJobRequest - 12, // 58: saas.accounts.v1.ModuleCapabilitiesService.AckJob:input_type -> saas.accounts.v1.ModuleAckJobRequest - 13, // 59: saas.accounts.v1.ModuleCapabilitiesService.NackJob:input_type -> saas.accounts.v1.ModuleNackJobRequest - 14, // 60: saas.accounts.v1.ModuleCapabilitiesService.NotifyUser:input_type -> saas.accounts.v1.ModuleNotifyUserRequest - 16, // 61: saas.accounts.v1.ModuleCapabilitiesService.NotifyOrgAdmins:input_type -> saas.accounts.v1.ModuleNotifyOrgAdminsRequest - 20, // 62: saas.accounts.v1.ModuleCapabilitiesService.RequestApproval:input_type -> saas.accounts.v1.ModuleRequestApprovalRequest - 22, // 63: saas.accounts.v1.ModuleCapabilitiesService.GetApproval:input_type -> saas.accounts.v1.ModuleGetApprovalRequest - 24, // 64: saas.accounts.v1.ModuleCapabilitiesService.CancelApproval:input_type -> saas.accounts.v1.ModuleCancelApprovalRequest - 25, // 65: saas.accounts.v1.ModuleCapabilitiesService.EmitAuditEvent:input_type -> saas.accounts.v1.ModuleEmitAuditEventRequest - 28, // 66: saas.accounts.v1.ModuleCapabilitiesService.DeclareAuditEventTypes:input_type -> saas.accounts.v1.ModuleDeclareAuditEventTypesRequest - 31, // 67: saas.accounts.v1.ModuleCapabilitiesService.ListSubjectVisibility:input_type -> saas.accounts.v1.ModuleListSubjectVisibilityRequest - 33, // 68: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceBlob:input_type -> saas.accounts.v1.FetchDatasourceBlobRequest - 36, // 69: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceFiles:input_type -> saas.accounts.v1.FetchDatasourceFilesRequest - 72, // 70: saas.accounts.v1.ModuleCapabilitiesService.MintModuleRegistration:input_type -> saas.accounts.v1.ModuleMintRegistrationRequest - 73, // 71: saas.accounts.v1.ModuleCapabilitiesService.MintSolutionRegistration:input_type -> saas.accounts.v1.SolutionMintRegistrationRequest - 74, // 72: saas.accounts.v1.ModuleCapabilitiesService.MintModuleWorkContext:input_type -> saas.accounts.v1.ModuleMintWorkContextRequest - 75, // 73: saas.accounts.v1.ModuleCapabilitiesService.MintModuleOperationContext:input_type -> saas.accounts.v1.ModuleMintOperationContextRequest - 76, // 74: saas.accounts.v1.ModuleCapabilitiesService.MintSourceOperationContext:input_type -> saas.accounts.v1.ModuleMintSourceOperationContextRequest - 41, // 75: saas.accounts.v1.ModuleCapabilitiesService.PublishEvent:input_type -> saas.accounts.v1.ModulePublishEventRequest - 43, // 76: saas.accounts.v1.ModuleCapabilitiesService.Subscribe:input_type -> saas.accounts.v1.ModuleSubscribeRequest - 46, // 77: saas.accounts.v1.ModuleCapabilitiesService.Unsubscribe:input_type -> saas.accounts.v1.ModuleUnsubscribeRequest - 47, // 78: saas.accounts.v1.ModuleCapabilitiesService.ListSubscriptions:input_type -> saas.accounts.v1.ModuleListSubscriptionsRequest - 49, // 79: saas.accounts.v1.ModuleCapabilitiesService.ReplayEvents:input_type -> saas.accounts.v1.ModuleReplayEventsRequest - 77, // 80: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedReadAudience:output_type -> saas.accounts.v1.IssuedWorkContext - 77, // 81: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedOperationAudience:output_type -> saas.accounts.v1.IssuedWorkContext - 59, // 82: saas.accounts.v1.ModuleCapabilitiesService.CheckWorkContextRecordAccess:output_type -> saas.accounts.v1.CheckWorkContextRecordAccessResponse - 55, // 83: saas.accounts.v1.ModuleCapabilitiesService.ListReadableSourceCollections:output_type -> saas.accounts.v1.ListReadableSourceCollectionsResponse - 57, // 84: saas.accounts.v1.ModuleCapabilitiesService.PlaceRecord:output_type -> saas.accounts.v1.ModulePlaceRecordResponse - 7, // 85: saas.accounts.v1.ModuleCapabilitiesService.EnqueueJob:output_type -> saas.accounts.v1.ModuleEnqueueJobResponse - 9, // 86: saas.accounts.v1.ModuleCapabilitiesService.ClaimJobs:output_type -> saas.accounts.v1.ModuleClaimJobsResponse - 11, // 87: saas.accounts.v1.ModuleCapabilitiesService.HeartbeatJob:output_type -> saas.accounts.v1.ModuleHeartbeatJobResponse - 78, // 88: saas.accounts.v1.ModuleCapabilitiesService.AckJob:output_type -> google.protobuf.Empty - 78, // 89: saas.accounts.v1.ModuleCapabilitiesService.NackJob:output_type -> google.protobuf.Empty - 15, // 90: saas.accounts.v1.ModuleCapabilitiesService.NotifyUser:output_type -> saas.accounts.v1.ModuleNotifyUserResponse - 17, // 91: saas.accounts.v1.ModuleCapabilitiesService.NotifyOrgAdmins:output_type -> saas.accounts.v1.ModuleNotifyOrgAdminsResponse - 21, // 92: saas.accounts.v1.ModuleCapabilitiesService.RequestApproval:output_type -> saas.accounts.v1.ModuleRequestApprovalResponse - 23, // 93: saas.accounts.v1.ModuleCapabilitiesService.GetApproval:output_type -> saas.accounts.v1.ModuleApproval - 78, // 94: saas.accounts.v1.ModuleCapabilitiesService.CancelApproval:output_type -> google.protobuf.Empty - 78, // 95: saas.accounts.v1.ModuleCapabilitiesService.EmitAuditEvent:output_type -> google.protobuf.Empty - 29, // 96: saas.accounts.v1.ModuleCapabilitiesService.DeclareAuditEventTypes:output_type -> saas.accounts.v1.ModuleDeclareAuditEventTypesResponse - 32, // 97: saas.accounts.v1.ModuleCapabilitiesService.ListSubjectVisibility:output_type -> saas.accounts.v1.ModuleListSubjectVisibilityResponse - 34, // 98: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceBlob:output_type -> saas.accounts.v1.FetchDatasourceBlobChunk - 40, // 99: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceFiles:output_type -> saas.accounts.v1.FetchDatasourceFilesFrame - 79, // 100: saas.accounts.v1.ModuleCapabilitiesService.MintModuleRegistration:output_type -> saas.accounts.v1.ModuleMintRegistrationResponse - 80, // 101: saas.accounts.v1.ModuleCapabilitiesService.MintSolutionRegistration:output_type -> saas.accounts.v1.SolutionMintRegistrationResponse - 81, // 102: saas.accounts.v1.ModuleCapabilitiesService.MintModuleWorkContext:output_type -> saas.accounts.v1.ModuleMintWorkContextResponse - 82, // 103: saas.accounts.v1.ModuleCapabilitiesService.MintModuleOperationContext:output_type -> saas.accounts.v1.ModuleMintOperationContextResponse - 83, // 104: saas.accounts.v1.ModuleCapabilitiesService.MintSourceOperationContext:output_type -> saas.accounts.v1.ModuleMintSourceOperationContextResponse - 42, // 105: saas.accounts.v1.ModuleCapabilitiesService.PublishEvent:output_type -> saas.accounts.v1.ModulePublishEventResponse - 45, // 106: saas.accounts.v1.ModuleCapabilitiesService.Subscribe:output_type -> saas.accounts.v1.ModuleSubscribeResponse - 78, // 107: saas.accounts.v1.ModuleCapabilitiesService.Unsubscribe:output_type -> google.protobuf.Empty - 48, // 108: saas.accounts.v1.ModuleCapabilitiesService.ListSubscriptions:output_type -> saas.accounts.v1.ModuleListSubscriptionsResponse - 50, // 109: saas.accounts.v1.ModuleCapabilitiesService.ReplayEvents:output_type -> saas.accounts.v1.ModuleReplayEventsResponse - 80, // [80:110] is the sub-list for method output_type - 50, // [50:80] is the sub-list for method input_type + 72, // 48: saas.accounts.v1.CollectionSyncProvenance.requested_at:type_name -> google.protobuf.Timestamp + 52, // 49: saas.accounts.v1.ListReadableSourceCollectionsResponse.collections:type_name -> saas.accounts.v1.ReadableSourceCollection + 63, // 50: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedReadAudience:input_type -> saas.accounts.v1.ModuleExchangeDelegatedReadAudienceRequest + 64, // 51: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedOperationAudience:input_type -> saas.accounts.v1.ModuleExchangeDelegatedOperationAudienceRequest + 59, // 52: saas.accounts.v1.ModuleCapabilitiesService.VerifyWorkContextRuntimeBoundary:input_type -> saas.accounts.v1.VerifyWorkContextRuntimeBoundaryRequest + 61, // 53: saas.accounts.v1.ModuleCapabilitiesService.CheckWorkContextRecordAccess:input_type -> saas.accounts.v1.CheckWorkContextRecordAccessRequest + 55, // 54: saas.accounts.v1.ModuleCapabilitiesService.ListReadableSourceCollections:input_type -> saas.accounts.v1.ListReadableSourceCollectionsRequest + 57, // 55: saas.accounts.v1.ModuleCapabilitiesService.PlaceRecord:input_type -> saas.accounts.v1.ModulePlaceRecordRequest + 6, // 56: saas.accounts.v1.ModuleCapabilitiesService.EnqueueJob:input_type -> saas.accounts.v1.ModuleEnqueueJobRequest + 8, // 57: saas.accounts.v1.ModuleCapabilitiesService.ClaimJobs:input_type -> saas.accounts.v1.ModuleClaimJobsRequest + 10, // 58: saas.accounts.v1.ModuleCapabilitiesService.HeartbeatJob:input_type -> saas.accounts.v1.ModuleHeartbeatJobRequest + 12, // 59: saas.accounts.v1.ModuleCapabilitiesService.AckJob:input_type -> saas.accounts.v1.ModuleAckJobRequest + 13, // 60: saas.accounts.v1.ModuleCapabilitiesService.NackJob:input_type -> saas.accounts.v1.ModuleNackJobRequest + 14, // 61: saas.accounts.v1.ModuleCapabilitiesService.NotifyUser:input_type -> saas.accounts.v1.ModuleNotifyUserRequest + 16, // 62: saas.accounts.v1.ModuleCapabilitiesService.NotifyOrgAdmins:input_type -> saas.accounts.v1.ModuleNotifyOrgAdminsRequest + 20, // 63: saas.accounts.v1.ModuleCapabilitiesService.RequestApproval:input_type -> saas.accounts.v1.ModuleRequestApprovalRequest + 22, // 64: saas.accounts.v1.ModuleCapabilitiesService.GetApproval:input_type -> saas.accounts.v1.ModuleGetApprovalRequest + 24, // 65: saas.accounts.v1.ModuleCapabilitiesService.CancelApproval:input_type -> saas.accounts.v1.ModuleCancelApprovalRequest + 26, // 66: saas.accounts.v1.ModuleCapabilitiesService.EmitAuditEvent:input_type -> saas.accounts.v1.ModuleEmitAuditEventRequest + 26, // 67: saas.accounts.v1.ModuleCapabilitiesService.LookupAuditEvent:input_type -> saas.accounts.v1.ModuleEmitAuditEventRequest + 29, // 68: saas.accounts.v1.ModuleCapabilitiesService.DeclareAuditEventTypes:input_type -> saas.accounts.v1.ModuleDeclareAuditEventTypesRequest + 32, // 69: saas.accounts.v1.ModuleCapabilitiesService.ListSubjectVisibility:input_type -> saas.accounts.v1.ModuleListSubjectVisibilityRequest + 34, // 70: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceBlob:input_type -> saas.accounts.v1.FetchDatasourceBlobRequest + 37, // 71: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceFiles:input_type -> saas.accounts.v1.FetchDatasourceFilesRequest + 75, // 72: saas.accounts.v1.ModuleCapabilitiesService.MintModuleRegistration:input_type -> saas.accounts.v1.ModuleMintRegistrationRequest + 76, // 73: saas.accounts.v1.ModuleCapabilitiesService.MintSolutionRegistration:input_type -> saas.accounts.v1.SolutionMintRegistrationRequest + 77, // 74: saas.accounts.v1.ModuleCapabilitiesService.MintModuleWorkContext:input_type -> saas.accounts.v1.ModuleMintWorkContextRequest + 78, // 75: saas.accounts.v1.ModuleCapabilitiesService.MintModuleOperationContext:input_type -> saas.accounts.v1.ModuleMintOperationContextRequest + 79, // 76: saas.accounts.v1.ModuleCapabilitiesService.MintSourceOperationContext:input_type -> saas.accounts.v1.ModuleMintSourceOperationContextRequest + 42, // 77: saas.accounts.v1.ModuleCapabilitiesService.PublishEvent:input_type -> saas.accounts.v1.ModulePublishEventRequest + 44, // 78: saas.accounts.v1.ModuleCapabilitiesService.Subscribe:input_type -> saas.accounts.v1.ModuleSubscribeRequest + 47, // 79: saas.accounts.v1.ModuleCapabilitiesService.Unsubscribe:input_type -> saas.accounts.v1.ModuleUnsubscribeRequest + 48, // 80: saas.accounts.v1.ModuleCapabilitiesService.ListSubscriptions:input_type -> saas.accounts.v1.ModuleListSubscriptionsRequest + 50, // 81: saas.accounts.v1.ModuleCapabilitiesService.ReplayEvents:input_type -> saas.accounts.v1.ModuleReplayEventsRequest + 80, // 82: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedReadAudience:output_type -> saas.accounts.v1.IssuedWorkContext + 80, // 83: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedOperationAudience:output_type -> saas.accounts.v1.IssuedWorkContext + 60, // 84: saas.accounts.v1.ModuleCapabilitiesService.VerifyWorkContextRuntimeBoundary:output_type -> saas.accounts.v1.VerifyWorkContextRuntimeBoundaryResponse + 62, // 85: saas.accounts.v1.ModuleCapabilitiesService.CheckWorkContextRecordAccess:output_type -> saas.accounts.v1.CheckWorkContextRecordAccessResponse + 56, // 86: saas.accounts.v1.ModuleCapabilitiesService.ListReadableSourceCollections:output_type -> saas.accounts.v1.ListReadableSourceCollectionsResponse + 58, // 87: saas.accounts.v1.ModuleCapabilitiesService.PlaceRecord:output_type -> saas.accounts.v1.ModulePlaceRecordResponse + 7, // 88: saas.accounts.v1.ModuleCapabilitiesService.EnqueueJob:output_type -> saas.accounts.v1.ModuleEnqueueJobResponse + 9, // 89: saas.accounts.v1.ModuleCapabilitiesService.ClaimJobs:output_type -> saas.accounts.v1.ModuleClaimJobsResponse + 11, // 90: saas.accounts.v1.ModuleCapabilitiesService.HeartbeatJob:output_type -> saas.accounts.v1.ModuleHeartbeatJobResponse + 81, // 91: saas.accounts.v1.ModuleCapabilitiesService.AckJob:output_type -> google.protobuf.Empty + 81, // 92: saas.accounts.v1.ModuleCapabilitiesService.NackJob:output_type -> google.protobuf.Empty + 15, // 93: saas.accounts.v1.ModuleCapabilitiesService.NotifyUser:output_type -> saas.accounts.v1.ModuleNotifyUserResponse + 17, // 94: saas.accounts.v1.ModuleCapabilitiesService.NotifyOrgAdmins:output_type -> saas.accounts.v1.ModuleNotifyOrgAdminsResponse + 21, // 95: saas.accounts.v1.ModuleCapabilitiesService.RequestApproval:output_type -> saas.accounts.v1.ModuleRequestApprovalResponse + 23, // 96: saas.accounts.v1.ModuleCapabilitiesService.GetApproval:output_type -> saas.accounts.v1.ModuleApproval + 81, // 97: saas.accounts.v1.ModuleCapabilitiesService.CancelApproval:output_type -> google.protobuf.Empty + 81, // 98: saas.accounts.v1.ModuleCapabilitiesService.EmitAuditEvent:output_type -> google.protobuf.Empty + 25, // 99: saas.accounts.v1.ModuleCapabilitiesService.LookupAuditEvent:output_type -> saas.accounts.v1.ModuleLookupAuditEventResponse + 30, // 100: saas.accounts.v1.ModuleCapabilitiesService.DeclareAuditEventTypes:output_type -> saas.accounts.v1.ModuleDeclareAuditEventTypesResponse + 33, // 101: saas.accounts.v1.ModuleCapabilitiesService.ListSubjectVisibility:output_type -> saas.accounts.v1.ModuleListSubjectVisibilityResponse + 35, // 102: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceBlob:output_type -> saas.accounts.v1.FetchDatasourceBlobChunk + 41, // 103: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceFiles:output_type -> saas.accounts.v1.FetchDatasourceFilesFrame + 82, // 104: saas.accounts.v1.ModuleCapabilitiesService.MintModuleRegistration:output_type -> saas.accounts.v1.ModuleMintRegistrationResponse + 83, // 105: saas.accounts.v1.ModuleCapabilitiesService.MintSolutionRegistration:output_type -> saas.accounts.v1.SolutionMintRegistrationResponse + 84, // 106: saas.accounts.v1.ModuleCapabilitiesService.MintModuleWorkContext:output_type -> saas.accounts.v1.ModuleMintWorkContextResponse + 85, // 107: saas.accounts.v1.ModuleCapabilitiesService.MintModuleOperationContext:output_type -> saas.accounts.v1.ModuleMintOperationContextResponse + 86, // 108: saas.accounts.v1.ModuleCapabilitiesService.MintSourceOperationContext:output_type -> saas.accounts.v1.ModuleMintSourceOperationContextResponse + 43, // 109: saas.accounts.v1.ModuleCapabilitiesService.PublishEvent:output_type -> saas.accounts.v1.ModulePublishEventResponse + 46, // 110: saas.accounts.v1.ModuleCapabilitiesService.Subscribe:output_type -> saas.accounts.v1.ModuleSubscribeResponse + 81, // 111: saas.accounts.v1.ModuleCapabilitiesService.Unsubscribe:output_type -> google.protobuf.Empty + 49, // 112: saas.accounts.v1.ModuleCapabilitiesService.ListSubscriptions:output_type -> saas.accounts.v1.ModuleListSubscriptionsResponse + 51, // 113: saas.accounts.v1.ModuleCapabilitiesService.ReplayEvents:output_type -> saas.accounts.v1.ModuleReplayEventsResponse + 82, // [82:114] is the sub-list for method output_type + 50, // [50:82] is the sub-list for method input_type 50, // [50:50] is the sub-list for extension type_name 50, // [50:50] is the sub-list for extension extendee 0, // [0:50] is the sub-list for field type_name @@ -4792,7 +4954,7 @@ 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[35].OneofWrappers = []any{ (*FetchDatasourceFilesFrame_Header)(nil), (*FetchDatasourceFilesFrame_Data)(nil), } @@ -4802,7 +4964,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: 6, - NumMessages: 56, + NumMessages: 59, 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 093af4c6b..1fed3472f 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 @@ -23,6 +23,7 @@ const _ = grpc.SupportPackageIsVersion9 const ( ModuleCapabilitiesService_ExchangeDelegatedReadAudience_FullMethodName = "/saas.accounts.v1.ModuleCapabilitiesService/ExchangeDelegatedReadAudience" ModuleCapabilitiesService_ExchangeDelegatedOperationAudience_FullMethodName = "/saas.accounts.v1.ModuleCapabilitiesService/ExchangeDelegatedOperationAudience" + ModuleCapabilitiesService_VerifyWorkContextRuntimeBoundary_FullMethodName = "/saas.accounts.v1.ModuleCapabilitiesService/VerifyWorkContextRuntimeBoundary" ModuleCapabilitiesService_CheckWorkContextRecordAccess_FullMethodName = "/saas.accounts.v1.ModuleCapabilitiesService/CheckWorkContextRecordAccess" ModuleCapabilitiesService_ListReadableSourceCollections_FullMethodName = "/saas.accounts.v1.ModuleCapabilitiesService/ListReadableSourceCollections" ModuleCapabilitiesService_PlaceRecord_FullMethodName = "/saas.accounts.v1.ModuleCapabilitiesService/PlaceRecord" @@ -37,6 +38,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_LookupAuditEvent_FullMethodName = "/saas.accounts.v1.ModuleCapabilitiesService/LookupAuditEvent" ModuleCapabilitiesService_DeclareAuditEventTypes_FullMethodName = "/saas.accounts.v1.ModuleCapabilitiesService/DeclareAuditEventTypes" ModuleCapabilitiesService_ListSubjectVisibility_FullMethodName = "/saas.accounts.v1.ModuleCapabilitiesService/ListSubjectVisibility" ModuleCapabilitiesService_FetchDatasourceBlob_FullMethodName = "/saas.accounts.v1.ModuleCapabilitiesService/FetchDatasourceBlob" @@ -64,6 +66,9 @@ type ModuleCapabilitiesServiceClient interface { // of a current parent context and exchanges only an installed operation's // invoke scopes, or its read-only receipt-lookup subset. ExchangeDelegatedOperationAudience(ctx context.Context, in *ModuleExchangeDelegatedOperationAudienceRequest, opts ...grpc.CallOption) (*IssuedWorkContext, error) + // Attests a current capability under an active host-owned solution boundary. + // This read grants no authority and does not expose the registration seed. + VerifyWorkContextRuntimeBoundary(ctx context.Context, in *VerifyWorkContextRuntimeBoundaryRequest, opts ...grpc.CallOption) (*VerifyWorkContextRuntimeBoundaryResponse, error) // Checks current owner and every delegated actor against true record placement, // intersected with the verified capability's attenuated resource/action scope. CheckWorkContextRecordAccess(ctx context.Context, in *CheckWorkContextRecordAccessRequest, opts ...grpc.CallOption) (*CheckWorkContextRecordAccessResponse, error) @@ -99,6 +104,10 @@ 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) + // Read-only recovery for the complete original intent. Requires a nonempty + // idempotency_key; same authority as EmitAuditEvent. No retry authorization is + // implied by an absent receipt. Old unbound keys return FAILED_PRECONDITION. + LookupAuditEvent(ctx context.Context, in *ModuleEmitAuditEventRequest, opts ...grpc.CallOption) (*ModuleLookupAuditEventResponse, 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) @@ -193,6 +202,16 @@ func (c *moduleCapabilitiesServiceClient) ExchangeDelegatedOperationAudience(ctx return out, nil } +func (c *moduleCapabilitiesServiceClient) VerifyWorkContextRuntimeBoundary(ctx context.Context, in *VerifyWorkContextRuntimeBoundaryRequest, opts ...grpc.CallOption) (*VerifyWorkContextRuntimeBoundaryResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(VerifyWorkContextRuntimeBoundaryResponse) + err := c.cc.Invoke(ctx, ModuleCapabilitiesService_VerifyWorkContextRuntimeBoundary_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *moduleCapabilitiesServiceClient) CheckWorkContextRecordAccess(ctx context.Context, in *CheckWorkContextRecordAccessRequest, opts ...grpc.CallOption) (*CheckWorkContextRecordAccessResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(CheckWorkContextRecordAccessResponse) @@ -333,6 +352,16 @@ func (c *moduleCapabilitiesServiceClient) EmitAuditEvent(ctx context.Context, in return out, nil } +func (c *moduleCapabilitiesServiceClient) LookupAuditEvent(ctx context.Context, in *ModuleEmitAuditEventRequest, opts ...grpc.CallOption) (*ModuleLookupAuditEventResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ModuleLookupAuditEventResponse) + err := c.cc.Invoke(ctx, ModuleCapabilitiesService_LookupAuditEvent_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) @@ -503,6 +532,9 @@ type ModuleCapabilitiesServiceServer interface { // of a current parent context and exchanges only an installed operation's // invoke scopes, or its read-only receipt-lookup subset. ExchangeDelegatedOperationAudience(context.Context, *ModuleExchangeDelegatedOperationAudienceRequest) (*IssuedWorkContext, error) + // Attests a current capability under an active host-owned solution boundary. + // This read grants no authority and does not expose the registration seed. + VerifyWorkContextRuntimeBoundary(context.Context, *VerifyWorkContextRuntimeBoundaryRequest) (*VerifyWorkContextRuntimeBoundaryResponse, error) // Checks current owner and every delegated actor against true record placement, // intersected with the verified capability's attenuated resource/action scope. CheckWorkContextRecordAccess(context.Context, *CheckWorkContextRecordAccessRequest) (*CheckWorkContextRecordAccessResponse, error) @@ -538,6 +570,10 @@ 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) + // Read-only recovery for the complete original intent. Requires a nonempty + // idempotency_key; same authority as EmitAuditEvent. No retry authorization is + // implied by an absent receipt. Old unbound keys return FAILED_PRECONDITION. + LookupAuditEvent(context.Context, *ModuleEmitAuditEventRequest) (*ModuleLookupAuditEventResponse, 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) @@ -618,6 +654,9 @@ func (UnimplementedModuleCapabilitiesServiceServer) ExchangeDelegatedReadAudienc func (UnimplementedModuleCapabilitiesServiceServer) ExchangeDelegatedOperationAudience(context.Context, *ModuleExchangeDelegatedOperationAudienceRequest) (*IssuedWorkContext, error) { return nil, status.Error(codes.Unimplemented, "method ExchangeDelegatedOperationAudience not implemented") } +func (UnimplementedModuleCapabilitiesServiceServer) VerifyWorkContextRuntimeBoundary(context.Context, *VerifyWorkContextRuntimeBoundaryRequest) (*VerifyWorkContextRuntimeBoundaryResponse, error) { + return nil, status.Error(codes.Unimplemented, "method VerifyWorkContextRuntimeBoundary not implemented") +} func (UnimplementedModuleCapabilitiesServiceServer) CheckWorkContextRecordAccess(context.Context, *CheckWorkContextRecordAccessRequest) (*CheckWorkContextRecordAccessResponse, error) { return nil, status.Error(codes.Unimplemented, "method CheckWorkContextRecordAccess not implemented") } @@ -660,6 +699,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) LookupAuditEvent(context.Context, *ModuleEmitAuditEventRequest) (*ModuleLookupAuditEventResponse, error) { + return nil, status.Error(codes.Unimplemented, "method LookupAuditEvent not implemented") +} func (UnimplementedModuleCapabilitiesServiceServer) DeclareAuditEventTypes(context.Context, *ModuleDeclareAuditEventTypesRequest) (*ModuleDeclareAuditEventTypesResponse, error) { return nil, status.Error(codes.Unimplemented, "method DeclareAuditEventTypes not implemented") } @@ -760,6 +802,24 @@ func _ModuleCapabilitiesService_ExchangeDelegatedOperationAudience_Handler(srv i return interceptor(ctx, in, info, handler) } +func _ModuleCapabilitiesService_VerifyWorkContextRuntimeBoundary_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(VerifyWorkContextRuntimeBoundaryRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ModuleCapabilitiesServiceServer).VerifyWorkContextRuntimeBoundary(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ModuleCapabilitiesService_VerifyWorkContextRuntimeBoundary_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ModuleCapabilitiesServiceServer).VerifyWorkContextRuntimeBoundary(ctx, req.(*VerifyWorkContextRuntimeBoundaryRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _ModuleCapabilitiesService_CheckWorkContextRecordAccess_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(CheckWorkContextRecordAccessRequest) if err := dec(in); err != nil { @@ -1012,6 +1072,24 @@ func _ModuleCapabilitiesService_EmitAuditEvent_Handler(srv interface{}, ctx cont return interceptor(ctx, in, info, handler) } +func _ModuleCapabilitiesService_LookupAuditEvent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ModuleEmitAuditEventRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ModuleCapabilitiesServiceServer).LookupAuditEvent(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ModuleCapabilitiesService_LookupAuditEvent_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ModuleCapabilitiesServiceServer).LookupAuditEvent(ctx, req.(*ModuleEmitAuditEventRequest)) + } + 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 { @@ -1265,6 +1343,10 @@ var ModuleCapabilitiesService_ServiceDesc = grpc.ServiceDesc{ MethodName: "ExchangeDelegatedOperationAudience", Handler: _ModuleCapabilitiesService_ExchangeDelegatedOperationAudience_Handler, }, + { + MethodName: "VerifyWorkContextRuntimeBoundary", + Handler: _ModuleCapabilitiesService_VerifyWorkContextRuntimeBoundary_Handler, + }, { MethodName: "CheckWorkContextRecordAccess", Handler: _ModuleCapabilitiesService_CheckWorkContextRecordAccess_Handler, @@ -1321,6 +1403,10 @@ var ModuleCapabilitiesService_ServiceDesc = grpc.ServiceDesc{ MethodName: "EmitAuditEvent", Handler: _ModuleCapabilitiesService_EmitAuditEvent_Handler, }, + { + MethodName: "LookupAuditEvent", + Handler: _ModuleCapabilitiesService_LookupAuditEvent_Handler, + }, { MethodName: "DeclareAuditEventTypes", Handler: _ModuleCapabilitiesService_DeclareAuditEventTypes_Handler, diff --git a/module/services/accounts/code/pkg/infra/postgres_audit_effect.go b/module/services/accounts/code/pkg/infra/postgres_audit_effect.go new file mode 100644 index 000000000..a55faa6f4 --- /dev/null +++ b/module/services/accounts/code/pkg/infra/postgres_audit_effect.go @@ -0,0 +1,81 @@ +package infra + +import ( + "context" + "errors" + "fmt" + + "accounts/pkg/business" + "github.com/jackc/pgx/v5" +) + +// ReserveAuditEffect is an owner-side transactional reservation. A duplicate +// INSERT waits for its competitor's commit/rollback; the separate SELECT then +// reads the committed binding under the store's READ COMMITTED transaction. +// Tenant RLS and the explicit key constrain both statements. +func (s *PostgresStore) ReserveAuditEffect(ctx context.Context, entry business.AuditEntry) (bool, error) { + if entry.ID == "" || entry.IdempotencyKey == "" { + return false, fmt.Errorf("audit effect requires an event identity and key") + } + fingerprint, err := business.AuditEffectFingerprint(entry) + if err != nil { + return false, err + } + org := entry.OrgID + if org == "" { + org = auditIdempotencySystemOrg + } + q := s.getQueryExecutor(ctx) + var inserted bool + err = q.QueryRow(ctx, `INSERT INTO audit_event_idempotency + (org_id, event_type, idempotency_key, request_fingerprint, audit_event_id) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (org_id, event_type, idempotency_key) DO NOTHING + RETURNING true`, org, string(entry.EventType), entry.IdempotencyKey, fingerprint, entry.ID).Scan(&inserted) + if err == nil { + return inserted, nil + } + if !errors.Is(err, pgx.ErrNoRows) { + return false, err + } + eventID, err := s.LookupAuditEffect(ctx, entry) + if err != nil { + return false, err + } + if eventID == "" { + return false, fmt.Errorf("conflicting audit reservation is not visible") + } + return false, nil +} + +// LookupAuditEffect never inserts or changes a reservation. +func (s *PostgresStore) LookupAuditEffect(ctx context.Context, entry business.AuditEntry) (string, error) { + if entry.IdempotencyKey == "" { + return "", fmt.Errorf("audit receipt requires a key") + } + fingerprint, err := business.AuditEffectFingerprint(entry) + if err != nil { + return "", err + } + org := entry.OrgID + if org == "" { + org = auditIdempotencySystemOrg + } + var priorFingerprint, eventID *string + err = s.getQueryExecutor(ctx).QueryRow(ctx, `SELECT request_fingerprint, audit_event_id::text + FROM audit_event_idempotency WHERE org_id=$1 AND event_type=$2 AND idempotency_key=$3`, + org, string(entry.EventType), entry.IdempotencyKey).Scan(&priorFingerprint, &eventID) + if errors.Is(err, pgx.ErrNoRows) { + return "", nil + } + if err != nil { + return "", err + } + if priorFingerprint == nil || eventID == nil { + return "", business.ErrAuditIdempotencyUnverifiable + } + if *priorFingerprint != fingerprint { + return "", business.ErrAuditIdempotencyConflict + } + return *eventID, nil +} diff --git a/module/services/accounts/code/pkg/infra/postgres_audit_effect_test.go b/module/services/accounts/code/pkg/infra/postgres_audit_effect_test.go new file mode 100644 index 000000000..0868fe6b0 --- /dev/null +++ b/module/services/accounts/code/pkg/infra/postgres_audit_effect_test.go @@ -0,0 +1,70 @@ +//go:build !pure + +package infra_test + +import ( + "context" + "errors" + "testing" + + "accounts/pkg/business" + "github.com/stretchr/testify/require" +) + +func TestPostgresAuditEffectBinding(t *testing.T) { + org := seedOrg(t, seedUser(t)) + entry := business.AuditEntry{ID: business.NewIDString(), OrgID: org, + EventType: "example.changed", ActorID: business.NewIDString(), ActorType: business.ActorTypeUser, + Resource: "example", ResourceID: "entry", IdempotencyKey: "effect", Payload: map[string]any{"value": 1}} + reserve := func(e business.AuditEntry) (bool, error) { + var inserted bool + err := testStore.As(business.Identity{OrgID: e.OrgID}).Within(testCtx, func(ctx context.Context) error { + var err error + inserted, err = testStore.ReserveAuditEffect(ctx, e) + return err + }) + return inserted, err + } + inserted, err := reserve(entry) + require.NoError(t, err) + require.True(t, inserted) + retry := entry + retry.ID = business.NewIDString() + inserted, err = reserve(retry) + require.NoError(t, err) + require.False(t, inserted) + changed := retry + changed.Payload = map[string]any{"value": 2} + _, err = reserve(changed) + require.ErrorIs(t, err, business.ErrAuditIdempotencyConflict) + // Historical key-only reservations cannot be upgraded from a retry's claims. + legacy := entry + legacy.IdempotencyKey = "legacy" + require.True(t, reserveAuditTenant(t, org, string(entry.EventType), legacy.IdempotencyKey)) + _, err = reserve(legacy) + require.ErrorIs(t, err, business.ErrAuditIdempotencyUnverifiable) + // A failed enclosing effect frees the reservation with its transaction. + rolledBack := entry + rolledBack.IdempotencyKey = "rollback" + aborted := errors.New("effect transaction aborted") + err = testStore.As(business.Identity{OrgID: org}).Within(testCtx, func(ctx context.Context) error { + inserted, err := testStore.ReserveAuditEffect(ctx, rolledBack) + if err != nil { + return err + } + require.True(t, inserted) + return aborted + }) + require.ErrorIs(t, err, aborted) + rolledBack.Payload = map[string]any{"value": 3} + inserted, err = reserve(rolledBack) + require.NoError(t, err) + require.True(t, inserted) + // The same event/key in a different tenant is an independent effect. + other := entry + other.OrgID = seedOrg(t, seedUser(t)) + other.ID = business.NewIDString() + inserted, err = reserve(other) + require.NoError(t, err) + require.True(t, inserted) +} diff --git a/module/services/accounts/generated/authz-methods.json b/module/services/accounts/generated/authz-methods.json index 58179b92a..9d257425e 100644 --- a/module/services/accounts/generated/authz-methods.json +++ b/module/services/accounts/generated/authz-methods.json @@ -4475,6 +4475,43 @@ "policy_sha256": "45df71bfa7f313b565b0e8c314557847bab913990d43ba59b97d755412b4a4fd", "rate_limit_backend_failure_mode": "RATE_LIMIT_BACKEND_FAILURE_MODE_FAIL_OPEN" }, + { + "procedure": "/saas.accounts.v1.ModuleCapabilitiesService/LookupAuditEvent", + "full_service": "saas.accounts.v1.ModuleCapabilitiesService", + "method": "LookupAuditEvent", + "input_type": "saas.accounts.v1.ModuleEmitAuditEventRequest", + "output_type": "saas.accounts.v1.ModuleLookupAuditEventResponse", + "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/MintModuleOperationContext", "full_service": "saas.accounts.v1.ModuleCapabilitiesService", @@ -5005,6 +5042,43 @@ "policy_sha256": "45df71bfa7f313b565b0e8c314557847bab913990d43ba59b97d755412b4a4fd", "rate_limit_backend_failure_mode": "RATE_LIMIT_BACKEND_FAILURE_MODE_FAIL_OPEN" }, + { + "procedure": "/saas.accounts.v1.ModuleCapabilitiesService/VerifyWorkContextRuntimeBoundary", + "full_service": "saas.accounts.v1.ModuleCapabilitiesService", + "method": "VerifyWorkContextRuntimeBoundary", + "input_type": "saas.accounts.v1.VerifyWorkContextRuntimeBoundaryRequest", + "output_type": "saas.accounts.v1.VerifyWorkContextRuntimeBoundaryResponse", + "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_SECRET", + "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": "cb57ee7f4e96ab9548ead04103dfd3e9a96dce973c06a8bd62a9ac991eab1e16", + "rate_limit_backend_failure_mode": "RATE_LIMIT_BACKEND_FAILURE_MODE_FAIL_OPEN" + }, { "procedure": "/saas.accounts.v1.NotificationService/DeleteNotification", "full_service": "saas.accounts.v1.NotificationService", diff --git a/module/services/accounts/generated/service-catalog.json b/module/services/accounts/generated/service-catalog.json index bacfcb956..aafdf5953 100644 --- a/module/services/accounts/generated/service-catalog.json +++ b/module/services/accounts/generated/service-catalog.json @@ -226,6 +226,7 @@ "/saas.accounts.v1.ModuleCapabilitiesService/ListReadableSourceCollections", "/saas.accounts.v1.ModuleCapabilitiesService/ListSubjectVisibility", "/saas.accounts.v1.ModuleCapabilitiesService/ListSubscriptions", + "/saas.accounts.v1.ModuleCapabilitiesService/LookupAuditEvent", "/saas.accounts.v1.ModuleCapabilitiesService/MintModuleOperationContext", "/saas.accounts.v1.ModuleCapabilitiesService/MintModuleRegistration", "/saas.accounts.v1.ModuleCapabilitiesService/MintModuleWorkContext", @@ -239,7 +240,8 @@ "/saas.accounts.v1.ModuleCapabilitiesService/ReplayEvents", "/saas.accounts.v1.ModuleCapabilitiesService/RequestApproval", "/saas.accounts.v1.ModuleCapabilitiesService/Subscribe", - "/saas.accounts.v1.ModuleCapabilitiesService/Unsubscribe" + "/saas.accounts.v1.ModuleCapabilitiesService/Unsubscribe", + "/saas.accounts.v1.ModuleCapabilitiesService/VerifyWorkContextRuntimeBoundary" ] }, { @@ -6041,6 +6043,49 @@ "service": "accounts" } }, + { + "procedure": "/saas.accounts.v1.ModuleCapabilitiesService/LookupAuditEvent", + "service": "ModuleCapabilitiesService", + "full_service": "saas.accounts.v1.ModuleCapabilitiesService", + "method": "LookupAuditEvent", + "input_type": "saas.accounts.v1.ModuleEmitAuditEventRequest", + "output_type": "saas.accounts.v1.ModuleLookupAuditEventResponse", + "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": "Recover a committed audit event identity for matching intent under current emission authority.", + "owner": { + "module": "saas-starter", + "service": "accounts" + } + }, { "procedure": "/saas.accounts.v1.ModuleCapabilitiesService/MintModuleOperationContext", "service": "ModuleCapabilitiesService", @@ -6655,6 +6700,49 @@ "service": "accounts" } }, + { + "procedure": "/saas.accounts.v1.ModuleCapabilitiesService/VerifyWorkContextRuntimeBoundary", + "service": "ModuleCapabilitiesService", + "full_service": "saas.accounts.v1.ModuleCapabilitiesService", + "method": "VerifyWorkContextRuntimeBoundary", + "input_type": "saas.accounts.v1.VerifyWorkContextRuntimeBoundaryRequest", + "output_type": "saas.accounts.v1.VerifyWorkContextRuntimeBoundaryResponse", + "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_SECRET", + "response_sensitivity": "SENSITIVITY_CONFIDENTIAL", + "platform_role": "PLATFORM_ROLE_REQUIREMENT_NONE", + "authentication_factor_attempt": false, + "conditions": [], + "impersonation": "IMPERSONATION_REQUIREMENT_UNSPECIFIED" + }, + "policy_tier": "internal", + "description": "Attest current capability membership in an active host-owned runtime boundary.", + "owner": { + "module": "saas-starter", + "service": "accounts" + } + }, { "procedure": "/saas.accounts.v1.NotificationService/DeleteNotification", "service": "NotificationService", 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 f6d799cb7..b76477978 100644 --- a/module/services/accounts/proto/saas/accounts/v1/module_capabilities.proto +++ b/module/services/accounts/proto/saas/accounts/v1/module_capabilities.proto @@ -284,6 +284,12 @@ message ModuleCancelApprovalRequest { // Audit // --------------------------------------------------------------------------- +// Receipt for the exact requested audit intent, under current emission authority. +// Empty event_id means no receipt was visible, not proof that nothing happened. +message ModuleLookupAuditEventResponse { + string event_id = 1; +} + // ModuleEmitAuditEventRequest emits a registered audit event onto the tenant's // single audit spine. event_type must be registered in the code-owned catalog // (unregistered types are rejected, not stored free-form). solution names the @@ -307,7 +313,8 @@ message ModuleEmitAuditEventRequest { google.protobuf.Struct fields = 6; // idempotency_key deduplicates retried emits: two emits with the same // (tenant, event_type, idempotency_key) collapse to a single audit row, and - // the duplicate returns success without writing again. Empty disables + // an identical intent returns success without writing again. Changed intent or + // an older key without recorded intent returns FAILED_PRECONDITION. Empty disables // deduplication (every emit is a distinct event). Scoped by tenant so one // tenant's keys can never suppress another's events. string idempotency_key = 7 [(buf.validate.field).string.max_len = 255]; @@ -855,6 +862,16 @@ message ModulePlaceRecordResponse { string node_id = 1 [(buf.validate.field).string.uuid = true]; } +// Read-only attestation; identity and boundary come exclusively from this signed +// context. The caller separately authenticates as its consuming module. +message VerifyWorkContextRuntimeBoundaryRequest { + string forwarded_work_context_token = 1 [(buf.validate.field).string = {min_len: 1, max_len: 32768}]; +} +message VerifyWorkContextRuntimeBoundaryResponse { + string tenant_id = 1; + string boundary_id = 2; +} + // Exact-record delegated oracle. Tenant and subjects come only from the signed // forwarded Work Context; a caller cannot provide a scope path or principal. message CheckWorkContextRecordAccessRequest { @@ -959,6 +976,22 @@ service ModuleCapabilitiesService { } + // Attests a current capability under an active host-owned solution boundary. + // This read grants no authority and does not expose the registration seed. + rpc VerifyWorkContextRuntimeBoundary(VerifyWorkContextRuntimeBoundaryRequest) returns (VerifyWorkContextRuntimeBoundaryResponse) { + 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_SECRET + response_sensitivity: SENSITIVITY_CONFIDENTIAL + platform_role: PLATFORM_ROLE_REQUIREMENT_NONE + }; + } + // Checks current owner and every delegated actor against true record placement, // intersected with the verified capability's attenuated resource/action scope. rpc CheckWorkContextRecordAccess(CheckWorkContextRecordAccessRequest) returns (CheckWorkContextRecordAccessResponse) { @@ -1168,6 +1201,22 @@ service ModuleCapabilitiesService { platform_role: PLATFORM_ROLE_REQUIREMENT_NONE }; } + // Read-only recovery for the complete original intent. Requires a nonempty + // idempotency_key; same authority as EmitAuditEvent. No retry authorization is + // implied by an absent receipt. Old unbound keys return FAILED_PRECONDITION. + rpc LookupAuditEvent(ModuleEmitAuditEventRequest) returns (ModuleLookupAuditEventResponse) { + 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/auth-gateway/AGENTS.md b/module/services/auth-gateway/AGENTS.md index f09c76c4e..14f87877c 100644 --- a/module/services/auth-gateway/AGENTS.md +++ b/module/services/auth-gateway/AGENTS.md @@ -109,7 +109,31 @@ own service principal. This host checks only that the secret presented for a prefix matches its digest; it neither knows nor cares which process presents it. The token is short-lived and fetched **per registration attempt**, not cached across a gateway restart. Registration only adds a proxy target — every -proxied `/v1//*` request still runs the full ext_authz check. +proxied `/v1//*` request still authenticates. By default it runs the full +ext_authz bearer check; the trusted headless opt-in below is the only exception. + +## Explicit headless module transport + +The `gateway` configuration group may set `WORK_CONTEXT_MODULE_PREFIXES` to a +JSON array of exact registered prefixes. Default `[]` keeps bearer-only access; +malformed, duplicate or wildcard prefixes fail boot. Registration requests and +caller headers cannot enable this policy. Enable only targets whose handlers +independently enforce signed Work Context scopes and current authority. + +For an opted-in prefix, a request with **no Authorization header** must carry +exactly one valid Work Context signed by this host and addressed to that prefix. +The gateway calls Accounts' existing internal `CheckAuthorizationRevision` with +the signed owner and every actor's scopes, using its own internal credential. +A refusal is 403 and an unavailable authority is 503; neither reaches the target. +A bearer header, including a bad bearer, retains the original bearer path and +never falls back to Work Context admission. + +Only verified owner/tenant/session correlation headers are projected; no role +is invented. The unchanged signed Work Context reaches the target through the +same rate limiter and registered-upstream transport. The target still owns +operation authorization, receipt read-only enforcement and any protected +runtime-boundary checks. This mode grants no access to Accounts' catalog or +internal RPC routes and supplies no module identity credential. ## A registered client calls without a proxy diff --git a/module/services/auth-gateway/code/authz_catalog_gen.go b/module/services/auth-gateway/code/authz_catalog_gen.go index 0496fef26..662874666 100644 --- a/module/services/auth-gateway/code/authz_catalog_gen.go +++ b/module/services/auth-gateway/code/authz_catalog_gen.go @@ -118,6 +118,7 @@ var generatedAuthorizationByProcedure = map[string]generatedAuthorizationMetadat "/saas.accounts.v1.ModuleCapabilitiesService/ListReadableSourceCollections": {exposure: edgeExposureInternal, rateLimitClass: edgeRateLimitClassInternal, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "45df71bfa7f313b565b0e8c314557847bab913990d43ba59b97d755412b4a4fd"}, "/saas.accounts.v1.ModuleCapabilitiesService/ListSubjectVisibility": {exposure: edgeExposureInternal, rateLimitClass: edgeRateLimitClassInternal, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "45df71bfa7f313b565b0e8c314557847bab913990d43ba59b97d755412b4a4fd"}, "/saas.accounts.v1.ModuleCapabilitiesService/ListSubscriptions": {exposure: edgeExposureInternal, rateLimitClass: edgeRateLimitClassInternal, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "45df71bfa7f313b565b0e8c314557847bab913990d43ba59b97d755412b4a4fd"}, + "/saas.accounts.v1.ModuleCapabilitiesService/LookupAuditEvent": {exposure: edgeExposureInternal, rateLimitClass: edgeRateLimitClassInternal, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "45df71bfa7f313b565b0e8c314557847bab913990d43ba59b97d755412b4a4fd"}, "/saas.accounts.v1.ModuleCapabilitiesService/MintModuleOperationContext": {exposure: edgeExposureInternal, rateLimitClass: edgeRateLimitClassInternal, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "6abed5b17a9523f3598f9ba09a521c5a22f07be55c94202678ecc326839e5786"}, "/saas.accounts.v1.ModuleCapabilitiesService/MintModuleRegistration": {exposure: edgeExposureInternal, rateLimitClass: edgeRateLimitClassInternal, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "141a867f3b0d6b3edc62441ed5dbbf4038bc393106184a0a14fda93bb34c7daf"}, "/saas.accounts.v1.ModuleCapabilitiesService/MintModuleWorkContext": {exposure: edgeExposureInternal, rateLimitClass: edgeRateLimitClassInternal, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "9052eaf621478336efd89307cb584eab77f08b6c5e1913064e32c55cf971f0b0"}, @@ -132,6 +133,7 @@ var generatedAuthorizationByProcedure = map[string]generatedAuthorizationMetadat "/saas.accounts.v1.ModuleCapabilitiesService/RequestApproval": {exposure: edgeExposureInternal, rateLimitClass: edgeRateLimitClassInternal, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "45df71bfa7f313b565b0e8c314557847bab913990d43ba59b97d755412b4a4fd"}, "/saas.accounts.v1.ModuleCapabilitiesService/Subscribe": {exposure: edgeExposureInternal, rateLimitClass: edgeRateLimitClassInternal, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "45df71bfa7f313b565b0e8c314557847bab913990d43ba59b97d755412b4a4fd"}, "/saas.accounts.v1.ModuleCapabilitiesService/Unsubscribe": {exposure: edgeExposureInternal, rateLimitClass: edgeRateLimitClassInternal, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "45df71bfa7f313b565b0e8c314557847bab913990d43ba59b97d755412b4a4fd"}, + "/saas.accounts.v1.ModuleCapabilitiesService/VerifyWorkContextRuntimeBoundary": {exposure: edgeExposureInternal, rateLimitClass: edgeRateLimitClassInternal, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "cb57ee7f4e96ab9548ead04103dfd3e9a96dce973c06a8bd62a9ac991eab1e16"}, "/saas.accounts.v1.NotificationService/DeleteNotification": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassStandardWrite, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "3c99c66c7a95d5e63acdd1625d78d33f5e3c33cd5faff67253b6a2d0306acb55"}, "/saas.accounts.v1.NotificationService/GetUnreadCount": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassStandardRead, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "3bdbf3230fa8c384ff8b4f4582c6da0954a1fb657604c6fd24e4f74f210b693f"}, "/saas.accounts.v1.NotificationService/ListNotifications": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassStandardRead, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "3bdbf3230fa8c384ff8b4f4582c6da0954a1fb657604c6fd24e4f74f210b693f"}, diff --git a/module/services/auth-gateway/code/configuration.go b/module/services/auth-gateway/code/configuration.go index 45b601c36..bf7fa6d38 100644 --- a/module/services/auth-gateway/code/configuration.go +++ b/module/services/auth-gateway/code/configuration.go @@ -1,6 +1,8 @@ package main import ( + "encoding/json" + "fmt" "os" "strings" @@ -21,3 +23,23 @@ func workspaceEnv(configuration, key string) string { } return os.Getenv(key) } + +// Only targets whose handlers enforce Work Context scopes may be enabled. +// Empty configuration leaves all federated routes on bearer authentication. +func parseHeadlessModulePrefixes(raw string) (map[string]bool, error) { + out := map[string]bool{} + if strings.TrimSpace(raw) == "" { + return out, nil + } + var prefixes []string + if err := json.Unmarshal([]byte(raw), &prefixes); err != nil || prefixes == nil { + return nil, fmt.Errorf("WORK_CONTEXT_MODULE_PREFIXES must be a JSON array") + } + for _, prefix := range prefixes { + if !validCatalogIdentity(prefix) || out[prefix] { + return nil, fmt.Errorf("WORK_CONTEXT_MODULE_PREFIXES requires unique exact module prefixes") + } + out[prefix] = true + } + return out, nil +} diff --git a/module/services/auth-gateway/code/gateway.go b/module/services/auth-gateway/code/gateway.go index 0a9b3af67..36820e9e4 100644 --- a/module/services/auth-gateway/code/gateway.go +++ b/module/services/auth-gateway/code/gateway.go @@ -61,6 +61,8 @@ type Gateway struct { // registrationReplay makes each solution-registration credential single-use. registrationReplay *registrationReplayGuard workContext *workContextVerifier + // Trusted startup policy only; registration and request input cannot enable it. + headlessModulePrefixes map[string]bool } // NewGateway constructs a gateway with explicit route matching. diff --git a/module/services/auth-gateway/code/gateway_headless_contract_test.go b/module/services/auth-gateway/code/gateway_headless_contract_test.go new file mode 100644 index 000000000..45377b761 --- /dev/null +++ b/module/services/auth-gateway/code/gateway_headless_contract_test.go @@ -0,0 +1,42 @@ +package main + +import ( + "context" + "crypto/ed25519" + "net/http" + "net/http/httptest" + "testing" + + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" + "github.com/codefly-dev/sdk-go/workcontext" + "github.com/stretchr/testify/require" +) + +// A target-scoped, cryptographically valid Work Context is currently an +// attenuation, not a substitute for a gateway access credential. This pins the +// existing perimeter while a separate installed headless transport is designed. +func TestGateway_Module_ValidWorkContextAloneDoesNotAuthenticate(t *testing.T) { + gw, _, _, priv := newGatewayHarness(t) + upstream, address := newModuleUpstream(t) + require.Equal(t, http.StatusOK, registerModule(t, gw, priv, "documents", address).Code) + keys := jwksServer(t, jwksDocument(map[string]ed25519.PublicKey{"headless-test": priv.Public().(ed25519.PublicKey)}), nil) + gw.workContext = newWorkContextVerifier(keys.URL) + signer, err := workcontext.NewWorkContextSigner(workcontext.WorkContextSignerOptions{ + Issuer: "saas-starter", KeyID: "headless-test", PrivateKey: priv, + }) + require.NoError(t, err) + token, _, err := signer.StartTask(workcontext.StartTaskInput{ + Audience: "documents", TenantID: "tenant-1", OwnerPrincipalID: "user-1", + TaskID: "task-1", SessionID: "session-1", + AuthorityScopes: []*basev0.WorkScopeV1{{ResourceKind: "documents", Actions: []string{"read"}}}, + }) + require.NoError(t, err) + require.NoError(t, gw.workContext.Verify(context.Background(), token), "must not mistake invalid capability rejection for bearer enforcement") + req := httptest.NewRequest(http.MethodGet, "/v1/documents/collection", nil) + req.Header.Set(workcontext.WorkContextHeaderName, token.Encoded()) + result := httptest.NewRecorder() + gw.ServeHTTP(result, req) + require.Equal(t, http.StatusUnauthorized, result.Code) + require.Contains(t, result.Body.String(), "authentication required") + require.Nil(t, upstream.lastHeaders, "no effect may reach the registered target") +} diff --git a/module/services/auth-gateway/code/gateway_headless_route_test.go b/module/services/auth-gateway/code/gateway_headless_route_test.go new file mode 100644 index 000000000..1418e3949 --- /dev/null +++ b/module/services/auth-gateway/code/gateway_headless_route_test.go @@ -0,0 +1,134 @@ +package main + +import ( + apigen "auth-gateway/external/saas-starter/accounts" + "context" + "crypto/ed25519" + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" + "github.com/codefly-dev/sdk-go/workcontext" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" + "google.golang.org/grpc/test/bufconn" + "net" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" + "time" +) + +func TestGatewayHeadlessOptInProxyAndRefusals(t *testing.T) { + gw, _, _, priv := newGatewayHarness(t) + upstream, address := newModuleUpstream(t) + require.Equal(t, 200, registerModule(t, gw, priv, "documents", address).Code) + keys := jwksServer(t, jwksDocument(map[string]ed25519.PublicKey{"headless": priv.Public().(ed25519.PublicKey)}), nil) + gw.workContext = newWorkContextVerifier(keys.URL) + gw.headlessModulePrefixes = map[string]bool{"documents": true} + var calls, replyCode atomic.Int32 + listener := bufconn.Listen(1 << 20) + server := grpc.NewServer() + apigen.RegisterWorkContextServiceServer(server, &revisionOracleFixture{check: func(ctx context.Context, req *apigen.CheckAuthorizationRevisionRequest) error { + calls.Add(1) + require.Equal(t, "tenant-1", req.OrgId) + require.Equal(t, "owner-1", req.OwnerPrincipalId) + if code := codes.Code(replyCode.Load()); code != codes.OK { + return status.Error(code, "oracle refusal") + } + return nil + }}) + go server.Serve(listener) + t.Cleanup(server.Stop) + t.Cleanup(func() { _ = listener.Close() }) + conn, err := grpc.NewClient("passthrough:///route-authority", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return listener.Dial() })) + require.NoError(t, err) + t.Cleanup(func() { _ = conn.Close() }) + gw.authz.backendConn = conn + gw.authz.internalToken = "fixture-internal-token" + mint := func(audience string, expired bool) string { + now := time.Now + if expired { + now = func() time.Time { return time.Now().Add(-time.Hour) } + } + signer, err := workcontext.NewWorkContextSigner(workcontext.WorkContextSignerOptions{Issuer: "saas-starter", KeyID: "headless", PrivateKey: priv, Now: now}) + require.NoError(t, err) + token, _, err := signer.StartTask(workcontext.StartTaskInput{Audience: audience, TenantID: "tenant-1", OwnerPrincipalID: "owner-1", TaskID: "task-1", SessionID: "session-1", AuthorizationRevision: 42, TTL: time.Minute, AuthorityScopes: []*basev0.WorkScopeV1{{ResourceKind: "documents", Actions: []string{"read"}}}}) + require.NoError(t, err) + return token.Encoded() + } + valid := mint("documents", false) + request := func(token string, mutate func(*http.Request)) *httptest.ResponseRecorder { + req := httptest.NewRequest("GET", "/v1/documents/collection", nil) + if token != "" { + req.Header.Set(workcontext.WorkContextHeaderName, token) + } + if mutate != nil { + mutate(req) + } + out := httptest.NewRecorder() + gw.ServeHTTP(out, req) + return out + } + for _, tc := range []struct { + name, token string + change func(*http.Request) + }{ + {name: "missing"}, {name: "wrong audience", token: mint("another", false)}, + {name: "expired", token: mint("documents", true)}, {name: "malformed", token: "invalid"}, + {name: "duplicate", token: valid, change: func(r *http.Request) { r.Header.Add(workcontext.WorkContextHeaderName, valid) }}, + {name: "bad bearer no fallback", token: valid, change: func(r *http.Request) { r.Header.Set("Authorization", "Bearer invalid") }}, + } { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, 401, request(tc.token, tc.change).Code) + require.Nil(t, upstream.lastHeaders) + }) + } + require.Zero(t, calls.Load(), "invalid request must not reach authority oracle") + for _, tc := range []struct { + code codes.Code + want int + }{{codes.PermissionDenied, 403}, {codes.FailedPrecondition, 403}, {codes.Unavailable, 503}} { + replyCode.Store(int32(tc.code)) + require.Equal(t, tc.want, request(valid, nil).Code) + require.Nil(t, upstream.lastHeaders) + } + replyCode.Store(int32(codes.OK)) + good := request(valid, func(r *http.Request) { + r.Header.Set("X-User-Id", "forged") + r.Header.Set("X-Org-Id", "forged") + r.Header.Set("X-Org-Role", "admin") + r.Header.Set("X-Codefly-Internal-Token", "forged") + r.Header.Set("Grpc-Metadata-X-User-Id", "forged") + }) + require.Equal(t, 200, good.Code) + require.Equal(t, "owner-1", upstream.lastHeaders.Get("X-User-Id")) + require.Equal(t, "tenant-1", upstream.lastHeaders.Get("X-Org-Id")) + for _, name := range []string{"X-Org-Role", "X-Codefly-Internal-Token", "Grpc-Metadata-X-User-Id"} { + require.Empty(t, upstream.lastHeaders.Get(name)) + } + require.Equal(t, valid, upstream.lastHeaders.Get(workcontext.WorkContextHeaderName)) + // Bearer requests retain their existing path even on an opted-in target. + require.Equal(t, 200, request("", func(r *http.Request) { r.Header.Set("Authorization", "Bearer "+signValidToken(t, priv)) }).Code) + gw.rateLimiter = NewRateLimiter(1) + // Existing limiter permits one request plus one burst request. + require.Equal(t, 200, request(valid, nil).Code) + require.Equal(t, 200, request(valid, nil).Code) + require.Equal(t, 429, request(valid, func(r *http.Request) { r.Header.Set("X-Org-Id", "another-budget") }).Code) +} + +func TestHeadlessModulePolicyRequiresExactOptIn(t *testing.T) { + for _, raw := range []string{"", "[]"} { + got, err := parseHeadlessModulePrefixes(raw) + require.NoError(t, err) + require.Empty(t, got) + } + got, err := parseHeadlessModulePrefixes(`["documents"]`) + require.NoError(t, err) + require.Equal(t, map[string]bool{"documents": true}, got) + for _, raw := range []string{`null`, `{}`, `["*"]`, `["documents/other"]`, `["documents","documents"]`, `[""]`, `[1]`} { + _, err := parseHeadlessModulePrefixes(raw) + require.Error(t, err, raw) + } +} diff --git a/module/services/auth-gateway/code/gateway_modules.go b/module/services/auth-gateway/code/gateway_modules.go index 983c419d9..311bbe54c 100644 --- a/module/services/auth-gateway/code/gateway_modules.go +++ b/module/services/auth-gateway/code/gateway_modules.go @@ -11,8 +11,9 @@ package main // // Registration only adds a proxy target; it never relaxes authentication. Every // proxied /v1//* request runs the same ext_authz Check and identity -// projection as a protected catalog route, so a bearer-less call is denied at -// the gateway (401) regardless of what is registered. +// projection as a protected catalog route, unless trusted startup policy +// explicitly enables Work Context admission for that prefix. Registration +// itself never enables that mode. // // Trust model: registration is gated on a per-module cryptographic identity, not // the shared cluster-internal token. The caller presents a signed registration @@ -58,6 +59,7 @@ import ( accountsv1 "auth-gateway/pkg/gen/saas/accounts/v1" + "github.com/codefly-dev/sdk-go/workcontext" "github.com/golang-jwt/jwt/v5" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" @@ -226,28 +228,34 @@ func (g *Gateway) handleFederatedModule(w http.ResponseWriter, r *http.Request) // Same discipline as every protected catalog route: drop caller-supplied // identity, run ext_authz, require a valid credential, and subject the - // forwarded request to the same rate-limit budget (below). A bearer-less - // call is denied here, so federation only adds a proxy target — it never - // widens the authenticated surface, and it does not open an unmetered one. - stripAllIdentityHeaders(r) - checkResp, err := g.authz.Check(r.Context(), buildCheckRequest(r)) - if err != nil { - httpError(w, http.StatusInternalServerError, "auth check failed") - return true - } - if denied := checkResp.GetDeniedResponse(); denied != nil { - code := int(denied.GetStatus().GetCode()) - if code == 0 { - code = http.StatusForbidden + // forwarded request to the same rate-limit budget (below). Headless admission + // is separately opted in by trusted startup policy; registration cannot widen + // the authenticated surface or open an unmetered one. + if g.headlessModulePrefixes[prefix] && len(r.Header.Values("Authorization")) == 0 { + if !g.authorizeHeadlessModule(w, r, prefix) { + return true } - httpError(w, code, denied.GetBody()) - return true - } - if int(checkResp.GetStatus().GetCode()) != int(codes.OK) { - httpError(w, http.StatusForbidden, "forbidden") - return true + } else { + stripAllIdentityHeaders(r) + checkResp, err := g.authz.Check(r.Context(), buildCheckRequest(r)) + if err != nil { + httpError(w, http.StatusInternalServerError, "auth check failed") + return true + } + if denied := checkResp.GetDeniedResponse(); denied != nil { + code := int(denied.GetStatus().GetCode()) + if code == 0 { + code = http.StatusForbidden + } + httpError(w, code, denied.GetBody()) + return true + } + if int(checkResp.GetStatus().GetCode()) != int(codes.OK) { + httpError(w, http.StatusForbidden, "forbidden") + return true + } + injectHeaders(r, checkResp.GetOkResponse().GetHeaders()) } - injectHeaders(r, checkResp.GetOkResponse().GetHeaders()) // Forward the full /v1//... path unchanged: the module owns and // serves its own /v1/ surface. The caller's bearer is preserved so @@ -973,3 +981,46 @@ func isAllowedResolvedModuleIP(ip net.IP) bool { } return ip.IsLoopback() || ip.IsPrivate() } + +// authorizeHeadlessModule is called only for a registered prefix explicitly +// enabled by trusted startup policy and only when Authorization is absent. +// A bad bearer is never downgraded to Work Context authentication. +func (g *Gateway) authorizeHeadlessModule(w http.ResponseWriter, r *http.Request, prefix string) bool { + values := r.Header.Values(workcontext.WorkContextHeaderName) + if len(values) != 1 || values[0] == "" { + httpError(w, http.StatusUnauthorized, "one work context required") + return false + } + if g.workContext == nil { + httpError(w, http.StatusServiceUnavailable, "work context authority unavailable") + return false + } + token, err := workcontext.ParseWorkContextToken(values[0]) + if err != nil { + httpError(w, http.StatusUnauthorized, "invalid work context") + return false + } + claims, err := g.workContext.VerifyForAudience(r.Context(), token, prefix) + if err != nil { + httpError(w, http.StatusUnauthorized, "invalid work context") + return false + } + if err = g.authz.checkCurrentWorkContext(r.Context(), claims); err != nil { + switch status.Code(err) { + case codes.PermissionDenied, codes.FailedPrecondition, codes.Unauthenticated, codes.InvalidArgument: + httpError(w, http.StatusForbidden, "work context authority refused") + default: + httpError(w, http.StatusServiceUnavailable, "work context authority unavailable") + } + return false + } + stripAllIdentityHeaders(r) + // These identify the original owner and tenant for metering/correlation, not + // a role or permission grant. The target must authorize the unchanged signed + // context, including its actor chain, on every operation. + r.Header.Set("X-User-Id", claims.GetOwnerPrincipalId()) + r.Header.Set("X-Org-Id", claims.GetTenantId()) + r.Header.Set("X-Session-Id", claims.GetSessionId()) + r.Header.Set("X-Credential-Kind", "work_context") + return true +} diff --git a/module/services/auth-gateway/code/main.go b/module/services/auth-gateway/code/main.go index a1ebb1a3e..8cd8bd4ef 100644 --- a/module/services/auth-gateway/code/main.go +++ b/module/services/auth-gateway/code/main.go @@ -233,6 +233,15 @@ func main() { // reach this replica without restarting it. clientRegistry := &accountsClientRegistry{conn: internalAPIConn, internalToken: authz.internalToken} gateway := NewGateway(authz, matcher, upstreams, rateLimiter, solutionRegistry, clientRegistry) + headlessPrefixes, err := parseHeadlessModulePrefixes(workspaceEnv("gateway", "WORK_CONTEXT_MODULE_PREFIXES")) + if err != nil { + panic(err) + } + if len(headlessPrefixes) > 0 && apiHTTPURL == "" { + panic("headless module routes require Work Context key discovery") + } + gateway.headlessModulePrefixes = headlessPrefixes + go gateway.solutions.reconcile(ctx) go gateway.clients.reconcile(ctx) if apiHTTPURL != "" { diff --git a/module/services/auth-gateway/code/work_context.go b/module/services/auth-gateway/code/work_context.go index f1d3c2586..4dd88446f 100644 --- a/module/services/auth-gateway/code/work_context.go +++ b/module/services/auth-gateway/code/work_context.go @@ -9,6 +9,7 @@ import ( "strings" "time" + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" "github.com/codefly-dev/sdk-go/workcontext" ) @@ -61,18 +62,35 @@ func (v *workContextVerifier) Refresh(ctx context.Context) error { // signature, freshness, and attenuation of the token against the published // keys; audience and scope are the callee's concern and are not asserted here. func (v *workContextVerifier) Verify(ctx context.Context, token workcontext.WorkContextToken) error { + _, err := v.verify(ctx, token, workcontext.WorkContextExpectations{}) + return err +} + +// VerifyForAudience is for explicitly enabled headless module routes. Unlike +// optional edge attenuation, admission must bind the capability to the selected +// target and this host's issuer. It does not establish current authorization; +// callers must check the sealed revision with Accounts before proxying. +func (v *workContextVerifier) VerifyForAudience(ctx context.Context, token workcontext.WorkContextToken, audience string) (*basev0.WorkContextV1, error) { + if audience == "" { + return nil, invalidWorkContext(fmt.Errorf("target audience required")) + } + return v.verify(ctx, token, workcontext.WorkContextExpectations{Issuer: "saas-starter", Audience: audience}) +} + +func (v *workContextVerifier) verify(ctx context.Context, token workcontext.WorkContextToken, expectations workcontext.WorkContextExpectations) (*basev0.WorkContextV1, error) { keyID, err := workContextTokenKeyID(token) if err != nil { - return err + return nil, err } verifier, err := v.cache.resolve(ctx, keyID) if err != nil { - return err + return nil, err } - if _, err := verifier.Verify(token, workcontext.WorkContextExpectations{}); err != nil { - return invalidWorkContext(err) + claims, err := verifier.Verify(token, expectations) + if err != nil { + return nil, invalidWorkContext(err) } - return nil + return claims, nil } func workContextTokenKeyID(token workcontext.WorkContextToken) (string, error) { diff --git a/module/services/auth-gateway/code/work_context_authority.go b/module/services/auth-gateway/code/work_context_authority.go new file mode 100644 index 000000000..66de8e683 --- /dev/null +++ b/module/services/auth-gateway/code/work_context_authority.go @@ -0,0 +1,66 @@ +package main + +import ( + "context" + + accountsv1 "auth-gateway/external/saas-starter/accounts" + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" +) + +const checkWorkContextRevisionMethod = "/saas.accounts.v1.WorkContextService/CheckAuthorizationRevision" + +// checkCurrentWorkContext calls the existing consumer revocation oracle as the +// gateway itself. claims must first pass signature, issuer and target-audience +// verification. No module credential is borrowed and no caller identity header +// contributes to this request. Accounts owns current RBAC/delegation decisions. +func (s *ExtAuthz) checkCurrentWorkContext(ctx context.Context, claims *basev0.WorkContextV1) error { + if s == nil || s.backendConn == nil || s.internalToken == "" { + return status.Error(codes.Unavailable, "work context authority unavailable") + } + if claims == nil || claims.GetTenantId() == "" || claims.GetOwnerPrincipalId() == "" || claims.GetAuthorizationRevision() == 0 { + return status.Error(codes.PermissionDenied, "incomplete work context authority") + } + owner, err := revisionSubject(claims.GetOwnerPrincipalId(), claims.GetAuthorityScopes()) + if err != nil { + return err + } + request := &accountsv1.CheckAuthorizationRevisionRequest{ + OrgId: claims.GetTenantId(), OwnerPrincipalId: claims.GetOwnerPrincipalId(), + AuthorizationRevision: claims.GetAuthorizationRevision(), + Subjects: []*accountsv1.WorkContextRevisionSubject{owner}, + } + for _, actor := range claims.GetActorChain() { + if actor == nil { + return status.Error(codes.PermissionDenied, "invalid work context actor") + } + subject, err := revisionSubject(actor.GetPrincipalId(), actor.GetGrantedScopes()) + if err != nil { + return err + } + request.Subjects = append(request.Subjects, subject) + } + // No inherited caller credentials or identity metadata cross this internal hop. + ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs("x-codefly-internal-token", s.internalToken)) + return s.backendConn.Invoke(ctx, checkWorkContextRevisionMethod, request, &emptypb.Empty{}) +} + +func revisionSubject(principal string, scopes []*basev0.WorkScopeV1) (*accountsv1.WorkContextRevisionSubject, error) { + if principal == "" || len(scopes) == 0 { + return nil, status.Error(codes.PermissionDenied, "incomplete work context subject") + } + subject := &accountsv1.WorkContextRevisionSubject{PrincipalId: principal} + for _, scope := range scopes { + if scope == nil || scope.GetResourceKind() == "" || len(scope.GetActions()) == 0 { + return nil, status.Error(codes.PermissionDenied, "invalid work context scope") + } + subject.Scopes = append(subject.Scopes, &accountsv1.WorkContextScope{ + ResourceKind: scope.GetResourceKind(), Actions: append([]string(nil), scope.GetActions()...), + ResourceIds: append([]string(nil), scope.GetResourceIds()...), + }) + } + return subject, nil +} diff --git a/module/services/auth-gateway/code/work_context_authority_test.go b/module/services/auth-gateway/code/work_context_authority_test.go new file mode 100644 index 000000000..7923a05d9 --- /dev/null +++ b/module/services/auth-gateway/code/work_context_authority_test.go @@ -0,0 +1,75 @@ +package main + +import ( + "context" + "net" + "testing" + + accountsv1 "auth-gateway/external/saas-starter/accounts" + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/grpc/test/bufconn" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/emptypb" +) + +type revisionOracleFixture struct { + accountsv1.UnimplementedWorkContextServiceServer + check func(context.Context, *accountsv1.CheckAuthorizationRevisionRequest) error +} + +func (f *revisionOracleFixture) CheckAuthorizationRevision(ctx context.Context, r *accountsv1.CheckAuthorizationRevisionRequest) (*emptypb.Empty, error) { + if err := f.check(ctx, r); err != nil { + return nil, err + } + return &emptypb.Empty{}, nil +} + +func TestCurrentWorkContextUsesGatewayOracleWithEverySignedSubject(t *testing.T) { + scopes := []*basev0.WorkScopeV1{{ResourceKind: "documents", Actions: []string{"read"}, ResourceIds: []string{"document-1"}}} + claims := &basev0.WorkContextV1{TenantId: "tenant-1", OwnerPrincipalId: "owner-1", AuthorizationRevision: 42, + AuthorityScopes: scopes, ActorChain: []*basev0.WorkActorV1{{PrincipalId: "actor-1", GrantedScopes: scopes}, {PrincipalId: "actor-2", GrantedScopes: scopes}}, + } + listener := bufconn.Listen(1 << 20) + server := grpc.NewServer() + calls := 0 + accountsv1.RegisterWorkContextServiceServer(server, &revisionOracleFixture{check: func(ctx context.Context, request *accountsv1.CheckAuthorizationRevisionRequest) error { + calls++ + md, _ := metadata.FromIncomingContext(ctx) + require.Equal(t, []string{"test-gateway-credential"}, md.Get("x-codefly-internal-token")) + require.Empty(t, md.Get("authorization")) + require.Empty(t, md.Get("x-user-id")) + require.Equal(t, "tenant-1", request.OrgId) + require.Equal(t, "owner-1", request.OwnerPrincipalId) + require.Equal(t, uint64(42), request.AuthorizationRevision) + require.Len(t, request.Subjects, 3) + for i, id := range []string{"owner-1", "actor-1", "actor-2"} { + require.Equal(t, id, request.Subjects[i].PrincipalId) + require.True(t, proto.Equal(&accountsv1.WorkContextScope{ResourceKind: "documents", Actions: []string{"read"}, ResourceIds: []string{"document-1"}}, request.Subjects[i].Scopes[0])) + } + if calls > 1 { + return status.Error(codes.PermissionDenied, "authority revoked") + } + return nil + }}) + go server.Serve(listener) + t.Cleanup(server.Stop) + t.Cleanup(func() { _ = listener.Close() }) + conn, err := grpc.NewClient("passthrough:///authority-fixture", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return listener.Dial() })) + require.NoError(t, err) + t.Cleanup(func() { _ = conn.Close() }) + authz := &ExtAuthz{backendConn: conn, internalToken: "test-gateway-credential"} + ctx := metadata.NewOutgoingContext(context.Background(), metadata.Pairs("authorization", "caller-credential", "x-user-id", "forged-person")) + require.NoError(t, authz.checkCurrentWorkContext(ctx, claims)) + require.Equal(t, codes.PermissionDenied, status.Code(authz.checkCurrentWorkContext(ctx, claims))) + require.Equal(t, 2, calls, "current authority is neither cached nor retried") + claims.ActorChain[1] = nil + require.Equal(t, codes.PermissionDenied, status.Code(authz.checkCurrentWorkContext(ctx, claims))) + require.Equal(t, 2, calls, "invalid subject must not reach the oracle") + require.Equal(t, codes.Unavailable, status.Code((&ExtAuthz{}).checkCurrentWorkContext(ctx, claims))) +} diff --git a/module/services/auth-gateway/code/work_context_test.go b/module/services/auth-gateway/code/work_context_test.go index 5576c6ad1..e15b54db6 100644 --- a/module/services/auth-gateway/code/work_context_test.go +++ b/module/services/auth-gateway/code/work_context_test.go @@ -351,3 +351,27 @@ func TestGateway_NoWorkContextHeaderUnaffected(t *testing.T) { require.Equal(t, http.StatusOK, w.Code) require.NotNil(t, apiFake.lastHeaders) } + +func TestWorkContextVerifierAudienceAdmission(t *testing.T) { + pub, priv := mustEd25519(t) + const kid = "audience-key" + server := jwksServer(t, jwksDocument(map[string]ed25519.PublicKey{kid: pub}), nil) + verifier := newWorkContextVerifier(server.URL) + token := mintWorkContext(t, kid, priv, nil) + claims, err := verifier.VerifyForAudience(context.Background(), token, "solution:demo") + require.NoError(t, err) + require.Equal(t, "user-1", claims.GetOwnerPrincipalId()) + require.Equal(t, "tenant-1", claims.GetTenantId()) + for _, audience := range []string{"", "another-target"} { + claims, err = verifier.VerifyForAudience(context.Background(), token, audience) + require.ErrorIs(t, err, workcontext.ErrWorkContextInvalid) + require.Nil(t, claims) + } + otherSigner, err := workcontext.NewWorkContextSigner(workcontext.WorkContextSignerOptions{Issuer: "another-issuer", KeyID: kid, PrivateKey: priv}) + require.NoError(t, err) + otherToken, _, err := otherSigner.StartTask(workcontext.StartTaskInput{Audience: "solution:demo", TenantID: "tenant-1", OwnerPrincipalID: "user-1", TaskID: "task-1", SessionID: "session-1", AuthorityScopes: []*basev0.WorkScopeV1{{ResourceKind: "audit", Actions: []string{"read"}}}}) + require.NoError(t, err) + claims, err = verifier.VerifyForAudience(context.Background(), otherToken, "solution:demo") + require.ErrorIs(t, err, workcontext.ErrWorkContextInvalid) + require.Nil(t, claims) +} 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 e5fad3b34..ca671166c 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+CgtyZWFkX2dyYW50cxgJIAMoCzIpLnNhYXMuYWNjb3VudHMudjEuUmVhZGFibGVDb2xsZWN0aW9uR3JhbnQSOAoEc3luYxgKIAEoCzIqLnNhYXMuYWNjb3VudHMudjEuQ29sbGVjdGlvblN5bmNQcm92ZW5hbmNlIrABChdSZWFkYWJsZUNvbGxlY3Rpb25HcmFudBIVCg1zdWJqZWN0X2xhYmVsGAEgASgJEhQKDHN1YmplY3Rfa2luZBgCIAEoCRIRCglyb2xlX25hbWUYAyABKAkSEgoKc2NvcGVfcGF0aBgEIAEoCRIRCglpbmhlcml0ZWQYBSABKAgSLgoKZXhwaXJlc19hdBgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiqQIKGENvbGxlY3Rpb25TeW5jUHJvdmVuYW5jZRIwCgVzdGFnZRgBIAEoDjIhLnNhYXMuYWNjb3VudHMudjEuU291cmNlU3luY1N0YWdlEiYKAmF0GAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIQCghyZXZpc2lvbhgDIAEoCRIPCgd0cmlnZ2VyGAQgASgJEkIKFHJlcXVlc3Rlcl9kaXNjbG9zdXJlGAUgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5NZXRhZGF0YURpc2Nsb3N1cmUSMAoMcmVxdWVzdGVkX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIaChJyZXF1ZXN0ZWRfYnlfbGFiZWwYByABKAkiYwokTGlzdFJlYWRhYmxlU291cmNlQ29sbGVjdGlvbnNSZXF1ZXN0Eh0KCXBhZ2Vfc2l6ZRgBIAEoBUIKukgHGgUY6AcoABIcCgpwYWdlX3Rva2VuGAIgASgJQgi6SAVyAxiAECKBAQolTGlzdFJlYWRhYmxlU291cmNlQ29sbGVjdGlvbnNSZXNwb25zZRI/Cgtjb2xsZWN0aW9ucxgBIAMoCzIqLnNhYXMuYWNjb3VudHMudjEuUmVhZGFibGVTb3VyY2VDb2xsZWN0aW9uEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSLpAQoYTW9kdWxlUGxhY2VSZWNvcmRSZXF1ZXN0EhgKBnRlbmFudBgBIAEoCUIIukgFcgOwAQESPQoKc2NvcGVfcGF0aBgCIAEoCUIpukgmciQQARiABDIdXlthLXowLTlfXSsoPzpcLlthLXowLTlfXSspKiQSFwoEa2luZBgDIAEoCUIJukgGcgQQARhAEhcKBWxhYmVsGAQgASgJQgi6SAVyAxj/ARIhCg1yZXNvdXJjZV90eXBlGAUgASgJQgq6SAdyBRABGIABEh8KC3Jlc291cmNlX2lkGAYgASgJQgq6SAdyBRABGIAEIjYKGU1vZHVsZVBsYWNlUmVjb3JkUmVzcG9uc2USGQoHbm9kZV9pZBgBIAEoCUIIukgFcgOwAQEihQEKI0NoZWNrV29ya0NvbnRleHRSZWNvcmRBY2Nlc3NSZXF1ZXN0EiEKDXJlc291cmNlX3R5cGUYASABKAlCCrpIB3IFEAEYgAISHwoLcmVzb3VyY2VfaWQYAiABKAlCCrpIB3IFEAEYgAQSGgoGYWN0aW9uGAMgASgJQgq6SAdyBRABGIACIk4KJENoZWNrV29ya0NvbnRleHRSZWNvcmRBY2Nlc3NSZXNwb25zZRIPCgdhbGxvd2VkGAEgASgIEhUKDXNjb3BlX25vZGVfaWQYAiABKAkifAoqTW9kdWxlRXhjaGFuZ2VEZWxlZ2F0ZWRSZWFkQXVkaWVuY2VSZXF1ZXN0Eh4KCmJpbmRpbmdfaWQYASABKAlCCrpIB3IFEAEYgAESLgoZcGFyZW50X3dvcmtfY29udGV4dF90b2tlbhgCIAEoCUILukgIcgYQARiAgAIigAMKL01vZHVsZUV4Y2hhbmdlRGVsZWdhdGVkT3BlcmF0aW9uQXVkaWVuY2VSZXF1ZXN0Eh4KCmJpbmRpbmdfaWQYASABKAlCCrpIB3IFEAEYgAESLAoZcGFyZW50X3dvcmtfY29udGV4dF90b2tlbhgCIAEoCUIJukgGcgQYgIACEg4KBmxvb2t1cBgDIAEoCBIiCg1kZWxlZ2F0aW9uX2lkGAQgASgJQgu6SAjYAQFyA7ABATrKAbpIxgEawwEKM2V4Y2hhbmdlX2RlbGVnYXRlZF9vcGVyYXRpb25fYXVkaWVuY2Uub25lX2F1dGhvcml0eRJGZXhhY3RseSBvbmUgb2YgcGFyZW50X3dvcmtfY29udGV4dF90b2tlbiBhbmQgZGVsZWdhdGlvbl9pZCBpcyByZXF1aXJlZBpEKHRoaXMucGFyZW50X3dvcmtfY29udGV4dF90b2tlbiAhPSAnJykgIT0gKHRoaXMuZGVsZWdhdGlvbl9pZCAhPSAnJykquAIKFE1vZHVsZUF1ZGl0RmllbGRLaW5kEicKI01PRFVMRV9BVURJVF9GSUVMRF9LSU5EX1VOU1BFQ0lGSUVEEAASIgoeTU9EVUxFX0FVRElUX0ZJRUxEX0tJTkRfU1RSSU5HEAESIAocTU9EVUxFX0FVRElUX0ZJRUxEX0tJTkRfVVVJRBACEh8KG01PRFVMRV9BVURJVF9GSUVMRF9LSU5EX0lOVBADEiIKHk1PRFVMRV9BVURJVF9GSUVMRF9LSU5EX05VTUJFUhAEEiAKHE1PRFVMRV9BVURJVF9GSUVMRF9LSU5EX0JPT0wQBRIgChxNT0RVTEVfQVVESVRfRklFTERfS0lORF9FTlVNEAYSKAokTU9EVUxFX0FVRElUX0ZJRUxEX0tJTkRfU1RSSU5HX0FSUkFZEAcqoQEKGk1vZHVsZUF1ZGl0RXZlbnRWaXNpYmlsaXR5Ei0KKU1PRFVMRV9BVURJVF9FVkVOVF9WSVNJQklMSVRZX1VOU1BFQ0lGSUVEEAASKAokTU9EVUxFX0FVRElUX0VWRU5UX1ZJU0lCSUxJVFlfVEVOQU5UEAESKgomTU9EVUxFX0FVRElUX0VWRU5UX1ZJU0lCSUxJVFlfRVhURVJOQUwQAircAQoaRGF0YXNvdXJjZUl0ZW1SZWFkZXJzQmFzaXMSLQopREFUQVNPVVJDRV9JVEVNX1JFQURFUlNfQkFTSVNfVU5TUEVDSUZJRUQQABIsCihEQVRBU09VUkNFX0lURU1fUkVBREVSU19CQVNJU19UUkFOU0xBVEVEEAESLworREFUQVNPVVJDRV9JVEVNX1JFQURFUlNfQkFTSVNfU09VUkNFX1NDT1BFRBACEjAKLERBVEFTT1VSQ0VfSVRFTV9SRUFERVJTX0JBU0lTX1VOVFJBTlNMQVRBQkxFEAMqaQoNRXZlbnREZWxpdmVyeRIeChpFVkVOVF9ERUxJVkVSWV9VTlNQRUNJRklFRBAAEhwKGEVWRU5UX0RFTElWRVJZX1VOT1JERVJFRBABEhoKFkVWRU5UX0RFTElWRVJZX09SREVSRUQQAip+ChJNZXRhZGF0YURpc2Nsb3N1cmUSIwofTUVUQURBVEFfRElTQ0xPU1VSRV9VTlNQRUNJRklFRBAAEiEKHU1FVEFEQVRBX0RJU0NMT1NVUkVfRElTQ0xPU0VEEAESIAocTUVUQURBVEFfRElTQ0xPU1VSRV9XSVRISEVMRBACKlwKD1NvdXJjZVN5bmNTdGFnZRIhCh1TT1VSQ0VfU1lOQ19TVEFHRV9VTlNQRUNJRklFRBAAEiYKIlNPVVJDRV9TWU5DX1NUQUdFX0NIQU5HRVNfRU5RVUVVRUQQATLRIwoZTW9kdWxlQ2FwYWJpbGl0aWVzU2VydmljZRLFAQodRXhjaGFuZ2VEZWxlZ2F0ZWRSZWFkQXVkaWVuY2USPC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUV4Y2hhbmdlRGVsZWdhdGVkUmVhZEF1ZGllbmNlUmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuSXNzdWVkV29ya0NvbnRleHQiQcLzGD0IAxABMAE6Kwonc2Fhcy5tb2R1bGUuZGVsZWdhdGVkX2F1ZGllbmNlX2V4Y2hhbmdlEARAAUgHUARYBGABEs8BCiJFeGNoYW5nZURlbGVnYXRlZE9wZXJhdGlvbkF1ZGllbmNlEkEuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFeGNoYW5nZURlbGVnYXRlZE9wZXJhdGlvbkF1ZGllbmNlUmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuSXNzdWVkV29ya0NvbnRleHQiQcLzGD0IAxABMAE6Kwonc2Fhcy5tb2R1bGUuZGVsZWdhdGVkX2F1ZGllbmNlX2V4Y2hhbmdlEARAAUgHUARYBGABEqcBChxDaGVja1dvcmtDb250ZXh0UmVjb3JkQWNjZXNzEjUuc2Fhcy5hY2NvdW50cy52MS5DaGVja1dvcmtDb250ZXh0UmVjb3JkQWNjZXNzUmVxdWVzdBo2LnNhYXMuYWNjb3VudHMudjEuQ2hlY2tXb3JrQ29udGV4dFJlY29yZEFjY2Vzc1Jlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESqgEKHUxpc3RSZWFkYWJsZVNvdXJjZUNvbGxlY3Rpb25zEjYuc2Fhcy5hY2NvdW50cy52MS5MaXN0UmVhZGFibGVTb3VyY2VDb2xsZWN0aW9uc1JlcXVlc3QaNy5zYWFzLmFjY291bnRzLnYxLkxpc3RSZWFkYWJsZVNvdXJjZUNvbGxlY3Rpb25zUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARKAAQoLUGxhY2VSZWNvcmQSKi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVBsYWNlUmVjb3JkUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUGxhY2VSZWNvcmRSZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEn0KCkVucXVldWVKb2ISKS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUVucXVldWVKb2JSZXF1ZXN0Giouc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFbnF1ZXVlSm9iUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARJ6CglDbGFpbUpvYnMSKC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUNsYWltSm9ic1JlcXVlc3QaKS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUNsYWltSm9ic1Jlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESgwEKDEhlYXJ0YmVhdEpvYhIrLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlSGVhcnRiZWF0Sm9iUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlSGVhcnRiZWF0Sm9iUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARJhCgZBY2tKb2ISJS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUFja0pvYlJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARJjCgdOYWNrSm9iEiYuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVOYWNrSm9iUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEn0KCk5vdGlmeVVzZXISKS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZU5vdGlmeVVzZXJSZXF1ZXN0Giouc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVOb3RpZnlVc2VyUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARKtAQoPTm90aWZ5T3JnQWRtaW5zEi4uc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVOb3RpZnlPcmdBZG1pbnNSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVOb3RpZnlPcmdBZG1pbnNSZXNwb25zZSI5wvMYNQgDEAEwATojCh9zYWFzLm1vZHVsZS5vcmdfYWRtaW5zX25vdGlmaWVkEAJAAUgHUANYA2ABEowBCg9SZXF1ZXN0QXBwcm92YWwSLi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVJlcXVlc3RBcHByb3ZhbFJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVJlcXVlc3RBcHByb3ZhbFJlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESdQoLR2V0QXBwcm92YWwSKi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUdldEFwcHJvdmFsUmVxdWVzdBogLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlQXBwcm92YWwiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARJxCg5DYW5jZWxBcHByb3ZhbBItLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlQ2FuY2VsQXBwcm92YWxSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAEScQoORW1pdEF1ZGl0RXZlbnQSLS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUVtaXRBdWRpdEV2ZW50UmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEsMBChZEZWNsYXJlQXVkaXRFdmVudFR5cGVzEjUuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVEZWNsYXJlQXVkaXRFdmVudFR5cGVzUmVxdWVzdBo2LnNhYXMuYWNjb3VudHMudjEuTW9kdWxlRGVjbGFyZUF1ZGl0RXZlbnRUeXBlc1Jlc3BvbnNlIjrC8xg2CAMQATABOiQKIHNhYXMubW9kdWxlLmF1ZGl0X3R5cGVzX2RlY2xhcmVkEAJAAUgHUANYA2ABEp4BChVMaXN0U3ViamVjdFZpc2liaWxpdHkSNC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUxpc3RTdWJqZWN0VmlzaWJpbGl0eVJlcXVlc3QaNS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUxpc3RTdWJqZWN0VmlzaWJpbGl0eVJlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESjgEKE0ZldGNoRGF0YXNvdXJjZUJsb2ISLC5zYWFzLmFjY291bnRzLnYxLkZldGNoRGF0YXNvdXJjZUJsb2JSZXF1ZXN0Giouc2Fhcy5hY2NvdW50cy52MS5GZXRjaERhdGFzb3VyY2VCbG9iQ2h1bmsiG4gCAcLzGBQIAxABMAE6AhABQAFIB1ADWANgATABEo4BChRGZXRjaERhdGFzb3VyY2VGaWxlcxItLnNhYXMuYWNjb3VudHMudjEuRmV0Y2hEYXRhc291cmNlRmlsZXNSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5GZXRjaERhdGFzb3VyY2VGaWxlc0ZyYW1lIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAEwARK2AQoWTWludE1vZHVsZVJlZ2lzdHJhdGlvbhIvLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlTWludFJlZ2lzdHJhdGlvblJlcXVlc3QaMC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZU1pbnRSZWdpc3RyYXRpb25SZXNwb25zZSI5wvMYNQgDEAEwATojCh9zYWFzLm1vZHVsZS5yZWdpc3RyYXRpb25fbWludGVkEAJAAUgHUARYBGABEr4BChhNaW50U29sdXRpb25SZWdpc3RyYXRpb24SMS5zYWFzLmFjY291bnRzLnYxLlNvbHV0aW9uTWludFJlZ2lzdHJhdGlvblJlcXVlc3QaMi5zYWFzLmFjY291bnRzLnYxLlNvbHV0aW9uTWludFJlZ2lzdHJhdGlvblJlc3BvbnNlIjvC8xg3CAMQATABOiUKIXNhYXMuc29sdXRpb24ucmVnaXN0cmF0aW9uX21pbnRlZBACQAFIB1AEWARgARKzAQoVTWludE1vZHVsZVdvcmtDb250ZXh0Ei4uc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVNaW50V29ya0NvbnRleHRSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVNaW50V29ya0NvbnRleHRSZXNwb25zZSI5wvMYNQgDEAEwATojCh9zYWFzLm1vZHVsZS53b3JrX2NvbnRleHRfbWludGVkEAJAAUgHUARYBGABEscBChpNaW50TW9kdWxlT3BlcmF0aW9uQ29udGV4dBIzLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlTWludE9wZXJhdGlvbkNvbnRleHRSZXF1ZXN0GjQuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVNaW50T3BlcmF0aW9uQ29udGV4dFJlc3BvbnNlIj7C8xg6CAMQATABOigKJHNhYXMubW9kdWxlLm9wZXJhdGlvbl9jb250ZXh0X21pbnRlZBACQAFIB1AEWARgARLOAQoaTWludFNvdXJjZU9wZXJhdGlvbkNvbnRleHQSOS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZU1pbnRTb3VyY2VPcGVyYXRpb25Db250ZXh0UmVxdWVzdBo6LnNhYXMuYWNjb3VudHMudjEuTW9kdWxlTWludFNvdXJjZU9wZXJhdGlvbkNvbnRleHRSZXNwb25zZSI5wvMYNQgDEAEwATojCh9zYWFzLmRhdGFzb3VyY2UuZGVsZWdhdGlvbi51c2VkEAJAAUgHUARYBGABEoMBCgxQdWJsaXNoRXZlbnQSKy5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVB1Ymxpc2hFdmVudFJlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVB1Ymxpc2hFdmVudFJlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESegoJU3Vic2NyaWJlEiguc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVTdWJzY3JpYmVSZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVTdWJzY3JpYmVSZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEmsKC1Vuc3Vic2NyaWJlEiouc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVVbnN1YnNjcmliZVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARKSAQoRTGlzdFN1YnNjcmlwdGlvbnMSMC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUxpc3RTdWJzY3JpcHRpb25zUmVxdWVzdBoxLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlTGlzdFN1YnNjcmlwdGlvbnNSZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEoMBCgxSZXBsYXlFdmVudHMSKy5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVJlcGxheUV2ZW50c1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVJlcGxheUV2ZW50c1Jlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAFCvwEKFGNvbS5zYWFzLmFjY291bnRzLnYxQhdNb2R1bGVDYXBhYmlsaXRpZXNQcm90b1ABWixhY2NvdW50cy9wa2cvZ2VuL3NhYXMvYWNjb3VudHMvdjE7YWNjb3VudHN2MaICA1NBWKoCEFNhYXMuQWNjb3VudHMuVjHKAhBTYWFzXEFjY291bnRzXFYx4gIcU2Fhc1xBY2NvdW50c1xWMVxHUEJNZXRhZGF0YeoCElNhYXM6OkFjY291bnRzOjpWMWIGcHJvdG8z", [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+Ch1Nb2R1bGVSZXF1ZXN0QXBwcm92YWxSZXNwb25zZRIdCgthcHByb3ZhbF9pZBgBIAEoCUIIukgFcgOwAQEiUwoYTW9kdWxlR2V0QXBwcm92YWxSZXF1ZXN0EhgKBnRlbmFudBgBIAEoCUIIukgFcgOwAQESHQoLYXBwcm92YWxfaWQYAiABKAlCCLpIBXIDsAEBIvUCCg5Nb2R1bGVBcHByb3ZhbBIKCgJpZBgBIAEoCRIOCgZ0ZW5hbnQYAiABKAkSEAoIcmVzb3VyY2UYAyABKAkSDgoGYWN0aW9uGAQgASgJEigKB3N1YmplY3QYBSABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EhQKDHJlcXVlc3RlZF9ieRgGIAEoCRIOCgZxdW9ydW0YByABKA0SDQoFc3RhdGUYCCABKAkSNQoKcmVzdW1lX3JlZhgJIAEoCzIhLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlUmVzdW1lUmVmEi4KCmV4cGlyZXNfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC2VzY2FsYXRlX2F0GAsgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIuCgpjcmVhdGVkX2F0GAwgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCJwChtNb2R1bGVDYW5jZWxBcHByb3ZhbFJlcXVlc3QSGAoGdGVuYW50GAEgASgJQgi6SAVyA7ABARIdCgthcHByb3ZhbF9pZBgCIAEoCUIIukgFcgOwAQESGAoGcmVhc29uGAMgASgJQgi6SAVyAxiACCIyCh5Nb2R1bGVMb29rdXBBdWRpdEV2ZW50UmVzcG9uc2USEAoIZXZlbnRfaWQYASABKAki7gEKG01vZHVsZUVtaXRBdWRpdEV2ZW50UmVxdWVzdBIOCgZ0ZW5hbnQYASABKAkSHgoKZXZlbnRfdHlwZRgCIAEoCUIKukgHcgUQARiAARIZCgVhY3RvchgDIAEoCUIKukgHcgUQARj/ARIcCghzb2x1dGlvbhgEIAEoCUIKukgHcgUQARiAARIaCghlbnRyeV9pZBgFIAEoCUIIukgFcgMY/wESJwoGZmllbGRzGAYgASgLMhcuZ29vZ2xlLnByb3RvYnVmLlN0cnVjdBIhCg9pZGVtcG90ZW5jeV9rZXkYByABKAlCCLpIBXIDGP8BIp8BChtNb2R1bGVBdWRpdEZpZWxkRGVjbGFyYXRpb24SFwoEbmFtZRgBIAEoCUIJukgGcgQQARhAEkAKBGtpbmQYAiABKA4yJi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUF1ZGl0RmllbGRLaW5kQgq6SAeCAQQQASAAEhgKBnZhbHVlcxgDIAMoCUIIukgFkgECEEASCwoDcGlpGAQgASgIIu8BCh9Nb2R1bGVBdWRpdEV2ZW50VHlwZURlY2xhcmF0aW9uEhgKBHR5cGUYASABKAlCCrpIB3IFEAEYgAESHQoLZGVzY3JpcHRpb24YAiABKAlCCLpIBXIDGIAIEkcKBmZpZWxkcxgDIAMoCzItLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlQXVkaXRGaWVsZERlY2xhcmF0aW9uQgi6SAWSAQIQIBJKCgp2aXNpYmlsaXR5GAQgASgOMiwuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVBdWRpdEV2ZW50VmlzaWJpbGl0eUIIukgFggECEAEiswEKI01vZHVsZURlY2xhcmVBdWRpdEV2ZW50VHlwZXNSZXF1ZXN0Ej4KBnByZWZpeBgBIAEoCUIuukgrcikQARiAATIiXlthLXowLTldKD86W2EtejAtOV8tXSpbYS16MC05XSk/JBJMCgV0eXBlcxgCIAMoCzIxLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlQXVkaXRFdmVudFR5cGVEZWNsYXJhdGlvbkIKukgHkgEECAEQQCJdCiRNb2R1bGVEZWNsYXJlQXVkaXRFdmVudFR5cGVzUmVzcG9uc2USFgoOYWRtaXR0ZWRfdHlwZXMYASADKAkSHQoVbmFtZXNwYWNlc190YWtlbl9vdmVyGAIgAygJIm0KFlN1YmplY3RWaXNpYmlsaXR5R3JhbnQSIwoSdmlzaWJsZV9zdWJqZWN0X2lkGAEgASgJQge6SARyAhABEi4KCmV4cGlyZXNfYXQYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wImMKIk1vZHVsZUxpc3RTdWJqZWN0VmlzaWJpbGl0eVJlcXVlc3QSGAoGdGVuYW50GAEgASgJQgi6SAVyA7ABARIjChF2aWV3ZXJfc3ViamVjdF9pZBgCIAEoCUIIukgFcgOwAQEiXwojTW9kdWxlTGlzdFN1YmplY3RWaXNpYmlsaXR5UmVzcG9uc2USOAoGZ3JhbnRzGAEgAygLMiguc2Fhcy5hY2NvdW50cy52MS5TdWJqZWN0VmlzaWJpbGl0eUdyYW50IlcKGkZldGNoRGF0YXNvdXJjZUJsb2JSZXF1ZXN0EhsKCXNvdXJjZV9pZBgBIAEoCUIIukgFcgOwAQESHAoIYmxvYl9zaGEYAiABKAlCCrpIB3IFEAEY/wEiUgoYRmV0Y2hEYXRhc291cmNlQmxvYkNodW5rEgwKBGRhdGEYASABKAwSEgoKdG90YWxfc2l6ZRgCIAEoAxIUCgxjb250ZW50X3R5cGUYAyABKAkiUgoRRGF0YXNvdXJjZUZpbGVSZWYSGwoHaXRlbV9pZBgBIAEoCUIKukgHcgUQARiAIBIgCgxpdGVtX3ZlcnNpb24YAiABKAlCCrpIB3IFEAEY/wEimAEKG0ZldGNoRGF0YXNvdXJjZUZpbGVzUmVxdWVzdBIbCglzb3VyY2VfaWQYASABKAlCCLpIBXIDsAEBEhsKB3ZlcnNpb24YAiABKAlCCrpIB3IFEAEY/wESPwoFZmlsZXMYAyADKAsyIy5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VGaWxlUmVmQgu6SAiSAQUIARDoByKLAQoURGF0YXNvdXJjZVByb3ZlbmFuY2USEQoJc291cmNlX2lkGAEgASgJEg4KBm9yZ19pZBgCIAEoCRIYChBib3VuZGFyeV9ub2RlX2lkGAMgASgJEg8KB3ZlcnNpb24YBCABKAkSDwoHaXRlbV9pZBgFIAEoCRIUCgxpdGVtX3ZlcnNpb24YBiABKAkiwAEKFURhdGFzb3VyY2VJdGVtUmVhZGVycxI7CgViYXNpcxgBIAEoDjIsLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZUl0ZW1SZWFkZXJzQmFzaXMSEAoIdXNlcl9pZHMYAiADKAkSEQoJZ3JvdXBfaWRzGAMgAygJEhgKEGJvdW5kYXJ5X3JlYWRlcnMYBCABKAgSFgoOdW5tYXBwZWRfY291bnQYBSABKAUSEwoLYWNsX3ZlcnNpb24YBiABKAkivgEKFERhdGFzb3VyY2VGaWxlSGVhZGVyEjoKCnByb3ZlbmFuY2UYASABKAsyJi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VQcm92ZW5hbmNlEgwKBHBhdGgYAiABKAkSFAoMY29udGVudF90eXBlGAMgASgJEgwKBHNpemUYBCABKAMSOAoHcmVhZGVycxgFIAEoCzInLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZUl0ZW1SZWFkZXJzIm4KGUZldGNoRGF0YXNvdXJjZUZpbGVzRnJhbWUSOAoGaGVhZGVyGAEgASgLMiYuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlRmlsZUhlYWRlckgAEg4KBGRhdGEYAiABKAxIAEIHCgVmcmFtZSJuChlNb2R1bGVQdWJsaXNoRXZlbnRSZXF1ZXN0EhgKBnRlbmFudBgBIAEoCUIIukgFcgOwAQESNwoIZW52ZWxvcGUYAiABKAsyHS5zYWFzLmV2ZW50cy52MS5FdmVudEVudmVsb3BlQga6SAPIAQEiOAoaTW9kdWxlUHVibGlzaEV2ZW50UmVzcG9uc2USGgoIZXZlbnRfaWQYASABKAlCCLpIBXIDsAEBIsgBChZNb2R1bGVTdWJzY3JpYmVSZXF1ZXN0EksKDHR5cGVfcGF0dGVybhgBIAEoCUI1ukgycjAQARiAATIpXlthLXpdW2EtejAtOV0qKD86XC5bYS16MC05XSspKig/OlwuXCopPyQSLgoFcXVldWUYAiABKAlCH7pIHHIaEAEYgAEyE15bYS16XVthLXowLTlfLi1dKiQSMQoIZGVsaXZlcnkYAyABKA4yHy5zYWFzLmFjY291bnRzLnYxLkV2ZW50RGVsaXZlcnkiyQEKEk1vZHVsZVN1YnNjcmlwdGlvbhIKCgJpZBgBIAEoCRIfChdzdWJzY3JpYmVyX3ByaW5jaXBhbF9pZBgCIAEoCRIUCgx0eXBlX3BhdHRlcm4YAyABKAkSDQoFcXVldWUYBCABKAkSMQoIZGVsaXZlcnkYBSABKA4yHy5zYWFzLmFjY291bnRzLnYxLkV2ZW50RGVsaXZlcnkSLgoKY3JlYXRlZF9hdBgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiVQoXTW9kdWxlU3Vic2NyaWJlUmVzcG9uc2USOgoMc3Vic2NyaXB0aW9uGAEgASgLMiQuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVTdWJzY3JpcHRpb24iPQoYTW9kdWxlVW5zdWJzY3JpYmVSZXF1ZXN0EiEKD3N1YnNjcmlwdGlvbl9pZBgBIAEoCUIIukgFcgOwAQEiIAoeTW9kdWxlTGlzdFN1YnNjcmlwdGlvbnNSZXF1ZXN0Il4KH01vZHVsZUxpc3RTdWJzY3JpcHRpb25zUmVzcG9uc2USOwoNc3Vic2NyaXB0aW9ucxgBIAMoCzIkLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlU3Vic2NyaXB0aW9uInoKGU1vZHVsZVJlcGxheUV2ZW50c1JlcXVlc3QSGAoGdGVuYW50GAEgASgJQgi6SAVyA7ABARIYCgR0eXBlGAIgASgJQgq6SAdyBRABGIABEikKBXNpbmNlGAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCIxChpNb2R1bGVSZXBsYXlFdmVudHNSZXNwb25zZRITCgtyZWRlbGl2ZXJlZBgBIAEoBSLTAgoYUmVhZGFibGVTb3VyY2VDb2xsZWN0aW9uEhEKCXNvdXJjZV9pZBgBIAEoCRITCgtib3VuZGFyeV9pZBgCIAEoCRIOCgZvcmlnaW4YAyABKAkSEQoJY29udGFpbmVyGAQgASgJEgsKA3JlZhgFIAEoCRINCgVwYXRocxgGIAMoCRIWCg5ib3VuZGFyeV9sYWJlbBgHIAEoCRI+ChBncmFudF9kaXNjbG9zdXJlGAggASgOMiQuc2Fhcy5hY2NvdW50cy52MS5NZXRhZGF0YURpc2Nsb3N1cmUSPgoLcmVhZF9ncmFudHMYCSADKAsyKS5zYWFzLmFjY291bnRzLnYxLlJlYWRhYmxlQ29sbGVjdGlvbkdyYW50EjgKBHN5bmMYCiABKAsyKi5zYWFzLmFjY291bnRzLnYxLkNvbGxlY3Rpb25TeW5jUHJvdmVuYW5jZSKwAQoXUmVhZGFibGVDb2xsZWN0aW9uR3JhbnQSFQoNc3ViamVjdF9sYWJlbBgBIAEoCRIUCgxzdWJqZWN0X2tpbmQYAiABKAkSEQoJcm9sZV9uYW1lGAMgASgJEhIKCnNjb3BlX3BhdGgYBCABKAkSEQoJaW5oZXJpdGVkGAUgASgIEi4KCmV4cGlyZXNfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIqkCChhDb2xsZWN0aW9uU3luY1Byb3ZlbmFuY2USMAoFc3RhZ2UYASABKA4yIS5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNTdGFnZRImCgJhdBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASEAoIcmV2aXNpb24YAyABKAkSDwoHdHJpZ2dlchgEIAEoCRJCChRyZXF1ZXN0ZXJfZGlzY2xvc3VyZRgFIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuTWV0YWRhdGFEaXNjbG9zdXJlEjAKDHJlcXVlc3RlZF9hdBgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGgoScmVxdWVzdGVkX2J5X2xhYmVsGAcgASgJImMKJExpc3RSZWFkYWJsZVNvdXJjZUNvbGxlY3Rpb25zUmVxdWVzdBIdCglwYWdlX3NpemUYASABKAVCCrpIBxoFGOgHKAASHAoKcGFnZV90b2tlbhgCIAEoCUIIukgFcgMYgBAigQEKJUxpc3RSZWFkYWJsZVNvdXJjZUNvbGxlY3Rpb25zUmVzcG9uc2USPwoLY29sbGVjdGlvbnMYASADKAsyKi5zYWFzLmFjY291bnRzLnYxLlJlYWRhYmxlU291cmNlQ29sbGVjdGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAki6QEKGE1vZHVsZVBsYWNlUmVjb3JkUmVxdWVzdBIYCgZ0ZW5hbnQYASABKAlCCLpIBXIDsAEBEj0KCnNjb3BlX3BhdGgYAiABKAlCKbpIJnIkEAEYgAQyHV5bYS16MC05X10rKD86XC5bYS16MC05X10rKSokEhcKBGtpbmQYAyABKAlCCbpIBnIEEAEYQBIXCgVsYWJlbBgEIAEoCUIIukgFcgMY/wESIQoNcmVzb3VyY2VfdHlwZRgFIAEoCUIKukgHcgUQARiAARIfCgtyZXNvdXJjZV9pZBgGIAEoCUIKukgHcgUQARiABCI2ChlNb2R1bGVQbGFjZVJlY29yZFJlc3BvbnNlEhkKB25vZGVfaWQYASABKAlCCLpIBXIDsAEBIlwKJ1ZlcmlmeVdvcmtDb250ZXh0UnVudGltZUJvdW5kYXJ5UmVxdWVzdBIxChxmb3J3YXJkZWRfd29ya19jb250ZXh0X3Rva2VuGAEgASgJQgu6SAhyBhABGICAAiJSCihWZXJpZnlXb3JrQ29udGV4dFJ1bnRpbWVCb3VuZGFyeVJlc3BvbnNlEhEKCXRlbmFudF9pZBgBIAEoCRITCgtib3VuZGFyeV9pZBgCIAEoCSKFAQojQ2hlY2tXb3JrQ29udGV4dFJlY29yZEFjY2Vzc1JlcXVlc3QSIQoNcmVzb3VyY2VfdHlwZRgBIAEoCUIKukgHcgUQARiAAhIfCgtyZXNvdXJjZV9pZBgCIAEoCUIKukgHcgUQARiABBIaCgZhY3Rpb24YAyABKAlCCrpIB3IFEAEYgAIiTgokQ2hlY2tXb3JrQ29udGV4dFJlY29yZEFjY2Vzc1Jlc3BvbnNlEg8KB2FsbG93ZWQYASABKAgSFQoNc2NvcGVfbm9kZV9pZBgCIAEoCSJ8CipNb2R1bGVFeGNoYW5nZURlbGVnYXRlZFJlYWRBdWRpZW5jZVJlcXVlc3QSHgoKYmluZGluZ19pZBgBIAEoCUIKukgHcgUQARiAARIuChlwYXJlbnRfd29ya19jb250ZXh0X3Rva2VuGAIgASgJQgu6SAhyBhABGICAAiKAAwovTW9kdWxlRXhjaGFuZ2VEZWxlZ2F0ZWRPcGVyYXRpb25BdWRpZW5jZVJlcXVlc3QSHgoKYmluZGluZ19pZBgBIAEoCUIKukgHcgUQARiAARIsChlwYXJlbnRfd29ya19jb250ZXh0X3Rva2VuGAIgASgJQgm6SAZyBBiAgAISDgoGbG9va3VwGAMgASgIEiIKDWRlbGVnYXRpb25faWQYBCABKAlCC7pICNgBAXIDsAEBOsoBukjGARrDAQozZXhjaGFuZ2VfZGVsZWdhdGVkX29wZXJhdGlvbl9hdWRpZW5jZS5vbmVfYXV0aG9yaXR5EkZleGFjdGx5IG9uZSBvZiBwYXJlbnRfd29ya19jb250ZXh0X3Rva2VuIGFuZCBkZWxlZ2F0aW9uX2lkIGlzIHJlcXVpcmVkGkQodGhpcy5wYXJlbnRfd29ya19jb250ZXh0X3Rva2VuICE9ICcnKSAhPSAodGhpcy5kZWxlZ2F0aW9uX2lkICE9ICcnKSq4AgoUTW9kdWxlQXVkaXRGaWVsZEtpbmQSJwojTU9EVUxFX0FVRElUX0ZJRUxEX0tJTkRfVU5TUEVDSUZJRUQQABIiCh5NT0RVTEVfQVVESVRfRklFTERfS0lORF9TVFJJTkcQARIgChxNT0RVTEVfQVVESVRfRklFTERfS0lORF9VVUlEEAISHwobTU9EVUxFX0FVRElUX0ZJRUxEX0tJTkRfSU5UEAMSIgoeTU9EVUxFX0FVRElUX0ZJRUxEX0tJTkRfTlVNQkVSEAQSIAocTU9EVUxFX0FVRElUX0ZJRUxEX0tJTkRfQk9PTBAFEiAKHE1PRFVMRV9BVURJVF9GSUVMRF9LSU5EX0VOVU0QBhIoCiRNT0RVTEVfQVVESVRfRklFTERfS0lORF9TVFJJTkdfQVJSQVkQByqhAQoaTW9kdWxlQXVkaXRFdmVudFZpc2liaWxpdHkSLQopTU9EVUxFX0FVRElUX0VWRU5UX1ZJU0lCSUxJVFlfVU5TUEVDSUZJRUQQABIoCiRNT0RVTEVfQVVESVRfRVZFTlRfVklTSUJJTElUWV9URU5BTlQQARIqCiZNT0RVTEVfQVVESVRfRVZFTlRfVklTSUJJTElUWV9FWFRFUk5BTBACKtwBChpEYXRhc291cmNlSXRlbVJlYWRlcnNCYXNpcxItCilEQVRBU09VUkNFX0lURU1fUkVBREVSU19CQVNJU19VTlNQRUNJRklFRBAAEiwKKERBVEFTT1VSQ0VfSVRFTV9SRUFERVJTX0JBU0lTX1RSQU5TTEFURUQQARIvCitEQVRBU09VUkNFX0lURU1fUkVBREVSU19CQVNJU19TT1VSQ0VfU0NPUEVEEAISMAosREFUQVNPVVJDRV9JVEVNX1JFQURFUlNfQkFTSVNfVU5UUkFOU0xBVEFCTEUQAyppCg1FdmVudERlbGl2ZXJ5Eh4KGkVWRU5UX0RFTElWRVJZX1VOU1BFQ0lGSUVEEAASHAoYRVZFTlRfREVMSVZFUllfVU5PUkRFUkVEEAESGgoWRVZFTlRfREVMSVZFUllfT1JERVJFRBACKn4KEk1ldGFkYXRhRGlzY2xvc3VyZRIjCh9NRVRBREFUQV9ESVNDTE9TVVJFX1VOU1BFQ0lGSUVEEAASIQodTUVUQURBVEFfRElTQ0xPU1VSRV9ESVNDTE9TRUQQARIgChxNRVRBREFUQV9ESVNDTE9TVVJFX1dJVEhIRUxEEAIqXAoPU291cmNlU3luY1N0YWdlEiEKHVNPVVJDRV9TWU5DX1NUQUdFX1VOU1BFQ0lGSUVEEAASJgoiU09VUkNFX1NZTkNfU1RBR0VfQ0hBTkdFU19FTlFVRVVFRBABMpcmChlNb2R1bGVDYXBhYmlsaXRpZXNTZXJ2aWNlEsUBCh1FeGNoYW5nZURlbGVnYXRlZFJlYWRBdWRpZW5jZRI8LnNhYXMuYWNjb3VudHMudjEuTW9kdWxlRXhjaGFuZ2VEZWxlZ2F0ZWRSZWFkQXVkaWVuY2VSZXF1ZXN0GiMuc2Fhcy5hY2NvdW50cy52MS5Jc3N1ZWRXb3JrQ29udGV4dCJBwvMYPQgDEAEwATorCidzYWFzLm1vZHVsZS5kZWxlZ2F0ZWRfYXVkaWVuY2VfZXhjaGFuZ2UQBEABSAdQBFgEYAESzwEKIkV4Y2hhbmdlRGVsZWdhdGVkT3BlcmF0aW9uQXVkaWVuY2USQS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUV4Y2hhbmdlRGVsZWdhdGVkT3BlcmF0aW9uQXVkaWVuY2VSZXF1ZXN0GiMuc2Fhcy5hY2NvdW50cy52MS5Jc3N1ZWRXb3JrQ29udGV4dCJBwvMYPQgDEAEwATorCidzYWFzLm1vZHVsZS5kZWxlZ2F0ZWRfYXVkaWVuY2VfZXhjaGFuZ2UQBEABSAdQBFgEYAESswEKIFZlcmlmeVdvcmtDb250ZXh0UnVudGltZUJvdW5kYXJ5Ejkuc2Fhcy5hY2NvdW50cy52MS5WZXJpZnlXb3JrQ29udGV4dFJ1bnRpbWVCb3VuZGFyeVJlcXVlc3QaOi5zYWFzLmFjY291bnRzLnYxLlZlcmlmeVdvcmtDb250ZXh0UnVudGltZUJvdW5kYXJ5UmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1AEWANgARKnAQocQ2hlY2tXb3JrQ29udGV4dFJlY29yZEFjY2VzcxI1LnNhYXMuYWNjb3VudHMudjEuQ2hlY2tXb3JrQ29udGV4dFJlY29yZEFjY2Vzc1JlcXVlc3QaNi5zYWFzLmFjY291bnRzLnYxLkNoZWNrV29ya0NvbnRleHRSZWNvcmRBY2Nlc3NSZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEqoBCh1MaXN0UmVhZGFibGVTb3VyY2VDb2xsZWN0aW9ucxI2LnNhYXMuYWNjb3VudHMudjEuTGlzdFJlYWRhYmxlU291cmNlQ29sbGVjdGlvbnNSZXF1ZXN0Gjcuc2Fhcy5hY2NvdW50cy52MS5MaXN0UmVhZGFibGVTb3VyY2VDb2xsZWN0aW9uc1Jlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESgAEKC1BsYWNlUmVjb3JkEiouc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVQbGFjZVJlY29yZFJlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVBsYWNlUmVjb3JkUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARJ9CgpFbnF1ZXVlSm9iEikuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFbnF1ZXVlSm9iUmVxdWVzdBoqLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlRW5xdWV1ZUpvYlJlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESegoJQ2xhaW1Kb2JzEiguc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVDbGFpbUpvYnNSZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVDbGFpbUpvYnNSZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEoMBCgxIZWFydGJlYXRKb2ISKy5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUhlYXJ0YmVhdEpvYlJlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUhlYXJ0YmVhdEpvYlJlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESYQoGQWNrSm9iEiUuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVBY2tKb2JSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESYwoHTmFja0pvYhImLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlTmFja0pvYlJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARJ9CgpOb3RpZnlVc2VyEikuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVOb3RpZnlVc2VyUmVxdWVzdBoqLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlTm90aWZ5VXNlclJlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESrQEKD05vdGlmeU9yZ0FkbWlucxIuLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlTm90aWZ5T3JnQWRtaW5zUmVxdWVzdBovLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlTm90aWZ5T3JnQWRtaW5zUmVzcG9uc2UiOcLzGDUIAxABMAE6Iwofc2Fhcy5tb2R1bGUub3JnX2FkbWluc19ub3RpZmllZBACQAFIB1ADWANgARKMAQoPUmVxdWVzdEFwcHJvdmFsEi4uc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVSZXF1ZXN0QXBwcm92YWxSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVSZXF1ZXN0QXBwcm92YWxSZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEnUKC0dldEFwcHJvdmFsEiouc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVHZXRBcHByb3ZhbFJlcXVlc3QaIC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUFwcHJvdmFsIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAEScQoOQ2FuY2VsQXBwcm92YWwSLS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUNhbmNlbEFwcHJvdmFsUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEnEKDkVtaXRBdWRpdEV2ZW50Ei0uc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFbWl0QXVkaXRFdmVudFJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARKNAQoQTG9va3VwQXVkaXRFdmVudBItLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlRW1pdEF1ZGl0RXZlbnRSZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVMb29rdXBBdWRpdEV2ZW50UmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARLDAQoWRGVjbGFyZUF1ZGl0RXZlbnRUeXBlcxI1LnNhYXMuYWNjb3VudHMudjEuTW9kdWxlRGVjbGFyZUF1ZGl0RXZlbnRUeXBlc1JlcXVlc3QaNi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZURlY2xhcmVBdWRpdEV2ZW50VHlwZXNSZXNwb25zZSI6wvMYNggDEAEwATokCiBzYWFzLm1vZHVsZS5hdWRpdF90eXBlc19kZWNsYXJlZBACQAFIB1ADWANgARKeAQoVTGlzdFN1YmplY3RWaXNpYmlsaXR5EjQuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVMaXN0U3ViamVjdFZpc2liaWxpdHlSZXF1ZXN0GjUuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVMaXN0U3ViamVjdFZpc2liaWxpdHlSZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEo4BChNGZXRjaERhdGFzb3VyY2VCbG9iEiwuc2Fhcy5hY2NvdW50cy52MS5GZXRjaERhdGFzb3VyY2VCbG9iUmVxdWVzdBoqLnNhYXMuYWNjb3VudHMudjEuRmV0Y2hEYXRhc291cmNlQmxvYkNodW5rIhuIAgHC8xgUCAMQATABOgIQAUABSAdQA1gDYAEwARKOAQoURmV0Y2hEYXRhc291cmNlRmlsZXMSLS5zYWFzLmFjY291bnRzLnYxLkZldGNoRGF0YXNvdXJjZUZpbGVzUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuRmV0Y2hEYXRhc291cmNlRmlsZXNGcmFtZSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABMAEStgEKFk1pbnRNb2R1bGVSZWdpc3RyYXRpb24SLy5zYWFzLmFjY291bnRzLnYxLk1vZHVsZU1pbnRSZWdpc3RyYXRpb25SZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVNaW50UmVnaXN0cmF0aW9uUmVzcG9uc2UiOcLzGDUIAxABMAE6Iwofc2Fhcy5tb2R1bGUucmVnaXN0cmF0aW9uX21pbnRlZBACQAFIB1AEWARgARK+AQoYTWludFNvbHV0aW9uUmVnaXN0cmF0aW9uEjEuc2Fhcy5hY2NvdW50cy52MS5Tb2x1dGlvbk1pbnRSZWdpc3RyYXRpb25SZXF1ZXN0GjIuc2Fhcy5hY2NvdW50cy52MS5Tb2x1dGlvbk1pbnRSZWdpc3RyYXRpb25SZXNwb25zZSI7wvMYNwgDEAEwATolCiFzYWFzLnNvbHV0aW9uLnJlZ2lzdHJhdGlvbl9taW50ZWQQAkABSAdQBFgEYAESswEKFU1pbnRNb2R1bGVXb3JrQ29udGV4dBIuLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlTWludFdvcmtDb250ZXh0UmVxdWVzdBovLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlTWludFdvcmtDb250ZXh0UmVzcG9uc2UiOcLzGDUIAxABMAE6Iwofc2Fhcy5tb2R1bGUud29ya19jb250ZXh0X21pbnRlZBACQAFIB1AEWARgARLHAQoaTWludE1vZHVsZU9wZXJhdGlvbkNvbnRleHQSMy5zYWFzLmFjY291bnRzLnYxLk1vZHVsZU1pbnRPcGVyYXRpb25Db250ZXh0UmVxdWVzdBo0LnNhYXMuYWNjb3VudHMudjEuTW9kdWxlTWludE9wZXJhdGlvbkNvbnRleHRSZXNwb25zZSI+wvMYOggDEAEwATooCiRzYWFzLm1vZHVsZS5vcGVyYXRpb25fY29udGV4dF9taW50ZWQQAkABSAdQBFgEYAESzgEKGk1pbnRTb3VyY2VPcGVyYXRpb25Db250ZXh0Ejkuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVNaW50U291cmNlT3BlcmF0aW9uQ29udGV4dFJlcXVlc3QaOi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZU1pbnRTb3VyY2VPcGVyYXRpb25Db250ZXh0UmVzcG9uc2UiOcLzGDUIAxABMAE6Iwofc2Fhcy5kYXRhc291cmNlLmRlbGVnYXRpb24udXNlZBACQAFIB1AEWARgARKDAQoMUHVibGlzaEV2ZW50Eisuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVQdWJsaXNoRXZlbnRSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVQdWJsaXNoRXZlbnRSZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEnoKCVN1YnNjcmliZRIoLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlU3Vic2NyaWJlUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlU3Vic2NyaWJlUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARJrCgtVbnN1YnNjcmliZRIqLnNhYXMuYWNjb3VudHMudjEuTW9kdWxlVW5zdWJzY3JpYmVSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESkgEKEUxpc3RTdWJzY3JpcHRpb25zEjAuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVMaXN0U3Vic2NyaXB0aW9uc1JlcXVlc3QaMS5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUxpc3RTdWJzY3JpcHRpb25zUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARKDAQoMUmVwbGF5RXZlbnRzEisuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVSZXBsYXlFdmVudHNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVSZXBsYXlFdmVudHNSZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABQr8BChRjb20uc2Fhcy5hY2NvdW50cy52MUIXTW9kdWxlQ2FwYWJpbGl0aWVzUHJvdG9QAVosYWNjb3VudHMvcGtnL2dlbi9zYWFzL2FjY291bnRzL3YxO2FjY291bnRzdjGiAgNTQViqAhBTYWFzLkFjY291bnRzLlYxygIQU2Fhc1xBY2NvdW50c1xWMeICHFNhYXNcQWNjb3VudHNcVjFcR1BCTWV0YWRhdGHqAhJTYWFzOjpBY2NvdW50czo6VjFiBnByb3RvMw", [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 @@ -656,6 +656,26 @@ export type ModuleCancelApprovalRequest = Message<"saas.accounts.v1.ModuleCancel export const ModuleCancelApprovalRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_saas_accounts_v1_module_capabilities, 18); +/** + * Receipt for the exact requested audit intent, under current emission authority. + * Empty event_id means no receipt was visible, not proof that nothing happened. + * + * @generated from message saas.accounts.v1.ModuleLookupAuditEventResponse + */ +export type ModuleLookupAuditEventResponse = Message<"saas.accounts.v1.ModuleLookupAuditEventResponse"> & { + /** + * @generated from field: string event_id = 1; + */ + eventId: string; +}; + +/** + * Describes the message saas.accounts.v1.ModuleLookupAuditEventResponse. + * Use `create(ModuleLookupAuditEventResponseSchema)` to create a new message. + */ +export const ModuleLookupAuditEventResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_module_capabilities, 19); + /** * ModuleEmitAuditEventRequest emits a registered audit event onto the tenant's * single audit spine. event_type must be registered in the code-owned catalog @@ -699,7 +719,8 @@ export type ModuleEmitAuditEventRequest = Message<"saas.accounts.v1.ModuleEmitAu /** * idempotency_key deduplicates retried emits: two emits with the same * (tenant, event_type, idempotency_key) collapse to a single audit row, and - * the duplicate returns success without writing again. Empty disables + * an identical intent returns success without writing again. Changed intent or + * an older key without recorded intent returns FAILED_PRECONDITION. Empty disables * deduplication (every emit is a distinct event). Scoped by tenant so one * tenant's keys can never suppress another's events. * @@ -713,7 +734,7 @@ export type ModuleEmitAuditEventRequest = Message<"saas.accounts.v1.ModuleEmitAu * Use `create(ModuleEmitAuditEventRequestSchema)` to create a new message. */ export const ModuleEmitAuditEventRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_module_capabilities, 19); + messageDesc(file_saas_accounts_v1_module_capabilities, 20); /** * ModuleAuditFieldDeclaration is one payload field of a declared audit event @@ -756,7 +777,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, 21); /** * ModuleAuditEventTypeDeclaration is one audit event type a composed module @@ -801,7 +822,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, 22); /** * ModuleDeclareAuditEventTypesRequest declares the audit event types a composed @@ -834,7 +855,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, 23); /** * ModuleDeclareAuditEventTypesResponse names what the call changed: the types @@ -860,7 +881,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, 24); /** * SubjectVisibilityGrant is one entry of a viewer's visible-subject set: another @@ -890,7 +911,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, 25); /** * ModuleListSubjectVisibilityRequest asks for one viewer's whole visible-subject @@ -924,7 +945,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, 26); /** * ModuleListSubjectVisibilityResponse carries the viewer's WHOLE set, read in @@ -955,7 +976,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, 27); /** * FetchDatasourceBlobRequest asks accounts to re-fetch one file blob from the @@ -986,7 +1007,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, 28); /** * FetchDatasourceBlobChunk is one frame of the streamed blob. total_size and @@ -1018,7 +1039,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, 29); /** * DatasourceFileRef names one file of a files-interface datasource at the @@ -1045,7 +1066,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, 30); /** * FetchDatasourceFilesRequest asks accounts for a batch of files of one source @@ -1081,7 +1102,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, 31); /** * DatasourceProvenance is the envelope every item a datasource serves carries, @@ -1128,7 +1149,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, 32); /** * DatasourceItemReaders is the envelope's per-item permission slot: who may read @@ -1181,7 +1202,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, 33); /** * DatasourceFileHeader opens one file of a FetchDatasourceFiles stream. size is @@ -1222,7 +1243,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, 34); /** * FetchDatasourceFilesFrame is one frame of a FetchDatasourceFiles stream. The @@ -1255,7 +1276,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, 35); /** * ModulePublishEventRequest publishes one domain event for the caller's bound @@ -1283,7 +1304,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, 36); /** * @generated from message saas.accounts.v1.ModulePublishEventResponse @@ -1305,7 +1326,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, 37); /** * ModuleSubscribeRequest creates, or idempotently re-affirms, a durable @@ -1338,7 +1359,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, 38); /** * ModuleSubscription is one durable subscription row as the module sees it. @@ -1382,7 +1403,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, 39); /** * @generated from message saas.accounts.v1.ModuleSubscribeResponse @@ -1399,7 +1420,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, 40); /** * ModuleUnsubscribeRequest revokes one of the caller's own subscriptions. @@ -1418,7 +1439,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, 41); /** * ModuleListSubscriptionsRequest lists the calling principal's live @@ -1434,7 +1455,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, 42); /** * @generated from message saas.accounts.v1.ModuleListSubscriptionsResponse @@ -1451,7 +1472,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, 43); /** * ModuleReplayEventsRequest re-delivers durable events of one type for the @@ -1483,7 +1504,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, 44); /** * @generated from message saas.accounts.v1.ModuleReplayEventsResponse @@ -1503,7 +1524,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, 45); /** * ModuleCapabilitiesService is the module-facing platform surface. Every RPC is @@ -1585,7 +1606,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, 46); /** * ReadableCollectionGrant is one active grant conferring read on a collection's @@ -1636,7 +1657,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, 47); /** * CollectionSyncProvenance is the host's record of how one feeding source last @@ -1700,7 +1721,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, 48); /** * Tenant and viewer come exclusively from the verified Work Context. @@ -1724,7 +1745,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, 49); /** * @generated from message saas.accounts.v1.ListReadableSourceCollectionsResponse @@ -1746,7 +1767,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, 50); /** * ModulePlaceRecordRequest binds one of the calling module's own records to a @@ -1803,7 +1824,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, 51); /** * @generated from message saas.accounts.v1.ModulePlaceRecordResponse @@ -1826,7 +1847,49 @@ 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, 52); + +/** + * Read-only attestation; identity and boundary come exclusively from this signed + * context. The caller separately authenticates as its consuming module. + * + * @generated from message saas.accounts.v1.VerifyWorkContextRuntimeBoundaryRequest + */ +export type VerifyWorkContextRuntimeBoundaryRequest = Message<"saas.accounts.v1.VerifyWorkContextRuntimeBoundaryRequest"> & { + /** + * @generated from field: string forwarded_work_context_token = 1; + */ + forwardedWorkContextToken: string; +}; + +/** + * Describes the message saas.accounts.v1.VerifyWorkContextRuntimeBoundaryRequest. + * Use `create(VerifyWorkContextRuntimeBoundaryRequestSchema)` to create a new message. + */ +export const VerifyWorkContextRuntimeBoundaryRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_module_capabilities, 53); + +/** + * @generated from message saas.accounts.v1.VerifyWorkContextRuntimeBoundaryResponse + */ +export type VerifyWorkContextRuntimeBoundaryResponse = Message<"saas.accounts.v1.VerifyWorkContextRuntimeBoundaryResponse"> & { + /** + * @generated from field: string tenant_id = 1; + */ + tenantId: string; + + /** + * @generated from field: string boundary_id = 2; + */ + boundaryId: string; +}; + +/** + * Describes the message saas.accounts.v1.VerifyWorkContextRuntimeBoundaryResponse. + * Use `create(VerifyWorkContextRuntimeBoundaryResponseSchema)` to create a new message. + */ +export const VerifyWorkContextRuntimeBoundaryResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_module_capabilities, 54); /** * Exact-record delegated oracle. Tenant and subjects come only from the signed @@ -1856,7 +1919,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 @@ -1882,7 +1945,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. @@ -1907,7 +1970,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 @@ -1968,7 +2031,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); /** * ModuleAuditFieldKind is the kind of one declared audit payload field: the @@ -2235,6 +2298,17 @@ export const ModuleCapabilitiesService: GenService<{ input: typeof ModuleExchangeDelegatedOperationAudienceRequestSchema; output: typeof IssuedWorkContextSchema; }, + /** + * Attests a current capability under an active host-owned solution boundary. + * This read grants no authority and does not expose the registration seed. + * + * @generated from rpc saas.accounts.v1.ModuleCapabilitiesService.VerifyWorkContextRuntimeBoundary + */ + verifyWorkContextRuntimeBoundary: { + methodKind: "unary"; + input: typeof VerifyWorkContextRuntimeBoundaryRequestSchema; + output: typeof VerifyWorkContextRuntimeBoundaryResponseSchema; + }, /** * Checks current owner and every delegated actor against true record placement, * intersected with the verified capability's attenuated resource/action scope. @@ -2382,6 +2456,18 @@ export const ModuleCapabilitiesService: GenService<{ input: typeof ModuleEmitAuditEventRequestSchema; output: typeof EmptySchema; }, + /** + * Read-only recovery for the complete original intent. Requires a nonempty + * idempotency_key; same authority as EmitAuditEvent. No retry authorization is + * implied by an absent receipt. Old unbound keys return FAILED_PRECONDITION. + * + * @generated from rpc saas.accounts.v1.ModuleCapabilitiesService.LookupAuditEvent + */ + lookupAuditEvent: { + methodKind: "unary"; + input: typeof ModuleEmitAuditEventRequestSchema; + output: typeof ModuleLookupAuditEventResponseSchema; + }, /** * 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/migrations/19_audit_effect_receipts.down.sql b/module/services/store/migrations/19_audit_effect_receipts.down.sql new file mode 100644 index 000000000..9f40b5f45 --- /dev/null +++ b/module/services/store/migrations/19_audit_effect_receipts.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE public.audit_event_idempotency + DROP CONSTRAINT audit_effect_binding_complete, + DROP COLUMN audit_event_id, + DROP COLUMN request_fingerprint; diff --git a/module/services/store/migrations/19_audit_effect_receipts.up.sql b/module/services/store/migrations/19_audit_effect_receipts.up.sql new file mode 100644 index 000000000..b39cf8236 --- /dev/null +++ b/module/services/store/migrations/19_audit_effect_receipts.up.sql @@ -0,0 +1,9 @@ +-- Preserve historical guards without inventing their missing request intent. +-- New effect reservations bind the request and generated event ID atomically +-- with the audit write; NULL pairs remain explicitly unverifiable. +ALTER TABLE public.audit_event_idempotency + ADD COLUMN request_fingerprint text, + ADD COLUMN audit_event_id uuid, + ADD CONSTRAINT audit_effect_binding_complete CHECK ( + (request_fingerprint IS NULL) = (audit_event_id IS NULL) + );