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