diff --git a/AUTHZ.md b/AUTHZ.md index 777dec73d..e8de0de6b 100644 --- a/AUTHZ.md +++ b/AUTHZ.md @@ -1005,7 +1005,10 @@ runs. It carries no repository, revision, or Argo resource. `ModuleCapabilitiesService.CheckWorkContextRecordAccess` is the internal, read-only exact-record companion to the subject-taking administrative oracle. -It verifies one signed viewer Work Context, derives tenant/owner/all actors, +It reads back one signed viewer Work Context at full strength WITHOUT CONSUMING +it (core's `Authority.ReadBack`), so it is read-only for the capability too: the +consumer that verified and consumed a single-use capability can still ask about +it, record after record. It derives tenant/owner/all actors, checks the audience's declared resource type and the SDK's attenuated kind/action/record scope, and rechecks current authorization revision and every actor-chain hop's revocation. Delegated calls require the durable chain journal. diff --git a/module/AGENTS.md b/module/AGENTS.md index b5abbc2ee..5593f2527 100644 --- a/module/AGENTS.md +++ b/module/AGENTS.md @@ -82,9 +82,13 @@ each invocation fresh database state. Do not opt into `WithSharedControlChannel` which is only for older CLIs and reuses fixed local database directories across otherwise independent runs and migration revisions. -Core dependencies are pinned to released v0.18.0 in every owning Go module, -including the module-authority qualification module. Run qualification with -`GOWORK=off` so a developer workspace cannot mask the released dependency. +Core is pinned per Go module. The host's five services (accounts, auth-gateway, +policy-log, store, telemetry) run one core release, v0.19.3, which carries the +issuer-side `Authority.ReadBack`. The module agent (the root module), +`module/tools` and the module-authority qualification module stay on released +v0.18.0. Never ship a host release on an untagged pseudo-version. Run +qualification with `GOWORK=off` so a developer workspace cannot mask the pinned +dependency. Accounts requires Go 1.27.2. All go-grpc services pin released agent 0.1.54, whose builder uses Go 1.27.2 under `GOTOOLCHAIN=local`; CI uses the same patch. Go 1.27.0 is affected by [GO-2026-6613](https://pkg.go.dev/vuln/GO-2026-6613). diff --git a/module/KEY_ROTATION.md b/module/KEY_ROTATION.md index c7adf526e..e4647a200 100644 --- a/module/KEY_ROTATION.md +++ b/module/KEY_ROTATION.md @@ -374,6 +374,15 @@ The Work Context path deliberately does not take the grace: a presented capability is an optional attenuation the caller can retry, so it fails closed the moment its key set expires. +Accounts' own read-backs are narrower still. A capability presented with a +question (`CheckWorkContextRecordAccess`, `ListReadableSourceCollections`, the +module-parent reads) is read back through core's `Authority.ReadBack`, which +trusts only the authority's CURRENT key id. From the moment the Work Context key +rotates, a capability signed under the previous key is refused by those +read-backs — while a consumer whose JWKS still lists the old key accepts it — +until its holder mints a new one; the capability's own lifetime +bounds the window. See `services/accounts/AGENTS.md`. + ## Observability Two counters, both with a fixed label set and no credential-derived values: diff --git a/module/WORK_CONTEXTS.md b/module/WORK_CONTEXTS.md index b58d26f03..914fda185 100644 --- a/module/WORK_CONTEXTS.md +++ b/module/WORK_CONTEXTS.md @@ -297,10 +297,19 @@ live state. The seal is re-resolved too, never copied from the parent: a person's capability is sealed to the organization's installation of the audience it now addresses (an exchange into an audience the organization never installed is refused), and a module or installation principal's to its own -installation. When this host reads back a capability it issued (a parent to derive -from, a viewer's capability presented with a question), it verifies it at full -strength for the audience the capability names; a single-use capability is -consumed by that read, as it is by whichever consumer verifies it first. +installation. A parent this host derives from is verified at full strength for +the audience it names, and a single-use parent is consumed by the derivation, as +by any consumer. A capability presented with a QUESTION — a viewer's capability +asked what it may read (`CheckWorkContextRecordAccess`, +`ListReadableSourceCollections`), a module's parent asked about its installation +or artifacts — is read back instead (core's `Authority.ReadBack`): every check a +verifier makes, against live state, but the replay store is never touched. A +question is not a use, so the consumer that verified and consumed a single-use +capability can still ask about it, on every page of a listing, and asking leaves +an unconsumed one for its consumer. A read-back trusts only the issuer's CURRENT +signing key: during a key rotation, a capability signed under the previous key +is refused by every read-back (fail closed) even while a consumer still accepts +it, until its holder mints a new one. ## The reserved module audience diff --git a/module/configurations/local/key-service.env b/module/configurations/local/key-service.env index dab838149..b75efdf25 100644 --- a/module/configurations/local/key-service.env +++ b/module/configurations/local/key-service.env @@ -47,10 +47,11 @@ # kms the key never leaves Cloud KMS and every signature is a request. # REFUSED BY NAME today: three consumers are HANDED the signing key # rather than being given a signer to call — the Work Context -# signer (github.com/codefly-dev/sdk-go/workcontext), the +# authority (core's workcontext.Authority, which accepts an +# injected crypto.Signer that this host does not wire yet), the # delegation minter (github.com/codefly-dev/core # policy.MintEd25519) and the OAuth state signer's seed — so each -# has to accept an injected crypto.Signer first. +# has to be given a signer first. # # A hosted cell that wants no Vault at all runs KEY_SERVICE_BACKEND=kms with # KEY_SERVICE_SIGNING_BACKEND=kms-wrapped today, and moves to `kms` once those diff --git a/module/contracts/api/accounts/authority/contract.binpb b/module/contracts/api/accounts/authority/contract.binpb index 65d02a037..5e7e7ab90 100644 Binary files a/module/contracts/api/accounts/authority/contract.binpb and b/module/contracts/api/accounts/authority/contract.binpb differ diff --git a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/work_contexts.proto b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/work_contexts.proto index c4a9d17f6..809ee1318 100644 --- a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/work_contexts.proto +++ b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/work_contexts.proto @@ -39,7 +39,15 @@ message WorkContextScope { ]; } +// StartTaskWorkContextRequest mints a Task capability for the caller. Its Session +// is the one the caller authenticated with — or a fresh one when it has none — +// assigned by the issuer and returned in IssuedWorkContext, never named by the +// caller: a session a caller could name is one it could name after another +// credential's, and a confined credential is paired with a capability on the +// session alone. message StartTaskWorkContextRequest { + reserved 3; + reserved "session_id"; string org_id = 1 [(buf.validate.field).string.uuid = true]; // The runtime boundary the capability is minted under. Required on an // ordinary mint, where the caller owns the Task it names. @@ -56,7 +64,6 @@ message StartTaskWorkContextRequest { (buf.validate.field).string.uuid = true, (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE ]; - string session_id = 3 [(buf.validate.field).string.uuid = true]; // Empty means a direct human-owned Task. Otherwise this must name an active, // registered agent Principal in org_id. string actor_principal_id = 4; diff --git a/module/contracts/api/accounts/connect/contract.binpb b/module/contracts/api/accounts/connect/contract.binpb index 65d02a037..5e7e7ab90 100644 Binary files a/module/contracts/api/accounts/connect/contract.binpb and b/module/contracts/api/accounts/connect/contract.binpb differ diff --git a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/work_contexts.proto b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/work_contexts.proto index c4a9d17f6..809ee1318 100644 --- a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/work_contexts.proto +++ b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/work_contexts.proto @@ -39,7 +39,15 @@ message WorkContextScope { ]; } +// StartTaskWorkContextRequest mints a Task capability for the caller. Its Session +// is the one the caller authenticated with — or a fresh one when it has none — +// assigned by the issuer and returned in IssuedWorkContext, never named by the +// caller: a session a caller could name is one it could name after another +// credential's, and a confined credential is paired with a capability on the +// session alone. message StartTaskWorkContextRequest { + reserved 3; + reserved "session_id"; string org_id = 1 [(buf.validate.field).string.uuid = true]; // The runtime boundary the capability is minted under. Required on an // ordinary mint, where the caller owns the Task it names. @@ -56,7 +64,6 @@ message StartTaskWorkContextRequest { (buf.validate.field).string.uuid = true, (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE ]; - string session_id = 3 [(buf.validate.field).string.uuid = true]; // Empty means a direct human-owned Task. Otherwise this must name an active, // registered agent Principal in org_id. string actor_principal_id = 4; diff --git a/module/contracts/api/catalog.codefly.json b/module/contracts/api/catalog.codefly.json index 914072569..c2afebc75 100644 --- a/module/contracts/api/catalog.codefly.json +++ b/module/contracts/api/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/authority/contract.binpb", - "digest": "sha256:4ce342e56e48fbe4514e9a7e3f3d4a47dfa5333de437841a9ea957fb4b05751f", + "digest": "sha256:3ba50fe62c505c2f6a2a16b8604f0abf151982dc5bb4d46bda5fa9fa72878047", "services": [ { "name": "ModuleCapabilitiesService", @@ -64,7 +64,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/connect/contract.binpb", - "digest": "sha256:4ce342e56e48fbe4514e9a7e3f3d4a47dfa5333de437841a9ea957fb4b05751f", + "digest": "sha256:3ba50fe62c505c2f6a2a16b8604f0abf151982dc5bb4d46bda5fa9fa72878047", "services": [ { "name": "APIKeyService", diff --git a/module/module.package.codefly.yaml b/module/module.package.codefly.yaml index 2f9240ef5..74dcc0998 100644 --- a/module/module.package.codefly.yaml +++ b/module/module.package.codefly.yaml @@ -18,12 +18,12 @@ services: kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/authority/contract.binpb - digest: sha256:4ce342e56e48fbe4514e9a7e3f3d4a47dfa5333de437841a9ea957fb4b05751f + digest: sha256:3ba50fe62c505c2f6a2a16b8604f0abf151982dc5bb4d46bda5fa9fa72878047 - endpoint: connect kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/connect/contract.binpb - digest: sha256:4ce342e56e48fbe4514e9a7e3f3d4a47dfa5333de437841a9ea957fb4b05751f + digest: sha256:3ba50fe62c505c2f6a2a16b8604f0abf151982dc5bb4d46bda5fa9fa72878047 - name: auth-gateway endpoints: [grpc, rest] api-contracts: diff --git a/module/services/accounts/AGENTS.md b/module/services/accounts/AGENTS.md index 0c7ae466d..3cc90ba2d 100644 --- a/module/services/accounts/AGENTS.md +++ b/module/services/accounts/AGENTS.md @@ -405,6 +405,29 @@ with: the `Mint*` exchanges' refusals carry theirs under [Source delegations](#source-delegations-a-sync-runs-with-the-connecting-persons-authority)), so matching on a bare reason across the whole service is wrong. +## Reading back a capability this host issued + +A capability presented with a QUESTION is read back, never verified: +`CheckWorkContextRecordAccess`, `ListReadableSourceCollections` and the +module-parent reads (`GetCurrentInstallation`, the executable-artifact reads) go +through `WorkContextAuthorityServer.readBack`, which is core's +`Authority.ReadBack`. It makes every check a verifier makes against live state — +signature, issuer, audience, window, revision, seal, epochs, binding — and never +touches the replay store, so the consumer that verified and consumed a +single-use capability can still ask about it, on every page of a listing. A path +that ACTS on a capability or derives from it (an exchange, a renewal, a child) +verifies and consumes with `verifyIssued`; do not move one onto `readBack`, and +do not answer a question through `verifyIssued`. + +**A read-back trusts only the current signing key.** `Authority.ReadBack` checks +the signature under the authority's own key id and nothing else, where a +verifier holds a key map that can also carry a previous key. So during a key +rotation, a capability signed under the PREVIOUS key is refused by every +read-back — fail closed, as an invalid capability — while a consumer whose JWKS +still lists that key accepts it. The capability's lifetime bounds the window: +its holder mints a new one under the current key. Rotating the Work Context key +therefore briefly refuses questions about capabilities already in flight. + ## Minting a module Work Context The identity itself is a **Work Context**. The auth-gateway brokers the exchange @@ -741,7 +764,15 @@ that authority (`pkg/business/source_delegation.go`): person-owned context with a module actor — takes the sealed binding id (`operation_binding_id`) and confirms it from the delegation rather than the row-backed agent path, so it stops confirming once the delegation, its binding, - the source, the person's role or their revision moves. + the source, the person's role or their revision moves. That revision is the + person's ordinary one — the higher of the organization's and the person's own — + so revocation is coarser than the delegation alone: ANY organization-wide + revision bump (a role or membership change anywhere in the organization) + revokes every delegated capability in flight. A verifier answers it as stale + (`FAILED_PRECONDITION`), and the consumer mints again; a delegation that has + itself ended then answers `PERMISSION_DENIED` (`DELEGATION_REVOKED`). The + lifetime is 60 seconds, so the cost is one extra round trip, not a stall — but + a consumer must not retry a stale capability as if the failure were transient. - **Delegation and installation are both required at use.** Minting, revision confirmation and exchange hold the actor's installation in the delegation's organization to the current envelope. `cross_tenant` permits consent in other diff --git a/module/services/accounts/code/go.mod b/module/services/accounts/code/go.mod index f3dccd2c1..3b26d40b8 100644 --- a/module/services/accounts/code/go.mod +++ b/module/services/accounts/code/go.mod @@ -8,7 +8,7 @@ require ( connectrpc.com/connect v1.21.0 connectrpc.com/otelconnect v0.9.0 github.com/codefly-dev/cli/contracts/deployment v0.0.0-20261006151622-5824cf5342fc - github.com/codefly-dev/core v0.19.2 + github.com/codefly-dev/core v0.19.3 github.com/codefly-dev/interface-cache/go/cache v0.2.2-0.20260926205419-327a6e1d9aba github.com/codefly-dev/sdk-go v0.4.0 github.com/codefly-dev/sdk-go/workcontext v0.1.0 diff --git a/module/services/accounts/code/go.sum b/module/services/accounts/code/go.sum index bb62324d2..f1d2bc8f7 100644 --- a/module/services/accounts/code/go.sum +++ b/module/services/accounts/code/go.sum @@ -60,8 +60,8 @@ github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/codefly-dev/cli/contracts/deployment v0.0.0-20261006151622-5824cf5342fc h1:olDnVJ1A1Retn0z9A59YMwDRmDLgkV2y1lbl+HzNrj8= github.com/codefly-dev/cli/contracts/deployment v0.0.0-20261006151622-5824cf5342fc/go.mod h1:VWPYY3FH4te7SzdN2xl9odNpWidJ1Grl/Hjs1SNoEF8= -github.com/codefly-dev/core v0.19.2 h1:eoto82yKQPxLvtn5ie0hk7MYtTGTvikjkQjcaUuQtso= -github.com/codefly-dev/core v0.19.2/go.mod h1:4ovwm7kaHFtY9+TT8yfjA4D/BiAhIFZez7lcil8muVo= +github.com/codefly-dev/core v0.19.3 h1:nWCK+wBtwYO91oyZ4G+jNgoVGcgVCr73wz2taLLiFgM= +github.com/codefly-dev/core v0.19.3/go.mod h1:4ovwm7kaHFtY9+TT8yfjA4D/BiAhIFZez7lcil8muVo= github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ= github.com/codefly-dev/gortk v0.2.0/go.mod h1:dDWUMFgAP063OCGhTEpV7FFUj9+zTcxxO/nV80VKEwg= github.com/codefly-dev/interface-cache/go/cache v0.2.2-0.20260926205419-327a6e1d9aba h1:6nFaDYsLyA5x3WiGAk6pMI3c0naqxiPeaTHMqRPvbVs= diff --git a/module/services/accounts/code/pkg/adapters/delegated_read_audience.go b/module/services/accounts/code/pkg/adapters/delegated_read_audience.go index 32290cd46..f93ce8ee6 100644 --- a/module/services/accounts/code/pkg/adapters/delegated_read_audience.go +++ b/module/services/accounts/code/pkg/adapters/delegated_read_audience.go @@ -91,7 +91,7 @@ func (s *ModuleCapabilitiesServer) exchangeDelegatedAudience(ctx context.Context return nil, refuseUnidentifiedParent() } stale := decoded.Context() - staleErr := refusedReadBack(err, nil) + staleErr := refusedReadBack(ctx, err, nil) if service != nil { service.ObserveDelegatedAudienceExchange(auth.WithVerifiedDatabaseIdentity(ctx, stale.OwnerPrincipalId, stale.TenantId), business.DelegatedAudienceExchangeObservation{ diff --git a/module/services/accounts/code/pkg/adapters/delegated_record_access.go b/module/services/accounts/code/pkg/adapters/delegated_record_access.go index 6543b0566..bce99b609 100644 --- a/module/services/accounts/code/pkg/adapters/delegated_record_access.go +++ b/module/services/accounts/code/pkg/adapters/delegated_record_access.go @@ -34,9 +34,9 @@ func (s *ModuleCapabilitiesServer) CheckWorkContextRecordAccess(ctx context.Cont if len(values) != 1 { return nil, status.Error(codes.Unauthenticated, "one viewer Work Context is required") } - verified, err := authority.verifyIssued(ctx, values[0]) + verified, err := authority.readBack(ctx, values[0]) if err != nil { - return nil, refusedReadBack(err, status.Error(codes.Unauthenticated, "invalid viewer Work Context")) + return nil, refusedReadBack(ctx, err, status.Error(codes.Unauthenticated, "invalid viewer Work Context")) } claims := verified.Context() declared, err := service.ModuleContentResources(claims.GetAudience()) diff --git a/module/services/accounts/code/pkg/adapters/module_parent.go b/module/services/accounts/code/pkg/adapters/module_parent.go index 013c5a406..dfa94d357 100644 --- a/module/services/accounts/code/pkg/adapters/module_parent.go +++ b/module/services/accounts/code/pkg/adapters/module_parent.go @@ -37,9 +37,9 @@ func authenticateModuleParent(ctx context.Context, encoded string) (moduleParent if _, err := service.AuthorizeModuleCapability(ctx, caller); err != nil { return moduleParentAuthentication{}, err } - presented, err := authority.verifyIssued(ctx, encoded) + presented, err := authority.readBack(ctx, encoded) if err != nil { - return moduleParentAuthentication{}, refusedReadBack(err, status.Error(codes.PermissionDenied, "invalid module parent")) + return moduleParentAuthentication{}, refusedReadBack(ctx, err, status.Error(codes.PermissionDenied, "invalid module parent")) } if presented.Context().GetTenantId() == "" || presented.Context().GetOwnerPrincipalId() == "" { return moduleParentAuthentication{}, status.Error(codes.PermissionDenied, "invalid module parent") diff --git a/module/services/accounts/code/pkg/adapters/readable_sources.go b/module/services/accounts/code/pkg/adapters/readable_sources.go index 691b01aa2..da30e13cb 100644 --- a/module/services/accounts/code/pkg/adapters/readable_sources.go +++ b/module/services/accounts/code/pkg/adapters/readable_sources.go @@ -44,9 +44,9 @@ func (s *ModuleCapabilitiesServer) ListReadableSourceCollections(ctx context.Con // module the capability was minted for, and that module's own declaration // says which permission resource types its content is governed by. A host // that spelled one here would answer "no access" for every other consumer. - verified, err := authority.verifyIssued(ctx, values[0]) + verified, err := authority.readBack(ctx, values[0]) if err != nil { - return nil, refusedReadBack(err, status.Error(codes.Unauthenticated, "invalid viewer Work Context")) + return nil, refusedReadBack(ctx, err, status.Error(codes.Unauthenticated, "invalid viewer Work Context")) } claims := verified.Context() // Both refusals answer identically. Telling "that audience declares no content" diff --git a/module/services/accounts/code/pkg/adapters/work_context_core.go b/module/services/accounts/code/pkg/adapters/work_context_core.go index f547146de..a2b44b906 100644 --- a/module/services/accounts/code/pkg/adapters/work_context_core.go +++ b/module/services/accounts/code/pkg/adapters/work_context_core.go @@ -9,6 +9,7 @@ import ( "time" basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" + "github.com/codefly-dev/core/wool" "github.com/codefly-dev/core/workcontext" carrier "github.com/codefly-dev/sdk-go/workcontext" "github.com/google/uuid" @@ -231,6 +232,31 @@ func (s *WorkContextAuthorityServer) verifyIssued(ctx context.Context, encoded s return s.coreVerifier(claims.GetTenantId(), claims.GetOwnerPrincipalId(), claims.GetAudience()).Verify(ctx, encoded) } +// readBack answers a question a capability's holder asks about a capability +// this host issued — what it may read, which collections it reaches, whether a +// module's parent is still good — at full strength and WITHOUT SPENDING IT +// (core's Authority.ReadBack). +// +// A question is not a use. Single-use bounds how often a capability is +// presented and acted on, and its consumer — the holder that verified it first +// and consumed it through ConsumeSingleUse — still has to be able to ask, on +// every page of a paginated answer. So this never touches the replay store, +// while revision, seal and grant are held against live state exactly as a +// verifier holds them. It answers questions only: a path that acts on the +// capability or derives from it verifies, and consumes, with verifyIssued. +func (s *WorkContextAuthorityServer) readBack(ctx context.Context, encoded string) (*workcontext.ReadBackCapability, error) { + if !s.configured() { + return nil, ErrWorkContextAuthorityUnconfigured + } + decoded, err := workcontext.Decode(encoded) + if err != nil { + return nil, err + } + claims := decoded.Context() + return s.coreAuthority(claims.GetTenantId(), claims.GetOwnerPrincipalId()).ReadBack(ctx, encoded, + workcontext.ReadBackCheck{Audience: claims.GetAudience(), Grants: noApprovalGrants{}}) +} + // delegationHop is one actor hop a mint appends after the owner's session. type delegationHop struct { PrincipalID string @@ -437,7 +463,17 @@ func workContextGrants(claims *basev0.WorkContextV1, resourceKind, action, resou // moved seal or epoch — answers FailedPrecondition, the code the host's own // recheck has always answered a stale capability with and the one a consumer // re-mints on. Anything else answers invalid, with the site's own wording. -func refusedReadBack(err error, invalid error) error { +// +// A capability in another encoding — one minted before this host signed with +// core, which no verifier here can read — is logged with its cause, never its +// token: the caller is told only that it is invalid, and the operator watching +// a wall of refusals during a cutover is told why. +func refusedReadBack(ctx context.Context, err error, invalid error) error { + if errors.Is(err, workcontext.ErrNotACoreToken) { + wool.Get(ctx).In("refusedReadBack").Warn( + "a Work Context in a pre-cutover encoding was presented; this host issues and reads only core/workcontext capabilities, so the caller must mint a new one", + wool.ErrField(err)) + } if errors.Is(err, workcontext.ErrRevoked) || errors.Is(err, workcontext.ErrNoSeal) { return status.Error(codes.FailedPrecondition, "Work Context authorization revision is stale: its authority changed since it was minted") } diff --git a/module/services/accounts/code/pkg/adapters/work_context_mcp_mint_test.go b/module/services/accounts/code/pkg/adapters/work_context_mcp_mint_test.go index 808755ee6..ea6a8da3c 100644 --- a/module/services/accounts/code/pkg/adapters/work_context_mcp_mint_test.go +++ b/module/services/accounts/code/pkg/adapters/work_context_mcp_mint_test.go @@ -7,6 +7,7 @@ import ( basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" "github.com/codefly-dev/core/workcontext" "github.com/stretchr/testify/require" + "google.golang.org/grpc/metadata" accountsauth "accounts/pkg/auth" "accounts/pkg/business" @@ -47,7 +48,6 @@ func TestStartTaskMintsWhatACoreVerifierReadsForAModule(t *testing.T) { issued, err := server.StartTask(ctx, &gen.StartTaskWorkContextRequest{ OrgId: renewOrgID, TaskId: renewTaskID, - SessionId: "019f6bf7-0000-7000-8000-000000000000", // named by the runtime; not what is signed Audience: "documents", TtlSeconds: 300, AuthorityScopes: []*gen.WorkContextScope{{ResourceKind: "documents", Actions: []string{"read"}}}, @@ -79,17 +79,18 @@ func TestStartTaskMintsWhatACoreVerifierReadsForAModule(t *testing.T) { // capability to, and is minted nothing. seals.uninstalled["documents"] = true _, err = server.StartTask(ctx, &gen.StartTaskWorkContextRequest{ - OrgId: renewOrgID, TaskId: renewTaskID, SessionId: confinedSession, Audience: "documents", TtlSeconds: 300, + OrgId: renewOrgID, TaskId: renewTaskID, Audience: "documents", TtlSeconds: 300, AuthorityScopes: []*gen.WorkContextScope{{ResourceKind: "documents", Actions: []string{"read"}}}, }) require.Error(t, err) require.Contains(t, err.Error(), "not installed in this organization") } -// StartChildSession and StartInstallationTask assign the session they sign, -// whatever the request carried: a session a caller could name is one it could -// name after another credential's, and the gateway pairs a confined credential -// with a capability on the session alone. +// StartChildSession assigns the session it signs: a session a caller could name +// is one it could name after another credential's, and the gateway pairs a +// confined credential with a capability on the session alone. +// StartInstallationTask is held to the same rule by +// TestStartInstallationTaskAssignsAFreshSessionEachTime. func TestDerivedSessionsAreAssignedByTheIssuer(t *testing.T) { previous := service svc, err := business.NewService(renewMembershipStore{}) @@ -123,3 +124,39 @@ func TestDerivedSessionsAreAssignedByTheIssuer(t *testing.T) { } require.NotEqual(t, first.GetSessionId(), second.GetSessionId(), "each child session is fresh") } + +// StartInstallationTask is a headless mint: no caller session exists to bind, so +// every capability it issues opens a fresh session, assigned by the issuer and +// returned in IssuedWorkContext — never one a request named, and never shared. +func TestStartInstallationTaskAssignsAFreshSessionEachTime(t *testing.T) { + _, facts, _, _ := sourceReadFixture(t) + svc, err := business.NewService(&exactRecordStore{}) + require.NoError(t, err) + svc.SetModuleAuthorityReads(currentModuleAuthority{service: svc}, nil) + svc.SetModuleCapabilities(nil, nil, business.ModulePrincipalRegistry{business.ModulePrincipalID("rows"): {Prefix: "rows", Resources: []string{"rows"}}}) + service = svc + declareTestAudiences(svc) + facts.installationFcts = &business.InstallationAuthorityFacts{ + OwnerPrincipalID: readOwner, + OrganizationRevision: facts.facts.OrganizationRevision, + OwnerPrincipalRevision: facts.facts.PrincipalRevision, + Actor: &business.Principal{ID: installAgentID, Kind: business.PrincipalKindAgent, + AgentIdentifier: "example.test/solution:1.0.0", AllowedAudiences: []string{"rows"}, AllowedScopes: []string{"rows"}}, + } + mint := func() *gen.IssuedWorkContext { + issued, err := workContextSingleton.StartInstallationTask( + metadata.NewIncomingContext(context.Background(), metadata.Pairs("x-codefly-internal-token", "source-read-test-perimeter")), + &gen.StartInstallationTaskRequest{OrgId: readOrg, InstallationId: installID, TaskId: installTaskID, Audience: "rows", + AuthorityScopes: []*gen.WorkContextScope{{ResourceKind: "rows", Actions: []string{"read"}, ResourceIds: []string{"record-a"}}}}) + require.NoError(t, err) + return issued + } + first, second := mint(), mint() + for _, issued := range []*gen.IssuedWorkContext{first, second} { + require.NotEmpty(t, issued.GetSessionId()) + verified, err := workContextSingleton.verifyIssued(context.Background(), issued.GetToken()) + require.NoError(t, err) + require.Equal(t, issued.GetSessionId(), verified.Context().GetSessionId(), "the session returned is the one signed") + } + require.NotEqual(t, first.GetSessionId(), second.GetSessionId(), "each installation capability opens its own session") +} diff --git a/module/services/accounts/code/pkg/adapters/work_context_pre_cutover_test.go b/module/services/accounts/code/pkg/adapters/work_context_pre_cutover_test.go new file mode 100644 index 000000000..06ac4566f --- /dev/null +++ b/module/services/accounts/code/pkg/adapters/work_context_pre_cutover_test.go @@ -0,0 +1,73 @@ +package adapters + +import ( + "context" + "crypto/ed25519" + "encoding/base64" + "encoding/json" + "strings" + "sync" + "testing" + "time" + + "github.com/codefly-dev/core/wool" + "github.com/codefly-dev/core/workcontext" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "accounts/pkg/business" +) + +// A capability minted BEFORE the cutover — the pre-core JSON payload, signed by +// the same key — is refused by every read-back, and the operator is told why: +// the refusal to the caller stays "invalid", and the log names the encoding, +// never the token. +func TestR1093_PreCutoverJSONCapabilityIsRefusedAndLogged(t *testing.T) { + logs := captureAdapterLogs(t) + server := newCoreTestServer(t, &workContextAuthorityFake{facts: &business.WorkContextAuthorityFacts{OrganizationRevision: 12}}, newSealAnswersFake(12)) + payload, err := json.Marshal(map[string]any{ + "typ": "codefly-work-context+v1", "alg": "EdDSA", "kid": server.keyID, "iss": server.issuer, + "aud": "tool.test", "tenant_id": renewOrgID, "owner_principal_id": renewActorID, + "exp": time.Now().Add(time.Minute).Unix(), "nonce": "n-1", + }) + require.NoError(t, err) + token := base64.RawURLEncoding.EncodeToString(payload) + "." + base64.RawURLEncoding.EncodeToString(ed25519.Sign(server.key, payload)) + + _, err = server.readBack(context.Background(), token) + require.ErrorIs(t, err, workcontext.ErrNotACoreToken) + _, _, err = server.verifyParent(context.Background(), renewOrgID, renewActorID, token) + require.Equal(t, codes.PermissionDenied, status.Code(err), "the caller learns only that it is invalid") + + logged := logs() + require.Contains(t, logged, "pre-cutover encoding") + require.Contains(t, logged, "not a core token") + require.NotContains(t, logged, token[:24], "the token is never logged") +} + +// captureAdapterLogs redirects wool's fallback logger — the one Get() uses for a +// context carrying no provider — and returns a reader for what was logged. +func captureAdapterLogs(t *testing.T) func() string { + t.Helper() + capture := &adapterLogCapture{} + wool.SetFallbackLogger(capture) + t.Cleanup(func() { wool.SetFallbackLogger(nil) }) + return capture.String +} + +type adapterLogCapture struct { + mu sync.Mutex + lines []string +} + +func (c *adapterLogCapture) Process(msg *wool.Log) { + c.mu.Lock() + defer c.mu.Unlock() + c.lines = append(c.lines, msg.String()) +} + +func (c *adapterLogCapture) String() string { + c.mu.Lock() + defer c.mu.Unlock() + return strings.Join(c.lines, "\n") +} diff --git a/module/services/accounts/code/pkg/adapters/work_context_read_back_test.go b/module/services/accounts/code/pkg/adapters/work_context_read_back_test.go new file mode 100644 index 000000000..3fbeea270 --- /dev/null +++ b/module/services/accounts/code/pkg/adapters/work_context_read_back_test.go @@ -0,0 +1,142 @@ +package adapters + +import ( + "context" + "testing" + "time" + + "connectrpc.com/connect" + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" + "github.com/codefly-dev/core/workcontext" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" + + "accounts/pkg/business" + gen "accounts/pkg/gen/saas/accounts/v1" +) + +// The host's read-backs answer questions about a viewer's capability; they do +// not spend it. The documented consumer flow is: verify the capability, consume +// it through ConsumeSingleUse, then ask the host what it may read — record by +// record, page by page. Every one of those questions must still be answered for +// a SINGLE_USE capability, and none of them may consume it. + +// singleUseViewer is a single-use capability readOwner holds for audience, +// reading kind — limited to ids when any are named. +func singleUseViewer(t *testing.T, audience, kind string, ids ...string) (string, *basev0.WorkContextV1) { + t.Helper() + return mintTestCapability(t, workContextSingleton, testCapability{ + Audience: audience, Tenant: readOrg, Owner: readOwner, + Task: "019f6bf7-1111-7111-8111-111111111111", Session: "019f6bf7-2222-7222-8222-222222222222", + Replay: workcontext.ReplaySingleUse, + Scopes: []*basev0.WorkScopeV1{{ResourceKind: kind, Actions: []string{"read"}, ResourceIds: ids}}, + }) +} + +// consumeAsTheConsumer is ConsumeSingleUse as a consumer calls it after +// verifying: the token's own nonce, its tenant and expiry. +func consumeAsTheConsumer(t *testing.T, signed *basev0.WorkContextV1) error { + t.Helper() + ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("x-codefly-internal-token", "source-read-test-perimeter")) + _, err := workContextSingleton.ConsumeSingleUse(ctx, &gen.ConsumeSingleUseWorkContextRequest{ + OrgId: signed.GetTenantId(), ContextId: signed.GetNonce(), + ExpiresAt: timestamppb.New(time.Unix(signed.GetExpiresAtUnix(), 0)), + }) + return err +} + +func recordAccessService(t *testing.T) { + t.Helper() + svc, err := business.NewService(&exactRecordStore{}) + require.NoError(t, err) + svc.SetModuleAuthorityReads(currentModuleAuthority{service: svc}, nil) + svc.SetModuleCapabilities(nil, nil, business.ModulePrincipalRegistry{business.ModulePrincipalID("rows"): {Prefix: "rows", Resources: []string{"rows"}}}) + service = svc + declareTestAudiences(svc) +} + +func TestR1093_AConsumedSingleUseViewerCapabilityCanStillBeAskedAbout(t *testing.T) { + _, _, client, _ := sourceReadFixture(t) + recordAccessService(t) + token, signed := singleUseViewer(t, "rows", "rows", "record-a") + + require.NoError(t, consumeAsTheConsumer(t, signed), "the consumer verifies and consumes its capability first") + + out, err := client.CheckWorkContextRecordAccess(context.Background(), exactRequest(token, "record-a")) + require.NoError(t, err, "and can still ask the host what it may read with it") + require.True(t, out.Msg.Allowed) +} + +func TestR1093_TwoQuestionsAboutOneSingleUseCapability(t *testing.T) { + _, _, client, _ := sourceReadFixture(t) + recordAccessService(t) + token, signed := singleUseViewer(t, "rows", "rows", "record-a") + + for range 2 { + out, err := client.CheckWorkContextRecordAccess(context.Background(), exactRequest(token, "record-a")) + require.NoError(t, err, "a question asks; it does not spend the capability") + require.True(t, out.Msg.Allowed) + } + require.NoError(t, consumeAsTheConsumer(t, signed), "the questions left it for its consumer") + err := consumeAsTheConsumer(t, signed) + require.Equal(t, codes.AlreadyExists, status.Code(err), "which still consumes it exactly once") +} + +// A paginated listing is drained with one viewer capability, as the source-read +// SDK says: every page is a read-back of the same single-use capability. +func TestR1093_EveryPageOfASourceListingReadsTheSameSingleUseCapability(t *testing.T) { + _, _, client, _ := sourceReadFixture(t) + token, signed := singleUseViewer(t, "documents", "documents") + require.NoError(t, consumeAsTheConsumer(t, signed)) + + first, err := client.ListReadableSourceCollections(context.Background(), sourceReadRequest(token)) + require.NoError(t, err) + require.NotEmpty(t, first.Msg.NextPageToken, "the fixture has more than one page") + next := sourceReadRequest(token) + next.Msg.PageToken = first.Msg.NextPageToken + _, err = client.ListReadableSourceCollections(context.Background(), next) + require.NoError(t, err, "page two reads back the same capability") +} + +// Reading back is still a full-strength check: a capability whose authority has +// moved is refused, consumed or not. +func TestReadBackStillRefusesAStaleCapability(t *testing.T) { + _, facts, client, _ := sourceReadFixture(t) + token, _ := singleUseViewer(t, "documents", "documents") + facts.facts.OrganizationRevision++ + + _, err := client.ListReadableSourceCollections(context.Background(), sourceReadRequest(token)) + require.Equal(t, connect.CodeFailedPrecondition, connect.CodeOf(err), "%v", err) +} + +// A module asking about a person's parent (here, the installation it was +// admitted under) reads the parent back; asking twice spends nothing. +func TestR1093_AModuleAsksAboutASingleUseParentMoreThanOnce(t *testing.T) { + _, _, client, _ := sourceReadFixture(t) + id, target := uuid.NewString(), uuid.NewString() + store := ¤tInstallationStore{ + artifactTransportStore: artifactTransportStore{member: true}, + installation: &gen.Installation{Id: id, OrgId: readOrg, TargetId: target, Status: gen.InstallationStatus_INSTALLATION_STATUS_ACTIVE}, + target: &business.SolutionTarget{ID: target, BindingID: "binding-acme-example-0001", SolutionID: "acme/example"}, + } + var err error + service, err = business.NewService(store) + require.NoError(t, err) + service.SetModuleCapabilities(nil, nil, business.ModulePrincipalRegistry{business.ModulePrincipalID("example"): {Prefix: "example", Tenant: readOrg}}) + installCurrentInstallationCaller(t, service, readOrg) + parent, _ := singleUseViewer(t, "example", "definitions") + + for range 2 { + r := connect.NewRequest(&gen.ModuleCurrentInstallationRequest{InstallationId: id, ParentWorkContextToken: parent}) + for k, v := range readExchangeRequest(t, parent).Header() { + r.Header()[k] = append([]string{}, v...) + } + out, err := client.GetCurrentInstallation(context.Background(), r) + require.NoError(t, err, "a question about the parent does not spend it") + require.Equal(t, id, out.Msg.InstallationId) + } +} diff --git a/module/services/accounts/code/pkg/adapters/work_context_reserved_audience_test.go b/module/services/accounts/code/pkg/adapters/work_context_reserved_audience_test.go index fdbfb49f5..ec4c89ed3 100644 --- a/module/services/accounts/code/pkg/adapters/work_context_reserved_audience_test.go +++ b/module/services/accounts/code/pkg/adapters/work_context_reserved_audience_test.go @@ -49,7 +49,6 @@ func TestPersonMintsRefuseTheModuleAudience(t *testing.T) { {"StartTask", func() error { _, err := server.StartTask(ctx, &gen.StartTaskWorkContextRequest{ OrgId: uuidForReservedAudienceTest, TaskId: uuidForReservedAudienceTest, - SessionId: uuidForReservedAudienceTest, ActorPrincipalId: uuidForReservedAudienceTest, AuthorityScopes: scopesForReservedAudienceTest(), Audience: ModuleWorkContextAudience, diff --git a/module/services/accounts/code/pkg/adapters/work_context_rpcs.go b/module/services/accounts/code/pkg/adapters/work_context_rpcs.go index abdc11063..20667e839 100644 --- a/module/services/accounts/code/pkg/adapters/work_context_rpcs.go +++ b/module/services/accounts/code/pkg/adapters/work_context_rpcs.go @@ -15,7 +15,6 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" - "github.com/codefly-dev/core/wool" "github.com/codefly-dev/core/workcontext" "github.com/google/uuid" @@ -531,7 +530,7 @@ func (s *WorkContextAuthorityServer) StartTask( }, // The capability opens the session the caller authenticated with, and // never one the request named: see callerSession. - Session: callerSession(ctx, req.GetSessionId()), + Session: callerSession(ctx), Replay: workContextReplayPolicy(req.GetReplayPolicy()), TTL: workContextTTL(req.GetTtlSeconds()), } @@ -1151,7 +1150,7 @@ func (s *WorkContextAuthorityServer) ExchangeAudience( // session they authenticated with. A hop in the chain that bears an // execution cannot be attested by an owner, so such a chain is exchanged by // its module through the module capability surface instead. - return s.exchangeVerifiedParent(ctx, parent.Context(), actor, req, workcontext.Execution{}, callerSession(ctx, "")) + return s.exchangeVerifiedParent(ctx, parent.Context(), actor, req, workcontext.Execution{}, callerSession(ctx)) } // exchangeVerifiedParent runs AFTER the caller's authentication and @@ -1451,30 +1450,17 @@ func (s *WorkContextAuthorityServer) authorizeOwner(ctx context.Context, orgID s // callerSession is the session a capability the caller opens is bound to: the // session the caller authenticated with, or "" — which core turns into a fresh -// one — when it has none. It is NEVER the id a request named. +// one — when it has none. A request cannot name one: StartTask has no session +// field (it is reserved), like every other mint. // // That is what makes a session id identify its owner. A party holding a // confined access token pairs a capability with it on the session alone (the // gateway's module routes do), and the pairing holds only while every session a -// capability carries is either its caller's verified session or a fresh one. A -// requested id signed as-is would let any caller mint a capability carrying -// another credential's session. -// -// A divergent requested id is ignored rather than refused: it is a correlation -// field the derived value replaces, not authority, and refusing it would break -// every caller that roots its own Task and Session ids while presenting a real -// session. The issued response carries the session actually bound, which is -// what a caller correlates on. Neither id is logged — a session id identifies a -// live session, and wool masks keys rather than values. -func callerSession(ctx context.Context, requested string) string { +// capability carries is either its caller's verified session or a fresh one. +// The issued response carries the session actually bound, which is what a +// caller correlates on. +func callerSession(ctx context.Context) string { verified, ok := accountsauth.VerifiedSessionID(ctx) - if ok && requested != "" { - if parsed, _ := uuid.Parse(requested); parsed != verified { - wool.Get(ctx).In("StartTask").Warn( - "requested session_id is not the caller's verified session; binding to the verified session", - ) - } - } return issuedSession(verified, ok) } @@ -1489,7 +1475,7 @@ func (s *WorkContextAuthorityServer) verifyParent( ) (*workcontext.Verified, *business.Principal, error) { parent, err := s.verifyIssued(ctx, encoded) if err != nil { - return nil, nil, refusedReadBack(err, status.Error(codes.PermissionDenied, "parent Work Context is not valid for caller")) + return nil, nil, refusedReadBack(ctx, err, status.Error(codes.PermissionDenied, "parent Work Context is not valid for caller")) } claims := parent.Context() if claims.GetTenantId() != orgID || claims.GetOwnerPrincipalId() != ownerID { @@ -1515,7 +1501,7 @@ func (s *WorkContextAuthorityServer) verifyActorParent( ) (*workcontext.Verified, *business.Principal, error) { parent, err := s.verifyIssued(ctx, encoded) if err != nil { - return nil, nil, refusedReadBack(err, status.Error(codes.PermissionDenied, "parent Work Context is not valid")) + return nil, nil, refusedReadBack(ctx, err, status.Error(codes.PermissionDenied, "parent Work Context is not valid")) } if parent.Context().GetTenantId() != orgID { return nil, nil, status.Error(codes.PermissionDenied, "parent Work Context is not valid") diff --git a/module/services/accounts/code/pkg/adapters/work_context_rpcs_test.go b/module/services/accounts/code/pkg/adapters/work_context_rpcs_test.go index 35603cf5b..c6e926445 100644 --- a/module/services/accounts/code/pkg/adapters/work_context_rpcs_test.go +++ b/module/services/accounts/code/pkg/adapters/work_context_rpcs_test.go @@ -746,7 +746,6 @@ func TestStartTaskRootsTaskInCallerVerifiedSession(t *testing.T) { return &gen.StartTaskWorkContextRequest{ OrgId: renewOrgID, TaskId: renewTaskID, - SessionId: sessionID, ActorPrincipalId: renewActorID, Audience: "tool.test", TtlSeconds: 900, diff --git a/module/services/accounts/code/pkg/adapters/work_context_seal_source.go b/module/services/accounts/code/pkg/adapters/work_context_seal_source.go deleted file mode 100644 index edab46f2e..000000000 --- a/module/services/accounts/code/pkg/adapters/work_context_seal_source.go +++ /dev/null @@ -1,167 +0,0 @@ -package adapters - -import ( - "context" - "errors" - - "accounts/pkg/business" - - "github.com/codefly-dev/core/workcontext" -) - -// The host as core's SealSource, which is what makes the Work Context cutover -// possible at all. -// -// Core v0.9.0's Authenticator REQUIRES a SealSource — its own type comment says -// there is "no mode in which the seal comparison is skipped, or in which the -// authorization revision is assumed, by any entrypoint". So the host cannot -// verify a core capability without answering, live, the four questions a seal is -// compared against. This is the adapter that answers them from what the host -// already knows. -// -// EVERY METHOD IS A LIVE READ, and that is the point rather than an -// implementation detail. A seal records what was true at mint; these answer what -// is true now; and the difference is every revocation that reaches a capability -// already in flight. An implementation that cached, or that derived an answer -// from the capability being checked, would make the comparison a tautology — -// which is the defect three review rounds of this PR have caught in other -// places. - -// WorkContextSealSource answers core's seal comparisons from the host's own -// state. -type WorkContextSealSource struct { - authority business.ModuleAuthorityStore - builds business.ExecutionAuthority - bindings business.ModuleOperationBindingStore - // tenant is the organisation these reads are scoped to. - // - // Core's SealSource takes no organisation: it asks about a principal and an - // installation, because in its model those identify the authority. The host - // needs one to read `installations` and `principal_authorization_revisions`, - // both of which are per-organisation — so the scope is bound when the source - // is constructed, for the tenant the authenticator serves, rather than - // guessed per call from the capability being verified. Reading it from the - // capability would let the capability choose the scope its own seal is - // compared in. - tenant string -} - -// NewWorkContextSealSource binds a seal source to one tenant. -func NewWorkContextSealSource( - tenant string, - authority business.ModuleAuthorityStore, - builds business.ExecutionAuthority, - bindings business.ModuleOperationBindingStore, -) *WorkContextSealSource { - return &WorkContextSealSource{ - authority: authority, builds: builds, bindings: bindings, tenant: tenant, - } -} - -// Seal answers the live installation a capability names. -// -// Core compares the returned installation id and revision against the seal's. -// `ErrModuleInstallationInactive` becomes an error rather than a zero Seal: a -// zero revision is a REAL revision a fresh organisation has, so returning one -// for a revoked installation would compare equal to a capability sealed before -// any revision existed. -func (s *WorkContextSealSource) Seal( - ctx context.Context, principalID, installationID string, -) (workcontext.Seal, error) { - if s.authority == nil { - return workcontext.Seal{}, errors.New("no live authority read is wired, so no seal can be answered") - } - live, err := s.authority.LiveModuleAuthority(ctx, principalID, s.tenant, installationID) - if err != nil { - return workcontext.Seal{}, err - } - return workcontext.Seal{ - InstallationID: live.InstallationID, - InstallationRevision: live.InstallationRevision, - }, nil -} - -// PrincipalEpoch answers the principal's current envelope epoch. -// -// Read with no installation, because the epoch is a property of the principal in -// the organisation and not of any installation — passing one would make a -// principal's epoch unanswerable for a capability that names no installation, -// which is every capability until the mint seals one. -func (s *WorkContextSealSource) PrincipalEpoch( - ctx context.Context, principalID string, -) (uint64, error) { - if s.authority == nil { - return 0, errors.New("no live authority read is wired, so no principal epoch can be answered") - } - live, err := s.authority.LiveModuleAuthority(ctx, principalID, s.tenant, "") - if err != nil { - return 0, err - } - return live.ProducerEpoch, nil -} - -// ApprovedBuild answers what this host approves for a principal. -// -// Core's own documentation puts the monotonicity contract on the IMPLEMENTER: -// it compares for equality and cannot detect a source moving backwards. That -// contract is kept by `business.MonotonicApprovedBuilds`, including the clause -// core's own in-memory source missed — a digest change must come with an -// incarnation increase, or a swap-back at a fixed counter revives every -// capability the move away had revoked. -// -// The three states map onto core's: a record answers; a known principal bearing -// none is `ErrNoApprovedBuild`, which core reads as "bears no execution" and -// which is NOT a refusal; and an unknown principal is an error, because a -// capability for an identity this host has never heard of must not be admitted -// as merely unbound. -func (s *WorkContextSealSource) ApprovedBuild( - ctx context.Context, principalID string, -) (string, uint64, error) { - if s.builds == nil { - return "", 0, errors.New("no approved-build authority is wired, so no execution can be answered") - } - digest, incarnation, err := s.builds.ApprovedBuild(ctx, principalID) - switch { - case errors.Is(err, business.ErrNoApprovedBuild): - // Translated to core's sentinel, which its mint and verify branch on. - // Returning the host's own error here would read to core as a failure - // to answer rather than as "bears none", and those are different. - return "", 0, workcontext.ErrNoApprovedBuild - case err != nil: - return "", 0, err - } - return string(digest), incarnation, nil -} - -// OperationBinding resolves one binding by its id, exactly. -// -// By id and nothing else, which is core's contract and the host's own rule: the -// search this replaces walked a principal's bindings for one whose scopes -// contained the presented set, so a capability naming a narrow binding was -// satisfied by any wider binding the principal also held. -// -// The host's store takes a principal as well, because a binding is held BY a -// principal and a lookup that ignored it would resolve another principal's -// binding by id. Core asks without one, so the binding's own principal is -// returned for core to compare — the record answers who holds it rather than the -// caller asserting it. -func (s *WorkContextSealSource) OperationBinding( - ctx context.Context, bindingID string, -) (workcontext.OperationBinding, error) { - if s.bindings == nil { - return workcontext.OperationBinding{}, errors.New("no operation-binding store is wired, so no binding can be resolved") - } - binding, err := s.bindings.OperationBindingByID(ctx, "", bindingID) - if err != nil { - return workcontext.OperationBinding{}, err - } - if binding == nil { - return workcontext.OperationBinding{}, workcontext.ErrNoBinding - } - return workcontext.OperationBinding{ - ID: binding.BindingID, - Revision: binding.Revision, - }, nil -} - -var _ workcontext.SealSource = (*WorkContextSealSource)(nil) diff --git a/module/services/accounts/code/pkg/adapters/work_context_seal_source_test.go b/module/services/accounts/code/pkg/adapters/work_context_seal_source_test.go deleted file mode 100644 index 33aaa990a..000000000 --- a/module/services/accounts/code/pkg/adapters/work_context_seal_source_test.go +++ /dev/null @@ -1,218 +0,0 @@ -//go:build pure - -package adapters - -import ( - "context" - "errors" - "testing" - - "accounts/pkg/business" - - "github.com/codefly-dev/core/workcontext" - "github.com/stretchr/testify/require" -) - -// The host as core's SealSource. Every method is a LIVE read, and these pin the -// translations where getting one wrong would make a comparison tautological or -// silently permissive. - -type sealAuthority struct { - live *business.LiveModuleAuthority - err error - // asked records the arguments, because the scope a seal is compared in must - // come from the source's construction and never from the capability. - askedPrincipal, askedOrg, askedInstallation string -} - -func (f *sealAuthority) LiveModuleAuthority( - _ context.Context, principalID, orgID, installationID string, -) (*business.LiveModuleAuthority, error) { - f.askedPrincipal, f.askedOrg, f.askedInstallation = principalID, orgID, installationID - if f.err != nil { - return nil, f.err - } - return f.live, nil -} - -type sealBuilds struct { - digest business.ApprovedDigest - incarnation uint64 - err error -} - -func (f *sealBuilds) ApprovedBuild( - context.Context, string, -) (business.ApprovedDigest, uint64, error) { - if f.err != nil { - return "", 0, f.err - } - return f.digest, f.incarnation, nil -} - -type sealBindings struct { - binding *business.ModuleOperationBinding - err error - asked []string -} - -func (f *sealBindings) OperationBindingByID( - _ context.Context, _, bindingID string, -) (*business.ModuleOperationBinding, error) { - f.asked = append(f.asked, bindingID) - if f.err != nil { - return nil, f.err - } - return f.binding, nil -} - -// Seal answers the live installation, scoped to the tenant the SOURCE was built -// for — not one read off the capability being verified. -// -// Core's SealSource takes no organisation, so if the scope came from the -// capability, the capability would choose the scope its own seal is compared in. -func TestSealIsScopedToTheSourcesTenantNotTheCapability(t *testing.T) { - authority := &sealAuthority{live: &business.LiveModuleAuthority{ - InstallationID: "install-1", InstallationRevision: 9, ProducerEpoch: 4, - }} - source := NewWorkContextSealSource("org-1", authority, nil, nil) - - seal, err := source.Seal(context.Background(), "principal-1", "install-1") - require.NoError(t, err) - require.Equal(t, "install-1", seal.InstallationID) - require.Equal(t, uint64(9), seal.InstallationRevision) - require.Equal(t, "org-1", authority.askedOrg, "the tenant is the source's, bound at construction") - require.Equal(t, "install-1", authority.askedInstallation) -} - -// A revoked installation is an ERROR, not a zero Seal. -// -// Zero is a real revision a fresh organisation has, so returning one would -// compare equal to a capability sealed before any revision existed — the -// absence-is-not-zero rule, in core's direction. -func TestARevokedInstallationIsAnErrorNotAZeroSeal(t *testing.T) { - source := NewWorkContextSealSource("org-1", - &sealAuthority{err: business.ErrModuleInstallationInactive}, nil, nil) - - seal, err := source.Seal(context.Background(), "principal-1", "install-gone") - require.Error(t, err) - require.Zero(t, seal.InstallationRevision) - require.ErrorIs(t, err, business.ErrModuleInstallationInactive) -} - -// PrincipalEpoch reads with NO installation, because the epoch belongs to the -// principal in the organisation. -// -// Passing one would make a principal's epoch unanswerable for a capability that -// names no installation — which is every capability until the mint seals one. -func TestPrincipalEpochReadsWithoutAnInstallation(t *testing.T) { - authority := &sealAuthority{live: &business.LiveModuleAuthority{ProducerEpoch: 7}} - source := NewWorkContextSealSource("org-1", authority, nil, nil) - - epoch, err := source.PrincipalEpoch(context.Background(), "principal-1") - require.NoError(t, err) - require.Equal(t, uint64(7), epoch) - require.Empty(t, authority.askedInstallation, - "an epoch is a property of the principal, not of any installation") -} - -// "Bears no approved build" is translated to CORE's sentinel, because core's -// mint and verify branch on it. -// -// Returning the host's own error would read to core as a failure to ANSWER -// rather than as "bears none", and those have opposite consequences: one is an -// outage, the other is a capability that legitimately carries no execution. -func TestBearingNoApprovedBuildBecomesCoresSentinel(t *testing.T) { - source := NewWorkContextSealSource("org-1", nil, - &sealBuilds{err: business.ErrNoApprovedBuild}, nil) - - _, _, err := source.ApprovedBuild(context.Background(), "principal-1") - require.ErrorIs(t, err, workcontext.ErrNoApprovedBuild) -} - -// An UNKNOWN principal is NOT translated to "bears none". -// -// Collapsing them would admit a capability for an identity this host has never -// heard of as merely unbound — the dangerous direction, and the one the -// three-state distinction exists for. -func TestAnUnknownPrincipalIsNotReportedAsBearingNone(t *testing.T) { - source := NewWorkContextSealSource("org-1", nil, - &sealBuilds{err: business.ErrUnknownExecutionPrincipal}, nil) - - _, _, err := source.ApprovedBuild(context.Background(), "stranger") - require.Error(t, err) - require.NotErrorIs(t, err, workcontext.ErrNoApprovedBuild, - "an unknown principal must not read to core as a principal bearing no execution") -} - -// An approved build answers with its digest and incarnation, both from the -// authority rather than from anything the caller presented. -func TestAnApprovedBuildAnswersDigestAndIncarnation(t *testing.T) { - source := NewWorkContextSealSource("org-1", nil, - &sealBuilds{digest: business.ApprovedDigest("sha256:abc"), incarnation: 5}, nil) - - digest, incarnation, err := source.ApprovedBuild(context.Background(), "principal-1") - require.NoError(t, err) - require.Equal(t, "sha256:abc", digest) - require.Equal(t, uint64(5), incarnation) -} - -// A binding is resolved by EXACT id, and a missing one is core's ErrNoBinding -// rather than a zero binding. -// -// A zero OperationBinding would have revision 0, which a capability sealed at -// revision 0 would match — so "no such binding" must not be expressible as a -// successful answer. -func TestAMissingBindingIsCoresSentinelNotAZeroBinding(t *testing.T) { - bindings := &sealBindings{binding: nil} - source := NewWorkContextSealSource("org-1", nil, nil, bindings) - - _, err := source.OperationBinding(context.Background(), "binding-gone") - require.ErrorIs(t, err, workcontext.ErrNoBinding) - require.Equal(t, []string{"binding-gone"}, bindings.asked, - "exactly the named binding is looked up — never a search") -} - -// A present binding answers with the record's own revision. -func TestAPresentBindingAnswersItsRevision(t *testing.T) { - source := NewWorkContextSealSource("org-1", nil, nil, - &sealBindings{binding: &business.ModuleOperationBinding{BindingID: "binding-1", Revision: 3}}) - - binding, err := source.OperationBinding(context.Background(), "binding-1") - require.NoError(t, err) - require.Equal(t, "binding-1", binding.ID) - require.Equal(t, uint64(3), binding.Revision) -} - -// An unwired source REFUSES every question rather than answering a zero. -// -// Core has no seal-less mode, so a source that answered zeros would make every -// comparison pass while nothing was read — the exact shape of a check that does -// not run. -func TestAnUnwiredSealSourceRefusesEveryQuestion(t *testing.T) { - source := NewWorkContextSealSource("org-1", nil, nil, nil) - - _, err := source.Seal(context.Background(), "p", "i") - require.Error(t, err) - _, err = source.PrincipalEpoch(context.Background(), "p") - require.Error(t, err) - _, _, err = source.ApprovedBuild(context.Background(), "p") - require.Error(t, err) - _, err = source.OperationBinding(context.Background(), "b") - require.Error(t, err) -} - -// A failing live read propagates as a failure, never as an answer. -func TestAFailingLiveReadIsNotAnAnswer(t *testing.T) { - source := NewWorkContextSealSource("org-1", - &sealAuthority{err: errors.New("connection reset")}, nil, nil) - - _, err := source.Seal(context.Background(), "p", "i") - require.Error(t, err) - _, err = source.PrincipalEpoch(context.Background(), "p") - require.Error(t, err) -} - -func (f *sealAuthority) BeginModuleAuthorityUse(ctx context.Context) (context.Context, func(), error) { - return ctx, func() {}, nil -} diff --git a/module/services/accounts/code/pkg/adapters/work_context_solution_boundary_test.go b/module/services/accounts/code/pkg/adapters/work_context_solution_boundary_test.go index f28762f06..3bfd1e35b 100644 --- a/module/services/accounts/code/pkg/adapters/work_context_solution_boundary_test.go +++ b/module/services/accounts/code/pkg/adapters/work_context_solution_boundary_test.go @@ -194,9 +194,8 @@ func boundaryCaller() context.Context { func boundaryMintRequest(audience string) *gen.StartTaskWorkContextRequest { return &gen.StartTaskWorkContextRequest{ - OrgId: renewOrgID, - SessionId: renewSession, - Audience: audience, + OrgId: renewOrgID, + Audience: audience, AuthorityScopes: []*gen.WorkContextScope{{ ResourceKind: "evidence", Actions: []string{"read"}, diff --git a/module/services/accounts/code/pkg/business/story_host_test.go b/module/services/accounts/code/pkg/business/story_host_test.go index 4b247d672..f31488515 100644 --- a/module/services/accounts/code/pkg/business/story_host_test.go +++ b/module/services/accounts/code/pkg/business/story_host_test.go @@ -136,10 +136,9 @@ const ( // the narrowest authority the request shape accepts. func storyTaskRequest(orgID string) *gen.StartTaskWorkContextRequest { return &gen.StartTaskWorkContextRequest{ - OrgId: orgID, - TaskId: uuid.NewString(), - SessionId: uuid.NewString(), - Audience: "documents", + OrgId: orgID, + TaskId: uuid.NewString(), + Audience: "documents", AuthorityScopes: []*gen.WorkContextScope{{ ResourceKind: "evidence", Actions: []string{"read"}, diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/work_contexts.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/work_contexts.pb.go index fb1722e40..d8602cb9e 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/work_contexts.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/work_contexts.pb.go @@ -141,6 +141,12 @@ func (x *WorkContextScope) GetResourceIds() []string { return nil } +// StartTaskWorkContextRequest mints a Task capability for the caller. Its Session +// is the one the caller authenticated with — or a fresh one when it has none — +// assigned by the issuer and returned in IssuedWorkContext, never named by the +// caller: a session a caller could name is one it could name after another +// credential's, and a confined credential is paired with a capability on the +// session alone. type StartTaskWorkContextRequest struct { state protoimpl.MessageState `protogen:"open.v1"` OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` @@ -155,8 +161,7 @@ type StartTaskWorkContextRequest struct { // rule here is only "a UUID when present" and the handler refuses the wrong // shape on either path: an absent boundary on an ordinary mint, a named one // on a solution-scoped mint. - TaskId string `protobuf:"bytes,2,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` - SessionId string `protobuf:"bytes,3,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + TaskId string `protobuf:"bytes,2,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` // Empty means a direct human-owned Task. Otherwise this must name an active, // registered agent Principal in org_id. ActorPrincipalId string `protobuf:"bytes,4,opt,name=actor_principal_id,json=actorPrincipalId,proto3" json:"actor_principal_id,omitempty"` @@ -217,13 +222,6 @@ func (x *StartTaskWorkContextRequest) GetTaskId() string { return "" } -func (x *StartTaskWorkContextRequest) GetSessionId() string { - if x != nil { - return x.SessionId - } - return "" -} - func (x *StartTaskWorkContextRequest) GetActorPrincipalId() string { if x != nil { return x.ActorPrincipalId @@ -1184,12 +1182,10 @@ const file_saas_accounts_v1_work_contexts_proto_rawDesc = "" + "\rresource_kind\x18\x01 \x01(\tB\n" + "\xbaH\ar\x05\x10\x01\x18\x80\x01R\fresourceKind\x12/\n" + "\aactions\x18\x02 \x03(\tB\x15\xbaH\x12\x92\x01\x0f\b\x01\x10@\x18\x01\"\ar\x05\x10\x01\x18\x80\x01R\aactions\x127\n" + - "\fresource_ids\x18\x03 \x03(\tB\x14\xbaH\x11\x92\x01\x0e\x10\x80\x02\x18\x01\"\ar\x05\x10\x01\x18\x80\x04R\vresourceIds\"\xbf\x04\n" + + "\fresource_ids\x18\x03 \x03(\tB\x14\xbaH\x11\x92\x01\x0e\x10\x80\x02\x18\x01\"\ar\x05\x10\x01\x18\x80\x04R\vresourceIds\"\xa8\x04\n" + "\x1bStartTaskWorkContextRequest\x12\x1f\n" + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x12$\n" + - "\atask_id\x18\x02 \x01(\tB\v\xbaH\b\xd8\x01\x01r\x03\xb0\x01\x01R\x06taskId\x12'\n" + - "\n" + - "session_id\x18\x03 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\tsessionId\x12,\n" + + "\atask_id\x18\x02 \x01(\tB\v\xbaH\b\xd8\x01\x01r\x03\xb0\x01\x01R\x06taskId\x12,\n" + "\x12actor_principal_id\x18\x04 \x01(\tR\x10actorPrincipalId\x12Y\n" + "\x10authority_scopes\x18\x05 \x03(\v2\".saas.accounts.v1.WorkContextScopeB\n" + "\xbaH\a\x92\x01\x04\b\x01\x10@R\x0fauthorityScopes\x12&\n" + @@ -1206,7 +1202,8 @@ const file_saas_accounts_v1_work_contexts_proto_rawDesc = "" + " \x01(\tB\n" + "\xbaH\ar\x05\x10\x01\x18\x80\x04H\x01R\tprojectId\x88\x01\x01B\x0f\n" + "\r_workspace_idB\r\n" + - "\v_project_id\"\xab\x04\n" + + "\v_project_idJ\x04\b\x03\x10\x04R\n" + + "session_id\"\xab\x04\n" + "\x1cStartInstallationTaskRequest\x12\x1f\n" + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x121\n" + "\x0finstallation_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0einstallationId\x12!\n" + diff --git a/module/services/accounts/code/pkg/keyservice/load.go b/module/services/accounts/code/pkg/keyservice/load.go index a0c173557..34dc30606 100644 --- a/module/services/accounts/code/pkg/keyservice/load.go +++ b/module/services/accounts/code/pkg/keyservice/load.go @@ -98,15 +98,16 @@ func Selection(ctx context.Context, local bool) (Selected, error) { } if selection.SigningBackend == BackendKMS { // Named here rather than discovered at the first request. Three - // consumers are handed the signing key and sign on their own — the Work - // Context signer and the delegation minter, which take an - // ed25519.PrivateKey in github.com/codefly-dev/sdk-go and - // github.com/codefly-dev/core, and the OAuth state signer's seed — so a - // key that cannot be exported leaves the Work Context authority - // answering every RPC with a configuration error and the delegation - // minter silently falling back to its v1 HMAC. Refusing the selection - // is the honest answer until those accept a crypto.Signer. - return Selected{}, fmt.Errorf("%s=%s is not supported yet: a Cloud KMS key cannot be exported, and the Work Context signer (github.com/codefly-dev/sdk-go/workcontext), the delegation minter (github.com/codefly-dev/core policy.MintEd25519) and the OAuth state signer are each HANDED the signing key rather than being given a signer to call, so each has to accept an injected crypto.Signer first. Use %s=%s to run with no secrets store today — the key is wrapped by a Cloud KMS key and unwrapped at boot, which is non-exportable at rest rather than in use — or %s=%s to keep it in Vault", + // consumers are handed the signing key and sign on their own: the Work + // Context authority (core's workcontext.Authority, which can take an + // injected crypto.Signer but is handed the key by this host), the + // delegation minter (core's policy.MintEd25519, which takes an + // ed25519.PrivateKey) and the OAuth state signer's seed. A key that + // cannot be exported leaves the Work Context authority answering every + // RPC with a configuration error and the delegation minter silently + // falling back to its v1 HMAC. Refusing the selection is the honest + // answer until each of the three is given a signer to call. + return Selected{}, fmt.Errorf("%s=%s is not supported yet: a Cloud KMS key cannot be exported, and three consumers are HANDED the signing key rather than given a signer to call: the Work Context authority (github.com/codefly-dev/core workcontext.Authority accepts an injected crypto.Signer, but this host does not wire one yet), the delegation minter (github.com/codefly-dev/core policy.MintEd25519) and the OAuth state signer. Use %s=%s to run with no secrets store today — the key is wrapped by a Cloud KMS key and unwrapped at boot, which is non-exportable at rest rather than in use — or %s=%s to keep it in Vault", SigningBackendKey, BackendKMS, SigningBackendKey, BackendKMSWrapped, SigningBackendKey, BackendVault) } if named := groupValue(ctx, PreviousBackendKey); named != "" { diff --git a/module/services/accounts/generated/openapi-raw/api.swagger.json b/module/services/accounts/generated/openapi-raw/api.swagger.json index a089453bd..bc318dc5b 100644 --- a/module/services/accounts/generated/openapi-raw/api.swagger.json +++ b/module/services/accounts/generated/openapi-raw/api.swagger.json @@ -6390,6 +6390,7 @@ "parameters": [ { "name": "body", + "description": "StartTaskWorkContextRequest mints a Task capability for the caller. Its Session\nis the one the caller authenticated with — or a fresh one when it has none —\nassigned by the issuer and returned in IssuedWorkContext, never named by the\ncaller: a session a caller could name is one it could name after another\ncredential's, and a confined credential is paired with a capability on the\nsession alone.", "in": "body", "required": true, "schema": { @@ -11582,9 +11583,6 @@ "type": "string", "description": "The runtime boundary the capability is minted under. Required on an\nordinary mint, where the caller owns the Task it names.\n\nFORBIDDEN on a solution-scoped mint. The host assigns each registered\nsolution one opaque boundary at registration and seals that one, so a\nsolution never chooses the boundary its runs are visible under and can\nnever name another solution's (issue #1015). Which of the two a request is\ndepends on the credential it carries, which no schema rule can see, so the\nrule here is only \"a UUID when present\" and the handler refuses the wrong\nshape on either path: an absent boundary on an ordinary mint, a named one\non a solution-scoped mint." }, - "sessionId": { - "type": "string" - }, "actorPrincipalId": { "type": "string", "description": "Empty means a direct human-owned Task. Otherwise this must name an active,\nregistered agent Principal in org_id." @@ -11614,7 +11612,8 @@ "type": "string", "description": "Optional product/project attribution inside workspace_id." } - } + }, + "description": "StartTaskWorkContextRequest mints a Task capability for the caller. Its Session\nis the one the caller authenticated with — or a fresh one when it has none —\nassigned by the issuer and returned in IssuedWorkContext, never named by the\ncaller: a session a caller could name is one it could name after another\ncredential's, and a confined credential is paired with a capability on the\nsession alone." }, "v1StopImpersonationRequest": { "type": "object", diff --git a/module/services/accounts/openapi/api.swagger.json b/module/services/accounts/openapi/api.swagger.json index 22d40349b..bcc61322d 100644 --- a/module/services/accounts/openapi/api.swagger.json +++ b/module/services/accounts/openapi/api.swagger.json @@ -5126,6 +5126,7 @@ "type": "object" }, "v1StartTaskWorkContextRequest": { + "description": "StartTaskWorkContextRequest mints a Task capability for the caller. Its Session\nis the one the caller authenticated with — or a fresh one when it has none —\nassigned by the issuer and returned in IssuedWorkContext, never named by the\ncaller: a session a caller could name is one it could name after another\ncredential's, and a confined credential is paired with a capability on the\nsession alone.", "properties": { "actorPrincipalId": { "description": "Empty means a direct human-owned Task. Otherwise this must name an active,\nregistered agent Principal in org_id.", @@ -5151,9 +5152,6 @@ "replayPolicy": { "$ref": "#/definitions/v1WorkContextReplayPolicy" }, - "sessionId": { - "type": "string" - }, "taskId": { "description": "The runtime boundary the capability is minted under. Required on an\nordinary mint, where the caller owns the Task it names.\n\nFORBIDDEN on a solution-scoped mint. The host assigns each registered\nsolution one opaque boundary at registration and seals that one, so a\nsolution never chooses the boundary its runs are visible under and can\nnever name another solution's (issue #1015). Which of the two a request is\ndepends on the credential it carries, which no schema rule can see, so the\nrule here is only \"a UUID when present\" and the handler refuses the wrong\nshape on either path: an absent boundary on an ordinary mint, a named one\non a solution-scoped mint.", "type": "string" @@ -12181,6 +12179,7 @@ "operationId": "WorkContextService_StartTask", "parameters": [ { + "description": "StartTaskWorkContextRequest mints a Task capability for the caller. Its Session\nis the one the caller authenticated with — or a fresh one when it has none —\nassigned by the issuer and returned in IssuedWorkContext, never named by the\ncaller: a session a caller could name is one it could name after another\ncredential's, and a confined credential is paired with a capability on the\nsession alone.", "in": "body", "name": "body", "required": true, diff --git a/module/services/accounts/proto/saas/accounts/v1/work_contexts.proto b/module/services/accounts/proto/saas/accounts/v1/work_contexts.proto index c4a9d17f6..809ee1318 100644 --- a/module/services/accounts/proto/saas/accounts/v1/work_contexts.proto +++ b/module/services/accounts/proto/saas/accounts/v1/work_contexts.proto @@ -39,7 +39,15 @@ message WorkContextScope { ]; } +// StartTaskWorkContextRequest mints a Task capability for the caller. Its Session +// is the one the caller authenticated with — or a fresh one when it has none — +// assigned by the issuer and returned in IssuedWorkContext, never named by the +// caller: a session a caller could name is one it could name after another +// credential's, and a confined credential is paired with a capability on the +// session alone. message StartTaskWorkContextRequest { + reserved 3; + reserved "session_id"; string org_id = 1 [(buf.validate.field).string.uuid = true]; // The runtime boundary the capability is minted under. Required on an // ordinary mint, where the caller owns the Task it names. @@ -56,7 +64,6 @@ message StartTaskWorkContextRequest { (buf.validate.field).string.uuid = true, (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE ]; - string session_id = 3 [(buf.validate.field).string.uuid = true]; // Empty means a direct human-owned Task. Otherwise this must name an active, // registered agent Principal in org_id. string actor_principal_id = 4; diff --git a/module/services/auth-gateway/AGENTS.md b/module/services/auth-gateway/AGENTS.md index db7dfb282..1498870a4 100644 --- a/module/services/auth-gateway/AGENTS.md +++ b/module/services/auth-gateway/AGENTS.md @@ -168,7 +168,11 @@ ordinary browser session, where no consumer ceiling applies. It is also sufficie to name the owner, because accounts signs into a capability only its caller's verified session or a fresh one — never a session a request names — and a session belongs to one person; so the owner is not read here, which keeps the edge to what -core's `Inspector` exposes to a forwarding hop. +core's `Inspector` exposes to a forwarding hop. Dropping the owner comparison is +deliberate, not an omission: the session already names the owner, and a second +check on a property the session establishes would be a check the edge cannot make +from what it holds. `TestModuleRoute_TheSessionIsTheBindingAndTheOwnerIsNotRead` +pins the dependency, so a change to how accounts assigns sessions is caught there. **The edge check is core's `Inspector`** (`work_context.go`): encoding, issuer, the signature under the key the capability names, window, shape, and the audience diff --git a/module/services/auth-gateway/code/gateway_ceiling_headers_test.go b/module/services/auth-gateway/code/gateway_ceiling_headers_test.go index 2fb570b32..db718b0d7 100644 --- a/module/services/auth-gateway/code/gateway_ceiling_headers_test.go +++ b/module/services/auth-gateway/code/gateway_ceiling_headers_test.go @@ -34,10 +34,9 @@ import ( // audience of the module its solution declared it consumes. func mintRequestOf(caller confinedCaller, forged http.Header) *http.Request { body, _ := json.Marshal(map[string]any{ - "orgId": caller.org, - "sessionId": caller.session, - "taskId": uuid.Must(uuid.NewV7()).String(), - "audience": "documents", + "orgId": caller.org, + "taskId": uuid.Must(uuid.NewV7()).String(), + "audience": "documents", "authorityScopes": []map[string]any{ {"resourceKind": "documents", "actions": []string{"read"}}, }, diff --git a/module/services/auth-gateway/code/go.mod b/module/services/auth-gateway/code/go.mod index 87245d0c6..44b056be7 100644 --- a/module/services/auth-gateway/code/go.mod +++ b/module/services/auth-gateway/code/go.mod @@ -4,7 +4,7 @@ go 1.27.0 require ( buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 - github.com/codefly-dev/core v0.19.2 + github.com/codefly-dev/core v0.19.3 github.com/codefly-dev/sdk-go v0.4.0 github.com/codefly-dev/sdk-go/workcontext v0.1.0 github.com/envoyproxy/go-control-plane/envoy v1.39.0 diff --git a/module/services/auth-gateway/code/go.sum b/module/services/auth-gateway/code/go.sum index 6d5c0797a..6fc6eeb1a 100644 --- a/module/services/auth-gateway/code/go.sum +++ b/module/services/auth-gateway/code/go.sum @@ -44,8 +44,8 @@ github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= -github.com/codefly-dev/core v0.19.2 h1:eoto82yKQPxLvtn5ie0hk7MYtTGTvikjkQjcaUuQtso= -github.com/codefly-dev/core v0.19.2/go.mod h1:4ovwm7kaHFtY9+TT8yfjA4D/BiAhIFZez7lcil8muVo= +github.com/codefly-dev/core v0.19.3 h1:nWCK+wBtwYO91oyZ4G+jNgoVGcgVCr73wz2taLLiFgM= +github.com/codefly-dev/core v0.19.3/go.mod h1:4ovwm7kaHFtY9+TT8yfjA4D/BiAhIFZez7lcil8muVo= github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ= github.com/codefly-dev/gortk v0.2.0/go.mod h1:dDWUMFgAP063OCGhTEpV7FFUj9+zTcxxO/nV80VKEwg= github.com/codefly-dev/sdk-go v0.4.0 h1:3Jm0bP+UsLI4iZYp3wqVa42bcXBumMIIF/aeweKOAhY= diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json index 679bad374..303256d83 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json +++ b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contract.binpb", - "digest": "sha256:4ce342e56e48fbe4514e9a7e3f3d4a47dfa5333de437841a9ea957fb4b05751f", + "digest": "sha256:3ba50fe62c505c2f6a2a16b8604f0abf151982dc5bb4d46bda5fa9fa72878047", "services": [ { "name": "APIKeyService", diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb b/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb index 65d02a037..5e7e7ab90 100644 Binary files a/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb and b/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb differ diff --git a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml index adf07ed99..279079015 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml +++ b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml @@ -12,7 +12,7 @@ sources: module: saas-starter service: accounts endpoint: connect - contract-digest: sha256:4ce342e56e48fbe4514e9a7e3f3d4a47dfa5333de437841a9ea957fb4b05751f + contract-digest: sha256:3ba50fe62c505c2f6a2a16b8604f0abf151982dc5bb4d46bda5fa9fa72878047 services: - APIKeyService - AccessibleScopeService diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/work_contexts_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/work_contexts_pb.ts index b0ce2e484..417a639e2 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/work_contexts_pb.ts +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/work_contexts_pb.ts @@ -15,7 +15,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/work_contexts.proto. */ export const file_saas_accounts_v1_work_contexts: GenFile = /*@__PURE__*/ - fileDesc("CiRzYWFzL2FjY291bnRzL3YxL3dvcmtfY29udGV4dHMucHJvdG8SEHNhYXMuYWNjb3VudHMudjEiiQEKEFdvcmtDb250ZXh0U2NvcGUSIQoNcmVzb3VyY2Vfa2luZBgBIAEoCUIKukgHcgUQARiAARImCgdhY3Rpb25zGAIgAygJQhW6SBKSAQ8IARBAGAEiB3IFEAEYgAESKgoMcmVzb3VyY2VfaWRzGAMgAygJQhS6SBGSAQ4QgAIYASIHcgUQARiABCLGAwobU3RhcnRUYXNrV29ya0NvbnRleHRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESHAoHdGFza19pZBgCIAEoCUILukgI2AEBcgOwAQESHAoKc2Vzc2lvbl9pZBgDIAEoCUIIukgFcgOwAQESGgoSYWN0b3JfcHJpbmNpcGFsX2lkGAQgASgJEkgKEGF1dGhvcml0eV9zY29wZXMYBSADKAsyIi5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0U2NvcGVCCrpIB5IBBAgBEEASHAoIYXVkaWVuY2UYBiABKAlCCrpIB3IFEAEYgAQSQAoNcmVwbGF5X3BvbGljeRgHIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRSZXBsYXlQb2xpY3kSHwoLdHRsX3NlY29uZHMYCCABKAVCCrpIBxoFGIQHKAASJQoMd29ya3NwYWNlX2lkGAkgASgJQgq6SAdyBRABGIAESACIAQESIwoKcHJvamVjdF9pZBgKIAEoCUIKukgHcgUQARiABEgBiAEBQg8KDV93b3Jrc3BhY2VfaWRCDQoLX3Byb2plY3RfaWQivwMKHFN0YXJ0SW5zdGFsbGF0aW9uVGFza1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhkKB3Rhc2tfaWQYAyABKAlCCLpIBXIDsAEBEkgKEGF1dGhvcml0eV9zY29wZXMYBSADKAsyIi5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0U2NvcGVCCrpIB5IBBAgBEEASHAoIYXVkaWVuY2UYBiABKAlCCrpIB3IFEAEYgAQSQAoNcmVwbGF5X3BvbGljeRgHIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRSZXBsYXlQb2xpY3kSHwoLdHRsX3NlY29uZHMYCCABKAVCCrpIBxoFGIQHKAASJQoMd29ya3NwYWNlX2lkGAkgASgJQgq6SAdyBRABGIAESACIAQESIwoKcHJvamVjdF9pZBgKIAEoCUIKukgHcgUQARiABEgBiAEBQg8KDV93b3Jrc3BhY2VfaWRCDQoLX3Byb2plY3RfaWRKBAgEEAVSCnNlc3Npb25faWQijQIKIlN0YXJ0Um9vdFNlc3Npb25Xb3JrQ29udGV4dFJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIuChlwYXJlbnRfd29ya19jb250ZXh0X3Rva2VuGAIgASgJQgu6SAhyBhABGICAAhIcCgpzZXNzaW9uX2lkGAMgASgJQgi6SAVyA7ABARIcCghhdWRpZW5jZRgEIAEoCUIKukgHcgUQARiABBJACg1yZXBsYXlfcG9saWN5GAUgASgOMikuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFJlcGxheVBvbGljeRIfCgt0dGxfc2Vjb25kcxgGIAEoBUIKukgHGgUYhAcoACK6AgoiRXhjaGFuZ2VXb3JrQ29udGV4dEF1ZGllbmNlUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEi4KGXBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4YAiABKAlCC7pICHIGEAEYgIACEhwKCGF1ZGllbmNlGAMgASgJQgq6SAdyBRABGIAEEkkKEWF0dGVudWF0ZWRfc2NvcGVzGAQgAygLMiIuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFNjb3BlQgq6SAeSAQQIARBAEkAKDXJlcGxheV9wb2xpY3kYBSABKA4yKS5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0UmVwbGF5UG9saWN5Eh8KC3R0bF9zZWNvbmRzGAYgASgFQgq6SAcaBRiEBygAIvACCiNTdGFydENoaWxkU2Vzc2lvbldvcmtDb250ZXh0UmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEi4KGXBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4YAiABKAlCC7pICHIGEAEYgIACEiQKEmFjdG9yX3ByaW5jaXBhbF9pZBgEIAEoCUIIukgFcgOwAQESRgoOZ3JhbnRlZF9zY29wZXMYBSADKAsyIi5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0U2NvcGVCCrpIB5IBBAgBEEASHAoIYXVkaWVuY2UYBiABKAlCCrpIB3IFEAEYgAQSQAoNcmVwbGF5X3BvbGljeRgHIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRSZXBsYXlQb2xpY3kSHwoLdHRsX3NlY29uZHMYCCABKAVCCrpIBxoFGIQHKABKBAgDEARSCnNlc3Npb25faWQivwIKF1JlbmV3V29ya0NvbnRleHRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESLgoZcGFyZW50X3dvcmtfY29udGV4dF90b2tlbhgCIAEoCUILukgIcgYQARiAgAISIQoIYXVkaWVuY2UYAyABKAlCCrpIB3IFEAEYgARIAIgBARJHChFhdHRlbnVhdGVkX3Njb3BlcxgEIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRTY29wZUIIukgFkgECEEASQAoNcmVwbGF5X3BvbGljeRgFIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRSZXBsYXlQb2xpY3kSHwoLdHRsX3NlY29uZHMYBiABKAVCCrpIBxoFGIQHKABCCwoJX2F1ZGllbmNlIvECChFJc3N1ZWRXb3JrQ29udGV4dBINCgV0b2tlbhgBIAEoCRIOCgZvcmdfaWQYAiABKAkSGgoSb3duZXJfcHJpbmNpcGFsX2lkGAMgASgJEg8KB3Rhc2tfaWQYBCABKAkSEgoKc2Vzc2lvbl9pZBgFIAEoCRIeChFwYXJlbnRfc2Vzc2lvbl9pZBgGIAEoCUgAiAEBEiIKGmN1cnJlbnRfYWN0b3JfcHJpbmNpcGFsX2lkGAcgASgJEh4KFmF1dGhvcml6YXRpb25fcmV2aXNpb24YCCABKAQSLgoKZXhwaXJlc19hdBgJIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoMd29ya3NwYWNlX2lkGAogASgJSAGIAQESFwoKcHJvamVjdF9pZBgLIAEoCUgCiAEBQhQKEl9wYXJlbnRfc2Vzc2lvbl9pZEIPCg1fd29ya3NwYWNlX2lkQg0KC19wcm9qZWN0X2lkInoKGldvcmtDb250ZXh0UmV2aXNpb25TdWJqZWN0Eh4KDHByaW5jaXBhbF9pZBgBIAEoCUIIukgFcgOwAQESPAoGc2NvcGVzGAIgAygLMiIuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFNjb3BlQgi6SAWSAQIQQCKgAgohQ2hlY2tBdXRob3JpemF0aW9uUmV2aXNpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESJAoSb3duZXJfcHJpbmNpcGFsX2lkGAIgASgJQgi6SAVyA7ABARInChZhdXRob3JpemF0aW9uX3JldmlzaW9uGAMgASgEQge6SAQyAiAAEkoKCHN1YmplY3RzGAQgAygLMiwuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFJldmlzaW9uU3ViamVjdEIKukgHkgEECAEQERItChRvcGVyYXRpb25fYmluZGluZ19pZBgFIAEoCUIKukgHcgUQARiABEgAiAEBQhcKFV9vcGVyYXRpb25fYmluZGluZ19pZCKDAgocQXV0aG9yaXplRXZpZGVuY2VSZWFkUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiUKE2NhbGxlcl9wcmluY2lwYWxfaWQYAiABKAlCCLpIBXIDsAEBEikKEm93bmVyX3ByaW5jaXBhbF9pZBgDIAEoCUIIukgFcgOwAQFIAIgBARIgCgd0YXNrX2lkGAQgASgJQgq6SAdyBRABGIAESAGIAQESIwoKc2Vzc2lvbl9pZBgFIAEoCUIKukgHcgUQARiABEgCiAEBQhUKE19vd25lcl9wcmluY2lwYWxfaWRCCgoIX3Rhc2tfaWRCDQoLX3Nlc3Npb25faWQilgEKIkNvbnN1bWVTaW5nbGVVc2VXb3JrQ29udGV4dFJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIeCgpjb250ZXh0X2lkGAIgASgJQgq6SAdyBRABGIAEEjYKCmV4cGlyZXNfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQga6SAPIAQEqmwEKF1dvcmtDb250ZXh0UmVwbGF5UG9saWN5EioKJldPUktfQ09OVEVYVF9SRVBMQVlfUE9MSUNZX1VOU1BFQ0lGSUVEEAASKQolV09SS19DT05URVhUX1JFUExBWV9QT0xJQ1lfSURFTVBPVEVOVBABEikKJVdPUktfQ09OVEVYVF9SRVBMQVlfUE9MSUNZX1NJTkdMRV9VU0UQAjL7DAoSV29ya0NvbnRleHRTZXJ2aWNlEoMBChpDaGVja0F1dGhvcml6YXRpb25SZXZpc2lvbhIzLnNhYXMuYWNjb3VudHMudjEuQ2hlY2tBdXRob3JpemF0aW9uUmV2aXNpb25SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESeQoVQXV0aG9yaXplRXZpZGVuY2VSZWFkEi4uc2Fhcy5hY2NvdW50cy52MS5BdXRob3JpemVFdmlkZW5jZVJlYWRSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESegoQQ29uc3VtZVNpbmdsZVVzZRI0LnNhYXMuYWNjb3VudHMudjEuQ29uc3VtZVNpbmdsZVVzZVdvcmtDb250ZXh0UmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEsoBCglTdGFydFRhc2sSLS5zYWFzLmFjY291bnRzLnYxLlN0YXJ0VGFza1dvcmtDb250ZXh0UmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuSXNzdWVkV29ya0NvbnRleHQiacLzGEQIAhADKgwKBm9yZ19pZBACGAEwAToiCh5zYWFzLndvcmtfY29udGV4dC50YXNrX3N0YXJ0ZWQQAkABSAVQA1gEYAF4AoLT5JMCGzoBKiIWL3YxL3dvcmstY29udGV4dHM6dGFzaxKGAQoVU3RhcnRJbnN0YWxsYXRpb25UYXNrEi4uc2Fhcy5hY2NvdW50cy52MS5TdGFydEluc3RhbGxhdGlvblRhc2tSZXF1ZXN0GiMuc2Fhcy5hY2NvdW50cy52MS5Jc3N1ZWRXb3JrQ29udGV4dCIYwvMYFAgDEAEwAToCEAFAAUgHUANYBGABEugBChBTdGFydFJvb3RTZXNzaW9uEjQuc2Fhcy5hY2NvdW50cy52MS5TdGFydFJvb3RTZXNzaW9uV29ya0NvbnRleHRSZXF1ZXN0GiMuc2Fhcy5hY2NvdW50cy52MS5Jc3N1ZWRXb3JrQ29udGV4dCJ5wvMYTAgCEAMqDAoGb3JnX2lkEAIYATABOioKJnNhYXMud29ya19jb250ZXh0LnJvb3Rfc2Vzc2lvbl9zdGFydGVkEAJAAUgFUARYBGABeAKC0+STAiM6ASoiHi92MS93b3JrLWNvbnRleHRzOnJvb3Qtc2Vzc2lvbhLrAQoQRXhjaGFuZ2VBdWRpZW5jZRI0LnNhYXMuYWNjb3VudHMudjEuRXhjaGFuZ2VXb3JrQ29udGV4dEF1ZGllbmNlUmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuSXNzdWVkV29ya0NvbnRleHQifMLzGEoIAhADKgwKBm9yZ19pZBACGAEwATooCiRzYWFzLndvcmtfY29udGV4dC5hdWRpZW5jZV9leGNoYW5nZWQQAkABSAVQBFgEYAF4AoLT5JMCKDoBKiIjL3YxL3dvcmstY29udGV4dHM6ZXhjaGFuZ2UtYXVkaWVuY2US7AEKEVN0YXJ0Q2hpbGRTZXNzaW9uEjUuc2Fhcy5hY2NvdW50cy52MS5TdGFydENoaWxkU2Vzc2lvbldvcmtDb250ZXh0UmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuSXNzdWVkV29ya0NvbnRleHQie8LzGE0IAhADKgwKBm9yZ19pZBACGAEwATorCidzYWFzLndvcmtfY29udGV4dC5jaGlsZF9zZXNzaW9uX3N0YXJ0ZWQQAkABSAVQBFgEYAF4AoLT5JMCJDoBKiIfL3YxL3dvcmstY29udGV4dHM6Y2hpbGQtc2Vzc2lvbhLJAQoQUmVuZXdXb3JrQ29udGV4dBIpLnNhYXMuYWNjb3VudHMudjEuUmVuZXdXb3JrQ29udGV4dFJlcXVlc3QaIy5zYWFzLmFjY291bnRzLnYxLklzc3VlZFdvcmtDb250ZXh0ImXC8xg/CAIQAyoMCgZvcmdfaWQQAhgBMAE6HQoZc2Fhcy53b3JrX2NvbnRleHQucmVuZXdlZBACQAFIBVAEWARgAXgCgtPkkwIcOgEqIhcvdjEvd29yay1jb250ZXh0czpyZW5ld2IGcHJvdG8z", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_policy_v1_options]); + fileDesc("CiRzYWFzL2FjY291bnRzL3YxL3dvcmtfY29udGV4dHMucHJvdG8SEHNhYXMuYWNjb3VudHMudjEiiQEKEFdvcmtDb250ZXh0U2NvcGUSIQoNcmVzb3VyY2Vfa2luZBgBIAEoCUIKukgHcgUQARiAARImCgdhY3Rpb25zGAIgAygJQhW6SBKSAQ8IARBAGAEiB3IFEAEYgAESKgoMcmVzb3VyY2VfaWRzGAMgAygJQhS6SBGSAQ4QgAIYASIHcgUQARiABCK6AwobU3RhcnRUYXNrV29ya0NvbnRleHRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESHAoHdGFza19pZBgCIAEoCUILukgI2AEBcgOwAQESGgoSYWN0b3JfcHJpbmNpcGFsX2lkGAQgASgJEkgKEGF1dGhvcml0eV9zY29wZXMYBSADKAsyIi5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0U2NvcGVCCrpIB5IBBAgBEEASHAoIYXVkaWVuY2UYBiABKAlCCrpIB3IFEAEYgAQSQAoNcmVwbGF5X3BvbGljeRgHIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRSZXBsYXlQb2xpY3kSHwoLdHRsX3NlY29uZHMYCCABKAVCCrpIBxoFGIQHKAASJQoMd29ya3NwYWNlX2lkGAkgASgJQgq6SAdyBRABGIAESACIAQESIwoKcHJvamVjdF9pZBgKIAEoCUIKukgHcgUQARiABEgBiAEBQg8KDV93b3Jrc3BhY2VfaWRCDQoLX3Byb2plY3RfaWRKBAgDEARSCnNlc3Npb25faWQivwMKHFN0YXJ0SW5zdGFsbGF0aW9uVGFza1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhkKB3Rhc2tfaWQYAyABKAlCCLpIBXIDsAEBEkgKEGF1dGhvcml0eV9zY29wZXMYBSADKAsyIi5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0U2NvcGVCCrpIB5IBBAgBEEASHAoIYXVkaWVuY2UYBiABKAlCCrpIB3IFEAEYgAQSQAoNcmVwbGF5X3BvbGljeRgHIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRSZXBsYXlQb2xpY3kSHwoLdHRsX3NlY29uZHMYCCABKAVCCrpIBxoFGIQHKAASJQoMd29ya3NwYWNlX2lkGAkgASgJQgq6SAdyBRABGIAESACIAQESIwoKcHJvamVjdF9pZBgKIAEoCUIKukgHcgUQARiABEgBiAEBQg8KDV93b3Jrc3BhY2VfaWRCDQoLX3Byb2plY3RfaWRKBAgEEAVSCnNlc3Npb25faWQijQIKIlN0YXJ0Um9vdFNlc3Npb25Xb3JrQ29udGV4dFJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIuChlwYXJlbnRfd29ya19jb250ZXh0X3Rva2VuGAIgASgJQgu6SAhyBhABGICAAhIcCgpzZXNzaW9uX2lkGAMgASgJQgi6SAVyA7ABARIcCghhdWRpZW5jZRgEIAEoCUIKukgHcgUQARiABBJACg1yZXBsYXlfcG9saWN5GAUgASgOMikuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFJlcGxheVBvbGljeRIfCgt0dGxfc2Vjb25kcxgGIAEoBUIKukgHGgUYhAcoACK6AgoiRXhjaGFuZ2VXb3JrQ29udGV4dEF1ZGllbmNlUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEi4KGXBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4YAiABKAlCC7pICHIGEAEYgIACEhwKCGF1ZGllbmNlGAMgASgJQgq6SAdyBRABGIAEEkkKEWF0dGVudWF0ZWRfc2NvcGVzGAQgAygLMiIuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFNjb3BlQgq6SAeSAQQIARBAEkAKDXJlcGxheV9wb2xpY3kYBSABKA4yKS5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0UmVwbGF5UG9saWN5Eh8KC3R0bF9zZWNvbmRzGAYgASgFQgq6SAcaBRiEBygAIvACCiNTdGFydENoaWxkU2Vzc2lvbldvcmtDb250ZXh0UmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEi4KGXBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4YAiABKAlCC7pICHIGEAEYgIACEiQKEmFjdG9yX3ByaW5jaXBhbF9pZBgEIAEoCUIIukgFcgOwAQESRgoOZ3JhbnRlZF9zY29wZXMYBSADKAsyIi5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0U2NvcGVCCrpIB5IBBAgBEEASHAoIYXVkaWVuY2UYBiABKAlCCrpIB3IFEAEYgAQSQAoNcmVwbGF5X3BvbGljeRgHIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRSZXBsYXlQb2xpY3kSHwoLdHRsX3NlY29uZHMYCCABKAVCCrpIBxoFGIQHKABKBAgDEARSCnNlc3Npb25faWQivwIKF1JlbmV3V29ya0NvbnRleHRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESLgoZcGFyZW50X3dvcmtfY29udGV4dF90b2tlbhgCIAEoCUILukgIcgYQARiAgAISIQoIYXVkaWVuY2UYAyABKAlCCrpIB3IFEAEYgARIAIgBARJHChFhdHRlbnVhdGVkX3Njb3BlcxgEIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRTY29wZUIIukgFkgECEEASQAoNcmVwbGF5X3BvbGljeRgFIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRSZXBsYXlQb2xpY3kSHwoLdHRsX3NlY29uZHMYBiABKAVCCrpIBxoFGIQHKABCCwoJX2F1ZGllbmNlIvECChFJc3N1ZWRXb3JrQ29udGV4dBINCgV0b2tlbhgBIAEoCRIOCgZvcmdfaWQYAiABKAkSGgoSb3duZXJfcHJpbmNpcGFsX2lkGAMgASgJEg8KB3Rhc2tfaWQYBCABKAkSEgoKc2Vzc2lvbl9pZBgFIAEoCRIeChFwYXJlbnRfc2Vzc2lvbl9pZBgGIAEoCUgAiAEBEiIKGmN1cnJlbnRfYWN0b3JfcHJpbmNpcGFsX2lkGAcgASgJEh4KFmF1dGhvcml6YXRpb25fcmV2aXNpb24YCCABKAQSLgoKZXhwaXJlc19hdBgJIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoMd29ya3NwYWNlX2lkGAogASgJSAGIAQESFwoKcHJvamVjdF9pZBgLIAEoCUgCiAEBQhQKEl9wYXJlbnRfc2Vzc2lvbl9pZEIPCg1fd29ya3NwYWNlX2lkQg0KC19wcm9qZWN0X2lkInoKGldvcmtDb250ZXh0UmV2aXNpb25TdWJqZWN0Eh4KDHByaW5jaXBhbF9pZBgBIAEoCUIIukgFcgOwAQESPAoGc2NvcGVzGAIgAygLMiIuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFNjb3BlQgi6SAWSAQIQQCKgAgohQ2hlY2tBdXRob3JpemF0aW9uUmV2aXNpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESJAoSb3duZXJfcHJpbmNpcGFsX2lkGAIgASgJQgi6SAVyA7ABARInChZhdXRob3JpemF0aW9uX3JldmlzaW9uGAMgASgEQge6SAQyAiAAEkoKCHN1YmplY3RzGAQgAygLMiwuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFJldmlzaW9uU3ViamVjdEIKukgHkgEECAEQERItChRvcGVyYXRpb25fYmluZGluZ19pZBgFIAEoCUIKukgHcgUQARiABEgAiAEBQhcKFV9vcGVyYXRpb25fYmluZGluZ19pZCKDAgocQXV0aG9yaXplRXZpZGVuY2VSZWFkUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiUKE2NhbGxlcl9wcmluY2lwYWxfaWQYAiABKAlCCLpIBXIDsAEBEikKEm93bmVyX3ByaW5jaXBhbF9pZBgDIAEoCUIIukgFcgOwAQFIAIgBARIgCgd0YXNrX2lkGAQgASgJQgq6SAdyBRABGIAESAGIAQESIwoKc2Vzc2lvbl9pZBgFIAEoCUIKukgHcgUQARiABEgCiAEBQhUKE19vd25lcl9wcmluY2lwYWxfaWRCCgoIX3Rhc2tfaWRCDQoLX3Nlc3Npb25faWQilgEKIkNvbnN1bWVTaW5nbGVVc2VXb3JrQ29udGV4dFJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIeCgpjb250ZXh0X2lkGAIgASgJQgq6SAdyBRABGIAEEjYKCmV4cGlyZXNfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQga6SAPIAQEqmwEKF1dvcmtDb250ZXh0UmVwbGF5UG9saWN5EioKJldPUktfQ09OVEVYVF9SRVBMQVlfUE9MSUNZX1VOU1BFQ0lGSUVEEAASKQolV09SS19DT05URVhUX1JFUExBWV9QT0xJQ1lfSURFTVBPVEVOVBABEikKJVdPUktfQ09OVEVYVF9SRVBMQVlfUE9MSUNZX1NJTkdMRV9VU0UQAjL7DAoSV29ya0NvbnRleHRTZXJ2aWNlEoMBChpDaGVja0F1dGhvcml6YXRpb25SZXZpc2lvbhIzLnNhYXMuYWNjb3VudHMudjEuQ2hlY2tBdXRob3JpemF0aW9uUmV2aXNpb25SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESeQoVQXV0aG9yaXplRXZpZGVuY2VSZWFkEi4uc2Fhcy5hY2NvdW50cy52MS5BdXRob3JpemVFdmlkZW5jZVJlYWRSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESegoQQ29uc3VtZVNpbmdsZVVzZRI0LnNhYXMuYWNjb3VudHMudjEuQ29uc3VtZVNpbmdsZVVzZVdvcmtDb250ZXh0UmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEsoBCglTdGFydFRhc2sSLS5zYWFzLmFjY291bnRzLnYxLlN0YXJ0VGFza1dvcmtDb250ZXh0UmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuSXNzdWVkV29ya0NvbnRleHQiacLzGEQIAhADKgwKBm9yZ19pZBACGAEwAToiCh5zYWFzLndvcmtfY29udGV4dC50YXNrX3N0YXJ0ZWQQAkABSAVQA1gEYAF4AoLT5JMCGzoBKiIWL3YxL3dvcmstY29udGV4dHM6dGFzaxKGAQoVU3RhcnRJbnN0YWxsYXRpb25UYXNrEi4uc2Fhcy5hY2NvdW50cy52MS5TdGFydEluc3RhbGxhdGlvblRhc2tSZXF1ZXN0GiMuc2Fhcy5hY2NvdW50cy52MS5Jc3N1ZWRXb3JrQ29udGV4dCIYwvMYFAgDEAEwAToCEAFAAUgHUANYBGABEugBChBTdGFydFJvb3RTZXNzaW9uEjQuc2Fhcy5hY2NvdW50cy52MS5TdGFydFJvb3RTZXNzaW9uV29ya0NvbnRleHRSZXF1ZXN0GiMuc2Fhcy5hY2NvdW50cy52MS5Jc3N1ZWRXb3JrQ29udGV4dCJ5wvMYTAgCEAMqDAoGb3JnX2lkEAIYATABOioKJnNhYXMud29ya19jb250ZXh0LnJvb3Rfc2Vzc2lvbl9zdGFydGVkEAJAAUgFUARYBGABeAKC0+STAiM6ASoiHi92MS93b3JrLWNvbnRleHRzOnJvb3Qtc2Vzc2lvbhLrAQoQRXhjaGFuZ2VBdWRpZW5jZRI0LnNhYXMuYWNjb3VudHMudjEuRXhjaGFuZ2VXb3JrQ29udGV4dEF1ZGllbmNlUmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuSXNzdWVkV29ya0NvbnRleHQifMLzGEoIAhADKgwKBm9yZ19pZBACGAEwATooCiRzYWFzLndvcmtfY29udGV4dC5hdWRpZW5jZV9leGNoYW5nZWQQAkABSAVQBFgEYAF4AoLT5JMCKDoBKiIjL3YxL3dvcmstY29udGV4dHM6ZXhjaGFuZ2UtYXVkaWVuY2US7AEKEVN0YXJ0Q2hpbGRTZXNzaW9uEjUuc2Fhcy5hY2NvdW50cy52MS5TdGFydENoaWxkU2Vzc2lvbldvcmtDb250ZXh0UmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuSXNzdWVkV29ya0NvbnRleHQie8LzGE0IAhADKgwKBm9yZ19pZBACGAEwATorCidzYWFzLndvcmtfY29udGV4dC5jaGlsZF9zZXNzaW9uX3N0YXJ0ZWQQAkABSAVQBFgEYAF4AoLT5JMCJDoBKiIfL3YxL3dvcmstY29udGV4dHM6Y2hpbGQtc2Vzc2lvbhLJAQoQUmVuZXdXb3JrQ29udGV4dBIpLnNhYXMuYWNjb3VudHMudjEuUmVuZXdXb3JrQ29udGV4dFJlcXVlc3QaIy5zYWFzLmFjY291bnRzLnYxLklzc3VlZFdvcmtDb250ZXh0ImXC8xg/CAIQAyoMCgZvcmdfaWQQAhgBMAE6HQoZc2Fhcy53b3JrX2NvbnRleHQucmVuZXdlZBACQAFIBVAEWARgAXgCgtPkkwIcOgEqIhcvdjEvd29yay1jb250ZXh0czpyZW5ld2IGcHJvdG8z", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_policy_v1_options]); /** * WorkContextScope is one product-neutral authority slice. Product plugins @@ -49,6 +49,13 @@ export const WorkContextScopeSchema: GenMessage = /*@__PURE__* messageDesc(file_saas_accounts_v1_work_contexts, 0); /** + * StartTaskWorkContextRequest mints a Task capability for the caller. Its Session + * is the one the caller authenticated with — or a fresh one when it has none — + * assigned by the issuer and returned in IssuedWorkContext, never named by the + * caller: a session a caller could name is one it could name after another + * credential's, and a confined credential is paired with a capability on the + * session alone. + * * @generated from message saas.accounts.v1.StartTaskWorkContextRequest */ export type StartTaskWorkContextRequest = Message<"saas.accounts.v1.StartTaskWorkContextRequest"> & { @@ -74,11 +81,6 @@ export type StartTaskWorkContextRequest = Message<"saas.accounts.v1.StartTaskWor */ taskId: string; - /** - * @generated from field: string session_id = 3; - */ - sessionId: string; - /** * Empty means a direct human-owned Task. Otherwise this must name an active, * registered agent Principal in org_id. diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/work_contexts_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/work_contexts_pb.ts index 41160f518..1d8ab0837 100644 --- a/module/services/frontend/code/src/gen/saas/accounts/v1/work_contexts_pb.ts +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/work_contexts_pb.ts @@ -15,7 +15,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/work_contexts.proto. */ export const file_saas_accounts_v1_work_contexts: GenFile = /*@__PURE__*/ - fileDesc("CiRzYWFzL2FjY291bnRzL3YxL3dvcmtfY29udGV4dHMucHJvdG8SEHNhYXMuYWNjb3VudHMudjEiiQEKEFdvcmtDb250ZXh0U2NvcGUSIQoNcmVzb3VyY2Vfa2luZBgBIAEoCUIKukgHcgUQARiAARImCgdhY3Rpb25zGAIgAygJQhW6SBKSAQ8IARBAGAEiB3IFEAEYgAESKgoMcmVzb3VyY2VfaWRzGAMgAygJQhS6SBGSAQ4QgAIYASIHcgUQARiABCLGAwobU3RhcnRUYXNrV29ya0NvbnRleHRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESHAoHdGFza19pZBgCIAEoCUILukgI2AEBcgOwAQESHAoKc2Vzc2lvbl9pZBgDIAEoCUIIukgFcgOwAQESGgoSYWN0b3JfcHJpbmNpcGFsX2lkGAQgASgJEkgKEGF1dGhvcml0eV9zY29wZXMYBSADKAsyIi5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0U2NvcGVCCrpIB5IBBAgBEEASHAoIYXVkaWVuY2UYBiABKAlCCrpIB3IFEAEYgAQSQAoNcmVwbGF5X3BvbGljeRgHIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRSZXBsYXlQb2xpY3kSHwoLdHRsX3NlY29uZHMYCCABKAVCCrpIBxoFGIQHKAASJQoMd29ya3NwYWNlX2lkGAkgASgJQgq6SAdyBRABGIAESACIAQESIwoKcHJvamVjdF9pZBgKIAEoCUIKukgHcgUQARiABEgBiAEBQg8KDV93b3Jrc3BhY2VfaWRCDQoLX3Byb2plY3RfaWQivwMKHFN0YXJ0SW5zdGFsbGF0aW9uVGFza1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhkKB3Rhc2tfaWQYAyABKAlCCLpIBXIDsAEBEkgKEGF1dGhvcml0eV9zY29wZXMYBSADKAsyIi5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0U2NvcGVCCrpIB5IBBAgBEEASHAoIYXVkaWVuY2UYBiABKAlCCrpIB3IFEAEYgAQSQAoNcmVwbGF5X3BvbGljeRgHIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRSZXBsYXlQb2xpY3kSHwoLdHRsX3NlY29uZHMYCCABKAVCCrpIBxoFGIQHKAASJQoMd29ya3NwYWNlX2lkGAkgASgJQgq6SAdyBRABGIAESACIAQESIwoKcHJvamVjdF9pZBgKIAEoCUIKukgHcgUQARiABEgBiAEBQg8KDV93b3Jrc3BhY2VfaWRCDQoLX3Byb2plY3RfaWRKBAgEEAVSCnNlc3Npb25faWQijQIKIlN0YXJ0Um9vdFNlc3Npb25Xb3JrQ29udGV4dFJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIuChlwYXJlbnRfd29ya19jb250ZXh0X3Rva2VuGAIgASgJQgu6SAhyBhABGICAAhIcCgpzZXNzaW9uX2lkGAMgASgJQgi6SAVyA7ABARIcCghhdWRpZW5jZRgEIAEoCUIKukgHcgUQARiABBJACg1yZXBsYXlfcG9saWN5GAUgASgOMikuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFJlcGxheVBvbGljeRIfCgt0dGxfc2Vjb25kcxgGIAEoBUIKukgHGgUYhAcoACK6AgoiRXhjaGFuZ2VXb3JrQ29udGV4dEF1ZGllbmNlUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEi4KGXBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4YAiABKAlCC7pICHIGEAEYgIACEhwKCGF1ZGllbmNlGAMgASgJQgq6SAdyBRABGIAEEkkKEWF0dGVudWF0ZWRfc2NvcGVzGAQgAygLMiIuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFNjb3BlQgq6SAeSAQQIARBAEkAKDXJlcGxheV9wb2xpY3kYBSABKA4yKS5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0UmVwbGF5UG9saWN5Eh8KC3R0bF9zZWNvbmRzGAYgASgFQgq6SAcaBRiEBygAIvACCiNTdGFydENoaWxkU2Vzc2lvbldvcmtDb250ZXh0UmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEi4KGXBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4YAiABKAlCC7pICHIGEAEYgIACEiQKEmFjdG9yX3ByaW5jaXBhbF9pZBgEIAEoCUIIukgFcgOwAQESRgoOZ3JhbnRlZF9zY29wZXMYBSADKAsyIi5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0U2NvcGVCCrpIB5IBBAgBEEASHAoIYXVkaWVuY2UYBiABKAlCCrpIB3IFEAEYgAQSQAoNcmVwbGF5X3BvbGljeRgHIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRSZXBsYXlQb2xpY3kSHwoLdHRsX3NlY29uZHMYCCABKAVCCrpIBxoFGIQHKABKBAgDEARSCnNlc3Npb25faWQivwIKF1JlbmV3V29ya0NvbnRleHRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESLgoZcGFyZW50X3dvcmtfY29udGV4dF90b2tlbhgCIAEoCUILukgIcgYQARiAgAISIQoIYXVkaWVuY2UYAyABKAlCCrpIB3IFEAEYgARIAIgBARJHChFhdHRlbnVhdGVkX3Njb3BlcxgEIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRTY29wZUIIukgFkgECEEASQAoNcmVwbGF5X3BvbGljeRgFIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRSZXBsYXlQb2xpY3kSHwoLdHRsX3NlY29uZHMYBiABKAVCCrpIBxoFGIQHKABCCwoJX2F1ZGllbmNlIvECChFJc3N1ZWRXb3JrQ29udGV4dBINCgV0b2tlbhgBIAEoCRIOCgZvcmdfaWQYAiABKAkSGgoSb3duZXJfcHJpbmNpcGFsX2lkGAMgASgJEg8KB3Rhc2tfaWQYBCABKAkSEgoKc2Vzc2lvbl9pZBgFIAEoCRIeChFwYXJlbnRfc2Vzc2lvbl9pZBgGIAEoCUgAiAEBEiIKGmN1cnJlbnRfYWN0b3JfcHJpbmNpcGFsX2lkGAcgASgJEh4KFmF1dGhvcml6YXRpb25fcmV2aXNpb24YCCABKAQSLgoKZXhwaXJlc19hdBgJIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoMd29ya3NwYWNlX2lkGAogASgJSAGIAQESFwoKcHJvamVjdF9pZBgLIAEoCUgCiAEBQhQKEl9wYXJlbnRfc2Vzc2lvbl9pZEIPCg1fd29ya3NwYWNlX2lkQg0KC19wcm9qZWN0X2lkInoKGldvcmtDb250ZXh0UmV2aXNpb25TdWJqZWN0Eh4KDHByaW5jaXBhbF9pZBgBIAEoCUIIukgFcgOwAQESPAoGc2NvcGVzGAIgAygLMiIuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFNjb3BlQgi6SAWSAQIQQCKgAgohQ2hlY2tBdXRob3JpemF0aW9uUmV2aXNpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESJAoSb3duZXJfcHJpbmNpcGFsX2lkGAIgASgJQgi6SAVyA7ABARInChZhdXRob3JpemF0aW9uX3JldmlzaW9uGAMgASgEQge6SAQyAiAAEkoKCHN1YmplY3RzGAQgAygLMiwuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFJldmlzaW9uU3ViamVjdEIKukgHkgEECAEQERItChRvcGVyYXRpb25fYmluZGluZ19pZBgFIAEoCUIKukgHcgUQARiABEgAiAEBQhcKFV9vcGVyYXRpb25fYmluZGluZ19pZCKDAgocQXV0aG9yaXplRXZpZGVuY2VSZWFkUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiUKE2NhbGxlcl9wcmluY2lwYWxfaWQYAiABKAlCCLpIBXIDsAEBEikKEm93bmVyX3ByaW5jaXBhbF9pZBgDIAEoCUIIukgFcgOwAQFIAIgBARIgCgd0YXNrX2lkGAQgASgJQgq6SAdyBRABGIAESAGIAQESIwoKc2Vzc2lvbl9pZBgFIAEoCUIKukgHcgUQARiABEgCiAEBQhUKE19vd25lcl9wcmluY2lwYWxfaWRCCgoIX3Rhc2tfaWRCDQoLX3Nlc3Npb25faWQilgEKIkNvbnN1bWVTaW5nbGVVc2VXb3JrQ29udGV4dFJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIeCgpjb250ZXh0X2lkGAIgASgJQgq6SAdyBRABGIAEEjYKCmV4cGlyZXNfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQga6SAPIAQEqmwEKF1dvcmtDb250ZXh0UmVwbGF5UG9saWN5EioKJldPUktfQ09OVEVYVF9SRVBMQVlfUE9MSUNZX1VOU1BFQ0lGSUVEEAASKQolV09SS19DT05URVhUX1JFUExBWV9QT0xJQ1lfSURFTVBPVEVOVBABEikKJVdPUktfQ09OVEVYVF9SRVBMQVlfUE9MSUNZX1NJTkdMRV9VU0UQAjL7DAoSV29ya0NvbnRleHRTZXJ2aWNlEoMBChpDaGVja0F1dGhvcml6YXRpb25SZXZpc2lvbhIzLnNhYXMuYWNjb3VudHMudjEuQ2hlY2tBdXRob3JpemF0aW9uUmV2aXNpb25SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESeQoVQXV0aG9yaXplRXZpZGVuY2VSZWFkEi4uc2Fhcy5hY2NvdW50cy52MS5BdXRob3JpemVFdmlkZW5jZVJlYWRSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESegoQQ29uc3VtZVNpbmdsZVVzZRI0LnNhYXMuYWNjb3VudHMudjEuQ29uc3VtZVNpbmdsZVVzZVdvcmtDb250ZXh0UmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEsoBCglTdGFydFRhc2sSLS5zYWFzLmFjY291bnRzLnYxLlN0YXJ0VGFza1dvcmtDb250ZXh0UmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuSXNzdWVkV29ya0NvbnRleHQiacLzGEQIAhADKgwKBm9yZ19pZBACGAEwAToiCh5zYWFzLndvcmtfY29udGV4dC50YXNrX3N0YXJ0ZWQQAkABSAVQA1gEYAF4AoLT5JMCGzoBKiIWL3YxL3dvcmstY29udGV4dHM6dGFzaxKGAQoVU3RhcnRJbnN0YWxsYXRpb25UYXNrEi4uc2Fhcy5hY2NvdW50cy52MS5TdGFydEluc3RhbGxhdGlvblRhc2tSZXF1ZXN0GiMuc2Fhcy5hY2NvdW50cy52MS5Jc3N1ZWRXb3JrQ29udGV4dCIYwvMYFAgDEAEwAToCEAFAAUgHUANYBGABEugBChBTdGFydFJvb3RTZXNzaW9uEjQuc2Fhcy5hY2NvdW50cy52MS5TdGFydFJvb3RTZXNzaW9uV29ya0NvbnRleHRSZXF1ZXN0GiMuc2Fhcy5hY2NvdW50cy52MS5Jc3N1ZWRXb3JrQ29udGV4dCJ5wvMYTAgCEAMqDAoGb3JnX2lkEAIYATABOioKJnNhYXMud29ya19jb250ZXh0LnJvb3Rfc2Vzc2lvbl9zdGFydGVkEAJAAUgFUARYBGABeAKC0+STAiM6ASoiHi92MS93b3JrLWNvbnRleHRzOnJvb3Qtc2Vzc2lvbhLrAQoQRXhjaGFuZ2VBdWRpZW5jZRI0LnNhYXMuYWNjb3VudHMudjEuRXhjaGFuZ2VXb3JrQ29udGV4dEF1ZGllbmNlUmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuSXNzdWVkV29ya0NvbnRleHQifMLzGEoIAhADKgwKBm9yZ19pZBACGAEwATooCiRzYWFzLndvcmtfY29udGV4dC5hdWRpZW5jZV9leGNoYW5nZWQQAkABSAVQBFgEYAF4AoLT5JMCKDoBKiIjL3YxL3dvcmstY29udGV4dHM6ZXhjaGFuZ2UtYXVkaWVuY2US7AEKEVN0YXJ0Q2hpbGRTZXNzaW9uEjUuc2Fhcy5hY2NvdW50cy52MS5TdGFydENoaWxkU2Vzc2lvbldvcmtDb250ZXh0UmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuSXNzdWVkV29ya0NvbnRleHQie8LzGE0IAhADKgwKBm9yZ19pZBACGAEwATorCidzYWFzLndvcmtfY29udGV4dC5jaGlsZF9zZXNzaW9uX3N0YXJ0ZWQQAkABSAVQBFgEYAF4AoLT5JMCJDoBKiIfL3YxL3dvcmstY29udGV4dHM6Y2hpbGQtc2Vzc2lvbhLJAQoQUmVuZXdXb3JrQ29udGV4dBIpLnNhYXMuYWNjb3VudHMudjEuUmVuZXdXb3JrQ29udGV4dFJlcXVlc3QaIy5zYWFzLmFjY291bnRzLnYxLklzc3VlZFdvcmtDb250ZXh0ImXC8xg/CAIQAyoMCgZvcmdfaWQQAhgBMAE6HQoZc2Fhcy53b3JrX2NvbnRleHQucmVuZXdlZBACQAFIBVAEWARgAXgCgtPkkwIcOgEqIhcvdjEvd29yay1jb250ZXh0czpyZW5ld0K5AQoUY29tLnNhYXMuYWNjb3VudHMudjFCEVdvcmtDb250ZXh0c1Byb3RvUAFaLGFjY291bnRzL3BrZy9nZW4vc2Fhcy9hY2NvdW50cy92MTthY2NvdW50c3YxogIDU0FYqgIQU2Fhcy5BY2NvdW50cy5WMcoCEFNhYXNcQWNjb3VudHNcVjHiAhxTYWFzXEFjY291bnRzXFYxXEdQQk1ldGFkYXRh6gISU2Fhczo6QWNjb3VudHM6OlYxYgZwcm90bzM", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_policy_v1_options]); + fileDesc("CiRzYWFzL2FjY291bnRzL3YxL3dvcmtfY29udGV4dHMucHJvdG8SEHNhYXMuYWNjb3VudHMudjEiiQEKEFdvcmtDb250ZXh0U2NvcGUSIQoNcmVzb3VyY2Vfa2luZBgBIAEoCUIKukgHcgUQARiAARImCgdhY3Rpb25zGAIgAygJQhW6SBKSAQ8IARBAGAEiB3IFEAEYgAESKgoMcmVzb3VyY2VfaWRzGAMgAygJQhS6SBGSAQ4QgAIYASIHcgUQARiABCK6AwobU3RhcnRUYXNrV29ya0NvbnRleHRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESHAoHdGFza19pZBgCIAEoCUILukgI2AEBcgOwAQESGgoSYWN0b3JfcHJpbmNpcGFsX2lkGAQgASgJEkgKEGF1dGhvcml0eV9zY29wZXMYBSADKAsyIi5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0U2NvcGVCCrpIB5IBBAgBEEASHAoIYXVkaWVuY2UYBiABKAlCCrpIB3IFEAEYgAQSQAoNcmVwbGF5X3BvbGljeRgHIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRSZXBsYXlQb2xpY3kSHwoLdHRsX3NlY29uZHMYCCABKAVCCrpIBxoFGIQHKAASJQoMd29ya3NwYWNlX2lkGAkgASgJQgq6SAdyBRABGIAESACIAQESIwoKcHJvamVjdF9pZBgKIAEoCUIKukgHcgUQARiABEgBiAEBQg8KDV93b3Jrc3BhY2VfaWRCDQoLX3Byb2plY3RfaWRKBAgDEARSCnNlc3Npb25faWQivwMKHFN0YXJ0SW5zdGFsbGF0aW9uVGFza1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhkKB3Rhc2tfaWQYAyABKAlCCLpIBXIDsAEBEkgKEGF1dGhvcml0eV9zY29wZXMYBSADKAsyIi5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0U2NvcGVCCrpIB5IBBAgBEEASHAoIYXVkaWVuY2UYBiABKAlCCrpIB3IFEAEYgAQSQAoNcmVwbGF5X3BvbGljeRgHIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRSZXBsYXlQb2xpY3kSHwoLdHRsX3NlY29uZHMYCCABKAVCCrpIBxoFGIQHKAASJQoMd29ya3NwYWNlX2lkGAkgASgJQgq6SAdyBRABGIAESACIAQESIwoKcHJvamVjdF9pZBgKIAEoCUIKukgHcgUQARiABEgBiAEBQg8KDV93b3Jrc3BhY2VfaWRCDQoLX3Byb2plY3RfaWRKBAgEEAVSCnNlc3Npb25faWQijQIKIlN0YXJ0Um9vdFNlc3Npb25Xb3JrQ29udGV4dFJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIuChlwYXJlbnRfd29ya19jb250ZXh0X3Rva2VuGAIgASgJQgu6SAhyBhABGICAAhIcCgpzZXNzaW9uX2lkGAMgASgJQgi6SAVyA7ABARIcCghhdWRpZW5jZRgEIAEoCUIKukgHcgUQARiABBJACg1yZXBsYXlfcG9saWN5GAUgASgOMikuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFJlcGxheVBvbGljeRIfCgt0dGxfc2Vjb25kcxgGIAEoBUIKukgHGgUYhAcoACK6AgoiRXhjaGFuZ2VXb3JrQ29udGV4dEF1ZGllbmNlUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEi4KGXBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4YAiABKAlCC7pICHIGEAEYgIACEhwKCGF1ZGllbmNlGAMgASgJQgq6SAdyBRABGIAEEkkKEWF0dGVudWF0ZWRfc2NvcGVzGAQgAygLMiIuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFNjb3BlQgq6SAeSAQQIARBAEkAKDXJlcGxheV9wb2xpY3kYBSABKA4yKS5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0UmVwbGF5UG9saWN5Eh8KC3R0bF9zZWNvbmRzGAYgASgFQgq6SAcaBRiEBygAIvACCiNTdGFydENoaWxkU2Vzc2lvbldvcmtDb250ZXh0UmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEi4KGXBhcmVudF93b3JrX2NvbnRleHRfdG9rZW4YAiABKAlCC7pICHIGEAEYgIACEiQKEmFjdG9yX3ByaW5jaXBhbF9pZBgEIAEoCUIIukgFcgOwAQESRgoOZ3JhbnRlZF9zY29wZXMYBSADKAsyIi5zYWFzLmFjY291bnRzLnYxLldvcmtDb250ZXh0U2NvcGVCCrpIB5IBBAgBEEASHAoIYXVkaWVuY2UYBiABKAlCCrpIB3IFEAEYgAQSQAoNcmVwbGF5X3BvbGljeRgHIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRSZXBsYXlQb2xpY3kSHwoLdHRsX3NlY29uZHMYCCABKAVCCrpIBxoFGIQHKABKBAgDEARSCnNlc3Npb25faWQivwIKF1JlbmV3V29ya0NvbnRleHRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESLgoZcGFyZW50X3dvcmtfY29udGV4dF90b2tlbhgCIAEoCUILukgIcgYQARiAgAISIQoIYXVkaWVuY2UYAyABKAlCCrpIB3IFEAEYgARIAIgBARJHChFhdHRlbnVhdGVkX3Njb3BlcxgEIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRTY29wZUIIukgFkgECEEASQAoNcmVwbGF5X3BvbGljeRgFIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuV29ya0NvbnRleHRSZXBsYXlQb2xpY3kSHwoLdHRsX3NlY29uZHMYBiABKAVCCrpIBxoFGIQHKABCCwoJX2F1ZGllbmNlIvECChFJc3N1ZWRXb3JrQ29udGV4dBINCgV0b2tlbhgBIAEoCRIOCgZvcmdfaWQYAiABKAkSGgoSb3duZXJfcHJpbmNpcGFsX2lkGAMgASgJEg8KB3Rhc2tfaWQYBCABKAkSEgoKc2Vzc2lvbl9pZBgFIAEoCRIeChFwYXJlbnRfc2Vzc2lvbl9pZBgGIAEoCUgAiAEBEiIKGmN1cnJlbnRfYWN0b3JfcHJpbmNpcGFsX2lkGAcgASgJEh4KFmF1dGhvcml6YXRpb25fcmV2aXNpb24YCCABKAQSLgoKZXhwaXJlc19hdBgJIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoMd29ya3NwYWNlX2lkGAogASgJSAGIAQESFwoKcHJvamVjdF9pZBgLIAEoCUgCiAEBQhQKEl9wYXJlbnRfc2Vzc2lvbl9pZEIPCg1fd29ya3NwYWNlX2lkQg0KC19wcm9qZWN0X2lkInoKGldvcmtDb250ZXh0UmV2aXNpb25TdWJqZWN0Eh4KDHByaW5jaXBhbF9pZBgBIAEoCUIIukgFcgOwAQESPAoGc2NvcGVzGAIgAygLMiIuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFNjb3BlQgi6SAWSAQIQQCKgAgohQ2hlY2tBdXRob3JpemF0aW9uUmV2aXNpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESJAoSb3duZXJfcHJpbmNpcGFsX2lkGAIgASgJQgi6SAVyA7ABARInChZhdXRob3JpemF0aW9uX3JldmlzaW9uGAMgASgEQge6SAQyAiAAEkoKCHN1YmplY3RzGAQgAygLMiwuc2Fhcy5hY2NvdW50cy52MS5Xb3JrQ29udGV4dFJldmlzaW9uU3ViamVjdEIKukgHkgEECAEQERItChRvcGVyYXRpb25fYmluZGluZ19pZBgFIAEoCUIKukgHcgUQARiABEgAiAEBQhcKFV9vcGVyYXRpb25fYmluZGluZ19pZCKDAgocQXV0aG9yaXplRXZpZGVuY2VSZWFkUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiUKE2NhbGxlcl9wcmluY2lwYWxfaWQYAiABKAlCCLpIBXIDsAEBEikKEm93bmVyX3ByaW5jaXBhbF9pZBgDIAEoCUIIukgFcgOwAQFIAIgBARIgCgd0YXNrX2lkGAQgASgJQgq6SAdyBRABGIAESAGIAQESIwoKc2Vzc2lvbl9pZBgFIAEoCUIKukgHcgUQARiABEgCiAEBQhUKE19vd25lcl9wcmluY2lwYWxfaWRCCgoIX3Rhc2tfaWRCDQoLX3Nlc3Npb25faWQilgEKIkNvbnN1bWVTaW5nbGVVc2VXb3JrQ29udGV4dFJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIeCgpjb250ZXh0X2lkGAIgASgJQgq6SAdyBRABGIAEEjYKCmV4cGlyZXNfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQga6SAPIAQEqmwEKF1dvcmtDb250ZXh0UmVwbGF5UG9saWN5EioKJldPUktfQ09OVEVYVF9SRVBMQVlfUE9MSUNZX1VOU1BFQ0lGSUVEEAASKQolV09SS19DT05URVhUX1JFUExBWV9QT0xJQ1lfSURFTVBPVEVOVBABEikKJVdPUktfQ09OVEVYVF9SRVBMQVlfUE9MSUNZX1NJTkdMRV9VU0UQAjL7DAoSV29ya0NvbnRleHRTZXJ2aWNlEoMBChpDaGVja0F1dGhvcml6YXRpb25SZXZpc2lvbhIzLnNhYXMuYWNjb3VudHMudjEuQ2hlY2tBdXRob3JpemF0aW9uUmV2aXNpb25SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESeQoVQXV0aG9yaXplRXZpZGVuY2VSZWFkEi4uc2Fhcy5hY2NvdW50cy52MS5BdXRob3JpemVFdmlkZW5jZVJlYWRSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESegoQQ29uc3VtZVNpbmdsZVVzZRI0LnNhYXMuYWNjb3VudHMudjEuQ29uc3VtZVNpbmdsZVVzZVdvcmtDb250ZXh0UmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEsoBCglTdGFydFRhc2sSLS5zYWFzLmFjY291bnRzLnYxLlN0YXJ0VGFza1dvcmtDb250ZXh0UmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuSXNzdWVkV29ya0NvbnRleHQiacLzGEQIAhADKgwKBm9yZ19pZBACGAEwAToiCh5zYWFzLndvcmtfY29udGV4dC50YXNrX3N0YXJ0ZWQQAkABSAVQA1gEYAF4AoLT5JMCGzoBKiIWL3YxL3dvcmstY29udGV4dHM6dGFzaxKGAQoVU3RhcnRJbnN0YWxsYXRpb25UYXNrEi4uc2Fhcy5hY2NvdW50cy52MS5TdGFydEluc3RhbGxhdGlvblRhc2tSZXF1ZXN0GiMuc2Fhcy5hY2NvdW50cy52MS5Jc3N1ZWRXb3JrQ29udGV4dCIYwvMYFAgDEAEwAToCEAFAAUgHUANYBGABEugBChBTdGFydFJvb3RTZXNzaW9uEjQuc2Fhcy5hY2NvdW50cy52MS5TdGFydFJvb3RTZXNzaW9uV29ya0NvbnRleHRSZXF1ZXN0GiMuc2Fhcy5hY2NvdW50cy52MS5Jc3N1ZWRXb3JrQ29udGV4dCJ5wvMYTAgCEAMqDAoGb3JnX2lkEAIYATABOioKJnNhYXMud29ya19jb250ZXh0LnJvb3Rfc2Vzc2lvbl9zdGFydGVkEAJAAUgFUARYBGABeAKC0+STAiM6ASoiHi92MS93b3JrLWNvbnRleHRzOnJvb3Qtc2Vzc2lvbhLrAQoQRXhjaGFuZ2VBdWRpZW5jZRI0LnNhYXMuYWNjb3VudHMudjEuRXhjaGFuZ2VXb3JrQ29udGV4dEF1ZGllbmNlUmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuSXNzdWVkV29ya0NvbnRleHQifMLzGEoIAhADKgwKBm9yZ19pZBACGAEwATooCiRzYWFzLndvcmtfY29udGV4dC5hdWRpZW5jZV9leGNoYW5nZWQQAkABSAVQBFgEYAF4AoLT5JMCKDoBKiIjL3YxL3dvcmstY29udGV4dHM6ZXhjaGFuZ2UtYXVkaWVuY2US7AEKEVN0YXJ0Q2hpbGRTZXNzaW9uEjUuc2Fhcy5hY2NvdW50cy52MS5TdGFydENoaWxkU2Vzc2lvbldvcmtDb250ZXh0UmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuSXNzdWVkV29ya0NvbnRleHQie8LzGE0IAhADKgwKBm9yZ19pZBACGAEwATorCidzYWFzLndvcmtfY29udGV4dC5jaGlsZF9zZXNzaW9uX3N0YXJ0ZWQQAkABSAVQBFgEYAF4AoLT5JMCJDoBKiIfL3YxL3dvcmstY29udGV4dHM6Y2hpbGQtc2Vzc2lvbhLJAQoQUmVuZXdXb3JrQ29udGV4dBIpLnNhYXMuYWNjb3VudHMudjEuUmVuZXdXb3JrQ29udGV4dFJlcXVlc3QaIy5zYWFzLmFjY291bnRzLnYxLklzc3VlZFdvcmtDb250ZXh0ImXC8xg/CAIQAyoMCgZvcmdfaWQQAhgBMAE6HQoZc2Fhcy53b3JrX2NvbnRleHQucmVuZXdlZBACQAFIBVAEWARgAXgCgtPkkwIcOgEqIhcvdjEvd29yay1jb250ZXh0czpyZW5ld0K5AQoUY29tLnNhYXMuYWNjb3VudHMudjFCEVdvcmtDb250ZXh0c1Byb3RvUAFaLGFjY291bnRzL3BrZy9nZW4vc2Fhcy9hY2NvdW50cy92MTthY2NvdW50c3YxogIDU0FYqgIQU2Fhcy5BY2NvdW50cy5WMcoCEFNhYXNcQWNjb3VudHNcVjHiAhxTYWFzXEFjY291bnRzXFYxXEdQQk1ldGFkYXRh6gISU2Fhczo6QWNjb3VudHM6OlYxYgZwcm90bzM", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_policy_v1_options]); /** * WorkContextScope is one product-neutral authority slice. Product plugins @@ -49,6 +49,13 @@ export const WorkContextScopeSchema: GenMessage = /*@__PURE__* messageDesc(file_saas_accounts_v1_work_contexts, 0); /** + * StartTaskWorkContextRequest mints a Task capability for the caller. Its Session + * is the one the caller authenticated with — or a fresh one when it has none — + * assigned by the issuer and returned in IssuedWorkContext, never named by the + * caller: a session a caller could name is one it could name after another + * credential's, and a confined credential is paired with a capability on the + * session alone. + * * @generated from message saas.accounts.v1.StartTaskWorkContextRequest */ export type StartTaskWorkContextRequest = Message<"saas.accounts.v1.StartTaskWorkContextRequest"> & { @@ -74,11 +81,6 @@ export type StartTaskWorkContextRequest = Message<"saas.accounts.v1.StartTaskWor */ taskId: string; - /** - * @generated from field: string session_id = 3; - */ - sessionId: string; - /** * Empty means a direct human-owned Task. Otherwise this must name an active, * registered agent Principal in org_id. diff --git a/module/services/policy-log/code/go.mod b/module/services/policy-log/code/go.mod index 933078ac3..3d616a56a 100644 --- a/module/services/policy-log/code/go.mod +++ b/module/services/policy-log/code/go.mod @@ -31,7 +31,7 @@ require ( github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect - github.com/codefly-dev/core v0.19.2 // indirect + github.com/codefly-dev/core v0.19.3 // indirect github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect github.com/felixge/httpsnoop v1.1.0 // indirect diff --git a/module/services/policy-log/code/go.sum b/module/services/policy-log/code/go.sum index 8e77fcad5..2661f1bc2 100644 --- a/module/services/policy-log/code/go.sum +++ b/module/services/policy-log/code/go.sum @@ -58,8 +58,8 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= -github.com/codefly-dev/core v0.19.2 h1:eoto82yKQPxLvtn5ie0hk7MYtTGTvikjkQjcaUuQtso= -github.com/codefly-dev/core v0.19.2/go.mod h1:4ovwm7kaHFtY9+TT8yfjA4D/BiAhIFZez7lcil8muVo= +github.com/codefly-dev/core v0.19.3 h1:nWCK+wBtwYO91oyZ4G+jNgoVGcgVCr73wz2taLLiFgM= +github.com/codefly-dev/core v0.19.3/go.mod h1:4ovwm7kaHFtY9+TT8yfjA4D/BiAhIFZez7lcil8muVo= github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ= github.com/codefly-dev/gortk v0.2.0/go.mod h1:dDWUMFgAP063OCGhTEpV7FFUj9+zTcxxO/nV80VKEwg= github.com/codefly-dev/sdk-go v0.4.0 h1:3Jm0bP+UsLI4iZYp3wqVa42bcXBumMIIF/aeweKOAhY= diff --git a/module/services/store/code/go.mod b/module/services/store/code/go.mod index 99a470226..19fcd29c1 100644 --- a/module/services/store/code/go.mod +++ b/module/services/store/code/go.mod @@ -16,7 +16,7 @@ require ( github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/semver/v3 v3.5.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect - github.com/codefly-dev/core v0.19.2 // indirect + github.com/codefly-dev/core v0.19.3 // indirect github.com/go-openapi/analysis v1.0.0 // indirect github.com/go-openapi/errors v0.22.9 // indirect github.com/go-openapi/jsonpointer v1.0.1 // indirect diff --git a/module/services/store/code/go.sum b/module/services/store/code/go.sum index 3de832dd7..a462d9c1a 100644 --- a/module/services/store/code/go.sum +++ b/module/services/store/code/go.sum @@ -30,8 +30,8 @@ github.com/cheggaaa/pb/v3 v3.2.1 h1:aprZbFRG+B7+ug76S8QZ6Y1PW168UHzOmbC3wa+aU6I= github.com/cheggaaa/pb/v3 v3.2.1/go.mod h1:U9hSVxoKqJrZIE3PkFG1xXXNaK/Ilzg+EF/scpXzEdw= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -github.com/codefly-dev/core v0.19.2 h1:eoto82yKQPxLvtn5ie0hk7MYtTGTvikjkQjcaUuQtso= -github.com/codefly-dev/core v0.19.2/go.mod h1:4ovwm7kaHFtY9+TT8yfjA4D/BiAhIFZez7lcil8muVo= +github.com/codefly-dev/core v0.19.3 h1:nWCK+wBtwYO91oyZ4G+jNgoVGcgVCr73wz2taLLiFgM= +github.com/codefly-dev/core v0.19.3/go.mod h1:4ovwm7kaHFtY9+TT8yfjA4D/BiAhIFZez7lcil8muVo= github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ= github.com/codefly-dev/gortk v0.2.0/go.mod h1:dDWUMFgAP063OCGhTEpV7FFUj9+zTcxxO/nV80VKEwg= github.com/codefly-dev/sdk-go v0.4.0 h1:3Jm0bP+UsLI4iZYp3wqVa42bcXBumMIIF/aeweKOAhY= diff --git a/module/services/telemetry/code/go.mod b/module/services/telemetry/code/go.mod index d07625205..116b07795 100644 --- a/module/services/telemetry/code/go.mod +++ b/module/services/telemetry/code/go.mod @@ -18,7 +18,7 @@ require ( github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/semver/v3 v3.5.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect - github.com/codefly-dev/core v0.19.2 // indirect + github.com/codefly-dev/core v0.19.3 // indirect github.com/go-openapi/analysis v1.0.0 // indirect github.com/go-openapi/errors v0.22.9 // indirect github.com/go-openapi/jsonpointer v1.0.1 // indirect diff --git a/module/services/telemetry/code/go.sum b/module/services/telemetry/code/go.sum index e36cb360b..e6b4f3ca9 100644 --- a/module/services/telemetry/code/go.sum +++ b/module/services/telemetry/code/go.sum @@ -28,8 +28,8 @@ github.com/cheggaaa/pb/v3 v3.2.1 h1:aprZbFRG+B7+ug76S8QZ6Y1PW168UHzOmbC3wa+aU6I= github.com/cheggaaa/pb/v3 v3.2.1/go.mod h1:U9hSVxoKqJrZIE3PkFG1xXXNaK/Ilzg+EF/scpXzEdw= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -github.com/codefly-dev/core v0.19.2 h1:eoto82yKQPxLvtn5ie0hk7MYtTGTvikjkQjcaUuQtso= -github.com/codefly-dev/core v0.19.2/go.mod h1:4ovwm7kaHFtY9+TT8yfjA4D/BiAhIFZez7lcil8muVo= +github.com/codefly-dev/core v0.19.3 h1:nWCK+wBtwYO91oyZ4G+jNgoVGcgVCr73wz2taLLiFgM= +github.com/codefly-dev/core v0.19.3/go.mod h1:4ovwm7kaHFtY9+TT8yfjA4D/BiAhIFZez7lcil8muVo= github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ= github.com/codefly-dev/gortk v0.2.0/go.mod h1:dDWUMFgAP063OCGhTEpV7FFUj9+zTcxxO/nV80VKEwg= github.com/codefly-dev/sdk-go v0.4.0 h1:3Jm0bP+UsLI4iZYp3wqVa42bcXBumMIIF/aeweKOAhY= diff --git a/qualification/module-authority/module_read_flow_test.go b/qualification/module-authority/module_read_flow_test.go index 510958ede..7d6a3082a 100644 --- a/qualification/module-authority/module_read_flow_test.go +++ b/qualification/module-authority/module_read_flow_test.go @@ -39,11 +39,10 @@ func TestTheModuleReadThroughTheRealGatewayIsMintedByTheRealStartTask(t *testing } // 2. The mint, with the same credential, through the gateway to StartTask. - // The request names a session of its own; what is signed is the one the - // credential authenticated, which is the one the gateway pairs on. + // The request names no session: what is signed is the one the credential + // authenticated, which is the one the gateway pairs on. mint := gatewayCall(t, h, http.MethodPost, "/saas.accounts.v1.WorkContextService/StartTask", map[string]any{ "orgId": flow.Org, - "sessionId": "019f6bf7-0000-7000-8000-000000000000", "taskId": "019f6bf7-1111-7111-8111-1111111111aa", "audience": flow.Module, "ttlSeconds": 300, @@ -95,7 +94,7 @@ func TestTheModuleReadThroughTheRealGatewayIsMintedByTheRealStartTask(t *testing "outside the declared binding": {flow.Module, flow.Module, []string{"read", "write"}, "outside what the consumer this credential was delivered to declares"}, } { refused := gatewayCall(t, h, http.MethodPost, "/saas.accounts.v1.WorkContextService/StartTask", map[string]any{ - "orgId": flow.Org, "sessionId": "019f6bf7-0000-7000-8000-000000000000", + "orgId": flow.Org, "taskId": "019f6bf7-1111-7111-8111-1111111111ab", "audience": ask.audience, "ttlSeconds": 300, "authorityScopes": []map[string]any{{"resourceKind": ask.kind, "actions": ask.actions}}, }, nil)