saml2.py 16 KB

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