Files
SciMesh/users/internal/storage/postgres/workerkey_repo.go
T
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

124 lines
3.2 KiB
Go

package postgres
import (
"context"
"errors"
sq "github.com/Masterminds/squirrel"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/emil28092005/SciMesh/users/internal/domain"
"github.com/emil28092005/SciMesh/users/internal/usecase"
)
var workerKeyColumns = []string{
"id", "user_id", "name", "token_hash", "prefix", "created_at", "last_used_at", "revoked_at",
}
// WorkerKeyRepo implements usecase.WorkerKeyRepository on PostgreSQL.
type WorkerKeyRepo struct {
pool *pgxpool.Pool
}
func NewWorkerKeyRepo(pool *pgxpool.Pool) *WorkerKeyRepo {
return &WorkerKeyRepo{pool: pool}
}
func (r *WorkerKeyRepo) Insert(ctx context.Context, k *domain.WorkerKey) error {
sql, args, err := psql.Insert("worker_keys").
Columns(workerKeyColumns...).
Values(k.ID, k.UserID, k.Name, k.TokenHash, k.Prefix, k.CreatedAt, k.LastUsedAt, k.RevokedAt).
ToSql()
if err != nil {
return err
}
_, err = conn(ctx, r.pool).Exec(ctx, sql, args...)
return err
}
func (r *WorkerKeyRepo) ListByUser(ctx context.Context, userID uuid.UUID) ([]*domain.WorkerKey, error) {
sql, args, err := psql.Select(workerKeyColumns...).
From("worker_keys").
Where(sq.Eq{"user_id": userID, "revoked_at": nil}).
OrderBy("created_at DESC").
ToSql()
if err != nil {
return nil, err
}
rows, err := conn(ctx, r.pool).Query(ctx, sql, args...)
if err != nil {
return nil, err
}
defer rows.Close()
keys := []*domain.WorkerKey{}
for rows.Next() {
k, err := scanWorkerKey(rows)
if err != nil {
return nil, err
}
keys = append(keys, k)
}
return keys, rows.Err()
}
func (r *WorkerKeyRepo) GetActiveByHash(ctx context.Context, tokenHash string) (*domain.WorkerKey, error) {
sql, args, err := psql.Select(workerKeyColumns...).
From("worker_keys").
Where(sq.Eq{"token_hash": tokenHash, "revoked_at": nil}).
ToSql()
if err != nil {
return nil, err
}
return scanWorkerKey(conn(ctx, r.pool).QueryRow(ctx, sql, args...))
}
// Revoke retires a live key the user owns. Scoping the UPDATE to both id and
// user_id means one user can never revoke another's key, and the revoked_at IS
// NULL guard makes a double-revoke a clean 404 rather than a silent success.
func (r *WorkerKeyRepo) Revoke(ctx context.Context, id, userID uuid.UUID) error {
sql, args, err := psql.Update("worker_keys").
Set("revoked_at", sq.Expr("now()")).
Where(sq.Eq{"id": id, "user_id": userID, "revoked_at": nil}).
ToSql()
if err != nil {
return err
}
tag, err := conn(ctx, r.pool).Exec(ctx, sql, args...)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return usecase.ErrWorkerKeyNotFound
}
return nil
}
func (r *WorkerKeyRepo) TouchLastUsed(ctx context.Context, id uuid.UUID) error {
sql, args, err := psql.Update("worker_keys").
Set("last_used_at", sq.Expr("now()")).
Where(sq.Eq{"id": id}).
ToSql()
if err != nil {
return err
}
_, err = conn(ctx, r.pool).Exec(ctx, sql, args...)
return err
}
func scanWorkerKey(row pgx.Row) (*domain.WorkerKey, error) {
var k domain.WorkerKey
if err := row.Scan(
&k.ID, &k.UserID, &k.Name, &k.TokenHash, &k.Prefix,
&k.CreatedAt, &k.LastUsedAt, &k.RevokedAt,
); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, usecase.ErrWorkerKeyNotFound
}
return nil, err
}
return &k, nil
}