Files
Efremenko Arhip 3a1461315f feat: self-service worker enrollment bound to a user account
Let a signed-in user turn their own machine into a worker without the
shared token. The coordinator already binds a JWT-authenticated
registration to owner_id as untrusted; this adds the missing pieces.

userservice: long-lived worker keys (scimesh_wk_live_*, hash-at-rest)
with create/list/revoke and a public /worker-tokens/exchange that trades
a key for a short-lived JWT carrying the owner current role/verified.

python worker: SCIMESH_WORKER_KEY + SCIMESH_USERSERVICE_URL; a token
provider exchanges the key and refreshes the JWT proactively and on 401,
so a long-running worker survives token expiry. Static bearer token path
is unchanged.

coordinator UI: an "add your machine" page that mints a key and shows a
ready-to-run command, proxying key management to the userservice; the
dashboard gains an owner-scoped "my machines" section.

docs: how to run a worker from your account, plus the untrusted/quorum/
verified trust model.
2026-07-27 16:11:07 +03:00

113 lines
3.2 KiB
Go

package usecase
import (
"context"
"errors"
"time"
"github.com/google/uuid"
"github.com/emil28092005/SciMesh/users/internal/domain"
)
// CreateWorkerKey mints a long-lived worker key for a user and returns the
// one-time plaintext to show once.
type CreateWorkerKey struct {
keys WorkerKeyRepository
clock Clock
}
func NewCreateWorkerKey(keys WorkerKeyRepository, clock Clock) *CreateWorkerKey {
return &CreateWorkerKey{keys: keys, clock: clock}
}
// Execute returns the stored key (hash only) and the plaintext secret. The
// secret is never persisted, so this is the sole moment it can be surfaced.
func (uc *CreateWorkerKey) Execute(ctx context.Context, userID uuid.UUID, name string) (*domain.WorkerKey, string, error) {
key, raw, err := domain.NewWorkerKey(userID, name, uc.clock.Now())
if err != nil {
return nil, "", err
}
if err := uc.keys.Insert(ctx, key); err != nil {
return nil, "", err
}
return key, raw, nil
}
// ListWorkerKeys returns a user's live keys for display and management.
type ListWorkerKeys struct {
keys WorkerKeyRepository
}
func NewListWorkerKeys(keys WorkerKeyRepository) *ListWorkerKeys {
return &ListWorkerKeys{keys: keys}
}
func (uc *ListWorkerKeys) Execute(ctx context.Context, userID uuid.UUID) ([]*domain.WorkerKey, error) {
return uc.keys.ListByUser(ctx, userID)
}
// RevokeWorkerKey retires one of the caller's keys.
type RevokeWorkerKey struct {
keys WorkerKeyRepository
}
func NewRevokeWorkerKey(keys WorkerKeyRepository) *RevokeWorkerKey {
return &RevokeWorkerKey{keys: keys}
}
func (uc *RevokeWorkerKey) Execute(ctx context.Context, userID, id uuid.UUID) error {
return uc.keys.Revoke(ctx, id, userID)
}
// ExchangeWorkerKey trades a valid worker key for a short-lived JWT. The JWT
// carries the owner's current role and verified flag, so a worker that refreshes
// after an admin verifies the owner picks up the upgraded trust on its next
// registration.
type ExchangeWorkerKey struct {
keys WorkerKeyRepository
users UserRepository
tokens TokenIssuer
ttl time.Duration
}
func NewExchangeWorkerKey(keys WorkerKeyRepository, users UserRepository, tokens TokenIssuer, ttl time.Duration) *ExchangeWorkerKey {
return &ExchangeWorkerKey{keys: keys, users: users, tokens: tokens, ttl: ttl}
}
// Execute returns a signed token and its lifetime in seconds. Every failure to
// resolve the key to a usable owner collapses to ErrInvalidWorkerKey so a caller
// cannot tell an unknown key from a revoked one or a deleted owner.
func (uc *ExchangeWorkerKey) Execute(ctx context.Context, rawKey string) (string, int, error) {
if rawKey == "" {
return "", 0, ErrInvalidWorkerKey
}
key, err := uc.keys.GetActiveByHash(ctx, domain.HashWorkerKey(rawKey))
if err != nil {
if errors.Is(err, ErrWorkerKeyNotFound) {
return "", 0, ErrInvalidWorkerKey
}
return "", 0, err
}
u, err := uc.users.GetByID(ctx, key.UserID)
if err != nil {
if errors.Is(err, ErrUserNotFound) {
return "", 0, ErrInvalidWorkerKey
}
return "", 0, err
}
token, err := uc.tokens.Issue(u)
if err != nil {
return "", 0, err
}
// Best-effort: a failed timestamp update must not sink an otherwise valid
// exchange the worker depends on to keep running.
_ = uc.keys.TouchLastUsed(ctx, key.ID)
return token, int(uc.ttl.Seconds()), nil
}