Validate new m.room.power_levels events (#10232)
Signed-off-by: Aaron Raimist <aaron@raim.ist>pull/10706/head
parent
ad17fbd20e
commit
40f619eaa5
|
@ -0,0 +1 @@
|
|||
Validate new `m.room.power_levels` events. Contributed by @aaronraimist.
|
|
@ -32,6 +32,9 @@ from . import EventBase
|
|||
# the literal fields "foo\" and "bar" but will instead be treated as "foo\\.bar"
|
||||
SPLIT_FIELD_REGEX = re.compile(r"(?<!\\)\.")
|
||||
|
||||
CANONICALJSON_MAX_INT = (2 ** 53) - 1
|
||||
CANONICALJSON_MIN_INT = -CANONICALJSON_MAX_INT
|
||||
|
||||
|
||||
def prune_event(event: EventBase) -> EventBase:
|
||||
"""Returns a pruned version of the given event, which removes all keys we
|
||||
|
@ -505,7 +508,7 @@ def validate_canonicaljson(value: Any):
|
|||
* NaN, Infinity, -Infinity
|
||||
"""
|
||||
if isinstance(value, int):
|
||||
if value <= -(2 ** 53) or 2 ** 53 <= value:
|
||||
if value < CANONICALJSON_MIN_INT or CANONICALJSON_MAX_INT < value:
|
||||
raise SynapseError(400, "JSON integer out of range", Codes.BAD_JSON)
|
||||
|
||||
elif isinstance(value, float):
|
||||
|
|
|
@ -11,16 +11,22 @@
|
|||
# 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 collections.abc
|
||||
from typing import Union
|
||||
|
||||
import jsonschema
|
||||
|
||||
from synapse.api.constants import MAX_ALIAS_LENGTH, EventTypes, Membership
|
||||
from synapse.api.errors import Codes, SynapseError
|
||||
from synapse.api.room_versions import EventFormatVersions
|
||||
from synapse.config.homeserver import HomeServerConfig
|
||||
from synapse.events import EventBase
|
||||
from synapse.events.builder import EventBuilder
|
||||
from synapse.events.utils import validate_canonicaljson
|
||||
from synapse.events.utils import (
|
||||
CANONICALJSON_MAX_INT,
|
||||
CANONICALJSON_MIN_INT,
|
||||
validate_canonicaljson,
|
||||
)
|
||||
from synapse.federation.federation_server import server_matches_acl_event
|
||||
from synapse.types import EventID, RoomID, UserID
|
||||
|
||||
|
@ -87,6 +93,29 @@ class EventValidator:
|
|||
400, "Can't create an ACL event that denies the local server"
|
||||
)
|
||||
|
||||
if event.type == EventTypes.PowerLevels:
|
||||
try:
|
||||
jsonschema.validate(
|
||||
instance=event.content,
|
||||
schema=POWER_LEVELS_SCHEMA,
|
||||
cls=plValidator,
|
||||
)
|
||||
except jsonschema.ValidationError as e:
|
||||
if e.path:
|
||||
# example: "users_default": '0' is not of type 'integer'
|
||||
message = '"' + e.path[-1] + '": ' + e.message # noqa: B306
|
||||
# jsonschema.ValidationError.message is a valid attribute
|
||||
else:
|
||||
# example: '0' is not of type 'integer'
|
||||
message = e.message # noqa: B306
|
||||
# jsonschema.ValidationError.message is a valid attribute
|
||||
|
||||
raise SynapseError(
|
||||
code=400,
|
||||
msg=message,
|
||||
errcode=Codes.BAD_JSON,
|
||||
)
|
||||
|
||||
def _validate_retention(self, event: EventBase):
|
||||
"""Checks that an event that defines the retention policy for a room respects the
|
||||
format enforced by the spec.
|
||||
|
@ -185,3 +214,47 @@ class EventValidator:
|
|||
def _ensure_state_event(self, event):
|
||||
if not event.is_state():
|
||||
raise SynapseError(400, "'%s' must be state events" % (event.type,))
|
||||
|
||||
|
||||
POWER_LEVELS_SCHEMA = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ban": {"$ref": "#/definitions/int"},
|
||||
"events": {"$ref": "#/definitions/objectOfInts"},
|
||||
"events_default": {"$ref": "#/definitions/int"},
|
||||
"invite": {"$ref": "#/definitions/int"},
|
||||
"kick": {"$ref": "#/definitions/int"},
|
||||
"notifications": {"$ref": "#/definitions/objectOfInts"},
|
||||
"redact": {"$ref": "#/definitions/int"},
|
||||
"state_default": {"$ref": "#/definitions/int"},
|
||||
"users": {"$ref": "#/definitions/objectOfInts"},
|
||||
"users_default": {"$ref": "#/definitions/int"},
|
||||
},
|
||||
"definitions": {
|
||||
"int": {
|
||||
"type": "integer",
|
||||
"minimum": CANONICALJSON_MIN_INT,
|
||||
"maximum": CANONICALJSON_MAX_INT,
|
||||
},
|
||||
"objectOfInts": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"$ref": "#/definitions/int"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _create_power_level_validator():
|
||||
validator = jsonschema.validators.validator_for(POWER_LEVELS_SCHEMA)
|
||||
|
||||
# by default jsonschema does not consider a frozendict to be an object so
|
||||
# we need to use a custom type checker
|
||||
# https://python-jsonschema.readthedocs.io/en/stable/validate/?highlight=object#validating-with-additional-types
|
||||
type_checker = validator.TYPE_CHECKER.redefine(
|
||||
"object", lambda checker, thing: isinstance(thing, collections.abc.Mapping)
|
||||
)
|
||||
|
||||
return jsonschema.validators.extend(validator, type_checker=type_checker)
|
||||
|
||||
|
||||
plValidator = _create_power_level_validator()
|
||||
|
|
|
@ -48,7 +48,8 @@ logger = logging.getLogger(__name__)
|
|||
# [1] https://pip.pypa.io/en/stable/reference/pip_install/#requirement-specifiers.
|
||||
|
||||
REQUIREMENTS = [
|
||||
"jsonschema>=2.5.1",
|
||||
# we use the TYPE_CHECKER.redefine method added in jsonschema 3.0.0
|
||||
"jsonschema>=3.0.0",
|
||||
"frozendict>=1",
|
||||
"unpaddedbase64>=1.1.0",
|
||||
"canonicaljson>=1.4.0",
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from synapse.api.errors import Codes
|
||||
from synapse.events.utils import CANONICALJSON_MAX_INT, CANONICALJSON_MIN_INT
|
||||
from synapse.rest import admin
|
||||
from synapse.rest.client import login, room, sync
|
||||
|
||||
|
@ -203,3 +205,79 @@ class PowerLevelsTestCase(HomeserverTestCase):
|
|||
tok=self.admin_access_token,
|
||||
expect_code=200, # expect success
|
||||
)
|
||||
|
||||
def test_cannot_set_string_power_levels(self):
|
||||
room_power_levels = self.helper.get_state(
|
||||
self.room_id,
|
||||
"m.room.power_levels",
|
||||
tok=self.admin_access_token,
|
||||
)
|
||||
|
||||
# Update existing power levels with user at PL "0"
|
||||
room_power_levels["users"].update({self.user_user_id: "0"})
|
||||
|
||||
body = self.helper.send_state(
|
||||
self.room_id,
|
||||
"m.room.power_levels",
|
||||
room_power_levels,
|
||||
tok=self.admin_access_token,
|
||||
expect_code=400, # expect failure
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["errcode"],
|
||||
Codes.BAD_JSON,
|
||||
body,
|
||||
)
|
||||
|
||||
def test_cannot_set_unsafe_large_power_levels(self):
|
||||
room_power_levels = self.helper.get_state(
|
||||
self.room_id,
|
||||
"m.room.power_levels",
|
||||
tok=self.admin_access_token,
|
||||
)
|
||||
|
||||
# Update existing power levels with user at PL above the max safe integer
|
||||
room_power_levels["users"].update(
|
||||
{self.user_user_id: CANONICALJSON_MAX_INT + 1}
|
||||
)
|
||||
|
||||
body = self.helper.send_state(
|
||||
self.room_id,
|
||||
"m.room.power_levels",
|
||||
room_power_levels,
|
||||
tok=self.admin_access_token,
|
||||
expect_code=400, # expect failure
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["errcode"],
|
||||
Codes.BAD_JSON,
|
||||
body,
|
||||
)
|
||||
|
||||
def test_cannot_set_unsafe_small_power_levels(self):
|
||||
room_power_levels = self.helper.get_state(
|
||||
self.room_id,
|
||||
"m.room.power_levels",
|
||||
tok=self.admin_access_token,
|
||||
)
|
||||
|
||||
# Update existing power levels with user at PL below the minimum safe integer
|
||||
room_power_levels["users"].update(
|
||||
{self.user_user_id: CANONICALJSON_MIN_INT - 1}
|
||||
)
|
||||
|
||||
body = self.helper.send_state(
|
||||
self.room_id,
|
||||
"m.room.power_levels",
|
||||
room_power_levels,
|
||||
tok=self.admin_access_token,
|
||||
expect_code=400, # expect failure
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["errcode"],
|
||||
Codes.BAD_JSON,
|
||||
body,
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue