Authentication establishes an identity through a trusted mechanism. Authorization decides whether that identity may perform a particular action on a particular resource. A session may identify user 12, but it does not establish that note 12 belongs to that user. Comparing similarly named numbers is not a substitute for identifying their roles.
The fixtures supply a principal from a trusted authentication boundary. It contains id, tenant_id, role and active. The resource independently contains id, tenant_id and owner_id. Request fields must never be used to overwrite principal attributes. Password storage, login throttling, recovery and identity verification are outside this lab; actual applications should use established authentication libraries and providers rather than inventing credential protocols.
Make the policy concrete before writing branches. Here every access requires an active principal, a matching tenant and one of two recognized actions, "read" or "update". Within that tenant, a member can read and update their own notes. An auditor can read any note and cannot update even a note they own. A tenant administrator can read and update any note in that tenant. Unknown roles, unknown actions and missing principals are denied.
principal = {"id": 12, "tenant_id": 7, "role": "member", "active": True}
note = {"id": 99, "tenant_id": 7, "owner_id": 12}
print(principal["id"] == note["owner_id"])
print(principal["id"] == note["id"])The first comparison represents ownership. The second compares an actor identifier with an object identifier and happens to be false here. Use fixtures where the numbers differ and where an unrelated object ID happens to equal an actor ID, so an accidental comparison cannot pass by coincidence.
A tenant administrator is not a global administrator. Evaluate tenant isolation before role exceptions. Otherwise a convenient early return True for administrators silently expands their authority to every tenant. Likewise, ownership cannot bypass a role whose purpose is read-only access. Conditions are not independent decorations: their order and combination express the policy.
The build also filters a list into visible IDs. A list route needs the same per-object decision as a detail route. Hiding an edit button in a browser or filtering a navigation menu is useful presentation, but the service must still enforce its policy for every access. Opaque or hard-to-guess object IDs can reduce casual discovery; possession or successful guessing of an ID grants no permission.
Test a matrix rather than a single successful login: same and different tenants, owner and non-owner, member and auditor, active and disabled, read and update. For each granted case, change one relevant attribute and predict whether the decision changes. These counterexamples explain the intended boundary and catch overbroad administrator branches or mistaken IDs.
The lab returns booleans and visible IDs, without fetching confidential content or mutating records. Later, the service-policy capstone places this decision beside a scoped query and a generic failure response. A local policy matrix provides useful regression evidence; it does not prove that every route, background task or database path in a deployed application actually calls the policy.