Plugin framework and surface: settings gate, names, wheels, webhook, templates, serial, lease, links #999

Merged
tiagoagueda merged 6 commits from sec2-f-plugin-framework into main 2026-10-18 14:09:34 +00:00
Owner

Plugin framework and surface fixes from the SEC2 review.

  • #967 The pages behind a plugin's Settings button (maps, data protection, email with its test, connection-test and consent views, identifier schemes including the htmx dialog) now refuse with 403 an administrator whose access rule says they may not change the settings. The plugins a page belongs to are read off the manifests' settings=; a page two plugins share (mail) is closed by either rule. An administrator who is not the operator can no longer write or lift a rule about their own account or one of their groups, nor change a group an access rule names.
  • #968 Install is refused for a wheel whose entry point is named like a built-in; the registry also leaves an installed plugin out when a built-in holds its name. Deviation: is_internal still answers by name, which is now correct because a built-in always keeps its name.
  • #969 Wheel metadata members are bounded (1 MiB, checked on the directory and on the read), encrypted or damaged members and wheels of over 5000 members are refused.
  • #970 http:// receivers are accepted only when the operator allowed private destinations and every resolved address is private; help text and error say so. The webhook plugin now records a dependency on core.destinations in the surface test.
  • #971 Followed the issue's first option: plugin template directories are read by a final loader (plugins/template_loader.py), after the filesystem and app-directory loaders, so they cannot shadow any existing template. TEMPLATES now names its loaders explicitly instead of APP_DIRS.
  • #972 The highest serial per catalogue and key is a new row (CatalogueSerial, migration plugins/0011), not a PluginRepository column, because environment-configured catalogues have no row. scripts/official_catalogue.py signs a 60-day expiry by default. The user must regenerate and re-sign catalogue/official/index.json with the key from vaultwarden (official_catalogue.py build then sign); I did not touch the signed manifest. Note that from then on it must be re-signed within 60 days. An index that drops its serial entirely is still accepted (not in scope).
  • #988 The sync lease holds a token and is released only if it still does.
  • #991 save_only_link raises ValidationError unless the address is a complete http(s) one.
  • Published read_body, deadline_in, until, BodyRefused on postulo.plugins.api (wiki line added; changelog entry cites #967 for lack of an issue number).

Tests: full unit suite 18901 passed, 122 skipped, 1 xfailed, 1 failed (changelog entry lacking an issue, fixed afterwards; test_changelog passes). Each issue has its "done when" test. The browser (e2e) suite was not run locally; the template loader change and the explicit loaders setting affect every template render, so please read the CI browser job.

Closes #967, Closes #968, Closes #969, Closes #970, Closes #971, Closes #972, Closes #988, Closes #991

🤖 Generated with Claude Code

Plugin framework and surface fixes from the SEC2 review. - **#967** The pages behind a plugin's Settings button (maps, data protection, email with its test, connection-test and consent views, identifier schemes including the htmx dialog) now refuse with 403 an administrator whose access rule says they may not change the settings. The plugins a page belongs to are read off the manifests' `settings=`; a page two plugins share (mail) is closed by either rule. An administrator who is not the operator can no longer write or lift a rule about their own account or one of their groups, nor change a group an access rule names. - **#968** Install is refused for a wheel whose entry point is named like a built-in; the registry also leaves an installed plugin out when a built-in holds its name. Deviation: `is_internal` still answers by name, which is now correct because a built-in always keeps its name. - **#969** Wheel metadata members are bounded (1 MiB, checked on the directory and on the read), encrypted or damaged members and wheels of over 5000 members are refused. - **#970** `http://` receivers are accepted only when the operator allowed private destinations and every resolved address is private; help text and error say so. The webhook plugin now records a dependency on `core.destinations` in the surface test. - **#971** Followed the issue's first option: plugin template directories are read by a final loader (`plugins/template_loader.py`), after the filesystem and app-directory loaders, so they cannot shadow any existing template. `TEMPLATES` now names its loaders explicitly instead of `APP_DIRS`. - **#972** The highest serial per catalogue and key is a new row (`CatalogueSerial`, migration `plugins/0011`), not a `PluginRepository` column, because environment-configured catalogues have no row. `scripts/official_catalogue.py` signs a 60-day expiry by default. **The user must regenerate and re-sign `catalogue/official/index.json` with the key from vaultwarden** (`official_catalogue.py build` then `sign`); I did not touch the signed manifest. Note that from then on it must be re-signed within 60 days. An index that drops its serial entirely is still accepted (not in scope). - **#988** The sync lease holds a token and is released only if it still does. - **#991** `save_only_link` raises `ValidationError` unless the address is a complete http(s) one. - Published `read_body`, `deadline_in`, `until`, `BodyRefused` on `postulo.plugins.api` (wiki line added; changelog entry cites #967 for lack of an issue number). Tests: full unit suite 18901 passed, 122 skipped, 1 xfailed, 1 failed (changelog entry lacking an issue, fixed afterwards; test_changelog passes). Each issue has its "done when" test. The browser (e2e) suite was not run locally; the template loader change and the explicit `loaders` setting affect every template render, so please read the CI browser job. Closes #967, Closes #968, Closes #969, Closes #970, Closes #971, Closes #972, Closes #988, Closes #991 🤖 Generated with [Claude Code](https://claude.com/claude-code)
An installed plugin was loaded before the built-ins, so one registered under a built-in's name displaced it and inherited its policy rows, access rules and connections. The install is now refused, and the registry leaves an installed plugin out when a built-in holds its name.

read_wheel also read its three metadata members whole. Each is now checked against the archive's directory and read within one mebibyte, an encrypted member, a damaged one and a wheel of thousands of files are refused with an InstallError.

Closes #969

Closes #968

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The signature authenticates the body and hides nothing, and the body is the notification. validate now refuses http:// unless the operator allowed private destinations and every address the host resolves to is private; the help text and the error say the same thing. The plugin's recorded dependency on core.destinations is listed with its reason in the surface test.

Closes #970

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A sync that outlived the lease released the next run's lease when it finished, letting a third start beside it. The lease now holds a token and is deleted only if it still does, as the scheduler's pass lease has since #576.

Closes #988

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The function every plugin writes a link with stored whatever it was given, so a javascript: or data: address from a card read by a plugin reached the page's href. It now holds the address to addresses.web_address and raises ValidationError, as the other doors do; the surface's docstring and the wiki say so.

Closes #991

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The DAV, Paperless and France Travail plugins need to bound what a remote server sends them, and read_body, deadline_in, until and BodyRefused only existed in postulo.plugins.http, which a plugin may not import. Listed on the surface and in Writing a plugin.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Settings pages, template search order, catalogue serial row, and the changelog
Some checks failed
CI / Unit tests and coverage (Python 3.14) (pull_request) Failing after 19m12s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 3m39s
CI / Database tests on PostgreSQL (pull_request) Successful in 4m26s
CI / Security audit (pull_request) Successful in 1m11s
CI / Browser tests (Chromium) (pull_request) Successful in 39m5s
3fac9ebd5d
The earlier commits for #967 (settings pages held to the access list), #971 (plugin templates searched last) and #972 (serial kept in the database) failed their first hook pass and were swept into this one: server_views.py, views_access.py, base.py, themes.py, template_loader.py, catalogue.py, models.py, migration 0011 and their tests, with the extracted catalogues and a changelog line per issue.

Closes #967
Closes #971
Closes #972

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
tiagoagueda force-pushed sec2-f-plugin-framework from 3fac9ebd5d
Some checks failed
CI / Unit tests and coverage (Python 3.14) (pull_request) Failing after 19m12s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 3m39s
CI / Database tests on PostgreSQL (pull_request) Successful in 4m26s
CI / Security audit (pull_request) Successful in 1m11s
CI / Browser tests (Chromium) (pull_request) Successful in 39m5s
to 4cdc49605b
All checks were successful
CI / Unit tests and coverage (Python 3.14) (pull_request) Successful in 11m57s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 2m23s
CI / Database tests on PostgreSQL (pull_request) Successful in 3m19s
CI / Security audit (pull_request) Successful in 50s
CI / Browser tests (Chromium) (pull_request) Successful in 28m1s
2026-10-18 11:40:25 +00:00
Compare
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo!999
No description provided.