Authorization is decided before the handler, and travels with its cacheability
A response cached for one principal must never be served to another. That holds only if the authorization decision and the cache metadata describing what it depended on stay together.
The rule
- Authorization for a run's job surfaces is decided before the handler runs, and the decision's cache metadata travels with it, so a response cached for one principal cannot be served to another.
- The decision is the entity's own authorization result, forwarded unchanged rather than reduced to a boolean; a handler must not re-implement a subset of the model, because doing so both locks out principals the surface admits and ignores extension points the model honours.
- Authority over a job is not authority over a run: a run the caller may not view is refused before anything about it (its jobs, its summary, its label) is assembled.
- Absence is answered before denial, so a request for an identifier that does not exist is
404even where a caller holding the surface's authority could not have viewed it; this is a decided trade, and identifiers must therefore carry no information beyond their existence.
What it means
Absence is answered before denial: a job identifier that resolves to nothing gets the documented not-found response even for a caller who, had the job existed, could not have viewed it. That is a deliberate trade — identifiers carry no information beyond their existence — not an oversight, so an identifier must not be built so that its shape alone reveals anything.
Authority over a job and authority over the run it belongs to are separate questions: holding one is not evidence for the other, so a run the caller may not view is refused before anything about it, its jobs included, is assembled.
Example
A job identifier that does not exist gets the same shape regardless of what the caller could have seen had it existed.
GET /api/flowdrop/job/{job_id}
{"error": "Job not found", "message": "…"}