feat(coordinator): worker trust tiers (C1) — enroll volunteers, quarantine untrusted
- migration 0012: workers.owner_id + trust_level (trusted/untrusted) - verifier/authctx read the JWT verified claim; IsTrusted() = admin||verified - /workers/register resolves trust from auth: service token or verified/admin JWT -> trusted; plain user JWT -> untrusted, tagged with owner_id - claim quarantines untrusted workers (no tasks) until quorum (C2) lands - unit tests for trust resolution, quarantine, and the verified claim Additive and backward compatible: shared-token workers stay trusted, so the existing worker flow and team tests are unchanged. Quorum verification (C2) is deferred.
This commit is contained in:
@@ -13,8 +13,9 @@ import (
|
||||
|
||||
// Claims is the subset of a userservice token the coordinator cares about.
|
||||
type Claims struct {
|
||||
UserID uuid.UUID
|
||||
Role string
|
||||
UserID uuid.UUID
|
||||
Role string
|
||||
Verified bool
|
||||
}
|
||||
|
||||
// Verifier checks tokens against the shared HS256 secret.
|
||||
@@ -32,7 +33,8 @@ func NewVerifier(secret string) *Verifier {
|
||||
}
|
||||
|
||||
type claims struct {
|
||||
Role string `json:"role"`
|
||||
Role string `json:"role"`
|
||||
Verified bool `json:"verified"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
@@ -54,5 +56,5 @@ func (v *Verifier) Verify(raw string) (Claims, error) {
|
||||
if err != nil {
|
||||
return Claims{}, fmt.Errorf("token subject is not a uuid: %w", err)
|
||||
}
|
||||
return Claims{UserID: id, Role: c.Role}, nil
|
||||
return Claims{UserID: id, Role: c.Role, Verified: c.Verified}, nil
|
||||
}
|
||||
|
||||
@@ -11,9 +11,15 @@ import (
|
||||
const secret = "coordinator-verify-secret-32-bytes!!"
|
||||
|
||||
func sign(t *testing.T, method jwt.SigningMethod, key any, sub, role string, exp time.Time) string {
|
||||
t.Helper()
|
||||
return signVerified(t, method, key, sub, role, false, exp)
|
||||
}
|
||||
|
||||
func signVerified(t *testing.T, method jwt.SigningMethod, key any, sub, role string, verified bool, exp time.Time) string {
|
||||
t.Helper()
|
||||
tok := jwt.NewWithClaims(method, claims{
|
||||
Role: role,
|
||||
Role: role,
|
||||
Verified: verified,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Subject: sub,
|
||||
ExpiresAt: jwt.NewNumericDate(exp),
|
||||
@@ -26,6 +32,19 @@ func sign(t *testing.T, method jwt.SigningMethod, key any, sub, role string, exp
|
||||
return raw
|
||||
}
|
||||
|
||||
func TestVerifyCarriesVerifiedClaim(t *testing.T) {
|
||||
v := NewVerifier(secret)
|
||||
raw := signVerified(t, jwt.SigningMethodHS256, []byte(secret), uuid.New().String(), "user", true, time.Now().Add(time.Hour))
|
||||
|
||||
claims, err := v.Verify(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("verify: %v", err)
|
||||
}
|
||||
if !claims.Verified {
|
||||
t.Error("verified claim not read from token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewVerifierNilWhenNoSecret(t *testing.T) {
|
||||
if NewVerifier("") != nil {
|
||||
t.Error("empty secret must yield a nil verifier (auth disabled)")
|
||||
|
||||
Reference in New Issue
Block a user