saml2_config.py 13 KB

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