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"
|
# the literal fields "foo\" and "bar" but will instead be treated as "foo\\.bar"
|
||||||
SPLIT_FIELD_REGEX = re.compile(r"(?<!\\)\.")
|
SPLIT_FIELD_REGEX = re.compile(r"(?<!\\)\.")
|
||||||
|
|
||||||
|
CANONICALJSON_MAX_INT = (2 ** 53) - 1
|
||||||
|
CANONICALJSON_MIN_INT = -CANONICALJSON_MAX_INT
|
||||||
|
|
||||||
|
|
||||||
def prune_event(event: EventBase) -> EventBase:
|
def prune_event(event: EventBase) -> EventBase:
|
||||||
"""Returns a pruned version of the given event, which removes all keys we
|
"""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
|
* NaN, Infinity, -Infinity
|
||||||
"""
|
"""
|
||||||
if isinstance(value, int):
|
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)
|
raise SynapseError(400, "JSON integer out of range", Codes.BAD_JSON)
|
||||||
|
|
||||||
elif isinstance(value, float):
|
elif isinstance(value, float):
|
||||||
|
|
|
@ -11,16 +11,22 @@
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
import collections.abc
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
import jsonschema
|
||||||
|
|
||||||
from synapse.api.constants import MAX_ALIAS_LENGTH, EventTypes, Membership
|
from synapse.api.constants import MAX_ALIAS_LENGTH, EventTypes, Membership
|
||||||
from synapse.api.errors import Codes, SynapseError
|
from synapse.api.errors import Codes, SynapseError
|
||||||
from synapse.api.room_versions import EventFormatVersions
|
from synapse.api.room_versions import EventFormatVersions
|
||||||
from synapse.config.homeserver import HomeServerConfig
|
from synapse.config.homeserver import HomeServerConfig
|
||||||
from synapse.events import EventBase
|
from synapse.events import EventBase
|
||||||
from synapse.events.builder import EventBuilder
|
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.federation.federation_server import server_matches_acl_event
|
||||||
from synapse.types import EventID, RoomID, UserID
|
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"
|
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):
|
def _validate_retention(self, event: EventBase):
|
||||||
"""Checks that an event that defines the retention policy for a room respects the
|
"""Checks that an event that defines the retention policy for a room respects the
|
||||||
format enforced by the spec.
|
format enforced by the spec.
|
||||||
|
@ -185,3 +214,47 @@ class EventValidator:
|
||||||
def _ensure_state_event(self, event):
|
def _ensure_state_event(self, event):
|
||||||
if not event.is_state():
|
if not event.is_state():
|
||||||
raise SynapseError(400, "'%s' must be state events" % (event.type,))
|
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.
|
# [1] https://pip.pypa.io/en/stable/reference/pip_install/#requirement-specifiers.
|
||||||
|
|
||||||
REQUIREMENTS = [
|
REQUIREMENTS = [
|
||||||
"jsonschema>=2.5.1",
|
# we use the TYPE_CHECKER.redefine method added in jsonschema 3.0.0
|
||||||
|
"jsonschema>=3.0.0",
|
||||||
"frozendict>=1",
|
"frozendict>=1",
|
||||||
"unpaddedbase64>=1.1.0",
|
"unpaddedbase64>=1.1.0",
|
||||||
"canonicaljson>=1.4.0",
|
"canonicaljson>=1.4.0",
|
||||||
|
|
|
@ -12,6 +12,8 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# 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 import admin
|
||||||
from synapse.rest.client import login, room, sync
|
from synapse.rest.client import login, room, sync
|
||||||
|
|
||||||
|
@ -203,3 +205,79 @@ class PowerLevelsTestCase(HomeserverTestCase):
|
||||||
tok=self.admin_access_token,
|
tok=self.admin_access_token,
|
||||||
expect_code=200, # expect success
|
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