From bc92f95e5f12538ef85a7c67561c199e95267f91 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 11 Oct 2026 19:48:53 +0200 Subject: [PATCH] Prepare closed service task execution input and policy contract Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade --- ...uman-nanosecond-fixture-compatibility.json | 20 +++ docs/service-task-execution-contract.md | 141 ++++++++++++++++++ .../policy_fixtures.yaml | 8 + .../policy_package.md | 118 +++++++++++++++ .../resource_manifest.yaml | 6 + .../policy/service_task_execution_test.go | 69 +++++++++ internal/policy/task_execution_test.go | 85 ++++------- internal/taskexecution/service_input.go | 101 +++++++++++++ internal/taskexecution/service_input_test.go | 81 ++++++++++ tools/service_task_execution_fixture.py | 13 ++ tools/task_execution_interop_fixture.py | 22 ++- workplans/ADHOC-2026-10-11.md | 32 ++++ 12 files changed, 639 insertions(+), 57 deletions(-) create mode 100644 docs/evidence/2026-10-11-task-execution/human-nanosecond-fixture-compatibility.json create mode 100644 docs/service-task-execution-contract.md create mode 100644 examples/secrets-engine-service-task-execution/policy_fixtures.yaml create mode 100644 examples/secrets-engine-service-task-execution/policy_package.md create mode 100644 examples/secrets-engine-service-task-execution/resource_manifest.yaml create mode 100644 internal/policy/service_task_execution_test.go create mode 100644 internal/taskexecution/service_input.go create mode 100644 internal/taskexecution/service_input_test.go create mode 100644 tools/service_task_execution_fixture.py diff --git a/docs/evidence/2026-10-11-task-execution/human-nanosecond-fixture-compatibility.json b/docs/evidence/2026-10-11-task-execution/human-nanosecond-fixture-compatibility.json new file mode 100644 index 0000000..3790ed4 --- /dev/null +++ b/docs/evidence/2026-10-11-task-execution/human-nanosecond-fixture-compatibility.json @@ -0,0 +1,20 @@ +{ + "schema": "source-fixture-compatibility/v1", + "previous_service_manifest_sha256": "98142d4b03d13eccde813483c88ada7d5412c397334e086df6ba64ee89f79d86", + "original_human_generator_sha256": "3015561cd4b1120177605a23f874220bb34a90548ea0fee5a158f3d16a9b5743", + "original_human_policy_sha256": "a790ecfbc8906c5a30e3eb066f391330d73d19b8cd46814d1d9778f24774d9e7", + "original_shape": { + "field": "task_execution_projection.not_after_ns", + "representation": "int(epoch_seconds)*1000000000", + "exact_profile": "unsupported Python integer magnitude > 2^53-1" + }, + "resolution": "Preserve original projection as explicitly refused counterexample; descriptor-only canonical generator, unchanged closed human policy. No schema relabeling/rounding/RFC3339 identity rewrite.", + "source_tests_pass_events": 133, + "source_tests_failures": 0, + "source_test_command": "SECRETS_ENGINE_SOURCE=/tmp/access-secrets-canonical-review/source GOCACHE=/tmp/access-taskexecution-go-cache go test -json ./internal/taskexecution ./internal/policy ./pkg/api -count=1", + "identity_contract_changed": false, + "human_policy_changed": false, + "policy_constant_flipped": false, + "native_execution": false, + "service_scope": "structural characterization and TokenReview input preparation; no exact approval/PDP/consume/journal/grant join or operational readiness" +} diff --git a/docs/service-task-execution-contract.md b/docs/service-task-execution-contract.md new file mode 100644 index 0000000..2adbccd --- /dev/null +++ b/docs/service-task-execution-contract.md @@ -0,0 +1,141 @@ +# Proposed faithful Secrets service task execution successor + +Source component for existing SECRETS-WP-0013 and RPF-WP-0035-T12. +Based on accepted-intent technical assessment in +/tmp/execute-ccr-accepted-intent-assessment.json. This is neither foreign owner +acceptance nor a new doctrine choice, live caller mapping or native permission. + +## Principal and scope + +The initiating/represented principal for this first pilot is the admitted Secrets +service itself. It does not claim to represent a named human. The operation still +declares human-control approval; the authenticated human approver is a separate +principal and cannot supply service caller authentication. Accepted v0.8/GH013/ +GH016 do not impose universal human initiation/User/Tenant membership on every +metadata action. Operations deliberately representing a human or relying on human +membership need their own verified initiating/delegation/provider contract. + +The prior proposed human package secrets-engine.task-execution v1 and its literal +false gate remain byte-for-byte unchanged. This separate proposed +secrets-engine.task-execution.service-pilot v1 package is not a toggle of that +variant, not legacy lifecycle v2 apply and not whole successor-standard acceptance. +Its literal runtime_admission=false keeps every runtime policy evaluation closed. +Candidate-predicate characterization never flips that constant or grants an act. + +## Concrete authenticated source input component + +internal/taskexecution.ServicePilotInputs uses actual existing callerauth +Authenticator.Authorize/TokenReview machinery, fixed enforce mode, flex-auth +audience and the explicitly proposed mapping: + +system:serviceaccount:secrets-engine:secrets-engine -> secrets-engine/type service, +protected system secrets-engine, tenant:platform. + +The transport/system association is present in current values/secrets-engine.yaml; +the explicit transport-to-subject association here is new proposed source and still +needs owner/runtime admission. A2 system mapping alone does not prove arbitrary +request subjects. The source constructor accepts only an owner-installed reviewer, +never a request-selected principal or warn/disabled mode. The unexported ServiceInput +records actual successful review, fixed matching subject/type and nonzero future +transport expiry; zero/default input refuses. Its detached request snapshot never +contains a bearer token or an unsigned authenticated flag. + +service_actor_binding is PUBLIC DECLARED metadata included before approval issue: +version proposed-service-task-caller/v1, exact principal/subject/type/tenant. It is +not authentication evidence. Prepare compares it to the independently authenticated +caller and source mapping. Incorrect/missing/extra binding fields refuse. Claimed +human/delegation/projection/verified/owner_admission fields refuse rather than being +interpreted as authority. Deadline must be live and no later than actual reviewed +transport expiry. The returned input is eligibility preparation, not a PDP allow, +trusted served approval, consume confirmation or operational entitlement. + +Maker tests use a synthetic TokenReviewer, not actual Kubernetes authentication. +Actual deployed TokenReview origin/CA/RBAC/audience/projection rotation and mapping +admission must be proved before runtime use. The adapter is not wired into HTTP +routes, decision engine evaluation or a native executor; source package remains +hard-closed. A forged public service_actor_binding alone remains insufficient. + +## Exact operation and independent gates + +Owner compiler derives exact metadata CCR/catalogue/source/body/target plan. Whole +SECRETS-EXEC-20261011-v1 descriptor binds unique operation UUID, CCR/grant immutable +hashes, consumer thirteen-field local catalogue fingerprint, independently pinned +source roles, estate/origin, expected before/after states, six GET/PUT/GET reader +ACL then GET/PUT/GET role operations, bounded deadline and 120s/12-use ceilings. +Platform rederives exact body bytes before each call. This source package models +shape, service declaration, production tenant/stage and human-control claim shape; +it neither authenticates sources nor compiles full baseline/grant/estate/deadline. + +Required sequence remains: + +1. Authenticate the admitted service caller and explicitly bind its subject; obtain + exact owner-derived complete request before approval issue. Local/public discovery + only precedes consume. No caller projection or founder bootstrap substitutes. +2. Obtain live served approval from independently authenticated Approval Engine + origin; require declared human_control with verified human approver under GH016, + fresh validity and canonical PDP path. Approval is a fact, never a decision. +3. Access renders fresh exact package/version result over authenticated caller, + owner operation and served facts; decision binds full enriched request, exact + submitted request, and approval-free request. The claim's recorded pdp_digest + must equal served binding.approval_binding_digest. Use repaired Go-compatible + canonical serialization; do not translate Approval native digest or rewrite old + claims. Server must make trusted input provenance reconstructable and prevent + caller substitution. This source candidate does not install that server join. +4. PEP obtains authenticated successful consume for exactly approval/request/decision + before any protected consumer login/effect. Platform independently confirms original + consumption using trusted provider transport; supplied booleans/receipts and + same-digest CAS retry are not a fresh execution grant. +5. Owner-private durable journal reserves the same operation once, maintains exclusive + admission and pending intent before each exact phase/call, and refuses replay or + unknown recovery. Whole canonical request, caller mapping, body/plan and source + correspondence remain unchanged across reserve/consume/applying/native phases. +6. Independently authenticate CCR38/OpenBao service provenance after consume. Owner + separately admits task-execution-grant CCR, exact disposable target/estate/route + and bounded owner-held ephemeral token. Token is never exported to the consumer; + native ACL alone cannot constrain policy/role bodies. Mediator enforces exact body + equality, real expiry/use budget, native readback, revoke and temporary cleanup. +7. Lost/uncertain response, crash, incomplete revoke or cleanup records unknown and + does not auto-retry. Owner reconciliation plus fresh exact authorization governs + another action. Actual positive/negative/interruption proofs and owner acceptance + are required before pilot completion, then separately reviewed lifecycle adoption. + +The pure candidate predicate's syntactic pdp_digest/freshness checks do not validate +live provider provenance, exact digest equality, human approver identity or consume. +They are source shape characterization only. Literal runtime_admission=false enforces +that absence. Authentication plus approval alone cannot enable it. Future source +successor must integrate all applicable gates, not expose an enable flag. + +## Verification and limits + +Maker source tests verify supported synthetic authenticated-service input binding, +wrong transport/subject/type/audience/tenant, missing/expired transport, deadline, +forged binding/delegation/flags and detached snapshot/zero-input refusal. Actual +Secrets ExecutionContract parser generates complete six-call synthetic input; +OPA candidate predicate is queried directly for eligible/negative shapes while +normal policy evaluation remains deny. Old human gate is unchanged. Source +fixtures register synthetic attributes solely for package-validation checks, +not an actual live catalogue or estate registry. No runtime image, credential, +provider admission, API change or native effect occurs. + + +## Exact component omissions (not completion evidence) + +This source candidate is STRUCTURAL CHARACTERIZATION and authenticated-input +preparation only. Its predicate checks a syntactically tagged pdp_digest, not its +actual equality to enriched approval_binding_digest. It does not prove served +claim origin or verified approver identity, reconstruct canonical native claim +binding, validate full submitted/enriched DecisionEnvelope or consumption triplet, +bind actual compiler bytes to the whole descriptor, enforce journal phase/order, +admit grant/baseline/estate or perform an effect. Those are explicit implementation +requirements; a matching predicate and reviewed TokenReview input are insufficient. +The service adapter carries authenticated transport evidence outside the canonical +request but is not connected to HTTP evaluation/provider validation. A caller's +service_actor_binding remains a public declaration independently compared by the +adapter, never a substitute for runtime provenance or exact operation authority. + +The old human fixture's epoch nanosecond integer exceeds the repaired consumer's +safe integer profile. Compatibility repair preserves that original projection as +an explicitly refused counterexample and tests descriptor-only canonical parity. +The old human package/identity schema are unchanged; no nanoseconds are rounded or +relabeled, no RFC3339 projection contract is invented, and no constant is flipped. +Any future human identity shape needs separate owner-reviewed source integration. diff --git a/examples/secrets-engine-service-task-execution/policy_fixtures.yaml b/examples/secrets-engine-service-task-execution/policy_fixtures.yaml new file mode 100644 index 0000000..c63a41f --- /dev/null +++ b/examples/secrets-engine-service-task-execution/policy_fixtures.yaml @@ -0,0 +1,8 @@ +- id: unsigned-service-caller-not-authority + request: + tenant: tenant:platform + subject: {id: secrets-engine, type: service} + action: execute-ccr + resource: {id: catalog:test, type: secret-catalog-lane, system: secrets-engine} + context: {human_control: true, runtime_admission: true, verified: true} + expect: {effect: deny, reason: service_execution_integration_unadmitted} diff --git a/examples/secrets-engine-service-task-execution/policy_package.md b/examples/secrets-engine-service-task-execution/policy_package.md new file mode 100644 index 0000000..cdb42de --- /dev/null +++ b/examples/secrets-engine-service-task-execution/policy_package.md @@ -0,0 +1,118 @@ +--- +id: secrets-engine.task-execution.service-pilot +name: Secrets admitted service task execution +namespace: secrets-engine:secret-catalog-lane +version: v1 +status: proposed +package: flexauth.secrets_engine.service_task_execution +allow_ttl: 15s +actions: [execute-ccr] +owner: team:platform-security +fixtures: [policy_fixtures.yaml] +caring: + profile: caring-0.4.0-rc2 + enforce: false +activation: + mode: local +--- + +# Proposed faithful service pilot successor + +A separate source package for an admitted Secrets service as the initiating and +represented principal. It imposes no universal human initiator/User/Tenant gate. +It still requires the operation's declared human-control approval. The human +approver is not the service caller and does not supply service authentication. + +Current runtime_admission is literal false because authenticated server mapping, +served claim validation, exact PDP joins and Platform executor integration are not +installed. Request flags cannot enable it. candidate_matches is source contract +characterization, not authority; positive predicate tests never flip the constant. +Raw caller metadata is not a verified identity. See docs/service-task-execution-contract.md. + +```rego +runtime_admission := false + +default decision := {"effect": "deny", "reason": "service_execution_integration_unadmitted"} + +decision := {"effect": "allow", "reason": "exact_task_execution_matched"} { + runtime_admission + candidate_matches +} + +hash_ok(x) { is_string(x); regex.match("^sha256:[0-9a-f]{64}$", x) } + +candidate_matches { + input.action == "execute-ccr" + input.tenant == "tenant:platform" + input.subject.id == "secrets-engine" + input.subject.type == "service" + input.resource.system == "secrets-engine" + input.resource.type == "secret-catalog-lane" + input.context.human_control == true + c := input.context.execution_contract + c.contract_version == "SECRETS-EXEC-20261011-v1" + c.operation == "metadata-apply-ccr" + c.execution_actor == "service:secrets-engine" + regex.match("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", c.operation_id) + c.ccr_id != c.execution_grant_ccr_id + hash_ok(c.ccr_immutable_sha256) + hash_ok(c.grant_immutable_sha256) + hash_ok(c.catalog_sha256) + hash_ok(c.expected_before) + hash_ok(c.expected_after) + c.backend_budget.ttl_seconds <= 120 + c.backend_budget.ttl_seconds >= 30 + c.backend_budget.uses == 12 + count(c.operations) == 6 + c.operations[0].method == "GET" + c.operations[1].method == "PUT" + c.operations[2].method == "GET" + c.operations[3].method == "GET" + c.operations[4].method == "PUT" + c.operations[5].method == "GET" + regex.match("^sys/policies/acl/[A-Za-z0-9_-]+$", c.operations[0].path) + regex.match("^auth/[A-Za-z0-9_-]+/role/[A-Za-z0-9_-]+$", c.operations[3].path) + c.operations[0].path == c.operations[1].path + c.operations[1].path == c.operations[2].path + c.operations[3].path == c.operations[4].path + c.operations[4].path == c.operations[5].path + every_operation_hash_valid + b := input.context.service_actor_binding + count(b) == 5 + b.version == "proposed-service-task-caller/v1" + b.transport_principal == "system:serviceaccount:secrets-engine:secrets-engine" + b.subject_id == input.subject.id + b.subject_type == "service" + b.tenant == input.tenant + not input.context.task_execution_projection + not input.context.initiator + not input.context.represented_principal + not input.context.delegation + input.resource.attributes.stage == "prod" + time.parse_rfc3339_ns(c.operation_deadline) > time.now_ns() + input.context.approval.schema_version == "0.1" + input.context.approval.kind == "approval-claim" + input.context.approval.issuer == "approval-engine" + input.context.approval.valid_now == true + input.context.approval.consumed == false + input.context.approval.reason_code == "ok" + input.context.approval.binding.pdp_path == true + hash_ok(input.context.approval.binding.pdp_digest) + time.parse_rfc3339_ns(input.context.approval.freshness.not_after) > time.now_ns() + time.parse_rfc3339_ns(input.context.approval.validity.expires_at) >= time.parse_rfc3339_ns(c.operation_deadline) + input.context.approval.binding.human_control == true +} + +every_operation_hash_valid { + count([x | x := input.context.execution_contract.operations[_]; hash_ok(x.payload_sha256)]) == 6 +} +``` + +```rego test +import data.flexauth.secrets_engine.service_task_execution + +test_unsigned_flags_cannot_enable { + service_task_execution.decision.effect == "deny" + with input as {"context": {"runtime_admission": true, "verified": true}} +} +``` diff --git a/examples/secrets-engine-service-task-execution/resource_manifest.yaml b/examples/secrets-engine-service-task-execution/resource_manifest.yaml new file mode 100644 index 0000000..68ee07d --- /dev/null +++ b/examples/secrets-engine-service-task-execution/resource_manifest.yaml @@ -0,0 +1,6 @@ +# Synthetic source-validation fixture only; no estate registry admission. +resources: + - id: catalog:synthetic + system: secrets-engine + type: secret-catalog-lane + attributes: {stage: prod} diff --git a/internal/policy/service_task_execution_test.go b/internal/policy/service_task_execution_test.go new file mode 100644 index 0000000..b805a3b --- /dev/null +++ b/internal/policy/service_task_execution_test.go @@ -0,0 +1,69 @@ +package policy_test + +import ( + "context" + "encoding/json" + "github.com/netkingdom/access-engine/internal/policy" + "github.com/netkingdom/access-engine/pkg/api" + "github.com/open-policy-agent/opa/rego" + "os/exec" + "path/filepath" + "testing" +) + +func TestServiceTaskExecutionSourcePredicateAndHardRefusal(t *testing.T) { + p, e := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md")) + if e != nil || !p.Valid { + t.Fatalf("package %v %+v", e, p.Validation) + } + out, e := exec.Command("/home/worsch/secrets-engine/.venv/bin/python", "../../tools/service_task_execution_fixture.py").Output() + if e != nil { + t.Fatal("actual Secrets descriptor generator", e) + } + var f struct { + Request api.CheckRequest `json:"request"` + } + if json.Unmarshal(out, &f) != nil { + t.Fatal("fixture") + } + candidate := func(r api.CheckRequest) bool { + b, _ := json.Marshal(r) + var input map[string]any + json.Unmarshal(b, &input) + result, e := rego.New(rego.Query("data.flexauth.secrets_engine.service_task_execution.candidate_matches"), rego.Module("candidate.rego", p.RegoModule), rego.Input(input)).Eval(context.Background()) + if e != nil { + t.Fatal(e) + } + return len(result) > 0 + } + if !candidate(f.Request) { + t.Fatal("supported synthetic service contract failed predicate") + } + d, e := p.Evaluate(context.Background(), f.Request) + if e != nil || d.Effect != api.DecisionEffectDeny || d.Reason != "service_execution_integration_unadmitted" { + t.Fatal("unintegrated source package opened") + } + cases := map[string]func(*api.CheckRequest){"human": func(r *api.CheckRequest) { r.Subject.Type = "human" }, "foreigntenant": func(r *api.CheckRequest) { r.Tenant = "tenant:coulomb" }, "apply": func(r *api.CheckRequest) { r.Action = "apply" }, "servicecaller": func(r *api.CheckRequest) { + r.Context["service_actor_binding"].(map[string]any)["transport_principal"] = "another" + }, "missing human control": func(r *api.CheckRequest) { + r.Context["approval"].(map[string]any)["binding"].(map[string]any)["human_control"] = false + }, "consumed": func(r *api.CheckRequest) { r.Context["approval"].(map[string]any)["consumed"] = true }, "PDP digest": func(r *api.CheckRequest) { + r.Context["approval"].(map[string]any)["binding"].(map[string]any)["pdp_digest"] = "" + }, "expired observation": func(r *api.CheckRequest) { + r.Context["approval"].(map[string]any)["freshness"].(map[string]any)["not_after"] = "2000-01-01T00:00:00Z" + }, "wrong operation": func(r *api.CheckRequest) { + r.Context["execution_contract"].(map[string]any)["operation"] = "generic-apply" + }} + for name, mutate := range cases { + t.Run(name, func(t *testing.T) { + var f struct { + Request api.CheckRequest `json:"request"` + } + json.Unmarshal(out, &f) + mutate(&f.Request) + if candidate(f.Request) { + t.Fatal("unsupported source contract matched") + } + }) + } +} diff --git a/internal/policy/task_execution_test.go b/internal/policy/task_execution_test.go index c43e111..9bc7c40 100644 --- a/internal/policy/task_execution_test.go +++ b/internal/policy/task_execution_test.go @@ -35,70 +35,47 @@ func TestTaskExecutionSourceHasNoActivationSwitch(t *testing.T) { } } -func TestTaskExecutionPythonDescriptorAndSyntheticFutureProjection(t *testing.T) { +// The prior human projection contains epoch nanoseconds outside the exact +// cross-runtime integer profile. Preserve/refuse it; no identity schema change. +func TestTaskExecutionDescriptorOnlyAndRefusedHumanProjection(t *testing.T) { output, e := exec.Command("/home/worsch/secrets-engine/.venv/bin/python", "../../tools/task_execution_interop_fixture.py").Output() if e != nil { - t.Fatal("Python actual descriptor/canonical generator failed:", e) + t.Fatal("actual descriptor/canonical generator failed:", e) } - var fixture struct { - Request api.CheckRequest `json:"request"` - RequestDigest string `json:"request_digest"` - ApprovalDigest string `json:"approval_binding_digest"` - DescriptorDigest string `json:"descriptor_digest"` + var f struct { + Request api.CheckRequest `json:"request"` + RequestDigest string `json:"request_digest"` + ApprovalDigest string `json:"approval_binding_digest"` + DescriptorDigest string `json:"descriptor_digest"` + OriginalProjection map[string]any `json:"original_nanosecond_projection"` + Refused bool `json:"original_projection_refused"` + IdentityChanged bool `json:"identity_contract_changed"` } - if json.Unmarshal(output, &fixture) != nil { + if json.Unmarshal(output, &f) != nil { t.Fatal("fixture JSON") } - r := fixture.Request - if api.RequestDigest(r) != fixture.RequestDigest || api.ApprovalBindingDigest(r) != fixture.ApprovalDigest || api.CanonicalDigest(r.Context["execution_contract"]) != fixture.DescriptorDigest { - t.Fatal("actual Python/Go canonical mismatch") + if !f.Refused || f.IdentityChanged || f.OriginalProjection["not_after_ns"].(float64) <= float64(1<<53) { + t.Fatal("unsupported original nanosecond projection was not explicitly preserved/refused") } - raw, e := os.ReadFile(filepath.Join("..", "..", "examples", "secrets-engine-task-execution", "policy_package.md")) - if e != nil { - t.Fatal(e) + if _, found := f.Request.Context["task_execution_projection"]; found { + t.Fatal("unsupported projection entered descriptor-only request") } - // This source replacement is intentionally test-only; no caller/config flag - // can change the production package's hard admission=false constant. - raw = []byte(strings.Replace(string(raw), "owner_admission := false", "owner_admission := true", 1)) - p, e := policy.Load(raw, "synthetic-future-provider-test.md") - if e != nil { - t.Fatal(e) + if api.RequestDigest(f.Request) != f.RequestDigest || api.ApprovalBindingDigest(f.Request) != f.ApprovalDigest || api.CanonicalDigest(f.Request.Context["execution_contract"]) != f.DescriptorDigest { + t.Fatal("actual descriptor-only Python/Go mismatch") } - d, e := p.Evaluate(context.Background(), r) - if e != nil || d.Effect != api.DecisionEffectAllow { - t.Fatalf("synthetic future projection did not match: %v %+v", e, d) + p, e := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-task-execution", "policy_package.md")) + if e != nil || !p.Valid { + t.Fatal("unchanged human source package") } - cases := map[string]func(*api.CheckRequest){ - "action": func(r *api.CheckRequest) { r.Action = "apply" }, "service subject": func(r *api.CheckRequest) { r.Subject.ID = "other" }, "foreign tenant": func(r *api.CheckRequest) { r.Tenant = "tenant:coulomb" }, - "plan hash": func(r *api.CheckRequest) { - r.Context["execution_contract"].(map[string]any)["expected_after"] = "sha256:" + strings.Repeat("b", 64) - }, - "unsigned human": func(r *api.CheckRequest) { delete(r.Context, "task_execution_projection") }, - "registered tenant": func(r *api.CheckRequest) { - r.Context["task_execution_projection"].(map[string]any)["tenant_source"] = "registration" - }, - "membership": func(r *api.CheckRequest) { - r.Context["task_execution_projection"].(map[string]any)["member_active"] = false - }, - "transport": func(r *api.CheckRequest) { - r.Context["task_execution_projection"].(map[string]any)["transport_principal"] = "other" - }, - "expiry": func(r *api.CheckRequest) { - r.Context["task_execution_projection"].(map[string]any)["not_after_ns"] = float64(1) - }, - "no human control": func(r *api.CheckRequest) { r.Context["human_control"] = false }, + d, e := p.Evaluate(context.Background(), f.Request) + if e != nil || d.Effect != api.DecisionEffectDeny || d.Reason != "initiating_provider_unadmitted" { + t.Fatal("closed human package was enabled") } - for name, mutate := range cases { - t.Run(name, func(t *testing.T) { - var f struct { - Request api.CheckRequest `json:"request"` - } - json.Unmarshal(output, &f) - mutate(&f.Request) - d, e := p.Evaluate(context.Background(), f.Request) - if e != nil || d.Effect != api.DecisionEffectDeny { - t.Fatalf("mutation allowed: %v %+v", e, d) - } - }) + // No literal is flipped. Restoring the unsupported caller projection does + // not turn the unchanged package into a grant either. + f.Request.Context["task_execution_projection"] = f.OriginalProjection + d, e = p.Evaluate(context.Background(), f.Request) + if e != nil || d.Effect != api.DecisionEffectDeny { + t.Fatal("old caller projection opened package") } } diff --git a/internal/taskexecution/service_input.go b/internal/taskexecution/service_input.go new file mode 100644 index 0000000..363e3e7 --- /dev/null +++ b/internal/taskexecution/service_input.go @@ -0,0 +1,101 @@ +package taskexecution + +import ( + "context" + "encoding/json" + "errors" + "github.com/netkingdom/access-engine/internal/callerauth" + "github.com/netkingdom/access-engine/pkg/api" + "time" +) + +var ErrServiceInput = errors.New("service task execution input refused") + +const ServicePilotMappingVersion = "proposed-service-task-caller/v1" +const ServicePilotPrincipal = "system:serviceaccount:secrets-engine:secrets-engine" + +// ServicePilotInputs is source preparation only. It installs the explicit +// proposed fixed subject-to-transport mapping, never a request-selected mapping. +// No constructor parameter can select warn/disabled mode or another subject. +type ServicePilotInputs struct{ auth *callerauth.Authenticator } + +func NewServicePilotInputs(reviewer callerauth.TokenReviewer) (*ServicePilotInputs, error) { + a, e := callerauth.New(callerauth.ModeEnforce, reviewer, "flex-auth", map[string]string{"secrets-engine": ServicePilotPrincipal}, nil) + if e != nil { + return nil, ErrServiceInput + } + return &ServicePilotInputs{auth: a}, nil +} + +// ServiceInput cannot be fabricated through exported fields. It proves only +// source input preparation through TokenReview, not approval provenance/consume +// or execution authority. Default zero value is not an authenticated input. +type ServiceInput struct { + serialized []byte + caller callerauth.Record +} + +func (s ServiceInput) Request() (api.CheckRequest, error) { + if s.caller.Mode != callerauth.ModeEnforce || s.caller.Principal != ServicePilotPrincipal || len(s.serialized) == 0 { + return api.CheckRequest{}, ErrServiceInput + } + var r api.CheckRequest + if json.Unmarshal(s.serialized, &r) != nil { + return r, ErrServiceInput + } + return r, nil +} +func (s ServiceInput) Caller() callerauth.Record { return s.caller } + +// DeclaredServiceBinding is public immutable metadata to bind at approval issue. +// It is not authentication evidence; Prepare independently verifies its principal. +func DeclaredServiceBinding() map[string]any { + return map[string]any{"version": ServicePilotMappingVersion, "transport_principal": ServicePilotPrincipal, "subject_id": "secrets-engine", "subject_type": "service", "tenant": "tenant:platform"} +} +func (s *ServicePilotInputs) Prepare(ctx context.Context, authorization string, r api.CheckRequest, now time.Time) (ServiceInput, error) { + refuse := func() (ServiceInput, error) { return ServiceInput{}, ErrServiceInput } + if s == nil || s.auth == nil || now.IsZero() || r.Action != "execute-ccr" || r.Tenant != "tenant:platform" || r.Subject.ID != "secrets-engine" || r.Subject.Type != "service" || r.Resource.System != "secrets-engine" || r.Resource.Type != "secret-catalog-lane" || r.Resource.Attributes["stage"] != "prod" || r.Context["human_control"] != true { + return refuse() + } + for _, key := range []string{"task_execution_projection", "initiator", "represented_principal", "delegation", "verified", "owner_admission"} { + if _, found := r.Context[key]; found { + return refuse() + } + } + if _, found := r.Subject.Attributes["principal"]; found { + return refuse() + } + supplied, err := json.Marshal(r.Context["service_actor_binding"]) + if err != nil { + return refuse() + } + wanted, _ := json.Marshal(DeclaredServiceBinding()) + if string(supplied) != string(wanted) { + return refuse() + } + identity, err := s.auth.Authorize(ctx, authorization, []string{r.Resource.System}) + if err != nil { + return refuse() + } + record := s.auth.Record(identity) + if record.Mode != callerauth.ModeEnforce || record.Principal != ServicePilotPrincipal || record.Audience != "flex-auth" || record.NotAfter.IsZero() || !record.NotAfter.After(now) { + return refuse() + } + c, ok := r.Context["execution_contract"].(map[string]any) + if !ok || c["contract_version"] != "SECRETS-EXEC-20261011-v1" || c["operation"] != "metadata-apply-ccr" || c["execution_actor"] != "service:secrets-engine" { + return refuse() + } + deadline, ok := c["operation_deadline"].(string) + if !ok { + return refuse() + } + end, err := time.Parse(time.RFC3339Nano, deadline) + if err != nil || !end.After(now) || end.After(record.NotAfter) { + return refuse() + } + raw, err := json.Marshal(r) + if err != nil { + return refuse() + } + return ServiceInput{serialized: raw, caller: record}, nil +} diff --git a/internal/taskexecution/service_input_test.go b/internal/taskexecution/service_input_test.go new file mode 100644 index 0000000..f7eee86 --- /dev/null +++ b/internal/taskexecution/service_input_test.go @@ -0,0 +1,81 @@ +package taskexecution + +import ( + "context" + "github.com/netkingdom/access-engine/internal/callerauth" + "github.com/netkingdom/access-engine/pkg/api" + "testing" + "time" +) + +type serviceReviewer struct { + identity callerauth.Identity + err error + calls int +} + +func (r *serviceReviewer) Review(_ context.Context, _ string) (callerauth.Identity, error) { + r.calls++ + return r.identity, r.err +} +func serviceRequest(t *testing.T, now time.Time) api.CheckRequest { + t.Helper() + r := api.CheckRequest{Tenant: "tenant:platform", Subject: api.SubjectRef{ID: "secrets-engine", Type: "service"}, Action: "execute-ccr", Resource: api.ResourceRef{ID: "catalog:synthetic", Type: "secret-catalog-lane", System: "secrets-engine", Attributes: map[string]any{"stage": "prod"}}, Context: map[string]any{"human_control": true, "service_actor_binding": DeclaredServiceBinding(), "execution_contract": map[string]any{"contract_version": "SECRETS-EXEC-20261011-v1", "operation": "metadata-apply-ccr", "execution_actor": "service:secrets-engine", "operation_deadline": now.Add(60 * time.Second).Format(time.RFC3339Nano)}}} + return r +} +func TestServicePilotAuthenticatedBinding(t *testing.T) { + now := time.Now() + reviewer := &serviceReviewer{identity: callerauth.Identity{Username: ServicePilotPrincipal, Audiences: []string{"flex-auth"}, NotAfter: now.Add(120 * time.Second)}} + a, e := NewServicePilotInputs(reviewer) + if e != nil { + t.Fatal(e) + } + r := serviceRequest(t, now) + s, e := a.Prepare(context.Background(), "Bearer synthetic", r, now) + if e != nil || reviewer.calls != 1 { + t.Fatal("source TokenReview path") + } + out, e := s.Request() + if e != nil || api.RequestDigest(out) != api.RequestDigest(r) || s.Caller().Principal != ServicePilotPrincipal { + t.Fatal("binding changed") + } + r.Context["service_actor_binding"].(map[string]any)["subject_id"] = "mutated" + again, _ := s.Request() + if again.Context["service_actor_binding"].(map[string]any)["subject_id"] != "secrets-engine" { + t.Fatal("opaque input retained mutable caller map") + } + if _, e := (ServiceInput{}).Request(); e == nil { + t.Fatal("zero input trusted") + } +} +func TestServicePilotRefusals(t *testing.T) { + now := time.Now() + cases := map[string]func(*api.CheckRequest, *serviceReviewer){"human": func(r *api.CheckRequest, _ *serviceReviewer) { r.Subject.Type = "human" }, "subject": func(r *api.CheckRequest, _ *serviceReviewer) { r.Subject.ID = "another" }, "legacy apply": func(r *api.CheckRequest, _ *serviceReviewer) { r.Action = "apply" }, "foreigntenant": func(r *api.CheckRequest, _ *serviceReviewer) { r.Tenant = "tenant:coulomb" }, "wrong service principal": func(_ *api.CheckRequest, p *serviceReviewer) { + p.identity.Username = "system:serviceaccount:other:other" + }, "wrong audience": func(_ *api.CheckRequest, p *serviceReviewer) { p.identity.Audiences = []string{"openbao"} }, "expired transport": func(_ *api.CheckRequest, p *serviceReviewer) { p.identity.NotAfter = now }, "missing expiry": func(_ *api.CheckRequest, p *serviceReviewer) { p.identity.NotAfter = time.Time{} }, "caller verified": func(r *api.CheckRequest, _ *serviceReviewer) { r.Context["verified"] = true }, "human projection": func(r *api.CheckRequest, _ *serviceReviewer) { + r.Context["task_execution_projection"] = map[string]any{"subject": "human"} + }, "delegation": func(r *api.CheckRequest, _ *serviceReviewer) { r.Context["delegation"] = false }, "forged binding": func(r *api.CheckRequest, _ *serviceReviewer) { + r.Context["service_actor_binding"].(map[string]any)["subject_id"] = "other" + }, "lifetime": func(r *api.CheckRequest, _ *serviceReviewer) { + r.Context["execution_contract"].(map[string]any)["operation_deadline"] = now.Add(240 * time.Second).Format(time.RFC3339Nano) + }} + for name, mutate := range cases { + t.Run(name, func(t *testing.T) { + r := serviceRequest(t, now) + p := &serviceReviewer{identity: callerauth.Identity{Username: ServicePilotPrincipal, Audiences: []string{"flex-auth"}, NotAfter: now.Add(120 * time.Second)}} + mutate(&r, p) + a, _ := NewServicePilotInputs(p) + if _, e := a.Prepare(context.Background(), "Bearer synthetic", r, now); e == nil { + t.Fatal("invalid input accepted") + } + }) + } + p := &serviceReviewer{identity: callerauth.Identity{Username: ServicePilotPrincipal, Audiences: []string{"flex-auth"}, NotAfter: now.Add(time.Minute)}} + a, _ := NewServicePilotInputs(p) + if _, e := a.Prepare(context.Background(), "", serviceRequest(t, now), now); e == nil { + t.Fatal("missing header accepted") + } + if _, e := NewServicePilotInputs(nil); e == nil { + t.Fatal("missing owner reviewer accepted") + } +} diff --git a/tools/service_task_execution_fixture.py b/tools/service_task_execution_fixture.py new file mode 100644 index 0000000..86b4875 --- /dev/null +++ b/tools/service_task_execution_fixture.py @@ -0,0 +1,13 @@ +"""Synthetic interoperability generator; never obtains credentials or native state.""" +import hashlib, json, sys +from datetime import datetime, timedelta, timezone +sys.path.insert(0, '/home/worsch/secrets-engine/src') +from secrets_engine.execution_contract import ExecutionContract, catalog_target_digest +from secrets_engine.authorization import request_digest, approval_binding_digest +h='sha256:'+'a'*64 +catalog={'lane':'synthetic','stage':'prod'} +c={'contract_version':'SECRETS-EXEC-20261011-v1','operation_id':'cbfbced0-09d5-48c8-a1e7-a2ea4dc69153','operation':'metadata-apply-ccr','execution_actor':'service:secrets-engine','ccr_id':'CCR-2099-0001','execution_grant_ccr_id':'CCR-2099-0002','ccr_immutable_sha256':h,'grant_immutable_sha256':h,'catalog_sha256':catalog_target_digest(catalog),'expected_before':h,'expected_after':h,'source_bundle':{r:{'path':'/synthetic/'+r+'.py','sha256':h} for r in ['engine_dispatcher','platform_mediator','typed_ccr','policy_auth']},'backend_origin':'http://127.0.0.1:18200','cluster_uid':'synthetic-not-native','backend_budget':{'ttl_seconds':120,'uses':12},'operation_deadline':(datetime.now(timezone.utc)+timedelta(seconds=100)).isoformat(),'operations':[{'method':method,'path':path,'payload_sha256':h} for path in ['sys/policies/acl/synthetic','auth/synthetic/role/synthetic'] for method in ['GET','PUT','GET']]} +c=ExecutionContract.parse(c).as_dict() +p={'version':'proposed-service-task-caller/v1','transport_principal':'system:serviceaccount:secrets-engine:secrets-engine','subject_id':'secrets-engine','subject_type':'service','tenant':'tenant:platform'} +r={'tenant':'tenant:platform','subject':{'id':'secrets-engine','type':'service'},'action':'execute-ccr','resource':{'id':'catalog:synthetic','type':'secret-catalog-lane','system':'secrets-engine','attributes':{'stage':'prod'}},'context':{'execution_contract':c,'catalog_target':catalog,'human_control':True,'service_actor_binding':p,'approval':{'schema_version':'0.1','kind':'approval-claim','issuer':'approval-engine','approval_id':'synthetic','valid_now':True,'consumed':False,'reason_code':'ok','binding':{'human_control':True,'pdp_path':True,'pdp_digest':h},'freshness':{'not_after':(datetime.now(timezone.utc)+timedelta(seconds=60)).isoformat()},'validity':{'expires_at':(datetime.now(timezone.utc)+timedelta(seconds=120)).isoformat()}}}} +print(json.dumps({'request':r,'request_digest':request_digest(r),'approval_binding_digest':approval_binding_digest(r),'descriptor_digest':catalog_target_digest(c)})) diff --git a/tools/task_execution_interop_fixture.py b/tools/task_execution_interop_fixture.py index 0b29884..782665e 100644 --- a/tools/task_execution_interop_fixture.py +++ b/tools/task_execution_interop_fixture.py @@ -1,13 +1,29 @@ """Synthetic interoperability generator; never obtains credentials or native state.""" -import hashlib, json, sys +import hashlib, json, os, sys from datetime import datetime, timedelta, timezone -sys.path.insert(0, '/home/worsch/secrets-engine/src') +sys.path.insert(0, os.environ.get('SECRETS_ENGINE_SOURCE', '/home/worsch/secrets-engine') + '/src') from secrets_engine.execution_contract import ExecutionContract, catalog_target_digest from secrets_engine.authorization import request_digest, approval_binding_digest +from secrets_engine.errors import DecisionError h='sha256:'+'a'*64 catalog={'lane':'synthetic','stage':'prod'} c={'contract_version':'SECRETS-EXEC-20261011-v1','operation_id':'cbfbced0-09d5-48c8-a1e7-a2ea4dc69153','operation':'metadata-apply-ccr','execution_actor':'service:secrets-engine','ccr_id':'CCR-2099-0001','execution_grant_ccr_id':'CCR-2099-0002','ccr_immutable_sha256':h,'grant_immutable_sha256':h,'catalog_sha256':catalog_target_digest(catalog),'expected_before':h,'expected_after':h,'source_bundle':{r:{'path':'/synthetic/'+r+'.py','sha256':h} for r in ['engine_dispatcher','platform_mediator','typed_ccr','policy_auth']},'backend_origin':'http://127.0.0.1:18200','cluster_uid':'synthetic-not-native','backend_budget':{'ttl_seconds':120,'uses':12},'operation_deadline':(datetime.now(timezone.utc)+timedelta(seconds=100)).isoformat(),'operations':[{'method':method,'path':path,'payload_sha256':h} for path in ['sys/policies/acl/synthetic','auth/synthetic/role/synthetic'] for method in ['GET','PUT','GET']]} c=ExecutionContract.parse(c).as_dict() p={'version':'proposed-task-execution-projection/v1','subject':'synthetic-human','principal_type':'human','tenant':'tenant:platform','tenant_source':'directory','mfa':True,'transport_principal':'system:serviceaccount:secrets-engine:secrets-engine','execution_contract_digest':catalog_target_digest(c),'resource_id':'catalog:synthetic','membership_source':'user-engine','tenant_state_source':'tenant-engine','member_active':True,'tenant_active':True,'not_after_ns':int((datetime.now(timezone.utc)+timedelta(seconds=60)).timestamp())*1000000000} r={'tenant':'tenant:platform','subject':{'id':'secrets-engine','type':'service'},'action':'execute-ccr','resource':{'id':'catalog:synthetic','type':'secret-catalog-lane','system':'secrets-engine'},'context':{'execution_contract':c,'catalog_target':catalog,'human_control':True,'task_execution_projection':p,'approval':{'kind':'approval-claim','binding':{'human_control':True}}}} -print(json.dumps({'request':r,'request_digest':request_digest(r),'approval_binding_digest':approval_binding_digest(r),'descriptor_digest':catalog_target_digest(c)})) +# Preserve the exact earlier shape as an explicit unsupported counterexample. +# It is NOT rounded, relabeled as RFC3339 or admitted into the old human policy. +original_projection = p +try: + request_digest(r) + original_projection_refused = False +except DecisionError: + original_projection_refused = True +# Descriptor-only interoperability is independent of a missing identity contract. +del r['context']['task_execution_projection'] +print(json.dumps({'request':r,'request_digest':request_digest(r), + 'approval_binding_digest':approval_binding_digest(r), + 'descriptor_digest':catalog_target_digest(c), + 'original_nanosecond_projection':original_projection, + 'original_projection_refused':original_projection_refused, + 'identity_contract_changed':False})) diff --git a/workplans/ADHOC-2026-10-11.md b/workplans/ADHOC-2026-10-11.md index 62405f5..701f08f 100644 --- a/workplans/ADHOC-2026-10-11.md +++ b/workplans/ADHOC-2026-10-11.md @@ -138,3 +138,35 @@ Any formal source receipt must follow persistence of this exact own-repo record. No operational milestone closes; authoritative issuer/client, User/Tenant membership and representation/server injection, served approval joins and native execution remain on the existing drivers. + + +## Prepare faithful service task execution source successor + +```task +id: FLEX-WP-ADHOC-2026-10-11-T05 +status: done +priority: high +``` + +Completed bounded source preparation based on accepted v0.8/GH013/GH016 intent +assessment for existing SECRETS-WP-0013/RPF-WP-0035-T12 drivers. Prepared separate +proposed service-pilot package with literal runtime-admission refusal, concrete +source TokenReview enforce adapter binding exact admitted service transport to +Secrets subject/type and immutable public declared mapping before approval issue, +plus maker positive-input/refusal and pure candidate-shape tests. No universal +human initiator/User/Tenant gate is imposed; a declared human-control approver +remains distinct from service initiator. Old proposed human package stays unchanged. + +This completes source component preparation only. No formal owner receipt, live +mapping/provider admission, policy activation, exact served approval/PDP/consume/ +journal/task-grant/executor integration, credential or native action is completed. +Operational drivers remain open; no clone workplan or source-policy enable switch +is introduced. Synthetic successful input eligibility is not runtime allowance. + + +T05 preparation refinement: preserved the original unadmitted human nanosecond +projection as explicitly refused by the repaired canonical profile; converted its +interop test to descriptor-only parity and unchanged hard-denial evidence. No old +human policy or identity contract changes and no source constant flips. Service +candidate remains structural characterization, not served approval/PDP/consume/ +compiler/journal/grant correspondence or operational readiness.