Accept admin API tokens for batch review and admin task updates

Batch review (including skipPeerReview) and POST /api/admin/tasks/:id required a browser session. Admin API tokens may now call them; other roles still need a password login.
This commit is contained in:
ragnarok-clanker 2026-10-28 16:13:44 +01:00
commit 57ea62ef8c
3 changed files with 33 additions and 2 deletions

View file

@ -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);

View file

@ -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');

View file

@ -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');