// 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'); }