Compare commits
20 Commits
dbf46f3891
...
16744644f6
Author | SHA1 | Date |
---|---|---|
Patrick Cloke | 16744644f6 | |
Patrick Cloke | 8388384a64 | |
Patrick Cloke | c21bdc813f | |
Richard van der Hoff | d3ed93504b | |
Andrew Morgan | edb3d3f827 | |
Richard van der Hoff | 4d9496559d | |
Richard van der Hoff | 9edff901d1 | |
Nicolas Chamo | 3f0cba657c | |
Richard van der Hoff | 89f7930730 | |
Richard van der Hoff | ddc4343683 | |
Richard van der Hoff | 09ac0569fe | |
Andrew Morgan | d1be293f00 | |
Richard van der Hoff | 59e18a1333 | |
Mathieu Velten | 9f0f274fe0 | |
Richard van der Hoff | f8d13ca13d | |
Andrew Morgan | 17fa58bdd1 | |
Jonathan de Jong | ca60822b34 | |
Richard van der Hoff | a090b86209 | |
Tulir Asokan | 856eab606b | |
Andrew Morgan | 5cbe8d93fe |
|
@ -6,7 +6,7 @@
|
|||
set -ex
|
||||
|
||||
apt-get update
|
||||
apt-get install -y python3.5 python3.5-dev python3-pip libxml2-dev libxslt-dev zlib1g-dev tox
|
||||
apt-get install -y python3.5 python3.5-dev python3-pip libxml2-dev libxslt-dev xmlsec1 zlib1g-dev tox
|
||||
|
||||
export LANG="C.UTF-8"
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Simplify the way the `HomeServer` object caches its internal attributes.
|
|
@ -0,0 +1 @@
|
|||
Allow per-room profiles to be used for the server notice user.
|
|
@ -0,0 +1 @@
|
|||
Add additional error checking for OpenID Connect and SAML mapping providers.
|
|
@ -0,0 +1 @@
|
|||
Allow Date header through CORS. Contributed by Nicolas Chamo.
|
|
@ -0,0 +1 @@
|
|||
Remove unnecessary function arguments and add typing to several membership replication classes.
|
|
@ -0,0 +1 @@
|
|||
Add tests for `password_auth_provider`s.
|
|
@ -0,0 +1 @@
|
|||
Add a config option, `push.group_by_unread_count`, which controls whether unread message counts in push notifications are defined as "the number of rooms with unread messages" or "total unread messages".
|
|
@ -0,0 +1 @@
|
|||
Disable pretty printing JSON responses for curl. Users who want pretty-printed output should use [jq](https://stedolan.github.io/jq/) in combination with curl. Contributed by @tulir.
|
|
@ -0,0 +1 @@
|
|||
Fix minor long-standing bug in login, where we would offer the `password` login type if a custom auth provider supported it, even if password login was disabled.
|
|
@ -0,0 +1 @@
|
|||
Add `force_purge` option to delete-room admin api.
|
|
@ -0,0 +1 @@
|
|||
Drop redundant database index on `event_json`.
|
|
@ -0,0 +1 @@
|
|||
Simplify `uk.half-shot.msc2778.login.application_service` login handler.
|
|
@ -0,0 +1 @@
|
|||
Fix a long-standing bug which caused Synapse to require unspecified parameters during user-interactive authentication.
|
|
@ -0,0 +1 @@
|
|||
Refactor `password_auth_provider` support code.
|
|
@ -0,0 +1 @@
|
|||
Add missing `ordering` to background database updates.
|
|
@ -0,0 +1 @@
|
|||
Simplify the way the `HomeServer` object caches its internal attributes.
|
|
@ -0,0 +1 @@
|
|||
Allow for specifying a room version when creating a room in unit tests via `RestHelper.create_room_as`.
|
|
@ -0,0 +1 @@
|
|||
Add support for re-trying generation of a localpart for OpenID Connect mapping providers.
|
|
@ -382,7 +382,7 @@ the new room. Users on other servers will be unaffected.
|
|||
|
||||
The API is:
|
||||
|
||||
```json
|
||||
```
|
||||
POST /_synapse/admin/v1/rooms/<room_id>/delete
|
||||
```
|
||||
|
||||
|
@ -439,6 +439,10 @@ The following JSON body parameters are available:
|
|||
future attempts to join the room. Defaults to `false`.
|
||||
* `purge` - Optional. If set to `true`, it will remove all traces of the room from your database.
|
||||
Defaults to `true`.
|
||||
* `force_purge` - Optional, and ignored unless `purge` is `true`. If set to `true`, it
|
||||
will force a purge to go ahead even if there are local users still in the room. Do not
|
||||
use this unless a regular `purge` operation fails, as it could leave those users'
|
||||
clients in a confused state.
|
||||
|
||||
The JSON body must not be empty. The body must be at least `{}`.
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ Password auth provider classes must provide the following methods:
|
|||
|
||||
It should perform any appropriate sanity checks on the provided
|
||||
configuration, and return an object which is then passed into
|
||||
`__init__`.
|
||||
|
||||
This method should have the `@staticmethod` decoration.
|
||||
|
||||
|
|
|
@ -2271,6 +2271,16 @@ push:
|
|||
#
|
||||
#include_content: false
|
||||
|
||||
# When a push notification is received, an unread count is also sent.
|
||||
# This number can either be calculated as the number of unread messages
|
||||
# for the user, or the number of *rooms* the user has unread messages in.
|
||||
#
|
||||
# The default value is "true", meaning push clients will see the number of
|
||||
# rooms with unread messages in them. Uncomment to instead send the number
|
||||
# of unread messages.
|
||||
#
|
||||
#group_unread_count_by_room: false
|
||||
|
||||
|
||||
# Spam checkers are third-party modules that can block specific actions
|
||||
# of local users, such as creating rooms and registering undesirable
|
||||
|
|
1
mypy.ini
1
mypy.ini
|
@ -80,6 +80,7 @@ files =
|
|||
synapse/util/metrics.py,
|
||||
tests/replication,
|
||||
tests/test_utils,
|
||||
tests/handlers/test_password_providers.py,
|
||||
tests/rest/client/v2_alpha/test_auth.py,
|
||||
tests/util/test_stream_change_cache.py
|
||||
|
||||
|
|
|
@ -23,6 +23,9 @@ class PushConfig(Config):
|
|||
def read_config(self, config, **kwargs):
|
||||
push_config = config.get("push") or {}
|
||||
self.push_include_content = push_config.get("include_content", True)
|
||||
self.push_group_unread_count_by_room = push_config.get(
|
||||
"group_unread_count_by_room", True
|
||||
)
|
||||
|
||||
pusher_instances = config.get("pusher_instances") or []
|
||||
self.pusher_shard_config = ShardedWorkerHandlingConfig(pusher_instances)
|
||||
|
@ -68,4 +71,14 @@ class PushConfig(Config):
|
|||
# include the event ID and room ID in push notification payloads.
|
||||
#
|
||||
#include_content: false
|
||||
|
||||
# When a push notification is received, an unread count is also sent.
|
||||
# This number can either be calculated as the number of unread messages
|
||||
# for the user, or the number of *rooms* the user has unread messages in.
|
||||
#
|
||||
# The default value is "true", meaning push clients will see the number of
|
||||
# rooms with unread messages in them. Uncomment to instead send the number
|
||||
# of unread messages.
|
||||
#
|
||||
#group_unread_count_by_room: false
|
||||
"""
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 - 2016 OpenMarket Ltd
|
||||
# Copyright 2017 Vector Creations Ltd
|
||||
# Copyright 2019 - 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.
|
||||
|
@ -25,6 +26,7 @@ from typing import (
|
|||
Dict,
|
||||
Iterable,
|
||||
List,
|
||||
Mapping,
|
||||
Optional,
|
||||
Tuple,
|
||||
Union,
|
||||
|
@ -181,17 +183,12 @@ class AuthHandler(BaseHandler):
|
|||
# better way to break the loop
|
||||
account_handler = ModuleApi(hs, self)
|
||||
|
||||
self.password_providers = []
|
||||
for module, config in hs.config.password_providers:
|
||||
try:
|
||||
self.password_providers.append(
|
||||
module(config=config, account_handler=account_handler)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Error while initializing %r: %s", module, e)
|
||||
raise
|
||||
self.password_providers = [
|
||||
PasswordProvider.load(module, config, account_handler)
|
||||
for module, config in hs.config.password_providers
|
||||
]
|
||||
|
||||
logger.info("Extra password_providers: %r", self.password_providers)
|
||||
logger.info("Extra password_providers: %s", self.password_providers)
|
||||
|
||||
self.hs = hs # FIXME better possibility to access registrationHandler later?
|
||||
self.macaroon_gen = hs.get_macaroon_generator()
|
||||
|
@ -205,15 +202,23 @@ class AuthHandler(BaseHandler):
|
|||
# type in the list. (NB that the spec doesn't require us to do so and
|
||||
# clients which favour types that they don't understand over those that
|
||||
# they do are technically broken)
|
||||
|
||||
# start out by assuming PASSWORD is enabled; we will remove it later if not.
|
||||
login_types = []
|
||||
if self._password_enabled:
|
||||
if hs.config.password_localdb_enabled:
|
||||
login_types.append(LoginType.PASSWORD)
|
||||
|
||||
for provider in self.password_providers:
|
||||
if hasattr(provider, "get_supported_login_types"):
|
||||
for t in provider.get_supported_login_types().keys():
|
||||
if t not in login_types:
|
||||
login_types.append(t)
|
||||
|
||||
if not self._password_enabled:
|
||||
login_types.remove(LoginType.PASSWORD)
|
||||
|
||||
self._supported_login_types = login_types
|
||||
|
||||
# Login types and UI Auth types have a heavy overlap, but are not
|
||||
# necessarily identical. Login types have SSO (and other login types)
|
||||
# added in the rest layer, see synapse.rest.client.v1.login.LoginRestServerlet.on_GET.
|
||||
|
@ -230,6 +235,13 @@ class AuthHandler(BaseHandler):
|
|||
burst_count=self.hs.config.rc_login_failed_attempts.burst_count,
|
||||
)
|
||||
|
||||
# Ratelimitier for failed /login attempts
|
||||
self._failed_login_attempts_ratelimiter = Ratelimiter(
|
||||
clock=hs.get_clock(),
|
||||
rate_hz=self.hs.config.rc_login_failed_attempts.per_second,
|
||||
burst_count=self.hs.config.rc_login_failed_attempts.burst_count,
|
||||
)
|
||||
|
||||
self._clock = self.hs.get_clock()
|
||||
|
||||
# Expire old UI auth sessions after a period of time.
|
||||
|
@ -642,14 +654,8 @@ class AuthHandler(BaseHandler):
|
|||
res = await checker.check_auth(authdict, clientip=clientip)
|
||||
return res
|
||||
|
||||
# build a v1-login-style dict out of the authdict and fall back to the
|
||||
# v1 code
|
||||
user_id = authdict.get("user")
|
||||
|
||||
if user_id is None:
|
||||
raise SynapseError(400, "", Codes.MISSING_PARAM)
|
||||
|
||||
(canonical_id, callback) = await self.validate_login(user_id, authdict)
|
||||
# fall back to the v1 login flow
|
||||
canonical_id, _ = await self.validate_login(authdict)
|
||||
return canonical_id
|
||||
|
||||
def _get_params_recaptcha(self) -> dict:
|
||||
|
@ -824,15 +830,157 @@ class AuthHandler(BaseHandler):
|
|||
return self._supported_login_types
|
||||
|
||||
async def validate_login(
|
||||
self, username: str, login_submission: Dict[str, Any]
|
||||
self, login_submission: Dict[str, Any], ratelimit: bool = False,
|
||||
) -> Tuple[str, Optional[Callable[[Dict[str, str]], None]]]:
|
||||
"""Authenticates the user for the /login API
|
||||
|
||||
Also used by the user-interactive auth flow to validate
|
||||
m.login.password auth types.
|
||||
Also used by the user-interactive auth flow to validate auth types which don't
|
||||
have an explicit UIA handler, including m.password.auth.
|
||||
|
||||
Args:
|
||||
username: username supplied by the user
|
||||
login_submission: the whole of the login submission
|
||||
(including 'type' and other relevant fields)
|
||||
ratelimit: whether to apply the failed_login_attempt ratelimiter
|
||||
Returns:
|
||||
A tuple of the canonical user id, and optional callback
|
||||
to be called once the access token and device id are issued
|
||||
Raises:
|
||||
StoreError if there was a problem accessing the database
|
||||
SynapseError if there was a problem with the request
|
||||
LoginError if there was an authentication problem.
|
||||
"""
|
||||
login_type = login_submission.get("type")
|
||||
if not isinstance(login_type, str):
|
||||
raise SynapseError(400, "Bad parameter: type", Codes.INVALID_PARAM)
|
||||
|
||||
# ideally, we wouldn't be checking the identifier unless we know we have a login
|
||||
# method which uses it (https://github.com/matrix-org/synapse/issues/8836)
|
||||
#
|
||||
# But the auth providers' check_auth interface requires a username, so in
|
||||
# practice we can only support login methods which we can map to a username
|
||||
# anyway.
|
||||
|
||||
# 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:
|
||||
raise SynapseError(400, "Password login has been disabled.")
|
||||
if not isinstance(password, str):
|
||||
raise SynapseError(400, "Bad parameter: password", Codes.INVALID_PARAM)
|
||||
|
||||
# map old-school login fields into new-school "identifier" fields.
|
||||
identifier_dict = convert_client_dict_legacy_fields_to_identifier(
|
||||
login_submission
|
||||
)
|
||||
|
||||
# convert phone type identifiers to generic threepids
|
||||
if identifier_dict["type"] == "m.id.phone":
|
||||
identifier_dict = login_id_phone_to_thirdparty(identifier_dict)
|
||||
|
||||
# convert threepid identifiers to user IDs
|
||||
if identifier_dict["type"] == "m.id.thirdparty":
|
||||
address = identifier_dict.get("address")
|
||||
medium = identifier_dict.get("medium")
|
||||
|
||||
if medium is None or address is None:
|
||||
raise SynapseError(400, "Invalid thirdparty identifier")
|
||||
|
||||
# For emails, canonicalise the address.
|
||||
# We store all email addresses canonicalised in the DB.
|
||||
# (See add_threepid in synapse/handlers/auth.py)
|
||||
if medium == "email":
|
||||
try:
|
||||
address = canonicalise_email(address)
|
||||
except ValueError as e:
|
||||
raise SynapseError(400, str(e))
|
||||
|
||||
# We also apply account rate limiting using the 3PID as a key, as
|
||||
# otherwise using 3PID bypasses the ratelimiting based on user ID.
|
||||
if ratelimit:
|
||||
self._failed_login_attempts_ratelimiter.ratelimit(
|
||||
(medium, address), update=False
|
||||
)
|
||||
|
||||
# Check for login providers that support 3pid login types
|
||||
if login_type == LoginType.PASSWORD:
|
||||
# we've already checked that there is a (valid) password field
|
||||
assert isinstance(password, str)
|
||||
(
|
||||
canonical_user_id,
|
||||
callback_3pid,
|
||||
) = await self.check_password_provider_3pid(medium, address, password)
|
||||
if canonical_user_id:
|
||||
# Authentication through password provider and 3pid succeeded
|
||||
return canonical_user_id, callback_3pid
|
||||
|
||||
# No password providers were able to handle this 3pid
|
||||
# Check local store
|
||||
user_id = await self.hs.get_datastore().get_user_id_by_threepid(
|
||||
medium, address
|
||||
)
|
||||
if not user_id:
|
||||
logger.warning(
|
||||
"unknown 3pid identifier medium %s, address %r", medium, address
|
||||
)
|
||||
# We mark that we've failed to log in here, as
|
||||
# `check_password_provider_3pid` might have returned `None` due
|
||||
# to an incorrect password, rather than the account not
|
||||
# existing.
|
||||
#
|
||||
# If it returned None but the 3PID was bound then we won't hit
|
||||
# this code path, which is fine as then the per-user ratelimit
|
||||
# will kick in below.
|
||||
if ratelimit:
|
||||
self._failed_login_attempts_ratelimiter.can_do_action(
|
||||
(medium, address)
|
||||
)
|
||||
raise LoginError(403, "", errcode=Codes.FORBIDDEN)
|
||||
|
||||
identifier_dict = {"type": "m.id.user", "user": user_id}
|
||||
|
||||
# by this point, the identifier should be an m.id.user: if it's anything
|
||||
# else, we haven't understood it.
|
||||
if identifier_dict["type"] != "m.id.user":
|
||||
raise SynapseError(400, "Unknown login identifier type")
|
||||
|
||||
username = identifier_dict.get("user")
|
||||
if not username:
|
||||
raise SynapseError(400, "User identifier is missing 'user' key")
|
||||
|
||||
if username.startswith("@"):
|
||||
qualified_user_id = username
|
||||
else:
|
||||
qualified_user_id = UserID(username, self.hs.hostname).to_string()
|
||||
|
||||
# Check if we've hit the failed ratelimit (but don't update it)
|
||||
if ratelimit:
|
||||
self._failed_login_attempts_ratelimiter.ratelimit(
|
||||
qualified_user_id.lower(), update=False
|
||||
)
|
||||
|
||||
try:
|
||||
return await self._validate_userid_login(username, login_submission)
|
||||
except LoginError:
|
||||
# The user has failed to log in, so we need to update the rate
|
||||
# limiter. Using `can_do_action` avoids us raising a ratelimit
|
||||
# exception and masking the LoginError. The actual ratelimiting
|
||||
# should have happened above.
|
||||
if ratelimit:
|
||||
self._failed_login_attempts_ratelimiter.can_do_action(
|
||||
qualified_user_id.lower()
|
||||
)
|
||||
raise
|
||||
|
||||
async def _validate_userid_login(
|
||||
self, username: str, login_submission: Dict[str, Any],
|
||||
) -> Tuple[str, Optional[Callable[[Dict[str, str]], None]]]:
|
||||
"""Helper for validate_login
|
||||
|
||||
Handles login, once we've mapped 3pids onto userids
|
||||
|
||||
Args:
|
||||
username: the username, from the identifier dict
|
||||
login_submission: the whole of the login submission
|
||||
(including 'type' and other relevant fields)
|
||||
Returns:
|
||||
|
@ -843,38 +991,18 @@ class AuthHandler(BaseHandler):
|
|||
SynapseError if there was a problem with the request
|
||||
LoginError if there was an authentication problem.
|
||||
"""
|
||||
|
||||
if username.startswith("@"):
|
||||
qualified_user_id = username
|
||||
else:
|
||||
qualified_user_id = UserID(username, self.hs.hostname).to_string()
|
||||
|
||||
login_type = login_submission.get("type")
|
||||
# we already checked that we have a valid login type
|
||||
assert isinstance(login_type, str)
|
||||
|
||||
known_login_type = False
|
||||
|
||||
# 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:
|
||||
raise SynapseError(400, "Password login has been disabled.")
|
||||
if not password:
|
||||
raise SynapseError(400, "Missing parameter: password")
|
||||
|
||||
for provider in self.password_providers:
|
||||
if hasattr(provider, "check_password") and login_type == LoginType.PASSWORD:
|
||||
known_login_type = True
|
||||
is_valid = await provider.check_password(qualified_user_id, password)
|
||||
if is_valid:
|
||||
return qualified_user_id, None
|
||||
|
||||
if not hasattr(provider, "get_supported_login_types") or not hasattr(
|
||||
provider, "check_auth"
|
||||
):
|
||||
# this password provider doesn't understand custom login types
|
||||
continue
|
||||
|
||||
supported_login_types = provider.get_supported_login_types()
|
||||
if login_type not in supported_login_types:
|
||||
# this password provider doesn't understand this login type
|
||||
|
@ -899,15 +1027,17 @@ class AuthHandler(BaseHandler):
|
|||
|
||||
result = await provider.check_auth(username, login_type, login_dict)
|
||||
if result:
|
||||
if isinstance(result, str):
|
||||
result = (result, None)
|
||||
return result
|
||||
|
||||
if login_type == LoginType.PASSWORD and self.hs.config.password_localdb_enabled:
|
||||
known_login_type = True
|
||||
|
||||
# we've already checked that there is a (valid) password field
|
||||
password = login_submission["password"]
|
||||
assert isinstance(password, str)
|
||||
|
||||
canonical_user_id = await self._check_local_password(
|
||||
qualified_user_id, password # type: ignore
|
||||
qualified_user_id, password
|
||||
)
|
||||
|
||||
if canonical_user_id:
|
||||
|
@ -938,19 +1068,9 @@ class AuthHandler(BaseHandler):
|
|||
unsuccessful, `user_id` and `callback` are both `None`.
|
||||
"""
|
||||
for provider in self.password_providers:
|
||||
if hasattr(provider, "check_3pid_auth"):
|
||||
# This function is able to return a deferred that either
|
||||
# resolves None, meaning authentication failure, or upon
|
||||
# success, to a str (which is the user_id) or a tuple of
|
||||
# (user_id, callback_func), where callback_func should be run
|
||||
# after we've finished everything else
|
||||
result = await provider.check_3pid_auth(medium, address, password)
|
||||
if result:
|
||||
# Check if the return value is a str or a tuple
|
||||
if isinstance(result, str):
|
||||
# If it's a str, set callback function to None
|
||||
result = (result, None)
|
||||
return result
|
||||
result = await provider.check_3pid_auth(medium, address, password)
|
||||
if result:
|
||||
return result
|
||||
|
||||
return None, None
|
||||
|
||||
|
@ -1008,16 +1128,11 @@ class AuthHandler(BaseHandler):
|
|||
|
||||
# see if any of our auth providers want to know about this
|
||||
for provider in self.password_providers:
|
||||
if hasattr(provider, "on_logged_out"):
|
||||
# This might return an awaitable, if it does block the log out
|
||||
# until it completes.
|
||||
result = provider.on_logged_out(
|
||||
user_id=user_info.user_id,
|
||||
device_id=user_info.device_id,
|
||||
access_token=access_token,
|
||||
)
|
||||
if inspect.isawaitable(result):
|
||||
await result
|
||||
await provider.on_logged_out(
|
||||
user_id=user_info.user_id,
|
||||
device_id=user_info.device_id,
|
||||
access_token=access_token,
|
||||
)
|
||||
|
||||
# delete pushers associated with this access token
|
||||
if user_info.token_id is not None:
|
||||
|
@ -1046,11 +1161,10 @@ class AuthHandler(BaseHandler):
|
|||
|
||||
# see if any of our auth providers want to know about this
|
||||
for provider in self.password_providers:
|
||||
if hasattr(provider, "on_logged_out"):
|
||||
for token, token_id, device_id in tokens_and_devices:
|
||||
await provider.on_logged_out(
|
||||
user_id=user_id, device_id=device_id, access_token=token
|
||||
)
|
||||
for token, token_id, device_id in tokens_and_devices:
|
||||
await provider.on_logged_out(
|
||||
user_id=user_id, device_id=device_id, access_token=token
|
||||