rewrite based on PR feedback:

* [ ] split config options into allowed_local_3pids and registrations_require_3pid
 * [ ] simplify and comment logic for picking registration flows
 * [ ] fix docstring and move check_3pid_allowed into a new util module
 * [ ] use check_3pid_allowed everywhere

@erikjohnston PTAL
This commit is contained in:
Matthew Hodgson 2018-01-19 15:33:55 +00:00
parent 9d332e0f79
commit 447f4f0d5f
7 changed files with 101 additions and 88 deletions

View File

@ -32,6 +32,7 @@ class RegistrationConfig(Config):
) )
self.registrations_require_3pid = config.get("registrations_require_3pid", []) self.registrations_require_3pid = config.get("registrations_require_3pid", [])
self.allowed_local_3pids = config.get("allowed_local_3pids", [])
self.registration_shared_secret = config.get("registration_shared_secret") self.registration_shared_secret = config.get("registration_shared_secret")
self.bcrypt_rounds = config.get("bcrypt_rounds", 12) self.bcrypt_rounds = config.get("bcrypt_rounds", 12)
@ -53,11 +54,16 @@ class RegistrationConfig(Config):
# Enable registration for new users. # Enable registration for new users.
enable_registration: False enable_registration: False
# Mandate that registrations require a 3PID which matches one or more # The user must provide all of the below types of 3PID when registering.
# of these 3PIDs. N.B. regexp escape backslashes are doubled (once for
# YAML and once for the regexp itself)
# #
# registrations_require_3pid: # registrations_require_3pid:
# - email
# - msisdn
# Mandate that users are only allowed to associate certain formats of
# 3PIDs with accounts on this server.
#
# allowed_local_3pids:
# - medium: email # - medium: email
# pattern: ".*@matrix\\.org" # pattern: ".*@matrix\\.org"
# - medium: email # - medium: email

View File

@ -15,7 +15,6 @@
"""Contains functions for registering clients.""" """Contains functions for registering clients."""
import logging import logging
import re
from twisted.internet import defer from twisted.internet import defer
@ -26,6 +25,7 @@ from synapse.http.client import CaptchaServerHttpClient
from synapse import types from synapse import types
from synapse.types import UserID from synapse.types import UserID
from synapse.util.async import run_on_reactor from synapse.util.async import run_on_reactor
from synapse.util.threepids import check_3pid_allowed
from ._base import BaseHandler from ._base import BaseHandler
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -308,12 +308,7 @@ class RegistrationHandler(BaseHandler):
logger.info("got threepid with medium '%s' and address '%s'", logger.info("got threepid with medium '%s' and address '%s'",
threepid['medium'], threepid['address']) threepid['medium'], threepid['address'])
for constraint in self.hs.config.registrations_require_3pid: if not check_3pid_allowed(self.hs, threepid['medium'], threepid['address']):
if (
constraint['medium'] == 'email' and
threepid['medium'] == 'email' and
re.match(constraint['pattern'], threepid['address'])
):
raise RegistrationError( raise RegistrationError(
403, "Third party identifier is not allowed" 403, "Third party identifier is not allowed"
) )

View File

@ -71,22 +71,13 @@ class RegisterRestServlet(ClientV1RestServlet):
def on_GET(self, request): def on_GET(self, request):
require_email = False require_email = 'email' in self.hs.config.registrations_require_3pid
require_msisdn = False require_msisdn = 'msisdn' in self.hs.config.registrations_require_3pid
for constraint in self.hs.config.registrations_require_3pid:
if constraint['medium'] == 'email':
require_email = True
elif constraint['medium'] == 'msisdn':
require_msisdn = True
else:
logger.warn(
"Unrecognised 3PID medium %s in registrations_require_3pid" %
constraint['medium']
)
flows = [] flows = []
if self.hs.config.enable_registration_captcha: if self.hs.config.enable_registration_captcha:
if require_email or not require_msisdn: # only support the email-only flow if we don't require MSISDN 3PIDs
if not require_msisdn:
flows.extend([ flows.extend([
{ {
"type": LoginType.RECAPTCHA, "type": LoginType.RECAPTCHA,
@ -97,6 +88,7 @@ class RegisterRestServlet(ClientV1RestServlet):
] ]
}, },
]) ])
# only support 3PIDless registration if no 3PIDs are required
if not require_email and not require_msisdn: if not require_email and not require_msisdn:
flows.extend([ flows.extend([
{ {
@ -105,6 +97,7 @@ class RegisterRestServlet(ClientV1RestServlet):
} }
]) ])
else: else:
# only support the email-only flow if we don't require MSISDN 3PIDs
if require_email or not require_msisdn: if require_email or not require_msisdn:
flows.extend([ flows.extend([
{ {
@ -114,6 +107,7 @@ class RegisterRestServlet(ClientV1RestServlet):
] ]
} }
]) ])
# only support 3PIDless registration if no 3PIDs are required
if not require_email and not require_msisdn: if not require_email and not require_msisdn:
flows.extend([ flows.extend([
{ {

View File

@ -60,27 +60,6 @@ def set_timeline_upper_limit(filter_json, filter_timeline_limit):
filter_timeline_limit) filter_timeline_limit)
def check_3pid_allowed(hs, medium, address):
# check whether the HS has whitelisted the given 3PID
allow = False
if hs.config.registrations_require_3pid:
for constraint in hs.config.registrations_require_3pid:
logger.debug("Checking 3PID %s (%s) against %s (%s)" % (
address, medium, constraint['pattern'], constraint['medium']
))
if (
medium == constraint['medium'] and
re.match(constraint['pattern'], address)
):
allow = True
break
else:
allow = True
return allow
def interactive_auth_handler(orig): def interactive_auth_handler(orig):
"""Wraps an on_POST method to handle InteractiveAuthIncompleteErrors """Wraps an on_POST method to handle InteractiveAuthIncompleteErrors

View File

@ -26,7 +26,8 @@ from synapse.http.servlet import (
) )
from synapse.util.async import run_on_reactor from synapse.util.async import run_on_reactor
from synapse.util.msisdn import phone_number_to_msisdn from synapse.util.msisdn import phone_number_to_msisdn
from ._base import client_v2_patterns, interactive_auth_handler, check_3pid_allowed from synapse.util.threepids import check_3pid_allowed
from ._base import client_v2_patterns, interactive_auth_handler
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -26,11 +26,11 @@ from synapse.http.servlet import (
RestServlet, parse_json_object_from_request, assert_params_in_request, parse_string RestServlet, parse_json_object_from_request, assert_params_in_request, parse_string
) )
from synapse.util.msisdn import phone_number_to_msisdn from synapse.util.msisdn import phone_number_to_msisdn
from synapse.util.threepids import check_3pid_allowed
from ._base import client_v2_patterns, interactive_auth_handler, check_3pid_allowed from ._base import client_v2_patterns, interactive_auth_handler
import logging import logging
import re
import hmac import hmac
from hashlib import sha1 from hashlib import sha1
from synapse.util.async import run_on_reactor from synapse.util.async import run_on_reactor
@ -316,41 +316,41 @@ class RegisterRestServlet(RestServlet):
if 'x_show_msisdn' in body and body['x_show_msisdn']: if 'x_show_msisdn' in body and body['x_show_msisdn']:
show_msisdn = True show_msisdn = True
require_email = False # FIXME: need a better error than "no auth flow found" for scenarios
require_msisdn = False # where we required 3PID for registration but the user didn't give one
for constraint in self.hs.config.registrations_require_3pid: require_email = 'email' in self.hs.config.registrations_require_3pid
if constraint['medium'] == 'email': require_msisdn = 'msisdn' in self.hs.config.registrations_require_3pid
require_email = True
elif constraint['medium'] == 'msisdn':
require_msisdn = True
else:
logger.warn(
"Unrecognised 3PID medium %s in registrations_require_3pid" %
constraint['medium']
)
flows = [] flows = []
if self.hs.config.enable_registration_captcha: if self.hs.config.enable_registration_captcha:
# only support 3PIDless registration if no 3PIDs are required
if not require_email and not require_msisdn: if not require_email and not require_msisdn:
flows.extend([[LoginType.RECAPTCHA]]) flows.extend([[LoginType.RECAPTCHA]])
if require_email or not require_msisdn: # only support the email-only flow if we don't require MSISDN 3PIDs
if not require_msisdn:
flows.extend([[LoginType.EMAIL_IDENTITY, LoginType.RECAPTCHA]]) flows.extend([[LoginType.EMAIL_IDENTITY, LoginType.RECAPTCHA]])
if show_msisdn: if show_msisdn:
if not require_email or require_msisdn: # only support the MSISDN-only flow if we don't require email 3PIDs
if not require_email:
flows.extend([[LoginType.MSISDN, LoginType.RECAPTCHA]]) flows.extend([[LoginType.MSISDN, LoginType.RECAPTCHA]])
# always let users provide both MSISDN & email
flows.extend([ flows.extend([
[LoginType.MSISDN, LoginType.EMAIL_IDENTITY, LoginType.RECAPTCHA], [LoginType.MSISDN, LoginType.EMAIL_IDENTITY, LoginType.RECAPTCHA],
]) ])
else: else:
# only support 3PIDless registration if no 3PIDs are required
if not require_email and not require_msisdn: if not require_email and not require_msisdn:
flows.extend([[LoginType.DUMMY]]) flows.extend([[LoginType.DUMMY]])
if require_email or not require_msisdn: # only support the email-only flow if we don't require MSISDN 3PIDs
if not require_msisdn:
flows.extend([[LoginType.EMAIL_IDENTITY]]) flows.extend([[LoginType.EMAIL_IDENTITY]])
if show_msisdn: if show_msisdn:
# only support the MSISDN-only flow if we don't require email 3PIDs
if not require_email or require_msisdn: if not require_email or require_msisdn:
flows.extend([[LoginType.MSISDN]]) flows.extend([[LoginType.MSISDN]])
# always let users provide both MSISDN & email
flows.extend([ flows.extend([
[LoginType.MSISDN, LoginType.EMAIL_IDENTITY] [LoginType.MSISDN, LoginType.EMAIL_IDENTITY]
]) ])
@ -359,30 +359,23 @@ class RegisterRestServlet(RestServlet):
flows, body, self.hs.get_ip_from_request(request) flows, body, self.hs.get_ip_from_request(request)
) )
# doublecheck that we're not trying to register an denied 3pid. # Check that we're not trying to register a denied 3pid.
# the user-facing checks should already have happened when we requested #
# a 3PID token to validate them in /register/email/requestToken etc # the user-facing checks will probably already have happened in
# /register/email/requestToken when we requested a 3pid, but that's not
# guaranteed.
for constraint in self.hs.config.registrations_require_3pid:
if ( if (
constraint['medium'] == 'email' and auth_result and
auth_result and LoginType.EMAIL_IDENTITY in auth_result and (
re.match( LoginType.EMAIL_IDENTITY in auth_result or
constraint['pattern'], LoginType.EMAIL_MSISDN in auth_result
auth_result[LoginType.EMAIL_IDENTITY].threepid.address
)
):
raise SynapseError(
403, "Third party identifier is not allowed", Codes.THREEPID_DENIED
)
elif (
constraint['medium'] == 'msisdn' and
auth_result and LoginType.MSISDN in auth_result and
re.match(
constraint['pattern'],
auth_result[LoginType.MSISDN].threepid.address
) )
): ):
medium = auth_result[LoginType.EMAIL_IDENTITY].threepid['medium']
address = auth_result[LoginType.EMAIL_IDENTITY].threepid['address']
if not check_3pid_allowed(self.hs, medium, address):
raise SynapseError( raise SynapseError(
403, "Third party identifier is not allowed", Codes.THREEPID_DENIED 403, "Third party identifier is not allowed", Codes.THREEPID_DENIED
) )

45
synapse/util/threepids.py Normal file
View File

@ -0,0 +1,45 @@
# -*- 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
import re
logger = logging.getLogger(__name__)
def check_3pid_allowed(hs, medium, address):
"""Checks whether a given format of 3PID is allowed to be used on this HS
Args:
hs (synapse.server.HomeServer): server
medium (str): 3pid medium - e.g. email, msisdn
address (str): address within that medium (e.g. "wotan@matrix.org")
msisdns need to first have been canonicalised
"""
if hs.config.allowed_local_3pids:
for constraint in hs.config.allowed_local_3pids:
logger.debug("Checking 3PID %s (%s) against %s (%s)" % (
address, medium, constraint['pattern'], constraint['medium']
))
if (
medium == constraint['medium'] and
re.match(constraint['pattern'], address)
):
return True
else:
return True
return False