Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Commit

Permalink
Merge tag 'v1.60.0rc2' into develop
Browse files Browse the repository at this point in the history
Synapse 1.60.0rc2 (2022-05-27)
==============================

This release of Synapse adds a unique index to the `state_group_edges` table, in
order to prevent accidentally introducing duplicate information (for example,
because a database backup was restored multiple times). If your Synapse database
already has duplicate rows in this table, this could fail with an error and
require manual remediation.

Additionally, the signature of the `check_event_for_spam` module callback has changed.
The previous signature has been deprecated and remains working for now. Module authors
should update their modules to use the new signature where possible.

See [the upgrade notes](https://github.com/matrix-org/synapse/blob/develop/docs/upgrade.md#upgrading-to-v1600)
for more details.

Features
--------

- Add an option allowing users to use their password to reauthenticate for privileged actions even though password login is disabled. ([\#12883](#12883))

Bugfixes
--------

- Explicitly close `ijson` coroutines once we are done with them, instead of leaving the garbage collector to close them. ([\#12875](#12875))

Internal Changes
----------------

- Improve URL previews by not including the content of media tags in the generated description. ([\#12887](#12887))
  • Loading branch information
Sean Quah committed May 27, 2022
2 parents a7da00d + e409ab8 commit 053ca5f
Show file tree
Hide file tree
Showing 11 changed files with 138 additions and 17 deletions.
23 changes: 22 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Synapse 1.60.0rc1 (2022-05-24)
Synapse 1.60.0rc2 (2022-05-27)
==============================

This release of Synapse adds a unique index to the `state_group_edges` table, in
Expand All @@ -17,6 +17,27 @@ for more details.
Features
--------

- Add an option allowing users to use their password to reauthenticate for privileged actions even though password login is disabled. ([\#12883](https://github.com/matrix-org/synapse/issues/12883))


Bugfixes
--------

- Explicitly close `ijson` coroutines once we are done with them, instead of leaving the garbage collector to close them. ([\#12875](https://github.com/matrix-org/synapse/issues/12875))


Internal Changes
----------------

- Improve URL previews by not including the content of media tags in the generated description. ([\#12887](https://github.com/matrix-org/synapse/issues/12887))


Synapse 1.60.0rc1 (2022-05-24)
==============================

Features
--------

- Measure the time taken in spam-checking callbacks and expose those measurements as metrics. ([\#12513](https://github.com/matrix-org/synapse/issues/12513))
- Add a `default_power_level_content_override` config option to set default room power levels per room preset. ([\#12618](https://github.com/matrix-org/synapse/issues/12618))
- Add support for [MSC3787: Allowing knocks to restricted rooms](https://github.com/matrix-org/matrix-spec-proposals/pull/3787). ([\#12623](https://github.com/matrix-org/synapse/issues/12623))
Expand Down
6 changes: 6 additions & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
matrix-synapse-py3 (1.60.0~rc2) stable; urgency=medium

* New Synapse release 1.60.0rc2.

-- Synapse Packaging team <[email protected]> Fri, 27 May 2022 11:04:55 +0100

matrix-synapse-py3 (1.60.0~rc1) stable; urgency=medium

* New Synapse release 1.60.0rc1.
Expand Down
4 changes: 3 additions & 1 deletion docs/sample_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2216,7 +2216,9 @@ sso:


password_config:
# Uncomment to disable password login
# Uncomment to disable password login.
# Set to `only_for_reauth` to permit reauthentication for users that
# have passwords and are already logged in.
#
#enabled: false

Expand Down
3 changes: 3 additions & 0 deletions docs/usage/configuration/config_documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -2930,6 +2930,9 @@ Use this setting to enable password-based logins.

This setting has the following sub-options:
* `enabled`: Defaults to true.
Set to false to disable password authentication.
Set to `only_for_reauth` to allow users with existing passwords to use them
to log in and reauthenticate, whilst preventing new users from setting passwords.
* `localdb_enabled`: Set to false to disable authentication against the local password
database. This is ignored if `enabled` is false, and is only useful
if you have other `password_providers`. Defaults to true.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ skip_gitignore = true

[tool.poetry]
name = "matrix-synapse"
version = "1.60.0rc1"
version = "1.60.0rc2"
description = "Homeserver for the Matrix decentralised comms protocol"
authors = ["Matrix.org Team and Contributors <[email protected]>"]
license = "Apache-2.0"
Expand Down
17 changes: 15 additions & 2 deletions synapse/config/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,18 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
if password_config is None:
password_config = {}

self.password_enabled = password_config.get("enabled", True)
passwords_enabled = password_config.get("enabled", True)
# 'only_for_reauth' allows users who have previously set a password to use it,
# even though passwords would otherwise be disabled.
passwords_for_reauth_only = passwords_enabled == "only_for_reauth"

self.password_enabled_for_login = (
passwords_enabled and not passwords_for_reauth_only
)
self.password_enabled_for_reauth = (
passwords_for_reauth_only or passwords_enabled
)

self.password_localdb_enabled = password_config.get("localdb_enabled", True)
self.password_pepper = password_config.get("pepper", "")

Expand All @@ -46,7 +57,9 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
def generate_config_section(self, **kwargs: Any) -> str:
return """\
password_config:
# Uncomment to disable password login
# Uncomment to disable password login.
# Set to `only_for_reauth` to permit reauthentication for users that
# have passwords and are already logged in.
#
#enabled: false
Expand Down
9 changes: 7 additions & 2 deletions synapse/federation/transport/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1361,7 +1361,7 @@ class SendJoinParser(ByteParser[SendJoinResponse]):
def __init__(self, room_version: RoomVersion, v1_api: bool):
self._response = SendJoinResponse([], [], event_dict={})
self._room_version = room_version
self._coros = []
self._coros: List[Generator[None, bytes, None]] = []

# The V1 API has the shape of `[200, {...}]`, which we handle by
# prefixing with `item.*`.
Expand Down Expand Up @@ -1409,6 +1409,9 @@ def write(self, data: bytes) -> int:
return len(data)

def finish(self) -> SendJoinResponse:
for c in self._coros:
c.close()

if self._response.event_dict:
self._response.event = make_event_from_dict(
self._response.event_dict, self._room_version
Expand All @@ -1431,7 +1434,7 @@ class _StateParser(ByteParser[StateRequestResponse]):
def __init__(self, room_version: RoomVersion):
self._response = StateRequestResponse([], [])
self._room_version = room_version
self._coros = [
self._coros: List[Generator[None, bytes, None]] = [
ijson.items_coro(
_event_list_parser(room_version, self._response.state),
"pdus.item",
Expand All @@ -1450,4 +1453,6 @@ def write(self, data: bytes) -> int:
return len(data)

def finish(self) -> StateRequestResponse:
for c in self._coros:
c.close()
return self._response
29 changes: 20 additions & 9 deletions synapse/handlers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,8 @@ def __init__(self, hs: "HomeServer"):

self.hs = hs # FIXME better possibility to access registrationHandler later?
self.macaroon_gen = hs.get_macaroon_generator()
self._password_enabled = hs.config.auth.password_enabled
self._password_enabled_for_login = hs.config.auth.password_enabled_for_login
self._password_enabled_for_reauth = hs.config.auth.password_enabled_for_reauth
self._password_localdb_enabled = hs.config.auth.password_localdb_enabled
self._third_party_rules = hs.get_third_party_event_rules()

Expand Down Expand Up @@ -387,13 +388,13 @@ def get_new_session_data() -> JsonDict:
return params, session_id

async def _get_available_ui_auth_types(self, user: UserID) -> Iterable[str]:
"""Get a list of the authentication types this user can use"""
"""Get a list of the user-interactive authentication types this user can use."""

ui_auth_types = set()

# if the HS supports password auth, and the user has a non-null password, we
# support password auth
if self._password_localdb_enabled and self._password_enabled:
if self._password_localdb_enabled and self._password_enabled_for_reauth:
lookupres = await self._find_user_id_and_pwd_hash(user.to_string())
if lookupres:
_, password_hash = lookupres
Expand All @@ -402,7 +403,7 @@ async def _get_available_ui_auth_types(self, user: UserID) -> Iterable[str]:

# also allow auth from password providers
for t in self.password_auth_provider.get_supported_login_types().keys():
if t == LoginType.PASSWORD and not self._password_enabled:
if t == LoginType.PASSWORD and not self._password_enabled_for_reauth:
continue
ui_auth_types.add(t)

Expand Down Expand Up @@ -710,7 +711,7 @@ async def _check_auth_dict(
return res

# fall back to the v1 login flow
canonical_id, _ = await self.validate_login(authdict)
canonical_id, _ = await self.validate_login(authdict, is_reauth=True)
return canonical_id

def _get_params_recaptcha(self) -> dict:
Expand Down Expand Up @@ -1064,7 +1065,7 @@ def can_change_password(self) -> bool:
Returns:
Whether users on this server are allowed to change or set a password
"""
return self._password_enabled and self._password_localdb_enabled
return self._password_enabled_for_login and self._password_localdb_enabled

def get_supported_login_types(self) -> Iterable[str]:
"""Get a the login types supported for the /login API
Expand All @@ -1089,9 +1090,9 @@ def get_supported_login_types(self) -> Iterable[str]:
# that comes first, where it's present.
if LoginType.PASSWORD in types:
types.remove(LoginType.PASSWORD)
if self._password_enabled:
if self._password_enabled_for_login:
types.insert(0, LoginType.PASSWORD)
elif self._password_localdb_enabled and self._password_enabled:
elif self._password_localdb_enabled and self._password_enabled_for_login:
types.insert(0, LoginType.PASSWORD)

return types
Expand All @@ -1100,6 +1101,7 @@ async def validate_login(
self,
login_submission: Dict[str, Any],
ratelimit: bool = False,
is_reauth: bool = False,
) -> Tuple[str, Optional[Callable[["LoginResponse"], Awaitable[None]]]]:
"""Authenticates the user for the /login API
Expand All @@ -1110,6 +1112,9 @@ async def validate_login(
login_submission: the whole of the login submission
(including 'type' and other relevant fields)
ratelimit: whether to apply the failed_login_attempt ratelimiter
is_reauth: whether this is part of a User-Interactive Authorisation
flow to reauthenticate for a privileged action (rather than a
new login)
Returns:
A tuple of the canonical user id, and optional callback
to be called once the access token and device id are issued
Expand All @@ -1132,8 +1137,14 @@ async def validate_login(
# special case to check for "password" for the check_password interface
# for the auth providers
password = login_submission.get("password")

if login_type == LoginType.PASSWORD:
if not self._password_enabled:
if is_reauth:
passwords_allowed_here = self._password_enabled_for_reauth
else:
passwords_allowed_here = self._password_enabled_for_login

if not passwords_allowed_here:
raise SynapseError(400, "Password login has been disabled.")
if not isinstance(password, str):
raise SynapseError(400, "Bad parameter: password", Codes.INVALID_PARAM)
Expand Down
11 changes: 11 additions & 0 deletions synapse/http/matrixfederationclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ async def _handle_response(

max_response_size = parser.MAX_RESPONSE_SIZE

finished = False
try:
check_content_type_is(response.headers, parser.CONTENT_TYPE)

Expand All @@ -231,6 +232,7 @@ async def _handle_response(

length = await make_deferred_yieldable(d)

finished = True
value = parser.finish()
except BodyExceededMaxSize as e:
# The response was too big.
Expand Down Expand Up @@ -281,6 +283,15 @@ async def _handle_response(
e,
)
raise
finally:
if not finished:
# There was an exception and we didn't `finish()` the parse.
# Let the parser know that it can free up any resources.
try:
parser.finish()
except Exception:
# Ignore any additional exceptions.
pass

time_taken_secs = reactor.seconds() - start_ms / 1000

Expand Down
10 changes: 9 additions & 1 deletion synapse/rest/media/v1/preview_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,9 @@ def parse_html_description(tree: "etree.Element") -> Optional[str]:
Grabs any text nodes which are inside the <body/> tag, unless they are within
an HTML5 semantic markup tag (<header/>, <nav/>, <aside/>, <footer/>), or
if they are within a <script/> or <style/> tag.
if they are within a <script/>, <svg/> or <style/> tag, or if they are within
a tag whose content is usually only shown to old browsers
(<iframe/>, <video/>, <canvas/>, <picture/>).
This is a very very very coarse approximation to a plain text render of the page.
Expand All @@ -268,6 +270,12 @@ def parse_html_description(tree: "etree.Element") -> Optional[str]:
"script",
"noscript",
"style",
"svg",
"iframe",
"video",
"canvas",
"img",
"picture",
etree.Comment,
)

Expand Down
41 changes: 41 additions & 0 deletions tests/rest/client/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,17 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
self.user_pass = "pass"
self.user = self.register_user("test", self.user_pass)
self.device_id = "dev1"

# Force-enable password login for just long enough to log in.
auth_handler = self.hs.get_auth_handler()
allow_auth_for_login = auth_handler._password_enabled_for_login
auth_handler._password_enabled_for_login = True

self.user_tok = self.login("test", self.user_pass, self.device_id)

# Restore password login to however it was.
auth_handler._password_enabled_for_login = allow_auth_for_login

def delete_device(
self,
access_token: str,
Expand Down Expand Up @@ -263,6 +272,38 @@ def test_ui_auth(self) -> None:
},
)

@override_config({"password_config": {"enabled": "only_for_reauth"}})
def test_ui_auth_with_passwords_for_reauth_only(self) -> None:
"""
Test user interactive authentication outside of registration.
"""

# Attempt to delete this device.
# Returns a 401 as per the spec
channel = self.delete_device(
self.user_tok, self.device_id, HTTPStatus.UNAUTHORIZED
)

# Grab the session
session = channel.json_body["session"]
# Ensure that flows are what is expected.
self.assertIn({"stages": ["m.login.password"]}, channel.json_body["flows"])

# Make another request providing the UI auth flow.
self.delete_device(
self.user_tok,
self.device_id,
HTTPStatus.OK,
{
"auth": {
"type": "m.login.password",
"identifier": {"type": "m.id.user", "user": self.user},
"password": self.user_pass,
"session": session,
},
},
)

def test_grandfathered_identifier(self) -> None:
"""Check behaviour without "identifier" dict
Expand Down

0 comments on commit 053ca5f

Please sign in to comment.