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
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>
67 lines
1.6 KiB
Go
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()
|
|
}
|