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:
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:
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:
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)
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.
SELECT is filtered.
Two independent layers enforcing the same rule — defense in depth.
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?
Q2. A request to a LocationRestricted method sends an empty location_ids. What's required?
Q3. Why isn't this service-layer check sufficient on its own?
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.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.
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.