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

Commit

Permalink
Send users a server notice about consent
Browse files Browse the repository at this point in the history
When a user first syncs, we will send them a server notice asking them to
consent to the privacy policy if they have not already done so.
  • Loading branch information
richvdh committed May 22, 2018
1 parent d14d7b8 commit 9ea219c
Show file tree
Hide file tree
Showing 11 changed files with 255 additions and 11 deletions.
8 changes: 8 additions & 0 deletions synapse/config/consent_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,17 @@
# the version to be served by the consent resource if there is no 'v'
# parameter.
#
# 'server_notice_content', if enabled, will send a user a "Server Notice"
# asking them to consent to the privacy policy. The 'server_notices' section
# must also be configured for this to work.
#
# user_consent:
# template_dir: res/templates/privacy
# version: 1.0
# server_notice_content:
# msgtype: m.text
# body: |
# Pls do consent kthx
"""


Expand Down
10 changes: 9 additions & 1 deletion synapse/handlers/presence.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,20 @@
class PresenceHandler(object):

def __init__(self, hs):
"""
Args:
hs (synapse.server.HomeServer):
"""
self.is_mine = hs.is_mine
self.is_mine_id = hs.is_mine_id
self.clock = hs.get_clock()
self.store = hs.get_datastore()
self.wheel_timer = WheelTimer()
self.notifier = hs.get_notifier()
self.federation = hs.get_federation_sender()

self.state = hs.get_state_handler()
self._server_notices_sender = hs.get_server_notices_sender()

federation_registry = hs.get_federation_registry()

Expand Down Expand Up @@ -428,6 +433,9 @@ def user_syncing(self, user_id, affect_presence=True):
last_user_sync_ts=self.clock.time_msec(),
)])

# send any outstanding server notices to the user.
yield self._server_notices_sender.on_user_syncing(user_id)

@defer.inlineCallbacks
def _end():
try:
Expand Down
2 changes: 2 additions & 0 deletions synapse/replication/tcp/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def __init__(self, hs):
self.presence_handler = hs.get_presence_handler()
self.clock = hs.get_clock()
self.notifier = hs.get_notifier()
self._server_notices_sender = hs.get_server_notices_sender()

# Current connections.
self.connections = []
Expand Down Expand Up @@ -253,6 +254,7 @@ def on_user_ip(self, user_id, access_token, ip, user_agent, device_id, last_seen
yield self.store.insert_client_ip(
user_id, access_token, ip, user_agent, device_id, last_seen,
)
yield self._server_notices_sender.on_user_ip(user_id)

def send_sync_to_all_connections(self, data):
"""Sends a SYNC command to all clients.
Expand Down
5 changes: 5 additions & 0 deletions synapse/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
MediaRepositoryResource,
)
from synapse.server_notices.server_notices_manager import ServerNoticesManager
from synapse.server_notices.server_notices_sender import ServerNoticesSender
from synapse.state import StateHandler, StateResolutionHandler
from synapse.storage import DataStore
from synapse.streams.events import EventSources
Expand Down Expand Up @@ -158,6 +159,7 @@ def build_DEPENDENCY(self)
'room_member_handler',
'federation_registry',
'server_notices_manager',
'server_notices_sender',
]

def __init__(self, hostname, **kwargs):
Expand Down Expand Up @@ -403,6 +405,9 @@ def build_federation_registry(self):
def build_server_notices_manager(self):
return ServerNoticesManager(self)

def build_server_notices_sender(self):
return ServerNoticesSender(self)

def remove_pusher(self, app_id, push_key, user_id):
return self.get_pusherpool().remove_pusher(app_id, push_key, user_id)

Expand Down
4 changes: 4 additions & 0 deletions synapse/server.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import synapse.handlers.e2e_keys
import synapse.handlers.set_password
import synapse.rest.media.v1.media_repository
import synapse.server_notices.server_notices_manager
import synapse.server_notices.server_notices_sender
import synapse.state
import synapse.storage

Expand Down Expand Up @@ -69,3 +70,6 @@ class HomeServer(object):

def get_server_notices_manager(self) -> synapse.server_notices.server_notices_manager.ServerNoticesManager:
pass

def get_server_notices_sender(self) -> synapse.server_notices.server_notices_sender.ServerNoticesSender:
pass
101 changes: 101 additions & 0 deletions synapse/server_notices/consent_server_notices.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# -*- coding: utf-8 -*-
# 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.
# 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.
import logging

from twisted.internet import defer

from synapse.api.errors import SynapseError
from synapse.config import ConfigError

logger = logging.getLogger(__name__)


class ConsentServerNotices(object):
"""Keeps track of whether we need to send users server_notices about
privacy policy consent, and sends one if we do.
"""
def __init__(self, hs):
"""
Args:
hs (synapse.server.HomeServer):
"""
self._server_notices_manager = hs.get_server_notices_manager()
self._store = hs.get_datastore()

self._current_consent_version = None
self._server_notice_content = None
self._users_in_progress = set()

consent_config = hs.config.consent_config
if consent_config is not None:
self._current_consent_version = str(consent_config["version"])
self._server_notice_content = consent_config.get(
"server_notice_content"
)

if self._server_notice_content is not None:
if not self._server_notices_manager.is_enabled():
raise ConfigError(
"user_consent configuration requires server notices, but "
"server notices are not enabled.",
)
if 'body' not in self._server_notice_content:
raise ConfigError(
"user_consent server_notice_consent must contain a 'body' "
"key.",
)

@defer.inlineCallbacks
def maybe_send_server_notice_to_user(self, user_id):
"""Check if we need to send a notice to this user, and does so if so
Args:
user_id (str): user to check
Returns:
Deferred
"""
if self._server_notice_content is None:
# not enabled
return

# make sure we don't send two messages to the same user at once
if user_id in self._users_in_progress:
return
self._users_in_progress.add(user_id)
try:
u = yield self._store.get_user_by_id(user_id)

if u["consent_version"] == self._current_consent_version:
# user has already consented
return

if u["consent_server_notice_sent"] == self._current_consent_version:
# we've already sent a notice to the user
return

# need to send a message
try:
yield self._server_notices_manager.send_notice(
user_id, self._server_notice_content,
)
yield self._store.user_set_consent_server_notice_sent(
user_id, self._current_consent_version,
)
except SynapseError as e:
logger.error("Error sending server notice about user consent: %s", e)
finally:
self._users_in_progress.remove(user_id)
58 changes: 58 additions & 0 deletions synapse/server_notices/server_notices_sender.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# 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.
# 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.
from synapse.server_notices.consent_server_notices import ConsentServerNotices


class ServerNoticesSender(object):
"""A centralised place which sends server notices automatically when
Certain Events take place
"""
def __init__(self, hs):
"""
Args:
hs (synapse.server.HomeServer):
"""
# todo: it would be nice to make this more dynamic
self._consent_server_notices = ConsentServerNotices(hs)

def on_user_syncing(self, user_id):
"""Called when the user performs a sync operation.
This is only called when /sync (or /events) is called on the synapse
master. In a deployment with synchrotrons, on_user_ip is called
Args:
user_id (str): mxid of user who synced
Returns:
Deferred
"""
return self._consent_server_notices.maybe_send_server_notice_to_user(
user_id,
)

def on_user_ip(self, user_id):
"""Called when a worker process saw a client request.
Args:
user_id (str): mxid
Returns:
Deferred
"""
return self._consent_server_notices.maybe_send_server_notice_to_user(
user_id,
)
46 changes: 39 additions & 7 deletions synapse/storage/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ def get_user_by_id(self, user_id):
keyvalues={
"name": user_id,
},
retcols=["name", "password_hash", "is_guest"],
retcols=[
"name", "password_hash", "is_guest",
"consent_version", "consent_server_notice_sent",
],
allow_none=True,
desc="get_user_by_id",
)
Expand Down Expand Up @@ -297,12 +300,41 @@ def user_set_consent_version(self, user_id, consent_version):
Raises:
StoreError(404) if user not found
"""
return self._simple_update_one(
table='users',
keyvalues={'name': user_id, },
updatevalues={'consent_version': consent_version, },
desc="user_set_consent_version"
)
def f(txn):
self._simple_update_one_txn(
txn,
table='users',
keyvalues={'name': user_id, },
updatevalues={'consent_version': consent_version, },
)
self._invalidate_cache_and_stream(
txn, self.get_user_by_id, (user_id,)
)
return self.runInteraction("user_set_consent_version", f)

def user_set_consent_server_notice_sent(self, user_id, consent_version):
"""Updates the user table to record that we have sent the user a server
notice about privacy policy consent
Args:
user_id (str): full mxid of the user to update
consent_version (str): version of the policy we have notified the
user about
Raises:
StoreError(404) if user not found
"""
def f(txn):
self._simple_update_one_txn(
txn,
table='users',
keyvalues={'name': user_id, },
updatevalues={'consent_server_notice_sent': consent_version, },
)
self._invalidate_cache_and_stream(
txn, self.get_user_by_id, (user_id,)
)
return self.runInteraction("user_set_consent_server_notice_sent", f)

def user_delete_access_tokens(self, user_id, except_token_id=None,
device_id=None):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* 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.
* 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.
*/

/* record whether we have sent a server notice about consenting to the
* privacy policy. Specifically records the version of the policy we sent
* a message about.
*/
ALTER TABLE users ADD COLUMN consent_server_notice_sent TEXT;
11 changes: 8 additions & 3 deletions tests/storage/test_registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,14 @@ def test_register(self):
yield self.store.register(self.user_id, self.tokens[0], self.pwhash)

self.assertEquals(
# TODO(paul): Surely this field should be 'user_id', not 'name'
# Additionally surely it shouldn't come in a 1-element list
{"name": self.user_id, "password_hash": self.pwhash, "is_guest": 0},
{
# TODO(paul): Surely this field should be 'user_id', not 'name'
"name": self.user_id,
"password_hash": self.pwhash,
"is_guest": 0,
"consent_version": None,
"consent_server_notice_sent": None,
},
(yield self.store.get_user_by_id(self.user_id))
)

Expand Down
1 change: 1 addition & 0 deletions tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ def setup_test_homeserver(name="test", datastore=None, config=None, **kargs):
config.federation_rc_concurrent = 10
config.filter_timeline_limit = 5000
config.user_directory_search_all_users = False
config.consent_config = None

# disable user directory updates, because they get done in the
# background, which upsets the test runner.
Expand Down

0 comments on commit 9ea219c

Please sign in to comment.