net-kingdom/tests/playwright_registration_native_test.go
tegwick 5dd74dfbdf
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
security: prepare scoped Playwright consumer registrations and owner delivery
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a1346b-72a4-7d40-b8bd-8b8ca346cfa2
2026-10-13 23:13:48 +02:00

120 lines
4.8 KiB
Go

// Copy into a disposable KeyCape src/internal/authclient checkout. This uses
// its native token endpoint/JWKS verifier, with a synthetic directory and seeded
// authorization sessions. It does not prove upstream login or Kubernetes access.
package authclient
import (
"context"
"crypto/sha256"
"encoding/base64"
"net/url"
"os"
"testing"
"time"
"keycape/internal/config"
"keycape/internal/domain"
"keycape/internal/server/oidc"
)
type playwrightUsers struct{ users }
func (playwrightUsers) LookupUser(context.Context, string) (*domain.User, error) {
return &domain.User{ID: "user:playwright-test", Username: "test", Enabled: true, Tenant: "tenant:test"}, nil
}
func TestNetKingdomPlaywrightGeneratedRegistration(t *testing.T) {
path := os.Getenv("PLAYWRIGHT_SYNTHETIC_CONFIG")
if path == "" {
t.Fatal("synthetic generated config required")
}
cfg, err := config.Load(path)
if err != nil {
t.Fatal("generated config did not load")
}
if len(config.ValidateConfig(cfg)) != 0 {
t.Fatal("native config validation failed")
}
records, err := cfg.Registrations()
if err != nil {
t.Fatal("native registration conversion failed")
}
c, d, h := provider(t)
h.Users = playwrightUsers{}
h.ClientConfig = map[string]*domain.Client{}
for i := range records {
record := &records[i]
if record.ClientType == "confidential" {
record.ClientSecret = "synthetic-playwright-fixture"
}
h.ClientConfig[record.ClientID] = record
}
ctx := context.Background()
agentID := "playwright-agent-test-agent"
form := url.Values{"grant_type": {"client_credentials"}, "scope": {"playwright:connect"}}
token, err := c.Exchange(ctx, d, form, agentID, "synthetic-playwright-fixture", "playwright-kubernetes", "")
if err != nil {
t.Fatal("generated service exchange failed")
}
claims, err := c.Verify(ctx, d, token.AccessToken, "playwright-kubernetes", "")
if err != nil || claims["principal_type"] != "service" || claims["sub"] != "service:test-agent:platform" || claims["tenant"] != "tenant:test" {
t.Fatal("service claim contract mismatch")
}
roles, ok := claims["roles"].([]any)
if !ok || len(roles) != 1 || roles[0] != "coding-agent" {
t.Fatal("agent role mismatch")
}
if token.ExpiresIn != 900 {
t.Fatal("agent lifetime mismatch")
}
if _, err = c.Verify(ctx, d, token.AccessToken, "openbao", ""); err == nil {
t.Fatal("wrong audience accepted")
}
if _, err = c.Verify(ctx, d, token.AccessToken+"tampered", "playwright-kubernetes", ""); err == nil {
t.Fatal("tampered token accepted")
}
if _, err = c.Exchange(ctx, d, form, agentID, "wrong", "playwright-kubernetes", ""); err == nil {
t.Fatal("wrong secret accepted")
}
form.Set("scope", "openbao:login")
if _, err = c.Exchange(ctx, d, form, agentID, "synthetic-playwright-fixture", "playwright-kubernetes", ""); err == nil {
t.Fatal("excess scope accepted")
}
verifier := "synthetic-playwright-verifier-with-enough-length-12345"
digest := sha256.Sum256([]byte(verifier))
callback := "http://127.0.0.1:18764/callback"
exchange := func(sentVerifier, sentCallback string) (Tokens, error) {
code := h.Sessions.Create(&oidc.PKCESession{ClientID: "playwright-cli", Username: "test", Nonce: "synthetic-nonce",
Scopes: []string{"openid", "profile", "groups", "playwright:connect"},
PKCEChallenge: base64.RawURLEncoding.EncodeToString(digest[:]), RedirectURI: callback, ExpiresAt: time.Now().Add(time.Minute)})
fields := url.Values{"grant_type": {"authorization_code"}, "client_id": {"playwright-cli"}, "code": {code},
"redirect_uri": {sentCallback}, "code_verifier": {sentVerifier}, "scope": {"openid playwright:connect"}}
return c.Exchange(ctx, d, fields, "playwright-cli", "", "playwright-kubernetes", "synthetic-nonce")
}
human, err := exchange(verifier, callback)
if err != nil {
t.Fatal("public PKCE token exchange failed")
}
claims, err = c.Verify(ctx, d, human.AccessToken, "playwright-kubernetes", "")
if err != nil || claims["principal_type"] != "human" || claims["sub"] != "user:playwright-test" || claims["tenant"] != "tenant:test" {
t.Fatal("human claim contract mismatch")
}
if _, err = c.Verify(ctx, d, human.IDToken, "playwright-kubernetes", ""); err == nil {
t.Fatal("ID token accepted as resource token")
}
if _, err = exchange("wrong-verifier", callback); err == nil {
t.Fatal("wrong PKCE verifier accepted")
}
if _, err = exchange(verifier, "http://127.0.0.1:18765/callback"); err == nil {
t.Fatal("wrong callback accepted")
}
delete(h.ClientConfig, agentID)
form.Set("scope", "playwright:connect")
if _, err = c.Exchange(ctx, d, form, agentID, "synthetic-playwright-fixture", "playwright-kubernetes", ""); err == nil {
t.Fatal("removed registration still issues")
}
if _, err = c.Verify(ctx, d, token.AccessToken, "playwright-kubernetes", ""); err != nil {
t.Fatal("issued JWT lifetime limitation changed")
}
}