Inherit parental restrictions on shared accounts, with a chosen rating cap
A shared account previously inherited its members' library access but none of their content restrictions, so a child could log into "alice+kid" with their own password and get around their own rating cap. The shared account now gets the strictest member's parental rating, unrated-item block, blocked tags and allowed tags, recomputed at creation, on membership change and at startup. An admin can raise the rating cap on a slider between the strictest and the loosest member; unrated and tag rules stay strictest-wins. What makes raising the cap safe is the unlock rule: after a member's password matches, both users' live policies are compared and the login is refused if the account is looser than the member on any field. So raising the cap above the child's rating means the child's password no longer opens the account, while the parent's still does. The same rule bounds the slider - past the loosest member nobody could unlock the account - so a chosen cap is clamped back into range whenever applied. Allowed tags need care: Jellyfin reads an empty list as "no whitelist", so an empty intersection of members' whitelists is written as a sentinel tag no item carries. Access schedules and channels are not inherited yet. The shared account is never an administrator. Groups created at the login screen always inherit and are restricted before the first session exists. The dashboard shows each member's cap, who a chosen cap shuts out, and the restrictions in effect, and gains a per-group edit form for the sync options. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -44,8 +44,13 @@ The sync is **one-way**: shared account → members. What Alice watches privatel
|
||||
never leaks into the shared account or onto Bob.
|
||||
|
||||
Library access is the **intersection** of the members', never the union: the group sees only what
|
||||
everyone in it could already see. Sharing an account is therefore never a way to reach a library you
|
||||
were not already allowed into.
|
||||
everyone in it could already see. Parental restrictions work the same way: the shared account
|
||||
inherits the *strictest* member's rating cap, unrated-item block and tag rules. Sharing an account
|
||||
is therefore never a way to reach something you were not already allowed to see.
|
||||
|
||||
A parent can deliberately **raise** a group's rating cap to watch something above a child's rating
|
||||
together. When they do, the child's own password stops unlocking the shared account, so raising
|
||||
the cap never becomes a way around it.
|
||||
|
||||
```
|
||||
login as "alice+bob+carol"
|
||||
@@ -139,6 +144,35 @@ Two consequences worth knowing:
|
||||
*different* member's password happened to be the one that matched, eventually locking out
|
||||
members who did nothing wrong.
|
||||
|
||||
A matching password is not the whole story. A member may only unlock a shared account that is **at
|
||||
least as restricted as they are**: after the password matches, both users' *live* policies are
|
||||
compared field by field (rating cap, unrated block, blocked tags, allowed tags) and the login is
|
||||
refused if the account is looser on any of them. With an inherited cap this always passes. With a
|
||||
chosen cap, the passwords of members stricter than it simply stop working on the group — logged at
|
||||
Information level, since a child trying the family account is expected, not an incident.
|
||||
|
||||
### Content restrictions
|
||||
|
||||
`RestrictionService` mirrors the library-access code for the parental fields on a user:
|
||||
|
||||
| Field | Combination |
|
||||
| --- | --- |
|
||||
| Parental rating cap (score, sub-score) | Lowest wins. Any cap beats none; at an equal score, any sub-cap beats none. |
|
||||
| Blocked unrated kinds | Union. |
|
||||
| Blocked tags | Union. |
|
||||
| Allowed tags (whitelist) | Only members *with* a whitelist constrain; their lists are intersected. An empty list in Jellyfin means "no whitelist", so when two whitelists have nothing in common the account is written a single tag no item carries (`watched-together:nothing`) — it must see nothing, not everything. |
|
||||
|
||||
A member that cannot be resolved contributes "fully restricted", on the same principle as
|
||||
library access. Access schedules and channel restrictions are **not** inherited yet.
|
||||
|
||||
The result is written to the shared account at creation, on every membership change, and at
|
||||
server startup — overwriting whatever was set on the account in the user editor. The one thing
|
||||
an admin can choose is the **rating cap**, anywhere between the strictest member's and the loosest
|
||||
member's; unrated and tag rules stay strictest-wins regardless. A chosen cap is kept inside that
|
||||
range whenever it is applied: at or below the strictest member it is simply inheritance, and past
|
||||
the loosest member nobody could unlock the account, so it is pulled back (and logged). The shared
|
||||
account is never an administrator.
|
||||
|
||||
### Watched-state sync
|
||||
|
||||
The plugin subscribes to `UserDataSaved` and decides per save reason what it means:
|
||||
@@ -214,6 +248,10 @@ Either way, a new user appears in your user list and can be renamed like any oth
|
||||
| Sync unwatched | on | Marking something *unwatched* on the shared account also marks it unwatched for every member. Turn this off to make sync additive: things only ever become watched. |
|
||||
| Sync play count | off | Raise a member's play count to at least 1 when an item becomes watched. Play counts are never decreased. |
|
||||
| Disabled | off | Suspends a group: it stops accepting logins and stops syncing, without deleting anything. |
|
||||
| Parental rating cap | strictest member | A slider from the strictest member's rating to the loosest member's (or "No cap"). At the left end the cap is inherited and every member can unlock the account. Move it right to let the group watch above a member's rating: members stricter than the chosen cap can no longer unlock the account with their password. The dashboard says who is in and who is out at each position. Not shown when no member has a cap. |
|
||||
|
||||
Unrated-item blocks and tag rules are always the strictest member's; the dashboard shows what is
|
||||
in effect under each group.
|
||||
|
||||
### Plugin settings
|
||||
|
||||
@@ -238,9 +276,19 @@ How this plugin bounds what a shared account can reach.
|
||||
Members with nothing in common produce an account that sees nothing.
|
||||
- **Blocked folders stay blocked.** An explicitly blocked library is subtracted even from a member
|
||||
who otherwise has "access to all libraries".
|
||||
- **Parental restrictions are inherited, strictest wins.** Rating cap, unrated-item block, blocked
|
||||
and allowed tags are combined so the shared account hides at least everything any member cannot
|
||||
see. Access schedules and channel restrictions are not inherited yet.
|
||||
- **Raising the cap cannot be used to get around it.** A member can only unlock a shared account
|
||||
that is at least as restricted as they are, checked against live policies at every login. A
|
||||
child's password stops opening a group the parent raised above the child's rating; the parent's
|
||||
still does. And the cap can never go past the loosest member's — there would be nobody left who
|
||||
could unlock it.
|
||||
- **A shared account is never an administrator.**
|
||||
- **The intersection is recomputed, not frozen.** It is recalculated whenever a group's membership
|
||||
changes, and re-applied to every group at server startup, so narrowing a member's own access
|
||||
narrows the groups they belong to.
|
||||
narrows the groups they belong to. Between restarts, the unlock rule covers the gap for
|
||||
restrictions: a member whose cap was lowered is refused until the group is recomputed.
|
||||
- **Disabled members are excluded.** A disabled Jellyfin user can no longer unlock the shared
|
||||
account, and no longer receives watched state.
|
||||
- **Shared accounts cannot be nested.** A shared account may not be a member of another group; this
|
||||
|
||||
Reference in New Issue
Block a user