saml2_config.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  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. import jinja2
  18. import pkg_resources
  19. from synapse.python_dependencies import DependencyException, check_requirements
  20. from synapse.util.module_loader import load_module, load_python_module
  21. from ._base import Config, ConfigError
  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(e.message)
  60. self.saml2_enabled = True
  61. self.saml2_grandfathered_mxid_source_attribute = saml2_config.get(
  62. "grandfathered_mxid_source_attribute", "uid"
  63. )
  64. # user_mapping_provider may be None if the key is present but has no value
  65. ump_dict = saml2_config.get("user_mapping_provider") or {}
  66. # Use the default user mapping provider if not set
  67. ump_dict.setdefault("module", DEFAULT_USER_MAPPING_PROVIDER)
  68. # Ensure a config is present
  69. ump_dict["config"] = ump_dict.get("config") or {}
  70. if ump_dict["module"] == DEFAULT_USER_MAPPING_PROVIDER:
  71. # Load deprecated options for use by the default module
  72. old_mxid_source_attribute = saml2_config.get("mxid_source_attribute")
  73. if old_mxid_source_attribute:
  74. logger.warning(
  75. "The config option saml2_config.mxid_source_attribute is deprecated. "
  76. "Please use saml2_config.user_mapping_provider.config"
  77. ".mxid_source_attribute instead."
  78. )
  79. ump_dict["config"]["mxid_source_attribute"] = old_mxid_source_attribute
  80. old_mxid_mapping = saml2_config.get("mxid_mapping")
  81. if old_mxid_mapping:
  82. logger.warning(
  83. "The config option saml2_config.mxid_mapping is deprecated. Please "
  84. "use saml2_config.user_mapping_provider.config.mxid_mapping instead."
  85. )
  86. ump_dict["config"]["mxid_mapping"] = old_mxid_mapping
  87. # Retrieve an instance of the module's class
  88. # Pass the config dictionary to the module for processing
  89. (
  90. self.saml2_user_mapping_provider_class,
  91. self.saml2_user_mapping_provider_config,
  92. ) = load_module(ump_dict)
  93. # Ensure loaded user mapping module has defined all necessary methods
  94. # Note parse_config() is already checked during the call to load_module
  95. required_methods = [
  96. "get_saml_attributes",
  97. "saml_response_to_user_attributes",
  98. "get_remote_user_id",
  99. ]
  100. missing_methods = [
  101. method
  102. for method in required_methods
  103. if not hasattr(self.saml2_user_mapping_provider_class, method)
  104. ]
  105. if missing_methods:
  106. raise ConfigError(
  107. "Class specified by saml2_config."
  108. "user_mapping_provider.module is missing required "
  109. "methods: %s" % (", ".join(missing_methods),)
  110. )
  111. # Get the desired saml auth response attributes from the module
  112. saml2_config_dict = self._default_saml_config_dict(
  113. *self.saml2_user_mapping_provider_class.get_saml_attributes(
  114. self.saml2_user_mapping_provider_config
  115. )
  116. )
  117. _dict_merge(
  118. merge_dict=saml2_config.get("sp_config", {}), into_dict=saml2_config_dict
  119. )
  120. config_path = saml2_config.get("config_path", None)
  121. if config_path is not None:
  122. mod = load_python_module(config_path)
  123. _dict_merge(merge_dict=mod.CONFIG, into_dict=saml2_config_dict)
  124. import saml2.config
  125. self.saml2_sp_config = saml2.config.SPConfig()
  126. self.saml2_sp_config.load(saml2_config_dict)
  127. # session lifetime: in milliseconds
  128. self.saml2_session_lifetime = self.parse_duration(
  129. saml2_config.get("saml_session_lifetime", "5m")
  130. )
  131. template_dir = saml2_config.get("template_dir")
  132. if not template_dir:
  133. template_dir = pkg_resources.resource_filename("synapse", "res/templates",)
  134. loader = jinja2.FileSystemLoader(template_dir)
  135. # enable auto-escape here, to having to remember to escape manually in the
  136. # template
  137. env = jinja2.Environment(loader=loader, autoescape=True)
  138. self.saml2_error_html_template = env.get_template("saml_error.html")
  139. def _default_saml_config_dict(
  140. self, required_attributes: set, optional_attributes: set
  141. ):
  142. """Generate a configuration dictionary with required and optional attributes that
  143. will be needed to process new user registration
  144. Args:
  145. required_attributes: SAML auth response attributes that are
  146. necessary to function
  147. optional_attributes: SAML auth response attributes that can be used to add
  148. additional information to Synapse user accounts, but are not required
  149. Returns:
  150. dict: A SAML configuration dictionary
  151. """
  152. import saml2
  153. public_baseurl = self.public_baseurl
  154. if public_baseurl is None:
  155. raise ConfigError("saml2_config requires a public_baseurl to be set")
  156. if self.saml2_grandfathered_mxid_source_attribute:
  157. optional_attributes.add(self.saml2_grandfathered_mxid_source_attribute)
  158. optional_attributes -= required_attributes
  159. metadata_url = public_baseurl + "_matrix/saml2/metadata.xml"
  160. response_url = public_baseurl + "_matrix/saml2/authn_response"
  161. return {
  162. "entityid": metadata_url,
  163. "service": {
  164. "sp": {
  165. "endpoints": {
  166. "assertion_consumer_service": [
  167. (response_url, saml2.BINDING_HTTP_POST)
  168. ]
  169. },
  170. "required_attributes": list(required_attributes),
  171. "optional_attributes": list(optional_attributes),
  172. # "name_id_format": saml2.saml.NAMEID_FORMAT_PERSISTENT,
  173. }
  174. },
  175. }
  176. def generate_config_section(self, config_dir_path, server_name, **kwargs):
  177. return """\
  178. ## Single sign-on integration ##
  179. # Enable SAML2 for registration and login. Uses pysaml2.
  180. #
  181. # At least one of `sp_config` or `config_path` must be set in this section to
  182. # enable SAML login.
  183. #
  184. # (You will probably also want to set the following options to `false` to
  185. # disable the regular login/registration flows:
  186. # * enable_registration
  187. # * password_config.enabled
  188. #
  189. # Once SAML support is enabled, a metadata file will be exposed at
  190. # https://<server>:<port>/_matrix/saml2/metadata.xml, which you may be able to
  191. # use to configure your SAML IdP with. Alternatively, you can manually configure
  192. # the IdP to use an ACS location of
  193. # https://<server>:<port>/_matrix/saml2/authn_response.
  194. #
  195. saml2_config:
  196. # `sp_config` is the configuration for the pysaml2 Service Provider.
  197. # See pysaml2 docs for format of config.
  198. #
  199. # Default values will be used for the 'entityid' and 'service' settings,
  200. # so it is not normally necessary to specify them unless you need to
  201. # override them.
  202. #
  203. #sp_config:
  204. # # point this to the IdP's metadata. You can use either a local file or
  205. # # (preferably) a URL.
  206. # metadata:
  207. # #local: ["saml2/idp.xml"]
  208. # remote:
  209. # - url: https://our_idp/metadata.xml
  210. #
  211. # # By default, the user has to go to our login page first. If you'd like
  212. # # to allow IdP-initiated login, set 'allow_unsolicited: true' in a
  213. # # 'service.sp' section:
  214. # #
  215. # #service:
  216. # # sp:
  217. # # allow_unsolicited: true
  218. #
  219. # # The examples below are just used to generate our metadata xml, and you
  220. # # may well not need them, depending on your setup. Alternatively you
  221. # # may need a whole lot more detail - see the pysaml2 docs!
  222. #
  223. # description: ["My awesome SP", "en"]
  224. # name: ["Test SP", "en"]
  225. #
  226. # organization:
  227. # name: Example com
  228. # display_name:
  229. # - ["Example co", "en"]
  230. # url: "http://example.com"
  231. #
  232. # contact_person:
  233. # - given_name: Bob
  234. # sur_name: "the Sysadmin"
  235. # email_address": ["admin@example.com"]
  236. # contact_type": technical
  237. # Instead of putting the config inline as above, you can specify a
  238. # separate pysaml2 configuration file:
  239. #
  240. #config_path: "%(config_dir_path)s/sp_conf.py"
  241. # The lifetime of a SAML session. This defines how long a user has to
  242. # complete the authentication process, if allow_unsolicited is unset.
  243. # The default is 5 minutes.
  244. #
  245. #saml_session_lifetime: 5m
  246. # An external module can be provided here as a custom solution to
  247. # mapping attributes returned from a saml provider onto a matrix user.
  248. #
  249. user_mapping_provider:
  250. # The custom module's class. Uncomment to use a custom module.
  251. #
  252. #module: mapping_provider.SamlMappingProvider
  253. # Custom configuration values for the module. Below options are
  254. # intended for the built-in provider, they should be changed if
  255. # using a custom module. This section will be passed as a Python
  256. # dictionary to the module's `parse_config` method.
  257. #
  258. config:
  259. # The SAML attribute (after mapping via the attribute maps) to use
  260. # to derive the Matrix ID from. 'uid' by default.
  261. #
  262. # Note: This used to be configured by the
  263. # saml2_config.mxid_source_attribute option. If that is still
  264. # defined, its value will be used instead.
  265. #
  266. #mxid_source_attribute: displayName
  267. # The mapping system to use for mapping the saml attribute onto a
  268. # matrix ID.
  269. #
  270. # Options include:
  271. # * 'hexencode' (which maps unpermitted characters to '=xx')
  272. # * 'dotreplace' (which replaces unpermitted characters with
  273. # '.').
  274. # The default is 'hexencode'.
  275. #
  276. # Note: This used to be configured by the
  277. # saml2_config.mxid_mapping option. If that is still defined, its
  278. # value will be used instead.
  279. #
  280. #mxid_mapping: dotreplace
  281. # In previous versions of synapse, the mapping from SAML attribute to
  282. # MXID was always calculated dynamically rather than stored in a
  283. # table. For backwards- compatibility, we will look for user_ids
  284. # matching such a pattern before creating a new account.
  285. #
  286. # This setting controls the SAML attribute which will be used for this
  287. # backwards-compatibility lookup. Typically it should be 'uid', but if
  288. # the attribute maps are changed, it may be necessary to change it.
  289. #
  290. # The default is 'uid'.
  291. #
  292. #grandfathered_mxid_source_attribute: upn
  293. # Directory in which Synapse will try to find the template files below.
  294. # If not set, default templates from within the Synapse package will be used.
  295. #
  296. # DO NOT UNCOMMENT THIS SETTING unless you want to customise the templates.
  297. # If you *do* uncomment it, you will need to make sure that all the templates
  298. # below are in the directory.
  299. #
  300. # Synapse will look for the following templates in this directory:
  301. #
  302. # * HTML page to display to users if something goes wrong during the
  303. # authentication process: 'saml_error.html'.
  304. #
  305. # When rendering, this template is given the following variables:
  306. # * code: an HTML error code corresponding to the error that is being
  307. # returned (typically 400 or 500)
  308. #
  309. # * msg: a textual message describing the error.
  310. #
  311. # The variables will automatically be HTML-escaped.
  312. #
  313. # You can see the default templates at:
  314. # https://github.com/matrix-org/synapse/tree/master/synapse/res/templates
  315. #
  316. #template_dir: "res/templates"
  317. """ % {
  318. "config_dir_path": config_dir_path
  319. }