Add a shadow-banned flag to users. (#8092)

This commit is contained in:
Patrick Cloke 2020-08-14 12:37:59 -04:00 committed by GitHub
parent b069b78bb4
commit ac77cdb64e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 83 additions and 12 deletions

1
changelog.d/8092.feature Normal file
View File

@ -0,0 +1 @@
Add support for shadow-banning users (ignoring any message send requests).

View File

@ -213,6 +213,7 @@ class Auth(object):
user = user_info["user"] user = user_info["user"]
token_id = user_info["token_id"] token_id = user_info["token_id"]
is_guest = user_info["is_guest"] is_guest = user_info["is_guest"]
shadow_banned = user_info["shadow_banned"]
# Deny the request if the user account has expired. # Deny the request if the user account has expired.
if self._account_validity.enabled and not allow_expired: if self._account_validity.enabled and not allow_expired:
@ -252,7 +253,12 @@ class Auth(object):
opentracing.set_tag("device_id", device_id) opentracing.set_tag("device_id", device_id)
return synapse.types.create_requester( return synapse.types.create_requester(
user, token_id, is_guest, device_id, app_service=app_service user,
token_id,
is_guest,
shadow_banned,
device_id,
app_service=app_service,
) )
except KeyError: except KeyError:
raise MissingClientTokenError() raise MissingClientTokenError()
@ -297,6 +303,7 @@ class Auth(object):
dict that includes: dict that includes:
`user` (UserID) `user` (UserID)
`is_guest` (bool) `is_guest` (bool)
`shadow_banned` (bool)
`token_id` (int|None): access token id. May be None if guest `token_id` (int|None): access token id. May be None if guest
`device_id` (str|None): device corresponding to access token `device_id` (str|None): device corresponding to access token
Raises: Raises:
@ -356,6 +363,7 @@ class Auth(object):
ret = { ret = {
"user": user, "user": user,
"is_guest": True, "is_guest": True,
"shadow_banned": False,
"token_id": None, "token_id": None,
# all guests get the same device id # all guests get the same device id
"device_id": GUEST_DEVICE_ID, "device_id": GUEST_DEVICE_ID,
@ -365,6 +373,7 @@ class Auth(object):
ret = { ret = {
"user": user, "user": user,
"is_guest": False, "is_guest": False,
"shadow_banned": False,
"token_id": None, "token_id": None,
"device_id": None, "device_id": None,
} }
@ -488,6 +497,7 @@ class Auth(object):
"user": UserID.from_string(ret.get("name")), "user": UserID.from_string(ret.get("name")),
"token_id": ret.get("token_id", None), "token_id": ret.get("token_id", None),
"is_guest": False, "is_guest": False,
"shadow_banned": ret.get("shadow_banned"),
"device_id": ret.get("device_id"), "device_id": ret.get("device_id"),
"valid_until_ms": ret.get("valid_until_ms"), "valid_until_ms": ret.get("valid_until_ms"),
} }

View File

@ -142,6 +142,7 @@ class RegistrationHandler(BaseHandler):
address=None, address=None,
bind_emails=[], bind_emails=[],
by_admin=False, by_admin=False,
shadow_banned=False,
): ):
"""Registers a new client on the server. """Registers a new client on the server.
@ -159,6 +160,7 @@ class RegistrationHandler(BaseHandler):
bind_emails (List[str]): list of emails to bind to this account. bind_emails (List[str]): list of emails to bind to this account.
by_admin (bool): True if this registration is being made via the by_admin (bool): True if this registration is being made via the
admin api, otherwise False. admin api, otherwise False.
shadow_banned (bool): Shadow-ban the created user.
Returns: Returns:
str: user_id str: user_id
Raises: Raises:
@ -194,6 +196,7 @@ class RegistrationHandler(BaseHandler):
admin=admin, admin=admin,
user_type=user_type, user_type=user_type,
address=address, address=address,
shadow_banned=shadow_banned,
) )
if self.hs.config.user_directory_search_all_users: if self.hs.config.user_directory_search_all_users:
@ -224,6 +227,7 @@ class RegistrationHandler(BaseHandler):
make_guest=make_guest, make_guest=make_guest,
create_profile_with_displayname=default_display_name, create_profile_with_displayname=default_display_name,
address=address, address=address,
shadow_banned=shadow_banned,
) )
# Successfully registered # Successfully registered
@ -529,6 +533,7 @@ class RegistrationHandler(BaseHandler):
admin=False, admin=False,
user_type=None, user_type=None,
address=None, address=None,
shadow_banned=False,
): ):
"""Register user in the datastore. """Register user in the datastore.
@ -546,6 +551,7 @@ class RegistrationHandler(BaseHandler):
user_type (str|None): type of user. One of the values from user_type (str|None): type of user. One of the values from
api.constants.UserTypes, or None for a normal user. api.constants.UserTypes, or None for a normal user.
address (str|None): the IP address used to perform the registration. address (str|None): the IP address used to perform the registration.
shadow_banned (bool): Whether to shadow-ban the user
Returns: Returns:
Awaitable Awaitable
@ -561,6 +567,7 @@ class RegistrationHandler(BaseHandler):
admin=admin, admin=admin,
user_type=user_type, user_type=user_type,
address=address, address=address,
shadow_banned=shadow_banned,
) )
else: else:
return self.store.register_user( return self.store.register_user(
@ -572,6 +579,7 @@ class RegistrationHandler(BaseHandler):
create_profile_with_displayname=create_profile_with_displayname, create_profile_with_displayname=create_profile_with_displayname,
admin=admin, admin=admin,
user_type=user_type, user_type=user_type,
shadow_banned=shadow_banned,
) )
async def register_device( async def register_device(

View File

@ -44,6 +44,7 @@ class ReplicationRegisterServlet(ReplicationEndpoint):
admin, admin,
user_type, user_type,
address, address,
shadow_banned,
): ):
""" """
Args: Args:
@ -60,6 +61,7 @@ class ReplicationRegisterServlet(ReplicationEndpoint):
user_type (str|None): type of user. One of the values from user_type (str|None): type of user. One of the values from
api.constants.UserTypes, or None for a normal user. api.constants.UserTypes, or None for a normal user.
address (str|None): the IP address used to perform the regitration. address (str|None): the IP address used to perform the regitration.
shadow_banned (bool): Whether to shadow-ban the user
""" """
return { return {
"password_hash": password_hash, "password_hash": password_hash,
@ -70,6 +72,7 @@ class ReplicationRegisterServlet(ReplicationEndpoint):
"admin": admin, "admin": admin,
"user_type": user_type, "user_type": user_type,
"address": address, "address": address,
"shadow_banned": shadow_banned,
} }
async def _handle_request(self, request, user_id): async def _handle_request(self, request, user_id):
@ -87,6 +90,7 @@ class ReplicationRegisterServlet(ReplicationEndpoint):
admin=content["admin"], admin=content["admin"],
user_type=content["user_type"], user_type=content["user_type"],
address=content["address"], address=content["address"],
shadow_banned=content["shadow_banned"],
) )
return 200, {} return 200, {}

View File

@ -304,7 +304,7 @@ class RegistrationWorkerStore(SQLBaseStore):
def _query_for_auth(self, txn, token): def _query_for_auth(self, txn, token):
sql = ( sql = (
"SELECT users.name, users.is_guest, access_tokens.id as token_id," "SELECT users.name, users.is_guest, users.shadow_banned, access_tokens.id as token_id,"
" access_tokens.device_id, access_tokens.valid_until_ms" " access_tokens.device_id, access_tokens.valid_until_ms"
" FROM users" " FROM users"
" INNER JOIN access_tokens on users.name = access_tokens.user_id" " INNER JOIN access_tokens on users.name = access_tokens.user_id"
@ -952,6 +952,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore):
create_profile_with_displayname=None, create_profile_with_displayname=None,
admin=False, admin=False,
user_type=None, user_type=None,
shadow_banned=False,
): ):
"""Attempts to register an account. """Attempts to register an account.
@ -968,6 +969,8 @@ class RegistrationStore(RegistrationBackgroundUpdateStore):
admin (boolean): is an admin user? admin (boolean): is an admin user?
user_type (str|None): type of user. One of the values from user_type (str|None): type of user. One of the values from
api.constants.UserTypes, or None for a normal user. 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 ignore them.
Raises: Raises:
StoreError if the user_id could not be registered. StoreError if the user_id could not be registered.
@ -986,6 +989,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore):
create_profile_with_displayname, create_profile_with_displayname,
admin, admin,
user_type, user_type,
shadow_banned,
) )
def _register_user( def _register_user(
@ -999,6 +1003,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore):
create_profile_with_displayname, create_profile_with_displayname,
admin, admin,
user_type, user_type,
shadow_banned,
): ):
user_id_obj = UserID.from_string(user_id) user_id_obj = UserID.from_string(user_id)
@ -1028,6 +1033,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore):
"appservice_id": appservice_id, "appservice_id": appservice_id,
"admin": 1 if admin else 0, "admin": 1 if admin else 0,
"user_type": user_type, "user_type": user_type,
"shadow_banned": shadow_banned,
}, },
) )
else: else:
@ -1042,6 +1048,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore):
"appservice_id": appservice_id, "appservice_id": appservice_id,
"admin": 1 if admin else 0, "admin": 1 if admin else 0,
"user_type": user_type, "user_type": user_type,
"shadow_banned": shadow_banned,
}, },
) )

View File

@ -0,0 +1,18 @@
/* 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.
*/
-- A shadow-banned user may be told that their requests succeeded when they were
-- actually ignored.
ALTER TABLE users ADD COLUMN shadow_banned BOOLEAN;

View File

@ -51,7 +51,15 @@ JsonDict = Dict[str, Any]
class Requester( class Requester(
namedtuple( namedtuple(
"Requester", ["user", "access_token_id", "is_guest", "device_id", "app_service"] "Requester",
[
"user",
"access_token_id",
"is_guest",
"shadow_banned",
"device_id",
"app_service",
],
) )
): ):
""" """
@ -62,6 +70,7 @@ class Requester(
access_token_id (int|None): *ID* of the access token used for this access_token_id (int|None): *ID* of the access token used for this
request, or None if it came via the appservice API or similar request, or None if it came via the appservice API or similar
is_guest (bool): True if the user making this request is a guest user is_guest (bool): True if the user making this request is a guest user
shadow_banned (bool): True if the user making this request has been shadow-banned.
device_id (str|None): device_id which was set at authentication time device_id (str|None): device_id which was set at authentication time
app_service (ApplicationService|None): the AS requesting on behalf of the user app_service (ApplicationService|None): the AS requesting on behalf of the user
""" """
@ -77,6 +86,7 @@ class Requester(
"user_id": self.user.to_string(), "user_id": self.user.to_string(),
"access_token_id": self.access_token_id, "access_token_id": self.access_token_id,
"is_guest": self.is_guest, "is_guest": self.is_guest,
"shadow_banned": self.shadow_banned,
"device_id": self.device_id, "device_id": self.device_id,
"app_server_id": self.app_service.id if self.app_service else None, "app_server_id": self.app_service.id if self.app_service else None,
} }
@ -101,13 +111,19 @@ class Requester(
user=UserID.from_string(input["user_id"]), user=UserID.from_string(input["user_id"]),
access_token_id=input["access_token_id"], access_token_id=input["access_token_id"],
is_guest=input["is_guest"], is_guest=input["is_guest"],
shadow_banned=input["shadow_banned"],
device_id=input["device_id"], device_id=input["device_id"],
app_service=appservice, app_service=appservice,
) )
def create_requester( def create_requester(
user_id, access_token_id=None, is_guest=False, device_id=None, app_service=None user_id,
access_token_id=None,
is_guest=False,
shadow_banned=False,
device_id=None,
app_service=None,
): ):
""" """
Create a new ``Requester`` object Create a new ``Requester`` object
@ -117,6 +133,7 @@ def create_requester(
access_token_id (int|None): *ID* of the access token used for this access_token_id (int|None): *ID* of the access token used for this
request, or None if it came via the appservice API or similar request, or None if it came via the appservice API or similar
is_guest (bool): True if the user making this request is a guest user is_guest (bool): True if the user making this request is a guest user
shadow_banned (bool): True if the user making this request is shadow-banned.
device_id (str|None): device_id which was set at authentication time device_id (str|None): device_id which was set at authentication time
app_service (ApplicationService|None): the AS requesting on behalf of the user app_service (ApplicationService|None): the AS requesting on behalf of the user
@ -125,7 +142,9 @@ def create_requester(
""" """
if not isinstance(user_id, UserID): if not isinstance(user_id, UserID):
user_id = UserID.from_string(user_id) user_id = UserID.from_string(user_id)
return Requester(user_id, access_token_id, is_guest, device_id, app_service) return Requester(
user_id, access_token_id, is_guest, shadow_banned, device_id, app_service
)
def get_domain_from_id(string): def get_domain_from_id(string):

View File

@ -38,7 +38,7 @@ class CleanupExtremBackgroundUpdateStoreTestCase(HomeserverTestCase):
# Create a test user and room # Create a test user and room
self.user = UserID("alice", "test") self.user = UserID("alice", "test")
self.requester = Requester(self.user, None, False, None, None) self.requester = Requester(self.user, None, False, False, None, None)
info, _ = self.get_success(self.room_creator.create_room(self.requester, {})) info, _ = self.get_success(self.room_creator.create_room(self.requester, {}))
self.room_id = info["room_id"] self.room_id = info["room_id"]
@ -260,7 +260,7 @@ class CleanupExtremDummyEventsTestCase(HomeserverTestCase):
# Create a test user and room # Create a test user and room
self.user = UserID.from_string(self.register_user("user1", "password")) self.user = UserID.from_string(self.register_user("user1", "password"))
self.token1 = self.login("user1", "password") self.token1 = self.login("user1", "password")
self.requester = Requester(self.user, None, False, None, None) self.requester = Requester(self.user, None, False, False, None, None)
info, _ = self.get_success(self.room_creator.create_room(self.requester, {})) info, _ = self.get_success(self.room_creator.create_room(self.requester, {}))
self.room_id = info["room_id"] self.room_id = info["room_id"]
self.event_creator = homeserver.get_event_creation_handler() self.event_creator = homeserver.get_event_creation_handler()

View File

@ -27,7 +27,7 @@ class ExtremStatisticsTestCase(HomeserverTestCase):
room_creator = self.hs.get_room_creation_handler() room_creator = self.hs.get_room_creation_handler()
user = UserID("alice", "test") user = UserID("alice", "test")
requester = Requester(user, None, False, None, None) requester = Requester(user, None, False, False, None, None)
# Real events, forward extremities # Real events, forward extremities
events = [(3, 2), (6, 2), (4, 6)] events = [(3, 2), (6, 2), (4, 6)]

View File

@ -187,7 +187,7 @@ class CurrentStateMembershipUpdateTestCase(unittest.HomeserverTestCase):
# Now let's create a room, which will insert a membership # Now let's create a room, which will insert a membership
user = UserID("alice", "test") user = UserID("alice", "test")
requester = Requester(user, None, False, None, None) requester = Requester(user, None, False, False, None, None)
self.get_success(self.room_creator.create_room(requester, {})) self.get_success(self.room_creator.create_room(requester, {}))
# Register the background update to run again. # Register the background update to run again.

View File

@ -42,7 +42,7 @@ class MessageAcceptTests(unittest.HomeserverTestCase):
) )
user_id = UserID("us", "test") user_id = UserID("us", "test")
our_user = Requester(user_id, None, False, None, None) our_user = Requester(user_id, None, False, False, None, None)
room_creator = self.homeserver.get_room_creation_handler() room_creator = self.homeserver.get_room_creation_handler()
room_deferred = ensureDeferred( room_deferred = ensureDeferred(
room_creator.create_room( room_creator.create_room(

View File

@ -250,7 +250,11 @@ class HomeserverTestCase(TestCase):
async def get_user_by_req(request, allow_guest=False, rights="access"): async def get_user_by_req(request, allow_guest=False, rights="access"):
return create_requester( return create_requester(
UserID.from_string(self.helper.auth_user_id), 1, False, None UserID.from_string(self.helper.auth_user_id),
1,
False,
False,
None,
) )
self.hs.get_auth().get_user_by_req = get_user_by_req self.hs.get_auth().get_user_by_req = get_user_by_req
@ -540,7 +544,7 @@ class HomeserverTestCase(TestCase):
""" """
event_creator = self.hs.get_event_creation_handler() event_creator = self.hs.get_event_creation_handler()
secrets = self.hs.get_secrets() secrets = self.hs.get_secrets()
requester = Requester(user, None, False, None, None) requester = Requester(user, None, False, False, None, None)
event, context = self.get_success( event, context = self.get_success(
event_creator.create_event( event_creator.create_event(