- module path renamed (no external importers); golangci local-prefixes - ACCESS_ENGINE_RUN_TOPAZ_INTEGRATION replaces FLEX_AUTH_RUN_TOPAZ_INTEGRATION - README title and identity paragraph reflect the landed renames - docs/runtime-rename-inventory.md: per-consumer references by kind and order - chart name, image package and /opt paths deliberately left for T05 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 735967@bnt-lap001 Assistant-Session: 5971967f-77f8-4ced-9e02-c6ef67040ffc
167 lines
5.7 KiB
Go
167 lines
5.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/netkingdom/access-engine/internal/audit"
|
|
"github.com/netkingdom/access-engine/internal/callerauth"
|
|
"github.com/netkingdom/access-engine/internal/policy"
|
|
"github.com/netkingdom/access-engine/internal/readback"
|
|
"github.com/netkingdom/access-engine/internal/registry"
|
|
)
|
|
|
|
// readbackBinding is the caller-binding key the readback routes authorize
|
|
// against. It is not a protected system, so a principal bound to it can never
|
|
// represent a system on any pin's /v1/check (FLEX-WP-0035-T02).
|
|
const readbackBinding = "application-readback"
|
|
|
|
type repeatedFlag []string
|
|
|
|
func (f *repeatedFlag) String() string { return strings.Join(*f, ",") }
|
|
|
|
func (f *repeatedFlag) Set(value string) error {
|
|
*f = append(*f, value)
|
|
return nil
|
|
}
|
|
|
|
// runServeReadback serves the advisory application-access readback. It mounts
|
|
// no /v1/check or /v1/batch_check: this pin cannot produce an enforcement
|
|
// decision (FLEX-DEC-2026-019).
|
|
func runServeReadback(args []string, _, stderr io.Writer) int {
|
|
fs := newFlagSet("serve-readback", stderr)
|
|
addr := fs.String("addr", "127.0.0.1:8080", "HTTP listen address")
|
|
logPath := fs.String("log", "", "optional JSONL decision log path")
|
|
callerAuthMode := fs.String("caller-auth-mode", "disabled", "disabled, warn, or enforce")
|
|
callerAudience := fs.String("caller-audience", "flex-auth", "required caller token audience")
|
|
callerKubernetesURL := fs.String("caller-kubernetes-url", "https://kubernetes.default.svc", "Kubernetes API base URL for TokenReview")
|
|
callerReviewerTokenFile := fs.String("caller-reviewer-token-file", "/var/run/secrets/flex-auth-reviewer/token", "projected Kubernetes API credential")
|
|
callerCAFile := fs.String("caller-ca-file", "/var/run/secrets/flex-auth-reviewer/ca.crt", "Kubernetes API CA bundle")
|
|
readbackCaller := fs.String("readback-caller", "", "authenticated Kubernetes principal admitted to the readback routes")
|
|
var registries repeatedFlag
|
|
fs.Var(®istries, "registry", "registry snapshot JSON file (repeatable; system ids must not repeat)")
|
|
var packages keyValueFlags
|
|
fs.Var(&packages, "package", "system=policy package Markdown file (repeatable)")
|
|
if err := fs.Parse(args); err != nil {
|
|
return 64
|
|
}
|
|
if len(registries) == 0 || len(packages) == 0 {
|
|
fmt.Fprintln(stderr, "serve-readback requires --registry and --package")
|
|
return 64
|
|
}
|
|
bindings := map[string]string{}
|
|
if *readbackCaller != "" {
|
|
bindings[readbackBinding] = *readbackCaller
|
|
}
|
|
|
|
service, err := buildReadback(context.Background(), registries, packages.StringMap())
|
|
if err != nil {
|
|
return fail(stderr, err)
|
|
}
|
|
if *logPath != "" {
|
|
service.SetDecisionLog(audit.NewJSONLDecisionLog(*logPath))
|
|
}
|
|
authenticator, err := buildCallerAuthenticator(
|
|
callerauth.Mode(*callerAuthMode),
|
|
*callerAudience,
|
|
*callerKubernetesURL,
|
|
*callerReviewerTokenFile,
|
|
*callerCAFile,
|
|
bindings,
|
|
stderr,
|
|
)
|
|
if err != nil {
|
|
return fail(stderr, err)
|
|
}
|
|
|
|
fmt.Fprintf(stderr, "access-engine readback serving on http://%s\n", *addr)
|
|
if err := http.ListenAndServe(*addr, newReadbackMux(service, authenticator)); err != nil {
|
|
return fail(stderr, err)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func buildReadback(ctx context.Context, registryPaths []string, packagePaths map[string]string) (*readback.Service, error) {
|
|
merged := registry.NewStore()
|
|
seen := map[string]string{}
|
|
for _, path := range registryPaths {
|
|
store, err := registry.LoadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
snapshot := store.Snapshot()
|
|
for _, system := range snapshot.Systems {
|
|
if first, dup := seen[system.ID]; dup {
|
|
return nil, fmt.Errorf("system %q appears in both %s and %s", system.ID, first, path)
|
|
}
|
|
seen[system.ID] = path
|
|
}
|
|
if err := merged.ApplySnapshot(snapshot); err != nil {
|
|
return nil, fmt.Errorf("merge %s: %w", path, err)
|
|
}
|
|
}
|
|
loaded := make(map[string]*policy.Package, len(packagePaths))
|
|
for system, path := range packagePaths {
|
|
pkg, err := policy.LoadAndValidateFile(ctx, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
loaded[system] = pkg
|
|
}
|
|
return readback.New(merged, loaded)
|
|
}
|
|
|
|
func newReadbackMux(service *readback.Service, authenticator *callerauth.Authenticator) *http.ServeMux {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("content-type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
|
})
|
|
authorize := func(w http.ResponseWriter, r *http.Request) (context.Context, bool) {
|
|
identity, err := authenticator.Authorize(r.Context(), r.Header.Get("Authorization"), []string{readbackBinding})
|
|
if err != nil {
|
|
writeCallerAuthError(w, err)
|
|
return nil, false
|
|
}
|
|
return callerauth.WithRecord(r.Context(), authenticator.Record(identity)), true
|
|
}
|
|
mux.HandleFunc("/v1/readback/application_access", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
var request readback.Request
|
|
decoder := json.NewDecoder(r.Body)
|
|
decoder.DisallowUnknownFields() // no action, resource or purpose can be smuggled in
|
|
if err := decoder.Decode(&request); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
ctx, ok := authorize(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
response, err := service.Evaluate(ctx, request)
|
|
if errors.Is(err, readback.ErrInvalidRequest) {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
writeHTTP(w, response, err)
|
|
})
|
|
mux.HandleFunc("/v1/readback/systems", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
if _, ok := authorize(w, r); !ok {
|
|
return
|
|
}
|
|
writeHTTP(w, service.Systems(), nil)
|
|
})
|
|
return mux
|
|
}
|