Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
Added
- Image uploads: EXIF auto-rotation, metadata and cached thumbnails (#404, #408,
epic #410).
hyperadmin.uploads.imagesprovidesextract_metadata(),auto_rotate(),generate_thumbnail()(cached under.thumbs/next to the original, aspect ratio preserved) and theprepare_image_upload()hook the create/update forms now run on everyImageTypeupload. Non-image input never raises; decompression bombs are refused from the header withImageTooLargeError(a422on the form).
Changed
- Breaking: Python 3.10 is no longer supported; Python 3.14 is.
requires-pythonis now>=3.11. Python 3.10 reached end of life in October 2026, and Pillow 13 (which fixes CVE-2026-112895 and seven related advisories) only ships for 3.11+. The lock pins certifi 2026.10.19 (GHSA-6q48-6c7w-9gw2) and Pillow 13.0.0. CI,poe deps:bumpand.python-versionfollow (3.11 to 3.14).
Security
- Authorization and row scoping on every item handler (st-v058-byoa-10).
- Inline cell edit/save, inline add-row, update form, file delete/upload and
single actions now enforce the model permission and, where an object is
loaded, the object permission (
change, oraction_<name>for actions). ModelAdmin.get_querysetrow scoping is request-scoped (held in aContextVar, not on the shared adapter) and applies to every item, bulk, inline and choices handler. Rows hidden from the user return 404, and concurrent requests can no longer see each other's filters.DELETE /{model}/{id}/file/{field}only accepts file fields and never removes a file outside the storage root (absolute paths,..and symlinks are refused). Uploads also require a file field.- Inline formsets reject a submitted child pk that is not an existing child of
the parent being saved (404, nothing written), and enforce the inline
model's own
add/change/deletepermissions when that model is registered. An unregistered inline model has no permission rows, so the parent'sadd/changepermission governs its rows. - Related rows need
viewon the target model wherever they are disclosed: the choices endpoint (403), list-page relation filters (omitted) and the create/edit relation widgets (only the selected key is kept, labelledModel (pk)). An unregistered target falls back to the source model's permission. POST /{model}/upload/{field}ignores the client path: it stores the sanitised basename under a fresh random prefix, never overwrites an existing file, refuses names outside the storage root, and returns the stored name relative to the storage root (not an absolute server path).- Upgrade note for adapter authors: an adapter that implements
save_inline_rowsmust also implementinline_child_pks(spec, parent_pk). The default returns an empty set, so every save of a parent with existing inline rows is rejected (404) until it is implemented. Seedocs/api/adapters.md. - Query oracles on secret columns closed (st-v058-byoa-53). A user who could
list a model could previously probe a column such as
password_hashone query at a time. All four oracles are closed: - Filters: URL
filter_<field>parameters are honoured only for fields inlist_filter; anything else is ignored. - Search: inferred search fields (and the adapters' fallback search, and
the choices
qsearch) skip sensitive fields, including fields made sensitive only throughAdminOptions.sensitive_fieldson the model's (or the choices target's) admin. An explicitsearch_fields=[]passed to an adapter now disables search instead of falling back to every text column. - Sort:
sort_byis whitelisted against the displayed, non-sensitive columns. An unknown value falls back to the default sort (200, never 500). - Choices:
/{model}/choices/{field}forwards only the cascade keys the relation widget declares (dependent_on,dependent_fields,relation_filters) and applies the target admin'sget_queryset. - Relation labels: FK filter dropdowns, relation widgets, the choices
endpoint, popup-create labels and list cells label related rows with the
model's own
__str__when it defines one, otherwise with the first non-sensitive ofname,title,label,username,email, otherwiseModel (pk). SQLModel's default__str__, which prints every column (password hashes included), is never used. - Writes: a submitted FK value must be a row that the target admin's
get_querysetexposes; otherwise the form is re-rendered with a field error (422) and nothing is written. - New
hyperadmin.core.sensitive: a field is sensitive when it carries thehyperadmin_sensitivemarker, or when it is a text (str/bytes) field whose name matches(^|_)(password|secret|token|hash)(_|$);AdminOptions.sensitive_fieldsoverrides this either way. With SQLModel, mark a field withField(schema_extra={"json_schema_extra": {"hyperadmin_sensitive": True}})(SQLModel'sField()rejects a directjson_schema_extra=argument); with plain Pydantic useField(json_schema_extra={"hyperadmin_sensitive": True}). Sensitive fields are hidden from the detail page and inferred list columns. Sensitive text fields are write-only on forms and inline rows (an empty submission keeps the stored value). A sensitive non-text field (a marked boolean or enum) is left out of the edit form and keeps its stored value. The built-inUser.password_hashis marked. - Upgrade note: a text column such as
secret_namenow counts as sensitive. Opt it back in withAdminOptions(sensitive_fields={"secret_name": False}). The name heuristic does not apply to booleans, numbers or enums (is_secret: bool,token_count: int).
Added
- Initial project scaffold