mirror of
https://mau.dev/maunium/synapse.git
synced 2024-10-01 01:36:05 -04:00
313581e4e9
* Pull runtime dep checks into their own module * Reimplement `check_requirements` using `importlib` I've tried to make this clearer. We start by working out which of Synapse's requirements we need to be installed here and now. I was surprised that there wasn't an easier way to see which packages were installed by a given extra. I've pulled out the error messages into functions that deal with "is this for an extra or not". And I've rearranged the loop over two different sets of requirements into one loop with a "must be instaled" flag. I hope you agree that this is clearer. * Test cases
425 lines
16 KiB
Python
425 lines
16 KiB
Python
# Copyright 2018 New Vector Ltd
|
|
# Copyright 2019-2021 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 typing import Any, List, Set
|
|
|
|
from synapse.config.sso import SsoAttributeRequirement
|
|
from synapse.types import JsonDict
|
|
from synapse.util.check_dependencies import DependencyException, check_requirements
|
|
from synapse.util.module_loader import load_module, load_python_module
|
|
|
|
from ._base import Config, ConfigError
|
|
from ._util import validate_config
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
DEFAULT_USER_MAPPING_PROVIDER = "synapse.handlers.saml.DefaultSamlMappingProvider"
|
|
# The module that DefaultSamlMappingProvider is in was renamed, we want to
|
|
# transparently handle both the same.
|
|
LEGACY_USER_MAPPING_PROVIDER = (
|
|
"synapse.handlers.saml_handler.DefaultSamlMappingProvider"
|
|
)
|
|
|
|
|
|
def _dict_merge(merge_dict: dict, into_dict: dict) -> None:
|
|
"""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 to merge
|
|
into_dict: target dict to be modified
|
|
"""
|
|
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):
|
|
section = "saml2"
|
|
|
|
def read_config(self, config, **kwargs) -> None:
|
|
self.saml2_enabled = False
|
|
|
|
saml2_config = config.get("saml2_config")
|
|
|
|
if not saml2_config or not saml2_config.get("enabled", True):
|
|
return
|
|
|
|
if not saml2_config.get("sp_config") and not saml2_config.get("config_path"):
|
|
return
|
|
|
|
try:
|
|
check_requirements("saml2")
|
|
except DependencyException as e:
|
|
raise ConfigError(
|
|
e.message # noqa: B306, DependencyException.message is a property
|
|
)
|
|
|
|
self.saml2_enabled = True
|
|
|
|
attribute_requirements = saml2_config.get("attribute_requirements") or []
|
|
self.attribute_requirements = _parse_attribute_requirements_def(
|
|
attribute_requirements
|
|
)
|
|
|
|
self.saml2_grandfathered_mxid_source_attribute = saml2_config.get(
|
|
"grandfathered_mxid_source_attribute", "uid"
|
|
)
|
|
|
|
self.saml2_idp_entityid = saml2_config.get("idp_entityid", None)
|
|
|
|
# user_mapping_provider may be None if the key is present but has no value
|
|
ump_dict = saml2_config.get("user_mapping_provider") or {}
|
|
|
|
# Use the default user mapping provider if not set
|
|
ump_dict.setdefault("module", DEFAULT_USER_MAPPING_PROVIDER)
|
|
if ump_dict.get("module") == LEGACY_USER_MAPPING_PROVIDER:
|
|
ump_dict["module"] = DEFAULT_USER_MAPPING_PROVIDER
|
|
|
|
# Ensure a config is present
|
|
ump_dict["config"] = ump_dict.get("config") or {}
|
|
|
|
if ump_dict["module"] == DEFAULT_USER_MAPPING_PROVIDER:
|
|
# Load deprecated options for use by the default module
|
|
old_mxid_source_attribute = saml2_config.get("mxid_source_attribute")
|
|
if old_mxid_source_attribute:
|
|
logger.warning(
|
|
"The config option saml2_config.mxid_source_attribute is deprecated. "
|
|
"Please use saml2_config.user_mapping_provider.config"
|
|
".mxid_source_attribute instead."
|
|
)
|
|
ump_dict["config"]["mxid_source_attribute"] = old_mxid_source_attribute
|
|
|
|
old_mxid_mapping = saml2_config.get("mxid_mapping")
|
|
if old_mxid_mapping:
|
|
logger.warning(
|
|
"The config option saml2_config.mxid_mapping is deprecated. Please "
|
|
"use saml2_config.user_mapping_provider.config.mxid_mapping instead."
|
|
)
|
|
ump_dict["config"]["mxid_mapping"] = old_mxid_mapping
|
|
|
|
# Retrieve an instance of the module's class
|
|
# Pass the config dictionary to the module for processing
|
|
(
|
|
self.saml2_user_mapping_provider_class,
|
|
self.saml2_user_mapping_provider_config,
|
|
) = load_module(ump_dict, ("saml2_config", "user_mapping_provider"))
|
|
|
|
# Ensure loaded user mapping module has defined all necessary methods
|
|
# Note parse_config() is already checked during the call to load_module
|
|
required_methods = [
|
|
"get_saml_attributes",
|
|
"saml_response_to_user_attributes",
|
|
"get_remote_user_id",
|
|
]
|
|
missing_methods = [
|
|
method
|
|
for method in required_methods
|
|
if not hasattr(self.saml2_user_mapping_provider_class, method)
|
|
]
|
|
if missing_methods:
|
|
raise ConfigError(
|
|
"Class specified by saml2_config."
|
|
"user_mapping_provider.module is missing required "
|
|
"methods: %s" % (", ".join(missing_methods),)
|
|
)
|
|
|
|
# Get the desired saml auth response attributes from the module
|
|
saml2_config_dict = self._default_saml_config_dict(
|
|
*self.saml2_user_mapping_provider_class.get_saml_attributes(
|
|
self.saml2_user_mapping_provider_config
|
|
)
|
|
)
|
|
_dict_merge(
|
|
merge_dict=saml2_config.get("sp_config", {}), into_dict=saml2_config_dict
|
|
)
|
|
|
|
config_path = saml2_config.get("config_path", None)
|
|
if config_path is not None:
|
|
mod = load_python_module(config_path)
|
|
config = getattr(mod, "CONFIG", None)
|
|
if config is None:
|
|
raise ConfigError(
|
|
"Config path specified by saml2_config.config_path does not "
|
|
"have a CONFIG property."
|
|
)
|
|
_dict_merge(merge_dict=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
|
|
self.saml2_session_lifetime = self.parse_duration(
|
|
saml2_config.get("saml_session_lifetime", "15m")
|
|
)
|
|
|
|
def _default_saml_config_dict(
|
|
self, required_attributes: Set[str], optional_attributes: Set[str]
|
|
) -> JsonDict:
|
|
"""Generate a configuration dictionary with required and optional attributes that
|
|
will be needed to process new user registration
|
|
|
|
Args:
|
|
required_attributes: SAML auth response attributes that are
|
|
necessary to function
|
|
optional_attributes: SAML auth response attributes that can be used to add
|
|
additional information to Synapse user accounts, but are not required
|
|
|
|
Returns:
|
|
A SAML configuration dictionary
|
|
"""
|
|
import saml2
|
|
|
|
if self.saml2_grandfathered_mxid_source_attribute:
|
|
optional_attributes.add(self.saml2_grandfathered_mxid_source_attribute)
|
|
optional_attributes -= required_attributes
|
|
|
|
public_baseurl = self.root.server.public_baseurl
|
|
metadata_url = public_baseurl + "_synapse/client/saml2/metadata.xml"
|
|
response_url = public_baseurl + "_synapse/client/saml2/authn_response"
|
|
return {
|
|
"entityid": metadata_url,
|
|
"service": {
|
|
"sp": {
|
|
"endpoints": {
|
|
"assertion_consumer_service": [
|
|
(response_url, saml2.BINDING_HTTP_POST)
|
|
]
|
|
},
|
|
"required_attributes": list(required_attributes),
|
|
"optional_attributes": list(optional_attributes),
|
|
# "name_id_format": saml2.saml.NAMEID_FORMAT_PERSISTENT,
|
|
}
|
|
},
|
|
}
|
|
|
|
def generate_config_section(self, config_dir_path, server_name, **kwargs) -> str:
|
|
return """\
|
|
## Single sign-on integration ##
|
|
|
|
# The following settings can be used to make Synapse use a single sign-on
|
|
# provider for authentication, instead of its internal password database.
|
|
#
|
|
# You will probably also want to set the following options to `false` to
|
|
# disable the regular login/registration flows:
|
|
# * enable_registration
|
|
# * password_config.enabled
|
|
#
|
|
# You will also want to investigate the settings under the "sso" configuration
|
|
# section below.
|
|
|
|
# Enable SAML2 for registration and login. Uses pysaml2.
|
|
#
|
|
# At least one of `sp_config` or `config_path` must be set in this section to
|
|
# enable SAML login.
|
|
#
|
|
# Once SAML support is enabled, a metadata file will be exposed at
|
|
# https://<server>:<port>/_synapse/client/saml2/metadata.xml, which you may be able to
|
|
# use to configure your SAML IdP with. Alternatively, you can manually configure
|
|
# the IdP to use an ACS location of
|
|
# https://<server>:<port>/_synapse/client/saml2/authn_response.
|
|
#
|
|
saml2_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 must provide either a local
|
|
# file via the `local` attribute or (preferably) a URL via the
|
|
# `remote` attribute.
|
|
#
|
|
#metadata:
|
|
# local: ["saml2/idp.xml"]
|
|
# remote:
|
|
# - url: https://our_idp/metadata.xml
|
|
|
|
# Allowed clock difference in seconds between the homeserver and IdP.
|
|
#
|
|
# Uncomment the below to increase the accepted time difference from 0 to 3 seconds.
|
|
#
|
|
#accepted_time_diff: 3
|
|
|
|
# By default, the user has to go to our login page first. If you'd like
|
|
# to allow IdP-initiated login, set 'allow_unsolicited: true' in a
|
|
# 'service.sp' section:
|
|
#
|
|
#service:
|
|
# sp:
|
|
# allow_unsolicited: true
|
|
|
|
# The examples below are just used to generate our metadata xml, and you
|
|
# may well not need them, depending on your setup. Alternatively you
|
|
# may need a whole lot more detail - see the pysaml2 docs!
|
|
|
|
#description: ["My awesome SP", "en"]
|
|
#name: ["Test SP", "en"]
|
|
|
|
#ui_info:
|
|
# display_name:
|
|
# - lang: en
|
|
# text: "Display Name is the descriptive name of your service."
|
|
# description:
|
|
# - lang: en
|
|
# text: "Description should be a short paragraph explaining the purpose of the service."
|
|
# information_url:
|
|
# - lang: en
|
|
# text: "https://example.com/terms-of-service"
|
|
# privacy_statement_url:
|
|
# - lang: en
|
|
# text: "https://example.com/privacy-policy"
|
|
# keywords:
|
|
# - lang: en
|
|
# text: ["Matrix", "Element"]
|
|
# logo:
|
|
# - lang: en
|
|
# text: "https://example.com/logo.svg"
|
|
# width: "200"
|
|
# height: "80"
|
|
|
|
#organization:
|
|
# name: Example com
|
|
# display_name:
|
|
# - ["Example co", "en"]
|
|
# url: "http://example.com"
|
|
|
|
#contact_person:
|
|
# - given_name: Bob
|
|
# sur_name: "the Sysadmin"
|
|
# email_address": ["admin@example.com"]
|
|
# contact_type": technical
|
|
|
|
# Instead of putting the config inline as above, you can specify a
|
|
# separate pysaml2 configuration file:
|
|
#
|
|
#config_path: "%(config_dir_path)s/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 15 minutes.
|
|
#
|
|
#saml_session_lifetime: 5m
|
|
|
|
# An external module can be provided here as a custom solution to
|
|
# mapping attributes returned from a saml provider onto a matrix user.
|
|
#
|
|
user_mapping_provider:
|
|
# The custom module's class. Uncomment to use a custom module.
|
|
#
|
|
#module: mapping_provider.SamlMappingProvider
|
|
|
|
# Custom configuration values for the module. Below options are
|
|
# intended for the built-in provider, they should be changed if
|
|
# using a custom module. This section will be passed as a Python
|
|
# dictionary to the module's `parse_config` method.
|
|
#
|
|
config:
|
|
# The SAML attribute (after mapping via the attribute maps) to use
|
|
# to derive the Matrix ID from. 'uid' by default.
|
|
#
|
|
# Note: This used to be configured by the
|
|
# saml2_config.mxid_source_attribute option. If that is still
|
|
# defined, its value will be used instead.
|
|
#
|
|
#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'.
|
|
#
|
|
# Note: This used to be configured by the
|
|
# saml2_config.mxid_mapping option. If that is still defined, its
|
|
# value will be used instead.
|
|
#
|
|
#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
|
|
|
|
# It is possible to configure Synapse to only allow logins if SAML attributes
|
|
# match particular values. The requirements can be listed under
|
|
# `attribute_requirements` as shown below. All of the listed attributes must
|
|
# match for the login to be permitted.
|
|
#
|
|
#attribute_requirements:
|
|
# - attribute: userGroup
|
|
# value: "staff"
|
|
# - attribute: department
|
|
# value: "sales"
|
|
|
|
# If the metadata XML contains multiple IdP entities then the `idp_entityid`
|
|
# option must be set to the entity to redirect users to.
|
|
#
|
|
# Most deployments only have a single IdP entity and so should omit this
|
|
# option.
|
|
#
|
|
#idp_entityid: 'https://our_idp/entityid'
|
|
""" % {
|
|
"config_dir_path": config_dir_path
|
|
}
|
|
|
|
|
|
ATTRIBUTE_REQUIREMENTS_SCHEMA = {
|
|
"type": "array",
|
|
"items": SsoAttributeRequirement.JSON_SCHEMA,
|
|
}
|
|
|
|
|
|
def _parse_attribute_requirements_def(
|
|
attribute_requirements: Any,
|
|
) -> List[SsoAttributeRequirement]:
|
|
validate_config(
|
|
ATTRIBUTE_REQUIREMENTS_SCHEMA,
|
|
attribute_requirements,
|
|
config_path=("saml2_config", "attribute_requirements"),
|
|
)
|
|
return [SsoAttributeRequirement(**x) for x in attribute_requirements]
|