A token whose account row is gone is admitted, where upstream refuses it #144
Loading…
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?
crates/jellyfin-http/src/auth.rsreads the account's permissions as a requestbecomes a caller and never asks whether the account is there. Upstream's
DefaultAuthorizationHandlerdoes:— which its exception middleware maps to
404. Ours reads an empty permissionlist, then an empty schedule list, and admits.
Reachable only through a token whose account has since been deleted.
session/users.rs::delete_usertakes the tokens with the row, so the dashboardpath closes it; what is left is a restore, a direct write, or a row removed by
anything that does not revoke.
Found in the review of #142/#134 alongside #143, which is the same shape one
layer up — worth doing in the same pass, because both are a row the caller path
does not read.
Where to start
Authenticated::ofincrates/jellyfin-http/src/auth.rs. The cost is the point:this is a third read per authenticated request unless the answer can be had off
one already made.
users.permissions()returns an empty vector for an accountthat is not there and for one carrying no permission rows, so it cannot stand
in — measure whether
by_idis worth it, or whether the repository shouldanswer both in one statement.
Whatever it costs, say so with a number: #134 argued its two reads and this one
should be argued the same way.
How to know it is done
A token for a deleted account is answered
404with the body upstream gives, ona route under each policy type. A route test — deleting an account on the shared
reference is a write.
House rules
Tests run under
cargo nextest; upstream's source is the checkout inupstream/at the pin; comments say what the code does, not how it got there.
Closed by
9775776.This issue's premise was wrong: the
404is unreachable.AuthorizationInfo.UserId => User?.Id ?? Guid.Empty, so a token whose accountrow has gone carries the empty id — and
DefaultAuthorizationHandlerreturnsat its first arm,
!isApiKey && userId.IsEmpty(), before ever reaching theGetUserByIdnull check that throwsResourceNotFoundException.And the answer is not one status. That first arm returns without deciding, and
ASP.NET satisfies a requirement when any handler succeeds it — so what a caller
with no account is told depends on whether the policy has another handler that
can. Measured route by route, on a private instance with the row taken out from
under a live token:
[Authorize],IgnoreParentalControlFirstTimeSetupOrDefault,FirstTimeSetupOrIgnoreParentalControlUserRoles.User, which every authenticated caller carriesFirstTimeSetupOrElevated,RequiresElevation,UserPermissionRequirementUser, not administratorSo
admitted()returnsAdmitted::{Yes, Undecided}rather than a bare result,and each policy says what undecided costs it. It also caught a second bug the
issue did not mention:
administers()was still reading the dead account'spermission rows, so an orphaned token administered the server. What an account
holds belongs to the account now.
The state needs a write that ignores the constraint.
Devices.UserIdcascades, so the route — and any
DELETEwith foreign keys on — takes thetokens with the row and the token simply stops resolving, which is
401. Whatis left is a restore, a migration, or a hand-edited database. Both are asserted.
The cost. One primary-key read per authenticated request, which is the read
this issue said would have to be argued: 0.464 ms median over a thousand
sequential
/Users/Merequests against 0.470 ms without it — below the floor ofthat measurement, and recorded in D32 as wanting a retake on a larger library.
Replayed as a pair afterwards: 7 of 8 routes agree exactly. The eighth is
upstream answering
500on the first request after a restart, where it tripsover the foreign-key violation while stamping the device row it just used — an
artefact of the state I had to manufacture, not a decision; every request after
it is the
403we give.A token whose account was deleted is admitted, where upstream answers 404to A token whose account row is gone is admitted, where upstream refuses it