Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from 6 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/8028.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Users can now be shadow banned -- they may be told their requests succeeded while Synapse ignored them.
16 changes: 16 additions & 0 deletions synapse/api/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,22 @@ def error_dict(self):
return cs_error(self.msg, self.errcode)


class ShadowBanError(SynapseError):
"""An exception that returns a 200 OK with a fake body.
"""

def __init__(self, body=None):
if body is None:
body = {}

self.body = body

super().__init__(200, "")

def error_dict(self):
return self.body


class ProxiedRequestError(SynapseError):
"""An error from a general matrix endpoint, eg. from a proxied Matrix API call.

Expand Down
13 changes: 13 additions & 0 deletions synapse/handlers/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import random
from typing import TYPE_CHECKING, List, Optional, Tuple

from canonicaljson import encode_canonical_json, json
Expand All @@ -34,6 +35,7 @@
Codes,
ConsentNotGivenError,
NotFoundError,
ShadowBanError,
SynapseError,
)
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersions
Expand All @@ -58,6 +60,7 @@
from synapse.util.async_helpers import Linearizer
from synapse.util.frozenutils import frozendict_json_encoder
from synapse.util.metrics import measure_func
from synapse.util.stringutils import random_string
from synapse.visibility import filter_events_for_client

from ._base import BaseHandler
Expand Down Expand Up @@ -710,13 +713,23 @@ async def create_and_send_nonmember_event(
event_dict: EventBase,
ratelimit: bool = True,
txn_id: Optional[str] = None,
ignore_shadow_ban: bool = False,
) -> Tuple[EventBase, int]:
"""
Creates an event, then sends it.

See self.create_event and self.send_nonmember_event.
"""

if not ignore_shadow_ban and await self.store.is_shadow_banned(
requester.user.to_string()
):
# We randomly sleep a bit just to annoy the requester a bit.
await self.clock.sleep(random.randint(1, 10))

# We return a response that looks roughly legit.
raise ShadowBanError({"event_id": "$" + random_string(43)})

# We limit the number of concurrent event sends in a room so that we
# don't fork the DAG too much. If we don't limit then we can end up in
# a situation where event persistence can't keep up, causing
Expand Down
8 changes: 8 additions & 0 deletions synapse/handlers/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ async def register_user(
address=None,
bind_emails=[],
by_admin=False,
shadow_banned=False,
):
"""Registers a new client on the server.

Expand All @@ -159,6 +160,7 @@ async def register_user(
bind_emails (List[str]): list of emails to bind to this account.
by_admin (bool): True if this registration is being made via the
admin api, otherwise False.
shadow_banned (bool): Shadow ban the created user.
Returns:
str: user_id
Raises:
Expand Down Expand Up @@ -194,6 +196,7 @@ async def register_user(
admin=admin,
user_type=user_type,
address=address,
shadow_banned=shadow_banned,
)

if self.hs.config.user_directory_search_all_users:
Expand Down Expand Up @@ -224,6 +227,7 @@ async def register_user(
make_guest=make_guest,
create_profile_with_displayname=default_display_name,
address=address,
shadow_banned=shadow_banned,
)

# Successfully registered
Expand Down Expand Up @@ -529,6 +533,7 @@ def register_with_store(
admin=False,
user_type=None,
address=None,
shadow_banned=False,
):
"""Register user in the datastore.

Expand All @@ -546,6 +551,7 @@ def register_with_store(
user_type (str|None): type of user. One of the values from
api.constants.UserTypes, or None for a normal user.
address (str|None): the IP address used to perform the registration.
shadow_banned (bool): Whether to shadow ban the user

Returns:
Awaitable
Expand All @@ -561,6 +567,7 @@ def register_with_store(
admin=admin,
user_type=user_type,
address=address,
shadow_banned=shadow_banned,
)
else:
return self.store.register_user(
Expand All @@ -572,6 +579,7 @@ def register_with_store(
create_profile_with_displayname=create_profile_with_displayname,
admin=admin,
user_type=user_type,
shadow_banned=shadow_banned,
)

async def register_device(
Expand Down
17 changes: 14 additions & 3 deletions synapse/handlers/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import itertools
import logging
import math
import random
import string
from collections import OrderedDict
from typing import Awaitable, Optional, Tuple
Expand Down Expand Up @@ -614,6 +615,7 @@ async def create_room(
else:
room_alias = None

invite_3pid_list = config.get("invite_3pid", [])
invite_list = config.get("invite", [])
for i in invite_list:
try:
Expand All @@ -622,6 +624,17 @@ async def create_room(
except Exception:
raise SynapseError(400, "Invalid user_id: %s" % (i,))

if (invite_list or invite_3pid_list) and await self.store.is_shadow_banned(
user_id
):
# We randomly sleep a bit just to annoy the requester a bit.
await self.clock.sleep(random.randint(1, 10))

# We actually allow this request to go through, but remove any
# associated invites
invite_list = []
invite_3pid_list = []

await self.event_creation_handler.assert_accepted_privacy_policy(requester)

power_level_content_override = config.get("power_level_content_override")
Expand All @@ -636,8 +649,6 @@ async def create_room(
% (user_id,),
)

invite_3pid_list = config.get("invite_3pid", [])

visibility = config.get("visibility", None)
is_public = visibility == "public"

Expand Down Expand Up @@ -802,7 +813,7 @@ async def send(etype, content, **kwargs) -> int:
_,
last_stream_id,
) = await self.event_creation_handler.create_and_send_nonmember_event(
creator, event, ratelimit=False
creator, event, ratelimit=False, ignore_shadow_ban=True,
)
return last_stream_id

Expand Down
24 changes: 23 additions & 1 deletion synapse/handlers/room_member.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,21 @@

import abc
import logging
import random
from http import HTTPStatus
from typing import Dict, Iterable, List, Optional, Tuple, Union

from unpaddedbase64 import encode_base64

from synapse import types
from synapse.api.constants import MAX_DEPTH, EventTypes, Membership
from synapse.api.errors import AuthError, Codes, LimitExceededError, SynapseError
from synapse.api.errors import (
AuthError,
Codes,
LimitExceededError,
ShadowBanError,
SynapseError,
)
from synapse.api.ratelimiting import Ratelimiter
from synapse.api.room_versions import EventFormatVersions
from synapse.crypto.event_signing import compute_event_reference_hash
Expand Down Expand Up @@ -281,6 +288,14 @@ async def update_membership(
content: Optional[dict] = None,
require_consent: bool = True,
) -> Tuple[str, int]:
if action == Membership.INVITE and await self.store.is_shadow_banned(
requester.user.to_string()
):
# We randomly sleep a bit just to annoy the requester a bit.
await self.clock.sleep(random.randint(1, 10))
# We return a response that looks roughly legit.
raise ShadowBanError({})

key = (room_id,)

with (await self.member_linearizer.queue(key)):
Expand Down Expand Up @@ -776,6 +791,13 @@ async def do_3pid_invite(
403, "Invites have been disabled on this server", Codes.FORBIDDEN
)

if await self.store.is_shadow_banned(requester.user.to_string()):
# We randomly sleep a bit just to annoy the requester a bit.
await self.clock.sleep(random.randint(1, 10))

# We return a response that looks roughly legit.
raise ShadowBanError({})

# We need to rate limit *before* we send out any 3PID invites, so we
# can't just rely on the standard ratelimiting of events.
await self.base_handler.ratelimit(requester)
Expand Down
4 changes: 4 additions & 0 deletions synapse/replication/http/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ async def _serialize_payload(
admin,
user_type,
address,
shadow_banned,
):
"""
Args:
Expand All @@ -60,6 +61,7 @@ async def _serialize_payload(
user_type (str|None): type of user. One of the values from
api.constants.UserTypes, or None for a normal user.
address (str|None): the IP address used to perform the regitration.
shadow_banned (bool): Whether to shadow ban the user
"""
return {
"password_hash": password_hash,
Expand All @@ -70,6 +72,7 @@ async def _serialize_payload(
"admin": admin,
"user_type": user_type,
"address": address,
"shadow_banned": shadow_banned,
}

async def _handle_request(self, request, user_id):
Expand All @@ -87,6 +90,7 @@ async def _handle_request(self, request, user_id):
admin=content["admin"],
user_type=content["user_type"],
address=content["address"],
shadow_banned=content["shadow_banned"],
)

return 200, {}
Expand Down
16 changes: 16 additions & 0 deletions synapse/storage/databases/main/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ def get_user_by_id(self, user_id):
desc="get_user_by_id",
)

async def is_shadow_banned(self, user_id):
return await self.db_pool.simple_select_one_onecol(
table="users",
keyvalues={"name": user_id},
retcol="shadow_banned",
allow_none=True,
desc="is_shadow_banned",
)

@defer.inlineCallbacks
def is_trial_user(self, user_id):
"""Checks if user is in the "trial" period, i.e. within the first
Expand Down Expand Up @@ -975,6 +984,7 @@ def register_user(
create_profile_with_displayname=None,
admin=False,
user_type=None,
shadow_banned=False,
):
"""Attempts to register an account.

Expand All @@ -991,6 +1001,8 @@ def register_user(
admin (boolean): is an admin user?
user_type (str|None): type of user. One of the values from
api.constants.UserTypes, or None for a normal user.
shadow_banned (bool): Whether the user is shadow banned,
i.e. they may be told their requests succeeded but we ignored them.

Raises:
StoreError if the user_id could not be registered.
Expand All @@ -1009,6 +1021,7 @@ def register_user(
create_profile_with_displayname,
admin,
user_type,
shadow_banned,
)

def _register_user(
Expand All @@ -1022,6 +1035,7 @@ def _register_user(
create_profile_with_displayname,
admin,
user_type,
shadow_banned,
):
user_id_obj = UserID.from_string(user_id)

Expand Down Expand Up @@ -1051,6 +1065,7 @@ def _register_user(
"appservice_id": appservice_id,
"admin": 1 if admin else 0,
"user_type": user_type,
"shadow_banned": shadow_banned,
},
)
else:
Expand All @@ -1065,6 +1080,7 @@ def _register_user(
"appservice_id": appservice_id,
"admin": 1 if admin else 0,
"user_type": user_type,
"shadow_banned": shadow_banned,
},
)

Expand Down
16 changes: 16 additions & 0 deletions synapse/storage/databases/main/schema/delta/58/09shadow_ban.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* Copyright 2020 The Matrix.org Foundation C.I.C
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

ALTER TABLE users ADD COLUMN shadow_banned BOOLEAN;
Loading