ConsentResource to gather policy consent from users
Hopefully there are enough comments and docs in this that it makes sense on its own.pull/3213/head
parent
bd8d0cfab1
commit
47815edcfa
|
@ -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>
|
|
@ -41,6 +41,7 @@ from synapse.python_dependencies import CONDITIONAL_REQUIREMENTS, \
|
||||||
from synapse.replication.http import ReplicationRestResource, REPLICATION_PREFIX
|
from synapse.replication.http import ReplicationRestResource, REPLICATION_PREFIX
|
||||||
from synapse.replication.tcp.resource import ReplicationStreamProtocolFactory
|
from synapse.replication.tcp.resource import ReplicationStreamProtocolFactory
|
||||||
from synapse.rest import ClientRestResource
|
from synapse.rest import ClientRestResource
|
||||||
|
from synapse.rest.consent.consent_resource import ConsentResource
|
||||||
from synapse.rest.key.v1.server_key_resource import LocalKey
|
from synapse.rest.key.v1.server_key_resource import LocalKey
|
||||||
from synapse.rest.key.v2 import KeyApiV2Resource
|
from synapse.rest.key.v2 import KeyApiV2Resource
|
||||||
from synapse.rest.media.v0.content_repository import ContentRepoResource
|
from synapse.rest.media.v0.content_repository import ContentRepoResource
|
||||||
|
@ -182,6 +183,14 @@ class SynapseHomeServer(HomeServer):
|
||||||
"/_matrix/client/versions": client_resource,
|
"/_matrix/client/versions": client_resource,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if name == "consent":
|
||||||
|
consent_resource = ConsentResource(self)
|
||||||
|
if compress:
|
||||||
|
consent_resource = gz_wrap(consent_resource)
|
||||||
|
resources.update({
|
||||||
|
"/_matrix/consent": consent_resource,
|
||||||
|
})
|
||||||
|
|
||||||
if name == "federation":
|
if name == "federation":
|
||||||
resources.update({
|
resources.update({
|
||||||
FEDERATION_PREFIX: TransportLayerServer(self),
|
FEDERATION_PREFIX: TransportLayerServer(self),
|
||||||
|
|
|
@ -12,3 +12,9 @@
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
from ._base import ConfigError
|
||||||
|
|
||||||
|
# export ConfigError if somebody does import *
|
||||||
|
# this is largely a fudge to stop PEP8 moaning about the import
|
||||||
|
__all__ = ["ConfigError"]
|
||||||
|
|
|
@ -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
|
|
@ -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.
|
||||||
|
@ -12,7 +13,6 @@
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from .tls import TlsConfig
|
from .tls import TlsConfig
|
||||||
from .server import ServerConfig
|
from .server import ServerConfig
|
||||||
from .logger import LoggingConfig
|
from .logger import LoggingConfig
|
||||||
|
@ -37,6 +37,7 @@ from .push import PushConfig
|
||||||
from .spam_checker import SpamCheckerConfig
|
from .spam_checker import SpamCheckerConfig
|
||||||
from .groups import GroupsConfig
|
from .groups import GroupsConfig
|
||||||
from .user_directory import UserDirectoryConfig
|
from .user_directory import UserDirectoryConfig
|
||||||
|
from .consent_config import ConsentConfig
|
||||||
|
|
||||||
|
|
||||||
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
|
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
|
||||||
|
@ -45,12 +46,13 @@ class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
|
||||||
AppServiceConfig, KeyConfig, SAML2Config, CasConfig,
|
AppServiceConfig, KeyConfig, SAML2Config, CasConfig,
|
||||||
JWTConfig, PasswordConfig, EmailConfig,
|
JWTConfig, PasswordConfig, EmailConfig,
|
||||||
WorkerConfig, PasswordAuthProviderConfig, PushConfig,
|
WorkerConfig, PasswordAuthProviderConfig, PushConfig,
|
||||||
SpamCheckerConfig, GroupsConfig, UserDirectoryConfig,):
|
SpamCheckerConfig, GroupsConfig, UserDirectoryConfig,
|
||||||
|
ConsentConfig):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import sys
|
import sys
|
||||||
sys.stdout.write(
|
sys.stdout.write(
|
||||||
HomeServerConfig().generate_config(sys.argv[1], sys.argv[2])[0]
|
HomeServerConfig().generate_config(sys.argv[1], sys.argv[2], True)[0]
|
||||||
)
|
)
|
||||||
|
|
|
@ -59,14 +59,20 @@ class KeyConfig(Config):
|
||||||
|
|
||||||
self.expire_access_token = config.get("expire_access_token", False)
|
self.expire_access_token = config.get("expire_access_token", False)
|
||||||
|
|
||||||
|
# a secret which is used to calculate HMACs for form values, to stop
|
||||||
|
# falsification of values
|
||||||
|
self.form_secret = config.get("form_secret", None)
|
||||||
|
|
||||||
def default_config(self, config_dir_path, server_name, is_generating_file=False,
|
def default_config(self, config_dir_path, server_name, is_generating_file=False,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
base_key_name = os.path.join(config_dir_path, server_name)
|
base_key_name = os.path.join(config_dir_path, server_name)
|
||||||
|
|
||||||
if is_generating_file:
|
if is_generating_file:
|
||||||
macaroon_secret_key = random_string_with_symbols(50)
|
macaroon_secret_key = random_string_with_symbols(50)
|
||||||
|
form_secret = '"%s"' % random_string_with_symbols(50)
|
||||||
else:
|
else:
|
||||||
macaroon_secret_key = None
|
macaroon_secret_key = None
|
||||||
|
form_secret = 'null'
|
||||||
|
|
||||||
return """\
|
return """\
|
||||||
macaroon_secret_key: "%(macaroon_secret_key)s"
|
macaroon_secret_key: "%(macaroon_secret_key)s"
|
||||||
|
@ -74,6 +80,10 @@ class KeyConfig(Config):
|
||||||
# Used to enable access token expiration.
|
# Used to enable access token expiration.
|
||||||
expire_access_token: False
|
expire_access_token: False
|
||||||
|
|
||||||
|
# a secret which is used to calculate HMACs for form values, to stop
|
||||||
|
# falsification of values
|
||||||
|
form_secret: %(form_secret)s
|
||||||
|
|
||||||
## Signing Keys ##
|
## Signing Keys ##
|
||||||
|
|
||||||
# Path to the signing key to sign messages with
|
# Path to the signing key to sign messages with
|
||||||
|
|
|
@ -13,7 +13,8 @@
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
import cgi
|
||||||
|
from six.moves import http_client
|
||||||
|
|
||||||
from synapse.api.errors import (
|
from synapse.api.errors import (
|
||||||
cs_exception, SynapseError, CodeMessageException, UnrecognizedRequestError, Codes
|
cs_exception, SynapseError, CodeMessageException, UnrecognizedRequestError, Codes
|
||||||
|
@ -44,6 +45,18 @@ import simplejson
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
HTML_ERROR_TEMPLATE = """<!DOCTYPE html>
|
||||||
|
<html lang=en>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Error {code}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>{msg}</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def wrap_json_request_handler(h):
|
def wrap_json_request_handler(h):
|
||||||
"""Wraps a request handler method with exception handling.
|
"""Wraps a request handler method with exception handling.
|
||||||
|
@ -104,6 +117,65 @@ def wrap_json_request_handler(h):
|
||||||
return wrap_request_handler_with_logging(wrapped_request_handler)
|
return wrap_request_handler_with_logging(wrapped_request_handler)
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_html_request_handler(h):
|
||||||
|
"""Wraps a request handler method with exception handling.
|
||||||
|
|
||||||
|
Also adds logging as per wrap_request_handler_with_logging.
|
||||||
|
|
||||||
|
The handler method must have a signature of "handle_foo(self, request)",
|
||||||
|
where "self" must have a "clock" attribute (and "request" must be a
|
||||||
|
SynapseRequest).
|
||||||
|
"""
|
||||||
|
def wrapped_request_handler(self, request):
|
||||||
|
d = defer.maybeDeferred(h, self, request)
|
||||||
|
d.addErrback(_return_html_error, request)
|
||||||
|
return d
|
||||||
|
|
||||||
|
return wrap_request_handler_with_logging(wrapped_request_handler)
|
||||||
|
|
||||||
|
|
||||||
|
def _return_html_error(f, request):
|
||||||
|
"""Sends an HTML error page corresponding to the given failure
|
||||||
|
|
||||||
|
Args:
|
||||||
|
f (twisted.python.failure.Failure):
|
||||||
|
request (twisted.web.iweb.IRequest):
|
||||||
|
"""
|
||||||
|
if f.check(CodeMessageException):
|
||||||
|
cme = f.value
|
||||||
|
code = cme.code
|
||||||
|
msg = cme.msg
|
||||||
|
|
||||||
|
if isinstance(cme, SynapseError):
|
||||||
|
logger.info(
|
||||||
|
"%s SynapseError: %s - %s", request, code, msg
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
"Failed handle request %r: %s",
|
||||||
|
request,
|
||||||
|
f.getTraceback().rstrip(),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
code = http_client.INTERNAL_SERVER_ERROR
|
||||||
|
msg = "Internal server error"
|
||||||
|
|
||||||
|
logger.error(
|
||||||
|
"Failed handle request %r: %s",
|
||||||
|
request,
|
||||||
|
f.getTraceback().rstrip(),
|
||||||
|
)
|
||||||
|
|
||||||
|
body = HTML_ERROR_TEMPLATE.format(
|
||||||
|
code=code, msg=cgi.escape(msg),
|
||||||
|
).encode("utf-8")
|
||||||
|
request.setResponseCode(code)
|
||||||
|
request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
|
||||||
|
request.setHeader(b"Content-Length", b"%i" % (len(body),))
|
||||||
|
request.write(body)
|
||||||
|
finish_request(request)
|
||||||
|
|
||||||
|
|
||||||
def wrap_request_handler_with_logging(h):
|
def wrap_request_handler_with_logging(h):
|
||||||
"""Wraps a request handler to provide logging and metrics
|
"""Wraps a request handler to provide logging and metrics
|
||||||
|
|
||||||
|
@ -134,7 +206,7 @@ def wrap_request_handler_with_logging(h):
|
||||||
servlet_name = self.__class__.__name__
|
servlet_name = self.__class__.__name__
|
||||||
with request.processing(servlet_name):
|
with request.processing(servlet_name):
|
||||||
with PreserveLoggingContext(request_context):
|
with PreserveLoggingContext(request_context):
|
||||||
d = h(self, request)
|
d = defer.maybeDeferred(h, self, request)
|
||||||
|
|
||||||
# record the arrival of the request *after*
|
# record the arrival of the request *after*
|
||||||
# dispatching to the handler, so that the handler
|
# dispatching to the handler, so that the handler
|
||||||
|
|
|
@ -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")
|
|
@ -97,6 +97,9 @@ class HomeServer(object):
|
||||||
which must be implemented by the subclass. This code may call any of the
|
which must be implemented by the subclass. This code may call any of the
|
||||||
required "get" methods on the instance to obtain the sub-dependencies that
|
required "get" methods on the instance to obtain the sub-dependencies that
|
||||||
one requires.
|
one requires.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
config (synapse.config.homeserver.HomeserverConfig):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DEPENDENCIES = [
|
DEPENDENCIES = [
|
||||||
|
|
|
@ -286,6 +286,24 @@ class RegistrationStore(RegistrationWorkerStore,
|
||||||
"user_set_password_hash", user_set_password_hash_txn
|
"user_set_password_hash", user_set_password_hash_txn
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def user_set_consent_version(self, user_id, consent_version):
|
||||||
|
"""Updates the user table to record privacy policy consent
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id (str): full mxid of the user to update
|
||||||
|
consent_version (str): version of the policy the user has consented
|
||||||
|
to
|
||||||
|
|
||||||
|
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 user_delete_access_tokens(self, user_id, except_token_id=None,
|
def user_delete_access_tokens(self, user_id, except_token_id=None,
|
||||||
device_id=None):
|
device_id=None):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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