Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/19554.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
When Matrix Authentication Service (MAS) integration is enabled, allow MAS to set the user locked status in Synapse.
15 changes: 15 additions & 0 deletions synapse/rest/synapse/mas/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,18 @@ class PostBody(RequestBodyModel):
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:
Expand Down Expand Up @@ -206,6 +218,9 @@ async def _async_render_POST(
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,
Expand Down
58 changes: 58 additions & 0 deletions tests/rest/synapse/mas/test_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,64 @@ def test_provision_user_invalid_json_types(self) -> None:
)
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):
Expand Down
Loading