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.
124 lines
3.2 KiB
Go
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
|
|
}
|