An administrator can decide a plugin for a person — and must not be able to decide it quietly #95

Closed
opened 2026-09-07 15:19:03 +00:00 by tiagoagueda · 1 comment
Owner

Decided — see the comment below. Symmetric and visible: four states, and a person always sees what was decided for them.

Observation

admin plugin page:
repos: internal (cannot be disable), oficial (can be enable/disable and customized, if it
is not on the env, like smtp), multiple repos that can be enable/disable/customized

plugins kinds: internal (shipped with postulo), oficial plugin (downloaded from official
repo, or thru a zip) custom plugin (downloaded from custom repo, or thru a zip)
all plugins can be enable and disable but a admin can impose enable/disable of a plugin on
any user

user-plugin:
can see what current plugins (either kind) are enable in their session

Third of four, and the one with something in it worth arguing about before any code is
written.

What exists

Two switches, neither of which is this:

  • Installed.disabled -- instance-wide, in plugins.json on the data volume. On or off for
    everybody.
  • Connection.enabled -- per connection, owned by the person, controlled by the person. One
    account's Telegram notifier.

There is nothing in between: no per-person plugin state an administrator can see or set, and
no record anywhere of an administrator having changed something about somebody's account.
core/server_views.py contains no record_event call at all -- making somebody an
administrator, deactivating them, renaming them, all happen with nothing written down.

What this asks for

A person may turn a plugin on or off for themselves, and an administrator may take that
decision away for a named person or for everybody: let them choose, always on,
always off.

The part that needs deciding first

Forbidding and compelling are not the same act, and this issue should probably not treat
them as one.

Always off is an ordinary operator decision. "This instance does not use that." It removes
a capability, it can lose nobody anything they had not already saved, and the worst case is
somebody's notifier stops firing.

Always on is a different thing, because of what these plugins are. Three of the four kinds
-- notifiers, stores, syncs -- exist to move a person's data somewhere else. A store keeps
documents; a sync pushes and pulls records. Postulo's first stated commitment is that one
person's data never reaches another's
. An administrator who can compel a person's account
to run a store plugin is an administrator who can, in principle, arrange for that person's CV
to end up somewhere of the administrator's choosing.

In practice a connected plugin needs a Connection holding credentials the person supplies,
so compelling one on does not by itself move anything -- it means "you may use this and may
not switch it off". But the shape of the permission is what matters, and this one is worth
being deliberate about rather than discovering later.

Three ways to go, and this needs an answer before implementation:

  1. Asymmetric. An administrator may forbid any plugin and may compel only sources, which
    read pages and hold nothing. Safest, and probably enough for the actual need.
  2. Symmetric but visible. Compelling is allowed for every kind, and a person is told
    plainly and permanently on their own page which plugins were decided for them and by
    whom. Honest, and relies on people reading.
  3. Symmetric and quiet. What the request says literally. Not recommended, and named here
    so that choosing it is a choice.

The rest of it, which is not contentious

Precedence. Instance-off beats everything: a plugin switched off for the instance is off,
whatever any per-person policy says, because the code should not be running at all.
Otherwise an administrator's decision beats the person's, and the person's applies where
there is none.

Nothing is deleted. Forcing a plugin off must not remove anybody's Connection rows or
their configuration. It stops them being used. Reversing the decision brings them back
exactly as they were -- a policy that destroys data on the way is not a policy, it is a
delete button with a confusing name.

It has to be written down. An administrator changing what somebody's account may run
belongs in an audit trail, with who, when, and what changed. Since server_views.py records
nothing at all today, this issue either brings the first such trail with it or waits for one.
That is worth deciding rather than skipping -- the event log being the truth is a rule
elsewhere in this codebase and there is no reason for administration to be the exception.

And the person has to be able to see it, which is the fourth issue.

Scope

  • A per-person plugin policy, and an instance-wide default, with the precedence above.
  • Server settings -> Plugins gains the instance default; the person's own row in
    Server settings -> People gains the exceptions.
  • The runtime asks the policy before a plugin acts for somebody, in one place rather than at
    every call site.
  • An audit record for every change.
  • tests/security/: instance-off cannot be overridden; a forced-off plugin does not run for
    that person; nothing is deleted by any transition; a non-administrator cannot reach any of
    it.

Classification

Enhancement, security. Tier 1 because it is a new permission over other people's accounts,
and those are worth getting right before they exist rather than after.

> **Decided** — see the comment below. Symmetric and visible: four states, and a person always sees what was decided for them. ## Observation > admin plugin page: > repos: internal (cannot be disable), oficial (can be enable/disable and customized, if it > is not on the env, like smtp), multiple repos that can be enable/disable/customized > > plugins kinds: internal (shipped with postulo), oficial plugin (downloaded from official > repo, or thru a zip) custom plugin (downloaded from custom repo, or thru a zip) > all plugins can be enable and disable but a admin can impose enable/disable of a plugin on > any user > > user-plugin: > can see what current plugins (either kind) are enable in their session Third of four, and the one with something in it worth arguing about before any code is written. ## What exists Two switches, neither of which is this: - `Installed.disabled` -- instance-wide, in `plugins.json` on the data volume. On or off for everybody. - `Connection.enabled` -- per connection, owned by the person, controlled by the person. One account's Telegram notifier. There is nothing in between: no per-person plugin state an administrator can see or set, and **no record anywhere of an administrator having changed something about somebody's account**. `core/server_views.py` contains no `record_event` call at all -- making somebody an administrator, deactivating them, renaming them, all happen with nothing written down. ## What this asks for A person may turn a plugin on or off for themselves, and an administrator may take that decision away for a named person or for everybody: *let them choose*, *always on*, *always off*. ## The part that needs deciding first **Forbidding and compelling are not the same act, and this issue should probably not treat them as one.** *Always off* is an ordinary operator decision. "This instance does not use that." It removes a capability, it can lose nobody anything they had not already saved, and the worst case is somebody's notifier stops firing. *Always on* is a different thing, because of what these plugins are. Three of the four kinds -- notifiers, stores, syncs -- exist to move a person's data somewhere else. A store keeps documents; a sync pushes and pulls records. Postulo's first stated commitment is that **one person's data never reaches another's**. An administrator who can compel a person's account to run a store plugin is an administrator who can, in principle, arrange for that person's CV to end up somewhere of the administrator's choosing. In practice a connected plugin needs a `Connection` holding credentials the person supplies, so compelling one on does not by itself move anything -- it means "you may use this and may not switch it off". But the shape of the permission is what matters, and this one is worth being deliberate about rather than discovering later. Three ways to go, and this needs an answer before implementation: 1. **Asymmetric.** An administrator may forbid any plugin and may compel only sources, which read pages and hold nothing. Safest, and probably enough for the actual need. 2. **Symmetric but visible.** Compelling is allowed for every kind, and a person is told plainly and permanently on their own page which plugins were decided for them and by whom. Honest, and relies on people reading. 3. **Symmetric and quiet.** What the request says literally. Not recommended, and named here so that choosing it is a choice. ## The rest of it, which is not contentious **Precedence.** Instance-off beats everything: a plugin switched off for the instance is off, whatever any per-person policy says, because the code should not be running at all. Otherwise an administrator's decision beats the person's, and the person's applies where there is none. **Nothing is deleted.** Forcing a plugin off must not remove anybody's `Connection` rows or their configuration. It stops them being used. Reversing the decision brings them back exactly as they were -- a policy that destroys data on the way is not a policy, it is a delete button with a confusing name. **It has to be written down.** An administrator changing what somebody's account may run belongs in an audit trail, with who, when, and what changed. Since `server_views.py` records nothing at all today, this issue either brings the first such trail with it or waits for one. That is worth deciding rather than skipping -- the event log being the truth is a rule elsewhere in this codebase and there is no reason for administration to be the exception. **And the person has to be able to see it**, which is the fourth issue. ## Scope - A per-person plugin policy, and an instance-wide default, with the precedence above. - *Server settings -> Plugins* gains the instance default; the person's own row in *Server settings -> People* gains the exceptions. - The runtime asks the policy before a plugin acts for somebody, in one place rather than at every call site. - An audit record for every change. - `tests/security/`: instance-off cannot be overridden; a forced-off plugin does not run for that person; nothing is deleted by any transition; a non-administrator cannot reach any of it. ## Classification Enhancement, security. Tier 1 because it is a new permission over other people's accounts, and those are worth getting right before they exist rather than after.
tiagoagueda added this to the 0.3.0 milestone 2026-09-07 15:19:03 +00:00
Author
Owner

Decided

the admin can make available, make unavaible, force enable, or force disable

A user can just enable/disable is the plugin is available and stuck on enable/disable if
it is forced by the admin

Option 2 from the issue: symmetric, and visible. An administrator may compel as well as
forbid, and the person always sees what was decided for them -- a control showing the forced
state and locked, rather than a control that has quietly vanished.

That answers the concern raised above rather than overriding it. The worry was never that an
administrator holds this power; it was that it could be held invisibly. A stuck toggle is the
mitigation.

The four states, and the fifth one they imply

State The person sees The person may
Available the plugin, with a working switch turn it on or off
Unavailable nothing --
Forced on the plugin, switch on and locked nothing, and is told who decided
Forced off the plugin, switch off and locked nothing, and is told who decided

Unavailable and forced-off are deliberately different, and both are worth having:
unavailable means not part of your Postulo; forced-off means you can see this exists and
that somebody turned it off for you
. The second is more honest and the first is quieter, and
which is right depends on why.

A fifth value is needed that the request does not name: inherit. The four above are set
at instance level as a default, and a person's row needs a way to say "no exception here,
follow the default" -- otherwise every account has to be written to whenever the default
changes.

One precision on "forced on"

For a notifier, store or sync, forcing a plugin on cannot make it run. Those need a
Connection holding credentials only the person can supply. So forced-on means "this is
available to you and you may not switch it off"
, not "this is now sending your documents
somewhere". Worth being exact in the interface too: a forced-on plugin with no connection
should say it is waiting for one, not appear to be working.

Sources are the exception -- they are stateless and need no connection, so forcing one on
genuinely turns it on.

Precedence, unchanged from the issue

Instance-off still beats everything: a plugin switched off for the whole instance is off
whatever any per-person policy says, because the code should not be loaded at all. Below
that, the administrator's decision beats the person's, and the person's applies wherever
there is no administrator decision.

Nothing is deleted by any of these transitions, and every change is written down. Both still
stand.

## Decided > the admin can make available, make unavaible, force enable, or force disable > > A user can just enable/disable is the plugin is available and stuck on enable/disable if > it is forced by the admin Option 2 from the issue: **symmetric, and visible.** An administrator may compel as well as forbid, and the person always sees what was decided for them -- a control showing the forced state and locked, rather than a control that has quietly vanished. That answers the concern raised above rather than overriding it. The worry was never that an administrator holds this power; it was that it could be held invisibly. A stuck toggle is the mitigation. ## The four states, and the fifth one they imply | State | The person sees | The person may | | --- | --- | --- | | Available | the plugin, with a working switch | turn it on or off | | Unavailable | nothing | -- | | Forced on | the plugin, switch on and locked | nothing, and is told who decided | | Forced off | the plugin, switch off and locked | nothing, and is told who decided | **Unavailable and forced-off are deliberately different**, and both are worth having: unavailable means *not part of your Postulo*; forced-off means *you can see this exists and that somebody turned it off for you*. The second is more honest and the first is quieter, and which is right depends on why. A fifth value is needed that the request does not name: **inherit**. The four above are set at instance level as a default, and a person's row needs a way to say "no exception here, follow the default" -- otherwise every account has to be written to whenever the default changes. ## One precision on "forced on" For a notifier, store or sync, forcing a plugin on cannot make it *run*. Those need a `Connection` holding credentials only the person can supply. So forced-on means **"this is available to you and you may not switch it off"**, not "this is now sending your documents somewhere". Worth being exact in the interface too: a forced-on plugin with no connection should say it is waiting for one, not appear to be working. Sources are the exception -- they are stateless and need no connection, so forcing one on genuinely turns it on. ## Precedence, unchanged from the issue Instance-off still beats everything: a plugin switched off for the whole instance is off whatever any per-person policy says, because the code should not be loaded at all. Below that, the administrator's decision beats the person's, and the person's applies wherever there is no administrator decision. Nothing is deleted by any of these transitions, and every change is written down. Both still stand.
Sign in to join this conversation.
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.

Reference
Postulo/postulo#95
No description provided.