- #006 (high, data loss): make_room_for() no longer deletes unkept artifacts on a doomed request - checks whether eviction can possibly succeed before deleting anything. Also fixed a related latent bug found while testing: the function returned evicted Artifact objects after calling .delete() on them, so their .id was always None (Django clears pk on delete); now returns ids captured beforehand. - #003: added server-side field-length validation (source/title/ source_name/source_id/theme) on POST and PATCH, matching the model's own max_length limits, which were previously unenforced on the ingest path entirely. - #004: ported daybook_post.py's content-shape validation (html_fragment/html_full/deck/markdown) to the server, closing the gap for any client that doesn't go through the helper script. - #005: PATCH now rejects wrong-typed `tags` (previously silently wiped existing tags) and rejects immutable fields (content/source/kind/day) with 400 instead of silently no-op'ing them. - #002: deleted the misleading skills/daybook-posting/.env.example (a zero-dependency script that never reads .env files) and clarified SKILL.md/docs/llm_posting_helper.md that these must be real process/shell env vars. Adds tests/unit/ for storage-layer tests. 46 tests passing (was 25). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
68 lines
3.2 KiB
Markdown
68 lines
3.2 KiB
Markdown
# 003 — `CharField(max_length=...)` limits are silently unenforced on ingest
|
|
|
|
**Status:** Fixed
|
|
|
|
## Description
|
|
|
|
`artifacts/models.py` declares length caps on several `Artifact` fields —
|
|
`source` (80), `title` (200), `source_name` (120), `source_id` (120),
|
|
`theme` (40) — but nothing on the ingest path enforces them. `views.py`'s
|
|
`create_text_artifact`/`create_binary_artifact` call
|
|
`Artifact.objects.create(...)` directly, which does **not** run Django's
|
|
`full_clean()`/validators. SQLite (the DB in both dev and production per
|
|
`docs/deployment.md` line 197) also doesn't enforce `VARCHAR(n)` length as
|
|
a hard constraint on `INSERT`, so oversized values are written and read
|
|
back verbatim with no error at any layer.
|
|
|
|
Confirmed against the local `test` account:
|
|
|
|
1. `POST /api/artifacts` with a 250-character `title` (limit 200) →
|
|
`201`, title stored and returned in full, uncut.
|
|
2. `POST /api/artifacts` with a 150-character `source` (limit 80) →
|
|
`201`, `source` stored in full. Because `source_name` defaults to
|
|
`source` when omitted (`source_fields()` in `views.py`), this also
|
|
silently blew past `source_name`'s own 120-char limit in the same
|
|
request.
|
|
|
|
For contrast, other validation on the same endpoint works correctly and
|
|
returns a clean `400` — e.g. an invalid `tag_colors` hex value
|
|
(`{"tag_colors": {"urgent": "notacolor"}}` → `400` with a helpful
|
|
message). So the endpoint clearly has a validation convention; these
|
|
five fields just aren't part of it.
|
|
|
|
Practically: a title long enough could break feed-row layout in the UI;
|
|
`source` feeds directly into the on-disk storage path
|
|
(`artifacts_store/{source}/{day}/...`) and the sidebar/label system, so
|
|
an extreme value there has a larger blast radius than just cosmetics.
|
|
|
|
## Reproduction
|
|
|
|
```bash
|
|
curl -X POST http://127.0.0.1:8001/api/artifacts \
|
|
-H "Authorization: Bearer <test-account-token>" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"source\":\"edge-case-test\",\"title\":\"$(python3 -c 'print("A"*250)')\",\"kind\":\"markdown\",\"content\":\"x\"}"
|
|
# -> 201, title returned uncut at 250 chars
|
|
```
|
|
|
|
## Suggested Fix
|
|
|
|
Add explicit length checks alongside the existing required-field/kind
|
|
checks in `ArtifactListCreateView.post` and `ArtifactDetailView.patch`
|
|
(and the two `create_*_artifact` helpers), returning `400` with a message
|
|
naming the field and its limit — consistent with how `tag_colors` and
|
|
`source_type` are already handled. A shared `validate_field_lengths(data)`
|
|
helper (mirroring `validate_source_fields`) covering `source`, `title`,
|
|
`source_name`, `source_id`, `theme` would keep it in one place.
|
|
|
|
## Resolution
|
|
|
|
Added `validate_field_lengths(data, fields)` in `artifacts/views.py`, called
|
|
from `validate_source_fields` (covers `source`, `title`, `source_name`,
|
|
`source_id`, `theme` on both text and binary `POST`, since both paths already
|
|
routed through that function) and directly in `ArtifactDetailView.patch`
|
|
(covers `title`, `source_name`, `source_id` — the only length-limited fields
|
|
`PATCH` can touch). Returns `400` naming the field and its limit, matching
|
|
the existing `tag_colors`/`source_type` convention. Tests added in
|
|
`tests/api/test_artifacts.py` covering oversized `title`/`source` on create,
|
|
an exact-limit boundary case, and oversized `title` on patch.
|