fix(admin): reject values that break the app - #886
Merged
Merged
Conversation
Three holes where a direct API call could save a value the app cannot survive. The client's rules were the only thing in the way, and a rule in a browser is not validation. A saved page size of 0 is a live 500: `get_browser_max_obj_per_page` honours "0" -- only an empty or unparseable value falls back -- and `ceil(count / 0)` raises ZeroDivisionError, so browsing stops working entirely until an admin fixes the flag from a UI they can no longer reach. A saved upload cap of -1 makes every custom-cover upload fail with "Upload exceeds -1 MB limit". Neither value is rejected anywhere today. Both live in a text column shared with every other flag, so a MinValueValidator cannot go on the field; `AdminFlagSerializer.validate` is the only place the bound can be expressed. It grows a per-key range map beside the existing per-key branch, and `validate` splits into two named helpers rather than growing a third arm inline. The SMTP port and timeout are not literally unbounded -- Django derives a range from the integer type and DRF lifts it into the field -- but the derived range is (0, 9223372036854775807), measured here, so port 0 and port 70000 both save while negatives are refused. Explicit serializer-level bounds, not model `validators=[...]`: validators deconstruct, so those would cost an AlterField migration for no schema change. On the client, `flag-card.vue` has no v-form and PATCHes on every keystroke, so a rule displays a message without stopping the write. The write itself is gated. Both client controls now read their bounds from the generated limits file rather than retyping them. Each test fails against the unbounded code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 21, 2026
ajslater
added this pull request to stack #890
September 21, 2026 16:16
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 P3c of
tasks/followups-implementation-plan.md§5.3, including the "items per page" rider.Three holes where a direct API call saves a value the app cannot survive. The client's rules were the only thing in the way, and a rule in a browser is not validation.
Two live breakages
Items per page (
MP) = 0 is a 500.get_browser_max_obj_per_pagehonours"0"— only an empty or unparseable value falls back to the default — andnum_pages = ceil(full_count / …)raisesZeroDivisionError. The same division appears inpaginate.py, both OPDS feeds,publications.py,groups.pyand the search parser, so browsing stops working entirely until an admin fixes the flag from a UI they can no longer reach.Upload cap (
CM) = -1 makes every custom-cover upload fail with "Upload exceeds -1 MB limit".Both are stored in a
CharFieldshared with every other flag, so aMinValueValidatorcannot live on the field —AdminFlagSerializer.validateis the only place the bound can be expressed. It gains a per-key range map beside the existingBROWSER_DEFAULT_COLLECTIONbranch, andvalidatesplits into two named helpers rather than growing a third arm inline.One corrected premise
D6 called the SMTP save path "unbounded". It is not literally unbounded — Django derives a range from the integer type and DRF lifts it into the serializer field. But measured here:
So port 0 and port 70000 both save, while negatives are refused. The practical gap stands; the wording should not say "enforces nothing".
Bounds are declared at the serializer level, not as model
validators=[...]— validators deconstruct, so those would cost anAlterFieldmigration for no schema change.required=Falsemirrors whatModelSerializeralready inferred from the model defaults, so declaring the fields changes the bounds and nothing else.The client needed a gate, not a rule
flag-card.vuehas nov-formandchangeColPATCHes on every keystroke — so a:rulesbinding displays a message while the write goes through anyway. The rule is added andchangeColrefuses an out-of-range value before callingupdateRow.Both client controls now read their bounds from the generated
limits.jsoninstead of retyping them:flag-card.vuefor the page size, andcustom-covers-tab.vue's hardcodedMAX_UPLOAD_MIN/MAX_UPLOAD_MAXfor the upload cap. The static rule arrays live indata, notcomputed.Tests
tests/test_admin_value_bounds.py, 11 cases. Verified load-bearing by removing each bound and re-running:Plus four that must keep passing: a valid page size, a valid port, and — importantly —
test_a_text_flag_is_unaffected, so the int bounds cannot leak onto flags holding strings.The rejecting cases also assert the stored value is unchanged and that the runtime getter still returns something usable, rather than only checking the status code.
make fix && make lint && make tyclean. Fullmake testgreen: 1275 pytest (+11), 583 vitest.NEWS, under Fixes: "Invalid admin settings values are rejected instead of breaking browsing."
🤖 Generated with Claude Code