FLEX-WP-0006: implement ops-warden signing gate policy

This commit is contained in:
tegwick 2026-06-23 21:17:42 +02:00
parent 53e0d055c9
commit 0fde95a87c
25 changed files with 1796 additions and 10 deletions

View file

@ -337,6 +337,16 @@ func runServe(args []string, stdout, stderr io.Writer) int {
return fail(stderr, err)
}
mux := newServeMux(engine)
fmt.Fprintf(stderr, "flex-auth serving on http://%s\n", *addr)
if err := http.ListenAndServe(*addr, mux); err != nil {
return fail(stderr, err)
}
return 0
}
func newServeMux(engine *decisioncore.Engine) *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("content-type", "application/json")
@ -368,12 +378,7 @@ func runServe(args []string, stdout, stderr io.Writer) int {
decisions, err := engine.BatchCheck(r.Context(), request)
writeHTTP(w, decisions, err)
})
fmt.Fprintf(stderr, "flex-auth serving on http://%s\n", *addr)
if err := http.ListenAndServe(*addr, mux); err != nil {
return fail(stderr, err)
}
return 0
return mux
}
func buildEngine(ctx context.Context, registryPath, policyPath, logPath string) (*decisioncore.Engine, error) {

View file

@ -2,7 +2,11 @@ package main
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
@ -74,6 +78,76 @@ func TestRunBatchCheck(t *testing.T) {
}
}
func TestRunCheckOpsWarden(t *testing.T) {
var stdout, stderr bytes.Buffer
code := run([]string{
"check",
"--registry", opsPath("registry_snapshot.json"),
"--policy", opsPath("policy_package.md"),
"--request", opsPath("check_request_allow_adm.json"),
}, &stdout, &stderr)
if code != 0 {
t.Fatalf("code = %d, stderr = %s", code, stderr.String())
}
var decision api.DecisionEnvelope
if err := json.Unmarshal(stdout.Bytes(), &decision); err != nil {
t.Fatalf("unmarshal decision: %v\n%s", err, stdout.String())
}
if decision.Effect != api.DecisionEffectAllow {
t.Fatalf("decision.Effect = %q; want allow", decision.Effect)
}
if decision.ID == "" {
t.Fatal("decision.ID is empty; ops-warden needs a policy_decision_id")
}
}
func TestServeOpsWardenCheckContract(t *testing.T) {
logPath := filepath.Join(t.TempDir(), "decisions.jsonl")
engine, err := buildEngine(context.Background(), opsPath("registry_snapshot.json"), opsPath("policy_package.md"), logPath)
if err != nil {
t.Fatalf("buildEngine: %v", err)
}
server := httptest.NewServer(newServeMux(engine))
defer server.Close()
allow := postCheck(t, server.URL+"/v1/check", opsPath("check_request_allow_adm.json"))
if allow.Effect != api.DecisionEffectAllow || allow.ID == "" {
t.Fatalf("allow decision = %+v; want allow with id", allow)
}
deny := postCheck(t, server.URL+"/v1/check", opsPath("check_request_deny_ttl_above_max.json"))
if deny.Effect != api.DecisionEffectDeny || deny.Reason != "ttl_out_of_bounds" {
t.Fatalf("deny decision = %+v; want ttl_out_of_bounds deny", deny)
}
resp, err := http.Get(server.URL + "/v1/check")
if err != nil {
t.Fatalf("GET /v1/check: %v", err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusMethodNotAllowed {
t.Fatalf("GET /v1/check status = %d; want 405", resp.StatusCode)
}
resp, err = http.Post(server.URL+"/v1/check", "application/json", strings.NewReader(`{"subject":`))
if err != nil {
t.Fatalf("POST malformed /v1/check: %v", err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("malformed POST status = %d; want 400", resp.StatusCode)
}
logData, err := os.ReadFile(logPath)
if err != nil {
t.Fatalf("read decision log: %v", err)
}
if !strings.Contains(string(logData), allow.ID) || !strings.Contains(string(logData), deny.ID) {
t.Fatalf("decision log does not contain both decision ids\nlog: %s\nallow: %s deny: %s", string(logData), allow.ID, deny.ID)
}
}
func TestRunValidateAccessDescriptor(t *testing.T) {
var stdout, stderr bytes.Buffer
code := run([]string{"validate", "--kind", "access-descriptor", "--file", examplePath("access_descriptor.yaml")}, &stdout, &stderr)
@ -88,3 +162,29 @@ func TestRunValidateAccessDescriptor(t *testing.T) {
func examplePath(name string) string {
return filepath.Join("..", "..", "examples", "caring", name)
}
func opsPath(name string) string {
return filepath.Join("..", "..", "examples", "ops-warden", name)
}
func postCheck(t *testing.T, url, path string) api.DecisionEnvelope {
t.Helper()
body, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
resp, err := http.Post(url, "application/json", bytes.NewReader(body))
if err != nil {
t.Fatalf("POST %s: %v", url, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("POST %s status = %d; want 200", path, resp.StatusCode)
}
var decision api.DecisionEnvelope
if err := json.NewDecoder(resp.Body).Decode(&decision); err != nil {
t.Fatalf("decode %s response: %v", path, err)
}
return decision
}