How to Test Admin Dashboards and Role Permissions
Short answer
Admin bugs expose cross-tenant data or enable privilege escalation. E2E must probe 403 on horizontal access (User A → User B resource), impersonation audit entries, and support-role billing restrictions—not hidden sidebar links. Seed users per role and tenant via test routes; never rely on "admin menu not visible" as security proof.
Part of Testing Guides by business flow.
Who this is for
B2B SaaS with admin consoles, support impersonation, billing admin, or multi-tenant operator tools.
Why testing admin RBAC matters
- Security incidents — horizontal escalation reads another customer's PII.
- Compliance — missing audit logs for impersonation fail SOC2.
- Operations — support cannot help if roles too restrictive—test intended positives too.
Complexity map
| Scenario | Edge case | Why tests break | Approach |
|---|---|---|---|
| Horizontal escalation | Member reads other tenant | 200 OK disaster | Probe 403 + zero rows |
| Vertical escalation | Member → admin API | Hidden UI only | Probe admin route |
| Impersonation | Support views tenant | Wrong tenant scope | Probe audit + claims |
| Impersonation exit | Session sticks | Still impersonating | Probe clear impersonation |
| Billing admin | Support not billing | Refund issued | Probe role deny refund |
| IDOR | Increment user id | Data leak | Probe B id with A token |
| GraphQL | Nested field auth | Field leaks | Probe sensitive field null |
| CSRF on admin | State-changing POST | Untested | Use API with token |
| API key admin | Separate authz | Keys too powerful | Probe scoped key |
| Tenant header spoof | X-Tenant-Id | Cross-tenant | Probe ignore client header |
| Read-only admin | Viewer deletes | Side effect | Probe DELETE 403 |
| Break-glass | Emergency role | Time boxed | Probe expiry |
Horizontal privilege escalation
const userA = await request.post('/api/test/seed-user-rbac', {
data: { runId, role: 'member', tenantId: `tenant-a-${runId}` },
}).then(r => r.json());
const userB = await request.post('/api/test/seed-user-rbac', {
data: { runId, role: 'member', tenantId: `tenant-b-${runId}` },
}).then(r => r.json());
const res = await request.get(`/api/v1/users/${userB.userId}/profile`, {
headers: { Authorization: `Bearer ${userA.token}` },
});
expect(res.status()).toBe(403);
const leaked = await request.get(`/api/admin/users/${userB.userId}`, {
headers: { Authorization: `Bearer ${userA.token}` },
});
expect(leaked.status()).toBe(403);
Playwright UI optional: navigate to /admin/users/${userB.userId} as userA—expect 403 page.
Impersonation with audit
const support = await seedSupportAgent(request, runId);
const tenantUser = await seedTenantMember(request, runId);
await request.post('/api/admin/impersonate', {
headers: { Authorization: `Bearer ${support.token}` },
data: { targetUserId: tenantUser.userId, reason: 'e2e ticket 123' },
});
const audit = await request.get(`/api/test/probes/audit/${runId}`).then(r => r.json());
expect(audit.events.some(e => e.action === 'impersonation.start')).toBe(true);
// Act as impersonated user — scope check
const data = await request.get('/api/v1/me', {
headers: { Authorization: `Bearer ${support.impersonationToken}` },
}).then(r => r.json());
expect(data.userId).toBe(tenantUser.userId);
expect(data.tenantId).toBe(tenantUser.tenantId);
await request.post('/api/admin/impersonate/end', {
headers: { Authorization: `Bearer ${support.token}` },
});
Role matrix spot checks
| role | resource | method | expected |
|---|---|---|---|
| member | /api/admin/billing | GET | 403 |
| billing_admin | /api/admin/refunds | POST | 200 with audit |
| support | /api/admin/impersonate | POST | 200 with audit |
| support | /api/admin/refunds | POST | 403 |
Generate matrix from prod permissions; one E2E per critical deny rule.
Link to auth RBAC guide
Application roles differ from auth RBAC permissions—Firebase/custom claims vs app admin console. Test both boundaries if both exist.
Anti-patterns
| Anti-pattern | Why it fails | Better approach |
|---|---|---|
| Sidebar not visible | Direct URL works | Probe API 403 |
| Single admin happy path | Escalation untested | Negative matrix |
| Shared admin password | Parallel session chaos | Per-run role users |
| No audit assert | Compliance gap | Probe audit log |
Example scenario
Situation: Member attempts to open another tenant's order via direct URL.
Expected outcome: 403 Forbidden; no order data in response body.
Why UI-only automation breaks: 404 page shown but API returns 200 with JSON body.
- Arrange: Seed two tenants with orders; login as tenant A member.
- Act: GET /api/v1/orders/{tenantB_orderId} with A token.
- Assert: Probe 403; response body has no PII; optional UI error page.
TestChimp workflow: Track role × permission × resource_type in TrueCoverage for support-role API usage.
Same Arrange/Act/Assert pattern as expired-coupon checkout.
Connect scenarios to your QA workflow
Capture business rules in markdown test plans and enforce them with seed routes and probe Assert. Link SmartTests with // @Scenario: for requirement traceability. Use /testchimp test on PRs; /testchimp explore on SmartTest paths for non-functional gaps (ExploreChimp).
Related scenarios
- Auth RBAC permissions — claims and roles
- Feature entitlements — plan vs admin
- Seat licensing — invite admin actions
External references
Frequently asked questions
How do I test horizontal privilege escalation?
Login as User A, attempt access to User B resource ID via URL or API, probe 403 and zero rows returned—never expect UI-only hiding.
How do I test support impersonation safely in E2E?
Use test-only support agent seed, start impersonation with reason, probe audit log entry, verify scoped access to target tenant only, then end impersonation and probe session cleared.
Should admin tests use UI or API?
API probes for authorization matrix; add few Playwright specs for critical admin UX. Security Assert is always API-first.
How do I test IDOR on sequential ids?
Seed two users with known ids; token for A requests B id on every sensitive endpoint pattern your app exposes.
GraphQL admin fields—special case?
Probe GraphQL with member token for admin-only fields—expect errors or null with no data leak, not only REST routes.
Audit logs in CI?
Expose test probe for audit events keyed by runId; assert impersonation, refund, and role change events without querying prod logging.
Which role × resource combos do prod admins actually use?
TrueCoverage on role × permission × resource_type. Run /testchimp evolve after new admin features—link // @Scenario: for each deny rule.
Apply these patterns in your repo
Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.