fix(files): a token made as every session ends goes with them, and a file being shared holds its job
This commit is contained in:
@@ -10,16 +10,19 @@ holds nothing, and a client trying tokens is slowed down.
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
from fastapi import Depends, Request
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import select
|
||||
|
||||
from app import apitokens
|
||||
from app.apitokens import files_caller
|
||||
from app.auth import current_user
|
||||
from app.downloads import folders, service
|
||||
from app.downloads import gc as gcmod
|
||||
from app.downloads import upload as uploadmod
|
||||
from app.downloads.status import JobStatus
|
||||
from app.config import settings
|
||||
from app.db import SessionLocal, get_db
|
||||
from app.main import app
|
||||
from app.models import (
|
||||
ApiToken,
|
||||
@@ -29,9 +32,11 @@ from app.models import (
|
||||
FileFolder,
|
||||
MediaAsset,
|
||||
Notification,
|
||||
User,
|
||||
)
|
||||
from app.ratelimit import RateLimiter
|
||||
from app.routes import tokens as tokens_routes
|
||||
from app.routes.auth import bump_session_epoch
|
||||
from app.security import hash_token
|
||||
|
||||
|
||||
@@ -173,6 +178,39 @@ def test_a_token_is_granted_only_the_users_own_folders_and_writes_only_where_upl
|
||||
assert db.execute(select(ApiToken)).all() == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("right", ["read", "write"])
|
||||
def test_a_folder_granted_is_held_until_the_token_is_in(db, make_user, right):
|
||||
"""A delete of the folder at the same moment waits and takes the new grant with it, rather than
|
||||
failing the grant's foreign key."""
|
||||
import threading
|
||||
|
||||
alice = make_user()
|
||||
fid = _own(db, alice.id, "Mine").id
|
||||
body = tokens_routes.TokenIn(
|
||||
name="x", grants=[tokens_routes.GrantIn(folder_id=fid, rights=[right])]
|
||||
)
|
||||
tokens_routes._wanted_grants(db, alice, body)
|
||||
outcome: list[object] = []
|
||||
|
||||
def delete_it():
|
||||
other = SessionLocal()
|
||||
try:
|
||||
outcome.append(folders.delete_own(other, other.get(FileFolder, fid)))
|
||||
other.commit()
|
||||
except Exception as e: # noqa: BLE001 — the assertion below names what went wrong
|
||||
outcome.append(e)
|
||||
finally:
|
||||
other.close()
|
||||
|
||||
t = threading.Thread(target=delete_it)
|
||||
t.start()
|
||||
t.join(timeout=1)
|
||||
assert t.is_alive() # the delete waits for the token on its way in
|
||||
db.commit()
|
||||
t.join(timeout=10)
|
||||
assert not isinstance(outcome[0], Exception), outcome
|
||||
|
||||
|
||||
def test_a_user_has_at_most_the_ceiling_of_tokens(db, make_user, as_user, monkeypatch):
|
||||
monkeypatch.setattr(tokens_routes, "MAX_TOKENS", 2)
|
||||
c = as_user(make_user())
|
||||
@@ -368,6 +406,60 @@ def test_whatever_ends_every_session_ends_every_token_too(db, make_user, client,
|
||||
assert revoked.actor_id == alice.id and "every session" in (revoked.summary or "")
|
||||
|
||||
|
||||
def test_a_token_asked_for_as_every_session_ends_is_refused(db, make_user, client, override_dep):
|
||||
"""The session was still good when its request came in, and ended before the token was made."""
|
||||
alice = make_user(email="alice@example.com")
|
||||
c = _signed_in(client)
|
||||
|
||||
def ended_meanwhile(request: Request, session=Depends(get_db)):
|
||||
user = current_user(request, session)
|
||||
other = SessionLocal()
|
||||
try:
|
||||
bump_session_epoch(other, other.get(User, alice.id))
|
||||
other.commit()
|
||||
finally:
|
||||
other.close()
|
||||
return user
|
||||
|
||||
override_dep(current_user, ended_meanwhile)
|
||||
r = c.post("/api/tokens", json={"name": "late", "preset": "hub"})
|
||||
assert r.status_code == 401
|
||||
assert db.execute(select(ApiToken.id)).scalars().all() == []
|
||||
|
||||
|
||||
def test_a_token_made_as_every_session_ends_goes_with_them(db, make_user):
|
||||
"""The other order: the token is on its way in when the sessions end."""
|
||||
import threading
|
||||
|
||||
alice = make_user(email="alice@example.com")
|
||||
db.execute(select(User.id).where(User.id == alice.id).with_for_update()) # as create_token
|
||||
raw = apitokens.mint()
|
||||
db.add(ApiToken(user_id=alice.id, name="t", prefix=raw[:12], token_hash=hash_token(raw)))
|
||||
db.flush()
|
||||
outcome: list[object] = []
|
||||
|
||||
def end_sessions():
|
||||
other = SessionLocal()
|
||||
try:
|
||||
bump_session_epoch(other, other.get(User, alice.id))
|
||||
other.commit()
|
||||
outcome.append("done")
|
||||
except Exception as e: # noqa: BLE001 — the assertion below names what went wrong
|
||||
outcome.append(e)
|
||||
finally:
|
||||
other.close()
|
||||
|
||||
t = threading.Thread(target=end_sessions)
|
||||
t.start()
|
||||
t.join(timeout=1)
|
||||
assert t.is_alive() # it waits for the token being made
|
||||
db.commit()
|
||||
t.join(timeout=10)
|
||||
assert outcome == ["done"]
|
||||
db.expire_all()
|
||||
assert db.execute(select(ApiToken.id)).scalars().all() == []
|
||||
|
||||
|
||||
# --- a token that does not hold ------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user