fix(coordinator): bind JWT caller to worker at claim (close quarantine bypass)
The trust tier was read off the caller-supplied worker_id, so a JWT user who knew any trusted worker's id could claim as it — draining and poisoning the trusted queue and bypassing the untrusted-worker quarantine entirely. Claim now requires a JWT caller to own the worker it acts as; a shared-token caller (lab operator) may still act as any worker. Claim is the sole grantor of a lease, so this also protects the downstream heartbeat/result/failure paths. Tests: reject claim as another user's worker; allow claim as own worker.
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/emil28092005/SciMesh/coordinator/internal/authctx"
|
||||
"github.com/emil28092005/SciMesh/coordinator/internal/domain"
|
||||
)
|
||||
|
||||
@@ -51,6 +52,18 @@ func (uc *ClaimTask) Execute(ctx context.Context, in ClaimTaskInput) (*domain.Cl
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Bind the caller to the worker it claims as. A JWT-authenticated
|
||||
// volunteer may operate only its own workers; without this the trust
|
||||
// tier would be read off a caller-supplied worker_id, letting anyone who
|
||||
// knows a trusted worker's id claim as it and bypass the quarantine
|
||||
// below. A shared-token caller (no requester) is a lab operator and may
|
||||
// act as any worker, preserving the original behaviour.
|
||||
if r, ok := authctx.From(ctx); ok {
|
||||
if worker.OwnerID == nil || *worker.OwnerID != r.UserID {
|
||||
// Don't disclose that another user's worker exists.
|
||||
return nil, domain.ErrWorkerNotFound
|
||||
}
|
||||
}
|
||||
// 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
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/emil28092005/SciMesh/coordinator/internal/authctx"
|
||||
"github.com/emil28092005/SciMesh/coordinator/internal/domain"
|
||||
"github.com/emil28092005/SciMesh/coordinator/internal/memstore"
|
||||
"github.com/emil28092005/SciMesh/coordinator/internal/usecase"
|
||||
@@ -300,6 +301,38 @@ func TestRegisterWorkerRecordsOwnerAndUntrusted(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWTCallerCannotClaimAsAnotherUsersWorker(t *testing.T) {
|
||||
h := newHarness()
|
||||
h.seedJob(t, "w", 1)
|
||||
|
||||
// A trusted lab worker owned by nobody (shared-token registration).
|
||||
victim, _ := h.register.Execute(ctx, usecase.RegisterWorkerInput{Name: "lab", Capabilities: []string{"w"}})
|
||||
|
||||
// An attacker authenticated as a JWT user tries to claim as the lab worker.
|
||||
attacker := authctx.With(ctx, authctx.Requester{UserID: uuid.New(), Role: "user"})
|
||||
claimed, err := h.claim.Execute(attacker, usecase.ClaimTaskInput{WorkerID: victim.ID.String()})
|
||||
if !errors.Is(err, domain.ErrWorkerNotFound) {
|
||||
t.Fatalf("claim as another's worker = (%v, %v), want ErrWorkerNotFound", claimed, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWTCallerClaimsAsOwnTrustedWorker(t *testing.T) {
|
||||
h := newHarness()
|
||||
h.seedJob(t, "w", 1)
|
||||
owner := uuid.New()
|
||||
|
||||
// The user's own worker, trusted (e.g. a verified contributor).
|
||||
mine, _ := h.register.Execute(ctx, usecase.RegisterWorkerInput{
|
||||
Name: "mine", Capabilities: []string{"w"}, OwnerID: &owner, TrustLevel: domain.WorkerTrusted,
|
||||
})
|
||||
|
||||
callerCtx := authctx.With(ctx, authctx.Requester{UserID: owner, Role: "user", Verified: true})
|
||||
got, err := h.claim.Execute(callerCtx, usecase.ClaimTaskInput{WorkerID: mine.ID.String()})
|
||||
if err != nil || got == nil {
|
||||
t.Fatalf("own trusted worker claim = (%v, %v), want a task", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUntrustedWorkerIsQuarantinedFromClaims(t *testing.T) {
|
||||
h := newHarness()
|
||||
h.seedJob(t, "w", 1) // a task is waiting
|
||||
|
||||
Reference in New Issue
Block a user