Merge branch 'develop' into babolivier/mark_unread

This commit is contained in:
Brendan Abolivier 2020-06-15 16:37:08 +01:00
commit c2b4621630
26 changed files with 448 additions and 115 deletions

View File

@ -1,3 +1,9 @@
Synapse 1.15.0 (2020-06-11)
===========================
No significant changes.
Synapse 1.15.0rc1 (2020-06-09) Synapse 1.15.0rc1 (2020-06-09)
============================== ==============================
@ -8,14 +14,14 @@ Features
- Add an option to disable autojoining rooms for guest accounts. ([\#6637](https://github.com/matrix-org/synapse/issues/6637)) - Add an option to disable autojoining rooms for guest accounts. ([\#6637](https://github.com/matrix-org/synapse/issues/6637))
- For SAML authentication, add the ability to pass email addresses to be added to new users' accounts via SAML attributes. Contributed by Christopher Cooper. ([\#7385](https://github.com/matrix-org/synapse/issues/7385)) - For SAML authentication, add the ability to pass email addresses to be added to new users' accounts via SAML attributes. Contributed by Christopher Cooper. ([\#7385](https://github.com/matrix-org/synapse/issues/7385))
- Add admin APIs to allow server admins to manage users' devices. Contributed by @dklimpel. ([\#7481](https://github.com/matrix-org/synapse/issues/7481)) - Add admin APIs to allow server admins to manage users' devices. Contributed by @dklimpel. ([\#7481](https://github.com/matrix-org/synapse/issues/7481))
- Add support for generating thumbnails for WebP images. Previously, users would see an empty box instead of preview image. ([\#7586](https://github.com/matrix-org/synapse/issues/7586)) - Add support for generating thumbnails for WebP images. Previously, users would see an empty box instead of preview image. Contributed by @WGH-. ([\#7586](https://github.com/matrix-org/synapse/issues/7586))
- Support the standardized `m.login.sso` user-interactive authentication flow. ([\#7630](https://github.com/matrix-org/synapse/issues/7630)) - Support the standardized `m.login.sso` user-interactive authentication flow. ([\#7630](https://github.com/matrix-org/synapse/issues/7630))
Bugfixes Bugfixes
-------- --------
- Allow new users to be registered via the admin API even if the monthly active user limit has been reached. Contributed by @dkimpel. ([\#7263](https://github.com/matrix-org/synapse/issues/7263)) - Allow new users to be registered via the admin API even if the monthly active user limit has been reached. Contributed by @dklimpel. ([\#7263](https://github.com/matrix-org/synapse/issues/7263))
- Fix email notifications not being enabled for new users when created via the Admin API. ([\#7267](https://github.com/matrix-org/synapse/issues/7267)) - Fix email notifications not being enabled for new users when created via the Admin API. ([\#7267](https://github.com/matrix-org/synapse/issues/7267))
- Fix str placeholders in an instance of `PrepareDatabaseException`. Introduced in Synapse v1.8.0. ([\#7575](https://github.com/matrix-org/synapse/issues/7575)) - Fix str placeholders in an instance of `PrepareDatabaseException`. Introduced in Synapse v1.8.0. ([\#7575](https://github.com/matrix-org/synapse/issues/7575))
- Fix a bug in automatic user creation during first time login with `m.login.jwt`. Regression in v1.6.0. Contributed by @olof. ([\#7585](https://github.com/matrix-org/synapse/issues/7585)) - Fix a bug in automatic user creation during first time login with `m.login.jwt`. Regression in v1.6.0. Contributed by @olof. ([\#7585](https://github.com/matrix-org/synapse/issues/7585))
@ -44,7 +50,7 @@ Internal Changes
---------------- ----------------
- Convert the identity handler to async/await. ([\#7561](https://github.com/matrix-org/synapse/issues/7561)) - Convert the identity handler to async/await. ([\#7561](https://github.com/matrix-org/synapse/issues/7561))
- Improve query performance for fetching state from a PostgreSQL database. ([\#7567](https://github.com/matrix-org/synapse/issues/7567)) - Improve query performance for fetching state from a PostgreSQL database. Contributed by @ilmari. ([\#7567](https://github.com/matrix-org/synapse/issues/7567))
- Speed up processing of federation stream RDATA rows. ([\#7584](https://github.com/matrix-org/synapse/issues/7584)) - Speed up processing of federation stream RDATA rows. ([\#7584](https://github.com/matrix-org/synapse/issues/7584))
- Add comment to systemd example to show postgresql dependency. ([\#7591](https://github.com/matrix-org/synapse/issues/7591)) - Add comment to systemd example to show postgresql dependency. ([\#7591](https://github.com/matrix-org/synapse/issues/7591))
- Refactor `Ratelimiter` to limit the amount of expensive config value accesses. ([\#7595](https://github.com/matrix-org/synapse/issues/7595)) - Refactor `Ratelimiter` to limit the amount of expensive config value accesses. ([\#7595](https://github.com/matrix-org/synapse/issues/7595))

1
changelog.d/7606.bugfix Normal file
View File

@ -0,0 +1 @@
Remove `user_id` from the response to `GET /_matrix/client/r0/presence/{userId}/status` to match the specification.

1
changelog.d/7639.feature Normal file
View File

@ -0,0 +1 @@
Add an option to enable encryption by default for new rooms.

1
changelog.d/7652.doc Normal file
View File

@ -0,0 +1 @@
Spelling correction in sample_config.yaml.

1
changelog.d/7657.misc Normal file
View File

@ -0,0 +1 @@
Clean-up the login fallback code.

1
changelog.d/7663.bugfix Normal file
View File

@ -0,0 +1 @@
Fix intermittent exception during startup, introduced in Synapse 1.14.0.

1
changelog.d/7664.misc Normal file
View File

@ -0,0 +1 @@
Increase the default SAML session expirary time to 15 minutes.

6
debian/changelog vendored
View File

@ -1,3 +1,9 @@
matrix-synapse-py3 (1.15.0) stable; urgency=medium
* New synapse release 1.15.0.
-- Synapse Packaging team <packages@matrix.org> Thu, 11 Jun 2020 13:27:06 +0100
matrix-synapse-py3 (1.14.0) stable; urgency=medium matrix-synapse-py3 (1.14.0) stable; urgency=medium
* New synapse release 1.14.0. * New synapse release 1.14.0.

View File

@ -283,7 +283,7 @@ listeners:
# number of monthly active users. # number of monthly active users.
# #
# 'limit_usage_by_mau' disables/enables monthly active user blocking. When # 'limit_usage_by_mau' disables/enables monthly active user blocking. When
# anabled and a limit is reached the server returns a 'ResourceLimitError' # enabled and a limit is reached the server returns a 'ResourceLimitError'
# with error type Codes.RESOURCE_LIMIT_EXCEEDED # with error type Codes.RESOURCE_LIMIT_EXCEEDED
# #
# 'max_mau_value' is the hard limit of monthly active users above which # 'max_mau_value' is the hard limit of monthly active users above which
@ -1454,7 +1454,7 @@ saml2_config:
# The lifetime of a SAML session. This defines how long a user has to # The lifetime of a SAML session. This defines how long a user has to
# complete the authentication process, if allow_unsolicited is unset. # complete the authentication process, if allow_unsolicited is unset.
# The default is 5 minutes. # The default is 15 minutes.
# #
#saml_session_lifetime: 5m #saml_session_lifetime: 5m
@ -1973,6 +1973,26 @@ spam_checker:
# example_stop_events_from: ['@bad:example.com'] # example_stop_events_from: ['@bad:example.com']
## Rooms ##
# Controls whether locally-created rooms should be end-to-end encrypted by
# default.
#
# Possible options are "all", "invite", and "off". They are defined as:
#
# * "all": any locally-created room
# * "invite": any room created with the "private_chat" or "trusted_private_chat"
# room creation presets
# * "off": this option will take no effect
#
# The default value is "off".
#
# Note that this option will only affect rooms created after it is set. It
# will also not affect rooms created by other servers.
#
#encryption_enabled_by_default_for_room_type: invite
# Uncomment to allow non-server-admin users to create groups on this server # Uncomment to allow non-server-admin users to create groups on this server
# #
#enable_group_creation: true #enable_group_creation: true

View File

@ -36,7 +36,7 @@ try:
except ImportError: except ImportError:
pass pass
__version__ = "1.15.0rc1" __version__ = "1.15.0"
if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)):
# We import here so that we don't have to install a bunch of deps when # We import here so that we don't have to install a bunch of deps when

View File

@ -150,3 +150,8 @@ class EventContentFields(object):
# Timestamp to delete the event after # Timestamp to delete the event after
# cf https://github.com/matrix-org/matrix-doc/pull/2228 # cf https://github.com/matrix-org/matrix-doc/pull/2228
SELF_DESTRUCT_AFTER = "org.matrix.self_destruct_after" SELF_DESTRUCT_AFTER = "org.matrix.self_destruct_after"
class RoomEncryptionAlgorithms(object):
MEGOLM_V1_AES_SHA2 = "m.megolm.v1.aes-sha2"
DEFAULT = MEGOLM_V1_AES_SHA2

View File

@ -15,6 +15,7 @@
import os import os
import re import re
import threading
from typing import Callable, Dict from typing import Callable, Dict
from ._base import Config, ConfigError from ._base import Config, ConfigError
@ -25,6 +26,9 @@ _CACHE_PREFIX = "SYNAPSE_CACHE_FACTOR"
# Map from canonicalised cache name to cache. # Map from canonicalised cache name to cache.
_CACHES = {} _CACHES = {}
# a lock on the contents of _CACHES
_CACHES_LOCK = threading.Lock()
_DEFAULT_FACTOR_SIZE = 0.5 _DEFAULT_FACTOR_SIZE = 0.5
_DEFAULT_EVENT_CACHE_SIZE = "10K" _DEFAULT_EVENT_CACHE_SIZE = "10K"
@ -66,7 +70,10 @@ def add_resizable_cache(cache_name: str, cache_resize_callback: Callable):
# Some caches have '*' in them which we strip out. # Some caches have '*' in them which we strip out.
cache_name = _canonicalise_cache_name(cache_name) cache_name = _canonicalise_cache_name(cache_name)
_CACHES[cache_name] = cache_resize_callback # sometimes caches are initialised from background threads, so we need to make
# sure we don't conflict with another thread running a resize operation
with _CACHES_LOCK:
_CACHES[cache_name] = cache_resize_callback
# Ensure all loaded caches are sized appropriately # Ensure all loaded caches are sized appropriately
# #
@ -87,7 +94,8 @@ class CacheConfig(Config):
os.environ.get(_CACHE_PREFIX, _DEFAULT_FACTOR_SIZE) os.environ.get(_CACHE_PREFIX, _DEFAULT_FACTOR_SIZE)
) )
properties.resize_all_caches_func = None properties.resize_all_caches_func = None
_CACHES.clear() with _CACHES_LOCK:
_CACHES.clear()
def generate_config_section(self, **kwargs): def generate_config_section(self, **kwargs):
return """\ return """\
@ -193,6 +201,8 @@ class CacheConfig(Config):
For each cache, run the mapped callback function with either For each cache, run the mapped callback function with either
a specific cache factor or the default, global one. a specific cache factor or the default, global one.
""" """
for cache_name, callback in _CACHES.items(): # block other threads from modifying _CACHES while we iterate it.
new_factor = self.cache_factors.get(cache_name, self.global_factor) with _CACHES_LOCK:
callback(new_factor) for cache_name, callback in _CACHES.items():
new_factor = self.cache_factors.get(cache_name, self.global_factor)
callback(new_factor)

View File

@ -36,6 +36,7 @@ from .ratelimiting import RatelimitConfig
from .redis import RedisConfig from .redis import RedisConfig
from .registration import RegistrationConfig from .registration import RegistrationConfig
from .repository import ContentRepositoryConfig from .repository import ContentRepositoryConfig
from .room import RoomConfig
from .room_directory import RoomDirectoryConfig from .room_directory import RoomDirectoryConfig
from .saml2_config import SAML2Config from .saml2_config import SAML2Config
from .server import ServerConfig from .server import ServerConfig
@ -79,6 +80,7 @@ class HomeServerConfig(RootConfig):
PasswordAuthProviderConfig, PasswordAuthProviderConfig,
PushConfig, PushConfig,
SpamCheckerConfig, SpamCheckerConfig,
RoomConfig,
GroupsConfig, GroupsConfig,
UserDirectoryConfig, UserDirectoryConfig,
ConsentConfig, ConsentConfig,

80
synapse/config/room.py Normal file
View File

@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
# Copyright 2020 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
from synapse.api.constants import RoomCreationPreset
from ._base import Config, ConfigError
logger = logging.Logger(__name__)
class RoomDefaultEncryptionTypes(object):
"""Possible values for the encryption_enabled_by_default_for_room_type config option"""
ALL = "all"
INVITE = "invite"
OFF = "off"
class RoomConfig(Config):
section = "room"
def read_config(self, config, **kwargs):
# Whether new, locally-created rooms should have encryption enabled
encryption_for_room_type = config.get(
"encryption_enabled_by_default_for_room_type",
RoomDefaultEncryptionTypes.OFF,
)
if encryption_for_room_type == RoomDefaultEncryptionTypes.ALL:
self.encryption_enabled_by_default_for_room_presets = [
RoomCreationPreset.PRIVATE_CHAT,
RoomCreationPreset.TRUSTED_PRIVATE_CHAT,
RoomCreationPreset.PUBLIC_CHAT,
]
elif encryption_for_room_type == RoomDefaultEncryptionTypes.INVITE:
self.encryption_enabled_by_default_for_room_presets = [
RoomCreationPreset.PRIVATE_CHAT,
RoomCreationPreset.TRUSTED_PRIVATE_CHAT,
]
elif encryption_for_room_type == RoomDefaultEncryptionTypes.OFF:
self.encryption_enabled_by_default_for_room_presets = []
else:
raise ConfigError(
"Invalid value for encryption_enabled_by_default_for_room_type"
)
def generate_config_section(self, **kwargs):
return """\
## Rooms ##
# Controls whether locally-created rooms should be end-to-end encrypted by
# default.
#
# Possible options are "all", "invite", and "off". They are defined as:
#
# * "all": any locally-created room
# * "invite": any room created with the "private_chat" or "trusted_private_chat"
# room creation presets
# * "off": this option will take no effect
#
# The default value is "off".
#
# Note that this option will only affect rooms created after it is set. It
# will also not affect rooms created by other servers.
#
#encryption_enabled_by_default_for_room_type: invite
"""

View File

@ -160,7 +160,7 @@ class SAML2Config(Config):
# session lifetime: in milliseconds # session lifetime: in milliseconds
self.saml2_session_lifetime = self.parse_duration( self.saml2_session_lifetime = self.parse_duration(
saml2_config.get("saml_session_lifetime", "5m") saml2_config.get("saml_session_lifetime", "15m")
) )
template_dir = saml2_config.get("template_dir") template_dir = saml2_config.get("template_dir")
@ -286,7 +286,7 @@ class SAML2Config(Config):
# The lifetime of a SAML session. This defines how long a user has to # The lifetime of a SAML session. This defines how long a user has to
# complete the authentication process, if allow_unsolicited is unset. # complete the authentication process, if allow_unsolicited is unset.
# The default is 5 minutes. # The default is 15 minutes.
# #
#saml_session_lifetime: 5m #saml_session_lifetime: 5m

View File

@ -856,7 +856,7 @@ class ServerConfig(Config):
# number of monthly active users. # number of monthly active users.
# #
# 'limit_usage_by_mau' disables/enables monthly active user blocking. When # 'limit_usage_by_mau' disables/enables monthly active user blocking. When
# anabled and a limit is reached the server returns a 'ResourceLimitError' # enabled and a limit is reached the server returns a 'ResourceLimitError'
# with error type Codes.RESOURCE_LIMIT_EXCEEDED # with error type Codes.RESOURCE_LIMIT_EXCEEDED
# #
# 'max_mau_value' is the hard limit of monthly active users above which # 'max_mau_value' is the hard limit of monthly active users above which

View File

@ -33,7 +33,12 @@ from unpaddedbase64 import decode_base64
from twisted.internet import defer from twisted.internet import defer
from synapse import event_auth from synapse import event_auth
from synapse.api.constants import EventTypes, Membership, RejectedReason from synapse.api.constants import (
EventTypes,
Membership,
RejectedReason,
RoomEncryptionAlgorithms,
)
from synapse.api.errors import ( from synapse.api.errors import (
AuthError, AuthError,
CodeMessageException, CodeMessageException,
@ -742,7 +747,10 @@ class FederationHandler(BaseHandler):
if device: if device:
keys = device.get("keys", {}).get("keys", {}) keys = device.get("keys", {}).get("keys", {})
if event.content.get("algorithm") == "m.megolm.v1.aes-sha2": if (
event.content.get("algorithm")
== RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2
):
# For this algorithm we expect a curve25519 key. # For this algorithm we expect a curve25519 key.
key_name = "curve25519:%s" % (device_id,) key_name = "curve25519:%s" % (device_id,)
current_keys = [keys.get(key_name)] current_keys = [keys.get(key_name)]

View File

@ -26,7 +26,12 @@ from typing import Tuple
from six import iteritems, string_types from six import iteritems, string_types
from synapse.api.constants import EventTypes, JoinRules, RoomCreationPreset from synapse.api.constants import (
EventTypes,
JoinRules,
RoomCreationPreset,
RoomEncryptionAlgorithms,
)
from synapse.api.errors import AuthError, Codes, NotFoundError, StoreError, SynapseError from synapse.api.errors import AuthError, Codes, NotFoundError, StoreError, SynapseError
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion
from synapse.events.utils import copy_power_levels_contents from synapse.events.utils import copy_power_levels_contents
@ -56,31 +61,6 @@ FIVE_MINUTES_IN_MS = 5 * 60 * 1000
class RoomCreationHandler(BaseHandler): class RoomCreationHandler(BaseHandler):
PRESETS_DICT = {
RoomCreationPreset.PRIVATE_CHAT: {
"join_rules": JoinRules.INVITE,
"history_visibility": "shared",
"original_invitees_have_ops": False,
"guest_can_join": True,
"power_level_content_override": {"invite": 0},
},
RoomCreationPreset.TRUSTED_PRIVATE_CHAT: {
"join_rules": JoinRules.INVITE,
"history_visibility": "shared",
"original_invitees_have_ops": True,
"guest_can_join": True,
"power_level_content_override": {"invite": 0},
},
RoomCreationPreset.PUBLIC_CHAT: {
"join_rules": JoinRules.PUBLIC,
"history_visibility": "shared",
"original_invitees_have_ops": False,
"guest_can_join": False,
"power_level_content_override": {},
},
}
def __init__(self, hs): def __init__(self, hs):
super(RoomCreationHandler, self).__init__(hs) super(RoomCreationHandler, self).__init__(hs)
@ -89,6 +69,39 @@ class RoomCreationHandler(BaseHandler):
self.room_member_handler = hs.get_room_member_handler() self.room_member_handler = hs.get_room_member_handler()
self.config = hs.config self.config = hs.config
# Room state based off defined presets
self._presets_dict = {
RoomCreationPreset.PRIVATE_CHAT: {
"join_rules": JoinRules.INVITE,
"history_visibility": "shared",
"original_invitees_have_ops": False,
"guest_can_join": True,
"power_level_content_override": {"invite": 0},
},
RoomCreationPreset.TRUSTED_PRIVATE_CHAT: {
"join_rules": JoinRules.INVITE,
"history_visibility": "shared",
"original_invitees_have_ops": True,
"guest_can_join": True,
"power_level_content_override": {"invite": 0},
},
RoomCreationPreset.PUBLIC_CHAT: {
"join_rules": JoinRules.PUBLIC,
"history_visibility": "shared",
"original_invitees_have_ops": False,
"guest_can_join": False,
"power_level_content_override": {},
},
}
# Modify presets to selectively enable encryption by default per homeserver config
for preset_name, preset_config in self._presets_dict.items():
encrypted = (
preset_name
in self.config.encryption_enabled_by_default_for_room_presets
)
preset_config["encrypted"] = encrypted
self._replication = hs.get_replication_data_handler() self._replication = hs.get_replication_data_handler()
# linearizer to stop two upgrades happening at once # linearizer to stop two upgrades happening at once
@ -798,7 +811,7 @@ class RoomCreationHandler(BaseHandler):
) )
return last_stream_id return last_stream_id
config = RoomCreationHandler.PRESETS_DICT[preset_config] config = self._presets_dict[preset_config]
creator_id = creator.user.to_string() creator_id = creator.user.to_string()
@ -888,6 +901,13 @@ class RoomCreationHandler(BaseHandler):
etype=etype, state_key=state_key, content=content etype=etype, state_key=state_key, content=content
) )
if config["encrypted"]:
last_sent_stream_id = await send(
etype=EventTypes.RoomEncryption,
state_key="",
content={"algorithm": RoomEncryptionAlgorithms.DEFAULT},
)
return last_sent_stream_id return last_sent_stream_id
async def _generate_room_id( async def _generate_room_id(

View File

@ -51,7 +51,9 @@ class PresenceStatusRestServlet(RestServlet):
raise AuthError(403, "You are not allowed to see their presence.") raise AuthError(403, "You are not allowed to see their presence.")
state = await self.presence_handler.get_state(target_user=user) state = await self.presence_handler.get_state(target_user=user)
state = format_user_presence_state(state, self.clock.time_msec()) state = format_user_presence_state(
state, self.clock.time_msec(), include_user_id=False
)
return 200, state return 200, state

View File

@ -1,24 +1,24 @@
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<title> Login </title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name='viewport' content='width=device-width, initial-scale=1, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0'> <title> Login </title>
<link rel="stylesheet" href="style.css"> <meta name='viewport' content='width=device-width, initial-scale=1, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0'>
<script src="js/jquery-3.4.1.min.js"></script> <link rel="stylesheet" href="style.css">
<script src="js/login.js"></script> <script src="js/jquery-3.4.1.min.js"></script>
<script src="js/login.js"></script>
</head> </head>
<body onload="matrixLogin.onLoad()"> <body onload="matrixLogin.onLoad()">
<center> <div id="container">
<br/>
<h1 id="title"></h1> <h1 id="title"></h1>
<span id="feedback" style="color: #f00"></span> <span id="feedback"></span>
<div id="loading"> <div id="loading">
<img src="spinner.gif" /> <img src="spinner.gif" />
</div> </div>
<div id="sso_flow" class="login_flow" style="display:none"> <div id="sso_flow" class="login_flow" style="display: none;">
Single-sign on: Single-sign on:
<form id="sso_form" action="/_matrix/client/r0/login/sso/redirect" method="get"> <form id="sso_form" action="/_matrix/client/r0/login/sso/redirect" method="get">
<input id="sso_redirect_url" type="hidden" name="redirectUrl" value=""/> <input id="sso_redirect_url" type="hidden" name="redirectUrl" value=""/>
@ -26,9 +26,9 @@
</form> </form>
</div> </div>
<div id="password_flow" class="login_flow" style="display:none"> <div id="password_flow" class="login_flow" style="display: none;">
Password Authentication: Password Authentication:
<form onsubmit="matrixLogin.password_login(); return false;"> <form onsubmit="matrixLogin.passwordLogin(); return false;">
<input id="user_id" size="32" type="text" placeholder="Matrix ID (e.g. bob)" autocapitalize="off" autocorrect="off" /> <input id="user_id" size="32" type="text" placeholder="Matrix ID (e.g. bob)" autocapitalize="off" autocorrect="off" />
<br/> <br/>
<input id="password" size="32" type="password" placeholder="Password"/> <input id="password" size="32" type="password" placeholder="Password"/>
@ -38,9 +38,9 @@
</form> </form>
</div> </div>
<div id="no_login_types" type="button" class="login_flow" style="display:none"> <div id="no_login_types" type="button" class="login_flow" style="display: none;">
Log in currently unavailable. Log in currently unavailable.
</div> </div>
</center> </div>
</body> </body>
</html> </html>

View File

@ -5,11 +5,11 @@ window.matrixLogin = {
}; };
// Titles get updated through the process to give users feedback. // Titles get updated through the process to give users feedback.
var TITLE_PRE_AUTH = "Log in with one of the following methods"; const TITLE_PRE_AUTH = "Log in with one of the following methods";
var TITLE_POST_AUTH = "Logging in..."; const TITLE_POST_AUTH = "Logging in...";
// The cookie used to store the original query parameters when using SSO. // The cookie used to store the original query parameters when using SSO.
var COOKIE_KEY = "synapse_login_fallback_qs"; const COOKIE_KEY = "synapse_login_fallback_qs";
/* /*
* Submit a login request. * Submit a login request.
@ -20,9 +20,9 @@ var COOKIE_KEY = "synapse_login_fallback_qs";
* login request, e.g. device_id. * login request, e.g. device_id.
* callback: (Optional) Function to call on successful login. * callback: (Optional) Function to call on successful login.
*/ */
var submitLogin = function(type, data, extra, callback) { function submitLogin(type, data, extra, callback) {
console.log("Logging in with " + type); console.log("Logging in with " + type);
set_title(TITLE_POST_AUTH); setTitle(TITLE_POST_AUTH);
// Add the login type. // Add the login type.
data.type = type; data.type = type;
@ -41,12 +41,15 @@ var submitLogin = function(type, data, extra, callback) {
} }
matrixLogin.onLogin(response); matrixLogin.onLogin(response);
}).fail(errorFunc); }).fail(errorFunc);
}; }
var errorFunc = function(err) { /*
* Display an error to the user and show the login form again.
*/
function errorFunc(err) {
// We want to show the error to the user rather than redirecting immediately to the // We want to show the error to the user rather than redirecting immediately to the
// SSO portal (if SSO is the only login option), so we inhibit the redirect. // SSO portal (if SSO is the only login option), so we inhibit the redirect.
show_login(true); showLogin(true);
if (err.responseJSON && err.responseJSON.error) { if (err.responseJSON && err.responseJSON.error) {
setFeedbackString(err.responseJSON.error + " (" + err.responseJSON.errcode + ")"); setFeedbackString(err.responseJSON.error + " (" + err.responseJSON.errcode + ")");
@ -54,27 +57,42 @@ var errorFunc = function(err) {
else { else {
setFeedbackString("Request failed: " + err.status); setFeedbackString("Request failed: " + err.status);
} }
}; }
var setFeedbackString = function(text) { /*
* Display an error to the user.
*/
function setFeedbackString(text) {
$("#feedback").text(text); $("#feedback").text(text);
}; }
var show_login = function(inhibit_redirect) { /*
// Set the redirect to come back to this page, a login token will get added * (Maybe) Show the login forms.
// and handled after the redirect. *
var this_page = window.location.origin + window.location.pathname; * This actually does a few unrelated functions:
$("#sso_redirect_url").val(this_page); *
* * Configures the SSO redirect URL to come back to this page.
* * Configures and shows the SSO form, if the server supports SSO.
* * Otherwise, shows the password form.
*/
function showLogin(inhibitRedirect) {
setTitle(TITLE_PRE_AUTH);
// If inhibit_redirect is false, and SSO is the only supported login method, // If inhibitRedirect is false, and SSO is the only supported login method,
// we can redirect straight to the SSO page. // we can redirect straight to the SSO page.
if (matrixLogin.serverAcceptsSso) { if (matrixLogin.serverAcceptsSso) {
// Set the redirect to come back to this page, a login token will get
// added as a query parameter and handled after the redirect.
$("#sso_redirect_url").val(window.location.origin + window.location.pathname);
// Before submitting SSO, set the current query parameters into a cookie // Before submitting SSO, set the current query parameters into a cookie
// for retrieval later. // for retrieval later.
var qs = parseQsFromUrl(); var qs = parseQsFromUrl();
setCookie(COOKIE_KEY, JSON.stringify(qs)); setCookie(COOKIE_KEY, JSON.stringify(qs));
if (!inhibit_redirect && !matrixLogin.serverAcceptsPassword) { // If password is not supported and redirects are allowed, then submit
// the form (redirecting to the SSO provider).
if (!inhibitRedirect && !matrixLogin.serverAcceptsPassword) {
$("#sso_form").submit(); $("#sso_form").submit();
return; return;
} }
@ -87,30 +105,39 @@ var show_login = function(inhibit_redirect) {
$("#password_flow").show(); $("#password_flow").show();
} }
// If neither password or SSO are supported, show an error to the user.
if (!matrixLogin.serverAcceptsPassword && !matrixLogin.serverAcceptsSso) { if (!matrixLogin.serverAcceptsPassword && !matrixLogin.serverAcceptsSso) {
$("#no_login_types").show(); $("#no_login_types").show();
} }
set_title(TITLE_PRE_AUTH);
$("#loading").hide(); $("#loading").hide();
}; }
var show_spinner = function() { /*
* Hides the forms and shows a loading throbber.
*/
function showSpinner() {
$("#password_flow").hide(); $("#password_flow").hide();
$("#sso_flow").hide(); $("#sso_flow").hide();
$("#no_login_types").hide(); $("#no_login_types").hide();
$("#loading").show(); $("#loading").show();
}; }
var set_title = function(title) { /*
* Helper to show the page's main title.
*/
function setTitle(title) {
$("#title").text(title); $("#title").text(title);
}; }
var fetch_info = function(cb) { /*
* Query the login endpoint for the homeserver's supported flows.
*
* This populates matrixLogin.serverAccepts* variables.
*/
function fetchLoginFlows(cb) {
$.get(matrixLogin.endpoint, function(response) { $.get(matrixLogin.endpoint, function(response) {
var serverAcceptsPassword = false; for (var i = 0; i < response.flows.length; i++) {
for (var i=0; i<response.flows.length; i++) {
var flow = response.flows[i]; var flow = response.flows[i];
if ("m.login.sso" === flow.type) { if ("m.login.sso" === flow.type) {
matrixLogin.serverAcceptsSso = true; matrixLogin.serverAcceptsSso = true;
@ -126,27 +153,41 @@ var fetch_info = function(cb) {
}).fail(errorFunc); }).fail(errorFunc);
} }
/*
* Called on load to fetch login flows and attempt SSO login (if a token is available).
*/
matrixLogin.onLoad = function() { matrixLogin.onLoad = function() {
fetch_info(function() { fetchLoginFlows(function() {
if (!try_token()) { // (Maybe) attempt logging in via SSO if a token is available.
show_login(false); if (!tryTokenLogin()) {
showLogin(false);
} }
}); });
}; };
matrixLogin.password_login = function() { /*
* Submit simple user & password login.
*/
matrixLogin.passwordLogin = function() {
var user = $("#user_id").val(); var user = $("#user_id").val();
var pwd = $("#password").val(); var pwd = $("#password").val();
setFeedbackString(""); setFeedbackString("");
show_spinner(); showSpinner();
submitLogin( submitLogin(
"m.login.password", "m.login.password",
{user: user, password: pwd}, {user: user, password: pwd},
parseQsFromUrl()); parseQsFromUrl());
}; };
/*
* The onLogin function gets called after a succesful login.
*
* It is expected that implementations override this to be notified when the
* login is complete. The response to the login call is provided as the single
* parameter.
*/
matrixLogin.onLogin = function(response) { matrixLogin.onLogin = function(response) {
// clobber this function // clobber this function
console.warn("onLogin - This function should be replaced to proceed."); console.warn("onLogin - This function should be replaced to proceed.");
@ -155,7 +196,7 @@ matrixLogin.onLogin = function(response) {
/* /*
* Process the query parameters from the current URL into an object. * Process the query parameters from the current URL into an object.
*/ */
var parseQsFromUrl = function() { function parseQsFromUrl() {
var pos = window.location.href.indexOf("?"); var pos = window.location.href.indexOf("?");
if (pos == -1) { if (pos == -1) {
return {}; return {};
@ -174,12 +215,12 @@ var parseQsFromUrl = function() {
result[key] = val; result[key] = val;
}); });
return result; return result;
}; }
/* /*
* Process the cookies and return an object. * Process the cookies and return an object.
*/ */
var parseCookies = function() { function parseCookies() {
var allCookies = document.cookie; var allCookies = document.cookie;
var result = {}; var result = {};
allCookies.split(";").forEach(function(part) { allCookies.split(";").forEach(function(part) {
@ -196,32 +237,32 @@ var parseCookies = function() {
result[key] = val; result[key] = val;
}); });
return result; return result;
}; }
/* /*
* Set a cookie that is valid for 1 hour. * Set a cookie that is valid for 1 hour.
*/ */
var setCookie = function(key, value) { function setCookie(key, value) {
// The maximum age is set in seconds. // The maximum age is set in seconds.
var maxAge = 60 * 60; var maxAge = 60 * 60;
// Set the cookie, this defaults to the current domain and path. // Set the cookie, this defaults to the current domain and path.
document.cookie = key + "=" + encodeURIComponent(value) + ";max-age=" + maxAge + ";sameSite=lax"; document.cookie = key + "=" + encodeURIComponent(value) + ";max-age=" + maxAge + ";sameSite=lax";
}; }
/* /*
* Removes a cookie by key. * Removes a cookie by key.
*/ */
var deleteCookie = function(key) { function deleteCookie(key) {
// Delete a cookie by setting the expiration to 0. (Note that the value // Delete a cookie by setting the expiration to 0. (Note that the value
// doesn't matter.) // doesn't matter.)
document.cookie = key + "=deleted;expires=0"; document.cookie = key + "=deleted;expires=0";
}; }
/* /*
* Submits the login token if one is found in the query parameters. Returns a * Submits the login token if one is found in the query parameters. Returns a
* boolean of whether the login token was found or not. * boolean of whether the login token was found or not.
*/ */
var try_token = function() { function tryTokenLogin() {
// Check if the login token is in the query parameters. // Check if the login token is in the query parameters.
var qs = parseQsFromUrl(); var qs = parseQsFromUrl();
@ -233,18 +274,18 @@ var try_token = function() {
// Retrieve the original query parameters (from before the SSO redirect). // Retrieve the original query parameters (from before the SSO redirect).
// They are stored as JSON in a cookie. // They are stored as JSON in a cookie.
var cookies = parseCookies(); var cookies = parseCookies();
var original_query_params = JSON.parse(cookies[COOKIE_KEY] || "{}") var originalQueryParams = JSON.parse(cookies[COOKIE_KEY] || "{}")
// If the login is successful, delete the cookie. // If the login is successful, delete the cookie.
var callback = function() { function callback() {
deleteCookie(COOKIE_KEY); deleteCookie(COOKIE_KEY);
} }
submitLogin( submitLogin(
"m.login.token", "m.login.token",
{token: loginToken}, {token: loginToken},
original_query_params, originalQueryParams,
callback); callback);
return true; return true;
}; }

View File

@ -31,20 +31,44 @@ form {
margin: 10px 0 0 0; margin: 10px 0 0 0;
} }
/*
* Add some padding to the viewport.
*/
#container {
padding: 10px;
}
/*
* Center all direct children of the main form.
*/
#container > * {
display: block;
margin-left: auto;
margin-right: auto;
text-align: center;
}
/*
* A wrapper around each login flow.
*/
.login_flow { .login_flow {
width: 300px; width: 300px;
text-align: left; text-align: left;
padding: 10px; padding: 10px;
margin-bottom: 40px; margin-bottom: 40px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px; border-radius: 10px;
-webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
-moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15); box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
background-color: #f8f8f8; background-color: #f8f8f8;
border: 1px #ccc solid; border: 1px #ccc solid;
} }
/*
* Used to show error content.
*/
#feedback {
/* Red text. */
color: #ff0000;
/* A little space to not overlap the box-shadow. */
margin-bottom: 20px;
}

View File

@ -21,6 +21,7 @@ from signedjson.types import BaseKey, SigningKey
from twisted.internet import defer from twisted.internet import defer
from synapse.api.constants import RoomEncryptionAlgorithms
from synapse.rest import admin from synapse.rest import admin
from synapse.rest.client.v1 import login from synapse.rest.client.v1 import login
from synapse.types import JsonDict, ReadReceipt from synapse.types import JsonDict, ReadReceipt
@ -536,7 +537,10 @@ def build_device_dict(user_id: str, device_id: str, sk: SigningKey):
return { return {
"user_id": user_id, "user_id": user_id,
"device_id": device_id, "device_id": device_id,
"algorithms": ["m.olm.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"], "algorithms": [
"m.olm.curve25519-aes-sha2",
RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
],
"keys": { "keys": {
"curve25519:" + device_id: "curve25519+key", "curve25519:" + device_id: "curve25519+key",
key_id(sk): encode_pubkey(sk), key_id(sk): encode_pubkey(sk),

View File

@ -25,6 +25,7 @@ from twisted.internet import defer
import synapse.handlers.e2e_keys import synapse.handlers.e2e_keys
import synapse.storage import synapse.storage
from synapse.api import errors from synapse.api import errors
from synapse.api.constants import RoomEncryptionAlgorithms
from tests import unittest, utils from tests import unittest, utils
@ -222,7 +223,10 @@ class E2eKeysHandlerTestCase(unittest.TestCase):
device_key_1 = { device_key_1 = {
"user_id": local_user, "user_id": local_user,
"device_id": "abc", "device_id": "abc",
"algorithms": ["m.olm.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"], "algorithms": [
"m.olm.curve25519-aes-sha2",
RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
],
"keys": { "keys": {
"ed25519:abc": "base64+ed25519+key", "ed25519:abc": "base64+ed25519+key",
"curve25519:abc": "base64+curve25519+key", "curve25519:abc": "base64+curve25519+key",
@ -232,7 +236,10 @@ class E2eKeysHandlerTestCase(unittest.TestCase):
device_key_2 = { device_key_2 = {
"user_id": local_user, "user_id": local_user,
"device_id": "def", "device_id": "def",
"algorithms": ["m.olm.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"], "algorithms": [
"m.olm.curve25519-aes-sha2",
RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
],
"keys": { "keys": {
"ed25519:def": "base64+ed25519+key", "ed25519:def": "base64+ed25519+key",
"curve25519:def": "base64+curve25519+key", "curve25519:def": "base64+curve25519+key",
@ -315,7 +322,10 @@ class E2eKeysHandlerTestCase(unittest.TestCase):
device_key = { device_key = {
"user_id": local_user, "user_id": local_user,
"device_id": device_id, "device_id": device_id,
"algorithms": ["m.olm.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"], "algorithms": [
"m.olm.curve25519-aes-sha2",
RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
],
"keys": {"curve25519:xyz": "curve25519+key", "ed25519:xyz": device_pubkey}, "keys": {"curve25519:xyz": "curve25519+key", "ed25519:xyz": device_pubkey},
"signatures": {local_user: {"ed25519:xyz": "something"}}, "signatures": {local_user: {"ed25519:xyz": "something"}},
} }
@ -392,7 +402,7 @@ class E2eKeysHandlerTestCase(unittest.TestCase):
"device_id": device_id, "device_id": device_id,
"algorithms": [ "algorithms": [
"m.olm.curve25519-aes-sha2", "m.olm.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2", RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
], ],
"keys": { "keys": {
"curve25519:xyz": "curve25519+key", "curve25519:xyz": "curve25519+key",

View File

@ -17,12 +17,13 @@ from mock import Mock
from twisted.internet import defer from twisted.internet import defer
import synapse.rest.admin import synapse.rest.admin
from synapse.api.constants import UserTypes from synapse.api.constants import EventTypes, RoomEncryptionAlgorithms, UserTypes
from synapse.rest.client.v1 import login, room from synapse.rest.client.v1 import login, room
from synapse.rest.client.v2_alpha import user_directory from synapse.rest.client.v2_alpha import user_directory
from synapse.storage.roommember import ProfileInfo from synapse.storage.roommember import ProfileInfo
from tests import unittest from tests import unittest
from tests.unittest import override_config
class UserDirectoryTestCase(unittest.HomeserverTestCase): class UserDirectoryTestCase(unittest.HomeserverTestCase):
@ -147,6 +148,94 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase):
s = self.get_success(self.handler.search_users(u1, "user3", 10)) s = self.get_success(self.handler.search_users(u1, "user3", 10))
self.assertEqual(len(s["results"]), 0) self.assertEqual(len(s["results"]), 0)
@override_config({"encryption_enabled_by_default_for_room_type": "all"})
def test_encrypted_by_default_config_option_all(self):
"""Tests that invite-only and non-invite-only rooms have encryption enabled by
default when the config option encryption_enabled_by_default_for_room_type is "all".
"""
# Create a user
user = self.register_user("user", "pass")
user_token = self.login(user, "pass")
# Create an invite-only room as that user
room_id = self.helper.create_room_as(user, is_public=False, tok=user_token)
# Check that the room has an encryption state event
event_content = self.helper.get_state(
room_id=room_id, event_type=EventTypes.RoomEncryption, tok=user_token,
)
self.assertEqual(event_content, {"algorithm": RoomEncryptionAlgorithms.DEFAULT})
# Create a non invite-only room as that user
room_id = self.helper.create_room_as(user, is_public=True, tok=user_token)
# Check that the room has an encryption state event
event_content = self.helper.get_state(
room_id=room_id, event_type=EventTypes.RoomEncryption, tok=user_token,
)
self.assertEqual(event_content, {"algorithm": RoomEncryptionAlgorithms.DEFAULT})
@override_config({"encryption_enabled_by_default_for_room_type": "invite"})
def test_encrypted_by_default_config_option_invite(self):
"""Tests that only new, invite-only rooms have encryption enabled by default when
the config option encryption_enabled_by_default_for_room_type is "invite".
"""
# Create a user
user = self.register_user("user", "pass")
user_token = self.login(user, "pass")
# Create an invite-only room as that user
room_id = self.helper.create_room_as(user, is_public=False, tok=user_token)
# Check that the room has an encryption state event
event_content = self.helper.get_state(
room_id=room_id, event_type=EventTypes.RoomEncryption, tok=user_token,
)
self.assertEqual(event_content, {"algorithm": RoomEncryptionAlgorithms.DEFAULT})
# Create a non invite-only room as that user
room_id = self.helper.create_room_as(user, is_public=True, tok=user_token)
# Check that the room does not have an encryption state event
self.helper.get_state(
room_id=room_id,
event_type=EventTypes.RoomEncryption,
tok=user_token,
expect_code=404,
)
@override_config({"encryption_enabled_by_default_for_room_type": "off"})
def test_encrypted_by_default_config_option_off(self):
"""Tests that neither new invite-only nor non-invite-only rooms have encryption
enabled by default when the config option
encryption_enabled_by_default_for_room_type is "off".
"""
# Create a user
user = self.register_user("user", "pass")
user_token = self.login(user, "pass")
# Create an invite-only room as that user
room_id = self.helper.create_room_as(user, is_public=False, tok=user_token)
# Check that the room does not have an encryption state event
self.helper.get_state(
room_id=room_id,
event_type=EventTypes.RoomEncryption,
tok=user_token,
expect_code=404,
)
# Create a non invite-only room as that user
room_id = self.helper.create_room_as(user, is_public=True, tok=user_token)
# Check that the room does not have an encryption state event
self.helper.get_state(
room_id=room_id,
event_type=EventTypes.RoomEncryption,
tok=user_token,
expect_code=404,
)
def test_spam_checker(self): def test_spam_checker(self):
""" """
A user which fails to the spam checks will not appear in search results. A user which fails to the spam checks will not appear in search results.

View File

@ -30,7 +30,7 @@ class MessageAcceptTests(unittest.HomeserverTestCase):
room_creator = self.homeserver.get_room_creation_handler() room_creator = self.homeserver.get_room_creation_handler()
room_deferred = ensureDeferred( room_deferred = ensureDeferred(
room_creator.create_room( room_creator.create_room(
our_user, room_creator.PRESETS_DICT["public_chat"], ratelimit=False our_user, room_creator._presets_dict["public_chat"], ratelimit=False
) )
) )
self.reactor.advance(0.1) self.reactor.advance(0.1)