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 #4004 from matrix-org/travis/login-terms
Browse files Browse the repository at this point in the history
Add m.login.terms to the registration flow
  • Loading branch information
turt2live authored Nov 1, 2018
2 parents 1b21e77 + a8c9faa commit c68aab1
Show file tree
Hide file tree
Showing 9 changed files with 277 additions and 27 deletions.
1 change: 1 addition & 0 deletions changelog.d/4004.feature
Original file line number Diff line number Diff line change
@@ -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.**
13 changes: 9 additions & 4 deletions docs/consent_tracking.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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>
<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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading

0 comments on commit c68aab1

Please sign in to comment.