Plugin connections: per-person configuration and secrets for plugins that talk to another service #11
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Blocks
#4 A notification interface, with SMTP as the built-in notifier
Postulo/postulo
#13 Document stores: local media built in, an interface for keeping copies elsewhere
Postulo/postulo
#15 postulo-paperless: keep every document in Paperless-ngx
Postulo/postulo
Reference
Postulo/postulo#11
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why this exists
Three of the plugins asked for on 2026-09-05 — postulo-paperless (#15), postulo-dav (#16) and, already, postulo-apprise (#5) — share one first need: a place where a person enters where the other service is and how to authenticate, which a plugin can read and Postulo can render a form for without knowing the plugin. #4 sketched it for notifiers as a
NotificationChannel. This issue makes it one thing for every plugin kind, so it is designed once and #4 builds on it.What exists today
plugins/registry.py,ENTRY_POINT_GROUP = "postulo.sources") and are stateless: a source has no configuration and no secrets.api/models.py), stored as a SHA-256 hash because Postulo only ever needs to compare it. A connection is the opposite case: Postulo must present the secret to another server, so it has to be readable — encryption at rest, not hashing.plugins/fetching.py, whosevalidate_public_urlrefuses any hostname that resolves to a private, loopback or link-local address. Right for a URL a stranger pasted into a capture; wrong for a person's own Paperless on the same LAN or in the same Compose network, which is exactly where self-hosted services live.Shape
Connectionmodel, owned like everything else: plugin name, kind (notifier, store, sync…), label,config(JSON, not secret),secrets(encrypted), enabled,last_ok_at,last_error. A person may hold several connections to the same plugin.config_fields()returns field specifications — name, type, label, help, required, secret flag — and Postulo builds the form. Same idea as #4's notifier contract; this issue owns it, and #4 inherits it.test(connection). Every plugin with a connection implements it; the form has a Test button and the outcome is stored and shown.SECRET_KEYby default, overridable withPOSTULO_FIELD_KEY, so that rotating Django's key does not silently lock every connection. Never rendered back in full: a masked tail. Left out of the export by default, because an export is a file that travels (core/export.pygains an explicit flag if anyone needs them included).POSTULO_CONNECTIONS_ALLOW_PRIVATE, default false, switched on in the Compose documentation with a sentence on why. Plugins get one sharedhttpxclient factory — timeouts, size caps, no redirects to a different host — rather than each rolling its own.plugins/registry.pylearns several groups:postulo.sources,postulo.notifiers(#4),postulo.stores(#13),postulo.syncs(#16). One package may register in more than one.Classification
Enhancement. Not breaking: new tables, nothing existing changes. It does reconcile #4, which should build on this rather than on a channel model of its own.
Open questions
SECRET_KEY, or a dedicated key required from day one? Derived is zero-configuration; dedicated is cleaner when a key has to be rotated.