saml2.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  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. config = getattr(mod, "CONFIG", None)
  136. if config is None:
  137. raise ConfigError(
  138. "Config path specified by saml2_config.config_path does not "
  139. "have a CONFIG property."
  140. )
  141. _dict_merge(merge_dict=config, into_dict=saml2_config_dict)
  142. import saml2.config
  143. self.saml2_sp_config = saml2.config.SPConfig()
  144. self.saml2_sp_config.load(saml2_config_dict)
  145. # session lifetime: in milliseconds
  146. self.saml2_session_lifetime = self.parse_duration(
  147. saml2_config.get("saml_session_lifetime", "15m")
  148. )
  149. def _default_saml_config_dict(
  150. self, required_attributes: set, optional_attributes: set
  151. ):
  152. """Generate a configuration dictionary with required and optional attributes that
  153. will be needed to process new user registration
  154. Args:
  155. required_attributes: SAML auth response attributes that are
  156. necessary to function
  157. optional_attributes: SAML auth response attributes that can be used to add
  158. additional information to Synapse user accounts, but are not required
  159. Returns:
  160. dict: A SAML configuration dictionary
  161. """
  162. import saml2
  163. public_baseurl = self.root.server.public_baseurl
  164. if public_baseurl is None:
  165. raise ConfigError("saml2_config requires a public_baseurl to be set")
  166. if self.saml2_grandfathered_mxid_source_attribute:
  167. optional_attributes.add(self.saml2_grandfathered_mxid_source_attribute)
  168. optional_attributes -= required_attributes
  169. metadata_url = public_baseurl + "_synapse/client/saml2/metadata.xml"
  170. response_url = public_baseurl + "_synapse/client/saml2/authn_response"
  171. return {
  172. "entityid": metadata_url,
  173. "service": {
  174. "sp": {
  175. "endpoints": {
  176. "assertion_consumer_service": [
  177. (response_url, saml2.BINDING_HTTP_POST)
  178. ]
  179. },
  180. "required_attributes": list(required_attributes),
  181. "optional_attributes": list(optional_attributes),
  182. # "name_id_format": saml2.saml.NAMEID_FORMAT_PERSISTENT,
  183. }
  184. },
  185. }
  186. def generate_config_section(self, config_dir_path, server_name, **kwargs):
  187. return """\
  188. ## Single sign-on integration ##
  189. # The following settings can be used to make Synapse use a single sign-on
  190. # provider for authentication, instead of its internal password database.
  191. #
  192. # You will probably also want to set the following options to `false` to
  193. # disable the regular login/registration flows:
  194. # * enable_registration
  195. # * password_config.enabled
  196. #
  197. # You will also want to investigate the settings under the "sso" configuration
  198. # section below.
  199. # Enable SAML2 for registration and login. Uses pysaml2.
  200. #
  201. # At least one of `sp_config` or `config_path` must be set in this section to
  202. # enable SAML login.
  203. #
  204. # Once SAML support is enabled, a metadata file will be exposed at
  205. # https://<server>:<port>/_synapse/client/saml2/metadata.xml, which you may be able to
  206. # use to configure your SAML IdP with. Alternatively, you can manually configure
  207. # the IdP to use an ACS location of
  208. # https://<server>:<port>/_synapse/client/saml2/authn_response.
  209. #
  210. saml2_config:
  211. # `sp_config` is the configuration for the pysaml2 Service Provider.
  212. # See pysaml2 docs for format of config.
  213. #
  214. # Default values will be used for the 'entityid' and 'service' settings,
  215. # so it is not normally necessary to specify them unless you need to
  216. # override them.
  217. #
  218. sp_config:
  219. # Point this to the IdP's metadata. You must provide either a local
  220. # file via the `local` attribute or (preferably) a URL via the
  221. # `remote` attribute.
  222. #
  223. #metadata:
  224. # local: ["saml2/idp.xml"]
  225. # remote:
  226. # - url: https://our_idp/metadata.xml
  227. # Allowed clock difference in seconds between the homeserver and IdP.
  228. #
  229. # Uncomment the below to increase the accepted time difference from 0 to 3 seconds.
  230. #
  231. #accepted_time_diff: 3
  232. # By default, the user has to go to our login page first. If you'd like
  233. # to allow IdP-initiated login, set 'allow_unsolicited: true' in a
  234. # 'service.sp' section:
  235. #
  236. #service:
  237. # sp:
  238. # allow_unsolicited: true
  239. # The examples below are just used to generate our metadata xml, and you
  240. # may well not need them, depending on your setup. Alternatively you
  241. # may need a whole lot more detail - see the pysaml2 docs!
  242. #description: ["My awesome SP", "en"]
  243. #name: ["Test SP", "en"]
  244. #ui_info:
  245. # display_name:
  246. # - lang: en
  247. # text: "Display Name is the descriptive name of your service."
  248. # description:
  249. # - lang: en
  250. # text: "Description should be a short paragraph explaining the purpose of the service."
  251. # information_url:
  252. # - lang: en
  253. # text: "https://example.com/terms-of-service"
  254. # privacy_statement_url:
  255. # - lang: en
  256. # text: "https://example.com/privacy-policy"
  257. # keywords:
  258. # - lang: en
  259. # text: ["Matrix", "Element"]
  260. # logo:
  261. # - lang: en
  262. # text: "https://example.com/logo.svg"
  263. # width: "200"
  264. # height: "80"
  265. #organization:
  266. # name: Example com
  267. # display_name:
  268. # - ["Example co", "en"]
  269. # url: "http://example.com"
  270. #contact_person:
  271. # - given_name: Bob
  272. # sur_name: "the Sysadmin"
  273. # email_address": ["admin@example.com"]
  274. # contact_type": technical
  275. # Instead of putting the config inline as above, you can specify a
  276. # separate pysaml2 configuration file:
  277. #
  278. #config_path: "%(config_dir_path)s/sp_conf.py"
  279. # The lifetime of a SAML session. This defines how long a user has to
  280. # complete the authentication process, if allow_unsolicited is unset.
  281. # The default is 15 minutes.
  282. #
  283. #saml_session_lifetime: 5m
  284. # An external module can be provided here as a custom solution to
  285. # mapping attributes returned from a saml provider onto a matrix user.
  286. #
  287. user_mapping_provider:
  288. # The custom module's class. Uncomment to use a custom module.
  289. #
  290. #module: mapping_provider.SamlMappingProvider
  291. # Custom configuration values for the module. Below options are
  292. # intended for the built-in provider, they should be changed if
  293. # using a custom module. This section will be passed as a Python
  294. # dictionary to the module's `parse_config` method.
  295. #
  296. config:
  297. # The SAML attribute (after mapping via the attribute maps) to use
  298. # to derive the Matrix ID from. 'uid' by default.
  299. #
  300. # Note: This used to be configured by the
  301. # saml2_config.mxid_source_attribute option. If that is still
  302. # defined, its value will be used instead.
  303. #
  304. #mxid_source_attribute: displayName
  305. # The mapping system to use for mapping the saml attribute onto a
  306. # matrix ID.
  307. #
  308. # Options include:
  309. # * 'hexencode' (which maps unpermitted characters to '=xx')
  310. # * 'dotreplace' (which replaces unpermitted characters with
  311. # '.').
  312. # The default is 'hexencode'.
  313. #
  314. # Note: This used to be configured by the
  315. # saml2_config.mxid_mapping option. If that is still defined, its
  316. # value will be used instead.
  317. #
  318. #mxid_mapping: dotreplace
  319. # In previous versions of synapse, the mapping from SAML attribute to
  320. # MXID was always calculated dynamically rather than stored in a
  321. # table. For backwards- compatibility, we will look for user_ids
  322. # matching such a pattern before creating a new account.
  323. #
  324. # This setting controls the SAML attribute which will be used for this
  325. # backwards-compatibility lookup. Typically it should be 'uid', but if
  326. # the attribute maps are changed, it may be necessary to change it.
  327. #
  328. # The default is 'uid'.
  329. #
  330. #grandfathered_mxid_source_attribute: upn
  331. # It is possible to configure Synapse to only allow logins if SAML attributes
  332. # match particular values. The requirements can be listed under
  333. # `attribute_requirements` as shown below. All of the listed attributes must
  334. # match for the login to be permitted.
  335. #
  336. #attribute_requirements:
  337. # - attribute: userGroup
  338. # value: "staff"
  339. # - attribute: department
  340. # value: "sales"
  341. # If the metadata XML contains multiple IdP entities then the `idp_entityid`
  342. # option must be set to the entity to redirect users to.
  343. #
  344. # Most deployments only have a single IdP entity and so should omit this
  345. # option.
  346. #
  347. #idp_entityid: 'https://our_idp/entityid'
  348. """ % {
  349. "config_dir_path": config_dir_path
  350. }
  351. ATTRIBUTE_REQUIREMENTS_SCHEMA = {
  352. "type": "array",
  353. "items": SsoAttributeRequirement.JSON_SCHEMA,
  354. }
  355. def _parse_attribute_requirements_def(
  356. attribute_requirements: Any,
  357. ) -> List[SsoAttributeRequirement]:
  358. validate_config(
  359. ATTRIBUTE_REQUIREMENTS_SCHEMA,
  360. attribute_requirements,
  361. config_path=("saml2_config", "attribute_requirements"),
  362. )
  363. return [SsoAttributeRequirement(**x) for x in attribute_requirements]