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

Commit 4507959

Browse files
committed
Merge commit '453dfe210' into anoa/dinsic_release_1_21_x
* commit '453dfe210': blacklist MSC2753 sytests until it's implemented in synapse (#8285) Don't remember `enabled` of deleted push rules and properly return 404 for missing push rules in `.../actions` and `.../enabled` (#7796)
2 parents 82c379c + 453dfe2 commit 4507959

File tree

8 files changed

+632
-69
lines changed

8 files changed

+632
-69
lines changed

changelog.d/7796.bugfix

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix inconsistent handling of non-existent push rules, and stop tracking the `enabled` state of removed push rules.

changelog.d/8285.misc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Blacklist [MSC2753](https://github.com/matrix-org/matrix-doc/pull/2753) SyTests until it is implemented.

synapse/rest/client/v1/push_rule.py

+13-2
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,18 @@ def notify_user(self, user_id):
163163
self.notifier.on_new_event("push_rules_key", stream_id, users=[user_id])
164164

165165
async def set_rule_attr(self, user_id, spec, val):
166+
if spec["attr"] not in ("enabled", "actions"):
167+
# for the sake of potential future expansion, shouldn't report
168+
# 404 in the case of an unknown request so check it corresponds to
169+
# a known attribute first.
170+
raise UnrecognizedRequestError()
171+
172+
namespaced_rule_id = _namespaced_rule_id_from_spec(spec)
173+
rule_id = spec["rule_id"]
174+
is_default_rule = rule_id.startswith(".")
175+
if is_default_rule:
176+
if namespaced_rule_id not in BASE_RULE_IDS:
177+
raise NotFoundError("Unknown rule %s" % (namespaced_rule_id,))
166178
if spec["attr"] == "enabled":
167179
if isinstance(val, dict) and "enabled" in val:
168180
val = val["enabled"]
@@ -171,9 +183,8 @@ async def set_rule_attr(self, user_id, spec, val):
171183
# This should *actually* take a dict, but many clients pass
172184
# bools directly, so let's not break them.
173185
raise SynapseError(400, "Value for 'enabled' must be boolean")
174-
namespaced_rule_id = _namespaced_rule_id_from_spec(spec)
175186
return await self.store.set_push_rule_enabled(
176-
user_id, namespaced_rule_id, val
187+
user_id, namespaced_rule_id, val, is_default_rule
177188
)
178189
elif spec["attr"] == "actions":
179190
actions = val.get("actions")

synapse/rest/client/v2_alpha/account.py

+13-56
Original file line numberDiff line numberDiff line change
@@ -362,20 +362,19 @@ async def on_POST(self, request):
362362
shadow_user = UserID(
363363
requester.user.localpart, self.hs.config.shadow_server.get("hs")
364364
)
365-
self.shadow_password(params, shadow_user.to_string())
365+
await self.shadow_password(params, shadow_user.to_string())
366366

367367
return 200, {}
368368

369369
def on_OPTIONS(self, _):
370370
return 200, {}
371371

372-
@defer.inlineCallbacks
373-
def shadow_password(self, body, user_id):
372+
async def shadow_password(self, body, user_id):
374373
# TODO: retries
375374
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
376375
as_token = self.hs.config.shadow_server.get("as_token")
377376

378-
yield self.http_client.post_json_get_json(
377+
await self.http_client.post_json_get_json(
379378
"%s/_matrix/client/r0/account/password?access_token=%s&user_id=%s"
380379
% (shadow_hs_url, as_token, user_id),
381380
body,
@@ -756,7 +755,7 @@ async def on_POST(self, request):
756755
shadow_user = UserID(
757756
requester.user.localpart, self.hs.config.shadow_server.get("hs")
758757
)
759-
self.shadow_3pid({"threepid": threepid}, shadow_user.to_string())
758+
await self.shadow_3pid({"threepid": threepid}, shadow_user.to_string())
760759

761760
return 200, {}
762761

@@ -791,21 +790,20 @@ async def on_POST(self, request):
791790
"address": validation_session["address"],
792791
"validated_at": validation_session["validated_at"],
793792
}
794-
self.shadow_3pid({"threepid": threepid}, shadow_user.to_string())
793+
await self.shadow_3pid({"threepid": threepid}, shadow_user.to_string())
795794

796795
return 200, {}
797796

798797
raise SynapseError(
799798
400, "No validated 3pid session found", Codes.THREEPID_AUTH_FAILED
800799
)
801800

802-
@defer.inlineCallbacks
803-
def shadow_3pid(self, body, user_id):
801+
async def shadow_3pid(self, body, user_id):
804802
# TODO: retries
805803
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
806804
as_token = self.hs.config.shadow_server.get("as_token")
807805

808-
yield self.http_client.post_json_get_json(
806+
await self.http_client.post_json_get_json(
809807
"%s/_matrix/client/r0/account/3pid?access_token=%s&user_id=%s"
810808
% (shadow_hs_url, as_token, user_id),
811809
body,
@@ -866,20 +864,19 @@ async def on_POST(self, request):
866864
"address": validation_session["address"],
867865
"validated_at": validation_session["validated_at"],
868866
}
869-
self.shadow_3pid({"threepid": threepid}, shadow_user.to_string())
867+
await self.shadow_3pid({"threepid": threepid}, shadow_user.to_string())
870868
return 200, {}
871869

872870
raise SynapseError(
873871
400, "No validated 3pid session found", Codes.THREEPID_AUTH_FAILED
874872
)
875873

876-
@defer.inlineCallbacks
877-
def shadow_3pid(self, body, user_id):
874+
async def shadow_3pid(self, body, user_id):
878875
# TODO: retries
879876
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
880877
as_token = self.hs.config.shadow_server.get("as_token")
881878

882-
yield self.http_client.post_json_get_json(
879+
await self.http_client.post_json_get_json(
883880
"%s/_matrix/client/r0/account/3pid?access_token=%s&user_id=%s"
884881
% (shadow_hs_url, as_token, user_id),
885882
body,
@@ -983,7 +980,7 @@ async def on_POST(self, request):
983980
shadow_user = UserID(
984981
requester.user.localpart, self.hs.config.shadow_server.get("hs")
985982
)
986-
self.shadow_3pid_delete(body, shadow_user.to_string())
983+
await self.shadow_3pid_delete(body, shadow_user.to_string())
987984

988985
if ret:
989986
id_server_unbind_result = "success"
@@ -992,13 +989,12 @@ async def on_POST(self, request):
992989

993990
return 200, {"id_server_unbind_result": id_server_unbind_result}
994991

995-
@defer.inlineCallbacks
996-
def shadow_3pid_delete(self, body, user_id):
992+
async def shadow_3pid_delete(self, body, user_id):
997993
# TODO: retries
998994
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
999995
as_token = self.hs.config.shadow_server.get("as_token")
1000996

1001-
yield self.http_client.post_json_get_json(
997+
await self.http_client.post_json_get_json(
1002998
"%s/_matrix/client/r0/account/3pid/delete?access_token=%s&user_id=%s"
1003999
% (shadow_hs_url, as_token, user_id),
10041000
body,
@@ -1101,45 +1097,6 @@ def assert_valid_next_link(hs: "HomeServer", next_link: str):
11011097
)
11021098

11031099

1104-
def assert_valid_next_link(hs: "HomeServer", next_link: str):
1105-
"""
1106-
Raises a SynapseError if a given next_link value is invalid
1107-
1108-
next_link is valid if the scheme is http(s) and the next_link.domain_whitelist config
1109-
option is either empty or contains a domain that matches the one in the given next_link
1110-
1111-
Args:
1112-
hs: The homeserver object
1113-
next_link: The next_link value given by the client
1114-
1115-
Raises:
1116-
SynapseError: If the next_link is invalid
1117-
"""
1118-
valid = True
1119-
1120-
# Parse the contents of the URL
1121-
next_link_parsed = urlparse(next_link)
1122-
1123-
# Scheme must not point to the local drive
1124-
if next_link_parsed.scheme == "file":
1125-
valid = False
1126-
1127-
# If the domain whitelist is set, the domain must be in it
1128-
if (
1129-
valid
1130-
and hs.config.next_link_domain_whitelist is not None
1131-
and next_link_parsed.hostname not in hs.config.next_link_domain_whitelist
1132-
):
1133-
valid = False
1134-
1135-
if not valid:
1136-
raise SynapseError(
1137-
400,
1138-
"'next_link' domain not included in whitelist, or not http(s)",
1139-
errcode=Codes.INVALID_PARAM,
1140-
)
1141-
1142-
11431100
class WhoamiRestServlet(RestServlet):
11441101
PATTERNS = client_patterns("/account/whoami$")
11451102

synapse/storage/databases/main/push_rule.py

+120-11
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
16-
1716
import abc
1817
import logging
1918
from typing import List, Tuple, Union
2019

20+
from synapse.api.errors import NotFoundError, StoreError
2121
from synapse.push.baserules import list_with_base_rules
2222
from synapse.replication.slave.storage._slaved_id_tracker import SlavedIdTracker
2323
from synapse.storage._base import SQLBaseStore, db_to_json
@@ -27,6 +27,7 @@
2727
from synapse.storage.databases.main.pusher import PusherWorkerStore
2828
from synapse.storage.databases.main.receipts import ReceiptsWorkerStore
2929
from synapse.storage.databases.main.roommember import RoomMemberWorkerStore
30+
from synapse.storage.engines import PostgresEngine, Sqlite3Engine
3031
from synapse.storage.push_rule import InconsistentRuleException, RuleNotFoundException
3132
from synapse.storage.util.id_generators import StreamIdGenerator
3233
from synapse.util import json_encoder
@@ -540,6 +541,25 @@ def _upsert_push_rule_txn(
540541
},
541542
)
542543

544+
# ensure we have a push_rules_enable row
545+
# enabledness defaults to true
546+
if isinstance(self.database_engine, PostgresEngine):
547+
sql = """
548+
INSERT INTO push_rules_enable (id, user_name, rule_id, enabled)
549+
VALUES (?, ?, ?, ?)
550+
ON CONFLICT DO NOTHING
551+
"""
552+
elif isinstance(self.database_engine, Sqlite3Engine):
553+
sql = """
554+
INSERT OR IGNORE INTO push_rules_enable (id, user_name, rule_id, enabled)
555+
VALUES (?, ?, ?, ?)
556+
"""
557+
else:
558+
raise RuntimeError("Unknown database engine")
559+
560+
new_enable_id = self._push_rules_enable_id_gen.get_next()
561+
txn.execute(sql, (new_enable_id, user_id, rule_id, 1))
562+
543563
async def delete_push_rule(self, user_id: str, rule_id: str) -> None:
544564
"""
545565
Delete a push rule. Args specify the row to be deleted and can be
@@ -552,6 +572,12 @@ async def delete_push_rule(self, user_id: str, rule_id: str) -> None:
552572
"""
553573

554574
def delete_push_rule_txn(txn, stream_id, event_stream_ordering):
575+
# we don't use simple_delete_one_txn because that would fail if the
576+
# user did not have a push_rule_enable row.
577+
self.db_pool.simple_delete_txn(
578+
txn, "push_rules_enable", {"user_name": user_id, "rule_id": rule_id}
579+
)
580+
555581
self.db_pool.simple_delete_one_txn(
556582
txn, "push_rules", {"user_name": user_id, "rule_id": rule_id}
557583
)
@@ -570,10 +596,29 @@ def delete_push_rule_txn(txn, stream_id, event_stream_ordering):
570596
event_stream_ordering,
571597
)
572598

573-
async def set_push_rule_enabled(self, user_id, rule_id, enabled) -> None:
599+
async def set_push_rule_enabled(
600+
self, user_id: str, rule_id: str, enabled: bool, is_default_rule: bool
601+
) -> None:
602+
"""
603+
Sets the `enabled` state of a push rule.
604+
605+
Args:
606+
user_id: the user ID of the user who wishes to enable/disable the rule
607+
e.g. '@tina:example.org'
608+
rule_id: the full rule ID of the rule to be enabled/disabled
609+
e.g. 'global/override/.m.rule.roomnotif'
610+
or 'global/override/myCustomRule'
611+
enabled: True if the rule is to be enabled, False if it is to be
612+
disabled
613+
is_default_rule: True if and only if this is a server-default rule.
614+
This skips the check for existence (as only user-created rules
615+
are always stored in the database `push_rules` table).
616+
617+
Raises:
618+
NotFoundError if the rule does not exist.
619+
"""
574620
with await self._push_rules_stream_id_gen.get_next() as stream_id:
575621
event_stream_ordering = self._stream_id_gen.get_current_token()
576-
577622
await self.db_pool.runInteraction(
578623
"_set_push_rule_enabled_txn",
579624
self._set_push_rule_enabled_txn,
@@ -582,12 +627,47 @@ async def set_push_rule_enabled(self, user_id, rule_id, enabled) -> None:
582627
user_id,
583628
rule_id,
584629
enabled,
630+
is_default_rule,
585631
)
586632

587633
def _set_push_rule_enabled_txn(
588-
self, txn, stream_id, event_stream_ordering, user_id, rule_id, enabled
634+
self,
635+
txn,
636+
stream_id,
637+
event_stream_ordering,
638+
user_id,
639+
rule_id,
640+
enabled,
641+
is_default_rule,
589642
):
590643
new_id = self._push_rules_enable_id_gen.get_next()
644+
645+
if not is_default_rule:
646+
# first check it exists; we need to lock for key share so that a
647+
# transaction that deletes the push rule will conflict with this one.
648+
# We also need a push_rule_enable row to exist for every push_rules
649+
# row, otherwise it is possible to simultaneously delete a push rule
650+
# (that has no _enable row) and enable it, resulting in a dangling
651+
# _enable row. To solve this: we either need to use SERIALISABLE or
652+
# ensure we always have a push_rule_enable row for every push_rule
653+
# row. We chose the latter.
654+
for_key_share = "FOR KEY SHARE"
655+
if not isinstance(self.database_engine, PostgresEngine):
656+
# For key share is not applicable/available on SQLite
657+
for_key_share = ""
658+
sql = (
659+
"""
660+
SELECT 1 FROM push_rules
661+
WHERE user_name = ? AND rule_id = ?
662+
%s
663+
"""
664+
% for_key_share
665+
)
666+
txn.execute(sql, (user_id, rule_id))
667+
if txn.fetchone() is None:
668+
# needed to set NOT_FOUND code.
669+
raise NotFoundError("Push rule does not exist.")
670+
591671
self.db_pool.simple_upsert_txn(
592672
txn,
593673
"push_rules_enable",
@@ -606,8 +686,30 @@ def _set_push_rule_enabled_txn(
606686
)
607687

608688
async def set_push_rule_actions(
609-
self, user_id, rule_id, actions, is_default_rule
689+
self,
690+
user_id: str,
691+
rule_id: str,
692+
actions: List[Union[dict, str]],
693+
is_default_rule: bool,
610694
) -> None:
695+
"""
696+
Sets the `actions` state of a push rule.
697+
698+
Will throw NotFoundError if the rule does not exist; the Code for this
699+
is NOT_FOUND.
700+
701+
Args:
702+
user_id: the user ID of the user who wishes to enable/disable the rule
703+
e.g. '@tina:example.org'
704+
rule_id: the full rule ID of the rule to be enabled/disabled
705+
e.g. 'global/override/.m.rule.roomnotif'
706+
or 'global/override/myCustomRule'
707+
actions: A list of actions (each action being a dict or string),
708+
e.g. ["notify", {"set_tweak": "highlight", "value": false}]
709+
is_default_rule: True if and only if this is a server-default rule.
710+
This skips the check for existence (as only user-created rules
711+
are always stored in the database `push_rules` table).
712+
"""
611713
actions_json = json_encoder.encode(actions)
612714

613715
def set_push_rule_actions_txn(txn, stream_id, event_stream_ordering):
@@ -629,12 +731,19 @@ def set_push_rule_actions_txn(txn, stream_id, event_stream_ordering):
629731
update_stream=False,
630732
)
631733
else:
632-
self.db_pool.simple_update_one_txn(
633-
txn,
634-
"push_rules",
635-
{"user_name": user_id, "rule_id": rule_id},
636-
{"actions": actions_json},
637-
)
734+
try:
735+
self.db_pool.simple_update_one_txn(
736+
txn,
737+
"push_rules",
738+
{"user_name": user_id, "rule_id": rule_id},
739+
{"actions": actions_json},
740+
)
741+
except StoreError as serr:
742+
if serr.code == 404:
743+
# this sets the NOT_FOUND error Code
744+
raise NotFoundError("Push rule does not exist")
745+
else:
746+
raise
638747

639748
self._insert_push_rules_update_txn(
640749
txn,

0 commit comments

Comments
 (0)