Skip to main content

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

ScenarioEdge caseWhy tests breakApproach
Horizontal escalationMember reads other tenant200 OK disasterProbe 403 + zero rows
Vertical escalationMember → admin APIHidden UI onlyProbe admin route
ImpersonationSupport views tenantWrong tenant scopeProbe audit + claims
Impersonation exitSession sticksStill impersonatingProbe clear impersonation
Billing adminSupport not billingRefund issuedProbe role deny refund
IDORIncrement user idData leakProbe B id with A token
GraphQLNested field authField leaksProbe sensitive field null
CSRF on adminState-changing POSTUntestedUse API with token
API key adminSeparate authzKeys too powerfulProbe scoped key
Tenant header spoofX-Tenant-IdCross-tenantProbe ignore client header
Read-only adminViewer deletesSide effectProbe DELETE 403
Break-glassEmergency roleTime boxedProbe 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

roleresourcemethodexpected
member/api/admin/billingGET403
billing_admin/api/admin/refundsPOST200 with audit
support/api/admin/impersonatePOST200 with audit
support/api/admin/refundsPOST403

Generate matrix from prod permissions; one E2E per critical deny rule.

Application roles differ from auth RBAC permissions—Firebase/custom claims vs app admin console. Test both boundaries if both exist.

Anti-patterns

Anti-patternWhy it failsBetter approach
Sidebar not visibleDirect URL worksProbe API 403
Single admin happy pathEscalation untestedNegative matrix
Shared admin passwordParallel session chaosPer-run role users
No audit assertCompliance gapProbe 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.

  1. Arrange: Seed two tenants with orders; login as tenant A member.
  2. Act: GET /api/v1/orders/{tenantB_orderId} with A token.
  3. 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).

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.

Start free on TestChimp · Book a demo