Bind trusted task approval to exact producer facts and lifetime
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
This commit is contained in:
parent
4f6946bbb3
commit
2b064eee51
31 changed files with 2170 additions and 30 deletions
|
|
@ -333,6 +333,7 @@ func runServe(args []string, stdout, stderr io.Writer) int {
|
|||
logPath := fs.String("log", "", "optional JSONL decision log path")
|
||||
signingKeyFile := fs.String("envelope-signing-key-file", "", "owner-mounted base64url Ed25519 seed file")
|
||||
signingKID := fs.String("envelope-signing-kid", "", "independently admitted envelope signing key id")
|
||||
approvalSourceFile := fs.String("approval-source-file", "", "owner fixed Approval HTTPS reader configuration file")
|
||||
outboxDir := fs.String("outbox-dir", "", "durable emission outbox directory (FLEX-WP-0031); exclusive with --log")
|
||||
emissionSource := fs.String("emission-source", "", "audit-core source name for emitted decision events, e.g. flex-auth.tenant-engine")
|
||||
auditURL := fs.String("audit-url", "", "admitted audit-core /v1/events endpoint; empty disables drain and heartbeats")
|
||||
|
|
@ -402,7 +403,19 @@ func runServe(args []string, stdout, stderr io.Writer) int {
|
|||
if err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
mux := newServeMuxWithServicePilotInputs(engine, authenticator, serviceInputs)
|
||||
var approvalReader *taskexecution.ApprovalReader
|
||||
if engine.IsTrustedServicePilot() {
|
||||
if *signingKeyFile == "" || *approvalSourceFile == "" {
|
||||
return fail(stderr, fmt.Errorf("trusted task source requires signing and fixed Approval reader configuration"))
|
||||
}
|
||||
approvalReader, err = taskexecution.LoadApprovalReader(*approvalSourceFile)
|
||||
if err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
} else if *approvalSourceFile != "" {
|
||||
return fail(stderr, fmt.Errorf("Approval reader requires exact trusted task source package"))
|
||||
}
|
||||
mux := newServeMuxWithTaskFacts(engine, authenticator, serviceInputs, approvalReader)
|
||||
if outbox != nil {
|
||||
mountEmission(mux, outbox)
|
||||
}
|
||||
|
|
@ -457,6 +470,9 @@ func newServeMuxWithCallerAuth(engine *decisioncore.Engine, authenticator *calle
|
|||
}
|
||||
|
||||
func newServeMuxWithServicePilotInputs(engine *decisioncore.Engine, authenticator *callerauth.Authenticator, serviceInputs *taskexecution.ServicePilotInputs) *http.ServeMux {
|
||||
return newServeMuxWithTaskFacts(engine, authenticator, serviceInputs, nil)
|
||||
}
|
||||
func newServeMuxWithTaskFacts(engine *decisioncore.Engine, authenticator *callerauth.Authenticator, serviceInputs *taskexecution.ServicePilotInputs, approvalReader *taskexecution.ApprovalReader) *http.ServeMux {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("content-type", "application/json")
|
||||
|
|
@ -467,12 +483,16 @@ func newServeMuxWithServicePilotInputs(engine *decisioncore.Engine, authenticato
|
|||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if engine.IsServicePilot() {
|
||||
if engine.IsTaskPin() {
|
||||
if serviceInputs == nil {
|
||||
http.Error(w, "service input unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
request, err := taskexecution.DecodeServiceRequest(r.Body)
|
||||
decode := taskexecution.DecodeServiceRequest
|
||||
if engine.IsTrustedServicePilot() {
|
||||
decode = taskexecution.DecodeTrustedServiceRequest
|
||||
}
|
||||
request, err := decode(r.Body)
|
||||
if err != nil || request.Purpose != "" {
|
||||
http.Error(w, "invalid service task request", http.StatusBadRequest)
|
||||
return
|
||||
|
|
@ -482,7 +502,12 @@ func newServeMuxWithServicePilotInputs(engine *decisioncore.Engine, authenticato
|
|||
http.Error(w, "service task input refused", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
decision, err := engine.CheckServicePilot(r.Context(), input)
|
||||
var decision api.DecisionEnvelope
|
||||
if engine.IsTrustedServicePilot() {
|
||||
decision, err = engine.CheckTrustedService(r.Context(), input, approvalReader)
|
||||
} else {
|
||||
decision, err = engine.CheckServicePilot(r.Context(), input)
|
||||
}
|
||||
writeHTTP(w, decision, err)
|
||||
return
|
||||
}
|
||||
|
|
@ -506,7 +531,7 @@ func newServeMuxWithServicePilotInputs(engine *decisioncore.Engine, authenticato
|
|||
writeHTTP(w, decision, err)
|
||||
})
|
||||
mux.HandleFunc("/v1/batch_check", func(w http.ResponseWriter, r *http.Request) {
|
||||
if engine.IsServicePilot() {
|
||||
if engine.IsTaskPin() {
|
||||
http.Error(w, "service task batch unavailable", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
|
@ -537,7 +562,7 @@ func newServeMuxWithServicePilotInputs(engine *decisioncore.Engine, authenticato
|
|||
// Only the existing exact service pilot package selects this source path.
|
||||
// This is not an eligibility toggle; successful preparation still only DENYs.
|
||||
func buildServicePilotInputs(engine *decisioncore.Engine, mode callerauth.Mode, audience, kubernetesURL, reviewerTokenFile, caFile string, bindings map[string]string) (*taskexecution.ServicePilotInputs, error) {
|
||||
if !engine.IsServicePilot() {
|
||||
if !engine.IsTaskPin() {
|
||||
return nil, nil
|
||||
}
|
||||
if mode != callerauth.ModeEnforce || audience != "flex-auth" || len(bindings) != 1 || bindings["secrets-engine"] != taskexecution.ServicePilotPrincipal {
|
||||
|
|
@ -547,6 +572,9 @@ func buildServicePilotInputs(engine *decisioncore.Engine, mode callerauth.Mode,
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if engine.IsTrustedServicePilot() {
|
||||
return taskexecution.NewTrustedServiceInputs(reviewer)
|
||||
}
|
||||
return taskexecution.NewServicePilotInputs(reviewer)
|
||||
}
|
||||
|
||||
|
|
@ -575,11 +603,15 @@ func writeCallerAuthError(w http.ResponseWriter, err error) {
|
|||
}
|
||||
|
||||
func buildEngine(ctx context.Context, registryPath, policyPath, logPath string) (*decisioncore.Engine, error) {
|
||||
store, err := registry.LoadFile(registryPath)
|
||||
pkg, err := policy.LoadAndValidateFile(ctx, policyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pkg, err := policy.LoadAndValidateFile(ctx, policyPath)
|
||||
load := registry.LoadFile
|
||||
if pkg.Metadata.ID == taskexecution.ServicePilotPackageID && pkg.Metadata.Version == taskexecution.TrustedServicePilotPackageVersion {
|
||||
load = registry.LoadFilePreservingNumbers
|
||||
}
|
||||
store, err := load(registryPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
430
cmd/access-engine/trusted_service_http_test.go
Normal file
430
cmd/access-engine/trusted_service_http_test.go
Normal file
|
|
@ -0,0 +1,430 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/netkingdom/access-engine/internal/audit"
|
||||
"github.com/netkingdom/access-engine/internal/callerauth"
|
||||
"github.com/netkingdom/access-engine/internal/decision"
|
||||
"github.com/netkingdom/access-engine/internal/policy"
|
||||
"github.com/netkingdom/access-engine/internal/registry"
|
||||
"github.com/netkingdom/access-engine/internal/sign"
|
||||
"github.com/netkingdom/access-engine/internal/taskexecution"
|
||||
"github.com/netkingdom/access-engine/pkg/api"
|
||||
"github.com/netkingdom/approval-engine/adapters/go/approvalfact"
|
||||
)
|
||||
|
||||
func trustedRequest(t *testing.T, now time.Time) api.CheckRequest {
|
||||
t.Helper()
|
||||
r := pilotRequest(now)
|
||||
r.Context["execution_contract"].(map[string]any)["operation_deadline"] = now.Add(60 * time.Second).UTC().Truncate(time.Second).Format(time.RFC3339)
|
||||
r.Subject.Tenant = r.Tenant
|
||||
r.Resource.Tenant = r.Tenant
|
||||
r.Resource.Attributes["numeric_fixture"] = json.Number("1.0")
|
||||
c := r.Context["execution_contract"].(map[string]any)
|
||||
c["operation_id"] = "11111111-2222-4333-8444-555555555555"
|
||||
c["ccr_id"] = "synthetic-change"
|
||||
c["execution_grant_ccr_id"] = "synthetic-grant"
|
||||
hash := "sha256:" + strings.Repeat("1", 64)
|
||||
for _, key := range []string{"ccr_immutable_sha256", "grant_immutable_sha256", "catalog_sha256", "expected_before", "expected_after"} {
|
||||
c[key] = hash
|
||||
}
|
||||
c["backend_budget"] = map[string]any{"ttl_seconds": json.Number("30"), "uses": json.Number("12")}
|
||||
var ops []any
|
||||
for i, method := range []string{"GET", "PUT", "GET", "GET", "PUT", "GET"} {
|
||||
path := "sys/policies/acl/synthetic"
|
||||
if i >= 3 {
|
||||
path = "auth/synthetic/role/synthetic"
|
||||
}
|
||||
ops = append(ops, map[string]any{"method": method, "path": path, "payload_sha256": hash})
|
||||
}
|
||||
c["operations"] = ops
|
||||
delete(r.Context, "approval")
|
||||
raw, _ := json.Marshal(map[string]any{"action": r.Action, "actor": r.Subject.ID, "principal": "secrets-engine", "purpose": "synthetic source-only proof", "target": r.Resource})
|
||||
r.Context["purpose"] = "synthetic source-only proof"
|
||||
_, digest, err := approvalfact.CanonicalBinding(raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bindingValue, err := approvalfact.DecodeJSON(raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
binding := bindingValue.(map[string]any)
|
||||
// PDP profile intentionally retains the published legacy numeric projection.
|
||||
encoded, _ := json.Marshal(r)
|
||||
var legacy api.CheckRequest
|
||||
json.Unmarshal(encoded, &legacy)
|
||||
binding["digest"] = digest
|
||||
binding["pdp_digest"] = api.ApprovalBindingDigest(legacy)
|
||||
binding["pdp_path"] = true
|
||||
binding["human_control"] = true
|
||||
r.Context["approval"] = map[string]any{"schema_version": "0.1", "kind": "approval-claim", "issuer": "approval-engine", "approval_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", "state": "valid", "valid_now": true, "consumed": false, "reason_code": "ok", "binding": binding, "freshness": map[string]any{"observed_at": now.Add(-time.Second).Format(time.RFC3339Nano), "ttl_seconds": json.Number("120"), "not_after": now.Add(119 * time.Second).Format(time.RFC3339Nano)}, "validity": map[string]any{"not_before": now.Add(-time.Hour).Format(time.RFC3339Nano), "expires_at": now.Add(time.Hour).Format(time.RFC3339Nano)}}
|
||||
return r
|
||||
}
|
||||
func TestTrustedServiceActualHTTPPositiveAndRefusals(t *testing.T) { testTrustedServiceHTTP(t, false) }
|
||||
func TestTrustedServiceRetainedRegistryPreapprovalAndRefusals(t *testing.T) {
|
||||
testTrustedServiceHTTP(t, true)
|
||||
}
|
||||
func testTrustedServiceHTTP(t *testing.T, retained bool) {
|
||||
now := time.Now().UTC()
|
||||
folder := t.TempDir()
|
||||
reviewer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/apis/authentication.k8s.io/v1/tokenreviews" || r.Header.Get("Authorization") != "Bearer synthetic-reviewer" {
|
||||
w.WriteHeader(403)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]any{"status": map[string]any{"authenticated": true, "audiences": []string{"flex-auth"}, "user": map[string]any{"username": taskexecution.ServicePilotPrincipal}}})
|
||||
}))
|
||||
defer reviewer.Close()
|
||||
ca := filepath.Join(folder, "review-ca")
|
||||
token := filepath.Join(folder, "review-token")
|
||||
os.WriteFile(ca, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: reviewer.Certificate().Raw}), 0600)
|
||||
os.WriteFile(token, []byte("synthetic-reviewer"), 0600)
|
||||
original := trustedRequest(t, now)
|
||||
if retained {
|
||||
delete(original.Context, "approval")
|
||||
}
|
||||
claimRaw, _ := json.Marshal(original.Context["approval"])
|
||||
mode := "ok"
|
||||
reads := 0
|
||||
provider := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
reads++
|
||||
if r.URL.Path != "/v1/approvals/aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee/claim" || r.Header.Get("Authorization") != "Bearer synthetic-approval-reader" {
|
||||
w.WriteHeader(403)
|
||||
return
|
||||
}
|
||||
if mode == "redirect" {
|
||||
http.Redirect(w, r, reviewer.URL, 302)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch mode {
|
||||
case "duplicate":
|
||||
io.WriteString(w, `{"state":"valid","state":"invalid"}`)
|
||||
case "changed":
|
||||
v, _ := approvalfact.DecodeJSON(claimRaw)
|
||||
v.(map[string]any)["consumed"] = true
|
||||
json.NewEncoder(w).Encode(v)
|
||||
case "short-fresh":
|
||||
v, _ := approvalfact.DecodeJSON(claimRaw)
|
||||
fresh := v.(map[string]any)["freshness"].(map[string]any)
|
||||
fresh["ttl_seconds"] = json.Number("20")
|
||||
fresh["not_after"] = now.Add(19 * time.Second).Format(time.RFC3339Nano)
|
||||
json.NewEncoder(w).Encode(v)
|
||||
case "expired":
|
||||
v, _ := approvalfact.DecodeJSON(claimRaw)
|
||||
v.(map[string]any)["freshness"].(map[string]any)["not_after"] = now.Add(-time.Second).Format(time.RFC3339Nano)
|
||||
json.NewEncoder(w).Encode(v)
|
||||
default:
|
||||
w.Write(claimRaw)
|
||||
}
|
||||
}))
|
||||
defer provider.Close()
|
||||
pca := filepath.Join(folder, "approval-ca")
|
||||
ptoken := filepath.Join(folder, "approval-token")
|
||||
os.WriteFile(pca, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: provider.Certificate().Raw}), 0600)
|
||||
os.WriteFile(ptoken, []byte("synthetic-approval-reader"), 0440)
|
||||
source, err := taskexecution.NewApprovalReader(taskexecution.ApprovalSourceConfig{SourceID: "synthetic-approval", SourceRevision: "test-source-revision", Origin: provider.URL, StoreTenant: "tenant:platform", CAFile: pca, TokenFile: ptoken})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pkg, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution-v2", "policy_package.md"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
store := registry.NewStore()
|
||||
if retained {
|
||||
store, err = registry.LoadFilePreservingNumbers(filepath.Join("..", "..", "examples", "secrets-engine", "registry_snapshot.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
engine, err := decision.NewEngine(store, pkg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
key := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{19}, 32))
|
||||
engine.SetEnvelopeSigningKey(key, "synthetic-v2")
|
||||
record := filepath.Join(folder, "record.jsonl")
|
||||
engine.SetDecisionLog(audit.NewJSONLDecisionLog(record))
|
||||
inputs, err := buildServicePilotInputs(engine, callerauth.ModeEnforce, "flex-auth", reviewer.URL, token, ca, map[string]string{"secrets-engine": taskexecution.ServicePilotPrincipal})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
server := httptest.NewServer(newServeMuxWithTaskFacts(engine, callerauth.Disabled(), inputs, source))
|
||||
defer server.Close()
|
||||
post := func(request api.CheckRequest) (int, []byte) {
|
||||
t.Helper()
|
||||
raw, _ := json.Marshal(request)
|
||||
req, _ := http.NewRequest("POST", server.URL+"/v1/check", bytes.NewReader(raw))
|
||||
req.Header.Set("Authorization", "Bearer "+pilotToken("valid", now))
|
||||
response, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
return response.StatusCode, body
|
||||
}
|
||||
var pre api.DecisionEnvelope
|
||||
if retained {
|
||||
preStatus, preRaw := post(original)
|
||||
if preStatus != 200 {
|
||||
t.Fatalf("preapproval status%d", preStatus)
|
||||
}
|
||||
pre, err = sign.DecodeEnvelope(preRaw)
|
||||
if err != nil || sign.Verify(pre, map[string]ed25519.PublicKey{"synthetic-v2": key.Public().(ed25519.PublicKey)}) != nil {
|
||||
t.Fatal("preapproval not authenticated")
|
||||
}
|
||||
if pre.Effect != api.DecisionEffectDeny || pre.Lifetime != nil || pre.Binding.ApprovalBindingDigest == "" || pre.Binding.ApprovalBindingDigest != pre.Binding.RequestDigest {
|
||||
t.Fatal("preapproval receipt is execution authority or lacks exactclaimfree digest")
|
||||
}
|
||||
if pre.Diagnostics["registry_subject"] != true || pre.Provenance.RegistrySnapshotDigest != store.Digest() {
|
||||
t.Fatal("retained registry not applied")
|
||||
}
|
||||
rawOriginal, _ := json.Marshal(original)
|
||||
var legacyPre api.CheckRequest
|
||||
json.Unmarshal(rawOriginal, &legacyPre)
|
||||
if pre.Binding.SubmittedRequestDigest != api.RequestDigest(legacyPre) || pre.Binding.RequestDigest == api.RequestDigest(legacyPre) {
|
||||
t.Fatal("original/evaluated retained registry distinction lost")
|
||||
}
|
||||
carried := trustedRequest(t, now).Context["approval"].(map[string]any)
|
||||
native := map[string]any{"action": pre.Binding.Action, "actor": pre.Binding.Subject.ID, "principal": "secrets-engine", "purpose": pre.Binding.Context["purpose"], "target": pre.Binding.Resource}
|
||||
nativeRaw, _ := json.Marshal(native)
|
||||
_, nativeDigest, err := approvalfact.CanonicalBinding(nativeRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
native["digest"] = nativeDigest
|
||||
native["pdp_digest"] = pre.Binding.ApprovalBindingDigest
|
||||
native["pdp_path"] = true
|
||||
native["human_control"] = true
|
||||
carried["binding"] = native
|
||||
original.Context["approval"] = carried
|
||||
claimRaw, _ = json.Marshal(carried)
|
||||
originalBytes, _ := json.Marshal(original)
|
||||
original, err = taskexecution.DecodeTrustedServiceRequest(bytes.NewReader(originalBytes))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
status, body := post(original)
|
||||
if status != 200 {
|
||||
t.Fatalf("positive status %d: %s", status, body)
|
||||
}
|
||||
envelope, err := sign.DecodeEnvelope(body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("actual HTTP source policy pin: %s %s %s", pkg.Metadata.ID, pkg.Metadata.Version, pkg.Digest())
|
||||
if envelope.Effect != api.DecisionEffectAllow || sign.Verify(envelope, map[string]ed25519.PublicKey{"synthetic-v2": key.Public().(ed25519.PublicKey)}) != nil {
|
||||
t.Fatalf("not signed ALLOW: %s", body)
|
||||
}
|
||||
if envelope.Provenance.ApprovalObservation == nil || envelope.Provenance.ApprovalObservation.SourceID != "synthetic-approval" || reads != 1 {
|
||||
t.Fatal("missing authenticated observation provenance")
|
||||
}
|
||||
legacyRaw, _ := json.Marshal(original)
|
||||
var legacyRequest api.CheckRequest
|
||||
json.Unmarshal(legacyRaw, &legacyRequest)
|
||||
if envelope.Binding.SubmittedRequestDigest != api.RequestDigest(legacyRequest) {
|
||||
t.Fatal("original submitted digest changed")
|
||||
}
|
||||
if retained {
|
||||
if envelope.Binding.RequestDigest == api.RequestDigest(legacyRequest) || envelope.Binding.ApprovalBindingDigest != pre.Binding.ApprovalBindingDigest {
|
||||
t.Fatal("normalized approval correspondence changed")
|
||||
}
|
||||
} else if envelope.Binding.RequestDigest != api.RequestDigest(legacyRequest) || envelope.Binding.ApprovalBindingDigest != api.ApprovalBindingDigest(legacyRequest) {
|
||||
t.Fatal("published digest profile changed")
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(envelope.Binding.Context, original.Context) {
|
||||
t.Fatal("original context changed")
|
||||
}
|
||||
if envelope.Lifetime.ExpiresAt != original.Context["execution_contract"].(map[string]any)["operation_deadline"] {
|
||||
t.Fatal("decision does not bound entire exact operation")
|
||||
}
|
||||
if envelope.Binding.Resource.Attributes["numeric_fixture"] != json.Number("1.0") {
|
||||
t.Fatal("native numeric kind lost")
|
||||
}
|
||||
recorded, err := os.ReadFile(record)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
records, err := audit.NewJSONLDecisionLog(record).ReadAll()
|
||||
expectedCount := 1
|
||||
if retained {
|
||||
expectedCount = 2
|
||||
}
|
||||
if err != nil || len(records) != expectedCount || !reflect.DeepEqual(records[len(records)-1], envelope) {
|
||||
t.Fatal("returned record parity")
|
||||
}
|
||||
if retained && !reflect.DeepEqual(records[0], pre) {
|
||||
t.Fatal("preapproval record parity")
|
||||
}
|
||||
for _, failure := range []string{"redirect", "duplicate", "changed", "expired", "short-fresh"} {
|
||||
t.Run(failure, func(t *testing.T) {
|
||||
mode = failure
|
||||
status, _ := post(original)
|
||||
if status == 200 {
|
||||
t.Fatal("bad source admitted")
|
||||
}
|
||||
})
|
||||
}
|
||||
mode = "ok"
|
||||
if retained {
|
||||
t.Run("registry-drift-cannot-rebind-approved-act", func(t *testing.T) {
|
||||
subject, _ := store.Subject("secrets-engine")
|
||||
originalSubject := subject
|
||||
subject.Roles = append(append([]api.CanonicalRole{}, subject.Roles...), api.CanonicalRole("synthetic-owner-drift"))
|
||||
if err := store.PutSubject(subject); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer store.PutSubject(originalSubject)
|
||||
if status, _ := post(original); status == 200 {
|
||||
t.Fatal("changed registry silently rebound original/fresh claim")
|
||||
}
|
||||
})
|
||||
}
|
||||
t.Run("original-stale", func(t *testing.T) {
|
||||
raw, _ := json.Marshal(original)
|
||||
r, err := taskexecution.DecodeTrustedServiceRequest(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fresh := r.Context["approval"].(map[string]any)["freshness"].(map[string]any)
|
||||
fresh["observed_at"] = now.Add(-121 * time.Second).Format(time.RFC3339Nano)
|
||||
fresh["not_after"] = now.Add(-time.Second).Format(time.RFC3339Nano)
|
||||
status, _ := post(r)
|
||||
if status == 200 {
|
||||
t.Fatal("fresh provider rescued stale original")
|
||||
}
|
||||
})
|
||||
t.Run("native-number-kind", func(t *testing.T) {
|
||||
raw, _ := json.Marshal(original)
|
||||
r, _ := taskexecution.DecodeTrustedServiceRequest(bytes.NewReader(raw))
|
||||
r.Resource.Attributes["numeric_fixture"] = json.Number("1")
|
||||
status, _ := post(r)
|
||||
if status == 200 {
|
||||
t.Fatal("changed native integer/float authority accepted")
|
||||
}
|
||||
})
|
||||
after, _ := os.ReadFile(record)
|
||||
if !bytes.Equal(after, recorded) {
|
||||
t.Fatal("refused calls recorded executable envelopes")
|
||||
}
|
||||
// Exercise actual production CLI configuration with no ambient credentials.
|
||||
seedPath := filepath.Join(folder, "cli-seed")
|
||||
os.WriteFile(seedPath, []byte(base64.RawURLEncoding.EncodeToString(bytes.Repeat([]byte{19}, 32))), 0440)
|
||||
sourcePath := filepath.Join(folder, "approval-source.json")
|
||||
sourceJSON, _ := json.Marshal(taskexecution.ApprovalSourceConfig{SourceID: "synthetic-approval", SourceRevision: "test-source-revision", Origin: provider.URL, StoreTenant: "tenant:platform", CAFile: pca, TokenFile: ptoken})
|
||||
os.WriteFile(sourcePath, sourceJSON, 0600)
|
||||
registryPath := filepath.Join(folder, "registry.json")
|
||||
registryBytes := []byte(`{}`)
|
||||
if retained {
|
||||
registryBytes, err = os.ReadFile(filepath.Join("..", "..", "examples", "secrets-engine", "registry_snapshot.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
os.WriteFile(registryPath, registryBytes, 0600)
|
||||
policyPath, _ := filepath.Abs(filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution-v2", "policy_package.md"))
|
||||
cliRecord := filepath.Join(folder, "cli-record.jsonl")
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
address := listener.Addr().String()
|
||||
listener.Close()
|
||||
args := []string{"serve", "--addr", address, "--registry", registryPath, "--policy", policyPath, "--log", cliRecord, "--caller-auth-mode", "enforce", "--caller-audience", "flex-auth", "--caller-kubernetes-url", reviewer.URL, "--caller-reviewer-token-file", token, "--caller-ca-file", ca, "--caller-binding", "secrets-engine=" + taskexecution.ServicePilotPrincipal, "--envelope-signing-key-file", seedPath, "--envelope-signing-kid", "synthetic-v2", "--approval-source-file", sourcePath}
|
||||
argsJSON, _ := json.Marshal(args)
|
||||
child := exec.Command(os.Args[0], "-test.run=^TestServicePilotChildCLI$")
|
||||
child.Env = []string{"ACCESS_B1_SERVER_ARGS=" + string(argsJSON)}
|
||||
child.Stdout = io.Discard
|
||||
child.Stderr = io.Discard
|
||||
if err := child.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { child.Process.Kill(); child.Wait() }()
|
||||
ready := false
|
||||
for attempt := 0; attempt < 100; attempt++ {
|
||||
response, err := http.Get("http://" + address + "/healthz")
|
||||
if err == nil {
|
||||
response.Body.Close()
|
||||
ready = true
|
||||
break
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
if !ready {
|
||||
t.Fatal("trusted CLI not ready")
|
||||
}
|
||||
raw, _ := json.Marshal(original)
|
||||
req, _ := http.NewRequest("POST", "http://"+address+"/v1/check", bytes.NewReader(raw))
|
||||
req.Header.Set("Authorization", "Bearer "+pilotToken("valid", now))
|
||||
response, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
wire, _ := io.ReadAll(response.Body)
|
||||
cliEnvelope, err := sign.DecodeEnvelope(wire)
|
||||
if response.StatusCode != 200 || err != nil || cliEnvelope.Effect != api.DecisionEffectAllow || sign.Verify(cliEnvelope, map[string]ed25519.PublicKey{"synthetic-v2": key.Public().(ed25519.PublicKey)}) != nil {
|
||||
t.Fatalf("CLI did not return signed trusted ALLOW: %s", wire)
|
||||
}
|
||||
cliRecords, err := audit.NewJSONLDecisionLog(cliRecord).ReadAll()
|
||||
if err != nil || len(cliRecords) != 1 || !reflect.DeepEqual(cliRecords[0], cliEnvelope) {
|
||||
t.Fatal("CLI response/readback record mismatch")
|
||||
}
|
||||
if !reflect.DeepEqual(cliEnvelope.Binding.Context, original.Context) {
|
||||
t.Fatal("CLI changed original numeric context")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestTrustedServiceMissingAndGenericAdmissionRefuse(t *testing.T) {
|
||||
pkg, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution-v2", "policy_package.md"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
engine, err := decision.NewEngine(registry.NewStore(), pkg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request := trustedRequest(t, time.Now().UTC())
|
||||
if _, err := engine.Check(context.Background(), request); err == nil {
|
||||
t.Fatal("generic check bypass")
|
||||
}
|
||||
if _, err := engine.BatchCheck(context.Background(), api.BatchCheckRequest{Action: "execute-ccr"}); err == nil {
|
||||
t.Fatal("generic batch bypass")
|
||||
}
|
||||
if _, err := engine.ListAllowed(context.Background(), decision.ListAllowedRequest{Action: "execute-ccr"}); err == nil {
|
||||
t.Fatal("generic list bypass")
|
||||
}
|
||||
if _, err := engine.CheckTrustedService(context.Background(), taskexecution.ServiceInput{}, nil); err == nil {
|
||||
t.Fatal("missing typed input and fixed source accepted")
|
||||
}
|
||||
recorder := httptest.NewRecorder()
|
||||
raw, _ := json.Marshal(request)
|
||||
newServeMux(engine).ServeHTTP(recorder, httptest.NewRequest("POST", "/v1/check", bytes.NewReader(raw)))
|
||||
if recorder.Code != 503 {
|
||||
t.Fatal("unwired typed path accepted")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue