Commit c9af1fd
fix(api): resolve via ModelAdmin.get_object (honour consumer overrides) (#187)
`load_object_or_none` (used by detail GET, update PATCH, bulk PATCH,
delete) resolved the target via `get_queryset().get(pk=pk)`. Django's
own change view resolves via `ModelAdmin.get_object(request, object_id)`
— and `get_object` is a documented extension point consumers override.
Django's default `get_object` *is* `get_queryset().get(...)`, so the
default security posture is unchanged. But a consumer that overrides
`get_object` to bypass a list-only filter (so an individual record
stays openable even when hidden from the list) was not honoured: the
SPA 404'd a row the legacy admin opens.
Observed in the laminr pilot: `LoanPackageAdmin.get_queryset` excludes
test-tenant packages (list scoping), but its `get_object` deliberately
bypasses that filter for the change view. The SPA detail 404'd those
packages; the legacy admin opens them.
Fix: `load_object_or_none` now calls `model_admin.get_object(request,
str(pk))`. The views still gate the returned object on
`has_view_permission` / `has_change_permission` / `has_delete_permission`,
so using `get_object` does NOT widen access — it only fixes *which
object resolves*, consistent with Django. Defensive except still
collapses DoesNotExist / ValidationError / ValueError / TypeError to
None → 404 (never 500).
Test (test_detail.py::test_detail_resolves_via_get_object_not_get_queryset):
get_queryset returns none() while get_object returns the row; detail
must 200. 52/52 detail+update+bulk+delete tests pass.
Co-authored-by: Martin Castro Laminrs <mcastro@laminr.ai>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent b7aae8d commit c9af1fd
2 files changed
Lines changed: 59 additions & 16 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| 41 | + | |
41 | 42 | | |
42 | 43 | | |
43 | 44 | | |
| |||
138 | 139 | | |
139 | 140 | | |
140 | 141 | | |
141 | | - | |
142 | | - | |
143 | | - | |
144 | | - | |
145 | | - | |
146 | | - | |
147 | | - | |
148 | | - | |
149 | | - | |
150 | | - | |
151 | | - | |
152 | | - | |
153 | | - | |
154 | | - | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
155 | 166 | | |
156 | 167 | | |
157 | | - | |
158 | | - | |
| 168 | + | |
| 169 | + | |
159 | 170 | | |
160 | 171 | | |
161 | 172 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
21 | 53 | | |
22 | 54 | | |
23 | 55 | | |
| |||
0 commit comments