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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user