access-engine: secret-read evaluator for warden fetch checks (CUST-WP-0075)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 3775@bnt-lap001 Assistant-Session: cfacb6a5-ad5b-4f79-ac67-053e3b0d34a3
This commit is contained in:
parent
1309c093bb
commit
02b15a7f72
16 changed files with 1542 additions and 0 deletions
219
cmd/flex-auth/ops_warden_fetch_test.go
Normal file
219
cmd/flex-auth/ops_warden_fetch_test.go
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/netkingdom/flex-auth/internal/callerauth"
|
||||
"github.com/netkingdom/flex-auth/pkg/api"
|
||||
)
|
||||
|
||||
// The ops-warden secret-read pin (CUST-WP-0075, bug b792c5d0): warden's
|
||||
// check_fetch_policy sends action read on resource type secret with
|
||||
// resource.system = the lane's owner_repo.
|
||||
|
||||
const opsWardenPrincipal = "system:serviceaccount:ops-warden:ops-warden"
|
||||
|
||||
func fetchPath(name string) string {
|
||||
return filepath.Join("..", "..", "examples", "ops-warden-fetch", name)
|
||||
}
|
||||
|
||||
func loadFetchRequest(t *testing.T, name string) api.CheckRequest {
|
||||
t.Helper()
|
||||
raw, err := os.ReadFile(fetchPath(name))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var request api.CheckRequest
|
||||
if err := json.Unmarshal(raw, &request); err != nil {
|
||||
t.Fatalf("%s: %v", name, err)
|
||||
}
|
||||
return request
|
||||
}
|
||||
|
||||
func TestOpsWardenFetchDecisions(t *testing.T) {
|
||||
engine, err := buildEngine(context.Background(), fetchPath("registry_snapshot.json"), fetchPath("policy_package.md"), "")
|
||||
if err != nil {
|
||||
t.Fatalf("buildEngine: %v", err)
|
||||
}
|
||||
cases := []struct {
|
||||
file string
|
||||
effect api.DecisionEffect
|
||||
reason string
|
||||
}{
|
||||
{"check_request_allow_forgejo_admin_api_token.json", api.DecisionEffectAllow, "secret_read_lane_matched"},
|
||||
{"check_request_allow_openrouter_llm_connect.json", api.DecisionEffectAllow, "secret_read_lane_matched"},
|
||||
{"check_request_deny_unknown_need.json", api.DecisionEffectDeny, "unknown_need"},
|
||||
{"check_request_deny_wrong_system.json", api.DecisionEffectDeny, "wrong_system"},
|
||||
{"check_request_deny_non_secret_type.json", api.DecisionEffectDeny, "wrong_resource_type"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.file, func(t *testing.T) {
|
||||
decision, err := engine.Check(context.Background(), loadFetchRequest(t, tc.file))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if decision.Effect != tc.effect || decision.Reason != tc.reason {
|
||||
t.Fatalf("decision = %s/%s; want %s/%s", decision.Effect, decision.Reason, tc.effect, tc.reason)
|
||||
}
|
||||
if decision.ID == "" {
|
||||
t.Fatal("decision.ID is empty; warden needs a policy_decision_id")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// fetchBindings reads the caller bindings exactly as the values file declares
|
||||
// them, so the test exercises the configuration that would be deployed.
|
||||
func fetchBindings(t *testing.T) (string, map[string]string) {
|
||||
t.Helper()
|
||||
raw, err := os.ReadFile(filepath.Join("..", "..", "values", "ops-warden-fetch.yaml"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var values struct {
|
||||
CallerAuth struct {
|
||||
Mode string `yaml:"mode"`
|
||||
Binding string `yaml:"binding"`
|
||||
ExtraBindings []string `yaml:"extraBindings"`
|
||||
} `yaml:"callerAuth"`
|
||||
}
|
||||
if err := yaml.Unmarshal(raw, &values); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bindings := map[string]string{}
|
||||
for _, pair := range append([]string{values.CallerAuth.Binding}, values.CallerAuth.ExtraBindings...) {
|
||||
system, principal, ok := strings.Cut(pair, "=")
|
||||
if !ok {
|
||||
t.Fatalf("malformed binding %q", pair)
|
||||
}
|
||||
bindings[system] = principal
|
||||
}
|
||||
return values.CallerAuth.Mode, bindings
|
||||
}
|
||||
|
||||
func TestOpsWardenFetchCallerBindings(t *testing.T) {
|
||||
mode, bindings := fetchBindings(t)
|
||||
if mode != "enforce" {
|
||||
t.Fatalf("callerAuth.mode = %q; the fetch pin must enforce", mode)
|
||||
}
|
||||
for system, principal := range bindings {
|
||||
if principal != opsWardenPrincipal {
|
||||
t.Fatalf("system %q bound to %q; only ops-warden may ask", system, principal)
|
||||
}
|
||||
}
|
||||
engine, err := buildEngine(context.Background(), fetchPath("registry_snapshot.json"), fetchPath("policy_package.md"), "")
|
||||
if err != nil {
|
||||
t.Fatalf("buildEngine: %v", err)
|
||||
}
|
||||
serve := func(username string) *httptest.Server {
|
||||
authenticator, err := callerauth.New(callerauth.ModeEnforce, fixedTokenReviewer{identity: callerauth.Identity{
|
||||
Username: username,
|
||||
Audiences: []string{"flex-auth"},
|
||||
}}, "flex-auth", bindings, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return httptest.NewServer(newServeMuxWithCallerAuth(engine, authenticator))
|
||||
}
|
||||
post := func(server *httptest.Server, request api.CheckRequest, token bool) (int, api.DecisionEnvelope) {
|
||||
body, _ := json.Marshal(request)
|
||||
req, _ := http.NewRequest(http.MethodPost, server.URL+"/v1/check", bytes.NewReader(body))
|
||||
req.Header.Set("content-type", "application/json")
|
||||
if token {
|
||||
req.Header.Set("authorization", "Bearer workload-token")
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var envelope api.DecisionEnvelope
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
if err := json.NewDecoder(resp.Body).Decode(&envelope); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return resp.StatusCode, envelope
|
||||
}
|
||||
|
||||
warden := serve(opsWardenPrincipal)
|
||||
defer warden.Close()
|
||||
allow := loadFetchRequest(t, "check_request_allow_forgejo_admin_api_token.json")
|
||||
|
||||
if code, envelope := post(warden, allow, true); code != http.StatusOK || envelope.Effect != api.DecisionEffectAllow {
|
||||
t.Fatalf("ops-warden forgejo read = %d %s/%s; want 200 allow", code, envelope.Effect, envelope.Reason)
|
||||
}
|
||||
if code, _ := post(warden, allow, false); code != http.StatusUnauthorized {
|
||||
t.Fatalf("anonymous = %d; want 401", code)
|
||||
}
|
||||
unbound := allow
|
||||
unbound.Resource.System = "secrets-engine"
|
||||
unbound.Context = map[string]any{"need_id": "forgejo-admin-api-token", "owner_repo": "secrets-engine"}
|
||||
if code, _ := post(warden, unbound, true); code != http.StatusForbidden {
|
||||
t.Fatalf("ops-warden on unbound system = %d; want 403", code)
|
||||
}
|
||||
|
||||
other := serve("system:serviceaccount:another:caller")
|
||||
defer other.Close()
|
||||
if code, _ := post(other, allow, true); code != http.StatusForbidden {
|
||||
t.Fatalf("wrong caller = %d; want 403", code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOpsWardenFetchRegistryMatchesPolicy keeps the two generated artifacts in
|
||||
// step: every catalog lane in the package is a registry resource of its owner
|
||||
// system, and nothing else is. Drift against the catalog itself is
|
||||
// tools/gen_ops_warden_fetch.py --check (tests/stage1.sh).
|
||||
func TestOpsWardenFetchRegistryMatchesPolicy(t *testing.T) {
|
||||
raw, err := os.ReadFile(fetchPath("policy_package.md"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
block := regexp.MustCompile(`(?s)catalog_lanes := \{(.*?)\}`).FindSubmatch(raw)
|
||||
if block == nil {
|
||||
t.Fatal("catalog_lanes block not found")
|
||||
}
|
||||
var fromPolicy []string
|
||||
for _, m := range regexp.MustCompile(`"([^"]+)": "([^"]+)"`).FindAllSubmatch(block[1], -1) {
|
||||
fromPolicy = append(fromPolicy, string(m[2])+"/secret:"+string(m[1]))
|
||||
}
|
||||
|
||||
snapRaw, err := os.ReadFile(fetchPath("registry_snapshot.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var snap struct {
|
||||
ResourceManifests []struct {
|
||||
System string `json:"system"`
|
||||
Resources []struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"resources"`
|
||||
} `json:"resource_manifests"`
|
||||
}
|
||||
if err := json.Unmarshal(snapRaw, &snap); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var fromRegistry []string
|
||||
for _, manifest := range snap.ResourceManifests {
|
||||
for _, resource := range manifest.Resources {
|
||||
fromRegistry = append(fromRegistry, manifest.System+"/"+resource.ID)
|
||||
}
|
||||
}
|
||||
sort.Strings(fromPolicy)
|
||||
sort.Strings(fromRegistry)
|
||||
if len(fromPolicy) == 0 || strings.Join(fromPolicy, ",") != strings.Join(fromRegistry, ",") {
|
||||
t.Fatalf("policy lanes %v\nregistry %v", fromPolicy, fromRegistry)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue