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

Commit a537007

Browse files
reivilibrerichvdh
andauthored
Don't remember enabled of deleted push rules and properly return 404 for missing push rules in .../actions and .../enabled (#7796)
Signed-off-by: Olivier Wilkinson (reivilibre) <[email protected]> Co-authored-by: Richard van der Hoff <[email protected]>
1 parent e45b834 commit a537007

File tree

5 files changed

+610
-13
lines changed

5 files changed

+610
-13
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.

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/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,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/* Copyright 2020 The Matrix.org Foundation C.I.C.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
/**
17+
Delete stuck 'enabled' bits that correspond to deleted or non-existent push rules.
18+
We ignore rules that are server-default rules because they are not defined
19+
in the `push_rules` table.
20+
**/
21+
22+
DELETE FROM push_rules_enable WHERE
23+
rule_id NOT LIKE 'global/%/.m.rule.%'
24+
AND NOT EXISTS (
25+
SELECT 1 FROM push_rules
26+
WHERE push_rules.user_name = push_rules_enable.user_name
27+
AND push_rules.rule_id = push_rules_enable.rule_id
28+
);

0 commit comments

Comments
 (0)