feat(files): API tokens — a machine reaches the library folder by folder
A user can make a token for a machine of theirs (`/api/tokens`, signed-in only): `sfl_` + 32 random bytes + a checksum, only its SHA-256 kept, shown once. Sent as a Bearer header it is honoured on six `/api/files` routes only — folders, list, file, hash, upload, delete — and there only in the folders its grants name, with the rights they give (list / read / write / delete). One check, `FilesCaller.may`, in every route; a folder or file out of reach answers like a missing one; the files shared with the user are never in reach. A token beats a cookie on the same request. The Hub preset makes the user's one Hub folder (a new folder kind) and grants it every right. Refused tokens are slowed down per client, a runaway machine per token; last use is stamped at most once a minute; making and revoking a token is audited, and a token's uploads and deletes name it in their audit rows.
This commit is contained in:
@@ -27,7 +27,7 @@ from starlette.requests import HTTPConnection
|
||||
|
||||
import app.models # register every model on Base.metadata
|
||||
import app.state as app_state
|
||||
from app import security
|
||||
from app import apitokens, security
|
||||
from app.auth import current_user
|
||||
from app.config import settings
|
||||
from app.db import Base, SessionLocal, engine, get_db
|
||||
@@ -242,8 +242,16 @@ def as_user(db, override_dep, _configured):
|
||||
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||
return registry[uid]
|
||||
|
||||
def _session_or_token(request: Request) -> User | None:
|
||||
# The Files routes' session half (`apitokens.session_caller`): a request carrying a token is
|
||||
# left to the real token check, exactly as in production.
|
||||
if apitokens.bearer_token(request) is not None:
|
||||
return None
|
||||
return _resolver(request)
|
||||
|
||||
override_dep(get_db, db.info["request"])
|
||||
override_dep(current_user, _resolver)
|
||||
override_dep(apitokens.session_caller, _session_or_token)
|
||||
|
||||
def _as(user: User) -> TestClient:
|
||||
registry[user.id] = user
|
||||
|
||||
Reference in New Issue
Block a user