2018-12-07 07:11:11 -05:00
|
|
|
#
|
2023-11-21 15:29:58 -05:00
|
|
|
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
|
|
|
#
|
2024-01-23 06:26:48 -05:00
|
|
|
# Copyright 2019-2021 The Matrix.org Foundation C.I.C.
|
2023-11-21 15:29:58 -05:00
|
|
|
# Copyright (C) 2023 New Vector, Ltd
|
|
|
|
#
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU Affero General Public License as
|
|
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
|
|
# License, or (at your option) any later version.
|
|
|
|
#
|
|
|
|
# See the GNU Affero General Public License for more details:
|
|
|
|
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
|
|
|
#
|
|
|
|
# Originally licensed under the Apache License, Version 2.0:
|
|
|
|
# <http://www.apache.org/licenses/LICENSE-2.0>.
|
|
|
|
#
|
|
|
|
# [This file includes modifications made by New Vector Limited]
|
2018-12-07 07:11:11 -05:00
|
|
|
#
|
|
|
|
#
|
2019-09-19 15:29:11 -04:00
|
|
|
|
2019-12-10 12:30:16 -05:00
|
|
|
import logging
|
2021-12-01 07:28:23 -05:00
|
|
|
from typing import Any, List, Set
|
2020-03-11 15:33:16 -04:00
|
|
|
|
2021-02-11 10:05:15 -05:00
|
|
|
from synapse.config.sso import SsoAttributeRequirement
|
2021-12-01 07:28:23 -05:00
|
|
|
from synapse.types import JsonDict
|
2022-06-30 13:48:04 -04:00
|
|
|
from synapse.util.check_dependencies import check_requirements
|
2019-12-10 12:30:16 -05:00
|
|
|
from synapse.util.module_loader import load_module, load_python_module
|
2018-12-07 07:11:11 -05:00
|
|
|
|
|
|
|
from ._base import Config, ConfigError
|
2020-08-11 11:08:10 -04:00
|
|
|
from ._util import validate_config
|
2018-12-07 07:11:11 -05:00
|
|
|
|
2019-12-10 12:30:16 -05:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2021-04-20 14:55:20 -04:00
|
|
|
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 = (
|
2019-12-10 12:30:16 -05:00
|
|
|
"synapse.handlers.saml_handler.DefaultSamlMappingProvider"
|
|
|
|
)
|
|
|
|
|
2018-12-07 07:11:11 -05:00
|
|
|
|
2021-12-01 07:28:23 -05:00
|
|
|
def _dict_merge(merge_dict: dict, into_dict: dict) -> None:
|
2019-09-24 06:15:08 -04:00
|
|
|
"""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:
|
2021-12-01 07:28:23 -05:00
|
|
|
merge_dict: dict to merge
|
|
|
|
into_dict: target dict to be modified
|
2019-09-24 06:15:08 -04:00
|
|
|
"""
|
2019-09-19 15:29:11 -04:00
|
|
|
for k, v in merge_dict.items():
|
2019-09-24 06:15:08 -04:00
|
|
|
if k not in into_dict:
|
|
|
|
into_dict[k] = v
|
2019-09-19 15:29:11 -04:00
|
|
|
continue
|
|
|
|
|
2019-09-24 06:15:08 -04:00
|
|
|
current_val = into_dict[k]
|
2019-09-19 15:29:11 -04:00
|
|
|
|
|
|
|
if isinstance(v, dict) and isinstance(current_val, dict):
|
|
|
|
_dict_merge(v, current_val)
|
|
|
|
continue
|
|
|
|
|
|
|
|
# otherwise we just overwrite
|
2019-09-24 06:15:08 -04:00
|
|
|
into_dict[k] = v
|
2019-09-19 15:29:11 -04:00
|
|
|
|
|
|
|
|
2018-12-07 07:11:11 -05:00
|
|
|
class SAML2Config(Config):
|
2019-10-10 04:39:35 -04:00
|
|
|
section = "saml2"
|
|
|
|
|
2022-04-11 12:07:23 -04:00
|
|
|
def read_config(self, config: JsonDict, **kwargs: Any) -> None:
|
2018-12-07 07:11:11 -05:00
|
|
|
self.saml2_enabled = False
|
|
|
|
|
|
|
|
saml2_config = config.get("saml2_config")
|
|
|
|
|
|
|
|
if not saml2_config or not saml2_config.get("enabled", True):
|
|
|
|
return
|
|
|
|
|
2019-09-13 07:07:03 -04:00
|
|
|
if not saml2_config.get("sp_config") and not saml2_config.get("config_path"):
|
|
|
|
return
|
|
|
|
|
2022-06-30 13:48:04 -04:00
|
|
|
check_requirements("saml2")
|
2019-06-10 19:03:57 -04:00
|
|
|
|
2018-12-07 07:11:11 -05:00
|
|
|
self.saml2_enabled = True
|
|
|
|
|
2020-08-11 11:08:10 -04:00
|
|
|
attribute_requirements = saml2_config.get("attribute_requirements") or []
|
|
|
|
self.attribute_requirements = _parse_attribute_requirements_def(
|
|
|
|
attribute_requirements
|
|
|
|
)
|
|
|
|
|
2019-09-13 10:20:49 -04:00
|
|
|
self.saml2_grandfathered_mxid_source_attribute = saml2_config.get(
|
|
|
|
"grandfathered_mxid_source_attribute", "uid"
|
|
|
|
)
|
|
|
|
|
2023-08-11 16:15:17 -04:00
|
|
|
# refers to a SAML IdP entity ID
|
2020-11-19 09:57:13 -05:00
|
|
|
self.saml2_idp_entityid = saml2_config.get("idp_entityid", None)
|
|
|
|
|
2023-08-11 16:15:17 -04:00
|
|
|
# IdP properties for Matrix clients
|
|
|
|
self.idp_name = saml2_config.get("idp_name", "SAML")
|
|
|
|
self.idp_icon = saml2_config.get("idp_icon")
|
|
|
|
self.idp_brand = saml2_config.get("idp_brand")
|
|
|
|
|
2019-12-10 12:30:16 -05:00
|
|
|
# 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)
|
2021-04-20 14:55:20 -04:00
|
|
|
if ump_dict.get("module") == LEGACY_USER_MAPPING_PROVIDER:
|
|
|
|
ump_dict["module"] = DEFAULT_USER_MAPPING_PROVIDER
|
2019-12-10 12:30:16 -05:00
|
|
|
|
|
|
|
# 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,
|
2020-12-08 09:04:35 -05:00
|
|
|
) = load_module(ump_dict, ("saml2_config", "user_mapping_provider"))
|
2019-12-10 12:30:16 -05:00
|
|
|
|
|
|
|
# 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",
|
2020-01-17 05:32:47 -05:00
|
|
|
"get_remote_user_id",
|
2019-12-10 12:30:16 -05:00
|
|
|
]
|
|
|
|
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
|
|
|
|
)
|
|
|
|
)
|
2019-09-24 06:15:08 -04:00
|
|
|
_dict_merge(
|
|
|
|
merge_dict=saml2_config.get("sp_config", {}), into_dict=saml2_config_dict
|
|
|
|
)
|
2018-12-07 07:11:11 -05:00
|
|
|
|
|
|
|
config_path = saml2_config.get("config_path", None)
|
|
|
|
if config_path is not None:
|
2019-09-19 15:29:11 -04:00
|
|
|
mod = load_python_module(config_path)
|
2022-04-11 12:07:23 -04:00
|
|
|
config_dict_from_file = getattr(mod, "CONFIG", None)
|
|
|
|
if config_dict_from_file is None:
|
2021-05-24 15:32:01 -04:00
|
|
|
raise ConfigError(
|
|
|
|
"Config path specified by saml2_config.config_path does not "
|
|
|
|
"have a CONFIG property."
|
|
|
|
)
|
2022-04-11 12:07:23 -04:00
|
|
|
_dict_merge(merge_dict=config_dict_from_file, into_dict=saml2_config_dict)
|
2019-09-19 15:29:11 -04:00
|
|
|
|
|
|
|
import saml2.config
|
|
|
|
|
|
|
|
self.saml2_sp_config = saml2.config.SPConfig()
|
|
|
|
self.saml2_sp_config.load(saml2_config_dict)
|
2018-12-07 07:11:11 -05:00
|
|
|
|
2019-06-26 18:50:55 -04:00
|
|
|
# session lifetime: in milliseconds
|
|
|
|
self.saml2_session_lifetime = self.parse_duration(
|
2020-06-11 07:55:45 -04:00
|
|
|
saml2_config.get("saml_session_lifetime", "15m")
|
2019-06-26 18:50:55 -04:00
|
|
|
)
|
|
|
|
|
2019-12-10 12:30:16 -05:00
|
|
|
def _default_saml_config_dict(
|
2021-12-01 07:28:23 -05:00
|
|
|
self, required_attributes: Set[str], optional_attributes: Set[str]
|
|
|
|
) -> JsonDict:
|
2019-12-10 12:30:16 -05:00
|
|
|
"""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:
|
2021-12-01 07:28:23 -05:00
|
|
|
A SAML configuration dictionary
|
2019-12-10 12:30:16 -05:00
|
|
|
"""
|
2018-12-07 07:11:11 -05:00
|
|
|
import saml2
|
|
|
|
|
2019-09-13 10:20:49 -04:00
|
|
|
if self.saml2_grandfathered_mxid_source_attribute:
|
|
|
|
optional_attributes.add(self.saml2_grandfathered_mxid_source_attribute)
|
|
|
|
optional_attributes -= required_attributes
|
|
|
|
|
2021-11-08 09:13:10 -05:00
|
|
|
public_baseurl = self.root.server.public_baseurl
|
2021-02-02 04:43:50 -05:00
|
|
|
metadata_url = public_baseurl + "_synapse/client/saml2/metadata.xml"
|
|
|
|
response_url = public_baseurl + "_synapse/client/saml2/authn_response"
|
2018-12-07 07:11:11 -05:00
|
|
|
return {
|
|
|
|
"entityid": metadata_url,
|
|
|
|
"service": {
|
|
|
|
"sp": {
|
|
|
|
"endpoints": {
|
|
|
|
"assertion_consumer_service": [
|
|
|
|
(response_url, saml2.BINDING_HTTP_POST)
|
|
|
|
]
|
|
|
|
},
|
2019-09-13 10:20:49 -04:00
|
|
|
"required_attributes": list(required_attributes),
|
|
|
|
"optional_attributes": list(optional_attributes),
|
|
|
|
# "name_id_format": saml2.saml.NAMEID_FORMAT_PERSISTENT,
|
2019-06-20 05:32:02 -04:00
|
|
|
}
|
2018-12-07 07:11:11 -05:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2020-08-11 11:08:10 -04:00
|
|
|
|
|
|
|
ATTRIBUTE_REQUIREMENTS_SCHEMA = {
|
|
|
|
"type": "array",
|
2021-02-11 10:05:15 -05:00
|
|
|
"items": SsoAttributeRequirement.JSON_SCHEMA,
|
2020-08-11 11:08:10 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_attribute_requirements_def(
|
|
|
|
attribute_requirements: Any,
|
2021-02-11 10:05:15 -05:00
|
|
|
) -> List[SsoAttributeRequirement]:
|
2020-08-11 11:08:10 -04:00
|
|
|
validate_config(
|
|
|
|
ATTRIBUTE_REQUIREMENTS_SCHEMA,
|
|
|
|
attribute_requirements,
|
2021-02-11 10:05:15 -05:00
|
|
|
config_path=("saml2_config", "attribute_requirements"),
|
2020-08-11 11:08:10 -04:00
|
|
|
)
|
2021-02-11 10:05:15 -05:00
|
|
|
return [SsoAttributeRequirement(**x) for x in attribute_requirements]
|