Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 2 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/11347.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add admin API to un-shadow-ban a user.
12 changes: 9 additions & 3 deletions docs/admin_api/user_admin_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -948,7 +948,7 @@ The following fields are returned in the JSON response body:
See also the
[Client-Server API Spec on pushers](https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushers).

## Shadow-banning users
## Controlling whether a user is shadow-banned

Shadow-banning is a useful tool for moderating malicious or egregiously abusive users.
A shadow-banned users receives successful responses to their client-server API requests,
Expand All @@ -961,16 +961,22 @@ or broken behaviour for the client. A shadow-banned user will not receive any
notification and it is generally more appropriate to ban or kick abusive users.
A shadow-banned user will be unable to contact anyone on the server.

The API is:
To shadow-ban a user the API is:

```
POST /_synapse/admin/v1/users/<user_id>/shadow_ban
```

To un-shadow-ban a user the API is:

```
DELETE /_synapse/admin/v1/users/<user_id>/shadow_ban
```

To use it, you will need to authenticate by providing an `access_token` for a
server admin: [Admin API](../usage/administration/admin_api)

An empty JSON dict is returned.
An empty JSON dict is returned in both cases.

**Parameters**

Expand Down
24 changes: 22 additions & 2 deletions synapse/rest/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -909,19 +909,27 @@ async def on_POST(


class ShadowBanRestServlet(RestServlet):
"""An admin API for shadow-banning a user.
"""An admin API for controlling whether a user is shadow-banned.

A shadow-banned users receives successful responses to their client-server
API requests, but the events are not propagated into rooms.

Shadow-banning a user should be used as a tool of last resort and may lead
to confusing or broken behaviour for the client.

Example:
Example of shadow-banning a user:

POST /_synapse/admin/v1/users/@test:example.com/shadow_ban
{}

200 OK
{}

Example of removing a user from being shadow-banned:

DELETE /_synapse/admin/v1/users/@test:example.com/shadow_ban
{}

200 OK
{}
"""
Expand All @@ -945,6 +953,18 @@ async def on_POST(

return 200, {}

async def on_DELETE(
self, request: SynapseRequest, user_id: str
) -> Tuple[int, JsonDict]:
await assert_requester_is_admin(self.auth, request)

if not self.hs.is_mine_id(user_id):
raise SynapseError(400, "Only local users can be shadow-banned")

await self.store.set_shadow_banned(UserID.from_string(user_id), False)

return 200, {}


class RateLimitRestServlet(RestServlet):
"""An admin API to override ratelimiting for an user.
Expand Down
2 changes: 1 addition & 1 deletion synapse/storage/databases/main/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ async def set_shadow_banned(self, user: UserID, shadow_banned: bool) -> None:
shadow_banned: true iff the user is to be shadow-banned, false otherwise.
"""

def set_shadow_banned_txn(txn):
def set_shadow_banned_txn(txn: LoggingTransaction) -> None:
user_id = user.to_string()
self.db_pool.simple_update_one_txn(
txn,
Expand Down
26 changes: 20 additions & 6 deletions tests/rest/admin/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -3592,31 +3592,34 @@ def prepare(self, reactor, clock, hs):
self.other_user
)

def test_no_auth(self):
@parameterized.expand(["POST", "DELETE"])
def test_no_auth(self, method: str):
"""
Try to get information of an user without authentication.
"""
channel = self.make_request("POST", self.url)
channel = self.make_request(method, self.url)
self.assertEqual(401, channel.code, msg=channel.json_body)
self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"])

def test_requester_is_not_admin(self):
@parameterized.expand(["POST", "DELETE"])
def test_requester_is_not_admin(self, method: str):
"""
If the user is not a server admin, an error is returned.
"""
other_user_token = self.login("user", "pass")

channel = self.make_request("POST", self.url, access_token=other_user_token)
channel = self.make_request(method, self.url, access_token=other_user_token)
self.assertEqual(403, channel.code, msg=channel.json_body)
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])

def test_user_is_not_local(self):
@parameterized.expand(["POST", "DELETE"])
def test_user_is_not_local(self, method: str):
"""
Tests that shadow-banning for a user that is not a local returns a 400
"""
url = "/_synapse/admin/v1/whois/@unknown_person:unknown_domain"

channel = self.make_request("POST", url, access_token=self.admin_user_tok)
channel = self.make_request(method, url, access_token=self.admin_user_tok)
self.assertEqual(400, channel.code, msg=channel.json_body)

def test_success(self):
Expand All @@ -3636,6 +3639,17 @@ def test_success(self):
result = self.get_success(self.store.get_user_by_access_token(other_user_token))
self.assertTrue(result.shadow_banned)

# Un-shadow-ban the user.
channel = self.make_request(
"DELETE", self.url, access_token=self.admin_user_tok
)
self.assertEqual(200, channel.code, msg=channel.json_body)
self.assertEqual({}, channel.json_body)

# Ensure the user is no longer shadow-banned (and the cache was cleared).
result = self.get_success(self.store.get_user_by_access_token(other_user_token))
self.assertFalse(result.shadow_banned)


class RateLimitTestCase(unittest.HomeserverTestCase):

Expand Down