Add an offline attribute-admission branch beside exact-record review.
Compact-sitting v8 can admit a new memo from carried class, submitter, and approval attributes, but shipped sets are empty and exact-record decisions stay on their pins. The sitting image pin is unchanged. Assistant: grok Assistant-Session: 01a10329-31cb-78d3-b7a3-788c2f91d6d4
This commit is contained in:
parent
d014453a1a
commit
e85f511c4f
8 changed files with 1237 additions and 10 deletions
87
internal/decision/attribute_context_test.go
Normal file
87
internal/decision/attribute_context_test.go
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
package decision_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/netkingdom/flex-auth/internal/decision"
|
||||
"github.com/netkingdom/flex-auth/internal/policy"
|
||||
"github.com/netkingdom/flex-auth/internal/registry"
|
||||
"github.com/netkingdom/flex-auth/pkg/api"
|
||||
)
|
||||
|
||||
func TestAttributeContextCarriedWithoutViewHash(t *testing.T) {
|
||||
root := filepath.Join("..", "..")
|
||||
pkg, err := policy.LoadAndValidateFile(context.Background(), filepath.Join(root, "examples", "informed-decision-sitting", "policy.md"))
|
||||
if err != nil {
|
||||
t.Fatalf("LoadAndValidateFile: %v", err)
|
||||
}
|
||||
store, err := registry.LoadFile(filepath.Join(root, "examples", "informed-decision-sitting", "registry.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
engine, err := decision.NewEngine(store, pkg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request := api.CheckRequest{
|
||||
ID: "attribute-context",
|
||||
Tenant: "tenant:platform",
|
||||
Subject: api.SubjectRef{
|
||||
ID: "synthetic-attribute-reviewer",
|
||||
Type: "human",
|
||||
Tenant: "tenant:platform",
|
||||
Attributes: map[string]any{
|
||||
"principal_type_source": "authentication-derived",
|
||||
"tenant_source": "registration-supplied",
|
||||
"groups": []any{"net-kingdom-admins"},
|
||||
"assurance": map[string]any{
|
||||
"level": "aal2", "mfa": true, "source": "key-cape",
|
||||
"methods": []any{"pwd", "otp"}, "at": time.Now().Unix(),
|
||||
},
|
||||
},
|
||||
},
|
||||
Action: "accept",
|
||||
Resource: api.ResourceRef{
|
||||
ID: "memo:intake-new-memo", Type: "decision-memo",
|
||||
System: "informed-decision", Tenant: "tenant:platform",
|
||||
},
|
||||
Context: map[string]any{
|
||||
"review_group": "net-kingdom-admins",
|
||||
"memo_class": "credential-lane",
|
||||
"intake_submitter": "state-hub",
|
||||
"approval_requester": "informed-decision",
|
||||
"approval_human_control": true,
|
||||
"memo_version": 1,
|
||||
"approval_id": "706f4205-aaa7-44f6-be30-d0f57664d69c",
|
||||
"approval_binding_digest": "sha256:c8a1a9c8fb22616e3aacfb877ba7974feb5eedaed491f856a6c90719c502ebe4",
|
||||
},
|
||||
PolicyVersion: "v8",
|
||||
}
|
||||
got, err := engine.Check(context.Background(), request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Effect != api.DecisionEffectDeny || got.Reason != "unknown_class" {
|
||||
t.Fatalf("effect = %s %s; shipped sets must refuse the class", got.Effect, got.Reason)
|
||||
}
|
||||
if got.Binding == nil {
|
||||
t.Fatal("binding missing")
|
||||
}
|
||||
for _, key := range []string{
|
||||
"review_group", "memo_class", "intake_submitter", "approval_requester",
|
||||
"approval_human_control", "memo_version", "approval_id", "approval_binding_digest",
|
||||
} {
|
||||
if _, ok := got.Binding.Context[key]; !ok {
|
||||
t.Fatalf("binding context missing %s", key)
|
||||
}
|
||||
}
|
||||
if _, ok := got.Binding.Context["view_hash"]; ok {
|
||||
t.Fatal("view_hash entered the decision binding")
|
||||
}
|
||||
if got.Binding.Context["approval_requester"] != "informed-decision" || got.Binding.Context["approval_human_control"] != true {
|
||||
t.Fatalf("binding context = %#v", got.Binding.Context)
|
||||
}
|
||||
}
|
||||
253
internal/policy/informed_decision_attribute_test.go
Normal file
253
internal/policy/informed_decision_attribute_test.go
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
package policy_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/netkingdom/flex-auth/internal/policy"
|
||||
"github.com/netkingdom/flex-auth/pkg/api"
|
||||
)
|
||||
|
||||
func TestInformedDecisionAttributeAdmission(t *testing.T) {
|
||||
pkg, err := policy.LoadAndValidateFile(context.Background(), sittingPolicyPath(t))
|
||||
if err != nil {
|
||||
t.Fatalf("LoadAndValidateFile: %v", err)
|
||||
}
|
||||
if !pkg.Valid {
|
||||
t.Fatalf("package invalid: %+v", pkg.Validation)
|
||||
}
|
||||
if pkg.Metadata.Version != "v8" {
|
||||
t.Fatalf("version = %s; want v8", pkg.Metadata.Version)
|
||||
}
|
||||
if strings.Contains(pkg.RegoModule, "view_hash") {
|
||||
t.Fatal("rule module reads view_hash")
|
||||
}
|
||||
if !strings.Contains(pkg.RegoModule, "admitted_submitters := set()") || !strings.Contains(pkg.RegoModule, "admitted_classes := set()") {
|
||||
t.Fatal("shipped admitted sets are not empty")
|
||||
}
|
||||
|
||||
raw, err := os.ReadFile(filepath.Join(sittingDir(t), "attribute_admission_v8.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var doc struct {
|
||||
PolicyVersion string `json:"policy_version"`
|
||||
Cases []struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Assurance string `json:"assurance"`
|
||||
Request api.CheckRequest `json:"request"`
|
||||
Expect api.DecisionExpectation `json:"expect"`
|
||||
} `json:"cases"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if doc.PolicyVersion != "v8" {
|
||||
t.Fatalf("fixture policy_version = %s", doc.PolicyVersion)
|
||||
}
|
||||
exactKeys := map[string]bool{"memo_version": true, "approval_id": true, "approval_binding_digest": true}
|
||||
attributeKeys := map[string]bool{
|
||||
"review_group": true, "memo_class": true, "intake_submitter": true,
|
||||
"approval_requester": true, "approval_human_control": true, "memo_version": true,
|
||||
"approval_id": true, "approval_binding_digest": true,
|
||||
}
|
||||
for _, fixture := range doc.Cases {
|
||||
request := fixture.Request
|
||||
stampAssurance(t, &request, fixture.Assurance)
|
||||
if _, ok := request.Context["view_hash"]; ok {
|
||||
t.Fatalf("%s carries view_hash", fixture.ID)
|
||||
}
|
||||
switch fixture.Kind {
|
||||
case "exact":
|
||||
assertKeys(t, fixture.ID, request.Context, exactKeys)
|
||||
case "attribute":
|
||||
assertKeys(t, fixture.ID, request.Context, attributeKeys)
|
||||
default:
|
||||
t.Fatalf("%s kind %q", fixture.ID, fixture.Kind)
|
||||
}
|
||||
got, err := pkg.Evaluate(context.Background(), request)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", fixture.ID, err)
|
||||
}
|
||||
if got.Effect != fixture.Expect.Effect || got.Reason != fixture.Expect.Reason {
|
||||
t.Fatalf("%s: got %s %s; want %s %s", fixture.ID, got.Effect, got.Reason, fixture.Expect.Effect, fixture.Expect.Reason)
|
||||
}
|
||||
if fixture.ID == "exact-record-allow" {
|
||||
withHash := request
|
||||
withHash.Context = copyContext(request.Context)
|
||||
withHash.Context["view_hash"] = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
hashed, err := pkg.Evaluate(context.Background(), withHash)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if hashed.Effect != got.Effect || hashed.Reason != got.Reason {
|
||||
t.Fatalf("view_hash changed exact decision to %s %s", hashed.Effect, hashed.Reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSittingImagePinUnchanged(t *testing.T) {
|
||||
raw, err := os.ReadFile(filepath.Join(repoRoot(t), "values", "informed-decision-sitting.yaml"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
text := string(raw)
|
||||
if !strings.Contains(text, "sha256:f6fa2062f7095e66c7a91f20cd130914b5481d46d9d909339e028f7ecdf0065d") {
|
||||
t.Fatal("sitting image digest changed")
|
||||
}
|
||||
if !strings.Contains(text, "/opt/flex-auth/examples/informed-decision-sitting/policy.md") {
|
||||
t.Fatal("sitting policy path changed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdmissionSetGenerator(t *testing.T) {
|
||||
root := repoRoot(t)
|
||||
check := exec.Command("python3", "tools/gen_informed_decision_admission.py", "--check")
|
||||
check.Dir = root
|
||||
if out, err := check.CombinedOutput(); err != nil {
|
||||
t.Fatalf("drift check: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
policyPath := filepath.Join(dir, "policy.md")
|
||||
shipped := sittingPolicyPath(t)
|
||||
source, err := os.ReadFile(shipped)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(policyPath, source, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
registry := filepath.Join(dir, "submitters.json")
|
||||
body := `{
|
||||
"system:serviceaccount:state-hub:state-hub": {
|
||||
"submitter_id": "state-hub",
|
||||
"memo_classes": ["credential-lane", "adr-accept"],
|
||||
"review_groups": ["net-kingdom-admins"],
|
||||
"daily_limit": 1
|
||||
}
|
||||
}`
|
||||
if err := os.WriteFile(registry, []byte(body), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
drift := exec.Command("python3", filepath.Join(root, "tools/gen_informed_decision_admission.py"),
|
||||
"--source", registry, "--policy", policyPath, "--check")
|
||||
if err := drift.Run(); err == nil {
|
||||
t.Fatal("non-empty registry matched the empty shipped sets")
|
||||
}
|
||||
after, err := os.ReadFile(policyPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(after, source) {
|
||||
t.Fatal("--check wrote the policy")
|
||||
}
|
||||
|
||||
write := exec.Command("python3", filepath.Join(root, "tools/gen_informed_decision_admission.py"),
|
||||
"--source", registry, "--policy", policyPath)
|
||||
if out, err := write.CombinedOutput(); err != nil {
|
||||
t.Fatalf("generate: %v\n%s", err, out)
|
||||
}
|
||||
generated, err := os.ReadFile(policyPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
text := string(generated)
|
||||
if !strings.Contains(text, `"adr-accept",`) || !strings.Contains(text, `"credential-lane",`) || !strings.Contains(text, `"state-hub",`) {
|
||||
t.Fatalf("generated sets missing:\n%s", text)
|
||||
}
|
||||
if !bytes.Equal(mustRead(t, shipped), source) {
|
||||
t.Fatal("generator wrote the shipped policy")
|
||||
}
|
||||
again := exec.Command("python3", filepath.Join(root, "tools/gen_informed_decision_admission.py"),
|
||||
"--source", registry, "--policy", policyPath, "--check")
|
||||
if out, err := again.CombinedOutput(); err != nil {
|
||||
t.Fatalf("regenerated policy drifted: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
bad := filepath.Join(dir, "bad.json")
|
||||
if err := os.WriteFile(bad, []byte("[]"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
refused := exec.Command("python3", filepath.Join(root, "tools/gen_informed_decision_admission.py"),
|
||||
"--source", bad, "--policy", policyPath)
|
||||
if err := refused.Run(); err == nil {
|
||||
t.Fatal("malformed registry was accepted")
|
||||
}
|
||||
kept, err := os.ReadFile(policyPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(kept, generated) {
|
||||
t.Fatal("refused generate changed the policy")
|
||||
}
|
||||
}
|
||||
|
||||
func stampAssurance(t *testing.T, request *api.CheckRequest, mode string) {
|
||||
t.Helper()
|
||||
assurance, _ := request.Subject.Attributes["assurance"].(map[string]any)
|
||||
if assurance == nil {
|
||||
t.Fatal("assurance missing")
|
||||
}
|
||||
switch mode {
|
||||
case "now":
|
||||
assurance["at"] = time.Now().Unix()
|
||||
case "stale":
|
||||
assurance["at"] = int64(1)
|
||||
default:
|
||||
t.Fatalf("assurance mode %q", mode)
|
||||
}
|
||||
}
|
||||
|
||||
func assertKeys(t *testing.T, id string, ctx map[string]any, want map[string]bool) {
|
||||
t.Helper()
|
||||
if len(ctx) != len(want) {
|
||||
t.Fatalf("%s context keys = %v", id, ctx)
|
||||
}
|
||||
for key := range want {
|
||||
if _, ok := ctx[key]; !ok {
|
||||
t.Fatalf("%s missing %s", id, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func copyContext(in map[string]any) map[string]any {
|
||||
out := make(map[string]any, len(in)+1)
|
||||
for key, value := range in {
|
||||
out[key] = value
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func sittingDir(t *testing.T) string {
|
||||
t.Helper()
|
||||
return filepath.Join(repoRoot(t), "examples", "informed-decision-sitting")
|
||||
}
|
||||
|
||||
func sittingPolicyPath(t *testing.T) string {
|
||||
t.Helper()
|
||||
return filepath.Join(sittingDir(t), "policy.md")
|
||||
}
|
||||
|
||||
func repoRoot(t *testing.T) string {
|
||||
t.Helper()
|
||||
return filepath.Join("..", "..")
|
||||
}
|
||||
|
||||
func mustRead(t *testing.T, path string) []byte {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue