ProofedMesh/services/dummy-sink/internal/auth/sa_token.go
CTO (Paperclip) b45ad34169
All checks were successful
validate / JSON Schema (ajv) (push) Successful in 19s
build-images / signing-service image (push) Successful in 22s
validate / Rego (opa fmt + opa test) (push) Successful in 13s
validate / DB (migrate up/down dry-run) (push) Successful in 26s
validate / Go (vet + test) (push) Successful in 1m13s
dummy-sink: Phase-1 mock sink (sink:dummy-sink) (DEV-611)
Trivial Senken-Adapter per §7 of architecture V1.1. Accepts
{decision_id, payload_hash, payload, approval_ref, transform_ref}
on POST /api/v1/sink/dummy/deliver; calls verification-service's
/api/v1/verify/chain and branches:

- valid=true → atomic .tmp+rename write of payload to
  /data/dummy-sink/<decision_id>.json, builds a
  basicstack.de/attestations/handover/v1 statement, signs via
  signing-service with the double identity flow (Pocket-ID bearer
  + projected SA token), submits DSSE envelope to the TLog and
  returns 201 {handover_ref, log_index}.
- valid=false & replay=true → 409.
- valid=false → 403 {reasons:[…]}.

Replay protection lives in verification-service per the parent plan;
dummy-sink keeps no local replay store. The payload_hash field is
verified against both the raw and canonical form so a verifier can
reproduce either hash.

Tests use httptest-backed fakes for verify / sign / tlog and cover
the DEV-611 acceptance criteria plus the bad-request shapes:
- happy path writes the file, calls signer with the handover
  statement, submits the envelope to the TLog
- 403 keeps the disk and never calls signer/tlog
- replay returns 409 without re-signing
- payload_hash mismatch rejects before verify
- malformed bodies return 400
- verify unavailable → 502, signing denied → 403

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-25 20:20:40 +00:00

67 lines
1.6 KiB
Go

package auth
import (
"context"
"fmt"
"os"
"strings"
"sync"
"time"
)
// ProjectedSATokenSource reads the kubelet-written projected
// ServiceAccount token from a mounted Volume (typically
// `/var/run/secrets/tokens/trust-api`) and re-reads it on each call so
// kubelet's periodic refresh is picked up without a service restart.
//
// A short in-process cache avoids a syscall on every signing-service call;
// kubelet refreshes projected tokens well before their `exp`, so a few
// seconds of staleness is fine. Set CacheTTL negative to disable.
type ProjectedSATokenSource struct {
Path string
CacheTTL time.Duration
Now func() time.Time
mu sync.Mutex
cachedToken string
cachedAt time.Time
}
// Token returns the current projected ServiceAccount token.
func (p *ProjectedSATokenSource) Token(_ context.Context) (string, error) {
if p == nil {
return "", fmt.Errorf("auth.ProjectedSATokenSource: nil receiver")
}
if p.Path == "" {
return "", fmt.Errorf("auth.ProjectedSATokenSource: Path is required")
}
p.mu.Lock()
defer p.mu.Unlock()
ttl := p.CacheTTL
if ttl == 0 {
ttl = 30 * time.Second
}
now := p.now()
if ttl > 0 && p.cachedToken != "" && now.Sub(p.cachedAt) < ttl {
return p.cachedToken, nil
}
raw, err := os.ReadFile(p.Path)
if err != nil {
return "", fmt.Errorf("read projected SA token %q: %w", p.Path, err)
}
tok := strings.TrimSpace(string(raw))
if tok == "" {
return "", fmt.Errorf("projected SA token at %q is empty", p.Path)
}
p.cachedToken = tok
p.cachedAt = now
return tok, nil
}
func (p *ProjectedSATokenSource) now() time.Time {
if p.Now != nil {
return p.Now()
}
return time.Now()
}