commit e389cb209dfe2728f5f1d0fde102099a77f58aff
parent 1a5080ded7478813abcf176ecc86ee6c9fbcfcf7
Author: finwo <finwo@pm.me>
Date: Sun, 27 Sep 2026 19:42:50 +0200
Cover owner-scoped worker dispatch and what deleting a user takes with it
Diffstat:
2 files changed, 218 insertions(+), 319 deletions(-)
diff --git a/test/ownership.test.js b/test/ownership.test.js
@@ -0,0 +1,218 @@
+// test/ownership.test.js - who owns what, and what that decides
+
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { startHarness, data, apiError } from './helpers/harness.js';
+
+async function withHarness(options, fn) {
+ const h = await startHarness({ bootstrap: true, ...options });
+ try {
+ await fn(h);
+ } finally {
+ await h.stop();
+ }
+}
+
+const json = { 'content-type': 'application/json' };
+
+async function addUser(h, admin, username, role = 'user') {
+ const password = `${username}-password`;
+ const created = await h.app.inject({
+ method: 'POST',
+ url: '/api/v1/users',
+ headers: { ...admin, ...json },
+ payload: JSON.stringify({ username, password, role }),
+ });
+ assert.equal(created.statusCode, 201, created.body);
+
+ const grant = await h.login(username, password);
+ return { user: data(created), headers: grant.headers };
+}
+
+function claimAs(h, token, body = {}) {
+ return h.app.inject({
+ method: 'POST',
+ url: '/api/v1/tasks/claim',
+ headers: { authorization: `Bearer ${token}`, ...json },
+ payload: JSON.stringify(body),
+ });
+}
+
+async function queueWork(h) {
+ const res = await h.trigger({ sha: h.sha, ref: 'refs/heads/main' });
+ const jobId = data(res).job_id;
+ await h.waitForJob(jobId);
+ return jobId;
+}
+
+test("a worker belonging to a user is not offered another user's work", async () => {
+ await withHarness({ visibility: 'private' }, async (h) => {
+ const { headers: admin } = await h.login();
+ const alice = await addUser(h, admin, 'alice');
+ const bob = await addUser(h, admin, 'bob');
+
+ await h.services.projects.setOwner('demo', alice.user.id);
+ const forAlice = await h.services.workerTokens.create('alice-pi', { ownerId: alice.user.id });
+ const forBob = await h.services.workerTokens.create('bob-pi', { ownerId: bob.user.id });
+
+ await queueWork(h);
+
+ assert.equal((await claimAs(h, forBob.token)).statusCode, 204);
+
+ const claimed = await claimAs(h, forAlice.token);
+ assert.equal(claimed.statusCode, 200, claimed.body);
+ assert.equal(data(claimed).task.name, 'lint');
+ });
+});
+
+test('a worker with no owner is offered work from any project', async () => {
+ await withHarness({ visibility: 'private' }, async (h) => {
+ const { headers: admin } = await h.login();
+ const alice = await addUser(h, admin, 'alice');
+ await h.services.projects.setOwner('demo', alice.user.id);
+
+ const shared = await h.services.workerTokens.create('pool', { ownerId: null });
+ await queueWork(h);
+
+ const claimed = await claimAs(h, shared.token);
+ assert.equal(claimed.statusCode, 200, claimed.body);
+ });
+});
+
+test('a project a user creates belongs to them and a stranger cannot touch it', async () => {
+ await withHarness({}, async (h) => {
+ const { headers: admin } = await h.login();
+ const alice = await addUser(h, admin, 'alice');
+ const bob = await addUser(h, admin, 'bob');
+
+ const created = await h.app.inject({
+ method: 'POST',
+ url: '/api/v1/projects',
+ headers: { ...alice.headers, ...json },
+ payload: JSON.stringify({ id: 'alice-app', repo_url: 'https://git.example.com/a.git' }),
+ });
+ assert.equal(created.statusCode, 201, created.body);
+ assert.equal(data(created).owner_id, alice.user.id);
+
+ const listed = await h.app.inject({ method: 'GET', url: '/api/v1/projects', headers: bob.headers });
+ assert.ok(!data(listed).some((p) => p.id === 'alice-app'));
+
+ const removed = await h.app.inject({
+ method: 'DELETE', url: '/api/v1/projects/alice-app', headers: bob.headers,
+ });
+ assert.equal(removed.statusCode, 404, removed.body);
+ });
+});
+
+test('a worker a user registers is invisible to another user', async () => {
+ await withHarness({}, async (h) => {
+ const { headers: admin } = await h.login();
+ const alice = await addUser(h, admin, 'alice');
+ const bob = await addUser(h, admin, 'bob');
+
+ const token = await h.app.inject({
+ method: 'POST',
+ url: '/api/v1/workers',
+ headers: { ...bob.headers, ...json },
+ payload: JSON.stringify({ name: 'bob-laptop' }),
+ });
+ assert.equal(token.statusCode, 201, token.body);
+ assert.equal(data(token).owner_id, bob.user.id);
+
+ const listed = await h.app.inject({ method: 'GET', url: '/api/v1/workers', headers: alice.headers });
+ assert.deepEqual(data(listed).map((w) => w.name), []);
+
+ const probed = await h.app.inject({
+ method: 'GET', url: `/api/v1/workers/${data(token).id}`, headers: alice.headers,
+ });
+ assert.equal(probed.statusCode, 404, probed.body);
+ });
+});
+
+test('an ordinary user asking for shared capacity gets a personal worker', async () => {
+ await withHarness({}, async (h) => {
+ const { headers: admin } = await h.login();
+ const alice = await addUser(h, admin, 'alice');
+
+ const sneaky = await h.app.inject({
+ method: 'POST',
+ url: '/api/v1/workers',
+ headers: { ...alice.headers, ...json },
+ payload: JSON.stringify({ name: 'sneaky', owner_id: null }),
+ });
+ assert.equal(data(sneaky).owner_id, alice.user.id);
+
+ const pool = await h.app.inject({
+ method: 'POST',
+ url: '/api/v1/workers',
+ headers: { ...admin, ...json },
+ payload: JSON.stringify({ name: 'pool', owner_id: null }),
+ });
+ assert.equal(data(pool).owner_id, null);
+ });
+});
+
+test('only an administrator can reassign a project', async () => {
+ await withHarness({}, async (h) => {
+ const { headers: admin } = await h.login();
+ const alice = await addUser(h, admin, 'alice');
+ await h.services.projects.setOwner('demo', alice.user.id);
+
+ const refused = await h.app.inject({
+ method: 'PATCH',
+ url: '/api/v1/projects/demo',
+ headers: { ...alice.headers, ...json },
+ payload: JSON.stringify({ owner_id: null }),
+ });
+ assert.equal(refused.statusCode, 403, refused.body);
+ assert.equal(apiError(refused).name, 'NotPermitted');
+
+ const allowed = await h.app.inject({
+ method: 'PATCH',
+ url: '/api/v1/projects/demo',
+ headers: { ...admin, ...json },
+ payload: JSON.stringify({ owner_id: null }),
+ });
+ assert.equal(allowed.statusCode, 200, allowed.body);
+ assert.equal(data(allowed).owner_id, null);
+ });
+});
+
+test('deleting a user takes their projects and workers with them', async () => {
+ await withHarness({}, async (h) => {
+ const { headers: admin } = await h.login();
+ const alice = await addUser(h, admin, 'alice');
+
+ await h.services.projects.setOwner('demo', alice.user.id);
+ const token = await h.services.workerTokens.create('alice-pi', { ownerId: alice.user.id });
+ const jobId = await queueWork(h);
+
+ const removed = await h.app.inject({
+ method: 'DELETE', url: `/api/v1/users/${alice.user.id}`, headers: admin,
+ });
+ assert.equal(removed.statusCode, 200, removed.body);
+
+ assert.equal(await h.services.projects.get('demo'), undefined);
+ assert.equal(await h.services.workerTokens.get(token.id), undefined);
+ assert.equal(await h.services.db.get('SELECT id FROM jobs WHERE id = {id}', { id: jobId }), undefined);
+ });
+});
+
+test('a worker token does not outlive its owner and become shared capacity', async () => {
+ await withHarness({}, async (h) => {
+ const { headers: admin } = await h.login();
+ const alice = await addUser(h, admin, 'alice');
+ const token = await h.services.workerTokens.create('alice-pi', { ownerId: alice.user.id });
+
+ await h.app.inject({ method: 'DELETE', url: `/api/v1/users/${alice.user.id}`, headers: admin });
+
+ const rows = await h.services.db.all(
+ 'SELECT id FROM worker_tokens WHERE id = {id}', { id: token.id }
+ );
+ assert.deepEqual(rows, []);
+
+ const claimed = await claimAs(h, token.token);
+ assert.equal(claimed.statusCode, 401, claimed.body);
+ assert.equal(apiError(claimed).name, 'InvalidWorkerToken');
+ });
+});
diff --git a/test/visibility.test.js.disabled b/test/visibility.test.js.disabled
@@ -1,319 +0,0 @@
-// test/visibility.test.js - who can see and run what
-//
-// Two rules are enforced here, and both matter for letting strangers use
-// the same installation:
-//
-// A run is private unless its project or its pipeline says otherwise.
-// An anonymous visitor sees only public runs; a user additionally sees
-// their own; an administrator sees everything.
-//
-// A worker registered by a user is only ever offered that user's jobs.
-
-import test from 'node:test';
-import assert from 'node:assert/strict';
-import { startHarness } from './helpers/harness.js';
-
-async function withHarness(options, fn) {
- const h = await startHarness({ bootstrap: true, ...options });
- try {
- return await fn(h);
- } finally {
- await h.stop();
- }
-}
-
-// Creates a user and returns their credentials and identity.
-async function addUser(h, admin, username, role = 'viewer') {
- const created = await h.app.inject({
- method: 'POST',
- url: '/api/admin/users',
- headers: { ...admin, 'content-type': 'application/json' },
- payload: JSON.stringify({ username, password: `${username}-password`, role }),
- });
- const user = created.json().user;
-
- const login = await h.app.inject({
- method: 'POST',
- url: '/api/auth/login',
- headers: { 'content-type': 'application/json' },
- payload: JSON.stringify({ username, password: `${username}-password` }),
- });
- return { user, headers: { cookie: login.headers['set-cookie'].split(';')[0] } };
-}
-
-const runIds = (res) => res.json().runs.map((r) => r.id);
-
-test('a private run is invisible to anonymous callers', async () => {
- await withHarness({ visibility: 'private' }, async (h) => {
- const run = (await h.trigger({ sha: h.sha, ref: 'refs/heads/main' })).json().run_id;
-
- assert.deepEqual(runIds(await h.app.inject({ method: 'GET', url: '/api/runs' })), []);
- // The same answer as a run that does not exist, so nothing is leaked
- // by the status code.
- assert.equal((await h.app.inject({ method: 'GET', url: `/api/runs/${run}` })).statusCode, 404);
- });
-});
-
-test('a public run is visible to anonymous callers', async () => {
- await withHarness({ visibility: 'public' }, async (h) => {
- const run = (await h.trigger({ sha: h.sha, ref: 'refs/heads/main' })).json().run_id;
-
- assert.deepEqual(runIds(await h.app.inject({ method: 'GET', url: '/api/runs' })), [run]);
- assert.equal((await h.app.inject({ method: 'GET', url: `/api/runs/${run}` })).statusCode, 200);
- });
-});
-
-test('the pipeline may override the project visibility', async () => {
- const pipeline = `
-version: 1
-visibility: public
-jobs:
- a:
- image: alpine
- script: ['true']
-`;
- await withHarness({ pipeline, visibility: 'private' }, async (h) => {
- const run = (await h.trigger({ sha: h.sha, ref: 'refs/heads/main' })).json().run_id;
-
- // The project is private, but this commit declared itself public.
- const detail = await h.app.inject({ method: 'GET', url: `/api/runs/${run}` });
- assert.equal(detail.statusCode, 200);
- assert.equal(detail.json().run.visibility, 'public');
- });
-});
-
-test('visibility is recorded per run, so a later commit can change it', async () => {
- const open = "version: 1\nvisibility: public\njobs:\n a: { image: alpine, script: ['true'] }\n";
- await withHarness({ pipeline: open, visibility: 'private' }, async (h) => {
- const first = (await h.trigger({ sha: h.sha, ref: 'refs/heads/main' })).json().run_id;
-
- const closed = "version: 1\nvisibility: private\njobs:\n a: { image: alpine, script: ['true'] }\n";
- const sha2 = await h.commit({ '.conductor.yml': closed }, 'go private');
- const second = (await h.trigger({ sha: sha2, ref: 'refs/heads/main' })).json().run_id;
-
- // The old run stays public; the new one does not.
- assert.deepEqual(runIds(await h.app.inject({ method: 'GET', url: '/api/runs' })), [first]);
- assert.equal((await h.app.inject({ method: 'GET', url: `/api/runs/${second}` })).statusCode, 404);
- });
-});
-
-test('an owner sees their private runs, a stranger does not', async () => {
- await withHarness({ visibility: 'private' }, async (h) => {
- const admin = await h.login();
- const owner = await addUser(h, admin, 'owner');
- const stranger = await addUser(h, admin, 'stranger');
-
- await h.services.projects.setOwner('demo', owner.user.id);
- const run = (await h.trigger({ sha: h.sha, ref: 'refs/heads/main' })).json().run_id;
-
- assert.deepEqual(runIds(await h.app.inject({ method: 'GET', url: '/api/runs', headers: owner.headers })), [run]);
- assert.deepEqual(runIds(await h.app.inject({ method: 'GET', url: '/api/runs', headers: stranger.headers })), []);
- // An administrator sees everything.
- assert.deepEqual(runIds(await h.app.inject({ method: 'GET', url: '/api/runs', headers: admin })), [run]);
- });
-});
-
-test('logs and artifacts of a private run are not readable by a stranger', async () => {
- await withHarness({ visibility: 'private' }, async (h) => {
- const admin = await h.login();
- const owner = await addUser(h, admin, 'owner');
- await h.services.projects.setOwner('demo', owner.user.id);
-
- await h.trigger({ sha: h.sha, ref: 'refs/heads/main' });
- const job = (await h.poll({})).json().job;
-
- await h.app.inject({
- method: 'POST',
- url: `/api/workers/jobs/${encodeURIComponent(job.id)}/log`,
- headers: { ...h.auth, 'content-type': 'application/octet-stream' },
- payload: Buffer.from('private build output\n'),
- });
- await h.app.inject({
- method: 'POST',
- url: `/api/workers/jobs/${encodeURIComponent(job.id)}/artifact`,
- headers: { ...h.auth, 'content-type': 'application/octet-stream', 'x-artifact-path': 'secret.bin' },
- payload: Buffer.from('private artifact'),
- });
-
- const jobUrl = `/api/jobs/${encodeURIComponent(job.id)}`;
- assert.equal((await h.app.inject({ method: 'GET', url: jobUrl })).statusCode, 404);
- assert.equal((await h.app.inject({ method: 'GET', url: `${jobUrl}/log` })).statusCode, 404);
-
- const asOwner = await h.app.inject({ method: 'GET', url: jobUrl, headers: owner.headers });
- assert.equal(asOwner.statusCode, 200);
-
- const artifactId = asOwner.json().artifacts[0].id;
- assert.equal((await h.app.inject({ method: 'GET', url: `/api/artifacts/${artifactId}` })).statusCode, 404);
- const download = await h.app.inject({ method: 'GET', url: `/api/artifacts/${artifactId}`, headers: owner.headers });
- assert.equal(download.body, 'private artifact');
- });
-});
-
-test("a worker belonging to a user only receives that owner's projects", async () => {
- await withHarness({ visibility: 'private' }, async (h) => {
- const admin = await h.login();
- const alice = await addUser(h, admin, 'alice');
- const bob = await addUser(h, admin, 'bob');
-
- // demo belongs to alice.
- await h.services.projects.setOwner('demo', alice.user.id);
-
- const aliceWorker = await h.services.workerTokens.create('alice-pi', { ownerId: alice.user.id });
- const bobWorker = await h.services.workerTokens.create('bob-pi', { ownerId: bob.user.id });
-
- await h.trigger({ sha: h.sha, ref: 'refs/heads/main' });
-
- // Bob's worker must never see alice's work.
- const forBob = await h.app.inject({
- method: 'GET', url: '/api/workers/poll', headers: { authorization: `Bearer ${bobWorker.token}` },
- });
- assert.equal(forBob.statusCode, 204);
-
- const forAlice = await h.app.inject({
- method: 'GET', url: '/api/workers/poll', headers: { authorization: `Bearer ${aliceWorker.token}` },
- });
- assert.equal(forAlice.statusCode, 200);
- assert.equal(forAlice.json().job.project_id, 'demo');
- });
-});
-
-test('a shared worker receives work from any project', async () => {
- await withHarness({ visibility: 'private' }, async (h) => {
- const admin = await h.login();
- const alice = await addUser(h, admin, 'alice');
- await h.services.projects.setOwner('demo', alice.user.id);
-
- // No owner means shared capacity.
- const shared = await h.services.workerTokens.create('shared', { ownerId: null });
- await h.trigger({ sha: h.sha, ref: 'refs/heads/main' });
-
- const res = await h.app.inject({
- method: 'GET', url: '/api/workers/poll', headers: { authorization: `Bearer ${shared.token}` },
- });
- assert.equal(res.statusCode, 200);
- });
-});
-
-test("a user registers their own project and worker, and cannot touch another's", async () => {
- await withHarness({}, async (h) => {
- const admin = await h.login();
- const alice = await addUser(h, admin, 'alice');
- const bob = await addUser(h, admin, 'bob');
- const json = (headers) => ({ ...headers, 'content-type': 'application/json' });
-
- const created = await h.app.inject({
- method: 'POST',
- url: '/api/projects',
- headers: json(alice.headers),
- payload: JSON.stringify({ id: 'alice-app', repo_url: 'https://git.example.com/a.git' }),
- });
- assert.equal(created.statusCode, 201);
- assert.equal(created.json().project.owner_id, alice.user.id);
-
- // Bob cannot see or manage it.
- const bobList = await h.app.inject({ method: 'GET', url: '/api/projects', headers: bob.headers });
- assert.deepEqual(bobList.json().projects.map((p) => p.id), []);
- assert.equal((await h.app.inject({
- method: 'DELETE', url: '/api/projects/alice-app', headers: bob.headers,
- })).statusCode, 403);
-
- // A worker bob registers belongs to bob.
- const token = await h.app.inject({
- method: 'POST',
- url: '/api/worker-tokens',
- headers: json(bob.headers),
- payload: JSON.stringify({ name: 'bob-laptop' }),
- });
- assert.equal(token.json().worker_token.owner_id, bob.user.id);
- assert.equal(token.json().worker_token.shared, false);
-
- // And alice cannot see it.
- const aliceWorkers = await h.app.inject({ method: 'GET', url: '/api/worker-tokens', headers: alice.headers });
- assert.deepEqual(aliceWorkers.json().worker_tokens.map((t) => t.name), []);
- });
-});
-
-test('only an administrator can create shared capacity or reassign a project', async () => {
- await withHarness({}, async (h) => {
- const admin = await h.login();
- const alice = await addUser(h, admin, 'alice');
- const json = (headers) => ({ ...headers, 'content-type': 'application/json' });
-
- // Asking for a shared worker as an ordinary user gets a personal one.
- const attempt = await h.app.inject({
- method: 'POST',
- url: '/api/worker-tokens',
- headers: json(alice.headers),
- payload: JSON.stringify({ name: 'sneaky', shared: true }),
- });
- assert.equal(attempt.json().worker_token.shared, false);
- assert.equal(attempt.json().worker_token.owner_id, alice.user.id);
-
- const asAdmin = await h.app.inject({
- method: 'POST',
- url: '/api/worker-tokens',
- headers: json(admin),
- payload: JSON.stringify({ name: 'pool', shared: true }),
- });
- assert.equal(asAdmin.json().worker_token.shared, true);
-
- // Reassigning an owner is administrator only.
- await h.services.projects.setOwner('demo', alice.user.id);
- const reassign = await h.app.inject({
- method: 'PATCH',
- url: '/api/projects/demo',
- headers: json(alice.headers),
- payload: JSON.stringify({ owner_id: null }),
- });
- assert.equal(reassign.statusCode, 400);
- });
-});
-
-test('deleting a user deletes what they owned, leaving nothing orphaned', async () => {
- await withHarness({}, async (h) => {
- const admin = await h.login();
- const alice = await addUser(h, admin, 'alice');
- await h.services.projects.setOwner('demo', alice.user.id);
- const token = await h.services.workerTokens.create('alice-pi', { ownerId: alice.user.id });
- const run = (await h.trigger({ sha: h.sha, ref: 'refs/heads/main' })).json().run_id;
-
- // The caller is told what will go before it goes.
- const impact = await h.app.inject({
- method: 'GET', url: `/api/admin/users/${alice.user.id}/impact`, headers: admin,
- });
- assert.deepEqual(impact.json().projects, ['demo']);
- assert.equal(impact.json().worker_tokens_deleted, 1);
- assert.ok(impact.json().runs_deleted > 0);
-
- const removed = await h.app.inject({
- method: 'DELETE', url: `/api/admin/users/${alice.user.id}`, headers: admin,
- });
- assert.equal(removed.statusCode, 200);
- assert.deepEqual(removed.json().projects, ['demo']);
-
- // Nothing of theirs is left behind.
- assert.equal(await h.services.projects.get('demo'), undefined);
- assert.equal(await h.services.db.get('SELECT id FROM runs WHERE id = {id}', { id: run }), undefined);
- assert.equal(await h.services.workerTokens.get(token.id), undefined);
- });
-});
-
-test('an orphaned worker token cannot appear and quietly become shared', async () => {
- await withHarness({}, async (h) => {
- const admin = await h.login();
- const alice = await addUser(h, admin, 'alice');
- const token = await h.services.workerTokens.create('alice-pi', { ownerId: alice.user.id });
-
- await h.app.inject({ method: 'DELETE', url: `/api/admin/users/${alice.user.id}`, headers: admin });
-
- // Were the row merely unowned, this token would now accept any
- // project's jobs instead of none.
- const rows = await h.services.db.all('SELECT id, owner_id FROM worker_tokens WHERE id = {id}', { id: token.id });
- assert.deepEqual(rows, []);
-
- const poll = await h.app.inject({
- method: 'GET', url: '/api/workers/poll', headers: { authorization: `Bearer ${token.token}` },
- });
- assert.equal(poll.statusCode, 401);
- });
-});