package main import ( "context" "encoding/json" "errors" "fmt" "io" "net/http" "strings" "github.com/netkingdom/flex-auth/internal/audit" "github.com/netkingdom/flex-auth/internal/callerauth" "github.com/netkingdom/flex-auth/internal/policy" "github.com/netkingdom/flex-auth/internal/readback" "github.com/netkingdom/flex-auth/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, "flex-auth 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 }