template-monorepo/test/k6/journeys/rbac_admin.js

132 lines
5.4 KiB
JavaScript
Raw Permalink Normal View History

2026-05-26 06:05:33 +00:00
// Journey: admin RBAC end-to-end (login admin → roles CRUD → assign → reload → verify → cleanup)
//
// Endpoints exercised (happy path, admin role required):
// POST /api/v1/auth/login (admin)
// GET /api/v1/permissions/roles
// POST /api/v1/permissions/roles
// PATCH /api/v1/permissions/roles/:id
// GET /api/v1/permissions/roles/:id/permissions
// PUT /api/v1/permissions/roles/:id/permissions
// GET /api/v1/permissions/catalog (to pick a permission_id)
// POST /api/v1/permissions/users/:uid/roles
// GET /api/v1/permissions/users/:uid/roles
// POST /api/v1/permissions/policy/reload
// GET /api/v1/permissions/me (as the assigned user)
// DELETE /api/v1/permissions/users/:uid/roles/:role_id
// DELETE /api/v1/permissions/roles/:id
//
// PREREQUISITE — admin user seeded by `make k6-seed-admin`. ADMIN_EMAIL,
// ADMIN_PASSWORD env vars must be set (k6.env exports them). If unset the
// journey prints a skip notice and exits 0 (covered by smoke/permission_admin
// for route-existence checks).
import { sleep } from 'k6';
import { get, post, put, patch, del, checkEnvelope } from '../lib/http.js';
import { login } from '../lib/auth.js';
import { registerAndConfirm } from '../lib/auth.js';
import { cfg, unique } from '../lib/config.js';
export const options = {
vus: 1,
iterations: 1,
thresholds: { checks: ['rate==1.0'] },
};
function pickAnyPermissionID(bearer) {
const cat = checkEnvelope(get('/api/v1/permissions/catalog', bearer), 'GET /permissions/catalog').data;
const list = (cat && (cat.list || cat.tree)) || [];
// Find a leaf — node with http_methods set.
const stack = [...list];
while (stack.length) {
const n = stack.shift();
if (n.http_methods && n.id) return n.id;
if (n.children) for (const c of n.children) stack.push(c);
}
if (list.length > 0 && list[0].id) return list[0].id;
throw new Error('catalog: no permissions found');
}
export default function () {
if (!cfg.adminEmail || (!cfg.adminPassword && !cfg.adminAccessToken)) {
console.log('[rbac_admin] skipped: ADMIN_EMAIL + (ADMIN_PASSWORD or ADMIN_ACCESS_TOKEN) env not set. Run `make k6-seed-admin` first.');
return;
}
// 1. obtain admin access_token. Prefer ADMIN_ACCESS_TOKEN (issued by
// k6-seed-admin at registration time) because ZITADEL v2 disables OAuth
// password grant by default, so /auth/login → VerifyPassword returns 502.
let adminAccessToken = cfg.adminAccessToken;
if (!adminAccessToken) {
const admin = login({ email: cfg.adminEmail, password: cfg.adminPassword });
adminAccessToken = admin.access_token;
}
const adminBearer = { Authorization: `Bearer ${adminAccessToken}` };
// 2. register a target user (we'll assign a role to them and verify via /me)
const { tokens: target } = registerAndConfirm();
const targetBearer = { Authorization: `Bearer ${target.access_token}` };
// 3. list roles (sanity)
const rolesBefore = checkEnvelope(get('/api/v1/permissions/roles', adminBearer), 'GET /roles').data;
if (!rolesBefore || !Array.isArray(rolesBefore.roles)) throw new Error('roles list bad shape');
// 4. create a role
const roleKey = unique('k6role').replace(/[^a-z0-9_]/g, '_');
const created = checkEnvelope(
post('/api/v1/permissions/roles', { key: roleKey, display_name: 'k6 RBAC role' }, adminBearer),
'POST /roles',
).data;
const roleID = created.id;
// 5. patch role display name
const newDisplay = `${created.display_name} (patched)`;
checkEnvelope(
patch(`/api/v1/permissions/roles/${roleID}`, { display_name: newDisplay }, adminBearer),
'PATCH /roles/:id',
);
// 6. get role permissions (empty initially)
checkEnvelope(get(`/api/v1/permissions/roles/${roleID}/permissions`, adminBearer), 'GET /roles/:id/permissions');
// 7. put role permissions (replace with one permission id picked from catalog)
const permID = pickAnyPermissionID(adminBearer);
checkEnvelope(
put(`/api/v1/permissions/roles/${roleID}/permissions`, { permission_ids: [permID] }, adminBearer),
'PUT /roles/:id/permissions',
);
// 8. assign role to target user
checkEnvelope(
post(`/api/v1/permissions/users/${target.uid}/roles`, { role_id: roleID }, adminBearer),
'POST /users/:uid/roles',
);
// 9. list target user's roles
const targetRoles = checkEnvelope(
get(`/api/v1/permissions/users/${target.uid}/roles`, adminBearer),
'GET /users/:uid/roles',
).data;
const hasRole = targetRoles.user_roles && targetRoles.user_roles.some((ur) => ur.role_id === roleID);
if (!hasRole) throw new Error('assigned role not visible in user_roles');
// 10. policy reload
checkEnvelope(post('/api/v1/permissions/policy/reload', {}, adminBearer), 'POST /policy/reload');
// give the gateway a moment for the pub/sub reload to settle
sleep(0.5);
// 11. target /permissions/me reflects the role
const me = checkEnvelope(get('/api/v1/permissions/me', targetBearer), 'GET /permissions/me').data;
if (!me.roles || !me.roles.includes(roleKey)) {
throw new Error(`/permissions/me did not include role ${roleKey}: got=${JSON.stringify(me.roles)}`);
}
// 12. revoke role
checkEnvelope(
del(`/api/v1/permissions/users/${target.uid}/roles/${roleID}`, null, adminBearer),
'DELETE /users/:uid/roles/:role_id',
);
// 13. delete role (now safe because no user is assigned)
checkEnvelope(del(`/api/v1/permissions/roles/${roleID}`, null, adminBearer), 'DELETE /roles/:id');
}