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

Add m.login.terms to the registration flow #4004

Merged
merged 26 commits into from Nov 1, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
fd99787
Incorporate Dave's work for GDPR login flows
turt2live Sep 27, 2018
149c4f1
Supply params for terms auth stage
turt2live Oct 3, 2018
3099d96
Flesh out the fallback auth for terms
turt2live Oct 3, 2018
dfcad5f
Make the terms flow requried
turt2live Oct 3, 2018
f9d34a7
Auto-consent to the privacy policy if the user registered with terms
turt2live Oct 3, 2018
537d0b7
Use a flag rather than a new route for the public policy
turt2live Oct 3, 2018
158d6c7
Changelog
turt2live Oct 3, 2018
7ede650
Merge branch 'develop' into travis/login-terms
turt2live Oct 12, 2018
22a2004
Update documentation and templates for new consent
turt2live Oct 12, 2018
5119818
Rely on the lack of ?u to represent public access
turt2live Oct 12, 2018
dd99db8
Update login terms structure for the proposed language support
turt2live Oct 13, 2018
f293d12
Merge branch 'develop' into travis/login-terms
turt2live Oct 15, 2018
762a098
Python is hard
turt2live Oct 15, 2018
442734f
Ensure the terms params are actually provided
turt2live Oct 15, 2018
a8ed93a
pep8
turt2live Oct 15, 2018
49a044a
Merge branch 'develop' into travis/login-terms
turt2live Oct 18, 2018
88c5ffe
Test for terms UI auth
turt2live Oct 18, 2018
dba84fa
Fix terms UI auth test
turt2live Oct 18, 2018
54def42
Merge branch 'develop' into travis/login-terms
turt2live Oct 24, 2018
9283987
Fix test
turt2live Oct 18, 2018
4acb6fe
Move test to where the other integration tests are
turt2live Oct 24, 2018
81880be
It helps to import things
turt2live Oct 24, 2018
a5468ea
pep8
turt2live Oct 24, 2018
d1e7b9c
Merge branch 'develop' into travis/login-terms
turt2live Oct 31, 2018
a8d41c6
Include a version query string arg for the consent route
turt2live Oct 31, 2018
a8c9faa
The tests also need a version parameter
turt2live Oct 31, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/4004.feature
@@ -0,0 +1 @@
Add `m.login.terms` to the registration flow when consent tracking is enabled. **This makes the template arguments conditionally optional on a new `public_version` variable - update your privacy templates to support this.**
turt2live marked this conversation as resolved.
Show resolved Hide resolved
13 changes: 9 additions & 4 deletions docs/consent_tracking.md
Expand Up @@ -31,7 +31,7 @@ Note that the templates must be stored under a name giving the language of the
template - currently this must always be `en` (for "English");
internationalisation support is intended for the future.

The template for the policy itself should be versioned and named according to
The template for the policy itself should be versioned and named according to
the version: for example `1.0.html`. The version of the policy which the user
has agreed to is stored in the database.

Expand Down Expand Up @@ -81,9 +81,9 @@ should be a matter of `pip install Jinja2`. On debian, try `apt-get install
python-jinja2`.

Once this is complete, and the server has been restarted, try visiting
`https://<server>/_matrix/consent`. If correctly configured, this should give
an error "Missing string query parameter 'u'". It is now possible to manually
construct URIs where users can give their consent.
`https://<server>/_matrix/consent`. If correctly configured, you should see a
default policy document. It is now possible to manually construct URIs where
users can give their consent.

### Constructing the consent URI

Expand All @@ -106,6 +106,11 @@ query parameters:
`https://<server>/_matrix/consent?u=<user>&h=68a152465a4d...`.


Note that not providing a `u` parameter will be interpreted as wanting to view
the document from an unauthenticated perspective, such as prior to registration.
Therefore, the `h` parameter is not required in this scenario.


Sending users a server notice asking them to agree to the policy
----------------------------------------------------------------

Expand Down
15 changes: 9 additions & 6 deletions docs/privacy_policy_templates/en/1.0.html
Expand Up @@ -12,12 +12,15 @@
<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>
{% if not public_version %}
<!-- The variables used here are only provided when the 'u' param is given to the homeserver -->
<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>
{% endif %}
{% endif %}
</body>
</html>
1 change: 1 addition & 0 deletions synapse/api/constants.py
Expand Up @@ -51,6 +51,7 @@ class LoginType(object):
EMAIL_IDENTITY = u"m.login.email.identity"
MSISDN = u"m.login.msisdn"
RECAPTCHA = u"m.login.recaptcha"
TERMS = u"m.login.terms"
DUMMY = u"m.login.dummy"

# Only for C/S API v1
Expand Down
21 changes: 21 additions & 0 deletions synapse/handlers/auth.py
Expand Up @@ -59,6 +59,7 @@ def __init__(self, hs):
LoginType.EMAIL_IDENTITY: self._check_email_identity,
LoginType.MSISDN: self._check_msisdn,
LoginType.DUMMY: self._check_dummy_auth,
LoginType.TERMS: self._check_terms_auth,
}
self.bcrypt_rounds = hs.config.bcrypt_rounds

Expand Down Expand Up @@ -431,6 +432,9 @@ def _check_msisdn(self, authdict, _):
def _check_dummy_auth(self, authdict, _):
return defer.succeed(True)

def _check_terms_auth(self, authdict, _):
return defer.succeed(True)

@defer.inlineCallbacks
def _check_threepid(self, medium, authdict):
if 'threepid_creds' not in authdict:
Expand Down Expand Up @@ -462,13 +466,30 @@ def _check_threepid(self, medium, authdict):
def _get_params_recaptcha(self):
return {"public_key": self.hs.config.recaptcha_public_key}

def _get_params_terms(self):
return {
"policies": {
"privacy_policy": {
"version": self.hs.config.user_consent_version,
"en": {
"name": "Privacy Policy",
"url": "%s/_matrix/consent?v=%s" % (
self.hs.config.public_baseurl,
self.hs.config.user_consent_version,
),
},
},
},
}

def _auth_dict_for_flows(self, flows, session):
public_flows = []
for f in flows:
public_flows.append(f)

get_params = {
LoginType.RECAPTCHA: self._get_params_recaptcha,
LoginType.TERMS: self._get_params_terms,
}

params = {}
Expand Down
81 changes: 80 additions & 1 deletion synapse/rest/client/v2_alpha/auth.py
Expand Up @@ -68,6 +68,29 @@
</html>
"""

TERMS_TEMPLATE = """
<html>
<head>
<title>Authentication</title>
<meta name='viewport' content='width=device-width, initial-scale=1,
user-scalable=no, minimum-scale=1.0, maximum-scale=1.0'>
<link rel="stylesheet" href="/_matrix/static/client/register/style.css">
</head>
<body>
<form id="registrationForm" method="post" action="%(myurl)s">
<div>
<p>
Please click the button below if you agree to the
<a href="%(terms_url)s">privacy policy of this homeserver.</a>
</p>
Copy link
Contributor

@rubo77 rubo77 Nov 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have been "Please click the button below if you have read the privacy policy of this homeserver."

Because if you have a button to "Agree" you also need a workflow to "disagree" later on, which we don't want to provide.

see #4185

Copy link

@ilu33 ilu33 Nov 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. The average home server doesn't have any consent to track because there should be nothing you have to consent to. Art. 7 1 b "Processing shall be lawful only if ... processing is necessary for the performance of a contract to which the data subject is party". Everything that is technically necessary to provide the service needs just information, not consent.

Consent needs to be freely given and can be retracted at any time - without terminating the service (Art. 7 GDPR and Recital 43). We are not Facebook or Google, we do not force the user to consent to anything. We can't help the technical necessities though and about them we INFORM.

"Consent" and "agree" implies that you store data that is not technically necessary to supply the service, which - I hope - no homeserver does. (Except: Consent to statistics/piwik is tracked in the settings which is correct. Consent to bots - which I personally think needs consent - should be in the settings too but that's not the topic here.)

<input type="hidden" name="session" value="%(session)s" />
<input type="submit" value="Agree" />
</div>
</form>
</body>
</html>
"""

SUCCESS_TEMPLATE = """
<html>
<head>
Expand Down Expand Up @@ -130,6 +153,27 @@ def on_GET(self, request, stagetype):
request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),))

request.write(html_bytes)
finish_request(request)
defer.returnValue(None)
elif stagetype == LoginType.TERMS:
session = request.args['session'][0]

html = TERMS_TEMPLATE % {
'session': session,
'terms_url': "%s/_matrix/consent?v=%s" % (
self.hs.config.public_baseurl,
self.hs.config.user_consent_version,
),
'myurl': "%s/auth/%s/fallback/web" % (
CLIENT_V2_ALPHA_PREFIX, LoginType.TERMS
),
}
html_bytes = html.encode("utf8")
request.setResponseCode(200)
request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),))

request.write(html_bytes)
finish_request(request)
defer.returnValue(None)
Expand All @@ -139,7 +183,7 @@ def on_GET(self, request, stagetype):
@defer.inlineCallbacks
def on_POST(self, request, stagetype):
yield
if stagetype == "m.login.recaptcha":
if stagetype == LoginType.RECAPTCHA:
if ('g-recaptcha-response' not in request.args or
len(request.args['g-recaptcha-response'])) == 0:
raise SynapseError(400, "No captcha response supplied")
Expand Down Expand Up @@ -178,6 +222,41 @@ def on_POST(self, request, stagetype):
request.write(html_bytes)
finish_request(request)

defer.returnValue(None)
elif stagetype == LoginType.TERMS:
if ('session' not in request.args or
len(request.args['session'])) == 0:
raise SynapseError(400, "No session supplied")

session = request.args['session'][0]
authdict = {'session': session}

success = yield self.auth_handler.add_oob_auth(
LoginType.TERMS,
authdict,
self.hs.get_ip_from_request(request)
)

if success:
html = SUCCESS_TEMPLATE
else:
html = TERMS_TEMPLATE % {
'session': session,
'terms_url': "%s/_matrix/consent?v=%s" % (
self.hs.config.public_baseurl,
self.hs.config.user_consent_version,
),
'myurl': "%s/auth/%s/fallback/web" % (
CLIENT_V2_ALPHA_PREFIX, LoginType.TERMS
),
}
html_bytes = html.encode("utf8")
request.setResponseCode(200)
request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),))

request.write(html_bytes)
finish_request(request)
defer.returnValue(None)
else:
raise SynapseError(404, "Unknown auth stage type")
Expand Down
13 changes: 13 additions & 0 deletions synapse/rest/client/v2_alpha/register.py
Expand Up @@ -359,6 +359,13 @@ def on_POST(self, request):
[LoginType.MSISDN, LoginType.EMAIL_IDENTITY]
])

# Append m.login.terms to all flows if we're requiring consent
if self.hs.config.block_events_without_consent_error is not None:
new_flows = []
for flow in flows:
flow.append(LoginType.TERMS)
flows.extend(new_flows)

auth_result, params, session_id = yield self.auth_handler.check_auth(
flows, body, self.hs.get_ip_from_request(request)
)
Expand Down Expand Up @@ -445,6 +452,12 @@ def on_POST(self, request):
params.get("bind_msisdn")
)

if auth_result and LoginType.TERMS in auth_result:
logger.info("%s has consented to the privacy policy" % registered_user_id)
yield self.store.user_set_consent_version(
registered_user_id, self.hs.config.user_consent_version,
)

defer.returnValue((200, return_dict))

def on_OPTIONS(self, _):
Expand Down
36 changes: 20 additions & 16 deletions synapse/rest/consent/consent_resource.py
Expand Up @@ -137,27 +137,31 @@ def _async_render_GET(self, request):
request (twisted.web.http.Request):
"""

version = parse_string(request, "v",
default=self._default_consent_version)
username = parse_string(request, "u", required=True)
userhmac = parse_string(request, "h", required=True, encoding=None)

self._check_hash(username, userhmac)

if username.startswith('@'):
qualified_user_id = username
else:
qualified_user_id = UserID(username, self.hs.hostname).to_string()

u = yield self.store.get_user_by_id(qualified_user_id)
if u is None:
raise NotFoundError("Unknown user")
version = parse_string(request, "v", default=self._default_consent_version)
username = parse_string(request, "u", required=False, default="")
userhmac = None
has_consented = False
public_version = username != ""
if not public_version:
userhmac = parse_string(request, "h", required=True, encoding=None)

self._check_hash(username, userhmac)

if username.startswith('@'):
qualified_user_id = username
else:
qualified_user_id = UserID(username, self.hs.hostname).to_string()

u = yield self.store.get_user_by_id(qualified_user_id)
if u is None:
raise NotFoundError("Unknown user")
has_consented = u["consent_version"] == version

try:
self._render_template(
request, "%s.html" % (version,),
user=username, userhmac=userhmac, version=version,
has_consented=(u["consent_version"] == version),
has_consented=has_consented, public_version=public_version,
)
except TemplateNotFound:
raise NotFoundError("Unknown policy version")
Expand Down