Decide where pluggability stops: SMTP and contact details are not shaped like plugins #100
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
Reference
Postulo/postulo#100
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?
Observation
Two of those four fit and two do not, and the two that do not are worth stopping over. This
issue is a decision, not an implementation.
SMTP: the notifier is already a plugin; the transport is not, and should probably stay
that way
Email already ships as an internal plugin.
notifications/apps.pycallsregister_builtin("notifier", EmailNotifier), and its docstring says why:So if "SMTP becomes an internal plugin" means the notifier, it is already done.
If it means the transport -- host, port, username, password, TLS -- that is Django's
MAILERS, and it is not only the notifier's. It is what allauth sends through, and:Nobody can create an account, verify an address, or reset a password without it. Make
that a plugin and account verification depends on a plugin being installed, loaded and
enabled. Then #95 lands, and an administrator can switch a plugin off for the instance -- at
which point sign-up and password reset stop, and the only person who could turn it back on
is one who is already signed in. An instance can be locked out of its own recovery path by a
switch that reads like a preference.
The three ways forward:
filed, already specified, and it gives the actual benefit -- configuring mail without
editing a file -- with none of this risk. Recommended.
switched off, configured through a plugin form -- a lot of machinery to arrive back where
it started.
settings page.
Contact details: a plugin cannot own a table today
Reading "multiple contacts per user" as #90 (several telephone numbers) and #92 (several
postal addresses) -- say if that is wrong, because the rest of this follows from it.
Those are models. And:
Nothing in the plugin machinery touches it. A plugin is an entry point that Postulo
instantiates; it cannot be a Django app, cannot ship migrations, and cannot own a table.
Making contact details a plugin therefore means building plugin-owned migrations first, and
that brings questions with sharp edges:
nothing can read it.
addresses with it. A "Remove" button that deletes personal data is a different button from
the one that exists.
core/export.pydo? Taking your data out has to include it, which means theexporter depends on a plugin being loaded to produce a complete archive.
There is a lighter design that gets the actual benefit. If the wish is that an operator
can decide their instance does not collect postal addresses -- which is a good wish, and
plain data minimisation -- then the tables stay in core and a feature switch governs
whether the interface offers them. Nobody's data is at the mercy of an entry point, the
export stays complete, and the switch is one row rather than a migration system.
That is a genuinely different design from "make it a plugin", and it is worth choosing on
purpose. Plugin-owned data may still be right one day -- but it should be decided for its own
reasons, not arrived at because contact details were the first thing to ask for it.
What this issue wants
An answer to two questions, so #90, #92 and #84 can proceed:
Classification
Security, tier 1 -- not because anything is broken now, but because the lockout in the first
half and the data-loss button in the second are both the kind of thing that is obvious in an
issue and invisible in a diff.
Decided
So: not a blanket rule and not a free switch — an interlock. SMTP may be turned off, and
may not be turned off while it is the only way anybody could get back into their account.
This is the rule Postulo already applies to administrators, one level deeper. Server
settings -> People says "The last administrator cannot be removed, deactivated or
deleted", and refuses rather than warns. The same shape: the last recovery route cannot be
removed either.
What that means today, checked rather than assumed
The recovery routes that actually exist:
ACCOUNT_*server_urls.pyoffers administrator, active, username, delete -- and no passwordSo the interlock is implementable now, and today it will almost always answer no. The one
existing alternative is SSO, and
POSTULO_OIDC_IS_SECOND_FACTORdefaults toFalse, whichmeans SSO is a login method in its own right rather than a second factor -- a genuine way
back in for anybody linked to the provider.
The SMS and Apprise routes named in the decision do not exist yet. Filed separately as a
prerequisite rather than smuggled in here.
The interlock is per person, not per instance
This is the part that would be easy to get wrong. SSO covers only accounts linked to the
provider. A notifier is a
Connectionrow owned by one person. So "is there another way in"is a question about every account separately, and the check is:
One person on the instance who signed up with a password and never linked SSO is enough to
refuse it -- and the refusal should name them, the way the last-administrator refusal names
what it is protecting.
And disabling it breaks sign-up as well as recovery
Separate consequence, easy to miss:
A new account cannot be created without a verification email, whatever alternative
routes existing accounts have. So switching SMTP off also stops registration and
invitations. Either the interlock covers that too, or turning SMTP off implies closing
registration, and the page has to say which.
What this settles
Option 3 from the issue, with the interlock making it safe -- rather than option 1, which
avoided the question by leaving the transport in core. #84 is unblocked and can proceed on
its own terms: putting the settings in the interface never required the transport to be a
plugin.
Decided — the SMTP half, in full
Option 2 from the issue, which I had described as "a plugin that cannot be switched off... a
lot of machinery to arrive back where it started". That reading was too narrow, and the
staging answers it: the switch is locked by the interlock, not by a special case, so
nothing is built that has to be unbuilt later. When #103 gives an instance another way back
in, the same rule lets go on its own.
This unblocks #100 rather than deepening it. The dependency on #103 is removed: shipping
the plugin locked on needs nothing from #103, and #103 is what eventually makes the switch
usable. Filed as its own implementation issue.
Still open here: the second half, whether contact details become plugin-owned tables or stay
in core behind a feature switch.
Decided — core tables behind a feature switch
The second half, and with it the whole issue.
PhoneNumberandPostalAddressare ordinary models in core, owned and migrated by core.A feature switch decides whether the interface offers them. Nobody's data sits at the
mercy of an entry point, there is no plugin migration system to build, and the switch is one
row rather than a subsystem.
It also gets the thing that was actually wanted. An operator who does not want their instance
collecting postal addresses can say so, which is plain data minimisation and a good reason on
its own — quite separate from whether the code that implements addresses happens to be a
plugin.
What that means for #90 and #92
accounts, besideProfile. Migrated with everything else.site.ENV_OVERRIDES, the mechanism four settings already use and #84extends, so an operator can pin it in the environment and the field greys out.
that already has addresses leaves them exactly where they are, and turning it back on shows
them again unchanged. A switch that destroys data is a delete button with a confusing name.
easy to get wrong: an instance with the feature off but rows still stored must still hand
those rows over, or take your data out has quietly stopped meaning all of it.
What this settles about plugins generally
Plugin-owned tables are not being built, and are not ruled out for ever — they are ruled out
as a thing to arrive at sideways because contact details were the first feature to ask. If a
plugin one day genuinely needs to own rows, that is its own issue with its own answers about
what happens on disable, on removal, and on a migration that fails halfway.
Both halves are now answered
interlock rather than a special case — #104, with #103 as what eventually opens it.
Closing. The implementations are the three issues that were waiting on it.