Add producer-owned authenticated Go decision client and validated Forgejo profile
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Container Image / build-and-push (push) Successful in 1m22s

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a14bfc-9643-7872-aa12-98326dcf25e7
This commit is contained in:
tegwick 2026-10-18 01:20:05 +02:00
parent d328a26010
commit ca0cb75729
24 changed files with 2980 additions and 4 deletions

View file

@ -0,0 +1,175 @@
package main
import (
"bytes"
"context"
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"encoding/json"
"encoding/pem"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/netkingdom/access-engine/internal/callerauth"
"github.com/netkingdom/access-engine/internal/decision"
"github.com/netkingdom/access-engine/internal/policy"
"github.com/netkingdom/access-engine/internal/registry"
"github.com/netkingdom/access-engine/internal/sign"
"github.com/netkingdom/access-engine/pkg/api"
"github.com/netkingdom/access-engine/pkg/decisionpep"
)
type forgejoSyntheticReviewer struct{}
func (forgejoSyntheticReviewer) Review(_ context.Context, token string) (callerauth.Identity, error) {
if token != "synthetic-forgejo-caller" {
return callerauth.Identity{}, callerauth.ErrUnauthenticated
}
return callerauth.Identity{Username: "system:serviceaccount:synthetic:forgejo", Audiences: []string{"access-engine"}, NotAfter: time.Now().Add(time.Minute)}, nil
}
func forgejoRequest(group string) api.CheckRequest {
groups := []string{group}
return api.CheckRequest{Tenant: "tenant:synthetic", Subject: api.SubjectRef{ID: "user:synthetic", Type: "human", Tenant: "tenant:synthetic", Attributes: map[string]any{"issuer": "https://issuer.invalid", "assurance": "aal2", "current_userinfo_groups": groups}}, Action: "application.access", Resource: api.ResourceRef{ID: "application:forgejo", Type: "application", System: "forgejo", Tenant: "tenant:synthetic"}, Context: map[string]any{"identity_tenant": "tenant:synthetic", "identity_subject": "user:synthetic", "identity_principal_type": "human", "identity_issuer": "https://issuer.invalid", "identity_assurance": "aal2", "current_userinfo_groups": groups, "required_groups_any_of": []string{"forge-admins", "forge-coulomb-owners", "forge-coulomb-readers"}, "big": json.Number("9007199254740993"), "fraction": json.Number("0.123456789123456789")}}
}
func TestForgejoActualPolicySignedLosslessAndLegacy(t *testing.T) {
ctx := context.Background()
pkg, e := policy.LoadAndValidateFile(ctx, filepath.Join("..", "..", "examples", "forgejo-application-access", "policy.md"))
if e != nil || !pkg.Valid {
t.Fatalf("policy: %v %+v", e, pkg)
}
store, e := registry.LoadFilePreservingNumbers(filepath.Join("..", "..", "examples", "forgejo-application-access", "registry_snapshot.json"))
if e != nil {
t.Fatal(e)
}
engine, e := decision.NewEngine(store, pkg)
if e != nil {
t.Fatal(e)
}
pub, key, e := ed25519.GenerateKey(rand.Reader)
if e != nil {
t.Fatal(e)
}
if e = engine.SetEnvelopeSigningKey(key, "synthetic-pdp"); e != nil {
t.Fatal(e)
}
auth, e := callerauth.New(callerauth.ModeEnforce, forgejoSyntheticReviewer{}, "access-engine", map[string]string{"forgejo": "system:serviceaccount:synthetic:forgejo"}, nil)
if e != nil {
t.Fatal(e)
}
server := httptest.NewTLSServer(newServeMuxWithCallerAuth(engine, auth))
defer server.Close()
folder := t.TempDir()
ca := filepath.Join(folder, "ca.pem")
token := filepath.Join(folder, "token")
os.WriteFile(ca, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: server.Certificate().Raw}), 0600)
os.WriteFile(token, []byte("synthetic-forgejo-caller"), 0600)
admitted := decisionpep.Admission{Endpoint: server.URL + "/v1/check", CAFile: ca, TokenFile: token, Keys: map[string]string{"synthetic-pdp": base64.RawURLEncoding.EncodeToString(pub)}, Evaluator: "flex-auth/local", PolicyPackage: pkg.Metadata.ID, PolicyVersion: pkg.Metadata.Version, PolicyDigest: pkg.Digest(), RegistryDigest: store.Digest(), CallerPrincipal: "system:serviceaccount:synthetic:forgejo", CallerAudience: "access-engine", MaxTTL: "30s", MaxClockSkew: "2s"}
client, e := decisionpep.New(admitted)
if e != nil {
t.Fatal(e)
}
for _, g := range []string{"forge-admins", "forge-coulomb-owners", "forge-coulomb-readers"} {
t.Run(g, func(t *testing.T) {
got, e := client.Check(ctx, forgejoRequest(g))
if e != nil {
t.Fatal(e)
}
if got.Binding.Context["big"] != json.Number("9007199254740993") || got.Binding.Context["fraction"] != json.Number("0.123456789123456789") {
t.Fatal("actual producer numeric precision lost")
}
if got.Provenance.Mode != "standalone/lossless-json-v1" || sign.Verify(got, map[string]ed25519.PublicKey{"synthetic-pdp": pub}) != nil {
t.Fatal("actual producer profile not authenticated")
}
})
}
for name, mutate := range map[string]func(*api.CheckRequest){"object-current-groups": func(r *api.CheckRequest) {
r.Context["current_userinfo_groups"] = map[string]any{"key": "forge-admins"}
}, "object-required-groups": func(r *api.CheckRequest) {
r.Context["required_groups_any_of"] = map[string]any{"a": "forge-admins", "b": "forge-coulomb-owners", "c": "forge-coulomb-readers"}
}, "outsider": func(r *api.CheckRequest) {
r.Context["current_userinfo_groups"] = []string{"outsider"}
r.Subject.Attributes["current_userinfo_groups"] = []string{"outsider"}
}, "empty-current-groups": func(r *api.CheckRequest) {
r.Context["current_userinfo_groups"] = []string{}
r.Subject.Attributes["current_userinfo_groups"] = []string{}
}, "wrong-tenant": func(r *api.CheckRequest) {
r.Tenant = "tenant:other"
r.Subject.Tenant = r.Tenant
r.Resource.Tenant = r.Tenant
r.Context["identity_tenant"] = r.Tenant
}, "wrong-issuer": func(r *api.CheckRequest) {
r.Subject.Attributes["issuer"] = "https://other.invalid"
r.Context["identity_issuer"] = "https://other.invalid"
}, "forged-resource-issuer": func(r *api.CheckRequest) {
r.Context["identity_issuer"] = "https://other.invalid"
r.Resource.Attributes = map[string]any{"identity_issuer": "https://other.invalid", "tenant": "tenant:synthetic"}
}, "wrong-assurance": func(r *api.CheckRequest) {
r.Subject.Attributes["assurance"] = "aal1"
r.Context["identity_assurance"] = "aal1"
}, "wrong-action": func(r *api.CheckRequest) { r.Action = "source.operation" }, "wrong-resource": func(r *api.CheckRequest) { r.Resource.ID = "application:unregistered" }, "claim-shortcut": func(r *api.CheckRequest) {
r.Context["required_groups_any_of"] = []string{"outsider"}
r.Context["current_userinfo_groups"] = []string{"outsider"}
r.Subject.Attributes["current_userinfo_groups"] = []string{"outsider"}
}} {
t.Run(name, func(t *testing.T) {
r := forgejoRequest("forge-admins")
mutate(&r)
if _, e := client.Check(ctx, r); e == nil {
t.Fatal("actual PDP allowed adverse input")
}
})
}
// A caller-provided JSON key cannot install the producer's private Go context
// key. This path uses the actual normal handler, not a constructed envelope.
r := forgejoRequest("forge-admins")
r.Context["decisionpep_request_nonce"] = strings.Repeat("0", 64)
r.Context["decisionpep_json_profile"] = decisionpep.JSONProfile
r.Context["jsonProfileContextKey"] = decisionpep.JSONProfile
raw, _ := json.Marshal(r)
for _, profile := range []string{"", decisionpep.JSONProfile, "unknown"} {
req, _ := http.NewRequest(http.MethodPost, server.URL+"/v1/check", bytes.NewReader(raw))
req.Header.Set("Authorization", "Bearer synthetic-forgejo-caller")
if profile != "" {
req.Header.Set("Access-Engine-JSON-Profile", profile)
}
response, e := server.Client().Do(req)
if e != nil {
t.Fatal(e)
}
body, _ := io.ReadAll(response.Body)
response.Body.Close()
if profile == "unknown" {
if response.StatusCode != 400 {
t.Fatal("unknown profile not refused")
}
continue
}
e1, e := sign.DecodeEnvelope(body)
if e != nil {
t.Fatal(e)
}
if sign.Verify(e1, map[string]ed25519.PublicKey{"synthetic-pdp": pub}) != nil {
t.Fatal("producer signature")
}
if profile == "" {
if e1.Provenance.Mode != "standalone" {
t.Fatal("caller JSON spoofed producer profile")
}
if e1.Binding.SubmittedRequestDigest == api.RequestDigest(r) {
t.Fatal("legacy numeric projection unexpectedly changed")
}
} else {
if e1.Provenance.Mode != "standalone/lossless-json-v1" || e1.Binding.SubmittedRequestDigest != api.RequestDigest(r) {
t.Fatal("lossless submitted binding mismatch")
}
}
}
}

View file

@ -512,8 +512,17 @@ func newServeMuxWithTaskFacts(engine *decisioncore.Engine, authenticator *caller
writeHTTP(w, decision, err)
return
}
profile := r.Header.Get("Access-Engine-JSON-Profile")
if profile != "" && profile != decisioncore.LosslessJSONProfile {
http.Error(w, "unsupported JSON numeric profile", http.StatusBadRequest)
return
}
var request api.CheckRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
decoder := json.NewDecoder(r.Body)
if profile == decisioncore.LosslessJSONProfile {
decoder.UseNumber()
}
if err := decoder.Decode(&request); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
@ -528,7 +537,11 @@ func newServeMuxWithTaskFacts(engine *decisioncore.Engine, authenticator *caller
writeCallerAuthError(w, err)
return
}
decision, err := engine.Check(callerauth.WithRecord(r.Context(), authenticator.Record(identity)), request)
decisionContext := callerauth.WithRecord(r.Context(), authenticator.Record(identity))
if profile == decisioncore.LosslessJSONProfile {
decisionContext = decisioncore.WithLosslessJSONProfile(decisionContext)
}
decision, err := engine.Check(decisionContext, request)
writeHTTP(w, decision, err)
})
mux.HandleFunc("/v1/batch_check", func(w http.ResponseWriter, r *http.Request) {