{"record":{"id":"34c37b51d0c566a9","repo":"affaan-m/ECC","slug":"not-authorized","errorCode":null,"errorMessage":"Not authorized","messagePattern":"Not authorized","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"skills/fastapi-patterns/SKILL.md","lineNumber":276,"sourceCode":"    db: DbDep,\n    current_user: ActiveUserDep,\n    skip: Annotated[int, Query(ge=0)] = 0,\n    limit: Annotated[int, Query(ge=1, le=100)] = 20,\n) -> UserListResponse:\n    service = UserService(db)\n    users, total = await service.list(skip=skip, limit=limit)\n    return UserListResponse(total=total, items=users)\n\n\n@router.patch(\"/{user_id}\", response_model=UserResponse)\nasync def update_user(\n    user_id: int,\n    payload: UserUpdate,\n    db: DbDep,\n    current_user: ActiveUserDep,\n) -> UserResponse:\n    if current_user.id != user_id:\n        raise HTTPException(status_code=403, detail=\"Not authorized\")\n    service = UserService(db)\n    try:\n        user = await service.update(user_id, payload)\n    except DuplicateUserError:\n        raise HTTPException(status_code=400, detail=\"Email already registered\")\n    if user is None:\n        raise HTTPException(status_code=404, detail=\"User not found\")\n    return user\n\n\n@router.post(\"/token\")\nasync def login(\n    form_data: Annotated[OAuth2PasswordRequestForm, Depends()],\n    db: DbDep,\n) -> dict[str, str]:\n    service = UserService(db)\n    token = await service.authenticate(form_data.username, form_data.password)\n    if token is None:","sourceCodeStart":258,"sourceCodeEnd":294,"githubUrl":"https://github.com/affaan-m/ECC/blob/01e15490f04e29cfefe3896951f43db46994d8ee/skills/fastapi-patterns/SKILL.md#L258-L294","documentation":"In `update_user`, before delegating to the service, the handler checks `current_user.id != user_id` and raises `HTTPException(403, \"Not authorized\")`. This is an ownership check: a user may only patch their own record. There is no admin override in this snippet.","triggerScenarios":"Authenticated user calls `PATCH /users/{user_id}` with a `user_id` that is not their own (e.g. `/users/5` when their JWT subject is `12`). No admin role exists in this handler to bypass the check.","commonSituations":"Front-end constructs the URL from a stale user id (cached after re-login). Client tries to edit another user. Tests reuse one token for many user ids.","solutions":["Front-end: derive `{user_id}` from the JWT-decoded `current_user.id`, never from a route param the user can tamper with.","If admins must edit others, add a role check: `if current_user.id != user_id and not current_user.is_admin:`.","Return 403 silently — do not leak whether `user_id` exists.","Consider `PATCH /me` as the primary self-edit route and restrict `/{user_id}` to admins."],"exampleFix":"# before\nif current_user.id != user_id:\n    raise HTTPException(status_code=403, detail=\"Not authorized\")\n\n# after — allow admins, otherwise restrict to self\nif current_user.id != user_id and not current_user.is_admin:\n    raise HTTPException(status_code=403, detail=\"Not authorized\")","handlingStrategy":"validation","validationCode":"# derive the route from the JWT, never from user input\nconst me = await client.get('/users/me')\nawait client.patch(`/users/${me.id}`, payload)","typeGuard":"null","tryCatchPattern":"from fastapi import HTTPException\ntry:\n    await client.patch(f'/users/{uid}', json=payload)\nexcept HTTPException as e:\n    if e.status_code == 403:\n        show_error('You can only edit your own account.')\n    raise","preventionTips":["Prefer a `PATCH /me` route for self-edits; gate `/{user_id}` behind admin role.","Never let the client choose another user's id from a URL param.","Add an integration test that a non-owner gets 403."],"tags":["fastapi","auth","authorization","http-403","ownership"],"backgroundTag":null,"analyzedSha":"01e15490f04e29cfefe3896951f43db46994d8ee","analyzedAt":"2026-08-13T00:31:08.655Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}