Delete a member's local user when the portal removes them
On a member_removed frame the connector deletes the local user mapped to that portal account, never the owner and never a superuser, since the portal only ever creates ordinary accounts here. An instance offline at the time keeps the inert row, as before.
This commit is contained in:
@@ -139,6 +139,8 @@ class CloudConnector:
|
||||
self._connected_since: float | None = None
|
||||
self._last_error: str | None = None
|
||||
self._revoked = False
|
||||
#: The portal account that owns this instance, as the last welcome said.
|
||||
self._owner: str | None = None
|
||||
|
||||
@property
|
||||
def revoked(self) -> bool:
|
||||
@@ -235,7 +237,8 @@ class CloudConnector:
|
||||
str(welcome.get("reason") or "refused by the portal")
|
||||
)
|
||||
|
||||
self._adopt_owner(config, welcome.get("owner"))
|
||||
self._owner = welcome.get("owner")
|
||||
self._adopt_owner(config, self._owner)
|
||||
|
||||
attached = True
|
||||
self._connected = True
|
||||
@@ -256,6 +259,12 @@ class CloudConnector:
|
||||
self._start_stream(socket, frame)
|
||||
elif op == "ws_close":
|
||||
self._cancel_stream(str(frame.get("id") or ""))
|
||||
elif op == "member_removed":
|
||||
await asyncio.to_thread(
|
||||
self._forget_member,
|
||||
str(frame.get("user") or ""),
|
||||
self._owner,
|
||||
)
|
||||
finally:
|
||||
keepalive.cancel()
|
||||
# Awaited, not just cancelled: a keepalive that died of its own
|
||||
@@ -310,6 +319,35 @@ class CloudConnector:
|
||||
# link: everything else this connection does still works.
|
||||
logger.exception("Could not map the enrolling account to the portal owner")
|
||||
|
||||
@staticmethod
|
||||
def _forget_member(portal_sub: str, owner: str | None) -> None:
|
||||
"""Delete the local user of somebody the portal no longer admits.
|
||||
|
||||
Never the owner, and never a superuser: the portal only ever creates
|
||||
ordinary accounts here, so those are not its to take away.
|
||||
"""
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from fluksio.core.db import engine
|
||||
from fluksio.models import User
|
||||
|
||||
if not portal_sub or portal_sub == owner:
|
||||
return
|
||||
try:
|
||||
with Session(engine) as session:
|
||||
user = session.exec(
|
||||
select(User).where(User.portal_sub == portal_sub)
|
||||
).first()
|
||||
if user is None or user.is_superuser:
|
||||
return
|
||||
session.delete(user)
|
||||
session.commit()
|
||||
logger.info(
|
||||
"Deleted %s, removed from this instance at the portal", user.email
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Could not delete the user the portal removed")
|
||||
|
||||
@staticmethod
|
||||
def _local_account(session: Any, config: cloud_config.CloudConfig) -> Any:
|
||||
"""The account this instance acts as, or the one that now stands for it.
|
||||
|
||||
@@ -691,3 +691,29 @@ async def test_a_revoked_link_stands_down(enrolled: User) -> None: # noqa: ARG0
|
||||
await asyncio.sleep(0.05)
|
||||
watcher.cancel()
|
||||
assert not started
|
||||
|
||||
|
||||
def test_a_member_removed_at_the_portal_loses_their_user(db: Session) -> None:
|
||||
"""The portal takes away only the ordinary accounts it gave."""
|
||||
from fluksio.cloud.connector import CloudConnector
|
||||
|
||||
member = User(email="gone@example.com", hashed_password="x", portal_sub="p-9")
|
||||
boss = User(
|
||||
email="boss@example.com",
|
||||
hashed_password="x",
|
||||
portal_sub="p-8",
|
||||
is_superuser=True,
|
||||
)
|
||||
db.add(member)
|
||||
db.add(boss)
|
||||
db.commit()
|
||||
member_id, boss_id = member.id, boss.id
|
||||
|
||||
CloudConnector._forget_member("p-9", "p-8")
|
||||
CloudConnector._forget_member("p-8", "p-8")
|
||||
CloudConnector._forget_member("p-8", None)
|
||||
db.expire_all()
|
||||
assert db.get(User, member_id) is None
|
||||
assert db.get(User, boss_id) is not None
|
||||
db.delete(boss)
|
||||
db.commit()
|
||||
|
||||
@@ -58,8 +58,9 @@ time-limited; generate a fresh one if it expires.
|
||||
connection error. The instance needs outbound access to the portal, and any
|
||||
proxy on that route must support WebSockets.
|
||||
|
||||
- To remove a person's access, remove them from the portal's sharing dialog
|
||||
or edit their account under **Admin → Users** and clear **Is active?**.
|
||||
- To remove a person's access, remove them from the portal's sharing dialog,
|
||||
which also deletes their local account while the instance is connected, or
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user