Disconnect deletes the users the portal let in

A local Disconnect deletes every portal-mapped user except the account the
instance was enrolled as and superusers, so no inert rows stay behind. The
confirm dialog says so, and Admin → Users refreshes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WgBfkm4rCmtd1T4vHWjEPx
This commit is contained in:
2026-10-15 11:02:07 +02:00
co-authored by Claude Opus 5.5
parent 391edc8766
commit 579c0c22b2
5 changed files with 57 additions and 6 deletions
+14 -2
View File
@@ -281,19 +281,31 @@ def forget_remote_user(portal_sub: str) -> None:
@router.delete(
"", dependencies=[Depends(get_current_active_superuser)], response_model=Message
)
def disconnect(request: Request) -> Message:
def disconnect(request: Request, session: SessionDep) -> Message:
"""Sever the connection from this side.
Unilateral and immediate: the config is the only reason portal tokens
verify here, so deleting it ends remote access whatever the portal still
has on file.
has on file. The users the portal let in go with it. The account it was
enrolled as stays, and so does every superuser.
"""
task = getattr(request.app.state, "cloud_task", None)
if task is not None:
task.cancel()
request.app.state.cloud_task = None
request.app.state.cloud_connector = None
config = cloud_config.load()
cloud_config.delete()
owner = config.local_user_id if config is not None else None
for user in session.exec(
select(User).where(
col(User.portal_sub).is_not(None),
User.is_superuser == False, # noqa: E712
)
):
if str(user.id) != owner:
session.delete(user)
session.commit()
return Message(message="Disconnected from the portal")
+35
View File
@@ -93,6 +93,41 @@ def test_disconnecting_ends_remote_access(
decode_token(portal_token(portal_key))
def test_disconnecting_deletes_the_users_the_portal_let_in(
client: TestClient,
enrolled: User,
superuser_token_headers: dict[str, str],
db: Session,
) -> None:
"""Nothing reaches them once the portal is gone; the rest stay."""
member = User(email="member-dc@example.com", hashed_password="x", portal_sub="p-1")
boss = User(
email="boss-dc@example.com",
hashed_password="x",
portal_sub="p-2",
is_superuser=True,
)
plain = create_random_user(db)
db.add(member)
db.add(boss)
db.commit()
member_id, boss_id = member.id, boss.id
response = client.delete(
f"{settings.API_V1_STR}/cloud", headers=superuser_token_headers
)
assert response.status_code == 200, response.text
db.expire_all()
assert db.get(User, member_id) is None
assert db.get(User, boss_id) is not None
assert db.get(User, plain.id) is not None
assert db.get(User, enrolled.id) is not None
assert cloud_config.load() is None
db.delete(db.get(User, boss_id))
db.delete(db.get(User, plain.id))
db.commit()
def test_status_reports_not_enrolled(
client: TestClient, superuser_token_headers: dict[str, str]
) -> None: