Adopt a member the portal vouches for

A share link admits somebody at the portal, so this instance first hears of
them when they arrive rather than when a superuser types their code in.
An unmapped portal identity is now checked once against the portal's own
list of who may reach this instance and given an ordinary local account
only if the portal vouches for it.

Asking rather than believing the token is the point: a token stays signed
and valid until it expires, so trusting its claims would let one rebuild
the account somebody deleted here and deleting a user would stop being the
whole of the revocation.

The account-making itself moved out of the route, since both ways in build
the same thing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GPMNwB2mGBP5j7dXRopcPH
This commit is contained in:
2026-09-02 17:50:29 +02:00
co-authored by Claude Opus 5
parent 058f16ec1d
commit 65272a135f
6 changed files with 181 additions and 42 deletions
+11 -31
View File
@@ -13,16 +13,13 @@ chain stops with whoever a superuser here typed a code for.
from __future__ import annotations
import logging
import secrets
from typing import Any
import httpx
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.concurrency import run_in_threadpool
from pydantic import BaseModel, Field
from sqlmodel import select
from fluksio import crud
from fluksio.api.deps import (
CurrentUser,
SessionDep,
@@ -31,8 +28,7 @@ from fluksio.api.deps import (
)
from fluksio.cloud import config as cloud_config
from fluksio.cloud import enroll as enroll_mod
from fluksio.core.security import get_password_hash
from fluksio.models import Message, User, UserPublic
from fluksio.models import Message, UserPublic
logger = logging.getLogger(__name__)
@@ -149,33 +145,17 @@ def add_remote_user(session: SessionDep, body: RemoteUserBody) -> Any:
)
data = response.json()
portal_sub = str(data["user_id"])
email = str(data["email"])
existing = session.exec(select(User).where(User.portal_sub == portal_sub)).first()
if existing is not None:
raise HTTPException(
status_code=409, detail=f"{existing.email} already has access"
try:
# Shared with the share-link path, which creates the same account from
# the other end — there the portal admitted them and this instance
# finds out when they arrive. Password recovery would issue a usable
# password, which is a gap worth closing the day local sign-in for
# these accounts is wanted.
return enroll_mod.create_remote_user(
session, str(data["user_id"]), str(data["email"])
)
if crud.get_user_by_email(session=session, email=email) is not None:
# Never quietly hand an existing local account — possibly a superuser's
# — to whoever holds that address on the portal.
raise HTTPException(
status_code=409, detail="A local user with this email already exists"
)
user = User(
email=email,
# Unusable by construction: this account is reached through the portal
# or not at all. Password recovery would issue one, which is a gap
# worth closing the day local sign-in for these accounts is wanted.
hashed_password=get_password_hash(secrets.token_urlsafe(32)),
is_superuser=False,
is_active=True,
portal_sub=portal_sub,
)
session.add(user)
session.commit()
session.refresh(user)
return user
except enroll_mod.EnrollError as exc:
raise HTTPException(status_code=exc.status, detail=exc.detail) from exc
def forget_remote_user(portal_sub: str) -> None: