浏览代码

Add an admin API for shadow-banning users. (#9209)

This expands the current shadow-banning feature to be usable via
the admin API and adds documentation for it.

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.
tags/v1.27.0rc1
Patrick Cloke 3 年前
committed by GitHub
父节点
当前提交
4a55d267ee
找不到此签名对应的密钥 GPG 密钥 ID: 4AEE18F83AFDEB23
共有 8 个文件被更改,包括 164 次插入7 次删除
  1. +1
    -0
      changelog.d/9209.feature
  2. +30
    -0
      docs/admin_api/user_admin_api.rst
  3. +0
    -1
      stubs/txredisapi.pyi
  4. +2
    -0
      synapse/rest/admin/__init__.py
  5. +36
    -0
      synapse/rest/admin/users.py
  6. +29
    -0
      synapse/storage/databases/main/registration.py
  7. +64
    -0
      tests/rest/admin/test_user.py
  8. +2
    -6
      tests/rest/client/test_shadow_banned.py

+ 1
- 0
changelog.d/9209.feature 查看文件

@@ -0,0 +1 @@
Add an admin API endpoint for shadow-banning users.

+ 30
- 0
docs/admin_api/user_admin_api.rst 查看文件

@@ -760,3 +760,33 @@ The following fields are returned in the JSON response body:
- ``total`` - integer - Number of pushers.

See also `Client-Server API Spec <https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushers>`_

Shadow-banning users
====================

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,
but the events are not propagated into rooms. This can be an effective tool as it
(hopefully) takes longer for the user to realise they are being moderated before
pivoting to another account.

Shadow-banning a user should be used as a tool of last resort and may lead to confusing
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::

POST /_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: see `README.rst <README.rst>`_.

An empty JSON dict is returned.

**Parameters**

The following parameters should be set in the URL:

- ``user_id`` - The fully qualified MXID: for example, ``@user:server.com``. The user must
be local.

+ 0
- 1
stubs/txredisapi.pyi 查看文件

@@ -15,7 +15,6 @@

"""Contains *incomplete* type hints for txredisapi.
"""

from typing import List, Optional, Type, Union

class RedisProtocol:


+ 2
- 0
synapse/rest/admin/__init__.py 查看文件

@@ -51,6 +51,7 @@ from synapse.rest.admin.users import (
PushersRestServlet,
ResetPasswordRestServlet,
SearchUsersRestServlet,
ShadowBanRestServlet,
UserAdminServlet,
UserMediaRestServlet,
UserMembershipRestServlet,
@@ -230,6 +231,7 @@ def register_servlets(hs, http_server):
EventReportsRestServlet(hs).register(http_server)
PushersRestServlet(hs).register(http_server)
MakeRoomAdminRestServlet(hs).register(http_server)
ShadowBanRestServlet(hs).register(http_server)


def register_servlets_for_client_rest_resource(hs, http_server):


+ 36
- 0
synapse/rest/admin/users.py 查看文件

@@ -890,3 +890,39 @@ class UserTokenRestServlet(RestServlet):
)

return 200, {"access_token": token}


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

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:

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

200 OK
{}
"""

PATTERNS = admin_patterns("/users/(?P<user_id>[^/]*)/shadow_ban")

def __init__(self, hs: "HomeServer"):
self.hs = hs
self.store = hs.get_datastore()
self.auth = hs.get_auth()

async def on_POST(self, request, user_id):
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), True)

return 200, {}

+ 29
- 0
synapse/storage/databases/main/registration.py 查看文件

@@ -360,6 +360,35 @@ class RegistrationWorkerStore(CacheInvalidationWorkerStore):

await self.db_pool.runInteraction("set_server_admin", set_server_admin_txn)

async def set_shadow_banned(self, user: UserID, shadow_banned: bool) -> None:
"""Sets whether a user shadow-banned.

Args:
user: user ID of the user to test
shadow_banned: true iff the user is to be shadow-banned, false otherwise.
"""

def set_shadow_banned_txn(txn):
self.db_pool.simple_update_one_txn(
txn,
table="users",
keyvalues={"name": user.to_string()},
updatevalues={"shadow_banned": shadow_banned},
)
# In order for this to apply immediately, clear the cache for this user.
tokens = self.db_pool.simple_select_onecol_txn(
txn,
table="access_tokens",
keyvalues={"user_id": user.to_string()},
retcol="token",
)
for token in tokens:
self._invalidate_cache_and_stream(
txn, self.get_user_by_access_token, (token,)
)

await self.db_pool.runInteraction("set_shadow_banned", set_shadow_banned_txn)

def _query_for_auth(self, txn, token: str) -> Optional[TokenLookupResult]:
sql = """
SELECT users.name as user_id,


+ 64
- 0
tests/rest/admin/test_user.py 查看文件

@@ -2380,3 +2380,67 @@ class WhoisRestTestCase(unittest.HomeserverTestCase):
self.assertEqual(200, channel.code, msg=channel.json_body)
self.assertEqual(self.other_user, channel.json_body["user_id"])
self.assertIn("devices", channel.json_body)


class ShadowBanRestTestCase(unittest.HomeserverTestCase):

servlets = [
synapse.rest.admin.register_servlets,
login.register_servlets,
]

def prepare(self, reactor, clock, hs):
self.store = hs.get_datastore()

self.admin_user = self.register_user("admin", "pass", admin=True)
self.admin_user_tok = self.login("admin", "pass")

self.other_user = self.register_user("user", "pass")

self.url = "/_synapse/admin/v1/users/%s/shadow_ban" % urllib.parse.quote(
self.other_user
)

def test_no_auth(self):
"""
Try to get information of an user without authentication.
"""
channel = self.make_request("POST", self.url)
self.assertEqual(401, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"])

def test_requester_is_not_admin(self):
"""
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)
self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])

def test_user_is_not_local(self):
"""
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)
self.assertEqual(400, channel.code, msg=channel.json_body)

def test_success(self):
"""
Shadow-banning should succeed for an admin.
"""
# The user starts off as not shadow-banned.
other_user_token = self.login("user", "pass")
result = self.get_success(self.store.get_user_by_access_token(other_user_token))
self.assertFalse(result.shadow_banned)

channel = self.make_request("POST", 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 shadow-banned (and the cache was cleared).
result = self.get_success(self.store.get_user_by_access_token(other_user_token))
self.assertTrue(result.shadow_banned)

+ 2
- 6
tests/rest/client/test_shadow_banned.py 查看文件

@@ -18,6 +18,7 @@ import synapse.rest.admin
from synapse.api.constants import EventTypes
from synapse.rest.client.v1 import directory, login, profile, room
from synapse.rest.client.v2_alpha import room_upgrade_rest_servlet
from synapse.types import UserID

from tests import unittest

@@ -31,12 +32,7 @@ class _ShadowBannedBase(unittest.HomeserverTestCase):
self.store = self.hs.get_datastore()

self.get_success(
self.store.db_pool.simple_update(
table="users",
keyvalues={"name": self.banned_user_id},
updatevalues={"shadow_banned": True},
desc="shadow_ban",
)
self.store.set_shadow_banned(UserID.from_string(self.banned_user_id), True)
)

self.other_user_id = self.register_user("otheruser", "pass")


正在加载...
取消
保存