mirror of
https://mau.dev/maunium/synapse.git
synced 2024-11-15 22:42:23 +01:00
Reject attempts to send event before privacy consent is given
Returns an M_CONSENT_NOT_GIVEN error (cf https://github.com/matrix-org/matrix-doc/issues/1252) if consent is not yet given.
This commit is contained in:
parent
8aeb529262
commit
a5e2941aad
6 changed files with 179 additions and 2 deletions
|
@ -19,6 +19,7 @@ import logging
|
||||||
|
|
||||||
import simplejson as json
|
import simplejson as json
|
||||||
from six import iteritems
|
from six import iteritems
|
||||||
|
from six.moves import http_client
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -51,6 +52,7 @@ class Codes(object):
|
||||||
THREEPID_DENIED = "M_THREEPID_DENIED"
|
THREEPID_DENIED = "M_THREEPID_DENIED"
|
||||||
INVALID_USERNAME = "M_INVALID_USERNAME"
|
INVALID_USERNAME = "M_INVALID_USERNAME"
|
||||||
SERVER_NOT_TRUSTED = "M_SERVER_NOT_TRUSTED"
|
SERVER_NOT_TRUSTED = "M_SERVER_NOT_TRUSTED"
|
||||||
|
CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN"
|
||||||
|
|
||||||
|
|
||||||
class CodeMessageException(RuntimeError):
|
class CodeMessageException(RuntimeError):
|
||||||
|
@ -138,6 +140,32 @@ class SynapseError(CodeMessageException):
|
||||||
return res
|
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):
|
class RegistrationError(SynapseError):
|
||||||
"""An error raised when a registration event fails."""
|
"""An error raised when a registration event fails."""
|
||||||
pass
|
pass
|
||||||
|
@ -292,7 +320,7 @@ def cs_error(msg, code=Codes.UNKNOWN, **kwargs):
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
msg (str): The error message.
|
msg (str): The error message.
|
||||||
code (int): The error code.
|
code (str): The error code.
|
||||||
kwargs : Additional keys to add to the response.
|
kwargs : Additional keys to add to the response.
|
||||||
Returns:
|
Returns:
|
||||||
A dict representing the error response JSON.
|
A dict representing the error response JSON.
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014-2016 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
|
# Copyright 2018 New Vector Ltd.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
@ -14,6 +15,12 @@
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
"""Contains the URL paths to prefix various aspects of the server with. """
|
"""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_PREFIX = "/_matrix/client/api/v1"
|
||||||
CLIENT_V2_ALPHA_PREFIX = "/_matrix/client/v2_alpha"
|
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"
|
SERVER_KEY_V2_PREFIX = "/_matrix/key/v2"
|
||||||
MEDIA_PREFIX = "/_matrix/media/r0"
|
MEDIA_PREFIX = "/_matrix/media/r0"
|
||||||
LEGACY_MEDIA_PREFIX = "/_matrix/media/v1"
|
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
|
# asking them to consent to the privacy policy. The 'server_notices' section
|
||||||
# must also be configured for this to work.
|
# 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:
|
# user_consent:
|
||||||
# template_dir: res/templates/privacy
|
# template_dir: res/templates/privacy
|
||||||
# version: 1.0
|
# version: 1.0
|
||||||
|
@ -41,6 +45,8 @@ DEFAULT_CONFIG = """\
|
||||||
# msgtype: m.text
|
# msgtype: m.text
|
||||||
# body: |
|
# body: |
|
||||||
# Pls do consent kthx
|
# 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_version = None
|
||||||
self.user_consent_template_dir = None
|
self.user_consent_template_dir = None
|
||||||
self.user_consent_server_notice_content = None
|
self.user_consent_server_notice_content = None
|
||||||
|
self.block_events_without_consent_error = None
|
||||||
|
|
||||||
def read_config(self, config):
|
def read_config(self, config):
|
||||||
consent_config = config.get("user_consent")
|
consent_config = config.get("user_consent")
|
||||||
|
@ -61,6 +68,9 @@ class ConsentConfig(Config):
|
||||||
self.user_consent_server_notice_content = consent_config.get(
|
self.user_consent_server_notice_content = consent_config.get(
|
||||||
"server_notice_content",
|
"server_notice_content",
|
||||||
)
|
)
|
||||||
|
self.block_events_without_consent_error = consent_config.get(
|
||||||
|
"block_events_error",
|
||||||
|
)
|
||||||
|
|
||||||
def default_config(self, **kwargs):
|
def default_config(self, **kwargs):
|
||||||
return DEFAULT_CONFIG
|
return DEFAULT_CONFIG
|
||||||
|
|
|
@ -20,10 +20,15 @@ import sys
|
||||||
from canonicaljson import encode_canonical_json
|
from canonicaljson import encode_canonical_json
|
||||||
import six
|
import six
|
||||||
from twisted.internet import defer, reactor
|
from twisted.internet import defer, reactor
|
||||||
|
from twisted.internet.defer import succeed
|
||||||
from twisted.python.failure import Failure
|
from twisted.python.failure import Failure
|
||||||
|
|
||||||
from synapse.api.constants import EventTypes, Membership, MAX_DEPTH
|
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.crypto.event_signing import add_hashes_and_signatures
|
||||||
from synapse.events.utils import serialize_event
|
from synapse.events.utils import serialize_event
|
||||||
from synapse.events.validator import EventValidator
|
from synapse.events.validator import EventValidator
|
||||||
|
@ -431,6 +436,9 @@ class EventCreationHandler(object):
|
||||||
|
|
||||||
self.spam_checker = hs.get_spam_checker()
|
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
|
@defer.inlineCallbacks
|
||||||
def create_event(self, requester, event_dict, token_id=None, txn_id=None,
|
def create_event(self, requester, event_dict, token_id=None, txn_id=None,
|
||||||
prev_events_and_hashes=None):
|
prev_events_and_hashes=None):
|
||||||
|
@ -482,6 +490,10 @@ class EventCreationHandler(object):
|
||||||
target, e
|
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:
|
if token_id is not None:
|
||||||
builder.internal_metadata.token_id = token_id
|
builder.internal_metadata.token_id = token_id
|
||||||
|
|
||||||
|
@ -496,6 +508,78 @@ class EventCreationHandler(object):
|
||||||
|
|
||||||
defer.returnValue((event, context))
|
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
|
@defer.inlineCallbacks
|
||||||
def send_nonmember_event(self, requester, event, context, ratelimit=True):
|
def send_nonmember_event(self, requester, event, context, ratelimit=True):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -126,6 +126,10 @@ class RoomCreationHandler(BaseHandler):
|
||||||
except Exception:
|
except Exception:
|
||||||
raise SynapseError(400, "Invalid user_id: %s" % (i,))
|
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", [])
|
invite_3pid_list = config.get("invite_3pid", [])
|
||||||
|
|
||||||
visibility = config.get("visibility", None)
|
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.filter_timeline_limit = 5000
|
||||||
config.user_directory_search_all_users = False
|
config.user_directory_search_all_users = False
|
||||||
config.user_consent_server_notice_content = None
|
config.user_consent_server_notice_content = None
|
||||||
|
config.block_events_without_consent_error = None
|
||||||
|
|
||||||
# disable user directory updates, because they get done in the
|
# disable user directory updates, because they get done in the
|
||||||
# background, which upsets the test runner.
|
# background, which upsets the test runner.
|
||||||
|
|
Loading…
Reference in a new issue