Implement scoped P06 authentication policy and guarded optional onboarding
All checks were successful
Authentication acceptance / acceptance (push) Successful in 1m22s
Authentication acceptance / provider-contract (push) Successful in 14s
Build and Publish Container Image / build-and-push (push) Successful in 41s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a09cbb-87c6-7900-a145-4ce53ba9f1a6
This commit is contained in:
tegwick 2026-09-14 00:00:05 +02:00
parent aa709fb854
commit e0b3c25f06
12 changed files with 1085 additions and 2 deletions

View file

@ -30,6 +30,7 @@ import (
"keycape/internal/domain"
servererrors "keycape/internal/server/errors"
"keycape/internal/server/oidc"
"keycape/internal/server/policy"
"keycape/internal/server/telemetry"
)
@ -172,6 +173,15 @@ func main() {
Issuer: issuer,
Emitter: emitter,
}
if path := os.Getenv("KEYCAPE_POLICY_PATH"); path != "" {
policies, err := policy.Open(path, clients)
if err != nil {
log.Error().Msg("authentication policy store unavailable")
os.Exit(1)
}
authorizeHandler.EffectivePolicy = policies.Effective
mux.Handle("/platform/authentication-policy", policy.Handler(policies, issuer, &privateKey.PublicKey))
}
mux.Handle("/authorize", enforcement.Middleware(authorizeHandler))
mux.Handle("/authorize/callback", authorizeHandler)
mux.Handle("/authorize/return", authorizeHandler)

View file

@ -66,6 +66,7 @@ func (p *pendingStateStore) Delete(state string) {
// AuthorizeHandler implements GET /authorize and GET /authorize/callback.
type AuthorizeHandler struct {
EffectivePolicy func(*domain.Client) (*domain.Client, error)
AccountPortalURL string
ClientConfig map[string]*domain.Client
Auth domain.AuthProvider
@ -372,6 +373,13 @@ func (h *AuthorizeHandler) ServeHTTPCallback(w http.ResponseWriter, r *http.Requ
func (h *AuthorizeHandler) decideAssurance(ctx context.Context, ps *PendingState, username string, login *LoginSession) (domain.AssuranceDecision, error) {
client := h.ClientConfig[ps.ClientID]
if h.EffectivePolicy != nil {
var err error
client, err = h.EffectivePolicy(client)
if err != nil {
return domain.AssuranceDecision{}, err
}
}
providerRequired := false
if !domain.ACRRequiresAAL2(ps.ACRValues) && (client == nil || client.MFARequired == nil) {
var err error

View file

@ -0,0 +1,124 @@
package oidc_test
import (
"context"
"crypto/hmac"
"crypto/sha1"
"encoding/base32"
"encoding/binary"
"encoding/json"
"fmt"
"keycape/internal/adapters/privacyidea"
"keycape/internal/domain"
"keycape/internal/server/oidc"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"time"
)
// Uses only the companion disposable provider fixture, never a production URL.
func TestNativeOptionalEnrollmentAndOldSession(t *testing.T) {
base := os.Getenv("P06_NATIVE_PROVIDER_URL")
if base == "" {
t.Skip("requires disposable installed-provider fixture")
}
parsed, e := url.Parse(base)
if e != nil || parsed.Hostname() != "127.0.0.1" || parsed.Scheme != "http" {
t.Fatal("loopback fixture required")
}
call := func(method, path, token string, data url.Values) map[string]interface{} {
r, e := http.NewRequest(method, base+path, strings.NewReader(data.Encode()))
if e != nil {
t.Fatal("fixture request")
}
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if token != "" {
r.Header.Set("Authorization", token)
}
client := &http.Client{Timeout: 10 * time.Second}
response, e := client.Do(r)
if e != nil {
t.Fatal("fixture unavailable")
}
defer response.Body.Close()
var body map[string]interface{}
if json.NewDecoder(response.Body).Decode(&body) != nil || response.StatusCode != 200 {
t.Fatal("fixture request failed", response.StatusCode)
}
return body
}
value := func(b map[string]interface{}) map[string]interface{} {
return b["result"].(map[string]interface{})["value"].(map[string]interface{})
}
reader := value(call("POST", "/auth", "", url.Values{"username": {"fixture-reader"}, "password": {"fixture-service-password"}}))["token"].(string)
user := value(call("POST", "/auth", "", url.Values{"username": {"native-alice"}, "password": {"fixture-password"}, "realm": {"fixture"}}))["token"].(string)
adapter := privacyidea.New(privacyidea.Config{BaseURL: base, Realm: "fixture", AdminToken: reader, ReadProbeSerial: "P06SCOPEPROBE", RequireForAll: true}, nil)
h := &oidc.AuthorizeHandler{ClientConfig: map[string]*domain.Client{"fixture": {ClientID: "fixture", MFAOptional: true}}, Auth: &mockAuthProvider{callbackResult: &domain.AuthResult{Username: "native-alice"}}, MFA: adapter, Sessions: oidc.NewSessionStore(), Logins: oidc.NewLoginSessionStore(), Emitter: &captureEmitter{}}
callback := func(acr []string, cookie *http.Cookie) *httptest.ResponseRecorder {
h.PendingStates().Store("native", &oidc.PendingState{ClientID: "fixture", RedirectURI: "https://fixture.test/callback", State: "native", ACRValues: acr, ExpiresAt: time.Now().Add(time.Minute)})
r := httptest.NewRequest("GET", "/authorize/callback?code=fixture&state=native", nil)
if cookie != nil {
r.AddCookie(cookie)
}
w := httptest.NewRecorder()
h.ServeHTTPCallback(w, r)
return w
}
first := callback(nil, nil)
if first.Code != 302 {
t.Fatal("native no-factor decision", first.Code)
}
cookie := first.Result().Cookies()[0]
detail := call("POST", "/token/init", user, url.Values{"type": {"totp"}, "genkey": {"1"}})["detail"].(map[string]interface{})
if detail["rollout_state"] != "verify" {
t.Fatal("possession confirmation not required")
}
if w := callback(nil, cookie); w.Code != 302 {
t.Fatal("pending setup activated MFA")
}
uri, e := url.Parse(detail["googleurl"].(map[string]interface{})["value"].(string))
if e != nil {
t.Fatal("invalid fixture enrollment URI")
}
key, e := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(strings.TrimRight(uri.Query().Get("secret"), "="))
if e != nil {
t.Fatal("fixture seed format")
}
otp := func() string {
counter := make([]byte, 8)
binary.BigEndian.PutUint64(counter, uint64(time.Now().Unix()/30))
mac := hmac.New(sha1.New, key)
mac.Write(counter)
digest := mac.Sum(nil)
offset := digest[len(digest)-1] & 15
return fmt.Sprintf("%06d", (binary.BigEndian.Uint32(digest[offset:offset+4])&0x7fffffff)%1000000)
}
call("POST", "/token/init", user, url.Values{"serial": {detail["serial"].(string)}, "type": {"totp"}, "verify": {otp()}})
if enrolled, e := adapter.HasEnrolledFactor(context.Background(), "native-alice"); e != nil || !enrolled {
t.Fatal("native activation not observed")
}
if w := callback(nil, cookie); w.Code != 200 || !strings.Contains(w.Body.String(), "KeyCape MFA") {
t.Fatal("old AAL1 session bypassed native enrolled factor")
}
if w := callback([]string{"aal2"}, cookie); w.Code != 200 || !strings.Contains(w.Body.String(), "KeyCape MFA") {
t.Fatal("native explicit step-up bypassed")
}
// Confirmation consumed the current TOTP; wait for the next independent code.
time.Sleep(time.Duration(31-time.Now().Unix()%30) * time.Second)
request := httptest.NewRequest("POST", "/authorize/callback", strings.NewReader(url.Values{"state": {"native"}, "mfa_token": {otp()}}.Encode()))
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
response := httptest.NewRecorder()
h.ServeHTTPCallback(response, request)
if response.Code != 302 {
t.Fatal("native OTP sign-in failed", response.Code)
}
location, _ := url.Parse(response.Header().Get("Location"))
session, ok := h.Sessions.Get(location.Query().Get("code"))
if !ok || !session.MFAVerified {
t.Fatal("native OTP did not establish MFA")
}
}

View file

@ -0,0 +1,69 @@
package oidc_test
import (
"errors"
"keycape/internal/domain"
"keycape/internal/server/oidc"
"keycape/internal/server/policy"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"time"
)
func TestRuntimePolicyEnrollmentOldSessionAndStepUp(t *testing.T) {
clients := map[string]*domain.Client{}
for _, id := range []string{"vergabe-demo-company", "user-engine-portal"} {
clients[id] = &domain.Client{ClientID: id, GrantTypes: []string{"authorization_code"}}
}
policies, e := policy.Open(filepath.Join(t.TempDir(), "policy.json"), clients)
if e != nil {
t.Fatal(e)
}
defer policies.Close()
mfa := &mockMFAProvider{required: true}
h := &oidc.AuthorizeHandler{ClientConfig: clients, EffectivePolicy: policies.Effective, Auth: &mockAuthProvider{callbackResult: &domain.AuthResult{Username: "alice"}}, MFA: mfa, Sessions: oidc.NewSessionStore(), Logins: oidc.NewLoginSessionStore(), Emitter: &captureEmitter{}}
callback := func(acr []string, cookie *http.Cookie) *httptest.ResponseRecorder {
h.PendingStates().Store("runtime", &oidc.PendingState{ClientID: "vergabe-demo-company", RedirectURI: "https://app.test/callback", State: "runtime", ACRValues: acr, ExpiresAt: time.Now().Add(time.Minute)})
r := httptest.NewRequest("GET", "/authorize/callback?code=fixture&state=runtime", nil)
if cookie != nil {
r.AddCookie(cookie)
}
w := httptest.NewRecorder()
h.ServeHTTPCallback(w, r)
return w
}
if w := callback(nil, nil); w.Code != 200 || !strings.Contains(w.Body.String(), "KeyCape MFA") {
t.Fatal("mandatory policy not effective")
}
preview, e := policies.Operation("operator", policy.Request{Action: "preview", Client: "vergabe-demo-company", Mode: policy.Optional, Reference: "change"})
if e != nil {
t.Fatal(e)
}
if _, e = policies.Operation("operator", policy.Request{Action: "apply", Confirmation: preview["confirmation"].(string), Acknowledged: true}); e != nil {
t.Fatal(e)
}
first := callback(nil, nil)
if first.Code != 302 {
t.Fatal("no-factor login denied", first.Code)
}
cookie := first.Result().Cookies()[0]
mfa.enrolled = true
if w := callback(nil, cookie); w.Code != 200 || !strings.Contains(w.Body.String(), "KeyCape MFA") {
t.Fatal("old AAL1 session bypassed enrolled factor")
}
mfa.enrolled = false
if w := callback([]string{"aal2"}, cookie); w.Code != 200 || !strings.Contains(w.Body.String(), "KeyCape MFA") {
t.Fatal("optional policy bypassed explicit MFA")
}
mfa.enrolledErr = errors.New("fixture outage")
if w := callback(nil, cookie); w.Code != 500 || strings.Contains(w.Header().Get("Location"), "code=") {
t.Fatal("lookup outage granted authorization")
}
mfa.enrolledErr = nil
if w := callback(nil, cookie); w.Code != 302 {
t.Fatal("lookup recovery failed")
}
}

View file

@ -0,0 +1,423 @@
// Package policy owns the narrowly scoped browser MFA policy and durable audit.
package policy
import (
"crypto/rand"
"crypto/rsa"
"encoding/hex"
"encoding/json"
"errors"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"syscall"
"time"
"keycape/internal/domain"
"keycape/internal/jose"
)
const Optional = "optional_after_enrollment"
const Mandatory = "mandatory"
var ErrUnavailable = errors.New("policy_unavailable")
type Receipt struct {
Reference string `json:"reference"`
Actor string `json:"actor"`
Client string `json:"client"`
Before string `json:"before"`
After string `json:"after"`
Revision uint64 `json:"revision"`
At int64 `json:"at"`
}
type state struct {
Revision uint64 `json:"revision"`
Modes map[string]string `json:"modes"`
History []Receipt `json:"history"`
}
type ticket struct {
Actor, Client, Mode, Reference string
Revision uint64
Expires int64
}
type Store struct {
lock *os.File
mu sync.Mutex
path string
current state
names map[string]string
pending map[string]ticket
failed bool
now func() time.Time
}
func Open(path string, clients map[string]*domain.Client) (*Store, error) {
s := &Store{path: path, names: map[string]string{}, pending: map[string]ticket{}, now: time.Now}
s.current.Modes = map[string]string{}
for _, id := range []string{"vergabe-demo-company", "user-engine-portal"} {
c := clients[id]
if c == nil || !contains(c.GrantTypes, "authorization_code") || contains(c.GrantTypes, "client_credentials") {
return nil, ErrUnavailable
}
mode := Mandatory
if c.MFAOptional && c.MFARequired == nil {
mode = Optional
} else if c.MFARequired != nil && !*c.MFARequired {
return nil, ErrUnavailable
}
s.current.Modes[id] = mode
s.names[id] = c.DisplayName
}
lock, err := os.OpenFile(path+".lock", os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return nil, ErrUnavailable
}
if syscall.Flock(int(lock.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) != nil {
lock.Close()
return nil, ErrUnavailable
}
s.lock = lock
initialized := false
defer func() {
if !initialized {
s.Close()
}
}()
raw, err := os.ReadFile(path)
if err != nil && !os.IsNotExist(err) {
return nil, ErrUnavailable
}
if err == nil {
if len(raw) > 16*1024*1024 || json.Unmarshal(raw, &s.current) != nil || len(s.current.Modes) != 2 {
return nil, ErrUnavailable
}
for id, mode := range s.current.Modes {
if _, ok := s.names[id]; !ok || !validMode(mode) {
return nil, ErrUnavailable
}
}
if len(s.current.History) > 0 && s.current.History[len(s.current.History)-1].Revision != s.current.Revision {
return nil, ErrUnavailable
}
} else if s.persist(s.current) != nil {
return nil, ErrUnavailable
}
initialized = true
return s, nil
}
func (s *Store) Close() {
if s.lock != nil {
syscall.Flock(int(s.lock.Fd()), syscall.LOCK_UN)
s.lock.Close()
s.lock = nil
}
}
func contains(values []string, value string) bool {
for _, v := range values {
if v == value {
return true
}
}
return false
}
func validMode(mode string) bool { return mode == Optional || mode == Mandatory }
func (s *Store) persist(next state) error {
raw, err := json.Marshal(next)
if err != nil || len(raw) > 16*1024*1024 {
return ErrUnavailable
}
file, err := os.CreateTemp(filepath.Dir(s.path), ".policy-")
if err != nil {
return ErrUnavailable
}
name := file.Name()
defer os.Remove(name)
if _, err = file.Write(raw); err == nil {
err = file.Sync()
}
closeErr := file.Close()
if err != nil || closeErr != nil {
return ErrUnavailable
}
if os.Rename(name, s.path) != nil {
return ErrUnavailable
}
dir, err := os.Open(filepath.Dir(s.path))
if err != nil {
s.failed = true
return ErrUnavailable
}
defer dir.Close()
if dir.Sync() != nil {
s.failed = true
return ErrUnavailable
}
return nil
}
// Effective returns a copy; unrelated registrations and explicit AAL2 stay intact.
func (s *Store) Effective(c *domain.Client) (*domain.Client, error) {
if c == nil {
return c, nil
}
s.mu.Lock()
defer s.mu.Unlock()
mode, ok := s.current.Modes[c.ClientID]
if !ok {
return c, nil
}
if s.failed {
return nil, ErrUnavailable
}
copy := *c
copy.MFAOptional = mode == Optional
copy.MFARequired = nil
if mode == Mandatory {
required := true
copy.MFARequired = &required
}
return &copy, nil
}
type Request struct {
Action string `json:"action"`
Client string `json:"client"`
Mode string `json:"mode"`
Reference string `json:"reference"`
Confirmation string `json:"confirmation"`
Acknowledged bool `json:"acknowledged"`
}
func (s *Store) snapshot() map[string]interface{} {
clients := []map[string]interface{}{}
for _, id := range []string{"vergabe-demo-company", "user-engine-portal"} {
clients = append(clients, map[string]interface{}{"id": id, "name": s.names[id], "mode": s.current.Modes[id]})
}
history := s.current.History
if len(history) > 20 {
history = history[len(history)-20:]
}
return map[string]interface{}{"success": true, "status": "current", "revision": s.current.Revision, "clients": clients, "history": history}
}
func (s *Store) Operation(actor string, request Request) (map[string]interface{}, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.failed {
return nil, ErrUnavailable
}
if request.Action == "status" {
return s.snapshot(), nil
}
if request.Action == "preview" || request.Action == "rollback" {
if _, ok := s.names[request.Client]; !ok || !validReference(request.Reference) {
return nil, errors.New("unsupported_policy")
}
mode := request.Mode
if request.Action == "rollback" {
mode = ""
for i := len(s.current.History) - 1; i >= 0; i-- {
r := s.current.History[i]
if r.Client == request.Client {
mode = r.Before
break
}
}
}
if !validMode(mode) {
return nil, errors.New("unsupported_policy")
}
if mode == s.current.Modes[request.Client] {
return nil, errors.New("policy_unchanged")
}
for _, r := range s.current.History {
if r.Reference == request.Reference {
return nil, errors.New("reference_already_used")
}
}
for key, t := range s.pending {
if t.Expires < s.now().Unix() {
delete(s.pending, key)
}
}
if len(s.pending) >= 1024 {
return nil, ErrUnavailable
}
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return nil, ErrUnavailable
}
key := hex.EncodeToString(bytes)
s.pending[key] = ticket{actor, request.Client, mode, request.Reference, s.current.Revision, s.now().Unix() + 900}
result := s.snapshot()
result["status"] = "preview"
result["confirmation"] = key
result["client"] = request.Client
result["before"] = s.current.Modes[request.Client]
result["after"] = mode
result["reference"] = request.Reference
return result, nil
}
if request.Action == "apply" {
t, ok := s.pending[request.Confirmation]
if !ok || t.Actor != actor || t.Expires < s.now().Unix() || !request.Acknowledged {
return nil, errors.New("preview_expired_or_changed")
}
if (request.Client != "" && request.Client != t.Client) || (request.Mode != "" && request.Mode != t.Mode) || (request.Reference != "" && request.Reference != t.Reference) {
return nil, errors.New("preview_expired_or_changed")
}
for _, r := range s.current.History {
if r.Reference == t.Reference {
if r.Actor != actor || r.Client != t.Client || r.After != t.Mode {
return nil, errors.New("preview_expired_or_changed")
}
result := s.snapshot()
result["status"] = "recorded"
result["receipt"] = r
result["replayed"] = true
return result, nil
}
}
if s.current.Revision != t.Revision {
return nil, errors.New("policy_changed_review_again")
}
modes := map[string]string{}
for k, v := range s.current.Modes {
modes[k] = v
}
modes[t.Client] = t.Mode
receipt := Receipt{t.Reference, actor, t.Client, s.current.Modes[t.Client], t.Mode, s.current.Revision + 1, s.now().Unix()}
history := append(append([]Receipt{}, s.current.History...), receipt)
next := state{s.current.Revision + 1, modes, history}
if s.persist(next) != nil {
return nil, ErrUnavailable
}
s.current = next
result := s.snapshot()
result["status"] = "recorded"
result["receipt"] = receipt
return result, nil
}
return nil, errors.New("unsupported_policy")
}
func validReference(value string) bool {
if len(value) < 1 || len(value) > 150 {
return false
}
for _, r := range value {
if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || strings.ContainsRune("_.:/-", r)) {
return false
}
}
return true
}
func Actor(claims map[string]interface{}, issuer string, now time.Time) (string, error) {
deny := errors.New("fresh_platform_mfa_required")
if claims["iss"] != issuer || claims["principal_type"] != "human" {
return "", deny
}
audience := false
switch value := claims["aud"].(type) {
case string:
audience = value == "user-engine-portal"
case []interface{}:
for _, v := range value {
if v == "user-engine-portal" {
audience = true
}
}
}
if !audience {
return "", deny
}
if azp, ok := claims["azp"]; ok && azp != "user-engine-portal" {
return "", deny
}
exp, ok := claims["exp"].(float64)
if !ok || exp <= float64(now.Unix()) {
return "", deny
}
if nbf, ok := claims["nbf"].(float64); ok && nbf > float64(now.Unix()) {
return "", deny
}
roles, ok := claims["roles"].([]interface{})
if !ok {
return "", deny
}
operator := false
for _, v := range roles {
if v == "platform-operator" {
operator = true
}
}
if !operator {
return "", deny
}
a, ok := claims["assurance"].(map[string]interface{})
if !ok || a["level"] != "aal2" || a["mfa"] != true {
return "", deny
}
at, ok := a["at"].(float64)
if !ok || float64(now.Unix())-at < 0 || float64(now.Unix())-at > 300 {
return "", deny
}
subject, ok := claims["sub"].(string)
if !ok || subject == "" || len(subject) > 150 {
return "", deny
}
return subject, nil
}
func Handler(store *Store, issuer string, key *rsa.PublicKey) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
fail := func(code int, reason string) {
w.WriteHeader(code)
json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "failure": reason})
}
if r.Method != "POST" {
fail(405, "method_not_allowed")
return
}
token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
if len(token) > 16384 {
fail(403, "fresh_platform_mfa_required")
return
}
claims, err := jose.Verify(token, jose.KeySet{"key-1": key})
if err != nil {
fail(403, "fresh_platform_mfa_required")
return
}
actor, err := Actor(claims, issuer, time.Now())
if err != nil {
fail(403, "fresh_platform_mfa_required")
return
}
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 8192))
decoder.DisallowUnknownFields()
var request Request
if decoder.Decode(&request) != nil {
fail(400, "unsupported_policy")
return
}
var extra interface{}
if decoder.Decode(&extra) != io.EOF {
fail(400, "unsupported_policy")
return
}
result, err := store.Operation(actor, request)
if err != nil {
code := 409
if errors.Is(err, ErrUnavailable) {
code = 503
}
fail(code, err.Error())
return
}
json.NewEncoder(w).Encode(result)
})
}

View file

@ -0,0 +1,196 @@
package policy
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"keycape/internal/domain"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func clients() map[string]*domain.Client {
result := map[string]*domain.Client{}
for _, id := range []string{"vergabe-demo-company", "user-engine-portal", "untouched"} {
result[id] = &domain.Client{ClientID: id, DisplayName: id, GrantTypes: []string{"authorization_code"}}
}
return result
}
func open(t *testing.T) *Store {
t.Helper()
s, e := Open(filepath.Join(t.TempDir(), "policy.json"), clients())
if e != nil {
t.Fatal(e)
}
t.Cleanup(func() { s.Close() })
return s
}
func preview(t *testing.T, s *Store, ref string) string {
t.Helper()
r, e := s.Operation("operator", Request{Action: "preview", Client: "vergabe-demo-company", Mode: Optional, Reference: ref})
if e != nil {
t.Fatal(e)
}
return r["confirmation"].(string)
}
func apply(s *Store, ticket string) (map[string]interface{}, error) {
return s.Operation("operator", Request{Action: "apply", Confirmation: ticket, Acknowledged: true})
}
func TestReviewedApplyReplayRestartRollback(t *testing.T) {
s := open(t)
key := preview(t, s, "case-1")
before, _ := s.Effective(clients()["vergabe-demo-company"])
if before.MFAOptional {
t.Fatal("preview mutated policy")
}
if _, e := s.Operation("other", Request{Action: "apply", Confirmation: key, Acknowledged: true}); e == nil {
t.Fatal("wrong actor")
}
if _, e := s.Operation("operator", Request{Action: "apply", Confirmation: key}); e == nil {
t.Fatal("missing acknowledgement")
}
if _, e := apply(s, key); e != nil {
t.Fatal(e)
}
if r, e := apply(s, key); e != nil || r["replayed"] != true {
t.Fatal("lost response replay", e)
}
after, _ := s.Effective(clients()["vergabe-demo-company"])
if !after.MFAOptional || after.MFARequired != nil {
t.Fatal("effective policy")
}
other := clients()["untouched"]
unchanged, _ := s.Effective(other)
if unchanged != other {
t.Fatal("unrelated registration changed")
}
s.Close()
restored, e := Open(s.path, clients())
if e != nil {
t.Fatal(e)
}
defer restored.Close()
if len(restored.current.History) != 1 || restored.current.Modes["vergabe-demo-company"] != Optional {
t.Fatal("durability")
}
r, e := restored.Operation("operator", Request{Action: "rollback", Client: "vergabe-demo-company", Reference: "case-2"})
if e != nil {
t.Fatal(e)
}
if _, e = apply(restored, r["confirmation"].(string)); e != nil {
t.Fatal(e)
}
if restored.current.Modes["vergabe-demo-company"] != Mandatory || len(restored.current.History) != 2 {
t.Fatal("rollback not audited")
}
}
func TestStaleExpiredUnsupportedAndAlteredRequests(t *testing.T) {
s := open(t)
first := preview(t, s, "first")
second := preview(t, s, "second")
if _, e := s.Operation("operator", Request{Action: "apply", Confirmation: first, Acknowledged: true, Mode: "disabled"}); e == nil {
t.Fatal("altered intent")
}
if _, e := apply(s, first); e != nil {
t.Fatal(e)
}
if _, e := apply(s, second); e == nil {
t.Fatal("stale review")
}
for _, r := range []Request{{Action: "preview", Client: "untouched", Mode: Optional, Reference: "x"}, {Action: "preview", Client: "user-engine-portal", Mode: "disabled", Reference: "x"}, {Action: "preview", Client: "user-engine-portal", Mode: Optional, Reference: "first"}} {
if _, e := s.Operation("operator", r); e == nil {
t.Fatal("unsupported request")
}
}
s2 := open(t)
ticket := preview(t, s2, "expire")
s2.now = func() time.Time { return time.Now().Add(time.Hour) }
if _, e := apply(s2, ticket); e == nil {
t.Fatal("expired review")
}
}
func TestWriteFailureDoesNotChangeEffectivePolicyAndSingleWriter(t *testing.T) {
s := open(t)
if second, e := Open(s.path, clients()); e == nil {
second.Close()
t.Fatal("second writer allowed")
}
ticket := preview(t, s, "fail")
s.path = filepath.Join(t.TempDir(), "missing", "policy.json")
if _, e := apply(s, ticket); e == nil {
t.Fatal("write failure accepted")
}
if s.current.Revision != 0 || s.current.Modes["vergabe-demo-company"] != Mandatory {
t.Fatal("failed write changed policy")
}
}
func TestCorruptPersistenceFailsClosed(t *testing.T) {
s := open(t)
s.Close()
os.WriteFile(s.path, []byte(`{"modes":{"other":"optional_after_enrollment"}}`), 0600)
if _, e := Open(s.path, clients()); e == nil {
t.Fatal("corrupt store accepted")
}
}
func signed(t *testing.T, key *rsa.PrivateKey, claims map[string]interface{}) string {
raw, _ := json.Marshal(claims)
header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"RS256","kid":"key-1"}`))
input := header + "." + base64.RawURLEncoding.EncodeToString(raw)
digest := sha256.Sum256([]byte(input))
signature, e := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, digest[:])
if e != nil {
t.Fatal(e)
}
return input + "." + base64.RawURLEncoding.EncodeToString(signature)
}
func TestHTTPRequiresSignedFreshPlatformMFA(t *testing.T) {
s := open(t)
key, e := rsa.GenerateKey(rand.Reader, 2048)
if e != nil {
t.Fatal(e)
}
handler := Handler(s, "https://issuer.test", &key.PublicKey)
base := func() map[string]interface{} {
return map[string]interface{}{"iss": "https://issuer.test", "aud": "user-engine-portal", "sub": "operator", "principal_type": "human", "roles": []string{"platform-operator"}, "exp": time.Now().Add(time.Minute).Unix(), "assurance": map[string]interface{}{"level": "aal2", "mfa": true, "at": time.Now().Unix()}}
}
for index, change := range []func(map[string]interface{}){func(c map[string]interface{}) {}, func(c map[string]interface{}) { c["aud"] = "other" }, func(c map[string]interface{}) { c["roles"] = []string{"tenant-admin"} }, func(c map[string]interface{}) {
c["assurance"] = map[string]interface{}{"level": "aal1", "mfa": false, "at": time.Now().Unix()}
}, func(c map[string]interface{}) {
c["assurance"] = map[string]interface{}{"level": "aal2", "mfa": true, "at": time.Now().Add(-time.Hour).Unix()}
}, func(c map[string]interface{}) { c["exp"] = 0 }} {
claims := base()
change(claims)
token := signed(t, key, claims)
r := httptest.NewRequest("POST", "/platform/authentication-policy", strings.NewReader(`{"action":"status"}`))
r.Header.Set("Authorization", "Bearer "+token)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
expected := 403
if index == 0 {
expected = 200
}
if w.Code != expected {
t.Fatalf("unexpected status %d", w.Code)
}
}
r := httptest.NewRequest("POST", "/platform/authentication-policy", strings.NewReader(`{"action":"status"}`))
r.Header.Set("Authorization", "Bearer unsigned")
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != 403 {
t.Fatal("unsigned accepted")
}
}
func mustClaims(token string) map[string]interface{} {
raw, _ := base64.RawURLEncoding.DecodeString(strings.Split(token, ".")[1])
var c map[string]interface{}
json.Unmarshal(raw, &c)
return c
}