Merge remote-tracking branch 'origin/develop' into matrix-org-hotfixes
commit
21cb804023
|
@ -340,9 +340,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.97"
|
version = "1.0.99"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bdf3bf93142acad5821c99197022e170842cdbc1c30482b98750c688c640842a"
|
checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Fix sqlite `user_filters` upgrade introduced in v1.86.0.
|
|
@ -0,0 +1 @@
|
||||||
|
Add spam checker module API for logins.
|
|
@ -348,6 +348,42 @@ callback returns `False`, Synapse falls through to the next one. The value of th
|
||||||
callback that does not return `False` will be used. If this happens, Synapse will not call
|
callback that does not return `False` will be used. If this happens, Synapse will not call
|
||||||
any of the subsequent implementations of this callback.
|
any of the subsequent implementations of this callback.
|
||||||
|
|
||||||
|
|
||||||
|
### `check_login_for_spam`
|
||||||
|
|
||||||
|
_First introduced in Synapse v1.87.0_
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def check_login_for_spam(
|
||||||
|
user_id: str,
|
||||||
|
device_id: Optional[str],
|
||||||
|
initial_display_name: Optional[str],
|
||||||
|
request_info: Collection[Tuple[Optional[str], str]],
|
||||||
|
auth_provider_id: Optional[str] = None,
|
||||||
|
) -> Union["synapse.module_api.NOT_SPAM", "synapse.module_api.errors.Codes"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Called when a user logs in.
|
||||||
|
|
||||||
|
The arguments passed to this callback are:
|
||||||
|
|
||||||
|
* `user_id`: The user ID the user is logging in with
|
||||||
|
* `device_id`: The device ID the user is re-logging into.
|
||||||
|
* `initial_display_name`: The device display name, if any.
|
||||||
|
* `request_info`: A collection of tuples, which first item is a user agent, and which
|
||||||
|
second item is an IP address. These user agents and IP addresses are the ones that were
|
||||||
|
used during the login process.
|
||||||
|
* `auth_provider_id`: The identifier of the SSO authentication provider, if any.
|
||||||
|
|
||||||
|
If multiple modules implement this callback, they will be considered in order. If a
|
||||||
|
callback returns `synapse.module_api.NOT_SPAM`, Synapse falls through to the next one.
|
||||||
|
The value of the first callback that does not return `synapse.module_api.NOT_SPAM` will
|
||||||
|
be used. If this happens, Synapse will not call any of the subsequent implementations of
|
||||||
|
this callback.
|
||||||
|
|
||||||
|
*Note:* This will not be called when a user registers.
|
||||||
|
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
The example below is a module that implements the spam checker callback
|
The example below is a module that implements the spam checker callback
|
||||||
|
|
|
@ -2245,28 +2245,28 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.0.272"
|
version = "0.0.275"
|
||||||
description = "An extremely fast Python linter, written in Rust."
|
description = "An extremely fast Python linter, written in Rust."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "ruff-0.0.272-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:ae9b57546e118660175d45d264b87e9b4c19405c75b587b6e4d21e6a17bf4fdf"},
|
{file = "ruff-0.0.275-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:5e6554a072e7ce81eb6f0bec1cebd3dcb0e358652c0f4900d7d630d61691e914"},
|
||||||
{file = "ruff-0.0.272-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:1609b864a8d7ee75a8c07578bdea0a7db75a144404e75ef3162e0042bfdc100d"},
|
{file = "ruff-0.0.275-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:1cc599022fe5ffb143a965b8d659eb64161ab8ab4433d208777eab018a1aab67"},
|
||||||
{file = "ruff-0.0.272-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee76b4f05fcfff37bd6ac209d1370520d509ea70b5a637bdf0a04d0c99e13dff"},
|
{file = "ruff-0.0.275-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5206fc1cd8c1c1deadd2e6360c0dbcd690f1c845da588ca9d32e4a764a402c60"},
|
||||||
{file = "ruff-0.0.272-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:48eccf225615e106341a641f826b15224b8a4240b84269ead62f0afd6d7e2d95"},
|
{file = "ruff-0.0.275-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c4e6468da26f77b90cae35319d310999f471a8c352998e9b39937a23750149e"},
|
||||||
{file = "ruff-0.0.272-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:677284430ac539bb23421a2b431b4ebc588097ef3ef918d0e0a8d8ed31fea216"},
|
{file = "ruff-0.0.275-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0dbdea02942131dbc15dd45f431d152224f15e1dd1859fcd0c0487b658f60f1a"},
|
||||||
{file = "ruff-0.0.272-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9c4bfb75456a8e1efe14c52fcefb89cfb8f2a0d31ed8d804b82c6cf2dc29c42c"},
|
{file = "ruff-0.0.275-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:22efd9f41af27ef8fb9779462c46c35c89134d33e326c889971e10b2eaf50c63"},
|
||||||
{file = "ruff-0.0.272-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86bc788245361a8148ff98667da938a01e1606b28a45e50ac977b09d3ad2c538"},
|
{file = "ruff-0.0.275-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c09662112cfa22d7467a19252a546291fd0eae4f423e52b75a7a2000a1894db"},
|
||||||
{file = "ruff-0.0.272-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27b2ea68d2aa69fff1b20b67636b1e3e22a6a39e476c880da1282c3e4bf6ee5a"},
|
{file = "ruff-0.0.275-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80043726662144876a381efaab88841c88e8df8baa69559f96b22d4fa216bef1"},
|
||||||
{file = "ruff-0.0.272-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd2bbe337a3f84958f796c77820d55ac2db1e6753f39d1d1baed44e07f13f96d"},
|
{file = "ruff-0.0.275-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5859ee543b01b7eb67835dfd505faa8bb7cc1550f0295c92c1401b45b42be399"},
|
||||||
{file = "ruff-0.0.272-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d5a208f8ef0e51d4746930589f54f9f92f84bb69a7d15b1de34ce80a7681bc00"},
|
{file = "ruff-0.0.275-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c8ace4d40a57b5ea3c16555f25a6b16bc5d8b2779ae1912ce2633543d4e9b1da"},
|
||||||
{file = "ruff-0.0.272-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:905ff8f3d6206ad56fcd70674453527b9011c8b0dc73ead27618426feff6908e"},
|
{file = "ruff-0.0.275-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8347fc16aa185aae275906c4ac5b770e00c896b6a0acd5ba521f158801911998"},
|
||||||
{file = "ruff-0.0.272-py3-none-musllinux_1_2_i686.whl", hash = "sha256:19643d448f76b1eb8a764719072e9c885968971bfba872e14e7257e08bc2f2b7"},
|
{file = "ruff-0.0.275-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ec43658c64bfda44fd84bbea9da8c7a3b34f65448192d1c4dd63e9f4e7abfdd4"},
|
||||||
{file = "ruff-0.0.272-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:691d72a00a99707a4e0b2846690961157aef7b17b6b884f6b4420a9f25cd39b5"},
|
{file = "ruff-0.0.275-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:508b13f7ca37274cceaba4fb3ea5da6ca192356323d92acf39462337c33ad14e"},
|
||||||
{file = "ruff-0.0.272-py3-none-win32.whl", hash = "sha256:dc406e5d756d932da95f3af082814d2467943631a587339ee65e5a4f4fbe83eb"},
|
{file = "ruff-0.0.275-py3-none-win32.whl", hash = "sha256:6afb1c4422f24f361e877937e2a44b3f8176774a476f5e33845ebfe887dd5ec2"},
|
||||||
{file = "ruff-0.0.272-py3-none-win_amd64.whl", hash = "sha256:a37ec80e238ead2969b746d7d1b6b0d31aa799498e9ba4281ab505b93e1f4b28"},
|
{file = "ruff-0.0.275-py3-none-win_amd64.whl", hash = "sha256:d9b264d78621bf7b698b6755d4913ab52c19bd28bee1a16001f954d64c1a1220"},
|
||||||
{file = "ruff-0.0.272-py3-none-win_arm64.whl", hash = "sha256:06b8ee4eb8711ab119db51028dd9f5384b44728c23586424fd6e241a5b9c4a3b"},
|
{file = "ruff-0.0.275-py3-none-win_arm64.whl", hash = "sha256:a19ce3bea71023eee5f0f089dde4a4272d088d5ac0b675867e074983238ccc65"},
|
||||||
{file = "ruff-0.0.272.tar.gz", hash = "sha256:273a01dc8c3c4fd4c2af7ea7a67c8d39bb09bce466e640dd170034da75d14cab"},
|
{file = "ruff-0.0.275.tar.gz", hash = "sha256:a63a0b645da699ae5c758fce19188e901b3033ec54d862d93fcd042addf7f38d"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2711,21 +2711,21 @@ files = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "towncrier"
|
name = "towncrier"
|
||||||
version = "22.12.0"
|
version = "23.6.0"
|
||||||
description = "Building newsfiles for your project."
|
description = "Building newsfiles for your project."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "towncrier-22.12.0-py3-none-any.whl", hash = "sha256:9767a899a4d6856950f3598acd9e8f08da2663c49fdcda5ea0f9e6ba2afc8eea"},
|
{file = "towncrier-23.6.0-py3-none-any.whl", hash = "sha256:da552f29192b3c2b04d630133f194c98e9f14f0558669d427708e203fea4d0a5"},
|
||||||
{file = "towncrier-22.12.0.tar.gz", hash = "sha256:9c49d7e75f646a9aea02ae904c0bc1639c8fd14a01292d2b123b8d307564034d"},
|
{file = "towncrier-23.6.0.tar.gz", hash = "sha256:fc29bd5ab4727c8dacfbe636f7fb5dc53b99805b62da1c96b214836159ff70c1"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
click = "*"
|
click = "*"
|
||||||
click-default-group = "*"
|
click-default-group = "*"
|
||||||
|
importlib-resources = {version = ">=5", markers = "python_version < \"3.10\""}
|
||||||
incremental = "*"
|
incremental = "*"
|
||||||
jinja2 = "*"
|
jinja2 = "*"
|
||||||
setuptools = "*"
|
|
||||||
tomli = {version = "*", markers = "python_version < \"3.11\""}
|
tomli = {version = "*", markers = "python_version < \"3.11\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
|
@ -2931,13 +2931,13 @@ files = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "types-opentracing"
|
name = "types-opentracing"
|
||||||
version = "2.4.10.4"
|
version = "2.4.10.5"
|
||||||
description = "Typing stubs for opentracing"
|
description = "Typing stubs for opentracing"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
files = [
|
files = [
|
||||||
{file = "types-opentracing-2.4.10.4.tar.gz", hash = "sha256:347040c9da4ada7d3c795659912c95d98c5651e242e8eaa0344815fee5bb97e2"},
|
{file = "types-opentracing-2.4.10.5.tar.gz", hash = "sha256:852d13ab1324832835d50c00cfd58b9267f0e79ec3189e5664c2a90c26880fd4"},
|
||||||
{file = "types_opentracing-2.4.10.4-py3-none-any.whl", hash = "sha256:73c9b958eea3df6c4906ebf3865608a562dd9981c1bbc75a373a583c613bed56"},
|
{file = "types_opentracing-2.4.10.5-py3-none-any.whl", hash = "sha256:8f12ab4dce3e298a8e6655da9a6d52171e7a275357eae4cec22a1663d94023a7"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -3003,13 +3003,13 @@ types-urllib3 = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "types-setuptools"
|
name = "types-setuptools"
|
||||||
version = "67.8.0.0"
|
version = "68.0.0.0"
|
||||||
description = "Typing stubs for setuptools"
|
description = "Typing stubs for setuptools"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
files = [
|
files = [
|
||||||
{file = "types-setuptools-67.8.0.0.tar.gz", hash = "sha256:95c9ed61871d6c0e258433373a4e1753c0a7c3627a46f4d4058c7b5a08ab844f"},
|
{file = "types-setuptools-68.0.0.0.tar.gz", hash = "sha256:fc958b4123b155ffc069a66d3af5fe6c1f9d0600c35c0c8444b2ab4147112641"},
|
||||||
{file = "types_setuptools-67.8.0.0-py3-none-any.whl", hash = "sha256:6df73340d96b238a4188b7b7668814b37e8018168aef1eef94a3b1872e3f60ff"},
|
{file = "types_setuptools-68.0.0.0-py3-none-any.whl", hash = "sha256:cc00e09ba8f535362cbe1ea8b8407d15d14b59c57f4190cceaf61a9e57616446"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -3294,4 +3294,4 @@ user-search = ["pyicu"]
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.7.1"
|
python-versions = "^3.7.1"
|
||||||
content-hash = "090924370b17fd265407b5a3f9cbc00997308f575b455399b39a48e3ca1a5a8e"
|
content-hash = "7f31754a1009d7b6c9a1bd7221a0b243ffd510f362c28f0da417aaac16757a87"
|
||||||
|
|
|
@ -311,7 +311,7 @@ all = [
|
||||||
# We pin black so that our tests don't start failing on new releases.
|
# We pin black so that our tests don't start failing on new releases.
|
||||||
isort = ">=5.10.1"
|
isort = ">=5.10.1"
|
||||||
black = ">=22.3.0"
|
black = ">=22.3.0"
|
||||||
ruff = "0.0.272"
|
ruff = "0.0.275"
|
||||||
|
|
||||||
# Typechecking
|
# Typechecking
|
||||||
lxml-stubs = ">=0.4.0"
|
lxml-stubs = ">=0.4.0"
|
||||||
|
|
|
@ -521,6 +521,11 @@ class SynapseRequest(Request):
|
||||||
else:
|
else:
|
||||||
return self.getClientAddress().host
|
return self.getClientAddress().host
|
||||||
|
|
||||||
|
def request_info(self) -> "RequestInfo":
|
||||||
|
h = self.getHeader(b"User-Agent")
|
||||||
|
user_agent = h.decode("ascii", "replace") if h else None
|
||||||
|
return RequestInfo(user_agent=user_agent, ip=self.get_client_ip_if_available())
|
||||||
|
|
||||||
|
|
||||||
class XForwardedForRequest(SynapseRequest):
|
class XForwardedForRequest(SynapseRequest):
|
||||||
"""Request object which honours proxy headers
|
"""Request object which honours proxy headers
|
||||||
|
@ -661,3 +666,9 @@ class SynapseSite(Site):
|
||||||
|
|
||||||
def log(self, request: SynapseRequest) -> None:
|
def log(self, request: SynapseRequest) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(auto_attribs=True, frozen=True, slots=True)
|
||||||
|
class RequestInfo:
|
||||||
|
user_agent: Optional[str]
|
||||||
|
ip: str
|
||||||
|
|
|
@ -80,6 +80,7 @@ from synapse.module_api.callbacks.account_validity_callbacks import (
|
||||||
)
|
)
|
||||||
from synapse.module_api.callbacks.spamchecker_callbacks import (
|
from synapse.module_api.callbacks.spamchecker_callbacks import (
|
||||||
CHECK_EVENT_FOR_SPAM_CALLBACK,
|
CHECK_EVENT_FOR_SPAM_CALLBACK,
|
||||||
|
CHECK_LOGIN_FOR_SPAM_CALLBACK,
|
||||||
CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK,
|
CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK,
|
||||||
CHECK_REGISTRATION_FOR_SPAM_CALLBACK,
|
CHECK_REGISTRATION_FOR_SPAM_CALLBACK,
|
||||||
CHECK_USERNAME_FOR_SPAM_CALLBACK,
|
CHECK_USERNAME_FOR_SPAM_CALLBACK,
|
||||||
|
@ -302,6 +303,7 @@ class ModuleApi:
|
||||||
CHECK_REGISTRATION_FOR_SPAM_CALLBACK
|
CHECK_REGISTRATION_FOR_SPAM_CALLBACK
|
||||||
] = None,
|
] = None,
|
||||||
check_media_file_for_spam: Optional[CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK] = None,
|
check_media_file_for_spam: Optional[CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK] = None,
|
||||||
|
check_login_for_spam: Optional[CHECK_LOGIN_FOR_SPAM_CALLBACK] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Registers callbacks for spam checking capabilities.
|
"""Registers callbacks for spam checking capabilities.
|
||||||
|
|
||||||
|
@ -319,6 +321,7 @@ class ModuleApi:
|
||||||
check_username_for_spam=check_username_for_spam,
|
check_username_for_spam=check_username_for_spam,
|
||||||
check_registration_for_spam=check_registration_for_spam,
|
check_registration_for_spam=check_registration_for_spam,
|
||||||
check_media_file_for_spam=check_media_file_for_spam,
|
check_media_file_for_spam=check_media_file_for_spam,
|
||||||
|
check_login_for_spam=check_login_for_spam,
|
||||||
)
|
)
|
||||||
|
|
||||||
def register_account_validity_callbacks(
|
def register_account_validity_callbacks(
|
||||||
|
|
|
@ -196,6 +196,26 @@ CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK = Callable[
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
|
CHECK_LOGIN_FOR_SPAM_CALLBACK = Callable[
|
||||||
|
[
|
||||||
|
str,
|
||||||
|
Optional[str],
|
||||||
|
Optional[str],
|
||||||
|
Collection[Tuple[Optional[str], str]],
|
||||||
|
Optional[str],
|
||||||
|
],
|
||||||
|
Awaitable[
|
||||||
|
Union[
|
||||||
|
Literal["NOT_SPAM"],
|
||||||
|
Codes,
|
||||||
|
# Highly experimental, not officially part of the spamchecker API, may
|
||||||
|
# disappear without warning depending on the results of ongoing
|
||||||
|
# experiments.
|
||||||
|
# Use this to return additional information as part of an error.
|
||||||
|
Tuple[Codes, JsonDict],
|
||||||
|
]
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def load_legacy_spam_checkers(hs: "synapse.server.HomeServer") -> None:
|
def load_legacy_spam_checkers(hs: "synapse.server.HomeServer") -> None:
|
||||||
|
@ -315,6 +335,7 @@ class SpamCheckerModuleApiCallbacks:
|
||||||
self._check_media_file_for_spam_callbacks: List[
|
self._check_media_file_for_spam_callbacks: List[
|
||||||
CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK
|
CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK
|
||||||
] = []
|
] = []
|
||||||
|
self._check_login_for_spam_callbacks: List[CHECK_LOGIN_FOR_SPAM_CALLBACK] = []
|
||||||
|
|
||||||
def register_callbacks(
|
def register_callbacks(
|
||||||
self,
|
self,
|
||||||
|
@ -335,6 +356,7 @@ class SpamCheckerModuleApiCallbacks:
|
||||||
CHECK_REGISTRATION_FOR_SPAM_CALLBACK
|
CHECK_REGISTRATION_FOR_SPAM_CALLBACK
|
||||||
] = None,
|
] = None,
|
||||||
check_media_file_for_spam: Optional[CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK] = None,
|
check_media_file_for_spam: Optional[CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK] = None,
|
||||||
|
check_login_for_spam: Optional[CHECK_LOGIN_FOR_SPAM_CALLBACK] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Register callbacks from module for each hook."""
|
"""Register callbacks from module for each hook."""
|
||||||
if check_event_for_spam is not None:
|
if check_event_for_spam is not None:
|
||||||
|
@ -378,6 +400,9 @@ class SpamCheckerModuleApiCallbacks:
|
||||||
if check_media_file_for_spam is not None:
|
if check_media_file_for_spam is not None:
|
||||||
self._check_media_file_for_spam_callbacks.append(check_media_file_for_spam)
|
self._check_media_file_for_spam_callbacks.append(check_media_file_for_spam)
|
||||||
|
|
||||||
|
if check_login_for_spam is not None:
|
||||||
|
self._check_login_for_spam_callbacks.append(check_login_for_spam)
|
||||||
|
|
||||||
@trace
|
@trace
|
||||||
async def check_event_for_spam(
|
async def check_event_for_spam(
|
||||||
self, event: "synapse.events.EventBase"
|
self, event: "synapse.events.EventBase"
|
||||||
|
@ -819,3 +844,58 @@ class SpamCheckerModuleApiCallbacks:
|
||||||
return synapse.api.errors.Codes.FORBIDDEN, {}
|
return synapse.api.errors.Codes.FORBIDDEN, {}
|
||||||
|
|
||||||
return self.NOT_SPAM
|
return self.NOT_SPAM
|
||||||
|
|
||||||
|
async def check_login_for_spam(
|
||||||
|
self,
|
||||||
|
user_id: str,
|
||||||
|
device_id: Optional[str],
|
||||||
|
initial_display_name: Optional[str],
|
||||||
|
request_info: Collection[Tuple[Optional[str], str]],
|
||||||
|
auth_provider_id: Optional[str] = None,
|
||||||
|
) -> Union[Tuple[Codes, dict], Literal["NOT_SPAM"]]:
|
||||||
|
"""Checks if we should allow the given registration request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The request user ID
|
||||||
|
request_info: List of tuples of user agent and IP that
|
||||||
|
were used during the registration process.
|
||||||
|
auth_provider_id: The SSO IdP the user used, e.g "oidc", "saml",
|
||||||
|
"cas". If any. Note this does not include users registered
|
||||||
|
via a password provider.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Enum for how the request should be handled
|
||||||
|
"""
|
||||||
|
|
||||||
|
for callback in self._check_login_for_spam_callbacks:
|
||||||
|
with Measure(
|
||||||
|
self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
|
||||||
|
):
|
||||||
|
res = await delay_cancellation(
|
||||||
|
callback(
|
||||||
|
user_id,
|
||||||
|
device_id,
|
||||||
|
initial_display_name,
|
||||||
|
request_info,
|
||||||
|
auth_provider_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Normalize return values to `Codes` or `"NOT_SPAM"`.
|
||||||
|
if res is self.NOT_SPAM:
|
||||||
|
continue
|
||||||
|
elif isinstance(res, synapse.api.errors.Codes):
|
||||||
|
return res, {}
|
||||||
|
elif (
|
||||||
|
isinstance(res, tuple)
|
||||||
|
and len(res) == 2
|
||||||
|
and isinstance(res[0], synapse.api.errors.Codes)
|
||||||
|
and isinstance(res[1], dict)
|
||||||
|
):
|
||||||
|
return res
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"Module returned invalid value, rejecting login as spam"
|
||||||
|
)
|
||||||
|
return synapse.api.errors.Codes.FORBIDDEN, {}
|
||||||
|
|
||||||
|
return self.NOT_SPAM
|
||||||
|
|
|
@ -50,7 +50,7 @@ from synapse.http.servlet import (
|
||||||
parse_json_object_from_request,
|
parse_json_object_from_request,
|
||||||
parse_string,
|
parse_string,
|
||||||
)
|
)
|
||||||
from synapse.http.site import SynapseRequest
|
from synapse.http.site import RequestInfo, SynapseRequest
|
||||||
from synapse.rest.client._base import client_patterns
|
from synapse.rest.client._base import client_patterns
|
||||||
from synapse.rest.well_known import WellKnownBuilder
|
from synapse.rest.well_known import WellKnownBuilder
|
||||||
from synapse.types import JsonDict, UserID
|
from synapse.types import JsonDict, UserID
|
||||||
|
@ -114,6 +114,7 @@ class LoginRestServlet(RestServlet):
|
||||||
self.auth_handler = self.hs.get_auth_handler()
|
self.auth_handler = self.hs.get_auth_handler()
|
||||||
self.registration_handler = hs.get_registration_handler()
|
self.registration_handler = hs.get_registration_handler()
|
||||||
self._sso_handler = hs.get_sso_handler()
|
self._sso_handler = hs.get_sso_handler()
|
||||||
|
self._spam_checker = hs.get_module_api_callbacks().spam_checker
|
||||||
|
|
||||||
self._well_known_builder = WellKnownBuilder(hs)
|
self._well_known_builder = WellKnownBuilder(hs)
|
||||||
self._address_ratelimiter = Ratelimiter(
|
self._address_ratelimiter = Ratelimiter(
|
||||||
|
@ -197,6 +198,8 @@ class LoginRestServlet(RestServlet):
|
||||||
self._refresh_tokens_enabled and client_requested_refresh_token
|
self._refresh_tokens_enabled and client_requested_refresh_token
|
||||||
)
|
)
|
||||||
|
|
||||||
|
request_info = request.request_info()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if login_submission["type"] == LoginRestServlet.APPSERVICE_TYPE:
|
if login_submission["type"] == LoginRestServlet.APPSERVICE_TYPE:
|
||||||
requester = await self.auth.get_user_by_req(request)
|
requester = await self.auth.get_user_by_req(request)
|
||||||
|
@ -216,6 +219,7 @@ class LoginRestServlet(RestServlet):
|
||||||
login_submission,
|
login_submission,
|
||||||
appservice,
|
appservice,
|
||||||
should_issue_refresh_token=should_issue_refresh_token,
|
should_issue_refresh_token=should_issue_refresh_token,
|
||||||
|
request_info=request_info,
|
||||||
)
|
)
|
||||||
elif (
|
elif (
|
||||||
self.jwt_enabled
|
self.jwt_enabled
|
||||||
|
@ -227,6 +231,7 @@ class LoginRestServlet(RestServlet):
|
||||||
result = await self._do_jwt_login(
|
result = await self._do_jwt_login(
|
||||||
login_submission,
|
login_submission,
|
||||||
should_issue_refresh_token=should_issue_refresh_token,
|
should_issue_refresh_token=should_issue_refresh_token,
|
||||||
|
request_info=request_info,
|
||||||
)
|
)
|
||||||
elif login_submission["type"] == LoginRestServlet.TOKEN_TYPE:
|
elif login_submission["type"] == LoginRestServlet.TOKEN_TYPE:
|
||||||
await self._address_ratelimiter.ratelimit(
|
await self._address_ratelimiter.ratelimit(
|
||||||
|
@ -235,6 +240,7 @@ class LoginRestServlet(RestServlet):
|
||||||
result = await self._do_token_login(
|
result = await self._do_token_login(
|
||||||
login_submission,
|
login_submission,
|
||||||
should_issue_refresh_token=should_issue_refresh_token,
|
should_issue_refresh_token=should_issue_refresh_token,
|
||||||
|
request_info=request_info,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await self._address_ratelimiter.ratelimit(
|
await self._address_ratelimiter.ratelimit(
|
||||||
|
@ -243,6 +249,7 @@ class LoginRestServlet(RestServlet):
|
||||||
result = await self._do_other_login(
|
result = await self._do_other_login(
|
||||||
login_submission,
|
login_submission,
|
||||||
should_issue_refresh_token=should_issue_refresh_token,
|
should_issue_refresh_token=should_issue_refresh_token,
|
||||||
|
request_info=request_info,
|
||||||
)
|
)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise SynapseError(400, "Missing JSON keys.")
|
raise SynapseError(400, "Missing JSON keys.")
|
||||||
|
@ -265,6 +272,8 @@ class LoginRestServlet(RestServlet):
|
||||||
login_submission: JsonDict,
|
login_submission: JsonDict,
|
||||||
appservice: ApplicationService,
|
appservice: ApplicationService,
|
||||||
should_issue_refresh_token: bool = False,
|
should_issue_refresh_token: bool = False,
|
||||||
|
*,
|
||||||
|
request_info: RequestInfo,
|
||||||
) -> LoginResponse:
|
) -> LoginResponse:
|
||||||
identifier = login_submission.get("identifier")
|
identifier = login_submission.get("identifier")
|
||||||
logger.info("Got appservice login request with identifier: %r", identifier)
|
logger.info("Got appservice login request with identifier: %r", identifier)
|
||||||
|
@ -300,10 +309,15 @@ class LoginRestServlet(RestServlet):
|
||||||
# The user represented by an appservice's configured sender_localpart
|
# The user represented by an appservice's configured sender_localpart
|
||||||
# is not actually created in Synapse.
|
# is not actually created in Synapse.
|
||||||
should_check_deactivated=qualified_user_id != appservice.sender,
|
should_check_deactivated=qualified_user_id != appservice.sender,
|
||||||
|
request_info=request_info,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _do_other_login(
|
async def _do_other_login(
|
||||||
self, login_submission: JsonDict, should_issue_refresh_token: bool = False
|
self,
|
||||||
|
login_submission: JsonDict,
|
||||||
|
should_issue_refresh_token: bool = False,
|
||||||
|
*,
|
||||||
|
request_info: RequestInfo,
|
||||||
) -> LoginResponse:
|
) -> LoginResponse:
|
||||||
"""Handle non-token/saml/jwt logins
|
"""Handle non-token/saml/jwt logins
|
||||||
|
|
||||||
|
@ -333,6 +347,7 @@ class LoginRestServlet(RestServlet):
|
||||||
login_submission,
|
login_submission,
|
||||||
callback,
|
callback,
|
||||||
should_issue_refresh_token=should_issue_refresh_token,
|
should_issue_refresh_token=should_issue_refresh_token,
|
||||||
|
request_info=request_info,
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -347,6 +362,8 @@ class LoginRestServlet(RestServlet):
|
||||||
should_issue_refresh_token: bool = False,
|
should_issue_refresh_token: bool = False,
|
||||||
auth_provider_session_id: Optional[str] = None,
|
auth_provider_session_id: Optional[str] = None,
|
||||||
should_check_deactivated: bool = True,
|
should_check_deactivated: bool = True,
|
||||||
|
*,
|
||||||
|
request_info: RequestInfo,
|
||||||
) -> LoginResponse:
|
) -> LoginResponse:
|
||||||
"""Called when we've successfully authed the user and now need to
|
"""Called when we've successfully authed the user and now need to
|
||||||
actually login them in (e.g. create devices). This gets called on
|
actually login them in (e.g. create devices). This gets called on
|
||||||
|
@ -371,6 +388,7 @@ class LoginRestServlet(RestServlet):
|
||||||
|
|
||||||
This exists purely for appservice's configured sender_localpart
|
This exists purely for appservice's configured sender_localpart
|
||||||
which doesn't have an associated user in the database.
|
which doesn't have an associated user in the database.
|
||||||
|
request_info: The user agent/IP address of the user.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary of account information after successful login.
|
Dictionary of account information after successful login.
|
||||||
|
@ -417,6 +435,22 @@ class LoginRestServlet(RestServlet):
|
||||||
)
|
)
|
||||||
|
|
||||||
initial_display_name = login_submission.get("initial_device_display_name")
|
initial_display_name = login_submission.get("initial_device_display_name")
|
||||||
|
spam_check = await self._spam_checker.check_login_for_spam(
|
||||||
|
user_id,
|
||||||
|
device_id=device_id,
|
||||||
|
initial_display_name=initial_display_name,
|
||||||
|
request_info=[(request_info.user_agent, request_info.ip)],
|
||||||
|
auth_provider_id=auth_provider_id,
|
||||||
|
)
|
||||||
|
if spam_check != self._spam_checker.NOT_SPAM:
|
||||||
|
logger.info("Blocking login due to spam checker")
|
||||||
|
raise SynapseError(
|
||||||
|
403,
|
||||||
|
msg="Login was blocked by the server",
|
||||||
|
errcode=spam_check[0],
|
||||||
|
additional_fields=spam_check[1],
|
||||||
|
)
|
||||||
|
|
||||||
(
|
(
|
||||||
device_id,
|
device_id,
|
||||||
access_token,
|
access_token,
|
||||||
|
@ -451,7 +485,11 @@ class LoginRestServlet(RestServlet):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def _do_token_login(
|
async def _do_token_login(
|
||||||
self, login_submission: JsonDict, should_issue_refresh_token: bool = False
|
self,
|
||||||
|
login_submission: JsonDict,
|
||||||
|
should_issue_refresh_token: bool = False,
|
||||||
|
*,
|
||||||
|
request_info: RequestInfo,
|
||||||
) -> LoginResponse:
|
) -> LoginResponse:
|
||||||
"""
|
"""
|
||||||
Handle token login.
|
Handle token login.
|
||||||
|
@ -474,10 +512,15 @@ class LoginRestServlet(RestServlet):
|
||||||
auth_provider_id=res.auth_provider_id,
|
auth_provider_id=res.auth_provider_id,
|
||||||
should_issue_refresh_token=should_issue_refresh_token,
|
should_issue_refresh_token=should_issue_refresh_token,
|
||||||
auth_provider_session_id=res.auth_provider_session_id,
|
auth_provider_session_id=res.auth_provider_session_id,
|
||||||
|
request_info=request_info,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _do_jwt_login(
|
async def _do_jwt_login(
|
||||||
self, login_submission: JsonDict, should_issue_refresh_token: bool = False
|
self,
|
||||||
|
login_submission: JsonDict,
|
||||||
|
should_issue_refresh_token: bool = False,
|
||||||
|
*,
|
||||||
|
request_info: RequestInfo,
|
||||||
) -> LoginResponse:
|
) -> LoginResponse:
|
||||||
"""
|
"""
|
||||||
Handle the custom JWT login.
|
Handle the custom JWT login.
|
||||||
|
@ -496,6 +539,7 @@ class LoginRestServlet(RestServlet):
|
||||||
login_submission,
|
login_submission,
|
||||||
create_non_existent_users=True,
|
create_non_existent_users=True,
|
||||||
should_issue_refresh_token=should_issue_refresh_token,
|
should_issue_refresh_token=should_issue_refresh_token,
|
||||||
|
request_info=request_info,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -61,9 +61,7 @@ def run_upgrade(
|
||||||
full_user_id text NOT NULL,
|
full_user_id text NOT NULL,
|
||||||
user_id text NOT NULL,
|
user_id text NOT NULL,
|
||||||
filter_id bigint NOT NULL,
|
filter_id bigint NOT NULL,
|
||||||
filter_json bytea NOT NULL,
|
filter_json bytea NOT NULL
|
||||||
UNIQUE (full_user_id),
|
|
||||||
UNIQUE (user_id)
|
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
cur.execute(create_sql)
|
cur.execute(create_sql)
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
# 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.
|
||||||
|
from synapse.config.homeserver import HomeServerConfig
|
||||||
|
from synapse.storage.database import LoggingTransaction
|
||||||
|
from synapse.storage.engines import BaseDatabaseEngine, Sqlite3Engine
|
||||||
|
|
||||||
|
|
||||||
|
def run_update(
|
||||||
|
cur: LoggingTransaction,
|
||||||
|
database_engine: BaseDatabaseEngine,
|
||||||
|
config: HomeServerConfig,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Fix to drop unused indexes caused by incorrectly adding UNIQUE constraint to
|
||||||
|
columns `user_id` and `full_user_id` of table `user_filters` in previous migration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(database_engine, Sqlite3Engine):
|
||||||
|
cur.execute("DROP TABLE IF EXISTS temp_user_filters")
|
||||||
|
create_sql = """
|
||||||
|
CREATE TABLE temp_user_filters (
|
||||||
|
full_user_id text NOT NULL,
|
||||||
|
user_id text NOT NULL,
|
||||||
|
filter_id bigint NOT NULL,
|
||||||
|
filter_json bytea NOT NULL
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
cur.execute(create_sql)
|
||||||
|
|
||||||
|
copy_sql = """
|
||||||
|
INSERT INTO temp_user_filters (
|
||||||
|
user_id,
|
||||||
|
filter_id,
|
||||||
|
filter_json,
|
||||||
|
full_user_id)
|
||||||
|
SELECT user_id, filter_id, filter_json, full_user_id FROM user_filters
|
||||||
|
"""
|
||||||
|
cur.execute(copy_sql)
|
||||||
|
|
||||||
|
drop_sql = """
|
||||||
|
DROP TABLE user_filters
|
||||||
|
"""
|
||||||
|
cur.execute(drop_sql)
|
||||||
|
|
||||||
|
rename_sql = """
|
||||||
|
ALTER TABLE temp_user_filters RENAME to user_filters
|
||||||
|
"""
|
||||||
|
cur.execute(rename_sql)
|
||||||
|
|
||||||
|
index_sql = """
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS user_filters_unique ON
|
||||||
|
user_filters (user_id, filter_id)
|
||||||
|
"""
|
||||||
|
cur.execute(index_sql)
|
|
@ -0,0 +1,25 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from synapse.storage.database import LoggingTransaction
|
||||||
|
from synapse.storage.engines import BaseDatabaseEngine, Sqlite3Engine
|
||||||
|
|
||||||
|
|
||||||
|
def run_create(cur: LoggingTransaction, database_engine: BaseDatabaseEngine) -> None:
|
||||||
|
if isinstance(database_engine, Sqlite3Engine):
|
||||||
|
idx_sql = """
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS user_filters_full_user_id_unique ON
|
||||||
|
user_filters (full_user_id, filter_id)
|
||||||
|
"""
|
||||||
|
cur.execute(idx_sql)
|
|
@ -13,11 +13,12 @@
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
import time
|
import time
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Collection, Dict, List, Optional, Tuple, Union
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
import pymacaroons
|
import pymacaroons
|
||||||
|
from typing_extensions import Literal
|
||||||
|
|
||||||
from twisted.test.proto_helpers import MemoryReactor
|
from twisted.test.proto_helpers import MemoryReactor
|
||||||
from twisted.web.resource import Resource
|
from twisted.web.resource import Resource
|
||||||
|
@ -26,11 +27,12 @@ import synapse.rest.admin
|
||||||
from synapse.api.constants import ApprovalNoticeMedium, LoginType
|
from synapse.api.constants import ApprovalNoticeMedium, LoginType
|
||||||
from synapse.api.errors import Codes
|
from synapse.api.errors import Codes
|
||||||
from synapse.appservice import ApplicationService
|
from synapse.appservice import ApplicationService
|
||||||
|
from synapse.module_api import ModuleApi
|
||||||
from synapse.rest.client import devices, login, logout, register
|
from synapse.rest.client import devices, login, logout, register
|
||||||
from synapse.rest.client.account import WhoamiRestServlet
|
from synapse.rest.client.account import WhoamiRestServlet
|
||||||
from synapse.rest.synapse.client import build_synapse_client_resource_tree
|
from synapse.rest.synapse.client import build_synapse_client_resource_tree
|
||||||
from synapse.server import HomeServer
|
from synapse.server import HomeServer
|
||||||
from synapse.types import create_requester
|
from synapse.types import JsonDict, create_requester
|
||||||
from synapse.util import Clock
|
from synapse.util import Clock
|
||||||
|
|
||||||
from tests import unittest
|
from tests import unittest
|
||||||
|
@ -88,6 +90,56 @@ ADDITIONAL_LOGIN_FLOWS = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class TestSpamChecker:
|
||||||
|
def __init__(self, config: None, api: ModuleApi):
|
||||||
|
api.register_spam_checker_callbacks(
|
||||||
|
check_login_for_spam=self.check_login_for_spam,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_config(config: JsonDict) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def check_login_for_spam(
|
||||||
|
self,
|
||||||
|
user_id: str,
|
||||||
|
device_id: Optional[str],
|
||||||
|
initial_display_name: Optional[str],
|
||||||
|
request_info: Collection[Tuple[Optional[str], str]],
|
||||||
|
auth_provider_id: Optional[str] = None,
|
||||||
|
) -> Union[
|
||||||
|
Literal["NOT_SPAM"],
|
||||||
|
Tuple["synapse.module_api.errors.Codes", JsonDict],
|
||||||
|
]:
|
||||||
|
return "NOT_SPAM"
|
||||||
|
|
||||||
|
|
||||||
|
class DenyAllSpamChecker:
|
||||||
|
def __init__(self, config: None, api: ModuleApi):
|
||||||
|
api.register_spam_checker_callbacks(
|
||||||
|
check_login_for_spam=self.check_login_for_spam,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_config(config: JsonDict) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def check_login_for_spam(
|
||||||
|
self,
|
||||||
|
user_id: str,
|
||||||
|
device_id: Optional[str],
|
||||||
|
initial_display_name: Optional[str],
|
||||||
|
request_info: Collection[Tuple[Optional[str], str]],
|
||||||
|
auth_provider_id: Optional[str] = None,
|
||||||
|
) -> Union[
|
||||||
|
Literal["NOT_SPAM"],
|
||||||
|
Tuple["synapse.module_api.errors.Codes", JsonDict],
|
||||||
|
]:
|
||||||
|
# Return an odd set of values to ensure that they get correctly passed
|
||||||
|
# to the client.
|
||||||
|
return Codes.LIMIT_EXCEEDED, {"extra": "value"}
|
||||||
|
|
||||||
|
|
||||||
class LoginRestServletTestCase(unittest.HomeserverTestCase):
|
class LoginRestServletTestCase(unittest.HomeserverTestCase):
|
||||||
servlets = [
|
servlets = [
|
||||||
synapse.rest.admin.register_servlets_for_client_rest_resource,
|
synapse.rest.admin.register_servlets_for_client_rest_resource,
|
||||||
|
@ -469,6 +521,58 @@ class LoginRestServletTestCase(unittest.HomeserverTestCase):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@override_config(
|
||||||
|
{
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"module": TestSpamChecker.__module__
|
||||||
|
+ "."
|
||||||
|
+ TestSpamChecker.__qualname__
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def test_spam_checker_allow(self) -> None:
|
||||||
|
"""Check that that adding a spam checker doesn't break login."""
|
||||||
|
self.register_user("kermit", "monkey")
|
||||||
|
|
||||||
|
body = {"type": "m.login.password", "user": "kermit", "password": "monkey"}
|
||||||
|
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
"/_matrix/client/r0/login",
|
||||||
|
body,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.result)
|
||||||
|
|
||||||
|
@override_config(
|
||||||
|
{
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"module": DenyAllSpamChecker.__module__
|
||||||
|
+ "."
|
||||||
|
+ DenyAllSpamChecker.__qualname__
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def test_spam_checker_deny(self) -> None:
|
||||||
|
"""Check that login"""
|
||||||
|
|
||||||
|
self.register_user("kermit", "monkey")
|
||||||
|
|
||||||
|
body = {"type": "m.login.password", "user": "kermit", "password": "monkey"}
|
||||||
|
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
"/_matrix/client/r0/login",
|
||||||
|
body,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 403, channel.result)
|
||||||
|
self.assertDictContainsSubset(
|
||||||
|
{"errcode": Codes.LIMIT_EXCEEDED, "extra": "value"}, channel.json_body
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@skip_unless(has_saml2 and HAS_OIDC, "Requires SAML2 and OIDC")
|
@skip_unless(has_saml2 and HAS_OIDC, "Requires SAML2 and OIDC")
|
||||||
class MultiSSOTestCase(unittest.HomeserverTestCase):
|
class MultiSSOTestCase(unittest.HomeserverTestCase):
|
||||||
|
|
Loading…
Reference in New Issue