When Matrix Authentication Service (MAS) integration is enabled, allow MAS to set the user locked status in Synapse. (#19554)

Companion PR:
https://github.com/element-hq/matrix-authentication-service/pull/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>
This commit is contained in:
Olivier 'reivilibre authored and GitHub committed 2026-03-16 18:27:54 +00:00
1 parent a71c468b04
commit 3aa948c50c
3 files changed
+74

No files matched your search

+1
View File
@@ -0,0 +1 @@
When Matrix Authentication Service (MAS) integration is enabled, allow MAS to set the user locked status in Synapse.
+15
View File
@@ -112,6 +112,18 @@ class MasProvisionUserResource(MasBaseResource):
unset_emails: StrictBool = False
set_emails: list[StrictStr] | None = None
locked: StrictBool | None = None
"""
True to lock user. False to unlock. None to leave the same.
This is mostly for informational purposes; if the user's account is locked in MAS
but not in Synapse, the token introspection response will prevent them from using
their account.
However, having a local copy of the locked state in Synapse is useful for excluding
the user from the user directory.
"""
@model_validator(mode="before")
@classmethod
def validate_exclusive(cls, values: Any) -> Any:
@@ -206,6 +218,9 @@ class MasProvisionUserResource(MasBaseResource):
validated_at=current_time,
)
if body.locked is not None:
await self.store.set_user_locked_status(user_id.to_string(), body.locked)
if body.unset_avatar_url:
await self.profile_handler.set_avatar_url(
target_user=user_id,
+58
View File
@@ -362,6 +362,64 @@ class MasProvisionUserResource(BaseTestCase):
)
self.assertEqual(channel.code, 400, f"Should fail for content: {content}")
def test_lock_and_unlock(self) -> None:
store = self.hs.get_datastores().main
# Create a user in the locked state
alice = UserID("alice", "test")
channel = self.make_request(
"POST",
"/_synapse/mas/provision_user",
shorthand=False,
access_token=self.SHARED_SECRET,
content={
"localpart": alice.localpart,
"locked": True,
},
)
# This created the user, hence the 201 status code
self.assertEqual(channel.code, 201, channel.json_body)
self.assertEqual(channel.json_body, {})
self.assertTrue(
self.get_success(store.get_user_locked_status(alice.to_string()))
)
# Then transition from locked to unlocked
channel = self.make_request(
"POST",
"/_synapse/mas/provision_user",
shorthand=False,
access_token=self.SHARED_SECRET,
content={
"localpart": alice.localpart,
"locked": False,
},
)
# This updated the user, hence the 200 status code
self.assertEqual(channel.code, 200, channel.json_body)
self.assertEqual(channel.json_body, {})
self.assertFalse(
self.get_success(store.get_user_locked_status(alice.to_string()))
)
# And back from unlocked to locked
channel = self.make_request(
"POST",
"/_synapse/mas/provision_user",
shorthand=False,
access_token=self.SHARED_SECRET,
content={
"localpart": alice.localpart,
"locked": True,
},
)
# This updated the user, hence the 200 status code
self.assertEqual(channel.code, 200, channel.json_body)
self.assertEqual(channel.json_body, {})
self.assertTrue(
self.get_success(store.get_user_locked_status(alice.to_string()))
)
@skip_unless(HAS_AUTHLIB, "requires authlib")
class MasIsLocalpartAvailableResource(BaseTestCase):