When a customer can’t use a feature that works for you, the tempting fix is to log in as an admin, confirm it works, and bump the customer’s role. That hides the bug instead of explaining it. To debug a customer permission issue properly, start from the exact action the customer can’t perform and compare every input to that authorization decision, while leaving the customer’s permissions untouched.
This guide is about user permissions inside your own SaaS application. Cloud IAM, database roles and admin consoles such as ServiceNow have their own debugging tools.
Problems like this are a common reason teams want to log in as the customer. One Directus developer asking for impersonation on GitHub (opens in a new tab) described regularly facing role- or user-specific problems, with dummy users or a temporary password reset as the only workarounds. Often you don’t need account access at all, as long as you work through the evidence in order.
Define the missing capability precisely
Turn “permissions are broken” into something testable:
A member of workspace Cedar can’t open a saved report they should be able to view. The report menu is missing, and the report’s read endpoint returns 403.
Name the resource, the action, the workspace, the time and the rule you expect to allow it. Viewing a report, creating one and exporting its rows are different permissions. Don’t lump them together as “report access”.
Then confirm the intended behavior with the product owner. A correct denial under a plan rule nobody remembered isn’t an authorization bug. Note where the expectation comes from: a spec, a configured role policy or the customer’s contract.
Start with the backend decision
Find the failing request by timestamp and request ID, and check whether it reached the backend at all. If it didn’t, look at UI gating, client configuration or how the request is built before blaming server permissions.
If the backend denied it, capture the denial reason and policy version. A 403 is a symptom, not a root cause. Which check failed: tenant membership, resource ownership, role grant, entitlement or something else?
Compare what the UI believed with what the backend resolved. Keep role names separate from effective grants, because a role label doesn’t describe every object-level restriction or per-user policy.
If your logs can answer these questions, you may not need to open the customer’s account at all. Keep tokens, cookies, private request bodies and full customer records out of the ticket. Store identifiers in an access-controlled investigation record instead.
Worked example: a stale entitlement
Here’s a made-up example to show the method. Cedar and Birch are fictional workspaces.
Say the rule for reading a saved report requires four things: tenant membership, a role grant, an enabled feature flag and an active entitlement. Assume the real cause is a stale entitlement projection, not a missing role.
| Input | Cedar member | Your admin account | What to check |
|---|---|---|---|
| Tenant | Member of Cedar; report belongs to Cedar | Different admin context | Membership and the resource’s tenant, independently |
| Role | Analyst with reports.read | Administrator | Effective grants, not role labels |
| Feature flag | saved_reports on for Cedar | On for the admin workspace | Evaluation context and flag version |
| Entitlement | Subscription active; permission projection says inactive | Active | Entitlement source and when the projection last updated |
Making the customer an admin would bypass the entitlement check, so the report would appear and the real problem would stay hidden. Instead, keep the role as it is and check whether the subscription record and the authorization projection agree on the workspace, feature and effective time.
The stale projection is still only a hypothesis at this point. The next step is to test it.
Use a diagnostic worksheet
Fill in one worksheet per failing action. Leave unknowns blank rather than guessing from an admin session.
| Field | What to enter |
|---|---|
| Expected behavior | Exact action and the rule that should allow it |
| Incident context | Timestamp with timezone, app version, request ID |
| Subject and tenant | Customer user ID, verified membership, resource’s tenant |
| Authorization | Role, effective grants, object restrictions, policy version |
| Entitlement | Source value, derived value, effective time, last refresh |
| Feature flag | Key, evaluated result, evaluation context, rule version |
| UI and backend | What the UI showed, the actual request, the denial reason |
| Then vs now | Inputs at incident time compared with inputs today |
| Next experiment | One input to change, expected result, reviewer |
| Result | What happened, or “not run yet” |
If someone does inspect the live account, record who it was and the approved scope too. This identifies the investigator. It doesn’t change the customer’s permissions.
Test one input at a time in staging
Rebuild the situation with synthetic data and change one input per run. Keep the analyst role constant unless the role is what you’re testing. The expected results below are pass conditions to check, not results we’ve run.
| Experiment | Expected result | What it tells you |
|---|---|---|
| Baseline: membership, read grant, flag on, entitlement active | Report loads | Your fixture is valid |
| Same, but entitlement projection inactive | Denied | Entitlement is part of the decision |
| Same, but flag off | Feature hidden per flag policy | Rollout gating is separate from entitlement |
| Same, but read grant removed | Denied | Role enforcement works |
| Cedar user requests a Birch report | Denied | Tenant isolation holds even with other inputs valid |
| Fix only the entitlement projection | Report loads, if the hypothesis is right | Supports the hypothesis; it doesn’t prove the fix |
| Set the projection back to inactive | Denied again | The changed input explains the difference |
Record the backend decision after each run and check for side effects. Before changing production, find out why the projection went stale, and test that the normal update path won’t let it happen again.
When to look at the live account
Sometimes the worksheet still has a gap that only the customer’s current state can fill, such as which records they can see or what a tenant setting is set to right now. That’s when an authorized, read-only look at the account helps.
Do it without shortcuts:
- Don’t raise the customer’s role, borrow an admin credential or reset their password.
- Use a time-limited, approved, read-only session and inspect only what the hypothesis needs.
- Keep exports and sensitive actions blocked. See read-only user impersonation for what to block.
- End the session and revoke its credential when you’re done.
With Devora, the session’s read-only scope is enforced by a backend guard that reads verified context (opens in a new tab) on your protected routes. Your app still runs its normal tenant and permission checks, and must revoke its own credential (opens in a new tab) when the session ends.
Remember that what you see today describes today’s state. It won’t recreate last week’s flag rollout, entitlement value, device or network. If the open question is historical, read when session replay isn’t enough.
Hand engineering an explanation, not a workaround
Send the completed worksheet, the denial evidence, the fixture inputs, the actual test results and what’s still unknown. Say clearly what’s confirmed, what’s a hypothesis and what hasn’t been run. If historical evidence is missing, say so.
Often, logs and a narrow support view are enough. When you do need to inspect a live account, review the security model and evaluate a read-only Devora session in staging. That way you get the access without changing the customer’s privileges.