diff --git a/src/server/app.js b/src/server/app.js index 0e0d3f2..eac3731 100644 --- a/src/server/app.js +++ b/src/server/app.js @@ -1057,7 +1057,8 @@ export function createApp(ctx) { // One click for a whole batch: every function of it that waits for a human. router.on('POST', '/api/batches/:id/review', async ({ req, params }) => { - const user = auth(req, { role: 'reviewer', session: true }); + const user = auth(req, { role: 'reviewer' }); + if (user.via !== 'session' && !hasRole(user, 'admin')) throw new HttpError(403, 'this action needs a password login in the browser; only admin API tokens can do it'); const body = await readJson(req, 16 * 1024); const attemptIds = Array.isArray(body.attemptIds) ? body.attemptIds.map((x) => intParam(x)) : null; const r = merger.reviewBatch(user, intParam(params.id), String(body.action ?? ''), body.comment ? String(body.comment) : null, { attemptIds, skipPeerReview: body.skipPeerReview === true }); @@ -1077,7 +1078,8 @@ export function createApp(ctx) { // --- maintainers and admins (browser session only) router.on('POST', '/api/admin/tasks/:id', async ({ req, params }) => { - auth(req, { role: 'maintainer', session: true }); + const user = auth(req, { role: 'maintainer' }); + if (user.via !== 'session' && !hasRole(user, 'admin')) throw new HttpError(403, 'this action needs a password login in the browser; only admin API tokens can do it'); const body = await readJson(req, 4096); const t = tasks.get(intParam(params.id)); if (body.priority !== undefined) store.run('UPDATE tasks SET priority = ? WHERE id = ?', Number(body.priority) | 0, t.id); diff --git a/test/manual-review.test.js b/test/manual-review.test.js index 2dc20ba..e07a440 100644 --- a/test/manual-review.test.js +++ b/test/manual-review.test.js @@ -115,6 +115,22 @@ test('admin batch self-review leaves peer review alone unless explicitly skipped assert.deepEqual(skipped.body.results, [{ attemptId: pending, state: 'approved' }]); }); +test('batch review accepts admin API tokens but not tokens of other roles', async (t) => { + const { store, accounts, user, attempt, post } = await fixture(t); + const admin = user('admin'); + const maintainer = user('maintainer'); + const author = user('contributor'); + const batch = Number(store.run(`INSERT INTO batches (user_id, base_commit, files_json, state, created_at) VALUES (?, 'base', '{}', 'open', ?)`, author.id, now()).lastInsertRowid); + const pending = attempt(author, 'peer_review', batch); + const maintainerToken = accounts.createApiToken(maintainer.id, 'agent').token; + assert.equal((await post(`/api/batches/${batch}/review`, { action: 'approve', skipPeerReview: true }, { token: maintainerToken })).status, 403); + assert.equal(store.get('SELECT state FROM attempts WHERE id = ?', pending).state, 'peer_review'); + const adminToken = accounts.createApiToken(admin.id, 'agent').token; + const result = await post(`/api/batches/${batch}/review`, { action: 'approve', attemptIds: [pending], skipPeerReview: true }, { token: adminToken }); + assert.equal(result.status, 200, JSON.stringify(result.body)); + assert.deepEqual(result.body.results, [{ attemptId: pending, state: 'approved' }]); +}); + test('identical resubmissions retain an explicit review waiver without inventing peer votes', async (t) => { const { store, user, attempt, post } = await fixture(t); const admin = user('admin'); diff --git a/test/server.test.js b/test/server.test.js index 4c7bf9f..adabb21 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -1389,6 +1389,19 @@ test('admin: a done task is reopened only with force, and then as open', async ( ctx.store.run(`UPDATE tasks SET state = 'done' WHERE id = ?`, done.id); }); +test('admin: task reopen accepts admin API tokens but not maintainer tokens', async () => { + await ctx.accounts.createUser('rvadmintok', 'admin', PASSWORD); + await ctx.accounts.createUser('rvmainttok', 'maintainer', PASSWORD); + const done = ctx.store.get(`SELECT id FROM tasks WHERE state = 'done' LIMIT 1`); + const maintToken = ctx.accounts.createApiToken(ctx.accounts.byName('rvmainttok').id, 'agent').token; + assert.equal((await request('POST', `/api/admin/tasks/${done.id}`, { state: 'open', force: true }, { token: maintToken })).status, 403); + assert.equal(ctx.store.get('SELECT state FROM tasks WHERE id = ?', done.id).state, 'done'); + const adminToken = ctx.accounts.createApiToken(ctx.accounts.byName('rvadmintok').id, 'agent').token; + const forced = await request('POST', `/api/admin/tasks/${done.id}`, { state: 'open', force: true }, { token: adminToken }); + assert.equal(forced.body.state, 'open'); + ctx.store.run(`UPDATE tasks SET state = 'done' WHERE id = ?`, done.id); +}); + test('maintenance batches: maintainers only; merge when no compiled code changes, back otherwise', async () => { const contributor = await activeUser('mtcontrib'); const maintainer = await activeUser('mtmaint', 'maintainer');