package decision_test import ( "bytes" "context" "encoding/json" "testing" "github.com/netkingdom/flex-auth/pkg/api" ) // A registry fact is an authority statement; the same key on the request is the // caller's proposal about itself. Enrichment once let the proposal survive, which // made every registry ceiling and allowlist advisory — a caller supplying // resource.attributes.max_ttl_hours raised its own ceiling, and one supplying // allowed_subjects authorized a subject the registry did not know // (FLEX-DEC-2026-012). // // These tests assert the registry value reaches policy on a request that // contradicts it. They are written against attribute keys real packages branch // on, because a test over an unused key would pass while the escalation stayed // open. func TestRegistryFactsOverrideCallerSuppliedAttributes(t *testing.T) { store := newTestStore(t) if err := store.ImportResourceManifest(api.ResourceManifest{ ID: "ceilings", System: "test-system", Resources: []api.Resource{{ ID: "resource:ceiling", Type: "document", Attributes: map[string]any{"max_ttl_hours": 8, "allowed_subjects": []any{"user:alice"}}, }}, }); err != nil { t.Fatalf("ImportResourceManifest: %v", err) } engine := newTestEngineWithStore(t, store) decision, err := engine.Check(context.Background(), api.CheckRequest{ Subject: api.SubjectRef{ID: "user:alice"}, Action: "read", Resource: api.ResourceRef{ ID: "resource:ceiling", System: "test-system", Attributes: map[string]any{ "max_ttl_hours": 99, "allowed_subjects": []any{"user:mallory"}, }, }, }) if err != nil { t.Fatalf("Check: %v", err) } attributes := decision.Binding.Resource.Attributes if got := attributes["max_ttl_hours"]; !equalJSON(t, got, 8) { t.Fatalf("max_ttl_hours = %v; want the registry's 8, not the caller's 99", got) } if got := attributes["allowed_subjects"]; !equalJSON(t, got, []any{"user:alice"}) { t.Fatalf("allowed_subjects = %v; caller displaced the registry allowlist", got) } // The override must be visible. A registry that silently discards a // contradicting claim hides that a caller asserted authority it did not have, // which is the half worth alerting on even though the decision is now correct. overrode, _ := decision.Diagnostics["registry_overrode"].([]string) if !containsAll(overrode, "resource.max_ttl_hours", "resource.allowed_subjects") { t.Fatalf("registry_overrode = %v; want both displaced keys named", overrode) } } // Tenant is a registry fact and the registry wins. subject.type deliberately does // NOT, and the asymmetry is the finding rather than an oversight: the registry's // Type is CARING vocabulary (Human, Agent, Automation, Service) while the // request's is the protected system's actor vocabulary (service, adm, agt, atm). // Two fields sharing a name. Overwriting substituted one vocabulary for the other // and denied every secrets-engine allow, because "Service" is not "service". // // This test pins both halves so the residual cannot be closed by accident, and so // a later reader sees the exclusion is reasoned. FLEX-WP-0025 separates them. func TestRegistryTenantWinsWhileSubjectTypeVocabularyIsPreserved(t *testing.T) { store := newTestStore(t) if err := store.ImportSubjectManifest(api.SubjectManifest{ ID: "principals", Tenants: []api.Tenant{{ID: "tenant:real"}}, Subjects: []api.Subject{{ ID: "subject:claimant", Type: "human", Tenant: "tenant:real", }}, }); err != nil { t.Fatalf("ImportSubjectManifest: %v", err) } engine := newTestEngineWithStore(t, store) decision, err := engine.Check(context.Background(), api.CheckRequest{ Tenant: "tenant:claimed", Subject: api.SubjectRef{ID: "subject:claimant", Type: "service", Tenant: "tenant:claimed"}, Action: "read", Resource: api.ResourceRef{ID: "document:internal-note", System: "markitect-tool"}, }) if err != nil { t.Fatalf("Check: %v", err) } if got := decision.Binding.Subject.Type; got != "service" { t.Fatalf("subject.type = %q; the caller's actor vocabulary must survive", got) } if got := decision.Binding.Subject.Tenant; got != "tenant:real" { t.Fatalf("subject.tenant = %q; want the registry's tenant:real", got) } } // An unregistered subject or resource has no facts to overlay, so what the caller // sent stands. That is the stated residual rather than an oversight: a policy // reading a key its manifest omits is consuming caller input, and no override // should be reported for a key nobody contradicted. func TestUnregisteredResourceKeepsCallerAttributesAndReportsNoOverride(t *testing.T) { engine := newTestEngine(t) decision, err := engine.Check(context.Background(), api.CheckRequest{ Subject: api.SubjectRef{ID: "user:alice"}, Action: "read", Resource: api.ResourceRef{ ID: "document:not-in-registry", System: "markitect-tool", Attributes: map[string]any{"stage": "prod"}, }, }) if err != nil { t.Fatalf("Check: %v", err) } if got := decision.Binding.Resource.Attributes["stage"]; got != "prod" { t.Fatalf("stage = %v; caller attributes on an unregistered resource must survive", got) } if overrode, _ := decision.Diagnostics["registry_overrode"].([]string); len(overrode) != 0 { t.Fatalf("registry_overrode = %v; nothing was contradicted", overrode) } } func equalJSON(t *testing.T, got, want any) bool { t.Helper() left, err := json.Marshal(got) if err != nil { return false } right, err := json.Marshal(want) if err != nil { return false } return bytes.Equal(left, right) } func containsAll(values []string, wanted ...string) bool { for _, want := range wanted { found := false for _, value := range values { if value == want { found = true break } } if !found { return false } } return true } // binding.request_digest is computed over the ENRICHED request, so a consumer // recomputing it over what it sent gets a different value on every request whose // subject or resource the registry knows. It was published as the §6.4.2 replay // test for consumers, which it cannot be. submitted_request_digest is that test: // RequestDigest over the request exactly as received (FLEX-DEC-2026-012). func TestSubmittedRequestDigestIsComputableByTheConsumer(t *testing.T) { engine := newTestEngine(t) request := api.CheckRequest{ Tenant: "tenant:platform", Subject: api.SubjectRef{ID: "user:alice"}, Action: "read", Resource: api.ResourceRef{ID: "document:internal-note", System: "markitect-tool"}, } decision, err := engine.Check(context.Background(), request) if err != nil { t.Fatalf("Check: %v", err) } // The consumer holds only the request it sent. This is the whole property. if got, want := decision.Binding.SubmittedRequestDigest, api.RequestDigest(request); got != want { t.Fatalf("submitted_request_digest = %q; consumer computes %q", got, want) } // And it must genuinely differ from the enriched digest here, or the test // would pass on a request the registry never touched and prove nothing. if decision.Binding.SubmittedRequestDigest == decision.Binding.RequestDigest { t.Fatal("digests agree on an enriched request; the distinction is not being exercised") } }