Files
wmantly 4326d5588e Add self-service API tokens (PATs) — the UI had no way to create one
jump-host had zero API-token support: no model, no route, no UI, and
Auth.checkApiToken was explicitly absent from the createOidcClient() call
(per the comment it left behind). proxy and sso-manager-node both have
this; jump-host didn't.

Ports proxy's models/api_token.js + routes/api_token.js pattern (jmp_
prefix instead of prx_), wires checkApiToken into createOidcClient(), adds
Bearer-token support to middleware/auth.js, and adds a token management
card to dashboard.ejs (create/list/rotate/revoke) using app.modal/
app.messages.

Scope note: a jump-host token carries no group claims (unlike proxy's,
which snapshots the creator's groups), so it authenticates as its creator
for non-admin routes (e.g. GET /api/user/hosts) but can never pass
requireAdmin — a deliberate, conservative default rather than recomputing
live admin status per-request.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 19:44:04 -04:00

121 lines
3.3 KiB
JavaScript

'use strict';
// Self-service API token (PAT) management. Every endpoint is owner-scoped: a
// user only sees / mutates tokens where created_by === req.user.username.
// Mirrors proxy's routes/api_token.js. Mounted under middleware.auth only
// (no requireAdmin) — any authenticated user may mint one, but the token
// itself carries no group claims (see models/api_token.js), so it can only
// reach non-admin routes (e.g. GET /api/user/hosts), never the admin-gated
// ones under routes/jump.js.
const router = require('express').Router();
const {ApiToken} = require('../models');
function forbidden(){
let error = new Error('Forbidden');
error.name = 'Forbidden';
error.message = 'You do not own this API token.';
error.status = 403;
return error;
}
// Resolve a token the caller owns. Missing or not-yours both raise 403 (no
// existence leak; ids are unguessable random hex anyway).
async function getOwned(req, id){
let token;
try{
token = await ApiToken.get(id);
}catch(e){
throw forbidden();
}
if(!token || token.created_by !== req.user.username) throw forbidden();
return token;
}
router.get('/', async function(req, res, next){
try{
return res.json({results: await ApiToken.listDetail({created_by: req.user.username})});
}catch(error){
next(error);
}
});
router.post('/', async function(req, res, next){
try{
const days = req.body.expires_in_days !== '' && req.body.expires_in_days !== undefined
? Number(req.body.expires_in_days) : 0;
const token = await ApiToken.add({
name: req.body.name,
description: req.body.description || '',
created_by: req.user.username,
expires_at: days > 0 ? (new Date).getTime() + days * 86400000 : 0,
});
return res.json({
results: token,
token: token._raw_token,
message: `API token '${token.name}' created. Save it now — it will not be shown again.`,
});
}catch(error){
next(error);
}
});
router.get('/:id', async function(req, res, next){
try{
return res.json({results: await getOwned(req, req.params.id)});
}catch(error){
next(error);
}
});
router.put('/:id', async function(req, res, next){
try{
const token = await getOwned(req, req.params.id);
const update = {};
for(const k of ['name', 'description']){
if(req.body[k] !== undefined) update[k] = req.body[k];
}
if(req.body.expires_in_days !== undefined && req.body.expires_in_days !== ''){
const days = Number(req.body.expires_in_days);
update.expires_at = days > 0 ? (new Date).getTime() + days * 86400000 : 0;
}else if(req.body.expires_at !== undefined){
update.expires_at = Number(req.body.expires_at) || 0;
}
return res.json({
results: await token.update(update),
message: `API token '${token.name}' updated.`,
});
}catch(error){
next(error);
}
});
router.delete('/:id', async function(req, res, next){
try{
const token = await getOwned(req, req.params.id);
await token.remove();
return res.json({id: req.params.id, message: `API token '${token.name}' revoked.`});
}catch(error){
next(error);
}
});
router.post('/:id/rotate', async function(req, res, next){
try{
const token = await getOwned(req, req.params.id);
const raw = await token.rotate();
return res.json({
token: raw,
message: `API token '${token.name}' rotated. Save it — it will not be shown again.`,
});
}catch(error){
next(error);
}
});
module.exports = router;