Merge pull request #3257 from matrix-org/rav/fonx_on_no_consent
Reject attempts to send event before privacy consent is givenpull/3263/head
						commit
						3b2def6c7a
					
				|  | @ -19,6 +19,7 @@ import logging | |||
| 
 | ||||
| import simplejson as json | ||||
| from six import iteritems | ||||
| from six.moves import http_client | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
|  | @ -51,6 +52,7 @@ class Codes(object): | |||
|     THREEPID_DENIED = "M_THREEPID_DENIED" | ||||
|     INVALID_USERNAME = "M_INVALID_USERNAME" | ||||
|     SERVER_NOT_TRUSTED = "M_SERVER_NOT_TRUSTED" | ||||
|     CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN" | ||||
| 
 | ||||
| 
 | ||||
| class CodeMessageException(RuntimeError): | ||||
|  | @ -138,6 +140,32 @@ class SynapseError(CodeMessageException): | |||
|         return res | ||||
| 
 | ||||
| 
 | ||||
| class ConsentNotGivenError(SynapseError): | ||||
|     """The error returned to the client when the user has not consented to the | ||||
|     privacy policy. | ||||
|     """ | ||||
|     def __init__(self, msg, consent_uri): | ||||
|         """Constructs a ConsentNotGivenError | ||||
| 
 | ||||
|         Args: | ||||
|             msg (str): The human-readable error message | ||||
|             consent_url (str): The URL where the user can give their consent | ||||
|         """ | ||||
|         super(ConsentNotGivenError, self).__init__( | ||||
|             code=http_client.FORBIDDEN, | ||||
|             msg=msg, | ||||
|             errcode=Codes.CONSENT_NOT_GIVEN | ||||
|         ) | ||||
|         self._consent_uri = consent_uri | ||||
| 
 | ||||
|     def error_dict(self): | ||||
|         return cs_error( | ||||
|             self.msg, | ||||
|             self.errcode, | ||||
|             consent_uri=self._consent_uri | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| class RegistrationError(SynapseError): | ||||
|     """An error raised when a registration event fails.""" | ||||
|     pass | ||||
|  | @ -292,7 +320,7 @@ def cs_error(msg, code=Codes.UNKNOWN, **kwargs): | |||
| 
 | ||||
|     Args: | ||||
|         msg (str): The error message. | ||||
|         code (int): The error code. | ||||
|         code (str): The error code. | ||||
|         kwargs : Additional keys to add to the response. | ||||
|     Returns: | ||||
|         A dict representing the error response JSON. | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # Copyright 2014-2016 OpenMarket Ltd | ||||
| # Copyright 2018 New Vector Ltd. | ||||
| # | ||||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| # you may not use this file except in compliance with the License. | ||||
|  | @ -14,6 +15,12 @@ | |||
| # limitations under the License. | ||||
| 
 | ||||
| """Contains the URL paths to prefix various aspects of the server with. """ | ||||
| from hashlib import sha256 | ||||
| import hmac | ||||
| 
 | ||||
| from six.moves.urllib.parse import urlencode | ||||
| 
 | ||||
| from synapse.config import ConfigError | ||||
| 
 | ||||
| CLIENT_PREFIX = "/_matrix/client/api/v1" | ||||
| CLIENT_V2_ALPHA_PREFIX = "/_matrix/client/v2_alpha" | ||||
|  | @ -25,3 +32,46 @@ SERVER_KEY_PREFIX = "/_matrix/key/v1" | |||
| SERVER_KEY_V2_PREFIX = "/_matrix/key/v2" | ||||
| MEDIA_PREFIX = "/_matrix/media/r0" | ||||
| LEGACY_MEDIA_PREFIX = "/_matrix/media/v1" | ||||
| 
 | ||||
| 
 | ||||
| class ConsentURIBuilder(object): | ||||
|     def __init__(self, hs_config): | ||||
|         """ | ||||
|         Args: | ||||
|             hs_config (synapse.config.homeserver.HomeServerConfig): | ||||
|         """ | ||||
|         if hs_config.form_secret is None: | ||||
|             raise ConfigError( | ||||
|                 "form_secret not set in config", | ||||
|             ) | ||||
|         if hs_config.public_baseurl is None: | ||||
|             raise ConfigError( | ||||
|                 "public_baseurl not set in config", | ||||
|             ) | ||||
| 
 | ||||
|         self._hmac_secret = hs_config.form_secret.encode("utf-8") | ||||
|         self._public_baseurl = hs_config.public_baseurl | ||||
| 
 | ||||
|     def build_user_consent_uri(self, user_id): | ||||
|         """Build a URI which we can give to the user to do their privacy | ||||
|         policy consent | ||||
| 
 | ||||
|         Args: | ||||
|             user_id (str): mxid or username of user | ||||
| 
 | ||||
|         Returns | ||||
|             (str) the URI where the user can do consent | ||||
|         """ | ||||
|         mac = hmac.new( | ||||
|             key=self._hmac_secret, | ||||
|             msg=user_id, | ||||
|             digestmod=sha256, | ||||
|         ).hexdigest() | ||||
|         consent_uri = "%s_matrix/consent?%s" % ( | ||||
|             self._public_baseurl, | ||||
|             urlencode({ | ||||
|                 "u": user_id, | ||||
|                 "h": mac | ||||
|             }), | ||||
|         ) | ||||
|         return consent_uri | ||||
|  |  | |||
|  | @ -34,6 +34,10 @@ DEFAULT_CONFIG = """\ | |||
| # asking them to consent to the privacy policy. The 'server_notices' section | ||||
| # must also be configured for this to work. | ||||
| # | ||||
| # 'block_events_error', if set, will block any attempts to send events | ||||
| # until the user consents to the privacy policy. The value of the setting is | ||||
| # used as the text of the error. | ||||
| # | ||||
| # user_consent: | ||||
| #   template_dir: res/templates/privacy | ||||
| #   version: 1.0 | ||||
|  | @ -41,6 +45,8 @@ DEFAULT_CONFIG = """\ | |||
| #     msgtype: m.text | ||||
| #     body: | | ||||
| #       Pls do consent kthx | ||||
| #   block_events_error: | | ||||
| #     You can't send any messages until you consent to the privacy policy. | ||||
| """ | ||||
| 
 | ||||
| 
 | ||||
|  | @ -51,6 +57,7 @@ class ConsentConfig(Config): | |||
|         self.user_consent_version = None | ||||
|         self.user_consent_template_dir = None | ||||
|         self.user_consent_server_notice_content = None | ||||
|         self.block_events_without_consent_error = None | ||||
| 
 | ||||
|     def read_config(self, config): | ||||
|         consent_config = config.get("user_consent") | ||||
|  | @ -61,6 +68,9 @@ class ConsentConfig(Config): | |||
|         self.user_consent_server_notice_content = consent_config.get( | ||||
|             "server_notice_content", | ||||
|         ) | ||||
|         self.block_events_without_consent_error = consent_config.get( | ||||
|             "block_events_error", | ||||
|         ) | ||||
| 
 | ||||
|     def default_config(self, **kwargs): | ||||
|         return DEFAULT_CONFIG | ||||
|  |  | |||
|  | @ -20,10 +20,15 @@ import sys | |||
| from canonicaljson import encode_canonical_json | ||||
| import six | ||||
| from twisted.internet import defer, reactor | ||||
| from twisted.internet.defer import succeed | ||||
| from twisted.python.failure import Failure | ||||
| 
 | ||||
| from synapse.api.constants import EventTypes, Membership, MAX_DEPTH | ||||
| from synapse.api.errors import AuthError, Codes, SynapseError | ||||
| from synapse.api.errors import ( | ||||
|     AuthError, Codes, SynapseError, | ||||
|     ConsentNotGivenError, | ||||
| ) | ||||
| from synapse.api.urls import ConsentURIBuilder | ||||
| from synapse.crypto.event_signing import add_hashes_and_signatures | ||||
| from synapse.events.utils import serialize_event | ||||
| from synapse.events.validator import EventValidator | ||||
|  | @ -431,6 +436,9 @@ class EventCreationHandler(object): | |||
| 
 | ||||
|         self.spam_checker = hs.get_spam_checker() | ||||
| 
 | ||||
|         if self.config.block_events_without_consent_error is not None: | ||||
|             self._consent_uri_builder = ConsentURIBuilder(self.config) | ||||
| 
 | ||||
|     @defer.inlineCallbacks | ||||
|     def create_event(self, requester, event_dict, token_id=None, txn_id=None, | ||||
|                      prev_events_and_hashes=None): | ||||
|  | @ -482,6 +490,10 @@ class EventCreationHandler(object): | |||
|                         target, e | ||||
|                     ) | ||||
| 
 | ||||
|         is_exempt = yield self._is_exempt_from_privacy_policy(builder) | ||||
|         if not is_exempt: | ||||
|             yield self.assert_accepted_privacy_policy(requester) | ||||
| 
 | ||||
|         if token_id is not None: | ||||
|             builder.internal_metadata.token_id = token_id | ||||
| 
 | ||||
|  | @ -496,6 +508,78 @@ class EventCreationHandler(object): | |||
| 
 | ||||
|         defer.returnValue((event, context)) | ||||
| 
 | ||||
|     def _is_exempt_from_privacy_policy(self, builder): | ||||
|         """"Determine if an event to be sent is exempt from having to consent | ||||
|         to the privacy policy | ||||
| 
 | ||||
|         Args: | ||||
|             builder (synapse.events.builder.EventBuilder): event being created | ||||
| 
 | ||||
|         Returns: | ||||
|             Deferred[bool]: true if the event can be sent without the user | ||||
|                 consenting | ||||
|         """ | ||||
|         # the only thing the user can do is join the server notices room. | ||||
|         if builder.type == EventTypes.Member: | ||||
|             membership = builder.content.get("membership", None) | ||||
|             if membership == Membership.JOIN: | ||||
|                 return self._is_server_notices_room(builder.room_id) | ||||
|         return succeed(False) | ||||
| 
 | ||||
|     @defer.inlineCallbacks | ||||
|     def _is_server_notices_room(self, room_id): | ||||
|         if self.config.server_notices_mxid is None: | ||||
|             defer.returnValue(False) | ||||
|         user_ids = yield self.store.get_users_in_room(room_id) | ||||
|         defer.returnValue(self.config.server_notices_mxid in user_ids) | ||||
| 
 | ||||
|     @defer.inlineCallbacks | ||||
|     def assert_accepted_privacy_policy(self, requester): | ||||
|         """Check if a user has accepted the privacy policy | ||||
| 
 | ||||
|         Called when the given user is about to do something that requires | ||||
|         privacy consent. We see if the user is exempt and otherwise check that | ||||
|         they have given consent. If they have not, a ConsentNotGiven error is | ||||
|         raised. | ||||
| 
 | ||||
|         Args: | ||||
|             requester (synapse.types.Requester): | ||||
|                 The user making the request | ||||
| 
 | ||||
|         Returns: | ||||
|             Deferred[None]: returns normally if the user has consented or is | ||||
|                 exempt | ||||
| 
 | ||||
|         Raises: | ||||
|             ConsentNotGivenError: if the user has not given consent yet | ||||
|         """ | ||||
|         if self.config.block_events_without_consent_error is None: | ||||
|             return | ||||
| 
 | ||||
|         # exempt AS users from needing consent | ||||
|         if requester.app_service is not None: | ||||
|             return | ||||
| 
 | ||||
|         user_id = requester.user.to_string() | ||||
| 
 | ||||
|         # exempt the system notices user | ||||
|         if ( | ||||
|             self.config.server_notices_mxid is not None and | ||||
|             user_id == self.config.server_notices_mxid | ||||
|         ): | ||||
|             return | ||||
| 
 | ||||
|         u = yield self.store.get_user_by_id(user_id) | ||||
|         assert u is not None | ||||
|         if u["consent_version"] == self.config.user_consent_version: | ||||
|             return | ||||
| 
 | ||||
|         consent_uri = self._consent_uri_builder.build_user_consent_uri(user_id) | ||||
|         raise ConsentNotGivenError( | ||||
|             msg=self.config.block_events_without_consent_error, | ||||
|             consent_uri=consent_uri, | ||||
|         ) | ||||
| 
 | ||||
|     @defer.inlineCallbacks | ||||
|     def send_nonmember_event(self, requester, event, context, ratelimit=True): | ||||
|         """ | ||||
|  |  | |||
|  | @ -126,6 +126,10 @@ class RoomCreationHandler(BaseHandler): | |||
|             except Exception: | ||||
|                 raise SynapseError(400, "Invalid user_id: %s" % (i,)) | ||||
| 
 | ||||
|         yield self.event_creation_handler.assert_accepted_privacy_policy( | ||||
|             requester, | ||||
|         ) | ||||
| 
 | ||||
|         invite_3pid_list = config.get("invite_3pid", []) | ||||
| 
 | ||||
|         visibility = config.get("visibility", None) | ||||
|  |  | |||
|  | @ -64,6 +64,7 @@ def setup_test_homeserver(name="test", datastore=None, config=None, **kargs): | |||
|         config.filter_timeline_limit = 5000 | ||||
|         config.user_directory_search_all_users = False | ||||
|         config.user_consent_server_notice_content = None | ||||
|         config.block_events_without_consent_error = None | ||||
| 
 | ||||
|         # disable user directory updates, because they get done in the | ||||
|         # background, which upsets the test runner. | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Richard van der Hoff
						Richard van der Hoff