Skip to content

feat: audit fixes (security logging, JSON 405, parity hints, i18n, docs) + 1.6.0 - #74

Merged
MartinCastroAlvarez merged 9 commits into
mainfrom
feat/audit-fixes-1.6.0
Jun 2, 2026
Merged

feat: audit fixes (security logging, JSON 405, parity hints, i18n, docs) + 1.6.0#74
MartinCastroAlvarez merged 9 commits into
mainfrom
feat/audit-fixes-1.6.0

Conversation

@MartinCastroAlvarez

Copy link
Copy Markdown
Owner

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 / bandit all clean.

Fixes

Not implemented as code (by design)

Extra (cross-repo react #666)

  • The changelist wire already emits a top-level list_display_links array honoring None = no link (#251, tested). Corrected api-contract.md §3.2, which wrongly documented a per-column links boolean — it is a top-level array from get_list_display_links. Added/confirmed test coverage.

Verification

  • poetry run pytest -q613 passed (exit 0), 92% coverage
  • ruff check / ruff format --check / mypy / bandit → all clean

Closes #64 #65 #66 #67 #69 #70 #71 #72 #73

🤖 Generated with Claude Code

martin-castro-laminr-ai and others added 9 commits June 2, 2026 01:25
…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>
Maps the 1.6.0 Added/Changed/Fixed/Security entries to the audit issues
(#64 #65 #66 #67 #69 #70 #71 #72 #73) and records the #68 README-hardening
decision.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[docs] README + api-contract list wrong verb/path for bulk and delete-preview

2 participants