ЁЯПл The SchoolтА║ЁЯЫбя╕П SecurityтА║ЁЯЪк рдзрдбрд╛ 06 тАФ Authorization рдЖрдгрд┐ IDOR: рдХрд╛рдп рдХрд░рдгреНрдпрд╛рдЪреА рдкрд░рд╡рд╛рдирдЧреА, рдХреЛрдгрддреНрдпрд╛ record рд╡рд░
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯЪк рдзрдбрд╛ 06 тАФ Authorization рдЖрдгрд┐ IDOR: рдХрд╛рдп рдХрд░рдгреНрдпрд╛рдЪреА рдкрд░рд╡рд╛рдирдЧреА, рдХреЛрдгрддреНрдпрд╛ record рд╡рд░

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 16 рдкреИрдХреА рдзрдбрд╛ 06 ┬╖ рдорд╛рдЧреЗ: lesson-05-oauth-oidc ┬╖ рдкреБрдвреЗ: lesson-07-secrets-kms


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ05, рдЖрдгрд┐ рдУрд│рдЦрдкрддреНрд░ рдЦрд┐рдбрдХреАрдЪрд╛ рд╢реЗрд╡рдЯрдЪрд╛ рдзрдбрд╛. рдПрдЦрд╛рджреА рд╡реНрдпрдХреНрддреА рдХреЛрдг рдЖрд╣реЗ рд╣реЗ рдорд╛рд╣реАрдд рдЕрд╕рд▓реНрдпрд╛рдиреЗ рддреА рдХрд╛рдп рдХрд░реВ рд╢рдХрддреЗ рд╣реЗ рдХрд│рдд рдирд╛рд╣реА. рддреБрдореНрд╣реА RBAC (roles тЖТ permissions) рд▓рд┐рд╣рд┐рддрд╛, рдордЧ рддреЗ рдкреБрд░реЗрд╕реЗ рдХрд╛ рдирд╛рд╣реА рддреЗ рдкрд╛рд╣рддрд╛: рджреАрдкрд┐рдХрд╛ рдкрд╛рд▓рдХ рдЖрд╣реЗ, рдкрд╛рд▓рдХ grades рд╡рд╛рдЪреВ рд╢рдХрддрд╛рдд тАФ рдкрдг рдХреЛрдгрд╛рдЪреНрдпрд╛ grades? Object-level рддрдкрд╛рд╕рдгреА рдпрд╛рдЪреЗ рдЙрддреНрддрд░ рджреЗрддреЗ, рдЖрдгрд┐ IDOR / BOLA рдерд╛рдВрдмрд╡рддреЗ. sec/demo.py рдордзрд▓реЗ authz() рд╣реЗ рджрд╛рдЦрд╡рддреЗ.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рджреАрдкрд┐рдХрд╛ рдкрд╛рд▓рдХ рдЖрд╣реЗ. рддрд┐рдЪреНрдпрд╛ badge рд╡рд░ PARENT рд▓рд┐рд╣рд┐рд▓реЗ рдЖрд╣реЗ. рдкрд╛рд▓рдХ рдкреНрд░рдЧрддреАрдкреБрд╕реНрддрдХреЗ рдкрд╛рд╣реВ рд╢рдХрддрд╛рдд. рдореНрд╣рдгреВрди records room рддрд┐рд▓рд╛ рдЖрдд рдпреЗрдК рджреЗрддреЗ.

рдкреНрд░рдЧрддреАрдкреБрд╕реНрддрдХреЗ рдХреНрд░рдорд╛рдВрдХ рдЕрд╕рд▓реЗрд▓реНрдпрд╛ рдЦрдгрд╛рдВрдордзреНрдпреЗ рдЖрд╣реЗрдд. рджреАрдкрд┐рдХрд╛ "aishwarya" рдЦрдг рдорд╛рдЧрддреЗ тАФ рддрд┐рдЪреНрдпрд╛ рдореБрд▓реАрдЪрд╛. рдареАрдХ рдЖрд╣реЗ.

рдордЧ рддреА "katrina" рдЦрдг рдорд╛рдЧрддреЗ. рддрд┐рдЪреНрдпрд╛ badge рд╡рд░ рдЕрдЬреВрдирд╣реА PARENT рдЖрд╣реЗ. рдХрд╛рд░рдХреВрди рдлрдХреНрдд badge рдкрд╛рд╣рдд рдЕрд╕реЗрд▓, рддрд░ рддреЛ рдЦрдг рдЙрдШрдбрддреЛ. рд╣реАрдЪ рдЪреВрдХ рдЖрд╣реЗ. Badge "рдкрд╛рд▓рдХ рдкреНрд░рдЧрддреАрдкреБрд╕реНрддрдХреЗ рд╡рд╛рдЪреВ рд╢рдХрддрд╛рдд рдХрд╛?" рдпрд╛рдЪреЗ рдЙрддреНрддрд░ рджреЗрддреЛ. "рд╣реЗ рдкреНрд░рдЧрддреАрдкреБрд╕реНрддрдХ рддрд┐рдЪреЗ рдЖрд╣реЗ рдХрд╛?" рдпрд╛рдЪреЗ рдЙрддреНрддрд░ рддреЛ рджреЗрдд рдирд╛рд╣реА.

рдореНрд╣рдгреВрди security office рджреБрд╕рд░рд╛ рдкреНрд░рд╢реНрди рдЬреЛрдбрддреЗ, рдкреНрд░рддреНрдпреЗрдХ рдЦрдгрд╛рд╕рд╛рдареА, рдкреНрд░рддреНрдпреЗрдХ рд╡реЗрд│реА: "рдпрд╛ рдЦрдгрд╛рддрд▓реА рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреА рддреБрдордЪреНрдпрд╛ рдореБрд▓рд╛рдВрдкреИрдХреА рдПрдХ рдЖрд╣реЗ рдХрд╛?" рдХрддрд░рд┐рдирд╛ рджреАрдкрд┐рдХрд╛рдЪреА рдореБрд▓рдЧреА рдирд╛рд╣реА тЖТ рдЦрдг рдмрдВрджрдЪ рд░рд╛рд╣рддреЛ.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    d["ЁЯСй Dipika<br/>role: parent<br/>children: aishwarya"]
    d --> r1["GET /grades/aishwarya"]
    d --> r2["GET /grades/katrina"]
    r1 --> rb{"RBAC<br/>parent тЖТ grades:read?"}
    r2 --> rb
    rb -->|"yes for both"| obj{"object check<br/>pupil in her children?"}
    obj -->|"aishwarya: yes"| ok["тЬЕ 200 тАФ her daughter's record"]
    obj -->|"katrina: no"| no["тЭМ 404 тАФ stops IDOR / BOLA"]

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрд╡реГрддреНрддреА + рдПрдХ lab: https://school-edh.pages.dev/security/lesson-diagrams.html#l06

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рд╣рд╛ bug tests рдордзреВрди рд╕реБрдЯрддреЛ. рддреНрдпрд╛рддрд▓реА рдкреНрд░рддреНрдпреЗрдХ request рдпреЛрдЧреНрдпрд░рд┐рддреНрдпрд╛ login рдХреЗрд▓реЗрд▓реА рдЕрд╕рддреЗ, рдпреЛрдЧреНрдп role рдЕрд╕рд▓реЗрд▓реА рдЕрд╕рддреЗ, рдЖрдгрд┐ рдЦрд▒реНрдпрд╛ endpoint рд╡рд░ рдЬрд╛рддреЗ. рдлрдХреНрдд id рд╡реЗрдЧрд│рд╛ рдЕрд╕рддреЛ. Automated scanners рддреЗ рдХреНрд╡рдЪрд┐рддрдЪ рд╢реЛрдзрддрд╛рдд тАФ record 1043 рджреБрд╕рд▒реНрдпрд╛ рдХреЛрдгрд╛рдЪрд╛ рдЖрд╣реЗ рд╣реЗ рддреНрдпрд╛рдВрдирд╛ рдорд╛рд╣реАрдд рдирд╕рддреЗ. рдЖрдгрд┐ ids рдмрджрд▓рдгреЗ рд╕реЛрдкреЗ рдЕрд╕рддреЗ: URL рдордзрд▓рд╛ рдПрдХ number, JSON рдордзрд▓реЗ рдПрдХ field, рд▓рдкрд╡рд▓реЗрд▓реА form value. Random ids (UUIDs) рдореБрд│реЗ рдЕрдВрджрд╛рдЬ рдХрд░рдгреЗ рдХрдареАрдг рд╣реЛрддреЗ, рдкрдг рддреА рддрдкрд╛рд╕рдгреА рдирд╛рд╣реА.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

sec/identity.py рдордзреНрдпреЗ: can(role, permission) permission ROLES рдордзреНрдпреЗ рд╢реЛрдзрддреЗ (рдЕрдиреЛрд│рдЦреА role рд▓рд╛ рд░рд┐рдХрд╛рдорд╛ set рдорд┐рд│рддреЛ). can_read_record(user, record) рд╣реА object-level рддрдкрд╛рд╕рдгреА рдЖрд╣реЗ: parent тЖТ record["pupil"] in user["children"]; teacher тЖТ record["class"] in user["classes"]; рдлрдХреНрдд admin рд▓рд╛ рд╕рдЧрд│реЗ рджрд┐рд╕рддреЗ. sec/demo.py рдордзрд▓реЗ authz() рджреЛрдиреНрд╣реА requests рд╕рд╛рдареА RBAC рдкрд╛рд╕ рд╣реЛрддрд╛рдирд╛, рдЖрдгрд┐ object рддрдкрд╛рд╕рдгреА рдирд┐рд░реНрдгрдп рдШреЗрддрд╛рдирд╛ рджрд╛рдЦрд╡рддреЗ.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 sec/demo.py authz
python3 - <<'EOF'
import sys; sys.path.insert(0, "sec"); from identity import can, can_read_record
people = {"Dipika (parent)": dict(role="parent", children={"aishwarya"}),
          "teacher of 3A": dict(role="teacher", classes={"3A"}),
          "teacher of 4B": dict(role="teacher", classes={"4B"}),
          "office admin": dict(role="admin")}
records = [dict(pupil="aishwarya", **{"class": "3A"}), dict(pupil="katrina", **{"class": "4B"})]
for who, u in people.items():
    rows = " ┬╖ ".join(f"{r['pupil']} {can_read_record(u, r)}" for r in records)
    print(f"{who:<16} grades:read {can(u['role'], 'grades:read')!s:<5} тЖТ {rows}")
print("unknown role 'visitor' grades:read тЖТ", can("visitor", "grades:read"))
EOF

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

authz рд╣реЗ print рдХрд░рддреЗ:

тФАтФА RBAC: parent may grades:read True ┬╖ grades:write False ┬╖ teacher grades:write True
тФАтФА GET /grades/{id}: the role check passes for both тАФ the OBJECT check is what matters
   Dipika (parent of Aishwarya) reads Aishwarya's record тЖТ True
   Dipika changes the id in the URL to Katrina's record  тЖТ False  (without this check: IDOR / BOLA)
   check ownership on the server for every object, every time тАФ never trust an id from the client

рддреБрдордЪрд╛ snippet рд╣реЗ print рдХрд░рддреЛ:

Dipika (parent)  grades:read True  тЖТ aishwarya True ┬╖ katrina False
teacher of 3A    grades:read True  тЖТ aishwarya True ┬╖ katrina False
teacher of 4B    grades:read True  тЖТ aishwarya False ┬╖ katrina True
office admin     grades:read True  тЖТ aishwarya True ┬╖ katrina True
unknown role 'visitor' grades:read тЖТ False

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

рдЪрд╛рд░рд╣реА users role рддрдкрд╛рд╕рдгреА рдкрд╛рд╕ рдХрд░рддрд╛рдд тАФ role column рдкреНрд░рддреНрдпреЗрдХрд╛рд╕рд╛рдареА True рдореНрд╣рдгрддреЛ. рддреНрдпрд╛рдВрдирд╛ рд╡реЗрдЧрд│реЗ рдХрд░рддреЗ рддреА object рддрдкрд╛рд╕рдгреА: рдкреНрд░рддреНрдпреЗрдХ teacher рд▓рд╛ рдлрдХреНрдд рд╕реНрд╡рддрдГрдЪрд╛ рд╡рд░реНрдЧ рджрд┐рд╕рддреЛ, рдкрд╛рд▓рдХрд╛рд▓рд╛ рдлрдХреНрдд рддрд┐рдЪреЗ рд╕реНрд╡рддрдГрдЪреЗ рдореВрд▓. RBAC "рдХреЛрдгрддреНрдпрд╛ рдкреНрд░рдХрд╛рд░рдЪреА рдХреГрддреА" рдпрд╛рдЪреЗ рдЙрддреНрддрд░ рджреЗрддреЗ; object рддрдкрд╛рд╕рдгреА "рдХреЛрдгрддреНрдпрд╛ record рд╡рд░" рдпрд╛рдЪреЗ рдЙрддреНрддрд░ рджреЗрддреЗ. API рд▓рд╛ рджреЛрдиреНрд╣реА рд▓рд╛рдЧрддрд╛рдд.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

рдЦрд▒реНрдпрд╛ account рд╡рд░ тАФ ownership query рдордзреНрдпреЗрдЪ рдареЗрд╡рд╛, рдореНрд╣рдгрдЬреЗ рдирд╕рд▓реЗрд▓рд╛ record рдЖрдгрд┐ рдордирд╛рдИ рдЕрд╕рд▓реЗрд▓рд╛ record рд╕рд╛рд░рдЦреЗрдЪ рджрд┐рд╕рддрд╛рдд (Python + psycopg):

cur.execute("""
    SELECT g.subject, g.grade FROM grades g
    JOIN guardians gu ON gu.pupil_id = g.pupil_id
    WHERE g.pupil_id = %s AND gu.parent_id = %s""", (pupil_id, current_user.id))
rows = cur.fetchall()
if not rows: abort(404)

Django ORM рд╕рд╣ рд╣реЗрдЪ тАФ filter рдордзреНрдпреЗ рдкреНрд░рддреНрдпреЗрдХ рд╡реЗрд│реА user рдЕрд╕рддреЛ:

grade = get_object_or_404(Grade, pk=pk, pupil__guardians__user=request.user)

рдХрд┐рдВрд╡рд╛ PostgreSQL row-level security рдиреЗ database рд▓рд╛рдЪ рд╣реЗ enforce рдХрд░реВ рджреНрдпрд╛ тАФ рд╡рд┐рд╕рд░рд▓реЗрд▓рд╛ filter рд╕реБрджреНрдзрд╛ рджреБрд╕рд▒реНрдпрд╛ рдХреБрдЯреБрдВрдмрд╛рдЪреНрдпрд╛ rows рдкрд░рдд рджреЗрдК рд╢рдХрдд рдирд╛рд╣реА:

ALTER TABLE grades ENABLE ROW LEVEL SECURITY;
CREATE POLICY parent_reads_own ON grades FOR SELECT
  USING (pupil_id IN (SELECT pupil_id FROM guardians
                      WHERE parent_id = current_setting('app.user_id')::int));
-- at the start of each request, in the same transaction, the app runs:
--   SELECT set_config('app.user_id', %s, true)   with the user id as a bound parameter

рддрдкрд╛рд╕рдгреА рдЕрд╕реНрддрд┐рддреНрд╡рд╛рдд рдЖрд╣реЗ рд╣реЗ рд╕рд┐рджреНрдз рдХрд░рдгрд╛рд░реА test (pytest) тАФ рдПрдХ user, рджреБрд╕рд▒реНрдпрд╛ user рдЪрд╛ id:

def test_parent_cannot_read_other_pupil(client, login_as):
    login_as("dipika")
    assert client.get("/grades/aishwarya").status_code == 200
    assert client.get("/grades/katrina").status_code == 404

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: object рддрдкрд╛рд╕рдгреА рд╡рд┐рд╕рд░рдгреЗ рдЕрд╢рдХреНрдп рдХрд░рд╛ тАФ рдиреЗрд╣рдореА рд╕рдзреНрдпрд╛рдЪрд╛ user рдШреЗрдгрд╛рд░реА repository method, рдХрд┐рдВрд╡рд╛ row-level security тАФ рдЖрдгрд┐ id рдШреЗрдгрд╛рд▒реНрдпрд╛ рдкреНрд░рддреНрдпреЗрдХ endpoint рд╕рд╛рдареА рдПрдХ "рджреБрд╕рд▒реНрдпрд╛ user рдЪрд╛ id" test рдЬреЛрдбрд╛. рдЕрдиреЗрдХ services рдордзреНрдпреЗ рдмрд╛рд░реАрдХ ABAC рд╕рд╛рдареА, policy engine (Open Policy Agent, Cedar / Amazon Verified Permissions) рдирд┐рдпрдо рдПрдХрд╛рдЪ test рдХреЗрд▓реЗрд▓реНрдпрд╛ рдард┐рдХрд╛рдгреА рдареЗрд╡рддреЗ.

тПня╕П рдкреБрдвреЗ

рдУрд│рдЦрдкрддреНрд░ рдЦрд┐рдбрдХреА рдкреВрд░реНрдг рдЭрд╛рд▓реА. рдЖрддрд╛ рдЗрдорд╛рд░рдд рдЦрд┐рдбрдХреА: рдкреВрд░реНрдг рдЗрдорд╛рд░рддреАрдЪреНрдпрд╛ рдХрд┐рд▓реНрд▓реНрдпрд╛. Database passwords рдЖрдгрд┐ encryption keys рдХреБрдареЗ рд░рд╛рд╣рддрд╛рдд тАФ рдЖрдгрд┐ code рдордзреНрдпреЗ рдХрдзреАрдЪ рдХрд╛ рдирд╛рд╣реАрдд?

git checkout lesson-07-secrets-kms

ЁЯЪк Lesson 06 тАФ Authorization and IDOR: allowed to do WHAT, to WHICH record

ЁЯУН You are here: Lesson 06 of 16 ┬╖ Previous: lesson-05-oauth-oidc ┬╖ Next: lesson-07-secrets-kms


ЁЯУж What's in this branch

Lessons 01тАУ05, plus the last lesson of the ID desk. Knowing who someone is does not say what they may do. You write RBAC (roles тЖТ permissions), then see why it is not enough: Dipika is a parent, parents may read grades тАФ but whose grades? The object-level check answers that, and stops IDOR / BOLA. authz() in sec/demo.py shows it.

ЁЯзТ Explain like I'm 5

Dipika is a parent. Her badge says PARENT. Parents may look at report cards. So the records room lets her in.

The report cards are in drawers with numbers. Dipika asks for drawer "aishwarya" тАФ her daughter's. Fine.

Then she asks for drawer "katrina". Her badge still says PARENT. If the clerk only looks at the badge, he opens the drawer. That is the mistake. The badge answers "may parents read report cards?". It does not answer "is this report card hers?".

So the security office adds a second question, for every drawer, every time: "Is the pupil in this drawer one of your children?" Katrina is not Dipika's child тЖТ the drawer stays closed.

ЁЯЧ║я╕П Diagram

flowchart LR
    d["ЁЯСй Dipika<br/>role: parent<br/>children: aishwarya"]
    d --> r1["GET /grades/aishwarya"]
    d --> r2["GET /grades/katrina"]
    r1 --> rb{"RBAC<br/>parent тЖТ grades:read?"}
    r2 --> rb
    rb -->|"yes for both"| obj{"object check<br/>pupil in her children?"}
    obj -->|"aishwarya: yes"| ok["тЬЕ 200 тАФ her daughter's record"]
    obj -->|"katrina: no"| no["тЭМ 404 тАФ stops IDOR / BOLA"]

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/security/lesson-diagrams.html#l06

тЭУ What

ЁЯдФ Why

Because this is the bug that tests miss. Every request in it is correctly logged in, has the right role, and hits a real endpoint. Only the id is different. Automated scanners rarely find it тАФ they do not know that record 1043 belongs to someone else. And ids are easy to change: a number in a URL, a field in JSON, a hidden form value. Random ids (UUIDs) make guessing harder, but they are not a check.

ЁЯФз How (in this repo)

In sec/identity.py: can(role, permission) looks the permission up in ROLES (an unknown role gets an empty set). can_read_record(user, record) is the object-level check: parent тЖТ record["pupil"] in user["children"]; teacher тЖТ record["class"] in user["classes"]; only admin sees all. authz() in sec/demo.py shows RBAC passing for both requests, and the object check deciding.

ЁЯзк Try it

python3 sec/demo.py authz
python3 - <<'EOF'
import sys; sys.path.insert(0, "sec"); from identity import can, can_read_record
people = {"Dipika (parent)": dict(role="parent", children={"aishwarya"}),
          "teacher of 3A": dict(role="teacher", classes={"3A"}),
          "teacher of 4B": dict(role="teacher", classes={"4B"}),
          "office admin": dict(role="admin")}
records = [dict(pupil="aishwarya", **{"class": "3A"}), dict(pupil="katrina", **{"class": "4B"})]
for who, u in people.items():
    rows = " ┬╖ ".join(f"{r['pupil']} {can_read_record(u, r)}" for r in records)
    print(f"{who:<16} grades:read {can(u['role'], 'grades:read')!s:<5} тЖТ {rows}")
print("unknown role 'visitor' grades:read тЖТ", can("visitor", "grades:read"))
EOF

тЬЕ Verify тАФ what you should see

authz prints:

тФАтФА RBAC: parent may grades:read True ┬╖ grades:write False ┬╖ teacher grades:write True
тФАтФА GET /grades/{id}: the role check passes for both тАФ the OBJECT check is what matters
   Dipika (parent of Aishwarya) reads Aishwarya's record тЖТ True
   Dipika changes the id in the URL to Katrina's record  тЖТ False  (without this check: IDOR / BOLA)
   check ownership on the server for every object, every time тАФ never trust an id from the client

Your snippet prints:

Dipika (parent)  grades:read True  тЖТ aishwarya True ┬╖ katrina False
teacher of 3A    grades:read True  тЖТ aishwarya True ┬╖ katrina False
teacher of 4B    grades:read True  тЖТ aishwarya False ┬╖ katrina True
office admin     grades:read True  тЖТ aishwarya True ┬╖ katrina True
unknown role 'visitor' grades:read тЖТ False

ЁЯПБ What you just proved

All four users pass the role check тАФ the role column says True for everyone. The object check is what separates them: each teacher sees only their own class, the parent only her own child. RBAC answers "what kind of action"; the object check answers "on which record". An API needs both.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ put the ownership into the query, so a missing record and a forbidden record look the same (Python + psycopg):

cur.execute("""
    SELECT g.subject, g.grade FROM grades g
    JOIN guardians gu ON gu.pupil_id = g.pupil_id
    WHERE g.pupil_id = %s AND gu.parent_id = %s""", (pupil_id, current_user.id))
rows = cur.fetchall()
if not rows: abort(404)

The same with the Django ORM тАФ the filter includes the user, every time:

grade = get_object_or_404(Grade, pk=pk, pupil__guardians__user=request.user)

Or let the database enforce it with PostgreSQL row-level security тАФ even a forgotten filter cannot return another family's rows:

ALTER TABLE grades ENABLE ROW LEVEL SECURITY;
CREATE POLICY parent_reads_own ON grades FOR SELECT
  USING (pupil_id IN (SELECT pupil_id FROM guardians
                      WHERE parent_id = current_setting('app.user_id')::int));
-- at the start of each request, in the same transaction, the app runs:
--   SELECT set_config('app.user_id', %s, true)   with the user id as a bound parameter

A test that proves the check exists (pytest) тАФ one user, another user's id:

def test_parent_cannot_read_other_pupil(client, login_as):
    login_as("dipika")
    assert client.get("/grades/aishwarya").status_code == 200
    assert client.get("/grades/katrina").status_code == 404

ЁЯПн Why this matters in production: make the object check impossible to forget тАФ a repository method that always takes the current user, or row-level security тАФ and add one "other user's id" test for every endpoint that takes an id. For fine-grained ABAC across many services, a policy engine (Open Policy Agent, Cedar / Amazon Verified Permissions) keeps the rules in one tested place.

тПня╕П Next

The ID desk is done. Now the building desk: the keys to the whole building. Where do database passwords and encryption keys live тАФ and why never in the code?

git checkout lesson-07-secrets-kms
тЖР Previousoauth oidcNext тЖТsecrets kms

This page is the lesson's README from the lesson-06-authorization branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.