Skip to content

Release v0.27.0

Choose a tag to compare

@DerKezorm DerKezorm released this 02 Sep 19:53
· 11 commits to main since this release

New

  • Existing accounts can be taken over from a media server. Whoever already runs Plex,
    Jellyfin or Emby already has the people. Until now an account came into being only when
    somebody signed in for the first time, and with Jellyfin and Emby it did not come into
    being at all: neither knows an e-mail address for an account, and without one Nexview
    cannot tell a new person from somebody who has had an account for years. Onboarding
    thirty people meant creating thirty accounts by hand, setting thirty passwords and
    passing them on, after which each person still had to link the server in their own
    profile.

    Nexview now asks the server for its accounts and shows them as a list. Nothing is
    preselected, and for every row the decision is the operator's: a new account, or a link
    to one that already exists. Linking is never the default, because a wrong link hands
    whoever controls that server identity the way into somebody else's Nexview account, and
    that must not happen by clicking past it.

    What the import writes is the account and the link, in one transaction. The link is
    the actual work: without it there would be an account nobody can get into.

    Three things it deliberately does not do. It does not guess who belongs to whom: across
    providers there is no reliable signal, Plex knows an e-mail address while Jellyfin and
    Emby know none, and the same person is often named differently on two servers. It does
    not offer a role, so an import creates plain users and never approvers or
    administrators. And it does not carry quotas over to an account that is merely linked,
    because those were set deliberately once.

    For newly created accounts the limits can be set for the batch: movies, shows and
    storage, each as house default, unlimited or a fixed number, plus a tick that creates
    them inactive. The three-valued form is the one used everywhere else, and for the same
    reason as in 0.26.2: 0 means "may request nothing", which is not the same as "house
    default".

  • The list says which accounts already exist, and under which name. An entry in the
    assignment offers every Nexview account that has no link for this provider - not
    "no link at all". Someone who is already on Plex has to be offered at the Jellyfin
    import, or a second account is created for the same human, and Nexview cannot merge two
    accounts. Each entry names what it already hangs on, "jamie (Plex)" rather than
    "jamie", because on the second import that is the only hint there is.

Fixed

  • The block list was only visible for Plex. When an account is deleted, its
    media-server identity lands on a block list so that it cannot simply create itself again
    at the next sign-in. The list was shown for Plex only, with a reason that was correct
    until this release: with Jellyfin and Emby no account could come into being anyway, so a
    block there could not achieve anything.

    The import makes that assumption false. The list is now shown for all three. Reported
    after exactly this sequence: take over, delete, take over again, "access blocked" - and
    the list holding the entry was hidden.

    A blocked identity is refused by the import, and the row says so with a badge. Ticking
    it anyway and confirming once lifts the block rather than bypassing it: a block left
    standing would mean account and link exist while the sign-in is still refused, which is
    precisely the state this feature is meant to prevent. Lifting a block is written to the
    log.

Security

  • The watcher over the owner account stopped at a folder boundary. It checks from two
    sides: the route table, and the source. The source side read app/routers in full but
    looked outside it only for writes that go through the query layer, never for the
    ordinary one. Two lines in services/tickets.py were enough: a second administrator
    sent the owner a perfectly normal ticket, and came out of it as the only administrator
    left. All 2,491 tests stayed green.

    Both sides now use one and the same detection, over the whole backend. Fifteen places
    outside the routers came to light and were each examined by hand; none of them was a
    hole. Three are false alarms worth keeping: email, username and parent_id exist on
    more than one table, and a setattr can hit any object at all, so the watcher reports
    them rather than guess. Two of the exemptions carry a warning for whoever reads them
    next. children.aendern is harmless only because ChildUpdate is a closed model of six
    fields; give it extra="allow" some day and it becomes a write to every column of the
    user table. And unlocking child accounts from a ticket is harmless only because it
    grants a permission and never takes one away.

  • And its other half hung on seven hand-written field names. A request can name a
    foreign account through a field in its body, and the watcher knew seven names it might
    carry. Four of them (konto_id, kind_id, benutzer_id, benutzer_ids) do not exist
    anywhere in the schema and never matched a thing; eight that do exist were missing,
    among them parent_id, for_child_id and approved_by. Exactly the kind of list this
    watcher exists to make unnecessary.

    The names now come from the database itself: every column with a foreign key to
    users.id, plus the plural form. That still leaves the case a name cannot cover, a
    field called ziel or empfaenger, so a third check looks for the grab itself,
    db.get(User, ...) in the handler. The watcher sees 17 addresses where it saw 8.

  • secret.key was readable by everyone. Measured in the published image:
    -rw-r--r--, next to the database, in the directory an operator mounts from outside. It
    is the key the stored Radarr, Sonarr, TMDB and mail credentials are encrypted with. It
    is now created with 0600, and tightened to it on every start, so an installation
    that has been running for weeks gets there too, not only a fresh one.

    Honest about the size of it: this is hardening, not a wall. Whoever can read /data can
    read nexview.db anyway. It helps in the case the encryption was built for, where the
    database travels somewhere without the directory around it.

    The backups inside the data directory stay plain SQLite files. Only the download is
    encrypted, and the README and the interface now say so instead of implying otherwise.

Under the hood

  • An age filter could be lost from a list without a sound. The age check itself is
    well tested and was never the problem; the problem was that nothing asked whether every
    list still calls it. Taking the filter out of the favourites list let a twelve year old
    see the FSK 18 title, and not one of 2,491 tests noticed.

    There is now a watcher that measures the result rather than the intention: an account
    limited to twelve, a catalogue of three titles, and every title-serving address called
    once. Two assertions per address, and the second one is what makes it work: the blocked
    title must be absent, and the allowed one must be present. Without that, an address
    answering with an error would have passed without ever serving anything.

    The first attempt was a source scan and failed its own counter-check, because the call
    to the filter stays in the code and an if in front of it only makes it unreachable.

  • A renamed column loses its data without a word. Nexview has no numbered migrations;
    the start path compares the models with the file on disk and adds what is missing. That
    covers the common case and is well tested. It does not cover renaming, and it did not
    say so. Rename a column in the model, start as if updating: a new empty column appears,
    the values stay in the old one, the application shows a blank, and nothing is logged.
    The test suite cannot see it either, because every test starts from a fresh database.

    tests/schema_abdruck.json puts that in the diff, and the start now names every column
    the database still has and the model does not. It changes nothing on its own: deleting
    a column automatically is precisely the move that costs data.

  • Two click paths are now tested in a real browser. Search, open, request, find it in
    the list; and a child wishes for something while a parent releases it. Both assert the
    outcome rather than that something is visible.