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:
+2 -1
View File
@@ -83,7 +83,8 @@ proxy on that route must support WebSockets.
delete or deactivate their account under **Admin → Users**.
- To end all portal access from the instance, press **Disconnect**. This stops
the connection and invalidates portal credentials locally, without requiring
the portal to be online.
the portal to be online. It also deletes the users the portal let in; the
account that enrolled and every superuser stay.
- To replace the pairing, use **Connect with a new code**. Remote users and
panels may need to pair again.
+2 -1
View File
@@ -469,7 +469,8 @@ export class CloudService {
*
* 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.
* @returns Message Successful Response
* @throws ApiError
*/
@@ -104,6 +104,7 @@ export function RemoteAccess() {
setConfirmDisconnect(false)
showSuccessToast("Disconnected from the portal")
invalidate()
queryClient.invalidateQueries({ queryKey: ["users"] })
},
onError: handleError.bind(showErrorToast),
})
@@ -290,8 +291,9 @@ export function RemoteAccess() {
<DialogTitle>Disconnect from the portal?</DialogTitle>
<DialogDescription>
Remote access ends immediately and the portal's credentials stop
working here. Nothing on this instance is changed or deleted, and
you can connect again with a new code.
working here. The users the portal let in are deleted. The account
it was connected as and every superuser stay, flows and data are
untouched, and you can connect again with a new code.
</DialogDescription>
</DialogHeader>
<DialogFooter>