Access Control
CAL FIRE, county dispatch, and a dozen partner agencies all watch the ALERTCalifornia network, but not everyone should see — let alone move — every camera. Access control is how the Admin API decides who can, and its model is a small graph of three entities joined two ways.
The one idea to hold on to
A user can access a camera if it's assigned to them directly, or if it's assigned to an agency they belong to. Three entities — user, agency, camera — joined by three assignment tables:
So a user's effective camera set is the union of their direct UserCamera assignments and
every AgencyCamera for every agency they're in. UserAgencyCamera's endpoints are the CRUD
over that graph: user-insert, agency-insert, camera-insert, and the joins
user-agency-insert, agency-camera-insert, and the user-camera assignments — plus the
…-all reads that dump each table.
This resolved set is what the rest of the platform enforces: the Frontend API's camera
queries scope to it, and the Camera.Operate gate plus this allow-list together decide
whether a movement is even offered in the UI.
Because access is the union of direct and agency-inherited assignments, deleting a user's
UserCamera row doesn't necessarily cut their access: if their agency still has that camera via
AgencyCamera, they keep it. To actually revoke a user from a camera you have to consider both
paths. This surprises people who think of access as a single list per user — it's a computed
join, not a stored set.
Wrapping up
Access control is a three-entity graph — users, agencies, cameras — where a user's reach is the
union of their direct camera assignments and everything their agencies grant. The Admin API is
the CRUD over that graph; the Frontend API and the Camera.Operate role enforce the result. The
one thing to hold onto: access is a computed join, so revoking it means accounting for both the
direct and the agency path.
Establishing the identities those assignments point at is the next lesson: Onboarding a User.