Lesson 8 · Authorization — what may you do?

Scoped authorization in action

Putting Lessons 6 and 7 to work: resolve a permission to its granted locations, then gate a request by them.

Your win: use the permission model at runtime — turn "does this user hold user.user.read?" into a concrete set of location ids, and describe the interceptor that refuses requests touching un-granted locations.

Mechanism 1 — resolve a permission to locations

The join chain from Lesson 6 (plus the subtree match from Lesson 7) is wrapped in one repo call. Given a user and a permission name, it returns the location ids where that user holds that permission:

internal/bob/repositories/granted_permission.go:15
FindLocationIDsByUserIDAndPermissionName(ctx, db, userID, "user.user.read") ([]string, error)
// → ["org/A/A1", "org/A/A2", …]  every location in the granted subtree(s)

It's exposed across services as a gRPC endpoint so any service can ask bob the question:

internal/bob/services/accesscontrol/internal_access_control.go:24
InternalAccessControlService.RetrieveUserGrantedLocations(...)   // the shared "where may this user act?" API

With that list in hand, a service does one of two things: intersect it with the locations the request is asking about (reject if the request wants a location not in the set), or push it into the query as WHERE location_id = ANY($grantedLocationIDs). Either way, the data is filtered to where the caller actually has permission.

Mechanism 2 — the LocationRestricted interceptor

For endpoints that take a location_ids field, an interceptor enforces the check before the handler runs — a reusable gate between authN and the business logic:

internal/mastermgmt/pkg/interceptors/location_restricted.go:31 (condensed)
locationIDs := req.(LocationIDRequest).GetLocationIds()

if len(locationIDs) == 0 {                          // asked for nothing specific…
    root := locationRepo.GetRootLocation(ctx, db)   // …require ORG-ROOT access
    if len(root) == 0 {
        return "permission denied: user is not granted org level"
    }
} else {                                            // asked for specific locations…
    locations := locationRepo.GetLocationsByLocationIDs(ctx, db, locationIDs, false)
    if len(locations) != len(locationIDs) {         // …every one must be granted
        return "permission denied: some locations are not granted for user"
    }
}
return handler(ctx, req)
Two rules, one gate Empty location_ids → "you're asking broadly, so you must have org-root access." Specific location_ids → "every location you named must be one you're granted" — the check is len(granted) == len(requested), so a single un-granted location in the list fails the whole request. (The location repo it calls is itself RLS- and grant-scoped, so "get these locations" only returns the ones the caller may see.) Fail-closed on both branches.

Where this sits — and what's still missing

This is authorization tier 2 (Lesson 1's map): the service layer checking a granular permission against a location scope. It's enforced in Go, before the query. But notice it depends on the service remembering to call the check — a handler that forgot to resolve granted locations, or a raw query, could still over-read.

This is why Part 3 exists Tier 2 is the application saying "I'll only ask for what you're allowed." Row-Level Security (tier 3, Part 3) is the database saying "I'll only return what you're allowed, no matter what you ask." The same permission + location logic you just saw is also encoded as RLS policies, so even a forgetful handler or a raw SELECT is filtered. Two independent layers enforcing the same rule — defense in depth.
Read this next

Attribute-based access decisions

Resolving "does subject S have permission P on resource R (here: at location L)?" is the ABAC decision shape — the model this scoping implements.

NIST SP 800-162 — ABAC
RBAC vs ABAC · in-repo internal/bob/repositories/granted_permission.go, internal/mastermgmt/pkg/interceptors/location_restricted.go

Check yourself (from memory)

Q1. What does FindLocationIDsByUserIDAndPermissionName return?

It runs the join chain + subtree match and returns the granted location ids — what a service intersects with the request's locations.

Q2. A request to a LocationRestricted method sends an empty location_ids. What's required?

Empty = asking broadly → you must be granted at the org root, else "not granted org level." Fail-closed.

Q3. Why isn't this service-layer check sufficient on its own?

Tier 2 relies on the app remembering to check. Tier 3 (RLS) enforces the same rule in the DB regardless — defense in depth.
Recall: the two scoping mechanisms.
resolve + gate, then reveal
1. Resolve: FindLocationIDsByUserIDAndPermissionName(userID, "user.user.read") (granted_permission.go:15) runs the join chain + subtree match → the location ids where the user holds that permission; exposed as InternalAccessControlService.RetrieveUserGrantedLocations. Services intersect it with the request's locations or add WHERE location_id = ANY($granted). 2. Gate: the LocationRestricted interceptor (location_restricted.go:31): empty location_ids → require org-root; else every requested location must be granted (len(granted)==len(requested)). Both fail-closed. This is authZ tier 2 (app layer) — it depends on the handler calling it, which is why tier 3 (RLS) re-enforces the same rule in the DB (Part 3). Defense in depth.
🏁 Part 2 complete — authorization: roles, permissions, locations You can now explain all of "what may you do?" at the application layer: the coarse role gate (rbacDecider, and the panic myth), the granular permission join chain, the location tree with materialized-path subtrees, and how services resolve and enforce a permission-on-a-location. Two of the three authZ tiers, covered.
Part 3 goes to the last gate — Row-Level Security in the database: resource_path tenancy, the location-aware policies that mirror this lesson, the full request trace through every gate, and the rls_scan that guards it. Tell me "build Part 3" when you're ready — and re-take Part 2's quizzes cold tomorrow.

1. In-repo: internal/bob/repositories/granted_permission.go:15, internal/bob/services/accesscontrol/internal_access_control.go:24, internal/mastermgmt/pkg/interceptors/location_restricted.go:31. NIST ABAC.