mirror of https://github.com/watcha-fr/synapse
Merge pull request #3213 from matrix-org/rav/consent_handler
ConsentResource to gather policy consent from userspull/14/head
commit
8030a825c8
@ -0,0 +1,23 @@ |
|||||||
|
If enabling the 'consent' resource in synapse, you will need some templates |
||||||
|
for the HTML to be served to the user. This directory contains very simple |
||||||
|
examples of the sort of thing that can be done. |
||||||
|
|
||||||
|
You'll need to add this sort of thing to your homeserver.yaml: |
||||||
|
|
||||||
|
``` |
||||||
|
form_secret: <unique but arbitrary secret> |
||||||
|
|
||||||
|
user_consent: |
||||||
|
template_dir: docs/privacy_policy_templates |
||||||
|
default_version: 1.0 |
||||||
|
``` |
||||||
|
|
||||||
|
You should then be able to enable the `consent` resource under a `listener` |
||||||
|
entry. For example: |
||||||
|
|
||||||
|
``` |
||||||
|
listeners: |
||||||
|
- port: 8008 |
||||||
|
resources: |
||||||
|
- names: [client, consent] |
||||||
|
``` |
@ -0,0 +1,17 @@ |
|||||||
|
<!doctype html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<title>Matrix.org Privacy policy</title> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<p> |
||||||
|
All your base are belong to us. |
||||||
|
</p> |
||||||
|
<form method="post" action="consent"> |
||||||
|
<input type="hidden" name="v" value="{{version}}"/> |
||||||
|
<input type="hidden" name="u" value="{{user}}"/> |
||||||
|
<input type="hidden" name="h" value="{{userhmac}}"/> |
||||||
|
<input type="submit" value="Sure thing!"/> |
||||||
|
</form> |
||||||
|
</body> |
||||||
|
</html> |
@ -0,0 +1,11 @@ |
|||||||
|
<!doctype html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<title>Matrix.org Privacy policy</title> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<p> |
||||||
|
Sweet. |
||||||
|
</p> |
||||||
|
</body> |
||||||
|
</html> |
@ -0,0 +1,42 @@ |
|||||||
|
# -*- 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 ._base import Config |
||||||
|
|
||||||
|
DEFAULT_CONFIG = """\ |
||||||
|
# User Consent configuration |
||||||
|
# |
||||||
|
# uncomment and configure if enabling the 'consent' resource under 'listeners'. |
||||||
|
# |
||||||
|
# 'template_dir' gives the location of the templates for the HTML forms. |
||||||
|
# This directory should contain one subdirectory per language (eg, 'en', 'fr'), |
||||||
|
# and each language directory should contain the policy document (named as |
||||||
|
# '<version>.html') and a success page (success.html). |
||||||
|
# |
||||||
|
# 'default_version' gives the version of the policy document to serve up if |
||||||
|
# there is no 'v' parameter. |
||||||
|
# |
||||||
|
# user_consent: |
||||||
|
# template_dir: res/templates/privacy |
||||||
|
# default_version: 1.0 |
||||||
|
""" |
||||||
|
|
||||||
|
|
||||||
|
class ConsentConfig(Config): |
||||||
|
def read_config(self, config): |
||||||
|
self.consent_config = config.get("user_consent") |
||||||
|
|
||||||
|
def default_config(self, **kwargs): |
||||||
|
return DEFAULT_CONFIG |
@ -0,0 +1,210 @@ |
|||||||
|
# -*- 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 hashlib import sha256 |
||||||
|
import hmac |
||||||
|
import logging |
||||||
|
from os import path |
||||||
|
from six.moves import http_client |
||||||
|
|
||||||
|
import jinja2 |
||||||
|
from jinja2 import TemplateNotFound |
||||||
|
from twisted.internet import defer |
||||||
|
from twisted.web.resource import Resource |
||||||
|
from twisted.web.server import NOT_DONE_YET |
||||||
|
|
||||||
|
from synapse.api.errors import NotFoundError, SynapseError, StoreError |
||||||
|
from synapse.config import ConfigError |
||||||
|
from synapse.http.server import ( |
||||||
|
finish_request, |
||||||
|
wrap_html_request_handler, |
||||||
|
) |
||||||
|
from synapse.http.servlet import parse_string |
||||||
|
from synapse.types import UserID |
||||||
|
|
||||||
|
|
||||||
|
# language to use for the templates. TODO: figure this out from Accept-Language |
||||||
|
TEMPLATE_LANGUAGE = "en" |
||||||
|
|
||||||
|
logger = logging.getLogger(__name__) |
||||||
|
|
||||||
|
# use hmac.compare_digest if we have it (python 2.7.7), else just use equality |
||||||
|
if hasattr(hmac, "compare_digest"): |
||||||
|
compare_digest = hmac.compare_digest |
||||||
|
else: |
||||||
|
def compare_digest(a, b): |
||||||
|
return a == b |
||||||
|
|
||||||
|
|
||||||
|
class ConsentResource(Resource): |
||||||
|
"""A twisted Resource to display a privacy policy and gather consent to it |
||||||
|
|
||||||
|
When accessed via GET, returns the privacy policy via a template. |
||||||
|
|
||||||
|
When accessed via POST, records the user's consent in the database and |
||||||
|
displays a success page. |
||||||
|
|
||||||
|
The config should include a template_dir setting which contains templates |
||||||
|
for the HTML. The directory should contain one subdirectory per language |
||||||
|
(eg, 'en', 'fr'), and each language directory should contain the policy |
||||||
|
document (named as '<version>.html') and a success page (success.html). |
||||||
|
|
||||||
|
Both forms take a set of parameters from the browser. For the POST form, |
||||||
|
these are normally sent as form parameters (but may be query-params); for |
||||||
|
GET requests they must be query params. These are: |
||||||
|
|
||||||
|
u: the complete mxid, or the localpart of the user giving their |
||||||
|
consent. Required for both GET (where it is used as an input to the |
||||||
|
template) and for POST (where it is used to find the row in the db |
||||||
|
to update). |
||||||
|
|
||||||
|
h: hmac_sha256(secret, u), where 'secret' is the privacy_secret in the |
||||||
|
config file. If it doesn't match, the request is 403ed. |
||||||
|
|
||||||
|
v: the version of the privacy policy being agreed to. |
||||||
|
|
||||||
|
For GET: optional, and defaults to whatever was set in the config |
||||||
|
file. Used to choose the version of the policy to pick from the |
||||||
|
templates directory. |
||||||
|
|
||||||
|
For POST: required; gives the value to be recorded in the database |
||||||
|
against the user. |
||||||
|
""" |
||||||
|
def __init__(self, hs): |
||||||
|
""" |
||||||
|
Args: |
||||||
|
hs (synapse.server.HomeServer): homeserver |
||||||
|
""" |
||||||
|
Resource.__init__(self) |
||||||
|
|
||||||
|
self.hs = hs |
||||||
|
self.store = hs.get_datastore() |
||||||
|
|
||||||
|
# this is required by the request_handler wrapper |
||||||
|
self.clock = hs.get_clock() |
||||||
|
|
||||||
|
consent_config = hs.config.consent_config |
||||||
|
if consent_config is None: |
||||||
|
raise ConfigError( |
||||||
|
"Consent resource is enabled but user_consent section is " |
||||||
|
"missing in config file.", |
||||||
|
) |
||||||
|
|
||||||
|
# daemonize changes the cwd to /, so make the path absolute now. |
||||||
|
consent_template_directory = path.abspath( |
||||||
|
consent_config["template_dir"], |
||||||
|
) |
||||||
|
if not path.isdir(consent_template_directory): |
||||||
|
raise ConfigError( |
||||||
|
"Could not find template directory '%s'" % ( |
||||||
|
consent_template_directory, |
||||||
|
), |
||||||
|
) |
||||||
|
|
||||||
|
loader = jinja2.FileSystemLoader(consent_template_directory) |
||||||
|
self._jinja_env = jinja2.Environment(loader=loader) |
||||||
|
|
||||||
|
self._default_consent_verison = consent_config["default_version"] |
||||||
|
|
||||||
|
if hs.config.form_secret is None: |
||||||
|
raise ConfigError( |
||||||
|
"Consent resource is enabled but form_secret is not set in " |
||||||
|
"config file. It should be set to an arbitrary secret string.", |
||||||
|
) |
||||||
|
|
||||||
|
self._hmac_secret = hs.config.form_secret.encode("utf-8") |
||||||
|
|
||||||
|
def render_GET(self, request): |
||||||
|
self._async_render_GET(request) |
||||||
|
return NOT_DONE_YET |
||||||
|
|
||||||
|
@wrap_html_request_handler |
||||||
|
def _async_render_GET(self, request): |
||||||
|
""" |
||||||
|
Args: |
||||||
|
request (twisted.web.http.Request): |
||||||
|
""" |
||||||
|
|
||||||
|
version = parse_string(request, "v", |
||||||
|
default=self._default_consent_verison) |
||||||
|
username = parse_string(request, "u", required=True) |
||||||
|
userhmac = parse_string(request, "h", required=True) |
||||||
|
|
||||||
|
self._check_hash(username, userhmac) |
||||||
|
|
||||||
|
try: |
||||||
|
self._render_template( |
||||||
|
request, "%s.html" % (version,), |
||||||
|
user=username, userhmac=userhmac, version=version, |
||||||
|
) |
||||||
|
except TemplateNotFound: |
||||||
|
raise NotFoundError("Unknown policy version") |
||||||
|
|
||||||
|
def render_POST(self, request): |
||||||
|
self._async_render_POST(request) |
||||||
|
return NOT_DONE_YET |
||||||
|
|
||||||
|
@wrap_html_request_handler |
||||||
|
@defer.inlineCallbacks |
||||||
|
def _async_render_POST(self, request): |
||||||
|
""" |
||||||
|
Args: |
||||||
|
request (twisted.web.http.Request): |
||||||
|
""" |
||||||
|
version = parse_string(request, "v", required=True) |
||||||
|
username = parse_string(request, "u", required=True) |
||||||
|
userhmac = parse_string(request, "h", required=True) |
||||||
|
|
||||||
|
self._check_hash(username, userhmac) |
||||||
|
|
||||||
|
if username.startswith('@'): |
||||||
|
qualified_user_id = username |
||||||
|
else: |
||||||
|
qualified_user_id = UserID(username, self.hs.hostname).to_string() |
||||||
|
|
||||||
|
try: |
||||||
|
yield self.store.user_set_consent_version(qualified_user_id, version) |
||||||
|
except StoreError as e: |
||||||
|
if e.code != 404: |
||||||
|
raise |
||||||
|
raise NotFoundError("Unknown user") |
||||||
|
|
||||||
|
try: |
||||||
|
self._render_template(request, "success.html") |
||||||
|
except TemplateNotFound: |
||||||
|
raise NotFoundError("success.html not found") |
||||||
|
|
||||||
|
def _render_template(self, request, template_name, **template_args): |
||||||
|
# get_template checks for ".." so we don't need to worry too much |
||||||
|
# about path traversal here. |
||||||
|
template_html = self._jinja_env.get_template( |
||||||
|
path.join(TEMPLATE_LANGUAGE, template_name) |
||||||
|
) |
||||||
|
html_bytes = template_html.render(**template_args).encode("utf8") |
||||||
|
|
||||||
|
request.setHeader(b"Content-Type", b"text/html; charset=utf-8") |
||||||
|
request.setHeader(b"Content-Length", b"%i" % len(html_bytes)) |
||||||
|
request.write(html_bytes) |
||||||
|
finish_request(request) |
||||||
|
|
||||||
|
def _check_hash(self, userid, userhmac): |
||||||
|
want_mac = hmac.new( |
||||||
|
key=self._hmac_secret, |
||||||
|
msg=userid, |
||||||
|
digestmod=sha256, |
||||||
|
).hexdigest() |
||||||
|
|
||||||
|
if not compare_digest(want_mac, userhmac): |
||||||
|
raise SynapseError(http_client.FORBIDDEN, "HMAC incorrect") |
@ -0,0 +1,18 @@ |
|||||||
|
/* 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 the version of the privacy policy the user has consented to |
||||||
|
*/ |
||||||
|
ALTER TABLE users ADD COLUMN consent_version TEXT; |
Loading…
Reference in new issue