summary refs log tree commit diff
path: root/synapse
diff options
context:
space:
mode:
Diffstat (limited to 'synapse')
-rwxr-xr-xsynapse/_scripts/generate_signing_key.py13
-rw-r--r--synapse/app/_base.py1
-rw-r--r--synapse/config/emailconfig.py5
-rw-r--r--synapse/config/key.py8
-rw-r--r--synapse/config/metrics.py1
-rw-r--r--synapse/config/server_notices.py12
-rw-r--r--synapse/push/emailpusher.py15
-rw-r--r--synapse/rest/__init__.py2
-rw-r--r--synapse/rest/client/auth_issuer.py63
-rw-r--r--synapse/server_notices/server_notices_manager.py113
10 files changed, 208 insertions, 25 deletions
diff --git a/synapse/_scripts/generate_signing_key.py b/synapse/_scripts/generate_signing_key.py
index 3f8f5da75f..581b991505 100755
--- a/synapse/_scripts/generate_signing_key.py
+++ b/synapse/_scripts/generate_signing_key.py
@@ -13,6 +13,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 import argparse
+import os
 import sys
 
 from signedjson.key import generate_signing_key, write_signing_keys
@@ -26,15 +27,21 @@ def main() -> None:
     parser.add_argument(
         "-o",
         "--output_file",
-        type=argparse.FileType("w"),
-        default=sys.stdout,
+        type=str,
+        default="-",
         help="Where to write the output to",
     )
     args = parser.parse_args()
 
     key_id = "a_" + random_string(4)
     key = (generate_signing_key(key_id),)
-    write_signing_keys(args.output_file, key)
+    if args.output_file == "-":
+        write_signing_keys(sys.stdout, key)
+    else:
+        with open(
+            args.output_file, "w", opener=lambda p, f: os.open(p, f, mode=0o640)
+        ) as signing_key_file:
+            write_signing_keys(signing_key_file, key)
 
 
 if __name__ == "__main__":
diff --git a/synapse/app/_base.py b/synapse/app/_base.py
index 9ac7e4313e..aed98f03af 100644
--- a/synapse/app/_base.py
+++ b/synapse/app/_base.py
@@ -665,6 +665,7 @@ def setup_sentry(hs: "HomeServer") -> None:
     sentry_sdk.init(
         dsn=hs.config.metrics.sentry_dsn,
         release=SYNAPSE_VERSION,
+        environment=hs.config.metrics.sentry_environment,
     )
 
     # We set some default tags that give some context to this instance
diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py
index a3af35b7c4..e33791fab9 100644
--- a/synapse/config/emailconfig.py
+++ b/synapse/config/emailconfig.py
@@ -294,6 +294,11 @@ class EmailConfig(Config):
             self.email_riot_base_url = email_config.get(
                 "client_base_url", email_config.get("riot_base_url", None)
             )
+            # The amount of time we always wait before ever emailing about a notification
+            # (to give the user a chance to respond to other push or notice the window)
+            self.notif_delay_before_mail_ms = Config.parse_duration(
+                email_config.get("notif_delay_before_mail", "10m")
+            )
 
         if self.root.account_validity.account_validity_renew_by_email_enabled:
             expiry_template_html = email_config.get(
diff --git a/synapse/config/key.py b/synapse/config/key.py
index f3dc4df695..1920498cd1 100644
--- a/synapse/config/key.py
+++ b/synapse/config/key.py
@@ -263,7 +263,9 @@ class KeyConfig(Config):
 
         if not self.path_exists(signing_key_path):
             print("Generating signing key file %s" % (signing_key_path,))
-            with open(signing_key_path, "w") as signing_key_file:
+            with open(
+                signing_key_path, "w", opener=lambda p, f: os.open(p, f, mode=0o640)
+            ) as signing_key_file:
                 key_id = "a_" + random_string(4)
                 write_signing_keys(signing_key_file, (generate_signing_key(key_id),))
         else:
@@ -274,7 +276,9 @@ class KeyConfig(Config):
                 key = decode_signing_key_base64(
                     NACL_ED25519, key_id, signing_keys.split("\n")[0]
                 )
-                with open(signing_key_path, "w") as signing_key_file:
+                with open(
+                    signing_key_path, "w", opener=lambda p, f: os.open(p, f, mode=0o640)
+                ) as signing_key_file:
                     write_signing_keys(signing_key_file, (key,))
 
 
diff --git a/synapse/config/metrics.py b/synapse/config/metrics.py
index 8c1c9bd12d..cb2a61a1c7 100644
--- a/synapse/config/metrics.py
+++ b/synapse/config/metrics.py
@@ -61,6 +61,7 @@ class MetricsConfig(Config):
             check_requirements("sentry")
 
             self.sentry_dsn = config["sentry"].get("dsn")
+            self.sentry_environment = config["sentry"].get("environment")
             if not self.sentry_dsn:
                 raise ConfigError(
                     "sentry.dsn field is required when sentry integration is enabled"
diff --git a/synapse/config/server_notices.py b/synapse/config/server_notices.py
index a8badba0f8..79f365cad5 100644
--- a/synapse/config/server_notices.py
+++ b/synapse/config/server_notices.py
@@ -38,6 +38,14 @@ class ServerNoticesConfig(Config):
         server_notices_room_name (str|None):
             The name to use for the server notices room.
             None if server notices are not enabled.
+
+        server_notices_room_avatar_url (str|None):
+            The avatar URL to use for the server notices room.
+            None if server notices are not enabled.
+
+        server_notices_room_topic (str|None):
+            The topic to use for the server notices room.
+            None if server notices are not enabled.
     """
 
     section = "servernotices"
@@ -48,6 +56,8 @@ class ServerNoticesConfig(Config):
         self.server_notices_mxid_display_name: Optional[str] = None
         self.server_notices_mxid_avatar_url: Optional[str] = None
         self.server_notices_room_name: Optional[str] = None
+        self.server_notices_room_avatar_url: Optional[str] = None
+        self.server_notices_room_topic: Optional[str] = None
         self.server_notices_auto_join: bool = False
 
     def read_config(self, config: JsonDict, **kwargs: Any) -> None:
@@ -63,4 +73,6 @@ class ServerNoticesConfig(Config):
         self.server_notices_mxid_avatar_url = c.get("system_mxid_avatar_url", None)
         # todo: i18n
         self.server_notices_room_name = c.get("room_name", "Server Notices")
+        self.server_notices_room_avatar_url = c.get("room_avatar_url", None)
+        self.server_notices_room_topic = c.get("room_topic", None)
         self.server_notices_auto_join = c.get("auto_join", False)
diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py
index cf45fd09a8..be7631e8d0 100644
--- a/synapse/push/emailpusher.py
+++ b/synapse/push/emailpusher.py
@@ -30,14 +30,9 @@ if TYPE_CHECKING:
 
 logger = logging.getLogger(__name__)
 
-# The amount of time we always wait before ever emailing about a notification
-# (to give the user a chance to respond to other push or notice the window)
-DELAY_BEFORE_MAIL_MS = 10 * 60 * 1000
-
 # THROTTLE is the minimum time between mail notifications sent for a given room.
 # Each room maintains its own throttle counter, but each new mail notification
 # sends the pending notifications for all rooms.
-THROTTLE_START_MS = 10 * 60 * 1000
 THROTTLE_MAX_MS = 24 * 60 * 60 * 1000  # 24h
 # THROTTLE_MULTIPLIER = 6              # 10 mins, 1 hour, 6 hours, 24 hours
 THROTTLE_MULTIPLIER = 144  # 10 mins, 24 hours - i.e. jump straight to 1 day
@@ -80,6 +75,8 @@ class EmailPusher(Pusher):
         except ValueError:
             raise PusherConfigException("Invalid email")
 
+        self._delay_before_mail_ms = self.hs.config.email.notif_delay_before_mail_ms
+
     def on_started(self, should_check_for_notifs: bool) -> None:
         """Called when this pusher has been started.
 
@@ -180,7 +177,7 @@ class EmailPusher(Pusher):
             received_at = push_action.received_ts
             if received_at is None:
                 received_at = 0
-            notif_ready_at = received_at + DELAY_BEFORE_MAIL_MS
+            notif_ready_at = received_at + self._delay_before_mail_ms
 
             room_ready_at = self.room_ready_to_notify_at(push_action.room_id)
 
@@ -196,7 +193,7 @@ class EmailPusher(Pusher):
                     "room_id": push_action.room_id,
                     "now": self.clock.time_msec(),
                     "received_at": received_at,
-                    "delay_before_mail_ms": DELAY_BEFORE_MAIL_MS,
+                    "delay_before_mail_ms": self._delay_before_mail_ms,
                     "last_sent_ts": self.get_room_last_sent_ts(push_action.room_id),
                     "throttle_ms": self.get_room_throttle_ms(push_action.room_id),
                 }
@@ -300,10 +297,10 @@ class EmailPusher(Pusher):
         current_throttle_ms = self.get_room_throttle_ms(room_id)
 
         if gap > THROTTLE_RESET_AFTER_MS:
-            new_throttle_ms = THROTTLE_START_MS
+            new_throttle_ms = self._delay_before_mail_ms
         else:
             if current_throttle_ms == 0:
-                new_throttle_ms = THROTTLE_START_MS
+                new_throttle_ms = self._delay_before_mail_ms
             else:
                 new_throttle_ms = min(
                     current_throttle_ms * THROTTLE_MULTIPLIER, THROTTLE_MAX_MS
diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py
index 1be9c47c61..53b8c319a6 100644
--- a/synapse/rest/__init__.py
+++ b/synapse/rest/__init__.py
@@ -22,6 +22,7 @@ from synapse.rest.client import (
     account_validity,
     appservice_ping,
     auth,
+    auth_issuer,
     capabilities,
     devices,
     directory,
@@ -148,3 +149,4 @@ class ClientRestResource(JsonResource):
             mutual_rooms.register_servlets(hs, client_resource)
             login_token_request.register_servlets(hs, client_resource)
             rendezvous.register_servlets(hs, client_resource)
+            auth_issuer.register_servlets(hs, client_resource)
diff --git a/synapse/rest/client/auth_issuer.py b/synapse/rest/client/auth_issuer.py
new file mode 100644
index 0000000000..77b9720956
--- /dev/null
+++ b/synapse/rest/client/auth_issuer.py
@@ -0,0 +1,63 @@
+# Copyright 2023 The Matrix.org Foundation C.I.C.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import logging
+import typing
+from typing import Tuple
+
+from synapse.api.errors import Codes, SynapseError
+from synapse.http.server import HttpServer
+from synapse.http.servlet import RestServlet
+from synapse.http.site import SynapseRequest
+from synapse.rest.client._base import client_patterns
+from synapse.types import JsonDict
+
+if typing.TYPE_CHECKING:
+    from synapse.server import HomeServer
+
+
+logger = logging.getLogger(__name__)
+
+
+class AuthIssuerServlet(RestServlet):
+    """
+    Advertises what OpenID Connect issuer clients should use to authorise users.
+    """
+
+    PATTERNS = client_patterns(
+        "/org.matrix.msc2965/auth_issuer$",
+        unstable=True,
+        releases=(),
+    )
+
+    def __init__(self, hs: "HomeServer"):
+        super().__init__()
+        self._config = hs.config
+
+    async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
+        if self._config.experimental.msc3861.enabled:
+            return 200, {"issuer": self._config.experimental.msc3861.issuer}
+        else:
+            # Wouldn't expect this to be reached: the servelet shouldn't have been
+            # registered. Still, fail gracefully if we are registered for some reason.
+            raise SynapseError(
+                404,
+                "OIDC discovery has not been configured on this homeserver",
+                Codes.NOT_FOUND,
+            )
+
+
+def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
+    # We use the MSC3861 values as they are used by multiple MSCs
+    if hs.config.experimental.msc3861.enabled:
+        AuthIssuerServlet(hs).register(http_server)
diff --git a/synapse/server_notices/server_notices_manager.py b/synapse/server_notices/server_notices_manager.py
index 2353b5d47f..39a54362d8 100644
--- a/synapse/server_notices/server_notices_manager.py
+++ b/synapse/server_notices/server_notices_manager.py
@@ -16,7 +16,7 @@ from typing import TYPE_CHECKING, Optional
 
 from synapse.api.constants import EventTypes, Membership, RoomCreationPreset
 from synapse.events import EventBase
-from synapse.types import Requester, StreamKeyType, UserID, create_requester
+from synapse.types import JsonDict, Requester, StreamKeyType, UserID, create_requester
 from synapse.util.caches.descriptors import cached
 
 if TYPE_CHECKING:
@@ -36,6 +36,7 @@ class ServerNoticesManager:
         self._room_member_handler = hs.get_room_member_handler()
         self._event_creation_handler = hs.get_event_creation_handler()
         self._message_handler = hs.get_message_handler()
+        self._storage_controllers = hs.get_storage_controllers()
         self._is_mine_id = hs.is_mine_id
         self._server_name = hs.hostname
 
@@ -160,6 +161,27 @@ class ServerNoticesManager:
                 self._config.servernotices.server_notices_mxid_display_name,
                 self._config.servernotices.server_notices_mxid_avatar_url,
             )
+            await self._update_room_info(
+                requester,
+                room_id,
+                EventTypes.Name,
+                "name",
+                self._config.servernotices.server_notices_room_name,
+            )
+            await self._update_room_info(
+                requester,
+                room_id,
+                EventTypes.RoomAvatar,
+                "url",
+                self._config.servernotices.server_notices_room_avatar_url,
+            )
+            await self._update_room_info(
+                requester,
+                room_id,
+                EventTypes.Topic,
+                "topic",
+                self._config.servernotices.server_notices_room_topic,
+            )
             return room_id
 
         # apparently no existing notice room: create a new one
@@ -178,15 +200,31 @@ class ServerNoticesManager:
                 "avatar_url": self._config.servernotices.server_notices_mxid_avatar_url,
             }
 
+        room_config: JsonDict = {
+            "preset": RoomCreationPreset.PRIVATE_CHAT,
+            "power_level_content_override": {"users_default": -10},
+        }
+
+        if self._config.servernotices.server_notices_room_name:
+            room_config["name"] = self._config.servernotices.server_notices_room_name
+        if self._config.servernotices.server_notices_room_topic:
+            room_config["topic"] = self._config.servernotices.server_notices_room_topic
+        if self._config.servernotices.server_notices_room_avatar_url:
+            room_config["initial_state"] = [
+                {
+                    "type": EventTypes.RoomAvatar,
+                    "state_key": "",
+                    "content": {
+                        "url": self._config.servernotices.server_notices_room_avatar_url,
+                    },
+                }
+            ]
+
         # `ignore_forced_encryption` is used to bypass `encryption_enabled_by_default_for_room_type`
         # setting if it set, since the server notices will not be encrypted anyway.
         room_id, _, _ = await self._room_creation_handler.create_room(
             requester,
-            config={
-                "preset": RoomCreationPreset.PRIVATE_CHAT,
-                "name": self._config.servernotices.server_notices_room_name,
-                "power_level_content_override": {"users_default": -10},
-            },
+            config=room_config,
             ratelimit=False,
             creator_join_profile=join_profile,
             ignore_forced_encryption=True,
@@ -265,11 +303,12 @@ class ServerNoticesManager:
 
         assert self.server_notices_mxid is not None
 
-        notice_user_data_in_room = await self._message_handler.get_room_data(
-            create_requester(self.server_notices_mxid),
-            room_id,
-            EventTypes.Member,
-            self.server_notices_mxid,
+        notice_user_data_in_room = (
+            await self._storage_controllers.state.get_current_state_event(
+                room_id,
+                EventTypes.Member,
+                self.server_notices_mxid,
+            )
         )
 
         assert notice_user_data_in_room is not None
@@ -288,3 +327,55 @@ class ServerNoticesManager:
                 ratelimit=False,
                 content={"displayname": display_name, "avatar_url": avatar_url},
             )
+
+    async def _update_room_info(
+        self,
+        requester: Requester,
+        room_id: str,
+        info_event_type: str,
+        info_content_key: str,
+        info_value: Optional[str],
+    ) -> None:
+        """
+        Updates a specific notice room's info if it's different from what is set.
+
+        Args:
+            requester: The user who is performing the update.
+            room_id: The ID of the server notice room
+            info_event_type: The event type holding the specific info
+            info_content_key: The key containing the specific info in the event's content
+            info_value: The expected value for the specific info
+        """
+        room_info_event = await self._storage_controllers.state.get_current_state_event(
+            room_id,
+            info_event_type,
+            "",
+        )
+
+        existing_info_value = None
+        if room_info_event:
+            existing_info_value = room_info_event.get(info_content_key)
+        if existing_info_value == info_value:
+            return
+        if not existing_info_value and not info_value:
+            # A missing `info_value` can either be represented by a None
+            # or an empty string, so we assume that if they're both falsey
+            # they're equivalent.
+            return
+
+        if info_value is None:
+            info_value = ""
+
+        room_info_event_dict = {
+            "type": info_event_type,
+            "room_id": room_id,
+            "sender": requester.user.to_string(),
+            "state_key": "",
+            "content": {
+                info_content_key: info_value,
+            },
+        }
+
+        event, _ = await self._event_creation_handler.create_and_send_nonmember_event(
+            requester, room_info_event_dict, ratelimit=False
+        )