From 8d1a07975c0ea46fc9bde6adec256d95b44d39c4 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 12 Aug 2021 11:03:50 +0100 Subject: [PATCH 01/15] Allow using several custom template directories --- synapse/config/_base.py | 27 ++++++++++++++------------- synapse/config/account_validity.py | 4 ++-- synapse/config/emailconfig.py | 6 +++--- synapse/config/sso.py | 2 +- synapse/module_api/__init__.py | 2 +- tests/config/test_base.py | 6 +++--- 6 files changed, 24 insertions(+), 23 deletions(-) diff --git a/synapse/config/_base.py b/synapse/config/_base.py index d6ec618f8f52..70ac9c5e39c4 100644 --- a/synapse/config/_base.py +++ b/synapse/config/_base.py @@ -237,7 +237,7 @@ def read_template(self, filename: str) -> jinja2.Template: def read_templates( self, filenames: List[str], - custom_template_directory: Optional[str] = None, + custom_template_directories: Optional[List[str]] = None, ) -> List[jinja2.Template]: """Load a list of template files from disk using the given variables. @@ -251,8 +251,8 @@ def read_templates( Args: filenames: A list of template filenames to read. - custom_template_directory: A directory to try to look for the templates - before using the default Synapse template directory instead. + custom_template_directories: A list of directories to try to look for the + templates before using the default Synapse template directory instead. Raises: ConfigError: if the file's path is incorrect or otherwise cannot be read. @@ -262,18 +262,19 @@ def read_templates( """ search_directories = [self.default_template_dir] - # The loader will first look in the custom template directory (if specified) for the + # The loader will first look in the custom template directories (if specified) for the # given filename. If it doesn't find it, it will use the default template dir instead - if custom_template_directory: - # Check that the given template directory exists - if not self.path_exists(custom_template_directory): - raise ConfigError( - "Configured template directory does not exist: %s" - % (custom_template_directory,) - ) + if custom_template_directories: + for custom_template_directory in custom_template_directories: + # Check that the given template directory exists + if not self.path_exists(custom_template_directory): + raise ConfigError( + "Configured template directory does not exist: %s" + % (custom_template_directory,) + ) - # Search the custom template directory as well - search_directories.insert(0, custom_template_directory) + # Search the custom template directory as well + search_directories.insert(0, custom_template_directory) # TODO: switch to synapse.util.templates.build_jinja_env loader = jinja2.FileSystemLoader(search_directories) diff --git a/synapse/config/account_validity.py b/synapse/config/account_validity.py index 6be4eafe5582..dcf419cf5152 100644 --- a/synapse/config/account_validity.py +++ b/synapse/config/account_validity.py @@ -86,7 +86,7 @@ def read_config(self, config, **kwargs): [ account_renewed_template_filename, "account_previously_renewed.html", - invalid_token_template_filename, + [invalid_token_template_filename], ], - account_validity_template_dir, + [account_validity_template_dir], ) diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 42526502f0e2..f51c1f71123e 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -257,7 +257,7 @@ def read_config(self, config, **kwargs): registration_template_success_html, add_threepid_template_success_html, ], - template_dir, + [template_dir], ) # Render templates that do not contain any placeholders @@ -297,7 +297,7 @@ def read_config(self, config, **kwargs): self.email_notif_template_text, ) = self.read_templates( [notif_template_html, notif_template_text], - template_dir, + [template_dir], ) self.email_notif_for_new_users = email_config.get( @@ -320,7 +320,7 @@ def read_config(self, config, **kwargs): self.account_validity_template_text, ) = self.read_templates( [expiry_template_html, expiry_template_text], - template_dir, + [template_dir], ) subjects_config = email_config.get("subjects", {}) diff --git a/synapse/config/sso.py b/synapse/config/sso.py index d0f04cf8e6b2..a616bf9661b9 100644 --- a/synapse/config/sso.py +++ b/synapse/config/sso.py @@ -63,7 +63,7 @@ def read_config(self, config, **kwargs): "sso_auth_success.html", "sso_auth_bad_user.html", ], - self.sso_template_dir, + [self.sso_template_dir], ) # These templates have no placeholders, so render them here diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py index 1cc13fc97b22..06562d54119c 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py @@ -677,7 +677,7 @@ def read_templates( A list containing the loaded templates, with the orders matching the one of the filenames parameter. """ - return self._hs.config.read_templates(filenames, custom_template_directory) + return self._hs.config.read_templates(filenames, [custom_template_directory]) class PublicRoomListManager: diff --git a/tests/config/test_base.py b/tests/config/test_base.py index 84ae3b88ae9b..b940a086a682 100644 --- a/tests/config/test_base.py +++ b/tests/config/test_base.py @@ -30,7 +30,7 @@ def test_loading_missing_templates(self): # contain template files with tempfile.TemporaryDirectory() as tmp_dir: # Attempt to load an HTML template from our custom template directory - template = self.hs.config.read_templates(["sso_error.html"], tmp_dir)[0] + template = self.hs.config.read_templates(["sso_error.html"], [tmp_dir])[0] # If no errors, we should've gotten the default template instead @@ -60,7 +60,7 @@ def test_loading_custom_templates(self): # Attempt to load the template from our custom template directory template = ( - self.hs.config.read_templates([template_filename], tmp_dir) + self.hs.config.read_templates([template_filename], [tmp_dir]) )[0] # Render the template @@ -77,5 +77,5 @@ def test_loading_custom_templates(self): def test_loading_template_from_nonexistent_custom_directory(self): with self.assertRaises(ConfigError): self.hs.config.read_templates( - ["some_filename.html"], "a_nonexistent_directory" + ["some_filename.html"], ["a_nonexistent_directory"] ) From 70298e02db2bcda6340238a87e775f5def1e8954 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 13 Aug 2021 13:57:49 +0100 Subject: [PATCH 02/15] Centralise templates into a single directory Except for the consent template directory, since it requires a specific hierarchy --- docs/SUMMARY.md | 1 + docs/sample_config.yaml | 226 ++--------------- docs/templates.md | 239 ++++++++++++++++++ synapse/config/account_validity.py | 6 +- synapse/config/emailconfig.py | 55 +--- synapse/config/server.py | 16 ++ synapse/config/sso.py | 171 +------------ synapse/module_api/__init__.py | 5 +- .../rest/synapse/client/new_user_consent.py | 2 + synapse/rest/synapse/client/pick_username.py | 2 + 10 files changed, 304 insertions(+), 419 deletions(-) create mode 100644 docs/templates.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 3d320a1c43ea..56e0141c2b3a 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -21,6 +21,7 @@ - [Homeserver Sample Config File](usage/configuration/homeserver_sample_config.md) - [Logging Sample Config File](usage/configuration/logging_sample_config.md) - [Structured Logging](structured_logging.md) + - [Templates](templates.md) - [User Authentication](usage/configuration/user_authentication/README.md) - [Single-Sign On]() - [OpenID Connect](openid.md) diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index aeebcaf45ff0..3849875872d7 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -565,6 +565,20 @@ retention: # #next_link_domain_whitelist: ["matrix.org"] +# Templates to use when generating email or HTML page contents. +# +templates: + # Directory in which Synapse will try to find template files to use to generate + # email or HTML page contents. + # If not set, or a file is not found within the template directory, a default + # template from within the Synapse package will be used. + # + # See https://matrix-org.github.io/synapse/latest/templates.html for more + # information about using custom templates. + # + #custom_template_directory: /path/to/custom/templates/ + + ## TLS ## @@ -1895,6 +1909,9 @@ cas_config: # Additional settings to use with single-sign on systems such as OpenID Connect, # SAML2 and CAS. # +# Server admins can configure custom templates for pages related to SSO. See +# https://matrix-org.github.io/synapse/latest/templates.html for more information. +# sso: # A list of client URLs which are whitelisted so that the user does not # have to confirm giving access to their account to the URL. Any client @@ -1927,169 +1944,6 @@ sso: # #update_profile_information: true - # Directory in which Synapse will try to find the template files below. - # If not set, or the files named below are not found within the template - # directory, default templates from within the Synapse package will be used. - # - # Synapse will look for the following templates in this directory: - # - # * HTML page to prompt the user to choose an Identity Provider during - # login: 'sso_login_idp_picker.html'. - # - # This is only used if multiple SSO Identity Providers are configured. - # - # When rendering, this template is given the following variables: - # * redirect_url: the URL that the user will be redirected to after - # login. - # - # * server_name: the homeserver's name. - # - # * providers: a list of available Identity Providers. Each element is - # an object with the following attributes: - # - # * idp_id: unique identifier for the IdP - # * idp_name: user-facing name for the IdP - # * idp_icon: if specified in the IdP config, an MXC URI for an icon - # for the IdP - # * idp_brand: if specified in the IdP config, a textual identifier - # for the brand of the IdP - # - # The rendered HTML page should contain a form which submits its results - # back as a GET request, with the following query parameters: - # - # * redirectUrl: the client redirect URI (ie, the `redirect_url` passed - # to the template) - # - # * idp: the 'idp_id' of the chosen IDP. - # - # * HTML page to prompt new users to enter a userid and confirm other - # details: 'sso_auth_account_details.html'. This is only shown if the - # SSO implementation (with any user_mapping_provider) does not return - # a localpart. - # - # When rendering, this template is given the following variables: - # - # * server_name: the homeserver's name. - # - # * idp: details of the SSO Identity Provider that the user logged in - # with: an object with the following attributes: - # - # * idp_id: unique identifier for the IdP - # * idp_name: user-facing name for the IdP - # * idp_icon: if specified in the IdP config, an MXC URI for an icon - # for the IdP - # * idp_brand: if specified in the IdP config, a textual identifier - # for the brand of the IdP - # - # * user_attributes: an object containing details about the user that - # we received from the IdP. May have the following attributes: - # - # * display_name: the user's display_name - # * emails: a list of email addresses - # - # The template should render a form which submits the following fields: - # - # * username: the localpart of the user's chosen user id - # - # * HTML page allowing the user to consent to the server's terms and - # conditions. This is only shown for new users, and only if - # `user_consent.require_at_registration` is set. - # - # When rendering, this template is given the following variables: - # - # * server_name: the homeserver's name. - # - # * user_id: the user's matrix proposed ID. - # - # * user_profile.display_name: the user's proposed display name, if any. - # - # * consent_version: the version of the terms that the user will be - # shown - # - # * terms_url: a link to the page showing the terms. - # - # The template should render a form which submits the following fields: - # - # * accepted_version: the version of the terms accepted by the user - # (ie, 'consent_version' from the input variables). - # - # * HTML page for a confirmation step before redirecting back to the client - # with the login token: 'sso_redirect_confirm.html'. - # - # When rendering, this template is given the following variables: - # - # * redirect_url: the URL the user is about to be redirected to. - # - # * display_url: the same as `redirect_url`, but with the query - # parameters stripped. The intention is to have a - # human-readable URL to show to users, not to use it as - # the final address to redirect to. - # - # * server_name: the homeserver's name. - # - # * new_user: a boolean indicating whether this is the user's first time - # logging in. - # - # * user_id: the user's matrix ID. - # - # * user_profile.avatar_url: an MXC URI for the user's avatar, if any. - # None if the user has not set an avatar. - # - # * user_profile.display_name: the user's display name. None if the user - # has not set a display name. - # - # * HTML page which notifies the user that they are authenticating to confirm - # an operation on their account during the user interactive authentication - # process: 'sso_auth_confirm.html'. - # - # When rendering, this template is given the following variables: - # * redirect_url: the URL the user is about to be redirected to. - # - # * description: the operation which the user is being asked to confirm - # - # * idp: details of the Identity Provider that we will use to confirm - # the user's identity: an object with the following attributes: - # - # * idp_id: unique identifier for the IdP - # * idp_name: user-facing name for the IdP - # * idp_icon: if specified in the IdP config, an MXC URI for an icon - # for the IdP - # * idp_brand: if specified in the IdP config, a textual identifier - # for the brand of the IdP - # - # * HTML page shown after a successful user interactive authentication session: - # 'sso_auth_success.html'. - # - # Note that this page must include the JavaScript which notifies of a successful authentication - # (see https://matrix.org/docs/spec/client_server/r0.6.0#fallback). - # - # This template has no additional variables. - # - # * HTML page shown after a user-interactive authentication session which - # does not map correctly onto the expected user: 'sso_auth_bad_user.html'. - # - # When rendering, this template is given the following variables: - # * server_name: the homeserver's name. - # * user_id_to_verify: the MXID of the user that we are trying to - # validate. - # - # * HTML page shown during single sign-on if a deactivated user (according to Synapse's database) - # attempts to login: 'sso_account_deactivated.html'. - # - # This template has no additional variables. - # - # * HTML page to display to users if something goes wrong during the - # OpenID Connect authentication process: 'sso_error.html'. - # - # When rendering, this template is given two variables: - # * error: the technical name of the error - # * error_description: a human-readable message for the error - # - # You can see the default templates at: - # https://github.com/matrix-org/synapse/tree/master/synapse/res/templates - # - #template_dir: "res/templates" - # JSON web token integration. The following settings can be used to make # Synapse JSON web tokens for authentication, instead of its internal @@ -2220,6 +2074,9 @@ ui_auth: # Configuration for sending emails from Synapse. # +# Server admins can configure custom templates for email content. See +# https://matrix-org.github.io/synapse/latest/templates.html for more information. +# email: # The hostname of the outgoing SMTP server to use. Defaults to 'localhost'. # @@ -2296,49 +2153,6 @@ email: # #invite_client_location: https://app.element.io - # Directory in which Synapse will try to find the template files below. - # If not set, or the files named below are not found within the template - # directory, default templates from within the Synapse package will be used. - # - # Synapse will look for the following templates in this directory: - # - # * The contents of email notifications of missed events: 'notif_mail.html' and - # 'notif_mail.txt'. - # - # * The contents of account expiry notice emails: 'notice_expiry.html' and - # 'notice_expiry.txt'. - # - # * The contents of password reset emails sent by the homeserver: - # 'password_reset.html' and 'password_reset.txt' - # - # * An HTML page that a user will see when they follow the link in the password - # reset email. The user will be asked to confirm the action before their - # password is reset: 'password_reset_confirmation.html' - # - # * HTML pages for success and failure that a user will see when they confirm - # the password reset flow using the page above: 'password_reset_success.html' - # and 'password_reset_failure.html' - # - # * The contents of address verification emails sent during registration: - # 'registration.html' and 'registration.txt' - # - # * HTML pages for success and failure that a user will see when they follow - # the link in an address verification email sent during registration: - # 'registration_success.html' and 'registration_failure.html' - # - # * The contents of address verification emails sent when an address is added - # to a Matrix account: 'add_threepid.html' and 'add_threepid.txt' - # - # * HTML pages for success and failure that a user will see when they follow - # the link in an address verification email sent when an address is added - # to a Matrix account: 'add_threepid_success.html' and - # 'add_threepid_failure.html' - # - # You can see the default templates at: - # https://github.com/matrix-org/synapse/tree/master/synapse/res/templates - # - #template_dir: "res/templates" - # Subjects to use when sending emails from Synapse. # # The placeholder '%(app)s' will be replaced with the value of the 'app_name' diff --git a/docs/templates.md b/docs/templates.md new file mode 100644 index 000000000000..a240f58b54fd --- /dev/null +++ b/docs/templates.md @@ -0,0 +1,239 @@ +# Templates + +Synapse uses parametrised templates to generate the content of emails it sends and +webpages it shows to users. + +By default, Synapse will use the templates listed [here](https://github.com/matrix-org/synapse/tree/master/synapse/res/templates). +Server admins can configure an additional directory for Synapse to look for templates +in, allowing them to specify custom templates: + +```yaml +templates: + custom_templates_directory: /path/to/custom/templates/ +``` + +If this setting is not set, or the files named below are not found within the directory, +default templates from within the Synapse package will be used. + +Templates that are given variables when being rendered are rendered using [Jinja 2](https://jinja.palletsprojects.com/en/2.11.x/). +Templates rendered by Jinja 2 can also access two functions on top of the functions +already available as part of Jinja 2: + +```python +format_ts(value: int, format: str) -> str +``` + +Formats a timestamp in milliseconds. + +Example: `reason.last_sent_ts|format_ts("%c")` + +```python +mxc_to_http(value: str, width: int, height: int, resize_method: str = "crop") -> str +``` + +Turns a `mxc://` URL for media content into an HTTP(S) one using the homeserver's +`public_baseurl` configuration setting as the URL's base. + +Example: `message.sender_avatar_url|mxc_to_http(32,32)` + + +## Email templates + +Below are the templates Synapse will look for when generating the content of an email: + +* `notif_mail.html` and `notif_mail.txt`: The contents of email notifications of missed + events. + When rendering, this template is given the following variables: + * `user_display_name`: the display name for the user receiving the notification + * `unsubscribe_link`: the link users can click to unsubscribe from email notifications + * `summary_text`: a summary of the notification(s). The text used can be customised + by configuring the various settings in the `email.subjects` section of the + configuration file. + * `rooms`: a list of rooms containing events to include in the email. Each element is + an object with the following attributes: + * `title`: a human-readable name for the room + * `hash`: a hash of the ID of the room + * `invite`: a boolean, which is `True` if the room is an invite the user hasn't + accepted yet, `False` otherwise + * `notifs`: a list of events, or an empty list if `invite` is `True`. Each element + is an object with the following attributes: + * `link`: a `matrix.to` link to the event + * `ts`: the time in milliseconds at which the event was received + * `messages`: a list of messages containing one message before the event, the + message in the event, and one message after the event. Each element is an + object with the following attributes: + * `event_type`: the type of the event + * `is_historical`: a boolean, which is `False` if the message is the one + that triggered the notification, `True` otherwise + * `id`: the ID of the event + * `ts`: the time in milliseconds at which the event was sent + * `sender_name`: the display name for the event's sender + * `sender_avatar_url`: the avatar URL (as a `mxc://` URL) for the event's + sender + * `sender_hash`: a hash of the user ID of the sender + * `link`: a `matrix.to` link to the room + * `reason`: information on the event that triggered the email to be sent. It's an + object with the following attributes: + * `room_id`: the ID of the room the event was sent in + * `room_name`: a human-readable name for the room the event was sent in + * `now`: the current time in milliseconds + * `received_at`: the time in milliseconds at which the event was received + * `delay_before_mail_ms`: the amount of time in milliseconds Synapse always waits + before ever emailing about a notification (to give the user a chance to respond + to other push or notice the window) + * `last_sent_ts`: the time in milliseconds at which a notification was last sent + for an event in this room + * `throttle_ms`: the minimum amount of time in milliseconds between two + notifications can be sent for this room +* `password_reset.html` and `password_reset.txt`: The contents of password reset emails + sent by the homeserver. + When rendering, these templates are given a `link` variable which contains the link the + user must click in order to reset their password. +* `registration.html` and `registration.txt`: The contents of address verification emails + sent during registration. + When rendering, these templates are given a `link` variable which contains the link the + user must click in order to validate their email address. +* `add_threepid.html` and `add_threepid.txt`: The contents of address verification emails + sent when an address is added to a Matrix account. + When rendering, these templates are given a `link` variable which contains the link the + user must click in order to validate their email address. + + +## HTML page templates for registration and password reset + +Below are the templates Synapse will look for when generating pages related to +registration and password reset: + +* `password_reset_confirmation.html`: An HTML page that a user will see when they follow + the link in the password reset email. The user will be asked to confirm the action + before their password is reset. + When rendering, this template is given the following variables: + * `sid`: the session ID for the password reset + * `token`: the token for the password reset + * `client_secret`: the client secret for the password reset +* `password_reset_success.html` and `password_reset_failure.html`: HTML pages for success + and failure that a user will see when they confirm the password reset flow using the + page above. + When rendering, `password_reset_success.html` is given no variable, and + `password_reset_failure.html` is given a `failure_reason`, which contains the reason + for the password reset failure. +* `registration_success.html` and `registration_failure.html`: HTML pages for success and + failure that a user will see when they follow the link in an address verification email + sent during registration. + When rendering, `registration_success.html` is given no variable, and + `registration_failure.html` is given a `failure_reason`, which contains the reason + for the registration failure. +* `add_threepid_success.html` and `add_threepid_failure.html`: HTML pages for success and + failure that a user will see when they follow the link in an address verification email + sent when an address is added to a Matrix account. + When rendering, `add_threepid_success.html` is given no variable, and + `add_threepid_failure.html` is given a `failure_reason`, which contains the reason + for the registration failure. + + +## HTML page templates for Single Sign-On (SSO) + +Below are the templates Synapse will look for when generating pages related to SSO: + +* `sso_login_idp_picker.html`: HTML page to prompt the user to choose an + Identity Provider during login. + This is only used if multiple SSO Identity Providers are configured. + When rendering, this template is given the following variables: + * `redirect_url`: the URL that the user will be redirected to after + login. + * `server_name`: the homeserver's name. + * `providers`: a list of available Identity Providers. Each element is + an object with the following attributes: + * `idp_id`: unique identifier for the IdP + * `idp_name`: user-facing name for the IdP + * `idp_icon`: if specified in the IdP config, an MXC URI for an icon + for the IdP + * `idp_brand`: if specified in the IdP config, a textual identifier + for the brand of the IdP + The rendered HTML page should contain a form which submits its results + back as a GET request, with the following query parameters: + * `redirectUrl`: the client redirect URI (ie, the `redirect_url` passed + to the template) + * `idp`: the 'idp_id' of the chosen IDP. +* `sso_auth_account_details.html`: HTML page to prompt new users to enter a + userid and confirm other details. This is only shown if the + SSO implementation (with any `user_mapping_provider`) does not return + a localpart. + When rendering, this template is given the following variables: + * `server_name`: the homeserver's name. + * `idp`: details of the SSO Identity Provider that the user logged in + with: an object with the following attributes: + * `idp_id`: unique identifier for the IdP + * `idp_name`: user-facing name for the IdP + * `idp_icon`: if specified in the IdP config, an MXC URI for an icon + for the IdP + * `idp_brand`: if specified in the IdP config, a textual identifier + for the brand of the IdP + * `user_attributes`: an object containing details about the user that + we received from the IdP. May have the following attributes: + * display_name: the user's display_name + * emails: a list of email addresses + The template should render a form which submits the following fields: + * `username`: the localpart of the user's chosen user id +* `sso_new_user_consent.html`: HTML page allowing the user to consent to the + server's terms and conditions. This is only shown for new users, and only if + `user_consent.require_at_registration` is set. + When rendering, this template is given the following variables: + * `server_name`: the homeserver's name. + * `user_id`: the user's matrix proposed ID. + * `user_profile.display_name`: the user's proposed display name, if any. + * consent_version: the version of the terms that the user will be + shown + * `terms_url`: a link to the page showing the terms. + The template should render a form which submits the following fields: + * `accepted_version`: the version of the terms accepted by the user + (ie, 'consent_version' from the input variables). +* `sso_redirect_confirm.html`: HTML page for a confirmation step before redirecting back + to the client with the login token. + When rendering, this template is given the following variables: + * `redirect_url`: the URL the user is about to be redirected to. + * `display_url`: the same as `redirect_url`, but with the query + parameters stripped. The intention is to have a + human-readable URL to show to users, not to use it as + the final address to redirect to. + * `server_name`: the homeserver's name. + * `new_user`: a boolean indicating whether this is the user's first time + logging in. + * `user_id`: the user's matrix ID. + * `user_profile.avatar_url`: an MXC URI for the user's avatar, if any. + `None` if the user has not set an avatar. + * `user_profile.display_name`: the user's display name. `None` if the user + has not set a display name. +* `sso_auth_confirm.html`: HTML page which notifies the user that they are authenticating + to confirm an operation on their account during the user interactive authentication + process. + When rendering, this template is given the following variables: + * `redirect_url`: the URL the user is about to be redirected to. + * `description`: the operation which the user is being asked to confirm + * `idp`: details of the Identity Provider that we will use to confirm + the user's identity: an object with the following attributes: + * `idp_id`: unique identifier for the IdP + * `idp_name`: user-facing name for the IdP + * `idp_icon`: if specified in the IdP config, an MXC URI for an icon + for the IdP + * `idp_brand`: if specified in the IdP config, a textual identifier + for the brand of the IdP +* `sso_auth_success.html`: HTML page shown after a successful user interactive + authentication session. + Note that this page must include the JavaScript which notifies of a successful + authentication (see https://matrix.org/docs/spec/client_server/r0.6.0#fallback). + This template has no additional variables. +* `sso_auth_bad_user.html`: HTML page shown after a user-interactive authentication + session which does not map correctly onto the expected user. + When rendering, this template is given the following variables: + * `server_name`: the homeserver's name. + * `user_id_to_verify`: the MXID of the user that we are trying to + validate. +* `sso_account_deactivated.html`: HTML page shown during single sign-on if a deactivated + user (according to Synapse's database) attempts to login. + This template has no additional variables. +* `sso_error.html`: HTML page to display to users if something goes wrong during the + OpenID Connect authentication process. + When rendering, this template is given two variables: + * `error`: the technical name of the error + * `error_description`: a human-readable message for the error diff --git a/synapse/config/account_validity.py b/synapse/config/account_validity.py index dcf419cf5152..807ca1304a91 100644 --- a/synapse/config/account_validity.py +++ b/synapse/config/account_validity.py @@ -78,6 +78,8 @@ def read_config(self, config, **kwargs): ) # Read and store template content + templates_config = config.get("templates", {}) + custom_template_directory = templates_config.get("custom_template_directory") ( self.account_validity_account_renewed_template, self.account_validity_account_previously_renewed_template, @@ -86,7 +88,7 @@ def read_config(self, config, **kwargs): [ account_renewed_template_filename, "account_previously_renewed.html", - [invalid_token_template_filename], + invalid_token_template_filename, ], - [account_validity_template_dir], + [custom_template_directory, account_validity_template_dir], ) diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index f51c1f71123e..a6560451c3b8 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -168,6 +168,9 @@ def read_config(self, config, **kwargs): email_config.get("validation_token_lifetime", "1h") ) + templates_config = config.get("templates", {}) + custom_template_directory = templates_config.get("custom_template_directory") + if self.threepid_behaviour_email == ThreepidBehaviour.LOCAL: missing = [] if not self.email_notif_from: @@ -257,7 +260,7 @@ def read_config(self, config, **kwargs): registration_template_success_html, add_threepid_template_success_html, ], - [template_dir], + [custom_template_directory, template_dir], ) # Render templates that do not contain any placeholders @@ -297,7 +300,7 @@ def read_config(self, config, **kwargs): self.email_notif_template_text, ) = self.read_templates( [notif_template_html, notif_template_text], - [template_dir], + [custom_template_directory, template_dir], ) self.email_notif_for_new_users = email_config.get( @@ -320,7 +323,7 @@ def read_config(self, config, **kwargs): self.account_validity_template_text, ) = self.read_templates( [expiry_template_html, expiry_template_text], - [template_dir], + [custom_template_directory, template_dir], ) subjects_config = email_config.get("subjects", {}) @@ -352,6 +355,9 @@ def generate_config_section(self, config_dir_path, server_name, **kwargs): """\ # Configuration for sending emails from Synapse. # + # Server admins can configure custom templates for email content. See + # https://matrix-org.github.io/synapse/latest/templates.html for more information. + # email: # The hostname of the outgoing SMTP server to use. Defaults to 'localhost'. # @@ -428,49 +434,6 @@ def generate_config_section(self, config_dir_path, server_name, **kwargs): # #invite_client_location: https://app.element.io - # Directory in which Synapse will try to find the template files below. - # If not set, or the files named below are not found within the template - # directory, default templates from within the Synapse package will be used. - # - # Synapse will look for the following templates in this directory: - # - # * The contents of email notifications of missed events: 'notif_mail.html' and - # 'notif_mail.txt'. - # - # * The contents of account expiry notice emails: 'notice_expiry.html' and - # 'notice_expiry.txt'. - # - # * The contents of password reset emails sent by the homeserver: - # 'password_reset.html' and 'password_reset.txt' - # - # * An HTML page that a user will see when they follow the link in the password - # reset email. The user will be asked to confirm the action before their - # password is reset: 'password_reset_confirmation.html' - # - # * HTML pages for success and failure that a user will see when they confirm - # the password reset flow using the page above: 'password_reset_success.html' - # and 'password_reset_failure.html' - # - # * The contents of address verification emails sent during registration: - # 'registration.html' and 'registration.txt' - # - # * HTML pages for success and failure that a user will see when they follow - # the link in an address verification email sent during registration: - # 'registration_success.html' and 'registration_failure.html' - # - # * The contents of address verification emails sent when an address is added - # to a Matrix account: 'add_threepid.html' and 'add_threepid.txt' - # - # * HTML pages for success and failure that a user will see when they follow - # the link in an address verification email sent when an address is added - # to a Matrix account: 'add_threepid_success.html' and - # 'add_threepid_failure.html' - # - # You can see the default templates at: - # https://github.com/matrix-org/synapse/tree/master/synapse/res/templates - # - #template_dir: "res/templates" - # Subjects to use when sending emails from Synapse. # # The placeholder '%%(app)s' will be replaced with the value of the 'app_name' diff --git a/synapse/config/server.py b/synapse/config/server.py index 187b4301a04b..64c15d84327a 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -710,6 +710,9 @@ class LimitRemoteRoomsConfig: # Turn the list into a set to improve lookup speed. self.next_link_domain_whitelist = set(next_link_domain_whitelist) + templates_config = config.get("templates", {}) + self.custom_template_directory = templates_config.get("custom_template_directory") + def has_tls_listener(self) -> bool: return any(listener.tls for listener in self.listeners) @@ -1284,6 +1287,19 @@ def generate_config_section( # all domains. # #next_link_domain_whitelist: ["matrix.org"] + + # Templates to use when generating email or HTML page contents. + # + templates: + # Directory in which Synapse will try to find template files to use to generate + # email or HTML page contents. + # If not set, or a file is not found within the template directory, a default + # template from within the Synapse package will be used. + # + # See https://matrix-org.github.io/synapse/latest/templates.html for more + # information about using custom templates. + # + #custom_template_directory: /path/to/custom/templates/ """ % locals() ) diff --git a/synapse/config/sso.py b/synapse/config/sso.py index a616bf9661b9..94a141bdda4b 100644 --- a/synapse/config/sso.py +++ b/synapse/config/sso.py @@ -45,6 +45,9 @@ def read_config(self, config, **kwargs): self.sso_template_dir = sso_config.get("template_dir") # Read templates from disk + templates_config = config.get("templates", {}) + custom_template_directory = templates_config.get("custom_template_directory") + ( self.sso_login_idp_picker_template, self.sso_redirect_confirm_template, @@ -63,7 +66,7 @@ def read_config(self, config, **kwargs): "sso_auth_success.html", "sso_auth_bad_user.html", ], - [self.sso_template_dir], + [custom_template_directory, self.sso_template_dir], ) # These templates have no placeholders, so render them here @@ -94,6 +97,9 @@ def generate_config_section(self, **kwargs): # Additional settings to use with single-sign on systems such as OpenID Connect, # SAML2 and CAS. # + # Server admins can configure custom templates for pages related to SSO. See + # https://matrix-org.github.io/synapse/latest/templates.html for more information. + # sso: # A list of client URLs which are whitelisted so that the user does not # have to confirm giving access to their account to the URL. Any client @@ -125,167 +131,4 @@ def generate_config_section(self, **kwargs): # information when first signing in. Defaults to false. # #update_profile_information: true - - # Directory in which Synapse will try to find the template files below. - # If not set, or the files named below are not found within the template - # directory, default templates from within the Synapse package will be used. - # - # Synapse will look for the following templates in this directory: - # - # * HTML page to prompt the user to choose an Identity Provider during - # login: 'sso_login_idp_picker.html'. - # - # This is only used if multiple SSO Identity Providers are configured. - # - # When rendering, this template is given the following variables: - # * redirect_url: the URL that the user will be redirected to after - # login. - # - # * server_name: the homeserver's name. - # - # * providers: a list of available Identity Providers. Each element is - # an object with the following attributes: - # - # * idp_id: unique identifier for the IdP - # * idp_name: user-facing name for the IdP - # * idp_icon: if specified in the IdP config, an MXC URI for an icon - # for the IdP - # * idp_brand: if specified in the IdP config, a textual identifier - # for the brand of the IdP - # - # The rendered HTML page should contain a form which submits its results - # back as a GET request, with the following query parameters: - # - # * redirectUrl: the client redirect URI (ie, the `redirect_url` passed - # to the template) - # - # * idp: the 'idp_id' of the chosen IDP. - # - # * HTML page to prompt new users to enter a userid and confirm other - # details: 'sso_auth_account_details.html'. This is only shown if the - # SSO implementation (with any user_mapping_provider) does not return - # a localpart. - # - # When rendering, this template is given the following variables: - # - # * server_name: the homeserver's name. - # - # * idp: details of the SSO Identity Provider that the user logged in - # with: an object with the following attributes: - # - # * idp_id: unique identifier for the IdP - # * idp_name: user-facing name for the IdP - # * idp_icon: if specified in the IdP config, an MXC URI for an icon - # for the IdP - # * idp_brand: if specified in the IdP config, a textual identifier - # for the brand of the IdP - # - # * user_attributes: an object containing details about the user that - # we received from the IdP. May have the following attributes: - # - # * display_name: the user's display_name - # * emails: a list of email addresses - # - # The template should render a form which submits the following fields: - # - # * username: the localpart of the user's chosen user id - # - # * HTML page allowing the user to consent to the server's terms and - # conditions. This is only shown for new users, and only if - # `user_consent.require_at_registration` is set. - # - # When rendering, this template is given the following variables: - # - # * server_name: the homeserver's name. - # - # * user_id: the user's matrix proposed ID. - # - # * user_profile.display_name: the user's proposed display name, if any. - # - # * consent_version: the version of the terms that the user will be - # shown - # - # * terms_url: a link to the page showing the terms. - # - # The template should render a form which submits the following fields: - # - # * accepted_version: the version of the terms accepted by the user - # (ie, 'consent_version' from the input variables). - # - # * HTML page for a confirmation step before redirecting back to the client - # with the login token: 'sso_redirect_confirm.html'. - # - # When rendering, this template is given the following variables: - # - # * redirect_url: the URL the user is about to be redirected to. - # - # * display_url: the same as `redirect_url`, but with the query - # parameters stripped. The intention is to have a - # human-readable URL to show to users, not to use it as - # the final address to redirect to. - # - # * server_name: the homeserver's name. - # - # * new_user: a boolean indicating whether this is the user's first time - # logging in. - # - # * user_id: the user's matrix ID. - # - # * user_profile.avatar_url: an MXC URI for the user's avatar, if any. - # None if the user has not set an avatar. - # - # * user_profile.display_name: the user's display name. None if the user - # has not set a display name. - # - # * HTML page which notifies the user that they are authenticating to confirm - # an operation on their account during the user interactive authentication - # process: 'sso_auth_confirm.html'. - # - # When rendering, this template is given the following variables: - # * redirect_url: the URL the user is about to be redirected to. - # - # * description: the operation which the user is being asked to confirm - # - # * idp: details of the Identity Provider that we will use to confirm - # the user's identity: an object with the following attributes: - # - # * idp_id: unique identifier for the IdP - # * idp_name: user-facing name for the IdP - # * idp_icon: if specified in the IdP config, an MXC URI for an icon - # for the IdP - # * idp_brand: if specified in the IdP config, a textual identifier - # for the brand of the IdP - # - # * HTML page shown after a successful user interactive authentication session: - # 'sso_auth_success.html'. - # - # Note that this page must include the JavaScript which notifies of a successful authentication - # (see https://matrix.org/docs/spec/client_server/r0.6.0#fallback). - # - # This template has no additional variables. - # - # * HTML page shown after a user-interactive authentication session which - # does not map correctly onto the expected user: 'sso_auth_bad_user.html'. - # - # When rendering, this template is given the following variables: - # * server_name: the homeserver's name. - # * user_id_to_verify: the MXID of the user that we are trying to - # validate. - # - # * HTML page shown during single sign-on if a deactivated user (according to Synapse's database) - # attempts to login: 'sso_account_deactivated.html'. - # - # This template has no additional variables. - # - # * HTML page to display to users if something goes wrong during the - # OpenID Connect authentication process: 'sso_error.html'. - # - # When rendering, this template is given two variables: - # * error: the technical name of the error - # * error_description: a human-readable message for the error - # - # You can see the default templates at: - # https://github.com/matrix-org/synapse/tree/master/synapse/res/templates - # - #template_dir: "res/templates" """ diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py index 06562d54119c..9c488501e4d2 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py @@ -91,6 +91,7 @@ def __init__(self, hs: "HomeServer", auth_handler): self._state = hs.get_state_handler() self._clock: Clock = hs.get_clock() self._send_email_handler = hs.get_send_email_handler() + self.custom_template_dir =hs.config.server.custom_template_directory try: app_name = self._hs.config.email_app_name @@ -677,7 +678,9 @@ def read_templates( A list containing the loaded templates, with the orders matching the one of the filenames parameter. """ - return self._hs.config.read_templates(filenames, [custom_template_directory]) + return self._hs.config.read_templates( + filenames, [self.custom_template_dir, custom_template_directory], + ) class PublicRoomListManager: diff --git a/synapse/rest/synapse/client/new_user_consent.py b/synapse/rest/synapse/client/new_user_consent.py index 488b97b32e02..fc62a09b7f07 100644 --- a/synapse/rest/synapse/client/new_user_consent.py +++ b/synapse/rest/synapse/client/new_user_consent.py @@ -46,6 +46,8 @@ def __init__(self, hs: "HomeServer"): self._consent_version = hs.config.consent.user_consent_version def template_search_dirs(): + if hs.config.server.custom_template_directory: + yield hs.config.server.custom_template_directory if hs.config.sso.sso_template_dir: yield hs.config.sso.sso_template_dir yield hs.config.sso.default_template_dir diff --git a/synapse/rest/synapse/client/pick_username.py b/synapse/rest/synapse/client/pick_username.py index ab24ec0a8e68..c15b83c387c2 100644 --- a/synapse/rest/synapse/client/pick_username.py +++ b/synapse/rest/synapse/client/pick_username.py @@ -74,6 +74,8 @@ def __init__(self, hs: "HomeServer"): self._sso_handler = hs.get_sso_handler() def template_search_dirs(): + if hs.config.server.custom_template_directory: + yield hs.config.server.custom_template_directory if hs.config.sso.sso_template_dir: yield hs.config.sso.sso_template_dir yield hs.config.sso.default_template_dir From 9e9539617a019e1a75ac4e09a71a68e39f78ed36 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 13 Aug 2021 14:00:53 +0100 Subject: [PATCH 03/15] Allow None paths --- synapse/config/_base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/synapse/config/_base.py b/synapse/config/_base.py index 70ac9c5e39c4..30cbe9aab193 100644 --- a/synapse/config/_base.py +++ b/synapse/config/_base.py @@ -237,7 +237,7 @@ def read_template(self, filename: str) -> jinja2.Template: def read_templates( self, filenames: List[str], - custom_template_directories: Optional[List[str]] = None, + custom_template_directories: List[Optional[str]] = [], ) -> List[jinja2.Template]: """Load a list of template files from disk using the given variables. @@ -251,7 +251,7 @@ def read_templates( Args: filenames: A list of template filenames to read. - custom_template_directories: A list of directories to try to look for the + custom_template_directories: A list of directory to try to look for the templates before using the default Synapse template directory instead. Raises: @@ -264,8 +264,8 @@ def read_templates( # The loader will first look in the custom template directories (if specified) for the # given filename. If it doesn't find it, it will use the default template dir instead - if custom_template_directories: - for custom_template_directory in custom_template_directories: + for custom_template_directory in custom_template_directories: + if custom_template_directory: # Check that the given template directory exists if not self.path_exists(custom_template_directory): raise ConfigError( From 466f184b653e57d7f3b94615822e44ca1151660c Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 13 Aug 2021 14:39:33 +0100 Subject: [PATCH 04/15] Lint --- synapse/config/_base.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/synapse/config/_base.py b/synapse/config/_base.py index 30cbe9aab193..ce4afa6de747 100644 --- a/synapse/config/_base.py +++ b/synapse/config/_base.py @@ -237,7 +237,7 @@ def read_template(self, filename: str) -> jinja2.Template: def read_templates( self, filenames: List[str], - custom_template_directories: List[Optional[str]] = [], + custom_template_directories: Optional[List[Optional[str]]] = None, ) -> List[jinja2.Template]: """Load a list of template files from disk using the given variables. @@ -262,19 +262,23 @@ def read_templates( """ search_directories = [self.default_template_dir] - # The loader will first look in the custom template directories (if specified) for the - # given filename. If it doesn't find it, it will use the default template dir instead - for custom_template_directory in custom_template_directories: - if custom_template_directory: - # Check that the given template directory exists - if not self.path_exists(custom_template_directory): - raise ConfigError( - "Configured template directory does not exist: %s" - % (custom_template_directory,) - ) + # The loader will first look in the custom template directories (if specified) + # for the given filename. If it doesn't find it, it will use the default + # template dir instead. + if custom_template_directories is not None: + for custom_template_directory in custom_template_directories: + # Elements in the list might be None if they were retrieved from the + # configuration dict using config_dict.get(...). + if custom_template_directory: + # Check that the given template directory exists + if not self.path_exists(custom_template_directory): + raise ConfigError( + "Configured template directory does not exist: %s" + % (custom_template_directory,) + ) - # Search the custom template directory as well - search_directories.insert(0, custom_template_directory) + # Search the custom template directory as well + search_directories.insert(0, custom_template_directory) # TODO: switch to synapse.util.templates.build_jinja_env loader = jinja2.FileSystemLoader(search_directories) From d35cc311a0726fd6a3d9fd85e0ae4e7fcd4b83ba Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 13 Aug 2021 14:40:15 +0100 Subject: [PATCH 05/15] Changelog --- changelog.d/10587.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/10587.misc diff --git a/changelog.d/10587.misc b/changelog.d/10587.misc new file mode 100644 index 000000000000..4c6167977c6c --- /dev/null +++ b/changelog.d/10587.misc @@ -0,0 +1 @@ +Allow multiple custom directories in `read_templates`. From fb68544a4dcace49fc60443dc1c3e07656e31585 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 13 Aug 2021 14:59:48 +0100 Subject: [PATCH 06/15] Typo --- synapse/config/account_validity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/config/account_validity.py b/synapse/config/account_validity.py index dcf419cf5152..f76f5fe35d98 100644 --- a/synapse/config/account_validity.py +++ b/synapse/config/account_validity.py @@ -86,7 +86,7 @@ def read_config(self, config, **kwargs): [ account_renewed_template_filename, "account_previously_renewed.html", - [invalid_token_template_filename], + invalid_token_template_filename, ], [account_validity_template_dir], ) From fef4b1225967ccd98e1198ebe910977420e120af Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 13 Aug 2021 15:48:00 +0100 Subject: [PATCH 07/15] Deprecate existing settings --- changelog.d/10596.removal | 1 + docs/upgrade.md | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 changelog.d/10596.removal diff --git a/changelog.d/10596.removal b/changelog.d/10596.removal new file mode 100644 index 000000000000..e69f632db442 --- /dev/null +++ b/changelog.d/10596.removal @@ -0,0 +1 @@ +The `template_dir` configuration settings in the `sso`, `account_validity` and `email` sections of the configuration file are now deprecated in favour of the global `templates.custom_template_directory` setting. See the [upgrade notes](https://matrix-org.github.io/synapse/latest/upgrade.html) for more information. diff --git a/docs/upgrade.md b/docs/upgrade.md index 8831c9d6cf59..1c459d8e2b64 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -112,6 +112,17 @@ environment variable. See [using a forward proxy with Synapse documentation](setup/forward_proxy.md) for details. +## Deprecation of `template_dir` + +The `template_dir` settings in the `sso`, `account_validity` and `email` sections of the +configuration file are now deprecated. Server admins should use the new +`templates.custom_template_directory` setting in the configuration file and use one single +custom template directory for all aforementioned features. Template file names remain +unchanged. See [the related documentation](https://matrix-org.github.io/synapse/latest/templates.html) +for more information and examples. + +We plan to remove support for these settings in October 2021. + # Upgrading to v1.39.0 From d2ee09cbf35a70529cd9f2752cfb58377edc1479 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 13 Aug 2021 15:52:19 +0100 Subject: [PATCH 08/15] Lint --- synapse/config/server.py | 6 ++++-- synapse/module_api/__init__.py | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/synapse/config/server.py b/synapse/config/server.py index 64c15d84327a..57a0e17876a7 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -711,7 +711,9 @@ class LimitRemoteRoomsConfig: self.next_link_domain_whitelist = set(next_link_domain_whitelist) templates_config = config.get("templates", {}) - self.custom_template_directory = templates_config.get("custom_template_directory") + self.custom_template_directory = templates_config.get( + "custom_template_directory" + ) def has_tls_listener(self) -> bool: return any(listener.tls for listener in self.listeners) @@ -1287,7 +1289,7 @@ def generate_config_section( # all domains. # #next_link_domain_whitelist: ["matrix.org"] - + # Templates to use when generating email or HTML page contents. # templates: diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py index 9c488501e4d2..cb729ef73887 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py @@ -91,7 +91,7 @@ def __init__(self, hs: "HomeServer", auth_handler): self._state = hs.get_state_handler() self._clock: Clock = hs.get_clock() self._send_email_handler = hs.get_send_email_handler() - self.custom_template_dir =hs.config.server.custom_template_directory + self.custom_template_dir = hs.config.server.custom_template_directory try: app_name = self._hs.config.email_app_name @@ -679,7 +679,8 @@ def read_templates( the filenames parameter. """ return self._hs.config.read_templates( - filenames, [self.custom_template_dir, custom_template_directory], + filenames, + [self.custom_template_dir, custom_template_directory], ) From a4961e2673079127702abfe5f1392c7c1056d87c Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 13 Aug 2021 16:28:36 +0100 Subject: [PATCH 09/15] Config --- docs/sample_config.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 3849875872d7..3ec76d5abf21 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -579,7 +579,6 @@ templates: #custom_template_directory: /path/to/custom/templates/ - ## TLS ## # PEM-encoded X509 certificate for TLS. From cbb6b490470e9a5331684d69341d8dbf4d2b5ff3 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 13 Aug 2021 16:45:58 +0100 Subject: [PATCH 10/15] Fix tests --- synapse/config/account_validity.py | 2 +- synapse/config/emailconfig.py | 2 +- synapse/config/server.py | 2 +- synapse/config/sso.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/synapse/config/account_validity.py b/synapse/config/account_validity.py index 807ca1304a91..412333e49ce3 100644 --- a/synapse/config/account_validity.py +++ b/synapse/config/account_validity.py @@ -78,7 +78,7 @@ def read_config(self, config, **kwargs): ) # Read and store template content - templates_config = config.get("templates", {}) + templates_config = config.get("templates") or {} custom_template_directory = templates_config.get("custom_template_directory") ( self.account_validity_account_renewed_template, diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index a6560451c3b8..7ee01f4d4958 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -168,7 +168,7 @@ def read_config(self, config, **kwargs): email_config.get("validation_token_lifetime", "1h") ) - templates_config = config.get("templates", {}) + templates_config = config.get("templates") or {} custom_template_directory = templates_config.get("custom_template_directory") if self.threepid_behaviour_email == ThreepidBehaviour.LOCAL: diff --git a/synapse/config/server.py b/synapse/config/server.py index 57a0e17876a7..7c43471c4e1a 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -710,7 +710,7 @@ class LimitRemoteRoomsConfig: # Turn the list into a set to improve lookup speed. self.next_link_domain_whitelist = set(next_link_domain_whitelist) - templates_config = config.get("templates", {}) + templates_config = config.get("templates") or {} self.custom_template_directory = templates_config.get( "custom_template_directory" ) diff --git a/synapse/config/sso.py b/synapse/config/sso.py index 94a141bdda4b..642948870709 100644 --- a/synapse/config/sso.py +++ b/synapse/config/sso.py @@ -45,7 +45,7 @@ def read_config(self, config, **kwargs): self.sso_template_dir = sso_config.get("template_dir") # Read templates from disk - templates_config = config.get("templates", {}) + templates_config = config.get("templates") or {} custom_template_directory = templates_config.get("custom_template_directory") ( From 952d1e1accd11a6238b6c5fe2797c6f063e004dd Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 16 Aug 2021 18:24:26 +0100 Subject: [PATCH 11/15] Incorporate review --- synapse/config/_base.py | 32 ++++++++-------- synapse/config/account_validity.py | 2 +- synapse/config/emailconfig.py | 6 +-- synapse/config/sso.py | 2 +- synapse/module_api/__init__.py | 5 ++- tests/config/test_base.py | 59 ++++++++++++++++++++++++++++-- 6 files changed, 82 insertions(+), 24 deletions(-) diff --git a/synapse/config/_base.py b/synapse/config/_base.py index ce4afa6de747..2cc242782add 100644 --- a/synapse/config/_base.py +++ b/synapse/config/_base.py @@ -237,13 +237,14 @@ def read_template(self, filename: str) -> jinja2.Template: def read_templates( self, filenames: List[str], - custom_template_directories: Optional[List[Optional[str]]] = None, + custom_template_directories: Optional[Iterable[str]] = None, ) -> List[jinja2.Template]: """Load a list of template files from disk using the given variables. This function will attempt to load the given templates from the default Synapse - template directory. If `custom_template_directory` is supplied, that directory - is tried first. + template directory. If `custom_template_directories` is supplied, any directory + in this list is tried (in the order they appear in the list) before trying + Synapse's default directory. Files read are treated as Jinja templates. The templates are not rendered yet and have autoescape enabled. @@ -260,25 +261,26 @@ def read_templates( Returns: A list of jinja2 templates. """ - search_directories = [self.default_template_dir] + search_directories = [] # The loader will first look in the custom template directories (if specified) # for the given filename. If it doesn't find it, it will use the default # template dir instead. if custom_template_directories is not None: for custom_template_directory in custom_template_directories: - # Elements in the list might be None if they were retrieved from the - # configuration dict using config_dict.get(...). - if custom_template_directory: - # Check that the given template directory exists - if not self.path_exists(custom_template_directory): - raise ConfigError( - "Configured template directory does not exist: %s" - % (custom_template_directory,) - ) + # Check that the given template directory exists + if not self.path_exists(custom_template_directory): + raise ConfigError( + "Configured template directory does not exist: %s" + % (custom_template_directory,) + ) + + # Search the custom template directory as well + search_directories.append(custom_template_directory) - # Search the custom template directory as well - search_directories.insert(0, custom_template_directory) + # Append the default directory at the end of the list so Jinja can fallback on it + # if a template is missing from any custom directory. + search_directories.append(self.default_template_dir) # TODO: switch to synapse.util.templates.build_jinja_env loader = jinja2.FileSystemLoader(search_directories) diff --git a/synapse/config/account_validity.py b/synapse/config/account_validity.py index f76f5fe35d98..9acce5996ec2 100644 --- a/synapse/config/account_validity.py +++ b/synapse/config/account_validity.py @@ -88,5 +88,5 @@ def read_config(self, config, **kwargs): "account_previously_renewed.html", invalid_token_template_filename, ], - [account_validity_template_dir], + (td for td in (account_validity_template_dir,) if td), ) diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index f51c1f71123e..7087e575de5f 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -257,7 +257,7 @@ def read_config(self, config, **kwargs): registration_template_success_html, add_threepid_template_success_html, ], - [template_dir], + (td for td in (template_dir,) if td), # Filter out template_dir if not provided ) # Render templates that do not contain any placeholders @@ -297,7 +297,7 @@ def read_config(self, config, **kwargs): self.email_notif_template_text, ) = self.read_templates( [notif_template_html, notif_template_text], - [template_dir], + (td for td in (template_dir,) if td), ) self.email_notif_for_new_users = email_config.get( @@ -320,7 +320,7 @@ def read_config(self, config, **kwargs): self.account_validity_template_text, ) = self.read_templates( [expiry_template_html, expiry_template_text], - [template_dir], + (td for td in (template_dir,) if td), ) subjects_config = email_config.get("subjects", {}) diff --git a/synapse/config/sso.py b/synapse/config/sso.py index a616bf9661b9..4b590e05356a 100644 --- a/synapse/config/sso.py +++ b/synapse/config/sso.py @@ -63,7 +63,7 @@ def read_config(self, config, **kwargs): "sso_auth_success.html", "sso_auth_bad_user.html", ], - [self.sso_template_dir], + (td for td in (self.sso_template_dir,) if td), ) # These templates have no placeholders, so render them here diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py index 06562d54119c..82725853bc6c 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py @@ -677,7 +677,10 @@ def read_templates( A list containing the loaded templates, with the orders matching the one of the filenames parameter. """ - return self._hs.config.read_templates(filenames, [custom_template_directory]) + return self._hs.config.read_templates( + filenames, + (td for td in (custom_template_directory,) if td), + ) class PublicRoomListManager: diff --git a/tests/config/test_base.py b/tests/config/test_base.py index b940a086a682..e4be8a170fd9 100644 --- a/tests/config/test_base.py +++ b/tests/config/test_base.py @@ -30,7 +30,7 @@ def test_loading_missing_templates(self): # contain template files with tempfile.TemporaryDirectory() as tmp_dir: # Attempt to load an HTML template from our custom template directory - template = self.hs.config.read_templates(["sso_error.html"], [tmp_dir])[0] + template = self.hs.config.read_templates(["sso_error.html"], (tmp_dir,))[0] # If no errors, we should've gotten the default template instead @@ -60,7 +60,7 @@ def test_loading_custom_templates(self): # Attempt to load the template from our custom template directory template = ( - self.hs.config.read_templates([template_filename], [tmp_dir]) + self.hs.config.read_templates([template_filename], (tmp_dir,)) )[0] # Render the template @@ -74,8 +74,61 @@ def test_loading_custom_templates(self): "Template file did not contain our test string", ) + def test_multiple_custom_template_directories(self): + """Tests that directories are searched in the right order if multiple custom + template directories are provided. + """ + # Create two temporary directories on the filesystem. + tempdirs = [ + tempfile.TemporaryDirectory(), + tempfile.TemporaryDirectory(), + ] + + # Create one template in each directory, which content is the index of the + # directory in the list. + template_filename = "my_template.html.j2" + for i in range(len(tempdirs)): + tempdir = tempdirs[i] + template_path = os.path.join(tempdir.name, template_filename) + + with open(template_path, "w") as fp: + fp.write(str(i)) + fp.flush() + + # Retrieve the template. + template = ( + self.hs.config.read_templates( + [template_filename], + (td.name for td in tempdirs), + ) + )[0] + + # Test that we got the template we dropped in the first directory in the list. + self.assertEqual(template.render(), "0") + + # Add another template, this one only in the second directory in the list, so we + # can test that the second directory is still searched into when no matching file + # could be found in the first one. + other_template_name = "my_other_template.html.j2" + other_template_path = os.path.join(tempdirs[1].name, other_template_name) + + with open(other_template_path, "w") as fp: + fp.write("hello world") + fp.flush() + + # Retrieve the template. + template = ( + self.hs.config.read_templates( + [other_template_name], + (td.name for td in tempdirs), + ) + )[0] + + # Test that the file has the expected content. + self.assertEqual(template.render(), "hello world") + def test_loading_template_from_nonexistent_custom_directory(self): with self.assertRaises(ConfigError): self.hs.config.read_templates( - ["some_filename.html"], ["a_nonexistent_directory"] + ["some_filename.html"], ("a_nonexistent_directory",) ) From bba6e5de6cb1adf71ca01f92c7c6ea38a6977002 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 16 Aug 2021 18:26:20 +0100 Subject: [PATCH 12/15] Cleanup temp dirs --- tests/config/test_base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/config/test_base.py b/tests/config/test_base.py index e4be8a170fd9..cabb66ddd104 100644 --- a/tests/config/test_base.py +++ b/tests/config/test_base.py @@ -127,6 +127,11 @@ def test_multiple_custom_template_directories(self): # Test that the file has the expected content. self.assertEqual(template.render(), "hello world") + # Cleanup the temporary directories manually since we're not using a context + # manager. + for td in tempdirs: + td.cleanup() + def test_loading_template_from_nonexistent_custom_directory(self): with self.assertRaises(ConfigError): self.hs.config.read_templates( From 9d90235446fe4a4e49a5e35498456439b33923c8 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 16 Aug 2021 18:35:21 +0100 Subject: [PATCH 13/15] Lint --- synapse/config/emailconfig.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 7087e575de5f..fc74b4a8b939 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -257,7 +257,9 @@ def read_config(self, config, **kwargs): registration_template_success_html, add_threepid_template_success_html, ], - (td for td in (template_dir,) if td), # Filter out template_dir if not provided + ( + td for td in (template_dir,) if td + ), # Filter out template_dir if not provided ) # Render templates that do not contain any placeholders From dc69ccd2d5b3ccf4cf44f81fb2d4844b2b4cb30f Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 16 Aug 2021 18:55:06 +0100 Subject: [PATCH 14/15] Incorporate review --- synapse/config/account_validity.py | 13 ++++++------- synapse/config/emailconfig.py | 25 ++++++++++++++++++++++--- synapse/config/server.py | 8 ++++++++ synapse/config/sso.py | 8 +++++--- 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/synapse/config/account_validity.py b/synapse/config/account_validity.py index f8c0052b31c6..52e63ab1f6f2 100644 --- a/synapse/config/account_validity.py +++ b/synapse/config/account_validity.py @@ -78,8 +78,11 @@ def read_config(self, config, **kwargs): ) # Read and store template content - templates_config = config.get("templates") or {} - custom_template_directory = templates_config.get("custom_template_directory") + custom_template_directories = ( + self.root.server.custom_template_directory, + account_validity_template_dir, + ) + ( self.account_validity_account_renewed_template, self.account_validity_account_previously_renewed_template, @@ -90,9 +93,5 @@ def read_config(self, config, **kwargs): "account_previously_renewed.html", invalid_token_template_filename, ], - ( - td - for td in (custom_template_directory, account_validity_template_dir,) - if td - ), + (td for td in custom_template_directories if td), ) diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index e351cb8ddf4e..b6ec5a8495ab 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -261,7 +261,12 @@ def read_config(self, config, **kwargs): add_threepid_template_success_html, ], ( - td for td in (custom_template_directory, template_dir,) if td + td + for td in ( + self.root.server.custom_template_directory, + template_dir, + ) + if td ), # Filter out template_dir if not provided ) @@ -302,7 +307,14 @@ def read_config(self, config, **kwargs): self.email_notif_template_text, ) = self.read_templates( [notif_template_html, notif_template_text], - (td for td in (custom_template_directory, template_dir,) if td), + ( + td + for td in ( + self.root.server.custom_template_directory, + template_dir, + ) + if td + ), # Filter out template_dir if not provided ) self.email_notif_for_new_users = email_config.get( @@ -325,7 +337,14 @@ def read_config(self, config, **kwargs): self.account_validity_template_text, ) = self.read_templates( [expiry_template_html, expiry_template_text], - (td for td in (custom_template_directory, template_dir,) if td), + ( + td + for td in ( + self.root.server.custom_template_directory, + template_dir, + ) + if td + ), # Filter out template_dir if not provided ) subjects_config = email_config.get("subjects", {}) diff --git a/synapse/config/server.py b/synapse/config/server.py index 7c43471c4e1a..75fb9ab36e74 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -711,9 +711,17 @@ class LimitRemoteRoomsConfig: self.next_link_domain_whitelist = set(next_link_domain_whitelist) templates_config = config.get("templates") or {} + if not isinstance(templates_config, dict): + raise ConfigError("The 'templates' section must be a dictionary") + self.custom_template_directory = templates_config.get( "custom_template_directory" ) + if ( + self.custom_template_directory is not None + and not isinstance(self.custom_template_directory, str) + ): + raise ConfigError("'custom_template_directory' must be a string") def has_tls_listener(self) -> bool: return any(listener.tls for listener in self.listeners) diff --git a/synapse/config/sso.py b/synapse/config/sso.py index 0810912280ab..fe1177ab8109 100644 --- a/synapse/config/sso.py +++ b/synapse/config/sso.py @@ -45,8 +45,10 @@ def read_config(self, config, **kwargs): self.sso_template_dir = sso_config.get("template_dir") # Read templates from disk - templates_config = config.get("templates") or {} - custom_template_directory = templates_config.get("custom_template_directory") + custom_template_directories = ( + self.root.server.custom_template_directory, + self.sso_template_dir, + ) ( self.sso_login_idp_picker_template, @@ -66,7 +68,7 @@ def read_config(self, config, **kwargs): "sso_auth_success.html", "sso_auth_bad_user.html", ], - (td for td in (custom_template_directory, self.sso_template_dir,) if td), + (td for td in custom_template_directories if td), ) # These templates have no placeholders, so render them here From 3de32a4b895efee12a49d83f9dc19ae59ee326e9 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 17 Aug 2021 10:27:07 +0100 Subject: [PATCH 15/15] Lint --- synapse/config/emailconfig.py | 3 --- synapse/config/server.py | 5 ++--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index b6ec5a8495ab..4477419196c2 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -168,9 +168,6 @@ def read_config(self, config, **kwargs): email_config.get("validation_token_lifetime", "1h") ) - templates_config = config.get("templates") or {} - custom_template_directory = templates_config.get("custom_template_directory") - if self.threepid_behaviour_email == ThreepidBehaviour.LOCAL: missing = [] if not self.email_notif_from: diff --git a/synapse/config/server.py b/synapse/config/server.py index 75fb9ab36e74..849479591971 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -717,9 +717,8 @@ class LimitRemoteRoomsConfig: self.custom_template_directory = templates_config.get( "custom_template_directory" ) - if ( - self.custom_template_directory is not None - and not isinstance(self.custom_template_directory, str) + if self.custom_template_directory is not None and not isinstance( + self.custom_template_directory, str ): raise ConfigError("'custom_template_directory' must be a string")