mirror of
https://github.com/element-hq/synapse.git
synced 2024-11-29 07:28:55 +03:00
Merge branch 'develop' of github.com:matrix-org/synapse into erikj/cleanup_user_ips_2
This commit is contained in:
commit
4fb3c129aa
42 changed files with 1318 additions and 496 deletions
1
changelog.d/5884.feature
Normal file
1
changelog.d/5884.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Enable cleaning up extremities with dummy events by default to prevent undue build up of forward extremities.
|
1
changelog.d/5972.misc
Normal file
1
changelog.d/5972.misc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Add m.require_identity_server flag to /version's unstable_features.
|
1
changelog.d/5974.feature
Normal file
1
changelog.d/5974.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Add m.id_access_token to unstable_features in /versions as per MSC2264.
|
1
changelog.d/6000.feature
Normal file
1
changelog.d/6000.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Apply the federation blacklist to requests to identity servers.
|
1
changelog.d/6037.feature
Normal file
1
changelog.d/6037.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Make the process for mapping SAML2 users to matrix IDs more flexible.
|
1
changelog.d/6043.feature
Normal file
1
changelog.d/6043.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Implement new Client Server API endpoints `/account/3pid/add` and `/account/3pid/bind` as per [MSC2290](https://github.com/matrix-org/matrix-doc/pull/2290).
|
1
changelog.d/6044.feature
Normal file
1
changelog.d/6044.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Add an unstable feature flag for separate add/bind 3pid APIs.
|
1
changelog.d/6064.misc
Normal file
1
changelog.d/6064.misc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Clean up the sample config for SAML authentication.
|
1
changelog.d/6069.bugfix
Normal file
1
changelog.d/6069.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Fix a bug which caused SAML attribute maps to be overridden by defaults.
|
1
changelog.d/6078.feature
Normal file
1
changelog.d/6078.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Add `POST /add_threepid/msisdn/submit_token` endpoint for proxying submitToken on an account_threepid_handler.
|
1
changelog.d/6079.feature
Normal file
1
changelog.d/6079.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Add `submit_url` response parameter to `*/msisdn/requestToken` endpoints.
|
1
changelog.d/6092.bugfix
Normal file
1
changelog.d/6092.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Fix the logged number of updated items for the users_set_deactivated_flag background update.
|
1
changelog.d/6097.bugfix
Normal file
1
changelog.d/6097.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Add sid to next_link for email validation.
|
1
changelog.d/6099.misc
Normal file
1
changelog.d/6099.misc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Remove unused parameter to get_user_id_by_threepid.
|
1
changelog.d/6104.bugfix
Normal file
1
changelog.d/6104.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Threepid validity checks on msisdns should not be dependent on 'threepid_behaviour_email'.
|
1
changelog.d/6105.misc
Normal file
1
changelog.d/6105.misc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Refactor the user-interactive auth handling.
|
1
changelog.d/6106.misc
Normal file
1
changelog.d/6106.misc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Refactor code for calculating registration flows.
|
1
changelog.d/6107.bugfix
Normal file
1
changelog.d/6107.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Ensure that servers which are not configured to support email address verification do not offer it in the registration flows.
|
|
@ -110,6 +110,9 @@ pid_file: DATADIR/homeserver.pid
|
||||||
# blacklist IP address CIDR ranges. If this option is not specified, or
|
# blacklist IP address CIDR ranges. If this option is not specified, or
|
||||||
# specified with an empty list, no ip range blacklist will be enforced.
|
# specified with an empty list, no ip range blacklist will be enforced.
|
||||||
#
|
#
|
||||||
|
# As of Synapse v1.4.0 this option also affects any outbound requests to identity
|
||||||
|
# servers provided by user input.
|
||||||
|
#
|
||||||
# (0.0.0.0 and :: are always blacklisted, whether or not they are explicitly
|
# (0.0.0.0 and :: are always blacklisted, whether or not they are explicitly
|
||||||
# listed here, since they correspond to unroutable addresses.)
|
# listed here, since they correspond to unroutable addresses.)
|
||||||
#
|
#
|
||||||
|
@ -943,6 +946,8 @@ uploads_path: "DATADIR/uploads"
|
||||||
# by the Matrix Identity Service API specification:
|
# by the Matrix Identity Service API specification:
|
||||||
# https://matrix.org/docs/spec/identity_service/latest
|
# https://matrix.org/docs/spec/identity_service/latest
|
||||||
#
|
#
|
||||||
|
# If a delegate is specified, the config option public_baseurl must also be filled out.
|
||||||
|
#
|
||||||
account_threepid_delegates:
|
account_threepid_delegates:
|
||||||
#email: https://example.com # Delegate email sending to example.org
|
#email: https://example.com # Delegate email sending to example.org
|
||||||
#msisdn: http://localhost:8090 # Delegate SMS sending to this local process
|
#msisdn: http://localhost:8090 # Delegate SMS sending to this local process
|
||||||
|
@ -1107,12 +1112,13 @@ signing_key_path: "CONFDIR/SERVERNAME.signing.key"
|
||||||
|
|
||||||
# Enable SAML2 for registration and login. Uses pysaml2.
|
# Enable SAML2 for registration and login. Uses pysaml2.
|
||||||
#
|
#
|
||||||
# `sp_config` is the configuration for the pysaml2 Service Provider.
|
# At least one of `sp_config` or `config_path` must be set in this section to
|
||||||
# See pysaml2 docs for format of config.
|
# enable SAML login.
|
||||||
#
|
#
|
||||||
# Default values will be used for the 'entityid' and 'service' settings,
|
# (You will probably also want to set the following options to `false` to
|
||||||
# so it is not normally necessary to specify them unless you need to
|
# disable the regular login/registration flows:
|
||||||
# override them.
|
# * enable_registration
|
||||||
|
# * password_config.enabled
|
||||||
#
|
#
|
||||||
# Once SAML support is enabled, a metadata file will be exposed at
|
# Once SAML support is enabled, a metadata file will be exposed at
|
||||||
# https://<server>:<port>/_matrix/saml2/metadata.xml, which you may be able to
|
# https://<server>:<port>/_matrix/saml2/metadata.xml, which you may be able to
|
||||||
|
@ -1120,52 +1126,85 @@ signing_key_path: "CONFDIR/SERVERNAME.signing.key"
|
||||||
# the IdP to use an ACS location of
|
# the IdP to use an ACS location of
|
||||||
# https://<server>:<port>/_matrix/saml2/authn_response.
|
# https://<server>:<port>/_matrix/saml2/authn_response.
|
||||||
#
|
#
|
||||||
#saml2_config:
|
saml2_config:
|
||||||
# sp_config:
|
# `sp_config` is the configuration for the pysaml2 Service Provider.
|
||||||
# # point this to the IdP's metadata. You can use either a local file or
|
# See pysaml2 docs for format of config.
|
||||||
# # (preferably) a URL.
|
#
|
||||||
# metadata:
|
# Default values will be used for the 'entityid' and 'service' settings,
|
||||||
# #local: ["saml2/idp.xml"]
|
# so it is not normally necessary to specify them unless you need to
|
||||||
# remote:
|
# override them.
|
||||||
# - url: https://our_idp/metadata.xml
|
#
|
||||||
#
|
#sp_config:
|
||||||
# # By default, the user has to go to our login page first. If you'd like to
|
# # point this to the IdP's metadata. You can use either a local file or
|
||||||
# # allow IdP-initiated login, set 'allow_unsolicited: True' in a
|
# # (preferably) a URL.
|
||||||
# # 'service.sp' section:
|
# metadata:
|
||||||
# #
|
# #local: ["saml2/idp.xml"]
|
||||||
# #service:
|
# remote:
|
||||||
# # sp:
|
# - url: https://our_idp/metadata.xml
|
||||||
# # allow_unsolicited: True
|
#
|
||||||
#
|
# # By default, the user has to go to our login page first. If you'd like
|
||||||
# # The examples below are just used to generate our metadata xml, and you
|
# # to allow IdP-initiated login, set 'allow_unsolicited: True' in a
|
||||||
# # may well not need it, depending on your setup. Alternatively you
|
# # 'service.sp' section:
|
||||||
# # may need a whole lot more detail - see the pysaml2 docs!
|
# #
|
||||||
#
|
# #service:
|
||||||
# description: ["My awesome SP", "en"]
|
# # sp:
|
||||||
# name: ["Test SP", "en"]
|
# # allow_unsolicited: true
|
||||||
#
|
#
|
||||||
# organization:
|
# # The examples below are just used to generate our metadata xml, and you
|
||||||
# name: Example com
|
# # may well not need them, depending on your setup. Alternatively you
|
||||||
# display_name:
|
# # may need a whole lot more detail - see the pysaml2 docs!
|
||||||
# - ["Example co", "en"]
|
#
|
||||||
# url: "http://example.com"
|
# description: ["My awesome SP", "en"]
|
||||||
#
|
# name: ["Test SP", "en"]
|
||||||
# contact_person:
|
#
|
||||||
# - given_name: Bob
|
# organization:
|
||||||
# sur_name: "the Sysadmin"
|
# name: Example com
|
||||||
# email_address": ["admin@example.com"]
|
# display_name:
|
||||||
# contact_type": technical
|
# - ["Example co", "en"]
|
||||||
#
|
# url: "http://example.com"
|
||||||
# # Instead of putting the config inline as above, you can specify a
|
#
|
||||||
# # separate pysaml2 configuration file:
|
# contact_person:
|
||||||
# #
|
# - given_name: Bob
|
||||||
# config_path: "CONFDIR/sp_conf.py"
|
# sur_name: "the Sysadmin"
|
||||||
#
|
# email_address": ["admin@example.com"]
|
||||||
# # the lifetime of a SAML session. This defines how long a user has to
|
# contact_type": technical
|
||||||
# # complete the authentication process, if allow_unsolicited is unset.
|
|
||||||
# # The default is 5 minutes.
|
# Instead of putting the config inline as above, you can specify a
|
||||||
# #
|
# separate pysaml2 configuration file:
|
||||||
# # saml_session_lifetime: 5m
|
#
|
||||||
|
#config_path: "CONFDIR/sp_conf.py"
|
||||||
|
|
||||||
|
# the lifetime of a SAML session. This defines how long a user has to
|
||||||
|
# complete the authentication process, if allow_unsolicited is unset.
|
||||||
|
# The default is 5 minutes.
|
||||||
|
#
|
||||||
|
#saml_session_lifetime: 5m
|
||||||
|
|
||||||
|
# The SAML attribute (after mapping via the attribute maps) to use to derive
|
||||||
|
# the Matrix ID from. 'uid' by default.
|
||||||
|
#
|
||||||
|
#mxid_source_attribute: displayName
|
||||||
|
|
||||||
|
# The mapping system to use for mapping the saml attribute onto a matrix ID.
|
||||||
|
# Options include:
|
||||||
|
# * 'hexencode' (which maps unpermitted characters to '=xx')
|
||||||
|
# * 'dotreplace' (which replaces unpermitted characters with '.').
|
||||||
|
# The default is 'hexencode'.
|
||||||
|
#
|
||||||
|
#mxid_mapping: dotreplace
|
||||||
|
|
||||||
|
# In previous versions of synapse, the mapping from SAML attribute to MXID was
|
||||||
|
# always calculated dynamically rather than stored in a table. For backwards-
|
||||||
|
# compatibility, we will look for user_ids matching such a pattern before
|
||||||
|
# creating a new account.
|
||||||
|
#
|
||||||
|
# This setting controls the SAML attribute which will be used for this
|
||||||
|
# backwards-compatibility lookup. Typically it should be 'uid', but if the
|
||||||
|
# attribute maps are changed, it may be necessary to change it.
|
||||||
|
#
|
||||||
|
# The default is 'uid'.
|
||||||
|
#
|
||||||
|
#grandfathered_mxid_source_attribute: upn
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -293,6 +293,8 @@ class RegistrationConfig(Config):
|
||||||
# by the Matrix Identity Service API specification:
|
# by the Matrix Identity Service API specification:
|
||||||
# https://matrix.org/docs/spec/identity_service/latest
|
# https://matrix.org/docs/spec/identity_service/latest
|
||||||
#
|
#
|
||||||
|
# If a delegate is specified, the config option public_baseurl must also be filled out.
|
||||||
|
#
|
||||||
account_threepid_delegates:
|
account_threepid_delegates:
|
||||||
#email: https://example.com # Delegate email sending to example.org
|
#email: https://example.com # Delegate email sending to example.org
|
||||||
#msisdn: http://localhost:8090 # Delegate SMS sending to this local process
|
#msisdn: http://localhost:8090 # Delegate SMS sending to this local process
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2018 New Vector Ltd
|
# Copyright 2018 New Vector Ltd
|
||||||
|
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
#
|
#
|
||||||
# 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,11 +13,47 @@
|
||||||
# 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 re
|
||||||
|
|
||||||
from synapse.python_dependencies import DependencyException, check_requirements
|
from synapse.python_dependencies import DependencyException, check_requirements
|
||||||
|
from synapse.types import (
|
||||||
|
map_username_to_mxid_localpart,
|
||||||
|
mxid_localpart_allowed_characters,
|
||||||
|
)
|
||||||
|
from synapse.util.module_loader import load_python_module
|
||||||
|
|
||||||
from ._base import Config, ConfigError
|
from ._base import Config, ConfigError
|
||||||
|
|
||||||
|
|
||||||
|
def _dict_merge(merge_dict, into_dict):
|
||||||
|
"""Do a deep merge of two dicts
|
||||||
|
|
||||||
|
Recursively merges `merge_dict` into `into_dict`:
|
||||||
|
* For keys where both `merge_dict` and `into_dict` have a dict value, the values
|
||||||
|
are recursively merged
|
||||||
|
* For all other keys, the values in `into_dict` (if any) are overwritten with
|
||||||
|
the value from `merge_dict`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
merge_dict (dict): dict to merge
|
||||||
|
into_dict (dict): target dict
|
||||||
|
"""
|
||||||
|
for k, v in merge_dict.items():
|
||||||
|
if k not in into_dict:
|
||||||
|
into_dict[k] = v
|
||||||
|
continue
|
||||||
|
|
||||||
|
current_val = into_dict[k]
|
||||||
|
|
||||||
|
if isinstance(v, dict) and isinstance(current_val, dict):
|
||||||
|
_dict_merge(v, current_val)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# otherwise we just overwrite
|
||||||
|
into_dict[k] = v
|
||||||
|
|
||||||
|
|
||||||
class SAML2Config(Config):
|
class SAML2Config(Config):
|
||||||
def read_config(self, config, **kwargs):
|
def read_config(self, config, **kwargs):
|
||||||
self.saml2_enabled = False
|
self.saml2_enabled = False
|
||||||
|
@ -26,6 +63,9 @@ class SAML2Config(Config):
|
||||||
if not saml2_config or not saml2_config.get("enabled", True):
|
if not saml2_config or not saml2_config.get("enabled", True):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not saml2_config.get("sp_config") and not saml2_config.get("config_path"):
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
check_requirements("saml2")
|
check_requirements("saml2")
|
||||||
except DependencyException as e:
|
except DependencyException as e:
|
||||||
|
@ -33,21 +73,40 @@ class SAML2Config(Config):
|
||||||
|
|
||||||
self.saml2_enabled = True
|
self.saml2_enabled = True
|
||||||
|
|
||||||
import saml2.config
|
self.saml2_mxid_source_attribute = saml2_config.get(
|
||||||
|
"mxid_source_attribute", "uid"
|
||||||
|
)
|
||||||
|
|
||||||
self.saml2_sp_config = saml2.config.SPConfig()
|
self.saml2_grandfathered_mxid_source_attribute = saml2_config.get(
|
||||||
self.saml2_sp_config.load(self._default_saml_config_dict())
|
"grandfathered_mxid_source_attribute", "uid"
|
||||||
self.saml2_sp_config.load(saml2_config.get("sp_config", {}))
|
)
|
||||||
|
|
||||||
|
saml2_config_dict = self._default_saml_config_dict()
|
||||||
|
_dict_merge(
|
||||||
|
merge_dict=saml2_config.get("sp_config", {}), into_dict=saml2_config_dict
|
||||||
|
)
|
||||||
|
|
||||||
config_path = saml2_config.get("config_path", None)
|
config_path = saml2_config.get("config_path", None)
|
||||||
if config_path is not None:
|
if config_path is not None:
|
||||||
self.saml2_sp_config.load_file(config_path)
|
mod = load_python_module(config_path)
|
||||||
|
_dict_merge(merge_dict=mod.CONFIG, into_dict=saml2_config_dict)
|
||||||
|
|
||||||
|
import saml2.config
|
||||||
|
|
||||||
|
self.saml2_sp_config = saml2.config.SPConfig()
|
||||||
|
self.saml2_sp_config.load(saml2_config_dict)
|
||||||
|
|
||||||
# session lifetime: in milliseconds
|
# session lifetime: in milliseconds
|
||||||
self.saml2_session_lifetime = self.parse_duration(
|
self.saml2_session_lifetime = self.parse_duration(
|
||||||
saml2_config.get("saml_session_lifetime", "5m")
|
saml2_config.get("saml_session_lifetime", "5m")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mapping = saml2_config.get("mxid_mapping", "hexencode")
|
||||||
|
try:
|
||||||
|
self.saml2_mxid_mapper = MXID_MAPPER_MAP[mapping]
|
||||||
|
except KeyError:
|
||||||
|
raise ConfigError("%s is not a known mxid_mapping" % (mapping,))
|
||||||
|
|
||||||
def _default_saml_config_dict(self):
|
def _default_saml_config_dict(self):
|
||||||
import saml2
|
import saml2
|
||||||
|
|
||||||
|
@ -55,6 +114,13 @@ class SAML2Config(Config):
|
||||||
if public_baseurl is None:
|
if public_baseurl is None:
|
||||||
raise ConfigError("saml2_config requires a public_baseurl to be set")
|
raise ConfigError("saml2_config requires a public_baseurl to be set")
|
||||||
|
|
||||||
|
required_attributes = {"uid", self.saml2_mxid_source_attribute}
|
||||||
|
|
||||||
|
optional_attributes = {"displayName"}
|
||||||
|
if self.saml2_grandfathered_mxid_source_attribute:
|
||||||
|
optional_attributes.add(self.saml2_grandfathered_mxid_source_attribute)
|
||||||
|
optional_attributes -= required_attributes
|
||||||
|
|
||||||
metadata_url = public_baseurl + "_matrix/saml2/metadata.xml"
|
metadata_url = public_baseurl + "_matrix/saml2/metadata.xml"
|
||||||
response_url = public_baseurl + "_matrix/saml2/authn_response"
|
response_url = public_baseurl + "_matrix/saml2/authn_response"
|
||||||
return {
|
return {
|
||||||
|
@ -66,8 +132,9 @@ class SAML2Config(Config):
|
||||||
(response_url, saml2.BINDING_HTTP_POST)
|
(response_url, saml2.BINDING_HTTP_POST)
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"required_attributes": ["uid"],
|
"required_attributes": list(required_attributes),
|
||||||
"optional_attributes": ["mail", "surname", "givenname"],
|
"optional_attributes": list(optional_attributes),
|
||||||
|
# "name_id_format": saml2.saml.NAMEID_FORMAT_PERSISTENT,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -76,12 +143,13 @@ class SAML2Config(Config):
|
||||||
return """\
|
return """\
|
||||||
# Enable SAML2 for registration and login. Uses pysaml2.
|
# Enable SAML2 for registration and login. Uses pysaml2.
|
||||||
#
|
#
|
||||||
# `sp_config` is the configuration for the pysaml2 Service Provider.
|
# At least one of `sp_config` or `config_path` must be set in this section to
|
||||||
# See pysaml2 docs for format of config.
|
# enable SAML login.
|
||||||
#
|
#
|
||||||
# Default values will be used for the 'entityid' and 'service' settings,
|
# (You will probably also want to set the following options to `false` to
|
||||||
# so it is not normally necessary to specify them unless you need to
|
# disable the regular login/registration flows:
|
||||||
# override them.
|
# * enable_registration
|
||||||
|
# * password_config.enabled
|
||||||
#
|
#
|
||||||
# Once SAML support is enabled, a metadata file will be exposed at
|
# Once SAML support is enabled, a metadata file will be exposed at
|
||||||
# https://<server>:<port>/_matrix/saml2/metadata.xml, which you may be able to
|
# https://<server>:<port>/_matrix/saml2/metadata.xml, which you may be able to
|
||||||
|
@ -89,8 +157,15 @@ class SAML2Config(Config):
|
||||||
# the IdP to use an ACS location of
|
# the IdP to use an ACS location of
|
||||||
# https://<server>:<port>/_matrix/saml2/authn_response.
|
# https://<server>:<port>/_matrix/saml2/authn_response.
|
||||||
#
|
#
|
||||||
#saml2_config:
|
saml2_config:
|
||||||
# sp_config:
|
# `sp_config` is the configuration for the pysaml2 Service Provider.
|
||||||
|
# See pysaml2 docs for format of config.
|
||||||
|
#
|
||||||
|
# Default values will be used for the 'entityid' and 'service' settings,
|
||||||
|
# so it is not normally necessary to specify them unless you need to
|
||||||
|
# override them.
|
||||||
|
#
|
||||||
|
#sp_config:
|
||||||
# # point this to the IdP's metadata. You can use either a local file or
|
# # point this to the IdP's metadata. You can use either a local file or
|
||||||
# # (preferably) a URL.
|
# # (preferably) a URL.
|
||||||
# metadata:
|
# metadata:
|
||||||
|
@ -98,16 +173,16 @@ class SAML2Config(Config):
|
||||||
# remote:
|
# remote:
|
||||||
# - url: https://our_idp/metadata.xml
|
# - url: https://our_idp/metadata.xml
|
||||||
#
|
#
|
||||||
# # By default, the user has to go to our login page first. If you'd like to
|
# # By default, the user has to go to our login page first. If you'd like
|
||||||
# # allow IdP-initiated login, set 'allow_unsolicited: True' in a
|
# # to allow IdP-initiated login, set 'allow_unsolicited: True' in a
|
||||||
# # 'service.sp' section:
|
# # 'service.sp' section:
|
||||||
# #
|
# #
|
||||||
# #service:
|
# #service:
|
||||||
# # sp:
|
# # sp:
|
||||||
# # allow_unsolicited: True
|
# # allow_unsolicited: true
|
||||||
#
|
#
|
||||||
# # The examples below are just used to generate our metadata xml, and you
|
# # The examples below are just used to generate our metadata xml, and you
|
||||||
# # may well not need it, depending on your setup. Alternatively you
|
# # may well not need them, depending on your setup. Alternatively you
|
||||||
# # may need a whole lot more detail - see the pysaml2 docs!
|
# # may need a whole lot more detail - see the pysaml2 docs!
|
||||||
#
|
#
|
||||||
# description: ["My awesome SP", "en"]
|
# description: ["My awesome SP", "en"]
|
||||||
|
@ -124,17 +199,63 @@ class SAML2Config(Config):
|
||||||
# sur_name: "the Sysadmin"
|
# sur_name: "the Sysadmin"
|
||||||
# email_address": ["admin@example.com"]
|
# email_address": ["admin@example.com"]
|
||||||
# contact_type": technical
|
# contact_type": technical
|
||||||
|
|
||||||
|
# Instead of putting the config inline as above, you can specify a
|
||||||
|
# separate pysaml2 configuration file:
|
||||||
#
|
#
|
||||||
# # Instead of putting the config inline as above, you can specify a
|
#config_path: "%(config_dir_path)s/sp_conf.py"
|
||||||
# # separate pysaml2 configuration file:
|
|
||||||
# #
|
# the lifetime of a SAML session. This defines how long a user has to
|
||||||
# config_path: "%(config_dir_path)s/sp_conf.py"
|
# complete the authentication process, if allow_unsolicited is unset.
|
||||||
|
# The default is 5 minutes.
|
||||||
#
|
#
|
||||||
# # the lifetime of a SAML session. This defines how long a user has to
|
#saml_session_lifetime: 5m
|
||||||
# # complete the authentication process, if allow_unsolicited is unset.
|
|
||||||
# # The default is 5 minutes.
|
# The SAML attribute (after mapping via the attribute maps) to use to derive
|
||||||
# #
|
# the Matrix ID from. 'uid' by default.
|
||||||
# # saml_session_lifetime: 5m
|
#
|
||||||
|
#mxid_source_attribute: displayName
|
||||||
|
|
||||||
|
# The mapping system to use for mapping the saml attribute onto a matrix ID.
|
||||||
|
# Options include:
|
||||||
|
# * 'hexencode' (which maps unpermitted characters to '=xx')
|
||||||
|
# * 'dotreplace' (which replaces unpermitted characters with '.').
|
||||||
|
# The default is 'hexencode'.
|
||||||
|
#
|
||||||
|
#mxid_mapping: dotreplace
|
||||||
|
|
||||||
|
# In previous versions of synapse, the mapping from SAML attribute to MXID was
|
||||||
|
# always calculated dynamically rather than stored in a table. For backwards-
|
||||||
|
# compatibility, we will look for user_ids matching such a pattern before
|
||||||
|
# creating a new account.
|
||||||
|
#
|
||||||
|
# This setting controls the SAML attribute which will be used for this
|
||||||
|
# backwards-compatibility lookup. Typically it should be 'uid', but if the
|
||||||
|
# attribute maps are changed, it may be necessary to change it.
|
||||||
|
#
|
||||||
|
# The default is 'uid'.
|
||||||
|
#
|
||||||
|
#grandfathered_mxid_source_attribute: upn
|
||||||
""" % {
|
""" % {
|
||||||
"config_dir_path": config_dir_path
|
"config_dir_path": config_dir_path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
DOT_REPLACE_PATTERN = re.compile(
|
||||||
|
("[^%s]" % (re.escape("".join(mxid_localpart_allowed_characters)),))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def dot_replace_for_mxid(username: str) -> str:
|
||||||
|
username = username.lower()
|
||||||
|
username = DOT_REPLACE_PATTERN.sub(".", username)
|
||||||
|
|
||||||
|
# regular mxids aren't allowed to start with an underscore either
|
||||||
|
username = re.sub("^_", "", username)
|
||||||
|
return username
|
||||||
|
|
||||||
|
|
||||||
|
MXID_MAPPER_MAP = {
|
||||||
|
"hexencode": map_username_to_mxid_localpart,
|
||||||
|
"dotreplace": dot_replace_for_mxid,
|
||||||
|
}
|
||||||
|
|
|
@ -362,10 +362,8 @@ class ServerConfig(Config):
|
||||||
|
|
||||||
_check_resource_config(self.listeners)
|
_check_resource_config(self.listeners)
|
||||||
|
|
||||||
# An experimental option to try and periodically clean up extremities
|
|
||||||
# by sending dummy events.
|
|
||||||
self.cleanup_extremities_with_dummy_events = config.get(
|
self.cleanup_extremities_with_dummy_events = config.get(
|
||||||
"cleanup_extremities_with_dummy_events", False
|
"cleanup_extremities_with_dummy_events", True
|
||||||
)
|
)
|
||||||
|
|
||||||
def has_tls_listener(self):
|
def has_tls_listener(self):
|
||||||
|
@ -552,6 +550,9 @@ class ServerConfig(Config):
|
||||||
# blacklist IP address CIDR ranges. If this option is not specified, or
|
# blacklist IP address CIDR ranges. If this option is not specified, or
|
||||||
# specified with an empty list, no ip range blacklist will be enforced.
|
# specified with an empty list, no ip range blacklist will be enforced.
|
||||||
#
|
#
|
||||||
|
# As of Synapse v1.4.0 this option also affects any outbound requests to identity
|
||||||
|
# servers provided by user input.
|
||||||
|
#
|
||||||
# (0.0.0.0 and :: are always blacklisted, whether or not they are explicitly
|
# (0.0.0.0 and :: are always blacklisted, whether or not they are explicitly
|
||||||
# listed here, since they correspond to unroutable addresses.)
|
# listed here, since they correspond to unroutable addresses.)
|
||||||
#
|
#
|
||||||
|
|
|
@ -21,10 +21,8 @@ import unicodedata
|
||||||
import attr
|
import attr
|
||||||
import bcrypt
|
import bcrypt
|
||||||
import pymacaroons
|
import pymacaroons
|
||||||
from canonicaljson import json
|
|
||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
from twisted.web.client import PartialDownloadError
|
|
||||||
|
|
||||||
import synapse.util.stringutils as stringutils
|
import synapse.util.stringutils as stringutils
|
||||||
from synapse.api.constants import LoginType
|
from synapse.api.constants import LoginType
|
||||||
|
@ -38,7 +36,8 @@ from synapse.api.errors import (
|
||||||
UserDeactivatedError,
|
UserDeactivatedError,
|
||||||
)
|
)
|
||||||
from synapse.api.ratelimiting import Ratelimiter
|
from synapse.api.ratelimiting import Ratelimiter
|
||||||
from synapse.config.emailconfig import ThreepidBehaviour
|
from synapse.handlers.ui_auth import INTERACTIVE_AUTH_CHECKERS
|
||||||
|
from synapse.handlers.ui_auth.checkers import UserInteractiveAuthChecker
|
||||||
from synapse.logging.context import defer_to_thread
|
from synapse.logging.context import defer_to_thread
|
||||||
from synapse.module_api import ModuleApi
|
from synapse.module_api import ModuleApi
|
||||||
from synapse.types import UserID
|
from synapse.types import UserID
|
||||||
|
@ -58,13 +57,13 @@ class AuthHandler(BaseHandler):
|
||||||
hs (synapse.server.HomeServer):
|
hs (synapse.server.HomeServer):
|
||||||
"""
|
"""
|
||||||
super(AuthHandler, self).__init__(hs)
|
super(AuthHandler, self).__init__(hs)
|
||||||
self.checkers = {
|
|
||||||
LoginType.RECAPTCHA: self._check_recaptcha,
|
self.checkers = {} # type: dict[str, UserInteractiveAuthChecker]
|
||||||
LoginType.EMAIL_IDENTITY: self._check_email_identity,
|
for auth_checker_class in INTERACTIVE_AUTH_CHECKERS:
|
||||||
LoginType.MSISDN: self._check_msisdn,
|
inst = auth_checker_class(hs)
|
||||||
LoginType.DUMMY: self._check_dummy_auth,
|
if inst.is_enabled():
|
||||||
LoginType.TERMS: self._check_terms_auth,
|
self.checkers[inst.AUTH_TYPE] = inst
|
||||||
}
|
|
||||||
self.bcrypt_rounds = hs.config.bcrypt_rounds
|
self.bcrypt_rounds = hs.config.bcrypt_rounds
|
||||||
|
|
||||||
# This is not a cache per se, but a store of all current sessions that
|
# This is not a cache per se, but a store of all current sessions that
|
||||||
|
@ -158,6 +157,14 @@ class AuthHandler(BaseHandler):
|
||||||
|
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
def get_enabled_auth_types(self):
|
||||||
|
"""Return the enabled user-interactive authentication types
|
||||||
|
|
||||||
|
Returns the UI-Auth types which are supported by the homeserver's current
|
||||||
|
config.
|
||||||
|
"""
|
||||||
|
return self.checkers.keys()
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def check_auth(self, flows, clientdict, clientip):
|
def check_auth(self, flows, clientdict, clientip):
|
||||||
"""
|
"""
|
||||||
|
@ -292,7 +299,7 @@ class AuthHandler(BaseHandler):
|
||||||
sess["creds"] = {}
|
sess["creds"] = {}
|
||||||
creds = sess["creds"]
|
creds = sess["creds"]
|
||||||
|
|
||||||
result = yield self.checkers[stagetype](authdict, clientip)
|
result = yield self.checkers[stagetype].check_auth(authdict, clientip)
|
||||||
if result:
|
if result:
|
||||||
creds[stagetype] = result
|
creds[stagetype] = result
|
||||||
self._save_session(sess)
|
self._save_session(sess)
|
||||||
|
@ -363,7 +370,7 @@ class AuthHandler(BaseHandler):
|
||||||
login_type = authdict["type"]
|
login_type = authdict["type"]
|
||||||
checker = self.checkers.get(login_type)
|
checker = self.checkers.get(login_type)
|
||||||
if checker is not None:
|
if checker is not None:
|
||||||
res = yield checker(authdict, clientip=clientip)
|
res = yield checker.check_auth(authdict, clientip=clientip)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
# build a v1-login-style dict out of the authdict and fall back to the
|
# build a v1-login-style dict out of the authdict and fall back to the
|
||||||
|
@ -376,125 +383,6 @@ class AuthHandler(BaseHandler):
|
||||||
(canonical_id, callback) = yield self.validate_login(user_id, authdict)
|
(canonical_id, callback) = yield self.validate_login(user_id, authdict)
|
||||||
return canonical_id
|
return canonical_id
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def _check_recaptcha(self, authdict, clientip, **kwargs):
|
|
||||||
try:
|
|
||||||
user_response = authdict["response"]
|
|
||||||
except KeyError:
|
|
||||||
# Client tried to provide captcha but didn't give the parameter:
|
|
||||||
# bad request.
|
|
||||||
raise LoginError(
|
|
||||||
400, "Captcha response is required", errcode=Codes.CAPTCHA_NEEDED
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
"Submitting recaptcha response %s with remoteip %s", user_response, clientip
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO: get this from the homeserver rather than creating a new one for
|
|
||||||
# each request
|
|
||||||
try:
|
|
||||||
client = self.hs.get_simple_http_client()
|
|
||||||
resp_body = yield client.post_urlencoded_get_json(
|
|
||||||
self.hs.config.recaptcha_siteverify_api,
|
|
||||||
args={
|
|
||||||
"secret": self.hs.config.recaptcha_private_key,
|
|
||||||
"response": user_response,
|
|
||||||
"remoteip": clientip,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
except PartialDownloadError as pde:
|
|
||||||
# Twisted is silly
|
|
||||||
data = pde.response
|
|
||||||
resp_body = json.loads(data)
|
|
||||||
|
|
||||||
if "success" in resp_body:
|
|
||||||
# Note that we do NOT check the hostname here: we explicitly
|
|
||||||
# intend the CAPTCHA to be presented by whatever client the
|
|
||||||
# user is using, we just care that they have completed a CAPTCHA.
|
|
||||||
logger.info(
|
|
||||||
"%s reCAPTCHA from hostname %s",
|
|
||||||
"Successful" if resp_body["success"] else "Failed",
|
|
||||||
resp_body.get("hostname"),
|
|
||||||
)
|
|
||||||
if resp_body["success"]:
|
|
||||||
return True
|
|
||||||
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)
|
|
||||||
|
|
||||||
def _check_email_identity(self, authdict, **kwargs):
|
|
||||||
return self._check_threepid("email", authdict, **kwargs)
|
|
||||||
|
|
||||||
def _check_msisdn(self, authdict, **kwargs):
|
|
||||||
return self._check_threepid("msisdn", authdict)
|
|
||||||
|
|
||||||
def _check_dummy_auth(self, authdict, **kwargs):
|
|
||||||
return defer.succeed(True)
|
|
||||||
|
|
||||||
def _check_terms_auth(self, authdict, **kwargs):
|
|
||||||
return defer.succeed(True)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def _check_threepid(self, medium, authdict, **kwargs):
|
|
||||||
if "threepid_creds" not in authdict:
|
|
||||||
raise LoginError(400, "Missing threepid_creds", Codes.MISSING_PARAM)
|
|
||||||
|
|
||||||
threepid_creds = authdict["threepid_creds"]
|
|
||||||
|
|
||||||
identity_handler = self.hs.get_handlers().identity_handler
|
|
||||||
|
|
||||||
logger.info("Getting validated threepid. threepidcreds: %r", (threepid_creds,))
|
|
||||||
if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE:
|
|
||||||
if medium == "email":
|
|
||||||
threepid = yield identity_handler.threepid_from_creds(
|
|
||||||
self.hs.config.account_threepid_delegate_email, threepid_creds
|
|
||||||
)
|
|
||||||
elif medium == "msisdn":
|
|
||||||
threepid = yield identity_handler.threepid_from_creds(
|
|
||||||
self.hs.config.account_threepid_delegate_msisdn, threepid_creds
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise SynapseError(400, "Unrecognized threepid medium: %s" % (medium,))
|
|
||||||
elif self.hs.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
|
|
||||||
row = yield self.store.get_threepid_validation_session(
|
|
||||||
medium,
|
|
||||||
threepid_creds["client_secret"],
|
|
||||||
sid=threepid_creds["sid"],
|
|
||||||
validated=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
threepid = (
|
|
||||||
{
|
|
||||||
"medium": row["medium"],
|
|
||||||
"address": row["address"],
|
|
||||||
"validated_at": row["validated_at"],
|
|
||||||
}
|
|
||||||
if row
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
if row:
|
|
||||||
# Valid threepid returned, delete from the db
|
|
||||||
yield self.store.delete_threepid_session(threepid_creds["sid"])
|
|
||||||
else:
|
|
||||||
raise SynapseError(
|
|
||||||
400, "Password resets are not enabled on this homeserver"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not threepid:
|
|
||||||
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)
|
|
||||||
|
|
||||||
if threepid["medium"] != medium:
|
|
||||||
raise LoginError(
|
|
||||||
401,
|
|
||||||
"Expecting threepid of type '%s', got '%s'"
|
|
||||||
% (medium, threepid["medium"]),
|
|
||||||
errcode=Codes.UNAUTHORIZED,
|
|
||||||
)
|
|
||||||
|
|
||||||
threepid["threepid_creds"] = authdict["threepid_creds"]
|
|
||||||
|
|
||||||
return threepid
|
|
||||||
|
|
||||||
def _get_params_recaptcha(self):
|
def _get_params_recaptcha(self):
|
||||||
return {"public_key": self.hs.config.recaptcha_public_key}
|
return {"public_key": self.hs.config.recaptcha_public_key}
|
||||||
|
|
||||||
|
|
|
@ -73,7 +73,9 @@ class DeactivateAccountHandler(BaseHandler):
|
||||||
# unbinding
|
# unbinding
|
||||||
identity_server_supports_unbinding = True
|
identity_server_supports_unbinding = True
|
||||||
|
|
||||||
threepids = yield self.store.user_get_threepids(user_id)
|
# Retrieve the 3PIDs this user has bound to an identity server
|
||||||
|
threepids = yield self.store.user_get_bound_threepids(user_id)
|
||||||
|
|
||||||
for threepid in threepids:
|
for threepid in threepids:
|
||||||
try:
|
try:
|
||||||
result = yield self._identity_handler.try_unbind_threepid(
|
result = yield self._identity_handler.try_unbind_threepid(
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
"""Utilities for interacting with Identity Servers"""
|
"""Utilities for interacting with Identity Servers"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import urllib
|
||||||
|
|
||||||
from canonicaljson import json
|
from canonicaljson import json
|
||||||
|
|
||||||
|
@ -30,6 +31,8 @@ from synapse.api.errors import (
|
||||||
HttpResponseException,
|
HttpResponseException,
|
||||||
SynapseError,
|
SynapseError,
|
||||||
)
|
)
|
||||||
|
from synapse.config.emailconfig import ThreepidBehaviour
|
||||||
|
from synapse.http.client import SimpleHttpClient
|
||||||
from synapse.util.stringutils import random_string
|
from synapse.util.stringutils import random_string
|
||||||
|
|
||||||
from ._base import BaseHandler
|
from ._base import BaseHandler
|
||||||
|
@ -41,40 +44,15 @@ class IdentityHandler(BaseHandler):
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
super(IdentityHandler, self).__init__(hs)
|
super(IdentityHandler, self).__init__(hs)
|
||||||
|
|
||||||
self.http_client = hs.get_simple_http_client()
|
self.http_client = SimpleHttpClient(hs)
|
||||||
|
# We create a blacklisting instance of SimpleHttpClient for contacting identity
|
||||||
|
# servers specified by clients
|
||||||
|
self.blacklisting_http_client = SimpleHttpClient(
|
||||||
|
hs, ip_blacklist=hs.config.federation_ip_range_blacklist
|
||||||
|
)
|
||||||
self.federation_http_client = hs.get_http_client()
|
self.federation_http_client = hs.get_http_client()
|
||||||
self.hs = hs
|
self.hs = hs
|
||||||
|
|
||||||
def _extract_items_from_creds_dict(self, creds):
|
|
||||||
"""
|
|
||||||
Retrieve entries from a "credentials" dictionary
|
|
||||||
|
|
||||||
Args:
|
|
||||||
creds (dict[str, str]): Dictionary of credentials that contain the following keys:
|
|
||||||
* client_secret|clientSecret: A unique secret str provided by the client
|
|
||||||
* id_server|idServer: the domain of the identity server to query
|
|
||||||
* id_access_token: The access token to authenticate to the identity
|
|
||||||
server with.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple(str, str, str|None): A tuple containing the client_secret, the id_server,
|
|
||||||
and the id_access_token value if available.
|
|
||||||
"""
|
|
||||||
client_secret = creds.get("client_secret") or creds.get("clientSecret")
|
|
||||||
if not client_secret:
|
|
||||||
raise SynapseError(
|
|
||||||
400, "No client_secret in creds", errcode=Codes.MISSING_PARAM
|
|
||||||
)
|
|
||||||
|
|
||||||
id_server = creds.get("id_server") or creds.get("idServer")
|
|
||||||
if not id_server:
|
|
||||||
raise SynapseError(
|
|
||||||
400, "No id_server in creds", errcode=Codes.MISSING_PARAM
|
|
||||||
)
|
|
||||||
|
|
||||||
id_access_token = creds.get("id_access_token")
|
|
||||||
return client_secret, id_server, id_access_token
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def threepid_from_creds(self, id_server, creds):
|
def threepid_from_creds(self, id_server, creds):
|
||||||
"""
|
"""
|
||||||
|
@ -113,35 +91,50 @@ class IdentityHandler(BaseHandler):
|
||||||
data = yield self.http_client.get_json(url, query_params)
|
data = yield self.http_client.get_json(url, query_params)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
raise SynapseError(500, "Timed out contacting identity server")
|
raise SynapseError(500, "Timed out contacting identity server")
|
||||||
return data if "medium" in data else None
|
except HttpResponseException as e:
|
||||||
|
logger.info(
|
||||||
|
"%s returned %i for threepid validation for: %s",
|
||||||
|
id_server,
|
||||||
|
e.code,
|
||||||
|
creds,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Old versions of Sydent return a 200 http code even on a failed validation
|
||||||
|
# check. Thus, in addition to the HttpResponseException check above (which
|
||||||
|
# checks for non-200 errors), we need to make sure validation_session isn't
|
||||||
|
# actually an error, identified by the absence of a "medium" key
|
||||||
|
# See https://github.com/matrix-org/sydent/issues/215 for details
|
||||||
|
if "medium" in data:
|
||||||
|
return data
|
||||||
|
|
||||||
|
logger.info("%s reported non-validated threepid: %s", id_server, creds)
|
||||||
|
return None
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def bind_threepid(self, creds, mxid, use_v2=True):
|
def bind_threepid(
|
||||||
|
self, client_secret, sid, mxid, id_server, id_access_token=None, use_v2=True
|
||||||
|
):
|
||||||
"""Bind a 3PID to an identity server
|
"""Bind a 3PID to an identity server
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
creds (dict[str, str]): Dictionary of credentials that contain the following keys:
|
client_secret (str): A unique secret provided by the client
|
||||||
* client_secret|clientSecret: A unique secret str provided by the client
|
|
||||||
* id_server|idServer: the domain of the identity server to query
|
sid (str): The ID of the validation session
|
||||||
* id_access_token: The access token to authenticate to the identity
|
|
||||||
server with. Required if use_v2 is true
|
|
||||||
mxid (str): The MXID to bind the 3PID to
|
mxid (str): The MXID to bind the 3PID to
|
||||||
use_v2 (bool): Whether to use v2 Identity Service API endpoints
|
|
||||||
|
id_server (str): The domain of the identity server to query
|
||||||
|
|
||||||
|
id_access_token (str): The access token to authenticate to the identity
|
||||||
|
server with, if necessary. Required if use_v2 is true
|
||||||
|
|
||||||
|
use_v2 (bool): Whether to use v2 Identity Service API endpoints. Defaults to True
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Deferred[dict]: The response from the identity server
|
Deferred[dict]: The response from the identity server
|
||||||
"""
|
"""
|
||||||
logger.debug("binding threepid %r to %s", creds, mxid)
|
logger.debug("Proxying threepid bind request for %s to %s", mxid, id_server)
|
||||||
|
|
||||||
client_secret, id_server, id_access_token = self._extract_items_from_creds_dict(
|
|
||||||
creds
|
|
||||||
)
|
|
||||||
|
|
||||||
sid = creds.get("sid")
|
|
||||||
if not sid:
|
|
||||||
raise SynapseError(
|
|
||||||
400, "No sid in three_pid_creds", errcode=Codes.MISSING_PARAM
|
|
||||||
)
|
|
||||||
|
|
||||||
# If an id_access_token is not supplied, force usage of v1
|
# If an id_access_token is not supplied, force usage of v1
|
||||||
if id_access_token is None:
|
if id_access_token is None:
|
||||||
|
@ -157,10 +150,11 @@ class IdentityHandler(BaseHandler):
|
||||||
bind_url = "https://%s/_matrix/identity/api/v1/3pid/bind" % (id_server,)
|
bind_url = "https://%s/_matrix/identity/api/v1/3pid/bind" % (id_server,)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = yield self.http_client.post_json_get_json(
|
# Use the blacklisting http client as this call is only to identity servers
|
||||||
|
# provided by a client
|
||||||
|
data = yield self.blacklisting_http_client.post_json_get_json(
|
||||||
bind_url, bind_data, headers=headers
|
bind_url, bind_data, headers=headers
|
||||||
)
|
)
|
||||||
logger.debug("bound threepid %r to %s", creds, mxid)
|
|
||||||
|
|
||||||
# Remember where we bound the threepid
|
# Remember where we bound the threepid
|
||||||
yield self.store.add_user_bound_threepid(
|
yield self.store.add_user_bound_threepid(
|
||||||
|
@ -182,7 +176,10 @@ class IdentityHandler(BaseHandler):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
logger.info("Got 404 when POSTing JSON %s, falling back to v1 URL", bind_url)
|
logger.info("Got 404 when POSTing JSON %s, falling back to v1 URL", bind_url)
|
||||||
return (yield self.bind_threepid(creds, mxid, use_v2=False))
|
res = yield self.bind_threepid(
|
||||||
|
client_secret, sid, mxid, id_server, id_access_token, use_v2=False
|
||||||
|
)
|
||||||
|
return res
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def try_unbind_threepid(self, mxid, threepid):
|
def try_unbind_threepid(self, mxid, threepid):
|
||||||
|
@ -258,7 +255,11 @@ class IdentityHandler(BaseHandler):
|
||||||
headers = {b"Authorization": auth_headers}
|
headers = {b"Authorization": auth_headers}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield self.http_client.post_json_get_json(url, content, headers)
|
# Use the blacklisting http client as this call is only to identity servers
|
||||||
|
# provided by a client
|
||||||
|
yield self.blacklisting_http_client.post_json_get_json(
|
||||||
|
url, content, headers
|
||||||
|
)
|
||||||
changed = True
|
changed = True
|
||||||
except HttpResponseException as e:
|
except HttpResponseException as e:
|
||||||
changed = False
|
changed = False
|
||||||
|
@ -328,6 +329,15 @@ class IdentityHandler(BaseHandler):
|
||||||
# Generate a session id
|
# Generate a session id
|
||||||
session_id = random_string(16)
|
session_id = random_string(16)
|
||||||
|
|
||||||
|
if next_link:
|
||||||
|
# Manipulate the next_link to add the sid, because the caller won't get
|
||||||
|
# it until we send a response, by which time we've sent the mail.
|
||||||
|
if "?" in next_link:
|
||||||
|
next_link += "&"
|
||||||
|
else:
|
||||||
|
next_link += "?"
|
||||||
|
next_link += "sid=" + urllib.parse.quote(session_id)
|
||||||
|
|
||||||
# Generate a new validation token
|
# Generate a new validation token
|
||||||
token = random_string(32)
|
token = random_string(32)
|
||||||
|
|
||||||
|
@ -452,13 +462,101 @@ class IdentityHandler(BaseHandler):
|
||||||
id_server + "/_matrix/identity/api/v1/validate/msisdn/requestToken",
|
id_server + "/_matrix/identity/api/v1/validate/msisdn/requestToken",
|
||||||
params,
|
params,
|
||||||
)
|
)
|
||||||
return data
|
|
||||||
except HttpResponseException as e:
|
except HttpResponseException as e:
|
||||||
logger.info("Proxied requestToken failed: %r", e)
|
logger.info("Proxied requestToken failed: %r", e)
|
||||||
raise e.to_synapse_error()
|
raise e.to_synapse_error()
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
raise SynapseError(500, "Timed out contacting identity server")
|
raise SynapseError(500, "Timed out contacting identity server")
|
||||||
|
|
||||||
|
assert self.hs.config.public_baseurl
|
||||||
|
|
||||||
|
# we need to tell the client to send the token back to us, since it doesn't
|
||||||
|
# otherwise know where to send it, so add submit_url response parameter
|
||||||
|
# (see also MSC2078)
|
||||||
|
data["submit_url"] = (
|
||||||
|
self.hs.config.public_baseurl
|
||||||
|
+ "_matrix/client/unstable/add_threepid/msisdn/submit_token"
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def validate_threepid_session(self, client_secret, sid):
|
||||||
|
"""Validates a threepid session with only the client secret and session ID
|
||||||
|
Tries validating against any configured account_threepid_delegates as well as locally.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client_secret (str): A secret provided by the client
|
||||||
|
|
||||||
|
sid (str): The ID of the session
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, str|int] if validation was successful, otherwise None
|
||||||
|
"""
|
||||||
|
# XXX: We shouldn't need to keep wrapping and unwrapping this value
|
||||||
|
threepid_creds = {"client_secret": client_secret, "sid": sid}
|
||||||
|
|
||||||
|
# We don't actually know which medium this 3PID is. Thus we first assume it's email,
|
||||||
|
# and if validation fails we try msisdn
|
||||||
|
validation_session = None
|
||||||
|
|
||||||
|
# Try to validate as email
|
||||||
|
if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE:
|
||||||
|
# Ask our delegated email identity server
|
||||||
|
validation_session = yield self.threepid_from_creds(
|
||||||
|
self.hs.config.account_threepid_delegate_email, threepid_creds
|
||||||
|
)
|
||||||
|
elif self.hs.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
|
||||||
|
# Get a validated session matching these details
|
||||||
|
validation_session = yield self.store.get_threepid_validation_session(
|
||||||
|
"email", client_secret, sid=sid, validated=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if validation_session:
|
||||||
|
return validation_session
|
||||||
|
|
||||||
|
# Try to validate as msisdn
|
||||||
|
if self.hs.config.account_threepid_delegate_msisdn:
|
||||||
|
# Ask our delegated msisdn identity server
|
||||||
|
validation_session = yield self.threepid_from_creds(
|
||||||
|
self.hs.config.account_threepid_delegate_msisdn, threepid_creds
|
||||||
|
)
|
||||||
|
|
||||||
|
return validation_session
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def proxy_msisdn_submit_token(self, id_server, client_secret, sid, token):
|
||||||
|
"""Proxy a POST submitToken request to an identity server for verification purposes
|
||||||
|
|
||||||
|
Args:
|
||||||
|
id_server (str): The identity server URL to contact
|
||||||
|
|
||||||
|
client_secret (str): Secret provided by the client
|
||||||
|
|
||||||
|
sid (str): The ID of the session
|
||||||
|
|
||||||
|
token (str): The verification token
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
SynapseError: If we failed to contact the identity server
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Deferred[dict]: The response dict from the identity server
|
||||||
|
"""
|
||||||
|
body = {"client_secret": client_secret, "sid": sid, "token": token}
|
||||||
|
|
||||||
|
try:
|
||||||
|
return (
|
||||||
|
yield self.http_client.post_json_get_json(
|
||||||
|
id_server + "/_matrix/identity/api/v1/validate/msisdn/submitToken",
|
||||||
|
body,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except TimeoutError:
|
||||||
|
raise SynapseError(500, "Timed out contacting identity server")
|
||||||
|
except HttpResponseException as e:
|
||||||
|
logger.warning("Error contacting msisdn account_threepid_delegate: %s", e)
|
||||||
|
raise SynapseError(400, "Error contacting the identity server")
|
||||||
|
|
||||||
|
|
||||||
def create_id_access_token_header(id_access_token):
|
def create_id_access_token_header(id_access_token):
|
||||||
"""Create an Authorization header for passing to SimpleHttpClient as the header value
|
"""Create an Authorization header for passing to SimpleHttpClient as the header value
|
||||||
|
|
|
@ -31,6 +31,7 @@ from synapse import types
|
||||||
from synapse.api.constants import EventTypes, Membership
|
from synapse.api.constants import EventTypes, Membership
|
||||||
from synapse.api.errors import AuthError, Codes, HttpResponseException, SynapseError
|
from synapse.api.errors import AuthError, Codes, HttpResponseException, SynapseError
|
||||||
from synapse.handlers.identity import LookupAlgorithm, create_id_access_token_header
|
from synapse.handlers.identity import LookupAlgorithm, create_id_access_token_header
|
||||||
|
from synapse.http.client import SimpleHttpClient
|
||||||
from synapse.types import RoomID, UserID
|
from synapse.types import RoomID, UserID
|
||||||
from synapse.util.async_helpers import Linearizer
|
from synapse.util.async_helpers import Linearizer
|
||||||
from synapse.util.distributor import user_joined_room, user_left_room
|
from synapse.util.distributor import user_joined_room, user_left_room
|
||||||
|
@ -62,7 +63,11 @@ class RoomMemberHandler(object):
|
||||||
self.auth = hs.get_auth()
|
self.auth = hs.get_auth()
|
||||||
self.state_handler = hs.get_state_handler()
|
self.state_handler = hs.get_state_handler()
|
||||||
self.config = hs.config
|
self.config = hs.config
|
||||||
self.simple_http_client = hs.get_simple_http_client()
|
# We create a blacklisting instance of SimpleHttpClient for contacting identity
|
||||||
|
# servers specified by clients
|
||||||
|
self.simple_http_client = SimpleHttpClient(
|
||||||
|
hs, ip_blacklist=hs.config.federation_ip_range_blacklist
|
||||||
|
)
|
||||||
|
|
||||||
self.federation_handler = hs.get_handlers().federation_handler
|
self.federation_handler = hs.get_handlers().federation_handler
|
||||||
self.directory_handler = hs.get_handlers().directory_handler
|
self.directory_handler = hs.get_handlers().directory_handler
|
||||||
|
|
|
@ -21,6 +21,8 @@ from saml2.client import Saml2Client
|
||||||
from synapse.api.errors import SynapseError
|
from synapse.api.errors import SynapseError
|
||||||
from synapse.http.servlet import parse_string
|
from synapse.http.servlet import parse_string
|
||||||
from synapse.rest.client.v1.login import SSOAuthHandler
|
from synapse.rest.client.v1.login import SSOAuthHandler
|
||||||
|
from synapse.types import UserID, map_username_to_mxid_localpart
|
||||||
|
from synapse.util.async_helpers import Linearizer
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -29,12 +31,26 @@ class SamlHandler:
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
self._saml_client = Saml2Client(hs.config.saml2_sp_config)
|
self._saml_client = Saml2Client(hs.config.saml2_sp_config)
|
||||||
self._sso_auth_handler = SSOAuthHandler(hs)
|
self._sso_auth_handler = SSOAuthHandler(hs)
|
||||||
|
self._registration_handler = hs.get_registration_handler()
|
||||||
|
|
||||||
|
self._clock = hs.get_clock()
|
||||||
|
self._datastore = hs.get_datastore()
|
||||||
|
self._hostname = hs.hostname
|
||||||
|
self._saml2_session_lifetime = hs.config.saml2_session_lifetime
|
||||||
|
self._mxid_source_attribute = hs.config.saml2_mxid_source_attribute
|
||||||
|
self._grandfathered_mxid_source_attribute = (
|
||||||
|
hs.config.saml2_grandfathered_mxid_source_attribute
|
||||||
|
)
|
||||||
|
self._mxid_mapper = hs.config.saml2_mxid_mapper
|
||||||
|
|
||||||
|
# identifier for the external_ids table
|
||||||
|
self._auth_provider_id = "saml"
|
||||||
|
|
||||||
# a map from saml session id to Saml2SessionData object
|
# a map from saml session id to Saml2SessionData object
|
||||||
self._outstanding_requests_dict = {}
|
self._outstanding_requests_dict = {}
|
||||||
|
|
||||||
self._clock = hs.get_clock()
|
# a lock on the mappings
|
||||||
self._saml2_session_lifetime = hs.config.saml2_session_lifetime
|
self._mapping_lock = Linearizer(name="saml_mapping", clock=self._clock)
|
||||||
|
|
||||||
def handle_redirect_request(self, client_redirect_url):
|
def handle_redirect_request(self, client_redirect_url):
|
||||||
"""Handle an incoming request to /login/sso/redirect
|
"""Handle an incoming request to /login/sso/redirect
|
||||||
|
@ -60,7 +76,7 @@ class SamlHandler:
|
||||||
# this shouldn't happen!
|
# this shouldn't happen!
|
||||||
raise Exception("prepare_for_authenticate didn't return a Location header")
|
raise Exception("prepare_for_authenticate didn't return a Location header")
|
||||||
|
|
||||||
def handle_saml_response(self, request):
|
async def handle_saml_response(self, request):
|
||||||
"""Handle an incoming request to /_matrix/saml2/authn_response
|
"""Handle an incoming request to /_matrix/saml2/authn_response
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -77,6 +93,10 @@ class SamlHandler:
|
||||||
# the dict.
|
# the dict.
|
||||||
self.expire_sessions()
|
self.expire_sessions()
|
||||||
|
|
||||||
|
user_id = await self._map_saml_response_to_user(resp_bytes)
|
||||||
|
self._sso_auth_handler.complete_sso_login(user_id, request, relay_state)
|
||||||
|
|
||||||
|
async def _map_saml_response_to_user(self, resp_bytes):
|
||||||
try:
|
try:
|
||||||
saml2_auth = self._saml_client.parse_authn_request_response(
|
saml2_auth = self._saml_client.parse_authn_request_response(
|
||||||
resp_bytes,
|
resp_bytes,
|
||||||
|
@ -91,18 +111,88 @@ class SamlHandler:
|
||||||
logger.warning("SAML2 response was not signed")
|
logger.warning("SAML2 response was not signed")
|
||||||
raise SynapseError(400, "SAML2 response was not signed")
|
raise SynapseError(400, "SAML2 response was not signed")
|
||||||
|
|
||||||
if "uid" not in saml2_auth.ava:
|
logger.info("SAML2 response: %s", saml2_auth.origxml)
|
||||||
|
logger.info("SAML2 mapped attributes: %s", saml2_auth.ava)
|
||||||
|
|
||||||
|
try:
|
||||||
|
remote_user_id = saml2_auth.ava["uid"][0]
|
||||||
|
except KeyError:
|
||||||
logger.warning("SAML2 response lacks a 'uid' attestation")
|
logger.warning("SAML2 response lacks a 'uid' attestation")
|
||||||
raise SynapseError(400, "uid not in SAML2 response")
|
raise SynapseError(400, "uid not in SAML2 response")
|
||||||
|
|
||||||
|
try:
|
||||||
|
mxid_source = saml2_auth.ava[self._mxid_source_attribute][0]
|
||||||
|
except KeyError:
|
||||||
|
logger.warning(
|
||||||
|
"SAML2 response lacks a '%s' attestation", self._mxid_source_attribute
|
||||||
|
)
|
||||||
|
raise SynapseError(
|
||||||
|
400, "%s not in SAML2 response" % (self._mxid_source_attribute,)
|
||||||
|
)
|
||||||
|
|
||||||
self._outstanding_requests_dict.pop(saml2_auth.in_response_to, None)
|
self._outstanding_requests_dict.pop(saml2_auth.in_response_to, None)
|
||||||
|
|
||||||
username = saml2_auth.ava["uid"][0]
|
|
||||||
displayName = saml2_auth.ava.get("displayName", [None])[0]
|
displayName = saml2_auth.ava.get("displayName", [None])[0]
|
||||||
|
|
||||||
return self._sso_auth_handler.on_successful_auth(
|
with (await self._mapping_lock.queue(self._auth_provider_id)):
|
||||||
username, request, relay_state, user_display_name=displayName
|
# first of all, check if we already have a mapping for this user
|
||||||
|
logger.info(
|
||||||
|
"Looking for existing mapping for user %s:%s",
|
||||||
|
self._auth_provider_id,
|
||||||
|
remote_user_id,
|
||||||
)
|
)
|
||||||
|
registered_user_id = await self._datastore.get_user_by_external_id(
|
||||||
|
self._auth_provider_id, remote_user_id
|
||||||
|
)
|
||||||
|
if registered_user_id is not None:
|
||||||
|
logger.info("Found existing mapping %s", registered_user_id)
|
||||||
|
return registered_user_id
|
||||||
|
|
||||||
|
# backwards-compatibility hack: see if there is an existing user with a
|
||||||
|
# suitable mapping from the uid
|
||||||
|
if (
|
||||||
|
self._grandfathered_mxid_source_attribute
|
||||||
|
and self._grandfathered_mxid_source_attribute in saml2_auth.ava
|
||||||
|
):
|
||||||
|
attrval = saml2_auth.ava[self._grandfathered_mxid_source_attribute][0]
|
||||||
|
user_id = UserID(
|
||||||
|
map_username_to_mxid_localpart(attrval), self._hostname
|
||||||
|
).to_string()
|
||||||
|
logger.info(
|
||||||
|
"Looking for existing account based on mapped %s %s",
|
||||||
|
self._grandfathered_mxid_source_attribute,
|
||||||
|
user_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
users = await self._datastore.get_users_by_id_case_insensitive(user_id)
|
||||||
|
if users:
|
||||||
|
registered_user_id = list(users.keys())[0]
|
||||||
|
logger.info("Grandfathering mapping to %s", registered_user_id)
|
||||||
|
await self._datastore.record_user_external_id(
|
||||||
|
self._auth_provider_id, remote_user_id, registered_user_id
|
||||||
|
)
|
||||||
|
return registered_user_id
|
||||||
|
|
||||||
|
# figure out a new mxid for this user
|
||||||
|
base_mxid_localpart = self._mxid_mapper(mxid_source)
|
||||||
|
|
||||||
|
suffix = 0
|
||||||
|
while True:
|
||||||
|
localpart = base_mxid_localpart + (str(suffix) if suffix else "")
|
||||||
|
if not await self._datastore.get_users_by_id_case_insensitive(
|
||||||
|
UserID(localpart, self._hostname).to_string()
|
||||||
|
):
|
||||||
|
break
|
||||||
|
suffix += 1
|
||||||
|
logger.info("Allocating mxid for new user with localpart %s", localpart)
|
||||||
|
|
||||||
|
registered_user_id = await self._registration_handler.register_user(
|
||||||
|
localpart=localpart, default_display_name=displayName
|
||||||
|
)
|
||||||
|
await self._datastore.record_user_external_id(
|
||||||
|
self._auth_provider_id, remote_user_id, registered_user_id
|
||||||
|
)
|
||||||
|
return registered_user_id
|
||||||
|
|
||||||
def expire_sessions(self):
|
def expire_sessions(self):
|
||||||
expire_before = self._clock.time_msec() - self._saml2_session_lifetime
|
expire_before = self._clock.time_msec() - self._saml2_session_lifetime
|
||||||
|
|
22
synapse/handlers/ui_auth/__init__.py
Normal file
22
synapse/handlers/ui_auth/__init__.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""This module implements user-interactive auth verification.
|
||||||
|
|
||||||
|
TODO: move more stuff out of AuthHandler in here.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from synapse.handlers.ui_auth.checkers import INTERACTIVE_AUTH_CHECKERS # noqa: F401
|
247
synapse/handlers/ui_auth/checkers.py
Normal file
247
synapse/handlers/ui_auth/checkers.py
Normal file
|
@ -0,0 +1,247 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
|
||||||
|
from canonicaljson import json
|
||||||
|
|
||||||
|
from twisted.internet import defer
|
||||||
|
from twisted.web.client import PartialDownloadError
|
||||||
|
|
||||||
|
from synapse.api.constants import LoginType
|
||||||
|
from synapse.api.errors import Codes, LoginError, SynapseError
|
||||||
|
from synapse.config.emailconfig import ThreepidBehaviour
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class UserInteractiveAuthChecker:
|
||||||
|
"""Abstract base class for an interactive auth checker"""
|
||||||
|
|
||||||
|
def __init__(self, hs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def is_enabled(self):
|
||||||
|
"""Check if the configuration of the homeserver allows this checker to work
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if this login type is enabled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def check_auth(self, authdict, clientip):
|
||||||
|
"""Given the authentication dict from the client, attempt to check this step
|
||||||
|
|
||||||
|
Args:
|
||||||
|
authdict (dict): authentication dictionary from the client
|
||||||
|
clientip (str): The IP address of the client.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
SynapseError if authentication failed
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Deferred: the result of authentication (to pass back to the client?)
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class DummyAuthChecker(UserInteractiveAuthChecker):
|
||||||
|
AUTH_TYPE = LoginType.DUMMY
|
||||||
|
|
||||||
|
def is_enabled(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_auth(self, authdict, clientip):
|
||||||
|
return defer.succeed(True)
|
||||||
|
|
||||||
|
|
||||||
|
class TermsAuthChecker(UserInteractiveAuthChecker):
|
||||||
|
AUTH_TYPE = LoginType.TERMS
|
||||||
|
|
||||||
|
def is_enabled(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_auth(self, authdict, clientip):
|
||||||
|
return defer.succeed(True)
|
||||||
|
|
||||||
|
|
||||||
|
class RecaptchaAuthChecker(UserInteractiveAuthChecker):
|
||||||
|
AUTH_TYPE = LoginType.RECAPTCHA
|
||||||
|
|
||||||
|
def __init__(self, hs):
|
||||||
|
super().__init__(hs)
|
||||||
|
self._enabled = bool(hs.config.recaptcha_private_key)
|
||||||
|
self._http_client = hs.get_simple_http_client()
|
||||||
|
self._url = hs.config.recaptcha_siteverify_api
|
||||||
|
self._secret = hs.config.recaptcha_private_key
|
||||||
|
|
||||||
|
def is_enabled(self):
|
||||||
|
return self._enabled
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def check_auth(self, authdict, clientip):
|
||||||
|
try:
|
||||||
|
user_response = authdict["response"]
|
||||||
|
except KeyError:
|
||||||
|
# Client tried to provide captcha but didn't give the parameter:
|
||||||
|
# bad request.
|
||||||
|
raise LoginError(
|
||||||
|
400, "Captcha response is required", errcode=Codes.CAPTCHA_NEEDED
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Submitting recaptcha response %s with remoteip %s", user_response, clientip
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: get this from the homeserver rather than creating a new one for
|
||||||
|
# each request
|
||||||
|
try:
|
||||||
|
resp_body = yield self._http_client.post_urlencoded_get_json(
|
||||||
|
self._url,
|
||||||
|
args={
|
||||||
|
"secret": self._secret,
|
||||||
|
"response": user_response,
|
||||||
|
"remoteip": clientip,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except PartialDownloadError as pde:
|
||||||
|
# Twisted is silly
|
||||||
|
data = pde.response
|
||||||
|
resp_body = json.loads(data)
|
||||||
|
|
||||||
|
if "success" in resp_body:
|
||||||
|
# Note that we do NOT check the hostname here: we explicitly
|
||||||
|
# intend the CAPTCHA to be presented by whatever client the
|
||||||
|
# user is using, we just care that they have completed a CAPTCHA.
|
||||||
|
logger.info(
|
||||||
|
"%s reCAPTCHA from hostname %s",
|
||||||
|
"Successful" if resp_body["success"] else "Failed",
|
||||||
|
resp_body.get("hostname"),
|
||||||
|
)
|
||||||
|
if resp_body["success"]:
|
||||||
|
return True
|
||||||
|
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)
|
||||||
|
|
||||||
|
|
||||||
|
class _BaseThreepidAuthChecker:
|
||||||
|
def __init__(self, hs):
|
||||||
|
self.hs = hs
|
||||||
|
self.store = hs.get_datastore()
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _check_threepid(self, medium, authdict):
|
||||||
|
if "threepid_creds" not in authdict:
|
||||||
|
raise LoginError(400, "Missing threepid_creds", Codes.MISSING_PARAM)
|
||||||
|
|
||||||
|
threepid_creds = authdict["threepid_creds"]
|
||||||
|
|
||||||
|
identity_handler = self.hs.get_handlers().identity_handler
|
||||||
|
|
||||||
|
logger.info("Getting validated threepid. threepidcreds: %r", (threepid_creds,))
|
||||||
|
|
||||||
|
# msisdns are currently always ThreepidBehaviour.REMOTE
|
||||||
|
if medium == "msisdn":
|
||||||
|
if not self.hs.config.account_threepid_delegate_msisdn:
|
||||||
|
raise SynapseError(
|
||||||
|
400, "Phone number verification is not enabled on this homeserver"
|
||||||
|
)
|
||||||
|
threepid = yield identity_handler.threepid_from_creds(
|
||||||
|
self.hs.config.account_threepid_delegate_msisdn, threepid_creds
|
||||||
|
)
|
||||||
|
elif medium == "email":
|
||||||
|
if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE:
|
||||||
|
assert self.hs.config.account_threepid_delegate_email
|
||||||
|
threepid = yield identity_handler.threepid_from_creds(
|
||||||
|
self.hs.config.account_threepid_delegate_email, threepid_creds
|
||||||
|
)
|
||||||
|
elif self.hs.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
|
||||||
|
threepid = None
|
||||||
|
row = yield self.store.get_threepid_validation_session(
|
||||||
|
medium,
|
||||||
|
threepid_creds["client_secret"],
|
||||||
|
sid=threepid_creds["sid"],
|
||||||
|
validated=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if row:
|
||||||
|
threepid = {
|
||||||
|
"medium": row["medium"],
|
||||||
|
"address": row["address"],
|
||||||
|
"validated_at": row["validated_at"],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Valid threepid returned, delete from the db
|
||||||
|
yield self.store.delete_threepid_session(threepid_creds["sid"])
|
||||||
|
else:
|
||||||
|
raise SynapseError(
|
||||||
|
400, "Email address verification is not enabled on this homeserver"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# this can't happen!
|
||||||
|
raise AssertionError("Unrecognized threepid medium: %s" % (medium,))
|
||||||
|
|
||||||
|
if not threepid:
|
||||||
|
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)
|
||||||
|
|
||||||
|
if threepid["medium"] != medium:
|
||||||
|
raise LoginError(
|
||||||
|
401,
|
||||||
|
"Expecting threepid of type '%s', got '%s'"
|
||||||
|
% (medium, threepid["medium"]),
|
||||||
|
errcode=Codes.UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
|
||||||
|
threepid["threepid_creds"] = authdict["threepid_creds"]
|
||||||
|
|
||||||
|
return threepid
|
||||||
|
|
||||||
|
|
||||||
|
class EmailIdentityAuthChecker(UserInteractiveAuthChecker, _BaseThreepidAuthChecker):
|
||||||
|
AUTH_TYPE = LoginType.EMAIL_IDENTITY
|
||||||
|
|
||||||
|
def __init__(self, hs):
|
||||||
|
UserInteractiveAuthChecker.__init__(self, hs)
|
||||||
|
_BaseThreepidAuthChecker.__init__(self, hs)
|
||||||
|
|
||||||
|
def is_enabled(self):
|
||||||
|
return self.hs.config.threepid_behaviour_email in (
|
||||||
|
ThreepidBehaviour.REMOTE,
|
||||||
|
ThreepidBehaviour.LOCAL,
|
||||||
|
)
|
||||||
|
|
||||||
|
def check_auth(self, authdict, clientip):
|
||||||
|
return self._check_threepid("email", authdict)
|
||||||
|
|
||||||
|
|
||||||
|
class MsisdnAuthChecker(UserInteractiveAuthChecker, _BaseThreepidAuthChecker):
|
||||||
|
AUTH_TYPE = LoginType.MSISDN
|
||||||
|
|
||||||
|
def __init__(self, hs):
|
||||||
|
UserInteractiveAuthChecker.__init__(self, hs)
|
||||||
|
_BaseThreepidAuthChecker.__init__(self, hs)
|
||||||
|
|
||||||
|
def is_enabled(self):
|
||||||
|
return bool(self.hs.config.account_threepid_delegate_msisdn)
|
||||||
|
|
||||||
|
def check_auth(self, authdict, clientip):
|
||||||
|
return self._check_threepid("msisdn", authdict)
|
||||||
|
|
||||||
|
|
||||||
|
INTERACTIVE_AUTH_CHECKERS = [
|
||||||
|
DummyAuthChecker,
|
||||||
|
TermsAuthChecker,
|
||||||
|
RecaptchaAuthChecker,
|
||||||
|
EmailIdentityAuthChecker,
|
||||||
|
MsisdnAuthChecker,
|
||||||
|
]
|
||||||
|
"""A list of UserInteractiveAuthChecker classes"""
|
|
@ -29,6 +29,7 @@ from synapse.http.servlet import (
|
||||||
parse_json_object_from_request,
|
parse_json_object_from_request,
|
||||||
parse_string,
|
parse_string,
|
||||||
)
|
)
|
||||||
|
from synapse.http.site import SynapseRequest
|
||||||
from synapse.rest.client.v2_alpha._base import client_patterns
|
from synapse.rest.client.v2_alpha._base import client_patterns
|
||||||
from synapse.rest.well_known import WellKnownBuilder
|
from synapse.rest.well_known import WellKnownBuilder
|
||||||
from synapse.types import UserID, map_username_to_mxid_localpart
|
from synapse.types import UserID, map_username_to_mxid_localpart
|
||||||
|
@ -507,6 +508,19 @@ class SSOAuthHandler(object):
|
||||||
localpart=localpart, default_display_name=user_display_name
|
localpart=localpart, default_display_name=user_display_name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.complete_sso_login(registered_user_id, request, client_redirect_url)
|
||||||
|
|
||||||
|
def complete_sso_login(
|
||||||
|
self, registered_user_id: str, request: SynapseRequest, client_redirect_url: str
|
||||||
|
):
|
||||||
|
"""Having figured out a mxid for this user, complete the HTTP request
|
||||||
|
|
||||||
|
Args:
|
||||||
|
registered_user_id:
|
||||||
|
request:
|
||||||
|
client_redirect_url:
|
||||||
|
"""
|
||||||
|
|
||||||
login_token = self._macaroon_gen.generate_short_term_login_token(
|
login_token = self._macaroon_gen.generate_short_term_login_token(
|
||||||
registered_user_id
|
registered_user_id
|
||||||
)
|
)
|
||||||
|
|
|
@ -21,12 +21,7 @@ from six.moves import http_client
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.api.constants import LoginType
|
from synapse.api.constants import LoginType
|
||||||
from synapse.api.errors import (
|
from synapse.api.errors import Codes, SynapseError, ThreepidValidationError
|
||||||
Codes,
|
|
||||||
HttpResponseException,
|
|
||||||
SynapseError,
|
|
||||||
ThreepidValidationError,
|
|
||||||
)
|
|
||||||
from synapse.config.emailconfig import ThreepidBehaviour
|
from synapse.config.emailconfig import ThreepidBehaviour
|
||||||
from synapse.http.server import finish_request
|
from synapse.http.server import finish_request
|
||||||
from synapse.http.servlet import (
|
from synapse.http.servlet import (
|
||||||
|
@ -485,10 +480,8 @@ class MsisdnThreepidRequestTokenRestServlet(RestServlet):
|
||||||
def on_POST(self, request):
|
def on_POST(self, request):
|
||||||
body = parse_json_object_from_request(request)
|
body = parse_json_object_from_request(request)
|
||||||
assert_params_in_dict(
|
assert_params_in_dict(
|
||||||
body,
|
body, ["client_secret", "country", "phone_number", "send_attempt"]
|
||||||
["id_server", "client_secret", "country", "phone_number", "send_attempt"],
|
|
||||||
)
|
)
|
||||||
id_server = "https://" + body["id_server"] # Assume https
|
|
||||||
client_secret = body["client_secret"]
|
client_secret = body["client_secret"]
|
||||||
country = body["country"]
|
country = body["country"]
|
||||||
phone_number = body["phone_number"]
|
phone_number = body["phone_number"]
|
||||||
|
@ -509,14 +502,29 @@ class MsisdnThreepidRequestTokenRestServlet(RestServlet):
|
||||||
if existing_user_id is not None:
|
if existing_user_id is not None:
|
||||||
raise SynapseError(400, "MSISDN is already in use", Codes.THREEPID_IN_USE)
|
raise SynapseError(400, "MSISDN is already in use", Codes.THREEPID_IN_USE)
|
||||||
|
|
||||||
|
if not self.hs.config.account_threepid_delegate_msisdn:
|
||||||
|
logger.warn(
|
||||||
|
"No upstream msisdn account_threepid_delegate configured on the server to "
|
||||||
|
"handle this request"
|
||||||
|
)
|
||||||
|
raise SynapseError(
|
||||||
|
400,
|
||||||
|
"Adding phone numbers to user account is not supported by this homeserver",
|
||||||
|
)
|
||||||
|
|
||||||
ret = yield self.identity_handler.requestMsisdnToken(
|
ret = yield self.identity_handler.requestMsisdnToken(
|
||||||
id_server, country, phone_number, client_secret, send_attempt, next_link
|
self.hs.config.account_threepid_delegate_msisdn,
|
||||||
|
country,
|
||||||
|
phone_number,
|
||||||
|
client_secret,
|
||||||
|
send_attempt,
|
||||||
|
next_link,
|
||||||
)
|
)
|
||||||
|
|
||||||
return 200, ret
|
return 200, ret
|
||||||
|
|
||||||
|
|
||||||
class AddThreepidSubmitTokenServlet(RestServlet):
|
class AddThreepidEmailSubmitTokenServlet(RestServlet):
|
||||||
"""Handles 3PID validation token submission for adding an email to a user's account"""
|
"""Handles 3PID validation token submission for adding an email to a user's account"""
|
||||||
|
|
||||||
PATTERNS = client_patterns(
|
PATTERNS = client_patterns(
|
||||||
|
@ -592,6 +600,48 @@ class AddThreepidSubmitTokenServlet(RestServlet):
|
||||||
finish_request(request)
|
finish_request(request)
|
||||||
|
|
||||||
|
|
||||||
|
class AddThreepidMsisdnSubmitTokenServlet(RestServlet):
|
||||||
|
"""Handles 3PID validation token submission for adding a phone number to a user's
|
||||||
|
account
|
||||||
|
"""
|
||||||
|
|
||||||
|
PATTERNS = client_patterns(
|
||||||
|
"/add_threepid/msisdn/submit_token$", releases=(), unstable=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, hs):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
hs (synapse.server.HomeServer): server
|
||||||
|
"""
|
||||||
|
super().__init__()
|
||||||
|
self.config = hs.config
|
||||||
|
self.clock = hs.get_clock()
|
||||||
|
self.store = hs.get_datastore()
|
||||||
|
self.identity_handler = hs.get_handlers().identity_handler
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_POST(self, request):
|
||||||
|
if not self.config.account_threepid_delegate_msisdn:
|
||||||
|
raise SynapseError(
|
||||||
|
400,
|
||||||
|
"This homeserver is not validating phone numbers. Use an identity server "
|
||||||
|
"instead.",
|
||||||
|
)
|
||||||
|
|
||||||
|
body = parse_json_object_from_request(request)
|
||||||
|
assert_params_in_dict(body, ["client_secret", "sid", "token"])
|
||||||
|
|
||||||
|
# Proxy submit_token request to msisdn threepid delegate
|
||||||
|
response = yield self.identity_handler.proxy_msisdn_submit_token(
|
||||||
|
self.config.account_threepid_delegate_msisdn,
|
||||||
|
body["client_secret"],
|
||||||
|
body["sid"],
|
||||||
|
body["token"],
|
||||||
|
)
|
||||||
|
return 200, response
|
||||||
|
|
||||||
|
|
||||||
class ThreepidRestServlet(RestServlet):
|
class ThreepidRestServlet(RestServlet):
|
||||||
PATTERNS = client_patterns("/account/3pid$")
|
PATTERNS = client_patterns("/account/3pid$")
|
||||||
|
|
||||||
|
@ -627,80 +677,87 @@ class ThreepidRestServlet(RestServlet):
|
||||||
client_secret = threepid_creds["client_secret"]
|
client_secret = threepid_creds["client_secret"]
|
||||||
sid = threepid_creds["sid"]
|
sid = threepid_creds["sid"]
|
||||||
|
|
||||||
# We don't actually know which medium this 3PID is. Thus we first assume it's email,
|
validation_session = yield self.identity_handler.validate_threepid_session(
|
||||||
# and if validation fails we try msisdn
|
client_secret, sid
|
||||||
validation_session = None
|
|
||||||
|
|
||||||
# Try to validate as email
|
|
||||||
if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE:
|
|
||||||
# Ask our delegated email identity server
|
|
||||||
try:
|
|
||||||
validation_session = yield self.identity_handler.threepid_from_creds(
|
|
||||||
self.hs.config.account_threepid_delegate_email, threepid_creds
|
|
||||||
)
|
)
|
||||||
except HttpResponseException:
|
if validation_session:
|
||||||
logger.debug(
|
|
||||||
"%s reported non-validated threepid: %s",
|
|
||||||
self.hs.config.account_threepid_delegate_email,
|
|
||||||
threepid_creds,
|
|
||||||
)
|
|
||||||
elif self.hs.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
|
|
||||||
# Get a validated session matching these details
|
|
||||||
validation_session = yield self.datastore.get_threepid_validation_session(
|
|
||||||
"email", client_secret, sid=sid, validated=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Old versions of Sydent return a 200 http code even on a failed validation check.
|
|
||||||
# Thus, in addition to the HttpResponseException check above (which checks for
|
|
||||||
# non-200 errors), we need to make sure validation_session isn't actually an error,
|
|
||||||
# identified by containing an "error" key
|
|
||||||
# See https://github.com/matrix-org/sydent/issues/215 for details
|
|
||||||
if validation_session and "error" not in validation_session:
|
|
||||||
yield self._add_threepid_to_account(user_id, validation_session)
|
|
||||||
return 200, {}
|
|
||||||
|
|
||||||
# Try to validate as msisdn
|
|
||||||
if self.hs.config.account_threepid_delegate_msisdn:
|
|
||||||
# Ask our delegated msisdn identity server
|
|
||||||
try:
|
|
||||||
validation_session = yield self.identity_handler.threepid_from_creds(
|
|
||||||
self.hs.config.account_threepid_delegate_msisdn, threepid_creds
|
|
||||||
)
|
|
||||||
except HttpResponseException:
|
|
||||||
logger.debug(
|
|
||||||
"%s reported non-validated threepid: %s",
|
|
||||||
self.hs.config.account_threepid_delegate_email,
|
|
||||||
threepid_creds,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check that validation_session isn't actually an error due to old Sydent instances
|
|
||||||
# See explanatory comment above
|
|
||||||
if validation_session and "error" not in validation_session:
|
|
||||||
yield self._add_threepid_to_account(user_id, validation_session)
|
|
||||||
return 200, {}
|
|
||||||
|
|
||||||
raise SynapseError(
|
|
||||||
400, "No validated 3pid session found", Codes.THREEPID_AUTH_FAILED
|
|
||||||
)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def _add_threepid_to_account(self, user_id, validation_session):
|
|
||||||
"""Add a threepid wrapped in a validation_session dict to an account
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_id (str): The mxid of the user to add this 3PID to
|
|
||||||
|
|
||||||
validation_session (dict): A dict containing the following:
|
|
||||||
* medium - medium of the threepid
|
|
||||||
* address - address of the threepid
|
|
||||||
* validated_at - timestamp of when the validation occurred
|
|
||||||
"""
|
|
||||||
yield self.auth_handler.add_threepid(
|
yield self.auth_handler.add_threepid(
|
||||||
user_id,
|
user_id,
|
||||||
validation_session["medium"],
|
validation_session["medium"],
|
||||||
validation_session["address"],
|
validation_session["address"],
|
||||||
validation_session["validated_at"],
|
validation_session["validated_at"],
|
||||||
)
|
)
|
||||||
|
return 200, {}
|
||||||
|
|
||||||
|
raise SynapseError(
|
||||||
|
400, "No validated 3pid session found", Codes.THREEPID_AUTH_FAILED
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ThreepidAddRestServlet(RestServlet):
|
||||||
|
PATTERNS = client_patterns("/account/3pid/add$", releases=(), unstable=True)
|
||||||
|
|
||||||
|
def __init__(self, hs):
|
||||||
|
super(ThreepidAddRestServlet, self).__init__()
|
||||||
|
self.hs = hs
|
||||||
|
self.identity_handler = hs.get_handlers().identity_handler
|
||||||
|
self.auth = hs.get_auth()
|
||||||
|
self.auth_handler = hs.get_auth_handler()
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_POST(self, request):
|
||||||
|
requester = yield self.auth.get_user_by_req(request)
|
||||||
|
user_id = requester.user.to_string()
|
||||||
|
body = parse_json_object_from_request(request)
|
||||||
|
|
||||||
|
assert_params_in_dict(body, ["client_secret", "sid"])
|
||||||
|
client_secret = body["client_secret"]
|
||||||
|
sid = body["sid"]
|
||||||
|
|
||||||
|
validation_session = yield self.identity_handler.validate_threepid_session(
|
||||||
|
client_secret, sid
|
||||||
|
)
|
||||||
|
if validation_session:
|
||||||
|
yield self.auth_handler.add_threepid(
|
||||||
|
user_id,
|
||||||
|
validation_session["medium"],
|
||||||
|
validation_session["address"],
|
||||||
|
validation_session["validated_at"],
|
||||||
|
)
|
||||||
|
return 200, {}
|
||||||
|
|
||||||
|
raise SynapseError(
|
||||||
|
400, "No validated 3pid session found", Codes.THREEPID_AUTH_FAILED
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ThreepidBindRestServlet(RestServlet):
|
||||||
|
PATTERNS = client_patterns("/account/3pid/bind$", releases=(), unstable=True)
|
||||||
|
|
||||||
|
def __init__(self, hs):
|
||||||
|
super(ThreepidBindRestServlet, self).__init__()
|
||||||
|
self.hs = hs
|
||||||
|
self.identity_handler = hs.get_handlers().identity_handler
|
||||||
|
self.auth = hs.get_auth()
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_POST(self, request):
|
||||||
|
body = parse_json_object_from_request(request)
|
||||||
|
|
||||||
|
assert_params_in_dict(body, ["id_server", "sid", "client_secret"])
|
||||||
|
id_server = body["id_server"]
|
||||||
|
sid = body["sid"]
|
||||||
|
client_secret = body["client_secret"]
|
||||||
|
id_access_token = body.get("id_access_token") # optional
|
||||||
|
|
||||||
|
requester = yield self.auth.get_user_by_req(request)
|
||||||
|
user_id = requester.user.to_string()
|
||||||
|
|
||||||
|
yield self.identity_handler.bind_threepid(
|
||||||
|
client_secret, sid, user_id, id_server, id_access_token
|
||||||
|
)
|
||||||
|
|
||||||
|
return 200, {}
|
||||||
|
|
||||||
|
|
||||||
class ThreepidUnbindRestServlet(RestServlet):
|
class ThreepidUnbindRestServlet(RestServlet):
|
||||||
|
@ -792,8 +849,11 @@ def register_servlets(hs, http_server):
|
||||||
DeactivateAccountRestServlet(hs).register(http_server)
|
DeactivateAccountRestServlet(hs).register(http_server)
|
||||||
EmailThreepidRequestTokenRestServlet(hs).register(http_server)
|
EmailThreepidRequestTokenRestServlet(hs).register(http_server)
|
||||||
MsisdnThreepidRequestTokenRestServlet(hs).register(http_server)
|
MsisdnThreepidRequestTokenRestServlet(hs).register(http_server)
|
||||||
AddThreepidSubmitTokenServlet(hs).register(http_server)
|
AddThreepidEmailSubmitTokenServlet(hs).register(http_server)
|
||||||
|
AddThreepidMsisdnSubmitTokenServlet(hs).register(http_server)
|
||||||
ThreepidRestServlet(hs).register(http_server)
|
ThreepidRestServlet(hs).register(http_server)
|
||||||
|
ThreepidAddRestServlet(hs).register(http_server)
|
||||||
|
ThreepidBindRestServlet(hs).register(http_server)
|
||||||
ThreepidUnbindRestServlet(hs).register(http_server)
|
ThreepidUnbindRestServlet(hs).register(http_server)
|
||||||
ThreepidDeleteRestServlet(hs).register(http_server)
|
ThreepidDeleteRestServlet(hs).register(http_server)
|
||||||
WhoamiRestServlet(hs).register(http_server)
|
WhoamiRestServlet(hs).register(http_server)
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
import hmac
|
import hmac
|
||||||
import logging
|
import logging
|
||||||
|
from typing import List, Union
|
||||||
|
|
||||||
from six import string_types
|
from six import string_types
|
||||||
|
|
||||||
|
@ -31,9 +32,14 @@ from synapse.api.errors import (
|
||||||
ThreepidValidationError,
|
ThreepidValidationError,
|
||||||
UnrecognizedRequestError,
|
UnrecognizedRequestError,
|
||||||
)
|
)
|
||||||
|
from synapse.config import ConfigError
|
||||||
|
from synapse.config.captcha import CaptchaConfig
|
||||||
|
from synapse.config.consent_config import ConsentConfig
|
||||||
from synapse.config.emailconfig import ThreepidBehaviour
|
from synapse.config.emailconfig import ThreepidBehaviour
|
||||||
from synapse.config.ratelimiting import FederationRateLimitConfig
|
from synapse.config.ratelimiting import FederationRateLimitConfig
|
||||||
|
from synapse.config.registration import RegistrationConfig
|
||||||
from synapse.config.server import is_threepid_reserved
|
from synapse.config.server import is_threepid_reserved
|
||||||
|
from synapse.handlers.auth import AuthHandler
|
||||||
from synapse.http.server import finish_request
|
from synapse.http.server import finish_request
|
||||||
from synapse.http.servlet import (
|
from synapse.http.servlet import (
|
||||||
RestServlet,
|
RestServlet,
|
||||||
|
@ -246,6 +252,12 @@ class RegistrationSubmitTokenServlet(RestServlet):
|
||||||
[self.config.email_registration_template_failure_html],
|
[self.config.email_registration_template_failure_html],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
|
||||||
|
self.failure_email_template, = load_jinja2_templates(
|
||||||
|
self.config.email_template_dir,
|
||||||
|
[self.config.email_registration_template_failure_html],
|
||||||
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def on_GET(self, request, medium):
|
def on_GET(self, request, medium):
|
||||||
if medium != "email":
|
if medium != "email":
|
||||||
|
@ -365,6 +377,10 @@ class RegisterRestServlet(RestServlet):
|
||||||
self.ratelimiter = hs.get_registration_ratelimiter()
|
self.ratelimiter = hs.get_registration_ratelimiter()
|
||||||
self.clock = hs.get_clock()
|
self.clock = hs.get_clock()
|
||||||
|
|
||||||
|
self._registration_flows = _calculate_registration_flows(
|
||||||
|
hs.config, self.auth_handler
|
||||||
|
)
|
||||||
|
|
||||||
@interactive_auth_handler
|
@interactive_auth_handler
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def on_POST(self, request):
|
def on_POST(self, request):
|
||||||
|
@ -485,69 +501,8 @@ class RegisterRestServlet(RestServlet):
|
||||||
assigned_user_id=registered_user_id,
|
assigned_user_id=registered_user_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# FIXME: need a better error than "no auth flow found" for scenarios
|
|
||||||
# where we required 3PID for registration but the user didn't give one
|
|
||||||
require_email = "email" in self.hs.config.registrations_require_3pid
|
|
||||||
require_msisdn = "msisdn" in self.hs.config.registrations_require_3pid
|
|
||||||
|
|
||||||
show_msisdn = True
|
|
||||||
if self.hs.config.disable_msisdn_registration:
|
|
||||||
show_msisdn = False
|
|
||||||
require_msisdn = False
|
|
||||||
|
|
||||||
flows = []
|
|
||||||
if self.hs.config.enable_registration_captcha:
|
|
||||||
# only support 3PIDless registration if no 3PIDs are required
|
|
||||||
if not require_email and not require_msisdn:
|
|
||||||
# Also add a dummy flow here, otherwise if a client completes
|
|
||||||
# recaptcha first we'll assume they were going for this flow
|
|
||||||
# and complete the request, when they could have been trying to
|
|
||||||
# complete one of the flows with email/msisdn auth.
|
|
||||||
flows.extend([[LoginType.RECAPTCHA, LoginType.DUMMY]])
|
|
||||||
# only support the email-only flow if we don't require MSISDN 3PIDs
|
|
||||||
if not require_msisdn:
|
|
||||||
flows.extend([[LoginType.RECAPTCHA, LoginType.EMAIL_IDENTITY]])
|
|
||||||
|
|
||||||
if show_msisdn:
|
|
||||||
# only support the MSISDN-only flow if we don't require email 3PIDs
|
|
||||||
if not require_email:
|
|
||||||
flows.extend([[LoginType.RECAPTCHA, LoginType.MSISDN]])
|
|
||||||
# always let users provide both MSISDN & email
|
|
||||||
flows.extend(
|
|
||||||
[[LoginType.RECAPTCHA, LoginType.MSISDN, LoginType.EMAIL_IDENTITY]]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# only support 3PIDless registration if no 3PIDs are required
|
|
||||||
if not require_email and not require_msisdn:
|
|
||||||
flows.extend([[LoginType.DUMMY]])
|
|
||||||
# only support the email-only flow if we don't require MSISDN 3PIDs
|
|
||||||
if not require_msisdn:
|
|
||||||
flows.extend([[LoginType.EMAIL_IDENTITY]])
|
|
||||||
|
|
||||||
if show_msisdn:
|
|
||||||
# only support the MSISDN-only flow if we don't require email 3PIDs
|
|
||||||
if not require_email or require_msisdn:
|
|
||||||
flows.extend([[LoginType.MSISDN]])
|
|
||||||
# always let users provide both MSISDN & email
|
|
||||||
flows.extend([[LoginType.MSISDN, LoginType.EMAIL_IDENTITY]])
|
|
||||||
|
|
||||||
# Append m.login.terms to all flows if we're requiring consent
|
|
||||||
if self.hs.config.user_consent_at_registration:
|
|
||||||
new_flows = []
|
|
||||||
for flow in flows:
|
|
||||||
inserted = False
|
|
||||||
# m.login.terms should go near the end but before msisdn or email auth
|
|
||||||
for i, stage in enumerate(flow):
|
|
||||||
if stage == LoginType.EMAIL_IDENTITY or stage == LoginType.MSISDN:
|
|
||||||
flow.insert(i, LoginType.TERMS)
|
|
||||||
inserted = True
|
|
||||||
break
|
|
||||||
if not inserted:
|
|
||||||
flow.append(LoginType.TERMS)
|
|
||||||
flows.extend(new_flows)
|
|
||||||
|
|
||||||
auth_result, params, session_id = yield self.auth_handler.check_auth(
|
auth_result, params, session_id = yield self.auth_handler.check_auth(
|
||||||
flows, body, self.hs.get_ip_from_request(request)
|
self._registration_flows, body, self.hs.get_ip_from_request(request)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check that we're not trying to register a denied 3pid.
|
# Check that we're not trying to register a denied 3pid.
|
||||||
|
@ -710,6 +665,83 @@ class RegisterRestServlet(RestServlet):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _calculate_registration_flows(
|
||||||
|
# technically `config` has to provide *all* of these interfaces, not just one
|
||||||
|
config: Union[RegistrationConfig, ConsentConfig, CaptchaConfig],
|
||||||
|
auth_handler: AuthHandler,
|
||||||
|
) -> List[List[str]]:
|
||||||
|
"""Get a suitable flows list for registration
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: server configuration
|
||||||
|
auth_handler: authorization handler
|
||||||
|
|
||||||
|
Returns: a list of supported flows
|
||||||
|
"""
|
||||||
|
# FIXME: need a better error than "no auth flow found" for scenarios
|
||||||
|
# where we required 3PID for registration but the user didn't give one
|
||||||
|
require_email = "email" in config.registrations_require_3pid
|
||||||
|
require_msisdn = "msisdn" in config.registrations_require_3pid
|
||||||
|
|
||||||
|
show_msisdn = True
|
||||||
|
show_email = True
|
||||||
|
|
||||||
|
if config.disable_msisdn_registration:
|
||||||
|
show_msisdn = False
|
||||||
|
require_msisdn = False
|
||||||
|
|
||||||
|
enabled_auth_types = auth_handler.get_enabled_auth_types()
|
||||||
|
if LoginType.EMAIL_IDENTITY not in enabled_auth_types:
|
||||||
|
show_email = False
|
||||||
|
if require_email:
|
||||||
|
raise ConfigError(
|
||||||
|
"Configuration requires email address at registration, but email "
|
||||||
|
"validation is not configured"
|
||||||
|
)
|
||||||
|
|
||||||
|
if LoginType.MSISDN not in enabled_auth_types:
|
||||||
|
show_msisdn = False
|
||||||
|
if require_msisdn:
|
||||||
|
raise ConfigError(
|
||||||
|
"Configuration requires msisdn at registration, but msisdn "
|
||||||
|
"validation is not configured"
|
||||||
|
)
|
||||||
|
|
||||||
|
flows = []
|
||||||
|
|
||||||
|
# only support 3PIDless registration if no 3PIDs are required
|
||||||
|
if not require_email and not require_msisdn:
|
||||||
|
# Add a dummy step here, otherwise if a client completes
|
||||||
|
# recaptcha first we'll assume they were going for this flow
|
||||||
|
# and complete the request, when they could have been trying to
|
||||||
|
# complete one of the flows with email/msisdn auth.
|
||||||
|
flows.append([LoginType.DUMMY])
|
||||||
|
|
||||||
|
# only support the email-only flow if we don't require MSISDN 3PIDs
|
||||||
|
if show_email and not require_msisdn:
|
||||||
|
flows.append([LoginType.EMAIL_IDENTITY])
|
||||||
|
|
||||||
|
# only support the MSISDN-only flow if we don't require email 3PIDs
|
||||||
|
if show_msisdn and not require_email:
|
||||||
|
flows.append([LoginType.MSISDN])
|
||||||
|
|
||||||
|
if show_email and show_msisdn:
|
||||||
|
# always let users provide both MSISDN & email
|
||||||
|
flows.append([LoginType.MSISDN, LoginType.EMAIL_IDENTITY])
|
||||||
|
|
||||||
|
# Prepend m.login.terms to all flows if we're requiring consent
|
||||||
|
if config.user_consent_at_registration:
|
||||||
|
for flow in flows:
|
||||||
|
flow.insert(0, LoginType.TERMS)
|
||||||
|
|
||||||
|
# Prepend recaptcha to all flows if we're requiring captcha
|
||||||
|
if config.enable_registration_captcha:
|
||||||
|
for flow in flows:
|
||||||
|
flow.insert(0, LoginType.RECAPTCHA)
|
||||||
|
|
||||||
|
return flows
|
||||||
|
|
||||||
|
|
||||||
def register_servlets(hs, http_server):
|
def register_servlets(hs, http_server):
|
||||||
EmailRegisterRequestTokenRestServlet(hs).register(http_server)
|
EmailRegisterRequestTokenRestServlet(hs).register(http_server)
|
||||||
MsisdnRegisterRequestTokenRestServlet(hs).register(http_server)
|
MsisdnRegisterRequestTokenRestServlet(hs).register(http_server)
|
||||||
|
|
|
@ -48,7 +48,24 @@ class VersionsRestServlet(RestServlet):
|
||||||
"r0.5.0",
|
"r0.5.0",
|
||||||
],
|
],
|
||||||
# as per MSC1497:
|
# as per MSC1497:
|
||||||
"unstable_features": {"m.lazy_load_members": True},
|
"unstable_features": {
|
||||||
|
"m.lazy_load_members": True,
|
||||||
|
# as per MSC2190, as amended by MSC2264
|
||||||
|
# to be removed in r0.6.0
|
||||||
|
"m.id_access_token": True,
|
||||||
|
# Advertise to clients that they need not include an `id_server`
|
||||||
|
# parameter during registration or password reset, as Synapse now decides
|
||||||
|
# itself which identity server to use (or none at all).
|
||||||
|
#
|
||||||
|
# This is also used by a client when they wish to bind a 3PID to their
|
||||||
|
# account, but not bind it to an identity server, the endpoint for which
|
||||||
|
# also requires `id_server`. If the homeserver is handling 3PID
|
||||||
|
# verification itself, there is no need to ask the user for `id_server` to
|
||||||
|
# be supplied.
|
||||||
|
"m.require_identity_server": False,
|
||||||
|
# as per MSC2290
|
||||||
|
"m.separate_add_and_bind": True,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -238,7 +238,7 @@ class BackgroundUpdateStore(SQLBaseStore):
|
||||||
duration_ms = time_stop - time_start
|
duration_ms = time_stop - time_start
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Updating %r. Updated %r items in %rms."
|
"Running background update %r. Processed %r items in %rms."
|
||||||
" (total_rate=%r/ms, current_rate=%r/ms, total_updated=%r, batch_size=%r)",
|
" (total_rate=%r/ms, current_rate=%r/ms, total_updated=%r, batch_size=%r)",
|
||||||
update_name,
|
update_name,
|
||||||
items_updated,
|
items_updated,
|
||||||
|
|
|
@ -397,7 +397,7 @@ class ClientIpStore(background_updates.BackgroundUpdateStore):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
keyvalues = {"user_id": user_id}
|
keyvalues = {"user_id": user_id}
|
||||||
if device_id:
|
if device_id is not None:
|
||||||
keyvalues["device_id"] = device_id
|
keyvalues["device_id"] = device_id
|
||||||
|
|
||||||
res = yield self._simple_select_list(
|
res = yield self._simple_select_list(
|
||||||
|
|
|
@ -22,6 +22,7 @@ from six import iterkeys
|
||||||
from six.moves import range
|
from six.moves import range
|
||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
from twisted.internet.defer import Deferred
|
||||||
|
|
||||||
from synapse.api.constants import UserTypes
|
from synapse.api.constants import UserTypes
|
||||||
from synapse.api.errors import Codes, StoreError, SynapseError, ThreepidValidationError
|
from synapse.api.errors import Codes, StoreError, SynapseError, ThreepidValidationError
|
||||||
|
@ -384,6 +385,26 @@ class RegistrationWorkerStore(SQLBaseStore):
|
||||||
|
|
||||||
return self.runInteraction("get_users_by_id_case_insensitive", f)
|
return self.runInteraction("get_users_by_id_case_insensitive", f)
|
||||||
|
|
||||||
|
async def get_user_by_external_id(
|
||||||
|
self, auth_provider: str, external_id: str
|
||||||
|
) -> str:
|
||||||
|
"""Look up a user by their external auth id
|
||||||
|
|
||||||
|
Args:
|
||||||
|
auth_provider: identifier for the remote auth provider
|
||||||
|
external_id: id on that system
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str|None: the mxid of the user, or None if they are not known
|
||||||
|
"""
|
||||||
|
return await self._simple_select_one_onecol(
|
||||||
|
table="user_external_ids",
|
||||||
|
keyvalues={"auth_provider": auth_provider, "external_id": external_id},
|
||||||
|
retcol="user_id",
|
||||||
|
allow_none=True,
|
||||||
|
desc="get_user_by_external_id",
|
||||||
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def count_all_users(self):
|
def count_all_users(self):
|
||||||
"""Counts all users registered on the homeserver."""
|
"""Counts all users registered on the homeserver."""
|
||||||
|
@ -495,7 +516,7 @@ class RegistrationWorkerStore(SQLBaseStore):
|
||||||
)
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_user_id_by_threepid(self, medium, address, require_verified=False):
|
def get_user_id_by_threepid(self, medium, address):
|
||||||
"""Returns user id from threepid
|
"""Returns user id from threepid
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -586,6 +607,26 @@ class RegistrationWorkerStore(SQLBaseStore):
|
||||||
desc="add_user_bound_threepid",
|
desc="add_user_bound_threepid",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def user_get_bound_threepids(self, user_id):
|
||||||
|
"""Get the threepids that a user has bound to an identity server through the homeserver
|
||||||
|
The homeserver remembers where binds to an identity server occurred. Using this
|
||||||
|
method can retrieve those threepids.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id (str): The ID of the user to retrieve threepids for
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Deferred[list[dict]]: List of dictionaries containing the following:
|
||||||
|
medium (str): The medium of the threepid (e.g "email")
|
||||||
|
address (str): The address of the threepid (e.g "bob@example.com")
|
||||||
|
"""
|
||||||
|
return self._simple_select_list(
|
||||||
|
table="user_threepid_id_server",
|
||||||
|
keyvalues={"user_id": user_id},
|
||||||
|
retcols=["medium", "address"],
|
||||||
|
desc="user_get_bound_threepids",
|
||||||
|
)
|
||||||
|
|
||||||
def remove_user_bound_threepid(self, user_id, medium, address, id_server):
|
def remove_user_bound_threepid(self, user_id, medium, address, id_server):
|
||||||
"""The server proxied an unbind request to the given identity server on
|
"""The server proxied an unbind request to the given identity server on
|
||||||
behalf of the given user, so we remove the mapping of threepid to
|
behalf of the given user, so we remove the mapping of threepid to
|
||||||
|
@ -655,7 +696,7 @@ class RegistrationWorkerStore(SQLBaseStore):
|
||||||
self, medium, client_secret, address=None, sid=None, validated=True
|
self, medium, client_secret, address=None, sid=None, validated=True
|
||||||
):
|
):
|
||||||
"""Gets a session_id and last_send_attempt (if available) for a
|
"""Gets a session_id and last_send_attempt (if available) for a
|
||||||
client_secret/medium/(address|session_id) combo
|
combination of validation metadata
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
medium (str|None): The medium of the 3PID
|
medium (str|None): The medium of the 3PID
|
||||||
|
@ -824,7 +865,7 @@ class RegistrationStore(
|
||||||
rows = self.cursor_to_dict(txn)
|
rows = self.cursor_to_dict(txn)
|
||||||
|
|
||||||
if not rows:
|
if not rows:
|
||||||
return True
|
return True, 0
|
||||||
|
|
||||||
rows_processed_nb = 0
|
rows_processed_nb = 0
|
||||||
|
|
||||||
|
@ -840,18 +881,18 @@ class RegistrationStore(
|
||||||
)
|
)
|
||||||
|
|
||||||
if batch_size > len(rows):
|
if batch_size > len(rows):
|
||||||
return True
|
return True, len(rows)
|
||||||
else:
|
else:
|
||||||
return False
|
return False, len(rows)
|
||||||
|
|
||||||
end = yield self.runInteraction(
|
end, nb_processed = yield self.runInteraction(
|
||||||
"users_set_deactivated_flag", _background_update_set_deactivated_flag_txn
|
"users_set_deactivated_flag", _background_update_set_deactivated_flag_txn
|
||||||
)
|
)
|
||||||
|
|
||||||
if end:
|
if end:
|
||||||
yield self._end_background_update("users_set_deactivated_flag")
|
yield self._end_background_update("users_set_deactivated_flag")
|
||||||
|
|
||||||
return batch_size
|
return nb_processed
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def add_access_token_to_user(self, user_id, token, device_id, valid_until_ms):
|
def add_access_token_to_user(self, user_id, token, device_id, valid_until_ms):
|
||||||
|
@ -1012,6 +1053,26 @@ class RegistrationStore(
|
||||||
self._invalidate_cache_and_stream(txn, self.get_user_by_id, (user_id,))
|
self._invalidate_cache_and_stream(txn, self.get_user_by_id, (user_id,))
|
||||||
txn.call_after(self.is_guest.invalidate, (user_id,))
|
txn.call_after(self.is_guest.invalidate, (user_id,))
|
||||||
|
|
||||||
|
def record_user_external_id(
|
||||||
|
self, auth_provider: str, external_id: str, user_id: str
|
||||||
|
) -> Deferred:
|
||||||
|
"""Record a mapping from an external user id to a mxid
|
||||||
|
|
||||||
|
Args:
|
||||||
|
auth_provider: identifier for the remote auth provider
|
||||||
|
external_id: id on that system
|
||||||
|
user_id: complete mxid that it is mapped to
|
||||||
|
"""
|
||||||
|
return self._simple_insert(
|
||||||
|
table="user_external_ids",
|
||||||
|
values={
|
||||||
|
"auth_provider": auth_provider,
|
||||||
|
"external_id": external_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
},
|
||||||
|
desc="record_user_external_id",
|
||||||
|
)
|
||||||
|
|
||||||
def user_set_password_hash(self, user_id, password_hash):
|
def user_set_password_hash(self, user_id, password_hash):
|
||||||
"""
|
"""
|
||||||
NB. This does *not* evict any cache because the one use for this
|
NB. This does *not* evict any cache because the one use for this
|
||||||
|
|
24
synapse/storage/schema/delta/56/user_external_ids.sql
Normal file
24
synapse/storage/schema/delta/56/user_external_ids.sql
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
/* Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* a table which records mappings from external auth providers to mxids
|
||||||
|
*/
|
||||||
|
CREATE TABLE IF NOT EXISTS user_external_ids (
|
||||||
|
auth_provider TEXT NOT NULL,
|
||||||
|
external_id TEXT NOT NULL,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
UNIQUE (auth_provider, external_id)
|
||||||
|
);
|
|
@ -14,12 +14,13 @@
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
from synapse.config._base import ConfigError
|
from synapse.config._base import ConfigError
|
||||||
|
|
||||||
|
|
||||||
def load_module(provider):
|
def load_module(provider):
|
||||||
""" Loads a module with its config
|
""" Loads a synapse module with its config
|
||||||
Take a dict with keys 'module' (the module name) and 'config'
|
Take a dict with keys 'module' (the module name) and 'config'
|
||||||
(the config dict).
|
(the config dict).
|
||||||
|
|
||||||
|
@ -38,3 +39,20 @@ def load_module(provider):
|
||||||
raise ConfigError("Failed to parse config for %r: %r" % (provider["module"], e))
|
raise ConfigError("Failed to parse config for %r: %r" % (provider["module"], e))
|
||||||
|
|
||||||
return provider_class, provider_config
|
return provider_class, provider_config
|
||||||
|
|
||||||
|
|
||||||
|
def load_python_module(location: str):
|
||||||
|
"""Load a python module, and return a reference to its global namespace
|
||||||
|
|
||||||
|
Args:
|
||||||
|
location (str): path to the module
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
python module object
|
||||||
|
"""
|
||||||
|
spec = importlib.util.spec_from_file_location(location, location)
|
||||||
|
if spec is None:
|
||||||
|
raise Exception("Unable to load module at %s" % (location,))
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
return mod
|
||||||
|
|
|
@ -29,12 +29,3 @@ Enabling an unknown default rule fails with 404
|
||||||
|
|
||||||
# Blacklisted due to https://github.com/matrix-org/synapse/issues/1663
|
# Blacklisted due to https://github.com/matrix-org/synapse/issues/1663
|
||||||
New federated private chats get full presence information (SYN-115)
|
New federated private chats get full presence information (SYN-115)
|
||||||
|
|
||||||
# Blacklisted temporarily due to https://github.com/matrix-org/matrix-doc/pull/2290
|
|
||||||
# These sytests need to be updated with new endpoints, which will come in a later PR
|
|
||||||
# That PR will also remove this blacklist
|
|
||||||
Can bind 3PID via home server
|
|
||||||
Can bind and unbind 3PID via homeserver
|
|
||||||
3PIDs are unbound after account deactivation
|
|
||||||
Can bind and unbind 3PID via /unbind by specifying the identity server
|
|
||||||
Can bind and unbind 3PID via /unbind without specifying the identity server
|
|
||||||
|
|
|
@ -18,11 +18,22 @@ from twisted.internet.defer import succeed
|
||||||
|
|
||||||
import synapse.rest.admin
|
import synapse.rest.admin
|
||||||
from synapse.api.constants import LoginType
|
from synapse.api.constants import LoginType
|
||||||
|
from synapse.handlers.ui_auth.checkers import UserInteractiveAuthChecker
|
||||||
from synapse.rest.client.v2_alpha import auth, register
|
from synapse.rest.client.v2_alpha import auth, register
|
||||||
|
|
||||||
from tests import unittest
|
from tests import unittest
|
||||||
|
|
||||||
|
|
||||||
|
class DummyRecaptchaChecker(UserInteractiveAuthChecker):
|
||||||
|
def __init__(self, hs):
|
||||||
|
super().__init__(hs)
|
||||||
|
self.recaptcha_attempts = []
|
||||||
|
|
||||||
|
def check_auth(self, authdict, clientip):
|
||||||
|
self.recaptcha_attempts.append((authdict, clientip))
|
||||||
|
return succeed(True)
|
||||||
|
|
||||||
|
|
||||||
class FallbackAuthTests(unittest.HomeserverTestCase):
|
class FallbackAuthTests(unittest.HomeserverTestCase):
|
||||||
|
|
||||||
servlets = [
|
servlets = [
|
||||||
|
@ -44,15 +55,9 @@ class FallbackAuthTests(unittest.HomeserverTestCase):
|
||||||
return hs
|
return hs
|
||||||
|
|
||||||
def prepare(self, reactor, clock, hs):
|
def prepare(self, reactor, clock, hs):
|
||||||
|
self.recaptcha_checker = DummyRecaptchaChecker(hs)
|
||||||
auth_handler = hs.get_auth_handler()
|
auth_handler = hs.get_auth_handler()
|
||||||
|
auth_handler.checkers[LoginType.RECAPTCHA] = self.recaptcha_checker
|
||||||
self.recaptcha_attempts = []
|
|
||||||
|
|
||||||
def _recaptcha(authdict, clientip):
|
|
||||||
self.recaptcha_attempts.append((authdict, clientip))
|
|
||||||
return succeed(True)
|
|
||||||
|
|
||||||
auth_handler.checkers[LoginType.RECAPTCHA] = _recaptcha
|
|
||||||
|
|
||||||
@unittest.INFO
|
@unittest.INFO
|
||||||
def test_fallback_captcha(self):
|
def test_fallback_captcha(self):
|
||||||
|
@ -89,8 +94,9 @@ class FallbackAuthTests(unittest.HomeserverTestCase):
|
||||||
self.assertEqual(request.code, 200)
|
self.assertEqual(request.code, 200)
|
||||||
|
|
||||||
# The recaptcha handler is called with the response given
|
# The recaptcha handler is called with the response given
|
||||||
self.assertEqual(len(self.recaptcha_attempts), 1)
|
attempts = self.recaptcha_checker.recaptcha_attempts
|
||||||
self.assertEqual(self.recaptcha_attempts[0][0]["response"], "a")
|
self.assertEqual(len(attempts), 1)
|
||||||
|
self.assertEqual(attempts[0][0]["response"], "a")
|
||||||
|
|
||||||
# also complete the dummy auth
|
# also complete the dummy auth
|
||||||
request, channel = self.make_request(
|
request, channel = self.make_request(
|
||||||
|
|
|
@ -34,19 +34,12 @@ from tests import unittest
|
||||||
class RegisterRestServletTestCase(unittest.HomeserverTestCase):
|
class RegisterRestServletTestCase(unittest.HomeserverTestCase):
|
||||||
|
|
||||||
servlets = [register.register_servlets]
|
servlets = [register.register_servlets]
|
||||||
|
url = b"/_matrix/client/r0/register"
|
||||||
|
|
||||||
def make_homeserver(self, reactor, clock):
|
def default_config(self, name="test"):
|
||||||
|
config = super().default_config(name)
|
||||||
self.url = b"/_matrix/client/r0/register"
|
config["allow_guest_access"] = True
|
||||||
|
return config
|
||||||
self.hs = self.setup_test_homeserver()
|
|
||||||
self.hs.config.enable_registration = True
|
|
||||||
self.hs.config.registrations_require_3pid = []
|
|
||||||
self.hs.config.auto_join_rooms = []
|
|
||||||
self.hs.config.enable_registration_captcha = False
|
|
||||||
self.hs.config.allow_guest_access = True
|
|
||||||
|
|
||||||
return self.hs
|
|
||||||
|
|
||||||
def test_POST_appservice_registration_valid(self):
|
def test_POST_appservice_registration_valid(self):
|
||||||
user_id = "@as_user_kermit:test"
|
user_id = "@as_user_kermit:test"
|
||||||
|
@ -199,6 +192,73 @@ class RegisterRestServletTestCase(unittest.HomeserverTestCase):
|
||||||
|
|
||||||
self.assertEquals(channel.result["code"], b"200", channel.result)
|
self.assertEquals(channel.result["code"], b"200", channel.result)
|
||||||
|
|
||||||
|
def test_advertised_flows(self):
|
||||||
|
request, channel = self.make_request(b"POST", self.url, b"{}")
|
||||||
|
self.render(request)
|
||||||
|
self.assertEquals(channel.result["code"], b"401", channel.result)
|
||||||
|
flows = channel.json_body["flows"]
|
||||||
|
|
||||||
|
# with the stock config, we only expect the dummy flow
|
||||||
|
self.assertCountEqual([["m.login.dummy"]], (f["stages"] for f in flows))
|
||||||
|
|
||||||
|
@unittest.override_config(
|
||||||
|
{
|
||||||
|
"enable_registration_captcha": True,
|
||||||
|
"user_consent": {
|
||||||
|
"version": "1",
|
||||||
|
"template_dir": "/",
|
||||||
|
"require_at_registration": True,
|
||||||
|
},
|
||||||
|
"account_threepid_delegates": {
|
||||||
|
"email": "https://id_server",
|
||||||
|
"msisdn": "https://id_server",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def test_advertised_flows_captcha_and_terms_and_3pids(self):
|
||||||
|
request, channel = self.make_request(b"POST", self.url, b"{}")
|
||||||
|
self.render(request)
|
||||||
|
self.assertEquals(channel.result["code"], b"401", channel.result)
|
||||||
|
flows = channel.json_body["flows"]
|
||||||
|
|
||||||
|
self.assertCountEqual(
|
||||||
|
[
|
||||||
|
["m.login.recaptcha", "m.login.terms", "m.login.dummy"],
|
||||||
|
["m.login.recaptcha", "m.login.terms", "m.login.email.identity"],
|
||||||
|
["m.login.recaptcha", "m.login.terms", "m.login.msisdn"],
|
||||||
|
[
|
||||||
|
"m.login.recaptcha",
|
||||||
|
"m.login.terms",
|
||||||
|
"m.login.msisdn",
|
||||||
|
"m.login.email.identity",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
(f["stages"] for f in flows),
|
||||||
|
)
|
||||||
|
|
||||||
|
@unittest.override_config(
|
||||||
|
{
|
||||||
|
"public_baseurl": "https://test_server",
|
||||||
|
"registrations_require_3pid": ["email"],
|
||||||
|
"disable_msisdn_registration": True,
|
||||||
|
"email": {
|
||||||
|
"smtp_host": "mail_server",
|
||||||
|
"smtp_port": 2525,
|
||||||
|
"notif_from": "sender@host",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def test_advertised_flows_no_msisdn_email_required(self):
|
||||||
|
request, channel = self.make_request(b"POST", self.url, b"{}")
|
||||||
|
self.render(request)
|
||||||
|
self.assertEquals(channel.result["code"], b"401", channel.result)
|
||||||
|
flows = channel.json_body["flows"]
|
||||||
|
|
||||||
|
# with the stock config, we expect all four combinations of 3pid
|
||||||
|
self.assertCountEqual(
|
||||||
|
[["m.login.email.identity"]], (f["stages"] for f in flows)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AccountValidityTestCase(unittest.HomeserverTestCase):
|
class AccountValidityTestCase(unittest.HomeserverTestCase):
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,21 @@ from tests import unittest
|
||||||
class TermsTestCase(unittest.HomeserverTestCase):
|
class TermsTestCase(unittest.HomeserverTestCase):
|
||||||
servlets = [register_servlets]
|
servlets = [register_servlets]
|
||||||
|
|
||||||
|
def default_config(self, name="test"):
|
||||||
|
config = super().default_config(name)
|
||||||
|
config.update(
|
||||||
|
{
|
||||||
|
"public_baseurl": "https://example.org/",
|
||||||
|
"user_consent": {
|
||||||
|
"version": "1.0",
|
||||||
|
"policy_name": "My Cool Privacy Policy",
|
||||||
|
"template_dir": "/",
|
||||||
|
"require_at_registration": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return config
|
||||||
|
|
||||||
def prepare(self, reactor, clock, hs):
|
def prepare(self, reactor, clock, hs):
|
||||||
self.clock = MemoryReactorClock()
|
self.clock = MemoryReactorClock()
|
||||||
self.hs_clock = Clock(self.clock)
|
self.hs_clock = Clock(self.clock)
|
||||||
|
@ -35,17 +50,8 @@ class TermsTestCase(unittest.HomeserverTestCase):
|
||||||
self.registration_handler = Mock()
|
self.registration_handler = Mock()
|
||||||
self.auth_handler = Mock()
|
self.auth_handler = Mock()
|
||||||
self.device_handler = Mock()
|
self.device_handler = Mock()
|
||||||
hs.config.enable_registration = True
|
|
||||||
hs.config.registrations_require_3pid = []
|
|
||||||
hs.config.auto_join_rooms = []
|
|
||||||
hs.config.enable_registration_captcha = False
|
|
||||||
|
|
||||||
def test_ui_auth(self):
|
def test_ui_auth(self):
|
||||||
self.hs.config.user_consent_at_registration = True
|
|
||||||
self.hs.config.user_consent_policy_name = "My Cool Privacy Policy"
|
|
||||||
self.hs.config.public_baseurl = "https://example.org/"
|
|
||||||
self.hs.config.user_consent_version = "1.0"
|
|
||||||
|
|
||||||
# Do a UI auth request
|
# Do a UI auth request
|
||||||
request, channel = self.make_request(b"POST", self.url, b"{}")
|
request, channel = self.make_request(b"POST", self.url, b"{}")
|
||||||
self.render(request)
|
self.render(request)
|
||||||
|
|
Loading…
Reference in a new issue