Skip to content

Commit 3aa948c

Browse files
authored
When Matrix Authentication Service (MAS) integration is enabled, allow MAS to set the user locked status in Synapse. (#19554)
Companion PR: element-hq/matrix-authentication-service#5550 to 1) send this flag and 2) provision users proactively when their lock status changes. --- Currently Synapse and MAS have two independent user lock implementations. This PR makes it so that MAS can push its lock status to Synapse when 'provisioning' the user. Having the lock status in Synapse is useful for removing users from the user directory when they are locked. There is otherwise no authentication requirement to have it in Synapse; the enforcement is done by MAS at token introspection time. --------- Signed-off-by: Olivier 'reivilibre <oliverw@matrix.org>
1 parent a71c468 commit 3aa948c

3 files changed

Lines changed: 74 additions & 0 deletions

File tree

changelog.d/19554.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
When Matrix Authentication Service (MAS) integration is enabled, allow MAS to set the user locked status in Synapse.

synapse/rest/synapse/mas/users.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,18 @@ class PostBody(RequestBodyModel):
112112
unset_emails: StrictBool = False
113113
set_emails: list[StrictStr] | None = None
114114

115+
locked: StrictBool | None = None
116+
"""
117+
True to lock user. False to unlock. None to leave the same.
118+
119+
This is mostly for informational purposes; if the user's account is locked in MAS
120+
but not in Synapse, the token introspection response will prevent them from using
121+
their account.
122+
123+
However, having a local copy of the locked state in Synapse is useful for excluding
124+
the user from the user directory.
125+
"""
126+
115127
@model_validator(mode="before")
116128
@classmethod
117129
def validate_exclusive(cls, values: Any) -> Any:
@@ -206,6 +218,9 @@ async def _async_render_POST(
206218
validated_at=current_time,
207219
)
208220

221+
if body.locked is not None:
222+
await self.store.set_user_locked_status(user_id.to_string(), body.locked)
223+
209224
if body.unset_avatar_url:
210225
await self.profile_handler.set_avatar_url(
211226
target_user=user_id,

tests/rest/synapse/mas/test_users.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,64 @@ def test_provision_user_invalid_json_types(self) -> None:
362362
)
363363
self.assertEqual(channel.code, 400, f"Should fail for content: {content}")
364364

365+
def test_lock_and_unlock(self) -> None:
366+
store = self.hs.get_datastores().main
367+
368+
# Create a user in the locked state
369+
alice = UserID("alice", "test")
370+
channel = self.make_request(
371+
"POST",
372+
"/_synapse/mas/provision_user",
373+
shorthand=False,
374+
access_token=self.SHARED_SECRET,
375+
content={
376+
"localpart": alice.localpart,
377+
"locked": True,
378+
},
379+
)
380+
# This created the user, hence the 201 status code
381+
self.assertEqual(channel.code, 201, channel.json_body)
382+
self.assertEqual(channel.json_body, {})
383+
self.assertTrue(
384+
self.get_success(store.get_user_locked_status(alice.to_string()))
385+
)
386+
387+
# Then transition from locked to unlocked
388+
channel = self.make_request(
389+
"POST",
390+
"/_synapse/mas/provision_user",
391+
shorthand=False,
392+
access_token=self.SHARED_SECRET,
393+
content={
394+
"localpart": alice.localpart,
395+
"locked": False,
396+
},
397+
)
398+
# This updated the user, hence the 200 status code
399+
self.assertEqual(channel.code, 200, channel.json_body)
400+
self.assertEqual(channel.json_body, {})
401+
self.assertFalse(
402+
self.get_success(store.get_user_locked_status(alice.to_string()))
403+
)
404+
405+
# And back from unlocked to locked
406+
channel = self.make_request(
407+
"POST",
408+
"/_synapse/mas/provision_user",
409+
shorthand=False,
410+
access_token=self.SHARED_SECRET,
411+
content={
412+
"localpart": alice.localpart,
413+
"locked": True,
414+
},
415+
)
416+
# This updated the user, hence the 200 status code
417+
self.assertEqual(channel.code, 200, channel.json_body)
418+
self.assertEqual(channel.json_body, {})
419+
self.assertTrue(
420+
self.get_success(store.get_user_locked_status(alice.to_string()))
421+
)
422+
365423

366424
@skip_unless(HAS_AUTHLIB, "requires authlib")
367425
class MasIsLocalpartAvailableResource(BaseTestCase):

0 commit comments

Comments
 (0)