Skip to content

feat: Validate version when building file URL CU-30er0v6 - #24

Draft
Skoda091 wants to merge 2 commits into
masterfrom
validate-url-version
Draft

feat: Validate version when building file URL CU-30er0v6#24
Skoda091 wants to merge 2 commits into
masterfrom
validate-url-version

Conversation

@Skoda091

@Skoda091 Skoda091 commented Sep 4, 2026

Copy link
Copy Markdown
Member

Summary

Closes the loophole described in CU-30er0v6: Bow.url/2,3 and Bow.Ecto.url/3,4 build a URL for any version atom, even one the uploader never defined, silently producing links to files that don't exist. The requested version is now validated, so version definitions in uploaders can be trusted (e.g. by a cleaner script) and a "finger slip" version surfaces immediately instead of as a dead link.

Straight version in versions(file) validation is not possible: versions/1 describes what is generated on store, while transform/3 can return derived versions ({:ok, file, next_versions}) and filename/2 can alias two versions to the same stored file — both patterns are exercised by bow's own test suite (PipelineUploader, UrlUploader) and by the API. The URL-addressable set is therefore a new overridable callback url_versions/1, defaulting to versions(file), which is correct for most uploaders.

Changes

  • Bow.Uploader gets an overridable url_versions/1 callback (default: versions(file)) declaring which versions URLs may be built for
  • Bow.url/3 (the funnel for Bow.Ecto.url too) validates the requested version against url_versions/1
  • Undefined version → Logger.warning by default; config :bow, on_undefined_url_version: :raise raises Bow.Error instead
  • Tests for warn mode (URL still returned, behavior preserved), raise mode, and url_versions/1 extending versions/1 (derived + aliased versions)
  • README section documenting the validation and when to override url_versions/1
  • Separate commit: locked dev deps refreshed (ecto/ecto_sql, ssl_verify_fun, mix_test_watch) — after the erlexec 2.2.2 bump (Bump erlexec #23, requires OTP 27) the old lock no longer compiled on any OTP, so mix test was unrunnable; this only affects bow's standalone dev env, consumers resolve deps via their own lockfiles

Why warn-by-default instead of raise (as the ticket suggests)

URL building runs inside hot view-rendering paths of the API (candidate CV links among them). An audit of the API found live call sites relying on the loophole today, so raising unconditionally would turn currently-working URLs into 500s:

  1. Cv / CoverLetter / ImportCv / ApplicationCv uploaders — for .pdf files versions/1 returns [:original, :copy], yet views request :pdf (works only because filename(:copy) == filename(:pdf))
  2. Attachment uploader — .pdf files map to DB kind "document", whose view branches request :pdf, while versions/1 returns [:original, :copy, :pdf_thumbnail]
  3. ReferrerAvatar (portal + admin) — read-only uploaders with default [:original], but call sites request :normal and :thumb

Rollout plan:

  1. Release this as warn-by-default (zero behavior change)
  2. API: fix the three uploader clusters above, bump bow, set on_undefined_url_version: :raise in test/dev so the suite catches stragglers
  3. Once logs are clean, enable :raise in production; potentially flip the library default in a next major

Type of change

  • New feature
  • Bug fix
  • Refactoring
  • Performance improvement
  • Documentation
  • Tests
  • Chore / maintenance

Pre-merge checklist

  • Tests cover the new/changed behavior (mix test: 51 tests, 0 failures, 42 excluded — the default-excluded :s3/:ecto tags, same as before)
  • No leftover debug code
  • No hardcoded secrets or credentials
  • Version bump + tag — left to maintainer; note mix.exs says 0.4.3 while the latest tag is 0.5.0 (API pins the tag), worth reconciling when releasing

Notes

  • Verified on Erlang/OTP 27.3.4.3 + Elixir 1.19.5 (repo has no CI)
  • Related project: Files/Attachment refactor CU-862kp34ua
  • API-side uploader fixes will follow as a separate PR in recruitee/api

🤖 Generated with Claude Code

Skoda091 and others added 2 commits September 4, 2026 09:57
After bumping erlexec to 2.2.2 (OTP 27+), the remaining locked deps
(ecto 3.7, ssl_verify_fun 1.1.6, mix_test_watch 1.1.0) no longer
compiled on any single OTP release, making mix test unrunnable.
Updates ecto/ecto_sql, ssl_verify_fun and mix_test_watch with their
transitive deps. Affects only bow's standalone dev/test environment -
consumers resolve these against their own lockfiles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bow.url/2,3 (and Bow.Ecto.url/3,4) happily built a URL for any version
atom, even one the uploader never defined, silently producing links to
files that do not exist. The requested version is now validated against
the uploader's URL-addressable versions.

Since versions/1 describes what is generated on store, and transform/3
can produce derived versions (next_versions) while filename/2 can alias
two versions to the same stored file, the addressable set is a new
overridable callback url_versions/1, defaulting to versions(file).

An undefined version logs a warning by default; set
config :bow, on_undefined_url_version: :raise to raise Bow.Error
instead (recommended for test/dev, and for prod once clean).

CU-30er0v6

Co-Authored-By: Claude Fable 5 <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.

1 participant