You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

416 lines
16 KiB

  1. # Copyright 2018 New Vector Ltd
  2. # Copyright 2019 The Matrix.org Foundation C.I.C.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import logging
  16. from typing import Any, List
  17. from synapse.config.sso import SsoAttributeRequirement
  18. from synapse.python_dependencies import DependencyException, check_requirements
  19. from synapse.util.module_loader import load_module, load_python_module
  20. from ._base import Config, ConfigError
  21. from ._util import validate_config
  22. logger = logging.getLogger(__name__)
  23. DEFAULT_USER_MAPPING_PROVIDER = (
  24. "synapse.handlers.saml_handler.DefaultSamlMappingProvider"
  25. )
  26. def _dict_merge(merge_dict, into_dict):
  27. """Do a deep merge of two dicts
  28. Recursively merges `merge_dict` into `into_dict`:
  29. * For keys where both `merge_dict` and `into_dict` have a dict value, the values
  30. are recursively merged
  31. * For all other keys, the values in `into_dict` (if any) are overwritten with
  32. the value from `merge_dict`.
  33. Args:
  34. merge_dict (dict): dict to merge
  35. into_dict (dict): target dict
  36. """
  37. for k, v in merge_dict.items():
  38. if k not in into_dict:
  39. into_dict[k] = v
  40. continue
  41. current_val = into_dict[k]
  42. if isinstance(v, dict) and isinstance(current_val, dict):
  43. _dict_merge(v, current_val)
  44. continue
  45. # otherwise we just overwrite
  46. into_dict[k] = v
  47. class SAML2Config(Config):
  48. section = "saml2"
  49. def read_config(self, config, **kwargs):
  50. self.saml2_enabled = False
  51. saml2_config = config.get("saml2_config")
  52. if not saml2_config or not saml2_config.get("enabled", True):
  53. return
  54. if not saml2_config.get("sp_config") and not saml2_config.get("config_path"):
  55. return
  56. try:
  57. check_requirements("saml2")
  58. except DependencyException as e:
  59. raise ConfigError(
  60. e.message # noqa: B306, DependencyException.message is a property
  61. )
  62. self.saml2_enabled = True
  63. attribute_requirements = saml2_config.get("attribute_requirements") or []
  64. self.attribute_requirements = _parse_attribute_requirements_def(
  65. attribute_requirements
  66. )
  67. self.saml2_grandfathered_mxid_source_attribute = saml2_config.get(
  68. "grandfathered_mxid_source_attribute", "uid"
  69. )
  70. self.saml2_idp_entityid = saml2_config.get("idp_entityid", None)
  71. # user_mapping_provider may be None if the key is present but has no value
  72. ump_dict = saml2_config.get("user_mapping_provider") or {}
  73. # Use the default user mapping provider if not set
  74. ump_dict.setdefault("module", DEFAULT_USER_MAPPING_PROVIDER)
  75. # Ensure a config is present
  76. ump_dict["config"] = ump_dict.get("config") or {}
  77. if ump_dict["module"] == DEFAULT_USER_MAPPING_PROVIDER:
  78. # Load deprecated options for use by the default module
  79. old_mxid_source_attribute = saml2_config.get("mxid_source_attribute")
  80. if old_mxid_source_attribute:
  81. logger.warning(
  82. "The config option saml2_config.mxid_source_attribute is deprecated. "
  83. "Please use saml2_config.user_mapping_provider.config"
  84. ".mxid_source_attribute instead."
  85. )
  86. ump_dict["config"]["mxid_source_attribute"] = old_mxid_source_attribute
  87. old_mxid_mapping = saml2_config.get("mxid_mapping")
  88. if old_mxid_mapping:
  89. logger.warning(
  90. "The config option saml2_config.mxid_mapping is deprecated. Please "
  91. "use saml2_config.user_mapping_provider.config.mxid_mapping instead."
  92. )
  93. ump_dict["config"]["mxid_mapping"] = old_mxid_mapping
  94. # Retrieve an instance of the module's class
  95. # Pass the config dictionary to the module for processing
  96. (
  97. self.saml2_user_mapping_provider_class,
  98. self.saml2_user_mapping_provider_config,
  99. ) = load_module(ump_dict, ("saml2_config", "user_mapping_provider"))
  100. # Ensure loaded user mapping module has defined all necessary methods
  101. # Note parse_config() is already checked during the call to load_module
  102. required_methods = [
  103. "get_saml_attributes",
  104. "saml_response_to_user_attributes",
  105. "get_remote_user_id",
  106. ]
  107. missing_methods = [
  108. method
  109. for method in required_methods
  110. if not hasattr(self.saml2_user_mapping_provider_class, method)
  111. ]
  112. if missing_methods:
  113. raise ConfigError(
  114. "Class specified by saml2_config."
  115. "user_mapping_provider.module is missing required "
  116. "methods: %s" % (", ".join(missing_methods),)
  117. )
  118. # Get the desired saml auth response attributes from the module
  119. saml2_config_dict = self._default_saml_config_dict(
  120. *self.saml2_user_mapping_provider_class.get_saml_attributes(
  121. self.saml2_user_mapping_provider_config
  122. )
  123. )
  124. _dict_merge(
  125. merge_dict=saml2_config.get("sp_config", {}), into_dict=saml2_config_dict
  126. )
  127. config_path = saml2_config.get("config_path", None)
  128. if config_path is not None:
  129. mod = load_python_module(config_path)
  130. _dict_merge(merge_dict=mod.CONFIG, into_dict=saml2_config_dict)
  131. import saml2.config
  132. self.saml2_sp_config = saml2.config.SPConfig()
  133. self.saml2_sp_config.load(saml2_config_dict)
  134. # session lifetime: in milliseconds
  135. self.saml2_session_lifetime = self.parse_duration(
  136. saml2_config.get("saml_session_lifetime", "15m")
  137. )
  138. def _default_saml_config_dict(
  139. self, required_attributes: set, optional_attributes: set
  140. ):
  141. """Generate a configuration dictionary with required and optional attributes that
  142. will be needed to process new user registration
  143. Args:
  144. required_attributes: SAML auth response attributes that are
  145. necessary to function
  146. optional_attributes: SAML auth response attributes that can be used to add
  147. additional information to Synapse user accounts, but are not required
  148. Returns:
  149. dict: A SAML configuration dictionary
  150. """
  151. import saml2
  152. public_baseurl = self.public_baseurl
  153. if public_baseurl is None:
  154. raise ConfigError("saml2_config requires a public_baseurl to be set")
  155. if self.saml2_grandfathered_mxid_source_attribute:
  156. optional_attributes.add(self.saml2_grandfathered_mxid_source_attribute)
  157. optional_attributes -= required_attributes
  158. metadata_url = public_baseurl + "_synapse/client/saml2/metadata.xml"
  159. response_url = public_baseurl + "_synapse/client/saml2/authn_response"
  160. return {
  161. "entityid": metadata_url,
  162. "service": {
  163. "sp": {
  164. "endpoints": {
  165. "assertion_consumer_service": [
  166. (response_url, saml2.BINDING_HTTP_POST)
  167. ]
  168. },
  169. "required_attributes": list(required_attributes),
  170. "optional_attributes": list(optional_attributes),
  171. # "name_id_format": saml2.saml.NAMEID_FORMAT_PERSISTENT,
  172. }
  173. },
  174. }
  175. def generate_config_section(self, config_dir_path, server_name, **kwargs):
  176. return """\
  177. ## Single sign-on integration ##
  178. # The following settings can be used to make Synapse use a single sign-on
  179. # provider for authentication, instead of its internal password database.
  180. #
  181. # You will probably also want to set the following options to `false` to
  182. # disable the regular login/registration flows:
  183. # * enable_registration
  184. # * password_config.enabled
  185. #
  186. # You will also want to investigate the settings under the "sso" configuration
  187. # section below.
  188. # Enable SAML2 for registration and login. Uses pysaml2.
  189. #
  190. # At least one of `sp_config` or `config_path` must be set in this section to
  191. # enable SAML login.
  192. #
  193. # Once SAML support is enabled, a metadata file will be exposed at
  194. # https://<server>:<port>/_synapse/client/saml2/metadata.xml, which you may be able to
  195. # use to configure your SAML IdP with. Alternatively, you can manually configure
  196. # the IdP to use an ACS location of
  197. # https://<server>:<port>/_synapse/client/saml2/authn_response.
  198. #
  199. saml2_config:
  200. # `sp_config` is the configuration for the pysaml2 Service Provider.
  201. # See pysaml2 docs for format of config.
  202. #
  203. # Default values will be used for the 'entityid' and 'service' settings,
  204. # so it is not normally necessary to specify them unless you need to
  205. # override them.
  206. #
  207. sp_config:
  208. # Point this to the IdP's metadata. You must provide either a local
  209. # file via the `local` attribute or (preferably) a URL via the
  210. # `remote` attribute.
  211. #
  212. #metadata:
  213. # local: ["saml2/idp.xml"]
  214. # remote:
  215. # - url: https://our_idp/metadata.xml
  216. # Allowed clock difference in seconds between the homeserver and IdP.
  217. #
  218. # Uncomment the below to increase the accepted time difference from 0 to 3 seconds.
  219. #
  220. #accepted_time_diff: 3
  221. # By default, the user has to go to our login page first. If you'd like
  222. # to allow IdP-initiated login, set 'allow_unsolicited: true' in a
  223. # 'service.sp' section:
  224. #
  225. #service:
  226. # sp:
  227. # allow_unsolicited: true
  228. # The examples below are just used to generate our metadata xml, and you
  229. # may well not need them, depending on your setup. Alternatively you
  230. # may need a whole lot more detail - see the pysaml2 docs!
  231. #description: ["My awesome SP", "en"]
  232. #name: ["Test SP", "en"]
  233. #ui_info:
  234. # display_name:
  235. # - lang: en
  236. # text: "Display Name is the descriptive name of your service."
  237. # description:
  238. # - lang: en
  239. # text: "Description should be a short paragraph explaining the purpose of the service."
  240. # information_url:
  241. # - lang: en
  242. # text: "https://example.com/terms-of-service"
  243. # privacy_statement_url:
  244. # - lang: en
  245. # text: "https://example.com/privacy-policy"
  246. # keywords:
  247. # - lang: en
  248. # text: ["Matrix", "Element"]
  249. # logo:
  250. # - lang: en
  251. # text: "https://example.com/logo.svg"
  252. # width: "200"
  253. # height: "80"
  254. #organization:
  255. # name: Example com
  256. # display_name:
  257. # - ["Example co", "en"]
  258. # url: "http://example.com"
  259. #contact_person:
  260. # - given_name: Bob
  261. # sur_name: "the Sysadmin"
  262. # email_address": ["admin@example.com"]
  263. # contact_type": technical
  264. # Instead of putting the config inline as above, you can specify a
  265. # separate pysaml2 configuration file:
  266. #
  267. #config_path: "%(config_dir_path)s/sp_conf.py"
  268. # The lifetime of a SAML session. This defines how long a user has to
  269. # complete the authentication process, if allow_unsolicited is unset.
  270. # The default is 15 minutes.
  271. #
  272. #saml_session_lifetime: 5m
  273. # An external module can be provided here as a custom solution to
  274. # mapping attributes returned from a saml provider onto a matrix user.
  275. #
  276. user_mapping_provider:
  277. # The custom module's class. Uncomment to use a custom module.
  278. #
  279. #module: mapping_provider.SamlMappingProvider
  280. # Custom configuration values for the module. Below options are
  281. # intended for the built-in provider, they should be changed if
  282. # using a custom module. This section will be passed as a Python
  283. # dictionary to the module's `parse_config` method.
  284. #
  285. config:
  286. # The SAML attribute (after mapping via the attribute maps) to use
  287. # to derive the Matrix ID from. 'uid' by default.
  288. #
  289. # Note: This used to be configured by the
  290. # saml2_config.mxid_source_attribute option. If that is still
  291. # defined, its value will be used instead.
  292. #
  293. #mxid_source_attribute: displayName
  294. # The mapping system to use for mapping the saml attribute onto a
  295. # matrix ID.
  296. #
  297. # Options include:
  298. # * 'hexencode' (which maps unpermitted characters to '=xx')
  299. # * 'dotreplace' (which replaces unpermitted characters with
  300. # '.').
  301. # The default is 'hexencode'.
  302. #
  303. # Note: This used to be configured by the
  304. # saml2_config.mxid_mapping option. If that is still defined, its
  305. # value will be used instead.
  306. #
  307. #mxid_mapping: dotreplace
  308. # In previous versions of synapse, the mapping from SAML attribute to
  309. # MXID was always calculated dynamically rather than stored in a
  310. # table. For backwards- compatibility, we will look for user_ids
  311. # matching such a pattern before creating a new account.
  312. #
  313. # This setting controls the SAML attribute which will be used for this
  314. # backwards-compatibility lookup. Typically it should be 'uid', but if
  315. # the attribute maps are changed, it may be necessary to change it.
  316. #
  317. # The default is 'uid'.
  318. #
  319. #grandfathered_mxid_source_attribute: upn
  320. # It is possible to configure Synapse to only allow logins if SAML attributes
  321. # match particular values. The requirements can be listed under
  322. # `attribute_requirements` as shown below. All of the listed attributes must
  323. # match for the login to be permitted.
  324. #
  325. #attribute_requirements:
  326. # - attribute: userGroup
  327. # value: "staff"
  328. # - attribute: department
  329. # value: "sales"
  330. # If the metadata XML contains multiple IdP entities then the `idp_entityid`
  331. # option must be set to the entity to redirect users to.
  332. #
  333. # Most deployments only have a single IdP entity and so should omit this
  334. # option.
  335. #
  336. #idp_entityid: 'https://our_idp/entityid'
  337. """ % {
  338. "config_dir_path": config_dir_path
  339. }
  340. ATTRIBUTE_REQUIREMENTS_SCHEMA = {
  341. "type": "array",
  342. "items": SsoAttributeRequirement.JSON_SCHEMA,
  343. }
  344. def _parse_attribute_requirements_def(
  345. attribute_requirements: Any,
  346. ) -> List[SsoAttributeRequirement]:
  347. validate_config(
  348. ATTRIBUTE_REQUIREMENTS_SCHEMA,
  349. attribute_requirements,
  350. config_path=("saml2_config", "attribute_requirements"),
  351. )
  352. return [SsoAttributeRequirement(**x) for x in attribute_requirements]