feat: audit fixes (security logging, JSON 405, parity hints, i18n, docs) + 1.6.0 - #74
Merged
Merged
Conversation
…ct (#64) - README endpoints table: bulk is PATCH .../bulk/ (was POST .../bulk-update/); delete-preview is GET .../<pk>/delete-preview/ (was POST, missing <pk>). - api-contract.md §6: delete-preview row corrected to GET .../<pk>/...; §5.5 heading corrected to PATCH. - Extend tests/test_doc_references.py with a verb/path drift guard that resolves every documented METHOD /path row against api/urls.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Views previously returned Django's bare HttpResponseNotAllowed (HTML/empty
body), breaking the uniform error envelope and the method_not_allowed code
the OpenAPI schema advertises. Add a shared BaseAPIView overriding
http_method_not_allowed to return {"error":{"code":"method_not_allowed",...}}
with the Allow header (and Cache-Control: no-store) preserved. Every API
view (leaf + the urls.py dispatch views) now subclasses it.
Document the code in api-contract.md §1.1. Add tests/test_method_not_allowed.py
and strengthen the existing password 405 test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#67) Add a dedicated django_admin_rest_api.security logger that emits one structured record per 403 denial (permissions.forbidden_response) and per failed login (views/auth), carrying {user, path, method, decision}. The password and request-body PII are never logged; only denials are emitted. Document the logger + a LOGGING snippet in SECURITY.md. Add tests asserting the record fires and that the password never leaks into it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…DATES (#69) bulk.py hardcoded _BULK_MAX_UPDATES = 200. Replace it with a tunable conf.MAX_BULK_UPDATES setting (mirrors MAX_ACTION_PKS) that defaults to MAX_PAGE_SIZE when unset, so lowering MAX_PAGE_SIZE for DoS reasons tightens the bulk cap too and a "save whole page" workflow always fits. 0 disables it. Document in api-contract §5.5 + SECURITY.md; update the cap test and add a default-tracking test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…71) docs(security): document form-spec GET-idempotency requirement (#70) #71: inline child rows previously issued an N+1 on FK columns (one query per row) and per-row M2M reads. Apply select_related for forward FK/O2O inline columns and prefetch_related for M2M before iterating, mirroring list.py. Add an inline query-count regression test. #70: document (SECURITY.md + form_spec.py comment) that an overridden change_view/add_view is invoked on a GET by the form-spec introspection probe and must therefore stay GET-idempotent — Django's own contract. Doc/comment only; no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(a) The add form-spec (build_form_spec, obj=None) now emits prepopulated_fields
({target: [sources]}), matching the add-form schema endpoint, so a client
rendering from the form-spec can slugify-on-keystroke. Shared via a new
form_spec.prepopulated_fields_payload helper (create_form delegates to it).
(b) Detail / create-form field descriptors gain a widget:"autocomplete" hint
when the field is in get_autocomplete_fields(request) AND the target admin
declares search_fields (Django's requirement for the typeahead to work).
Document both in api-contract §4 / §4.1. Add unit + integration tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ocaleMiddleware (#73) Error/UI envelope strings (Not found., You do not have permission., session expired, invalid credentials, conflict, validation/bad-request defaults) are now gettext_lazy proxies that JsonResponse resolves against the request-active locale. Machine-readable codes stay plain ASCII. Document in README that LocaleMiddleware drives request-locale activation (envelopes + verbose_names), with ordering guidance. Add tests/test_i18n.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…display_links wire (EXTRA) #66: add an honest Honored/Partial/Not-yet matrix to the README covering the ModelAdmin surface the package supports, including the known gaps (autocomplete authorization breadth, prepopulated, generic inlines, date-range list_filter, get_urls custom views, legacy-iframe fallback). EXTRA (react #666): the changelist wire already emits a top-level list_display_links array honoring None=no-link (#251, tested). Correct api-contract §3.2 — it wrongly documented a per-column `links` boolean; it is a top-level array resolved from get_list_display_links. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jun 2, 2026
[parity] Emitted strings not translatable; no request-locale activation or LocaleMiddleware docs
#73
Closed
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the second-audit findings for 1.6.0 (security + UX/arch). Every change keeps the "thin JSON layer over django-admin" thesis; no new permission/attribute conventions. Full suite: 613 passed (was 597),
ruff/ruff format/mypy/banditall clean.Fixes
bulkisPATCH /api/v1/<app>/<model>/bulk/(wasPOST .../bulk-update/);delete-previewisGET /api/v1/<app>/<model>/<pk>/delete-preview/(wasPOST, missing<pk>). Same delete-preview fix inapi-contract.md§6; §5.5 heading nowPATCH.tests/test_doc_references.pyextended with a verb/path drift guard that resolves every documentedMETHOD /pathrow (README + contract) against the realapi/urls.pyroute table.BaseAPIViewoverrideshttp_method_not_allowedto emit the canonical{"error":{"code":"method_not_allowed",…}}JSON with theAllowheader preserved; all leaf + dispatch views inherit it. Documented in contract §1.1. Newtests/test_method_not_allowed.py.django_admin_rest_api.securitylogger emits one structured record at each 403 denial and failed login:{user, path, method, decision}, never the password / PII, denials only. SECURITY.md documents it with aLOGGINGsnippet. Newtests/test_security_logging.py(incl. a "password never in the record" assertion).MAX_BULK_UPDATESsetting (defaults toMAX_PAGE_SIZE,0disables), replacing the hardcoded200. MirrorsMAX_ACTION_PKS. Docs + tests updated.form_spec.pycomment) that an overriddenchange_view/add_viewis invoked on a GET by the form-spec probe and must stay GET-idempotent (Django's own contract). Doc/comment only.select_relatedforward FK/O2O columns andprefetch_relatedM2M columns before iterating (mirrorslist.py). New inline query-count regression test (verified it fails without the fix).prepopulated_fieldsnow also on the add form-spec (shared helper;/add/schema delegates to it); (b)widget:"autocomplete"hint on detail/create-form descriptors when the field is inget_autocomplete_fields(request)and the target admin declaressearch_fields. Contract §4/§4.1 + tests.gettext_lazy(resolve to request-active locale; codes stay ASCII). README documents theLocaleMiddlewarerequirement + ordering. Newtests/test_i18n.py.Not implemented as code (by design)
django-axes(with theAUTHENTICATION_BACKENDSnote) anddjango-ratelimitconfigs pointed at/api/v1/login/.Extra (cross-repo react #666)
list_display_linksarray honoringNone= no link (#251, tested). Correctedapi-contract.md§3.2, which wrongly documented a per-columnlinksboolean — it is a top-level array fromget_list_display_links. Added/confirmed test coverage.Verification
poetry run pytest -q→ 613 passed (exit 0), 92% coverageruff check/ruff format --check/mypy/bandit→ all cleanCloses #64 #65 #66 #67 #69 #70 #71 #72 #73
🤖 Generated with Claude Code