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

Don't remember enabled of deleted push rules and properly return 404 for missing push rules in .../actions and .../enabled #7796

Merged
merged 32 commits into from
Sep 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ab3c6ba
Write some relevant tests (some failing)
reivilibre Jul 6, 2020
4b5bfc7
Fix test bug
reivilibre Jul 7, 2020
7f6791f
Make test_actions_404_when_put_inexistent_server_rule pass
reivilibre Jul 7, 2020
d01e536
Delete push_rule_enable row on rule deletion
reivilibre Jul 7, 2020
bdfef69
Use M_NOTFOUND code for missing push rules
reivilibre Jul 7, 2020
3911200
Check for nonexistant rules first
reivilibre Jul 7, 2020
271a937
Check non-server-default push rules exist before enabling/disabling
reivilibre Jul 7, 2020
cc3dfa7
Add SQL migration to remove dangling push_rules_enabled rows.
reivilibre Jul 7, 2020
e0cb3d2
Antilint
reivilibre Jul 7, 2020
9e66462
Newsfile
reivilibre Jul 7, 2020
7987fb9
I sort, you sort, we all sort!
reivilibre Jul 7, 2020
aebeb9f
Apply magic from Rich
reivilibre Jul 17, 2020
0cc3f02
Move delta to correct place and simplify.
reivilibre Jul 17, 2020
afea1e5
inexistent could be said to be non-existent
reivilibre Jul 17, 2020
0118ac5
Tighten to StoreError
reivilibre Jul 17, 2020
872a39c
Wow, I did not find the NotFoundError :)
reivilibre Jul 17, 2020
e8ea6a8
Antilint
reivilibre Jul 17, 2020
6667e27
Change syntax to support old SQLite3s
reivilibre Jul 17, 2020
7565a44
Docstrings, type annotations and minor changes for readability
reivilibre Aug 4, 2020
ee165dc
Always create an enabled row for push rules.
reivilibre Aug 18, 2020
58d7498
Merge branch 'develop' into rei/pushrules_latent_enabled
reivilibre Aug 18, 2020
32c1c2a
Use FOR KEY SHARE
reivilibre Aug 18, 2020
d5b63b9
rowcount is not what I thought
reivilibre Aug 18, 2020
5f211a6
Oops, 1 is truthy
reivilibre Aug 19, 2020
cca039d
For key share is a Postgresqlism
reivilibre Aug 19, 2020
bd1fad6
Fix test docstrings
reivilibre Aug 19, 2020
52035df
Fix old SQLite3 support
reivilibre Aug 19, 2020
1ae92c4
Merge branch 'develop' into rei/pushrules_latent_enabled
reivilibre Aug 19, 2020
ce94cc8
Cosmetic changes
reivilibre Aug 21, 2020
114b01f
Antilint
reivilibre Aug 27, 2020
80c7dd1
Merge branch 'develop' into rei/pushrules_latent_enabled
reivilibre Aug 27, 2020
62c096e
Merge remote-tracking branch 'origin/develop' into rei/pushrules_late…
reivilibre Sep 2, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/7796.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix inconsistent handling of non-existent push rules, and stop tracking the `enabled` state of removed push rules.
15 changes: 13 additions & 2 deletions synapse/rest/client/v1/push_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,18 @@ def notify_user(self, user_id):
self.notifier.on_new_event("push_rules_key", stream_id, users=[user_id])

async def set_rule_attr(self, user_id, spec, val):
if spec["attr"] not in ("enabled", "actions"):
# for the sake of potential future expansion, shouldn't report
# 404 in the case of an unknown request so check it corresponds to
# a known attribute first.
raise UnrecognizedRequestError()

namespaced_rule_id = _namespaced_rule_id_from_spec(spec)
rule_id = spec["rule_id"]
is_default_rule = rule_id.startswith(".")
if is_default_rule:
if namespaced_rule_id not in BASE_RULE_IDS:
raise NotFoundError("Unknown rule %s" % (namespaced_rule_id,))
if spec["attr"] == "enabled":
if isinstance(val, dict) and "enabled" in val:
val = val["enabled"]
Expand All @@ -171,9 +183,8 @@ async def set_rule_attr(self, user_id, spec, val):
# This should *actually* take a dict, but many clients pass
# bools directly, so let's not break them.
raise SynapseError(400, "Value for 'enabled' must be boolean")
namespaced_rule_id = _namespaced_rule_id_from_spec(spec)
return await self.store.set_push_rule_enabled(
user_id, namespaced_rule_id, val
user_id, namespaced_rule_id, val, is_default_rule
)
elif spec["attr"] == "actions":
actions = val.get("actions")
Expand Down
131 changes: 120 additions & 11 deletions synapse/storage/databases/main/push_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
# 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.

import abc
import logging
from typing import List, Tuple, Union

from synapse.api.errors import NotFoundError, StoreError
from synapse.push.baserules import list_with_base_rules
from synapse.replication.slave.storage._slaved_id_tracker import SlavedIdTracker
from synapse.storage._base import SQLBaseStore, db_to_json
Expand All @@ -27,6 +27,7 @@
from synapse.storage.databases.main.pusher import PusherWorkerStore
from synapse.storage.databases.main.receipts import ReceiptsWorkerStore
from synapse.storage.databases.main.roommember import RoomMemberWorkerStore
from synapse.storage.engines import PostgresEngine, Sqlite3Engine
from synapse.storage.push_rule import InconsistentRuleException, RuleNotFoundException
from synapse.storage.util.id_generators import StreamIdGenerator
from synapse.util import json_encoder
Expand Down Expand Up @@ -540,6 +541,25 @@ def _upsert_push_rule_txn(
},
)

# ensure we have a push_rules_enable row
# enabledness defaults to true
if isinstance(self.database_engine, PostgresEngine):
sql = """
INSERT INTO push_rules_enable (id, user_name, rule_id, enabled)
VALUES (?, ?, ?, ?)
ON CONFLICT DO NOTHING
"""
elif isinstance(self.database_engine, Sqlite3Engine):
sql = """
INSERT OR IGNORE INTO push_rules_enable (id, user_name, rule_id, enabled)
VALUES (?, ?, ?, ?)
"""
else:
raise RuntimeError("Unknown database engine")

new_enable_id = self._push_rules_enable_id_gen.get_next()
txn.execute(sql, (new_enable_id, user_id, rule_id, 1))

async def delete_push_rule(self, user_id: str, rule_id: str) -> None:
"""
Delete a push rule. Args specify the row to be deleted and can be
Expand All @@ -552,6 +572,12 @@ async def delete_push_rule(self, user_id: str, rule_id: str) -> None:
"""

def delete_push_rule_txn(txn, stream_id, event_stream_ordering):
# we don't use simple_delete_one_txn because that would fail if the
# user did not have a push_rule_enable row.
self.db_pool.simple_delete_txn(
txn, "push_rules_enable", {"user_name": user_id, "rule_id": rule_id}
)

self.db_pool.simple_delete_one_txn(
txn, "push_rules", {"user_name": user_id, "rule_id": rule_id}
)
Expand All @@ -570,10 +596,29 @@ def delete_push_rule_txn(txn, stream_id, event_stream_ordering):
event_stream_ordering,
)

async def set_push_rule_enabled(self, user_id, rule_id, enabled) -> None:
async def set_push_rule_enabled(
self, user_id: str, rule_id: str, enabled: bool, is_default_rule: bool
) -> None:
"""
Sets the `enabled` state of a push rule.

Args:
user_id: the user ID of the user who wishes to enable/disable the rule
e.g. '@tina:example.org'
rule_id: the full rule ID of the rule to be enabled/disabled
e.g. 'global/override/.m.rule.roomnotif'
or 'global/override/myCustomRule'
enabled: True if the rule is to be enabled, False if it is to be
disabled
is_default_rule: True if and only if this is a server-default rule.
This skips the check for existence (as only user-created rules
are always stored in the database `push_rules` table).

Raises:
NotFoundError if the rule does not exist.
"""
with await self._push_rules_stream_id_gen.get_next() as stream_id:
event_stream_ordering = self._stream_id_gen.get_current_token()

await self.db_pool.runInteraction(
"_set_push_rule_enabled_txn",
self._set_push_rule_enabled_txn,
Expand All @@ -582,12 +627,47 @@ async def set_push_rule_enabled(self, user_id, rule_id, enabled) -> None:
user_id,
rule_id,
enabled,
is_default_rule,
)

def _set_push_rule_enabled_txn(
self, txn, stream_id, event_stream_ordering, user_id, rule_id, enabled
self,
txn,
stream_id,
event_stream_ordering,
user_id,
rule_id,
enabled,
is_default_rule,
):
new_id = self._push_rules_enable_id_gen.get_next()

if not is_default_rule:
# first check it exists; we need to lock for key share so that a
# transaction that deletes the push rule will conflict with this one.
# We also need a push_rule_enable row to exist for every push_rules
# row, otherwise it is possible to simultaneously delete a push rule
# (that has no _enable row) and enable it, resulting in a dangling
# _enable row. To solve this: we either need to use SERIALISABLE or
# ensure we always have a push_rule_enable row for every push_rule
# row. We chose the latter.
for_key_share = "FOR KEY SHARE"
if not isinstance(self.database_engine, PostgresEngine):
# For key share is not applicable/available on SQLite
for_key_share = ""
sql = (
"""
SELECT 1 FROM push_rules
WHERE user_name = ? AND rule_id = ?
%s
"""
% for_key_share
)
txn.execute(sql, (user_id, rule_id))
if txn.fetchone() is None:
# needed to set NOT_FOUND code.
raise NotFoundError("Push rule does not exist.")

self.db_pool.simple_upsert_txn(
txn,
"push_rules_enable",
Expand All @@ -606,8 +686,30 @@ def _set_push_rule_enabled_txn(
)

async def set_push_rule_actions(
self, user_id, rule_id, actions, is_default_rule
self,
user_id: str,
rule_id: str,
actions: List[Union[dict, str]],
is_default_rule: bool,
) -> None:
"""
Sets the `actions` state of a push rule.

Will throw NotFoundError if the rule does not exist; the Code for this
is NOT_FOUND.

Args:
user_id: the user ID of the user who wishes to enable/disable the rule
e.g. '@tina:example.org'
rule_id: the full rule ID of the rule to be enabled/disabled
e.g. 'global/override/.m.rule.roomnotif'
or 'global/override/myCustomRule'
actions: A list of actions (each action being a dict or string),
e.g. ["notify", {"set_tweak": "highlight", "value": false}]
is_default_rule: True if and only if this is a server-default rule.
This skips the check for existence (as only user-created rules
are always stored in the database `push_rules` table).
"""
actions_json = json_encoder.encode(actions)

def set_push_rule_actions_txn(txn, stream_id, event_stream_ordering):
Expand All @@ -629,12 +731,19 @@ def set_push_rule_actions_txn(txn, stream_id, event_stream_ordering):
update_stream=False,
)
else:
self.db_pool.simple_update_one_txn(
txn,
"push_rules",
{"user_name": user_id, "rule_id": rule_id},
{"actions": actions_json},
)
try:
self.db_pool.simple_update_one_txn(
txn,
"push_rules",
{"user_name": user_id, "rule_id": rule_id},
{"actions": actions_json},
)
except StoreError as serr:
if serr.code == 404:
# this sets the NOT_FOUND error Code
raise NotFoundError("Push rule does not exist")
else:
raise

self._insert_push_rules_update_txn(
txn,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* Copyright 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.
* 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.
*/

/**
Delete stuck 'enabled' bits that correspond to deleted or non-existent push rules.
We ignore rules that are server-default rules because they are not defined
in the `push_rules` table.
**/

DELETE FROM push_rules_enable WHERE
rule_id NOT LIKE 'global/%/.m.rule.%'
AND NOT EXISTS (
SELECT 1 FROM push_rules
WHERE push_rules.user_name = push_rules_enable.user_name
AND push_rules.rule_id = push_rules_enable.rule_id
);
Loading