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:
@@ -4,6 +4,8 @@ import (
|
||||
"io"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/emil28092005/SciMesh/coordinator/internal/domain"
|
||||
)
|
||||
|
||||
// Use-case boundary types. Adapters map their wire formats onto these, so the
|
||||
@@ -28,6 +30,11 @@ type ChunkInput struct {
|
||||
type RegisterWorkerInput struct {
|
||||
Name string
|
||||
Capabilities []string
|
||||
// OwnerID is the userservice user registering this worker; nil for a
|
||||
// shared-token registration. TrustLevel is resolved by the transport layer
|
||||
// from how the caller authenticated.
|
||||
OwnerID *uuid.UUID
|
||||
TrustLevel domain.WorkerTrust
|
||||
}
|
||||
|
||||
type ClaimTaskInput struct {
|
||||
|
||||
@@ -51,6 +51,13 @@ func (uc *ClaimTask) Execute(ctx context.Context, in ClaimTaskInput) (*domain.Cl
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// C1 quarantine: an untrusted volunteer worker may register but receives
|
||||
// no tasks, because there is not yet (until quorum, C2) any way to verify
|
||||
// its results. Report an empty queue rather than an error, so its poller
|
||||
// simply idles.
|
||||
if worker.TrustLevel == domain.WorkerUntrusted {
|
||||
return nil, nil
|
||||
}
|
||||
// Never trust caller-supplied capabilities: registration is the durable
|
||||
// worker identity and its allowlist.
|
||||
workloads = worker.Capabilities
|
||||
|
||||
@@ -265,6 +265,71 @@ func TestClaimEmptyQueueReturnsNil(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterWorkerDefaultsToTrusted(t *testing.T) {
|
||||
h := newHarness()
|
||||
// A shared-token registration carries no owner and no explicit trust.
|
||||
w, err := h.register.Execute(ctx, usecase.RegisterWorkerInput{
|
||||
Name: "lab", Capabilities: []string{"w"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if w.TrustLevel != domain.WorkerTrusted {
|
||||
t.Errorf("trust = %q, want trusted", w.TrustLevel)
|
||||
}
|
||||
if w.OwnerID != nil {
|
||||
t.Errorf("owner = %v, want nil for a shared-token worker", w.OwnerID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterWorkerRecordsOwnerAndUntrusted(t *testing.T) {
|
||||
h := newHarness()
|
||||
owner := uuid.New()
|
||||
w, err := h.register.Execute(ctx, usecase.RegisterWorkerInput{
|
||||
Name: "volunteer", Capabilities: []string{"w"},
|
||||
OwnerID: &owner, TrustLevel: domain.WorkerUntrusted,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if w.TrustLevel != domain.WorkerUntrusted {
|
||||
t.Errorf("trust = %q, want untrusted", w.TrustLevel)
|
||||
}
|
||||
if w.OwnerID == nil || *w.OwnerID != owner {
|
||||
t.Errorf("owner = %v, want %v", w.OwnerID, owner)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUntrustedWorkerIsQuarantinedFromClaims(t *testing.T) {
|
||||
h := newHarness()
|
||||
h.seedJob(t, "w", 1) // a task is waiting
|
||||
owner := uuid.New()
|
||||
worker, err := h.register.Execute(ctx, usecase.RegisterWorkerInput{
|
||||
Name: "volunteer", Capabilities: []string{"w"},
|
||||
OwnerID: &owner, TrustLevel: domain.WorkerUntrusted,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Even with a matching task available, an untrusted worker gets nothing:
|
||||
// its results cannot be verified until quorum (C2) exists.
|
||||
claimed, err := h.claim.Execute(ctx, usecase.ClaimTaskInput{WorkerID: worker.ID.String()})
|
||||
if err != nil {
|
||||
t.Fatalf("claim: %v", err)
|
||||
}
|
||||
if claimed != nil {
|
||||
t.Error("untrusted worker must receive no task (quarantine)")
|
||||
}
|
||||
|
||||
// A trusted worker still drains the same queue.
|
||||
trusted, _ := h.register.Execute(ctx, usecase.RegisterWorkerInput{Name: "lab", Capabilities: []string{"w"}})
|
||||
got, err := h.claim.Execute(ctx, usecase.ClaimTaskInput{WorkerID: trusted.ID.String()})
|
||||
if err != nil || got == nil {
|
||||
t.Fatalf("trusted claim = (%v, %v), want a task", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaimRequiresWorkerID(t *testing.T) {
|
||||
h := newHarness()
|
||||
if _, err := h.claim.Execute(ctx, usecase.ClaimTaskInput{}); !errors.Is(err, domain.ErrInvalidInput) {
|
||||
|
||||
@@ -22,6 +22,13 @@ func (uc *RegisterWorker) Execute(ctx context.Context, in RegisterWorkerInput) (
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w.OwnerID = in.OwnerID
|
||||
// The transport layer resolves trust from the caller's credentials; fall
|
||||
// back to the domain default (trusted) only when it was left unset, so a
|
||||
// zero-value input never silently downgrades a shared-token worker.
|
||||
if in.TrustLevel != "" {
|
||||
w.TrustLevel = in.TrustLevel
|
||||
}
|
||||
if err := uc.workers.Insert(ctx, w); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user