Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Commit

Permalink
Merge pull request #3257 from matrix-org/rav/fonx_on_no_consent
Browse files Browse the repository at this point in the history
Reject attempts to send event before privacy consent is given
  • Loading branch information
richvdh authored May 22, 2018
2 parents 8aeb529 + a5e2941 commit 3b2def6
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 2 deletions.
30 changes: 29 additions & 1 deletion synapse/api/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import simplejson as json
from six import iteritems
from six.moves import http_client

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -138,6 +140,32 @@ def from_http_response_exception(cls, err):
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
Expand Down Expand Up @@ -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.
Expand Down
50 changes: 50 additions & 0 deletions synapse/api/urls.py
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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"
Expand All @@ -25,3 +32,46 @@
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
10 changes: 10 additions & 0 deletions synapse/config/consent_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,19 @@
# 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
# server_notice_content:
# msgtype: m.text
# body: |
# Pls do consent kthx
# block_events_error: |
# You can't send any messages until you consent to the privacy policy.
"""


Expand All @@ -51,6 +57,7 @@ def __init__(self):
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")
Expand All @@ -61,6 +68,9 @@ def read_config(self, 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
86 changes: 85 additions & 1 deletion synapse/handlers/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,15 @@
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
Expand Down Expand Up @@ -431,6 +436,9 @@ def __init__(self, hs):

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):
Expand Down Expand Up @@ -482,6 +490,10 @@ def create_event(self, requester, event_dict, token_id=None, txn_id=None,
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

Expand All @@ -496,6 +508,78 @@ def create_event(self, requester, event_dict, token_id=None, txn_id=None,

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):
"""
Expand Down
4 changes: 4 additions & 0 deletions synapse/handlers/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ def create_room(self, requester, config, ratelimit=True,
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)
Expand Down
1 change: 1 addition & 0 deletions tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 3b2def6

Please sign in to comment.