# Cheat sheet — Authentication & Authorization (this repo)

Dense revision sheet. One glance per lesson. Interview one-liners at the bottom.

---

## The gate sequence (say it out loud)

```
TOKEN (JWT in gRPC md "token")
 → AUTH INTERCEPTOR   verify vs JWKS (sig/iss/aud/exp) → ctx: UserID, UserGroup, UserRoles, JWTClaims
 → RBAC (rbacDecider) role gate: map[method]→roles  (nil = any authed; absent = PermissionDenied)
 → SERVICE CHECK      ownership (UserIDFromContext) + granular permission (location subtree)
 → REPO               WHERE resource_path=$… AND deleted_at IS NULL
 → RLS (Postgres)     BeforeAcquire set_config → permission_check(resource_path) + location policies
                      row survives only if repo WHERE **and** RLS both pass  ← defense in depth
```

## authN — the token & its authority

- **Manabie JWT** = Shamir-minted, iss `"manabie"`, RS256. Carries the **`Manabie` claim**:
  `{user_id, default_role, allowed_roles[], school_ids[], user_group, resource_path, tenant_id}`
  (`auth_claims.go:101`). Same values also minted as `x-hasura-*` for Hasura RLS.
- **Shamir** = verify + exchange/mint + JWKS host (`/.well-known/jwks.json`). Signs with a primary
  RSA key from a SOPS glob. NOT Shamir secret-sharing.
- **Login → token:** password → Firebase idToken (`LoginInAuthPlatform` → Identity Toolkit) →
  `ExchangeToken` → Shamir verifies, looks up user, mints Manabie JWT with roles/tenant.
- **Verify** (`TokenVerifier.Verify`, `golibs/interceptors/auth.go:269`): parse signed → match JWKS
  key → check iss/aud/exp; re-fetch JWKS once on miss; multi-issuer (first success wins).

## authN — the interceptor

```go
// internal/usermgmt/pkg/interceptors/auth.go:79
if skipAuthMethods[method] { return handler }         // ignoreAuthEndpoint → bypass
claims := verify(ctx)                                  // md.Get("token") → JWKS verify
ctx = ContextWithUserID(claims.Subject)                // + UserGroup (if Manabie.UserGroup)
ctx = ContextWithJWTClaims(claims)
groups := groupDecider.Check(ctx, subject, method)     // ← the role gate
ctx = ContextWithUserGroup(groups[0]); ContextWithUserRoles(groups)
```
- **Two interceptor packages** (golibs legacy `→string`, usermgmt current `→[]string`); services use
  usermgmt. `fakeJwtCtxEndpoint` → `FakeJwtContext` synthesizes School-Admin claims from
  `req.OrganizationId` (service-to-service).

## authZ tier 1 — RBAC (`rbacDecider`)

`GroupDecider.Check` (`auth.go:201`):
| `AllowedGroups[method]` | result |
|---|---|
| role list | user must hold ≥1 of them, else `sttNotAllowed` |
| `nil` | any authenticated user with ≥1 role |
| **absent key** | **`sttNoDeciderProvided` = `PermissionDenied` at runtime** (NOT a startup panic) |
| (roles empty) | `sttDeniedAll`; deactivated → `sttDeactivatedUser` |

Roles = `constant.Role*` (~27). `GroupFetcher` = `RetrieveUserRoles*` reads the DB.

## authZ tier 2 — granular permissions + locations

```
user → user_group_member → user_group → granted_role → role → permission_role → permission
                                            └─ granted_role_access_path → LOCATION (subtree)
```
- Permission = dotted string (`user.user.read`, `master.location.write`).
- `granted_permissions` VIEW joins the chain + expands subtree: `l1.access_path ~~ (l.access_path||'%')`.
- Resolve at runtime: `FindLocationIDsByUserIDAndPermissionName(userID, "x.y.read")`
  → `InternalAccessControlService.RetrieveUserGrantedLocations`.
- `access_path` = **materialized path** (`org/brand/center`); subtree = `LIKE 'prefix%'`.
- `LocationRestricted` interceptor: empty req locations → need org-root; else all requested must be granted.

## authZ tier 3 — RLS (tenant + location, in the DB)

- **Session vars** (per acquire, `pool_gcp.go:63,161`):
  `set_config('permission.resource_path' / 'permission.user_group' / 'app.user_id', …, false)` from JWT.
- `permission_check(rp,t)` = `rp = current_setting('permission.resource_path')` → **tenant isolation**.
- `autofillresourcepath()` = DEFAULT for `resource_path` cols → inserts auto-stamp tenant.
- Tables: `ENABLE` + **`FORCE ROW LEVEL SECURITY`** (owner bound); some `AS RESTRICTIVE`.
- Location-aware policy (`migrations/auth/1001`): row visible iff `granted_permissions ⋈
  user_access_paths` shows caller (`app.user_id`) holds the permission on that row's location; +
  self-visibility (`app.user_id = user_id`).
- `resource_path` = org (`-2147483648` = Manabie). `rls_scan` = daily drift check for missing RLS.

---

## Files to know

```
internal/golibs/interceptors/auth.go          TokenVerifier, CustomClaims, ctx accessors (shared)
internal/golibs/interceptors/auth_claims.go   ManabieClaims :101, HasuraClaims :91
internal/usermgmt/pkg/interceptors/auth.go    the live interceptor + GroupDecider.Check :201
cmd/server/<svc>/auth.go                       rbacDecider, ignoreAuthEndpoint, fakeJwtCtxEndpoint
internal/shamir/...                            token authority (verify/mint/JWKS)
internal/golibs/database/pool_gcp.go           setPostgres BeforeAcquire :63,161
migrations/auth/1001_migrate.up.sql            permission_check, autofillresourcepath, location RLS
migrations/bob/1182_migrate.up.sql             permission/role/... tables + rls_<t> policies
cmd/server/rls_scan/rls_scan.go                RLS drift scanner
```

## Interview one-liners

- **"authN vs authZ?"** — authN proves *who* (JWKS-verified JWT); authZ decides *what* (role gate →
  granular permission/location → RLS). Different questions, different layers.
- **"How is a token validated?"** — fetch the issuer's JWKS, verify the RS256 signature, then check
  issuer, audience, and expiry. Multi-issuer; first match wins.
- **"What is Shamir?"** — the internal token authority: it verifies external (Firebase/Auth0) tokens,
  mints Manabie JWTs with the user's roles + tenant, and publishes the JWKS everyone verifies against.
- **"RBAC here?"** — a per-method role allow-list (`rbacDecider`); `nil` = any authed user; a missing
  entry is `PermissionDenied` at runtime (not, despite the docs, a startup panic).
- **"Beyond roles?"** — granular permissions (`user.user.read`) granted through user-groups and
  **scoped to a location subtree** via `access_path` materialized paths.
- **"How is multi-tenancy enforced?"** — Postgres RLS. `setPostgres` stamps `resource_path` from the
  JWT onto each connection; `permission_check()` filters every row to the tenant; `FORCE RLS` binds
  even the owner. Never trust `resource_path` from the request.
- **"Why RLS *and* app checks?"** — defense in depth: the interceptor guards the role, the service the
  ownership, RLS the tenant/location. A bug in one layer doesn't leak data.
