conductor

CI task system
git clone git://git.finwo.net/app/conductor
Log | Files | Refs | README | LICENSE

workers.js (3324B)


      1 // src/lib/workers.js - worker registration tokens
      2 //
      3 // A worker authenticates with a bearer token. Only the SHA-256 of the token
      4 // is stored, so the database never holds a usable credential; the plaintext
      5 // is shown once at creation and cannot be recovered afterwards.
      6 //
      7 // A token may belong to a user, in which case it is only ever offered jobs
      8 // from that user's projects. A token with no owner is shared and can run
      9 // anything, which is how an administrator provides general capacity. That
     10 // distinction is enforced in the scheduler, not here.
     11 
     12 import { newToken, hashToken, newWorkerTokenId } from './ids.js';
     13 
     14 export function createWorkerTokens({ db }) {
     15   return {
     16     async create(name, { ownerId = null } = {}) {
     17       if (!name || typeof name !== 'string') throw new Error('worker token needs a name');
     18       const token = newToken();
     19       const id = newWorkerTokenId();
     20       await db.run(
     21         `INSERT INTO worker_tokens (id, name, token_hash, enabled, owner_id, created_at)
     22          VALUES ({id}, {name}, {hash}, 1, {owner}, {now})`,
     23         { id, name, hash: hashToken(token), owner: ownerId, now: Date.now() }
     24       );
     25       // The only time the plaintext exists outside the worker.
     26       return { id, name, token, owner_id: ownerId };
     27     },
     28 
     29     async get(id) {
     30       return db.get(
     31         'SELECT id, name, enabled, owner_id, created_at, last_seen_at, last_ip FROM worker_tokens WHERE id = {id}',
     32         { id }
     33       );
     34     },
     35 
     36     // Returns the token record, or null. Lookup is by hash, so a timing
     37     // difference cannot reveal anything beyond whether a hash exists.
     38     async verify(presented, { ip = null } = {}) {
     39       if (typeof presented !== 'string' || presented.length === 0) return null;
     40       const hash = hashToken(presented);
     41       const row = await db.get(
     42         'SELECT id, name, enabled, owner_id FROM worker_tokens WHERE token_hash = {hash}',
     43         { hash }
     44       );
     45       if (!row || row.enabled !== 1) return null;
     46 
     47       await db.run(
     48         'UPDATE worker_tokens SET last_seen_at = {now}, last_ip = {ip} WHERE id = {id}',
     49         { id: row.id, now: Date.now(), ip }
     50       );
     51       return { id: row.id, name: row.name, owner_id: row.owner_id };
     52     },
     53 
     54     async list() {
     55       return db.all(
     56         `SELECT id, name, enabled, owner_id, created_at, last_seen_at, last_ip
     57            FROM worker_tokens ORDER BY created_at DESC`
     58       );
     59     },
     60 
     61     async listVisible(user) {
     62       if (user && user.role === 'admin') return this.list();
     63       if (!user) return [];
     64       return db.all(
     65         `SELECT id, name, enabled, owner_id, created_at, last_seen_at, last_ip
     66            FROM worker_tokens WHERE owner_id = {owner} ORDER BY created_at DESC`,
     67         { owner: user.id }
     68       );
     69     },
     70 
     71     async setEnabled(id, enabled) {
     72       const res = await db.run(
     73         'UPDATE worker_tokens SET enabled = {enabled} WHERE id = {id}',
     74         { id, enabled: enabled ? 1 : 0 }
     75       );
     76       return res.changes > 0;
     77     },
     78 
     79     async remove(id) {
     80       const res = await db.run('DELETE FROM worker_tokens WHERE id = {id}', { id });
     81       return res.changes > 0;
     82     },
     83   };
     84 }
     85 
     86 export function canManageWorker(user, token) {
     87   if (!user || !token) return false;
     88   if (user.role === 'admin') return true;
     89   return token.owner_id !== null && token.owner_id === user.id;
     90 }