# Authorization and Sharing

Course: [High-Level Design](https://www.devtutorials.ai/learn/system-design/hld) · Section: B2B SaaS Architectures
Source: https://www.devtutorials.ai/learn/system-design/hld/b2b-saas/authorization

> Decide who inside a tenant may read, edit or delete each record: role-based access (RBAC), record-level sharing, private records and row-level security, checked in a fixed order with deny by default.

## Who May Do What, Inside One Tenant?

Lesson 2 kept tenants apart. But inside Acme Retail there are agents, team leads, an admin and a contractor. The contractor may only read the tickets someone shared with her. A billing agent must not edit a support ticket. An HR complaint must be private to its owner.

A support team lead at Acme reports a problem: "I can edit Billing's tickets. I don't think I should be able to." She is right. One rule shipped with the wrong scope.

**The main idea:** check access in a **fixed order**: tenant boundary, record rules, then role-and-scope rules and sharing. **Deny by default.** Test the rules with scenarios, because an over-permissive rule never causes an error; it only shows data it should not.

## TL;DR

- **RBAC** (role-based access control) gives each role permissions, such as "agents may edit".
- A **scope** says *which* records: **own**, **team**, **shared** with you, or the whole **tenant**.
- **Record-level** rules can only narrow (private records) or widen for one record (sharing).
- **Deny by default:** if no rule allows an action, the answer is no.
- **Row-level security** can enforce the same scopes inside the database, so list queries cannot leak.
- Write **access scenarios** ("lee edits T1: deny") and run them in CI. Over-permissive rules are silent bugs.

## A Real-World Example

A project tool lets admins create custom roles. A customer creates "Project Viewer" by copying "Project Manager" and unticking a few boxes. A permission added in a later release, "export project", is ticked by default on every copied role. For a year, viewers can export every project.

No test failed, because no test ever asked "can a viewer export?". The fix was a list of access scenarios, run on every release.

## What You Will Learn

1. Roles and scopes.
2. Record-level sharing and private records.
3. The order of checks, and deny by default.
4. Testing rules with scenarios.
5. Enforcing scopes in the database.

## Roles and Scopes

A role alone is too coarse. "Agents may edit tickets" does not say *which* tickets. Pair every permission with a scope.

## Sharing and Private Records

**Sharing** widens access for one record: "share T4 with raj and vic". **Private** narrows it: "only the owner and admins see T3", whatever the roles say.

## The Order of Checks

Every decision runs the same steps, in the same order:

1. **Tenant boundary.** Different tenant: deny. Nothing below can override this.
2. **Record rules.** Private and not the owner or an admin: deny.
3. **Role and scope.** Does the user's role allow this action on records in this scope?
4. **Sharing.** Was this record shared with this user?
5. Nothing allowed it: **deny**.

Never write "allow" rules that can override step 1 or 2. A support tool that lets "super agents" read across tenants has quietly removed the tenant boundary for every super agent, including compromised ones.

## Test With Scenarios

An over-permissive rule never throws an error. The only way to catch it is to state what *should* happen and check.

## Try It: Find the Over-Permissive Rule

The lab has the rules Northwind shipped for Acme Retail and 30 access checks. Two checks give the wrong answer. Find the rule, fix its scope, and get all 30 right without blocking anyone from their own work.

**Pass rule:** 30 of 30 checks give the expected answer.

Keep scenarios as data (user, action, record, expected) in your test suite, and add one every time a customer reports a permission bug. The suite becomes a written-down policy that cannot drift.

## Enforce Scopes in the Database Too

The evaluator answers "may lee edit T1?". List pages ask a different question: "which tickets may lee see?". If the app fetches every ticket and filters in code, one forgotten filter leaks. Row-level security can apply the scope inside the query:

A row passes the policy only if all of these are true:

| Check | Passes when |
|---|---|
| Tenant | the row's `tenant_id` is the request's tenant |
| Scope | the user is an admin, **or** the row's team is one of the user's teams, **or** the row is shared with the user |
| Private | the row is not private, **or** the user owns it, **or** the user is an admin |

The app sets `app.user_id`, `app.role` and `app.teams` for each request, exactly like `app.tenant_id` in Lesson 2.

The same rule as a list filter, applied to every row before any row leaves the database:

For very large tenants with deep team hierarchies and many sharing rules, teams often precompute "who can see what" into an access table or use a dedicated authorization service built on relationship tuples (the "Zanzibar" model behind tools like OpenFGA and SpiceDB). The order of checks stays the same.

## Check Yourself

## Common mistakes

| Mistake | Wrong | Correct | Why it happens |
|---|---|---|---|
| Tenant-wide scope by accident | team-lead edit: tenant | team-lead edit: team | Wide scopes are the easy default when shipping |
| Allow by default | "No rule says no, so yes" | No rule says yes, so no | New actions arrive without rules |
| Private records checked last | A role rule allows before the private check runs | Private check before roles | Order of checks was never written down |
| UI-only filtering | API returns all tickets; the UI hides some | Filter in the API and the database | The API is called directly by scripts and integrations |
| No scenarios | Permissions tested by hand | Scenario table in CI | Over-permissive rules never fail a test |

## Practice

## What to Remember

- Roles say **what**; scopes say **which records**.
- Check in order: tenant, record rules, roles and sharing, then deny.
- Private records narrow access; sharing widens it for one record.
- Over-permissive rules fail silently. Test with written scenarios.
- Apply scopes to list queries too, ideally in the database.

## Further Reading

- [OWASP: Authorization Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html): deny by default, least privilege, checks on every request.
- [PostgreSQL: Row Security Policies](https://www.postgresql.org/docs/current/ddl-rowsecurity.html): policies that use session settings.
- [Zanzibar: Google's consistent, global authorization system (paper)](https://research.google/pubs/zanzibar-googles-consistent-global-authorization-system/): relationship-based access control at scale.

Next: **Enterprise Identity: SSO and SCIM**. Customers want their staff to sign in with the company account, and to lose access the moment they leave.
