Every request should resolve a tenant or workspace from authenticated context and verify that the requested resource belongs to it. Do not accept tenant IDs only from a URL or hidden form field. Keep the scope available to repository queries, background jobs, exports, and audit records. A permission check that works in the web page but is missing from an asynchronous worker is still a cross-tenant vulnerability.
Separate role from resource scope
A role such as admin, operator, or analyst is not sufficient by itself. The same role may have access to one workspace, region, team, or project and not another. Define permissions as actions over resource types with explicit scope. Document whether access is inherited, delegated, or denied when scopes overlap. Make the policy understandable to a customer administrator and testable by code rather than relying on informal assumptions about role names.
Use deny-by-default policy evaluation
Start with no access and add a narrowly defined grant. Check authentication, tenant membership, role permission, resource ownership or scope, record state, and any approval requirement before the mutation. Return a safe error without revealing whether another tenant’s resource exists. Avoid authorization logic spread across UI components; the server-side policy must remain authoritative for every read and write path.
Protect bulk actions and exports
Bulk operations turn a small permission mistake into a large data exposure or mass mutation. Require a bounded selection, re-check each resource’s scope, and report partial failures without leaking excluded records. Apply separate permissions for exporting, deleting, impersonating, and changing access. Store the export actor, tenant, filters, record count, and delivery destination. A customer admin should not automatically inherit unrestricted support or platform access.
Design support access as an explicit plane
Internal support access needs its own identity, purpose, approval, expiry, and audit trail. Do not create a permanent superuser shortcut to debug customer issues. Use time-limited access to a named tenant, restrict actions where possible, and show the customer or internal reviewer what was accessed. Break-glass access should be rare, monitored, and easy to revoke. The support path is part of the product’s security model, not an exception outside it.
Keep authorization close to data access
A controller-level check can be bypassed by a new endpoint, report, import, or job. Centralize policy evaluation and enforce tenant predicates in data-access functions or queries. Test direct resource lookup, list filters, nested relations, file downloads, webhook handlers, and background jobs. For caches, include tenant and permission context in the key so a valid response cannot be served to the wrong workspace.
Audit and rehearse policy changes
Record denied and allowed sensitive decisions with actor, tenant, action, resource, policy version, and result reason. Test tenant isolation, role changes during a session, revoked invitations, copied URLs, stale tokens, support expiry, and bulk exports. Review policy changes as production code with migration and rollback notes. A permission model is healthy when the organization can explain not only who has access, but how that access is withdrawn and verified.
Prove isolation continuously
Tenant isolation deserves automated tests and operational evidence, not only a one-time security review. Generate fixtures with multiple tenants, overlapping resource names, nested projects, shared support users, and revoked memberships. Test reads, writes, search, sort, counts, exports, file URLs, notifications, webhooks, and background jobs. Assert that a resource from one tenant cannot be inferred through a count, timing difference, error message, or cached response. When a role or invitation changes, invalidate sessions or re-evaluate policy according to the documented rule. Make audit logs tenant-aware while restricting who may view platform-wide logs. Review authorization decisions after adding a new resource type because inheritance assumptions often fail at the edges. A customer-facing permission report should show effective access in plain language, while an internal policy report shows the underlying grants and denials. Treat tenant moves, merges, and deletions as controlled migrations with a dry run and rollback plan. Continuous isolation testing turns “multi-tenant” from a deployment description into a property the product can repeatedly demonstrate.
Document effective access
Explain permission results in terms customers understand: who can view, change, export, invite, or support a resource, within which workspace and until when. Pair that view with an internal decision trace. When a customer reports an access surprise, support can see the effective grant, its source, and the policy version without asking engineering to inspect production tables manually.



