1# Copyright 2014-2016 OpenMarket Ltd 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. 14import collections.abc 15from typing import Iterable, Union 16 17import jsonschema 18 19from synapse.api.constants import MAX_ALIAS_LENGTH, EventTypes, Membership 20from synapse.api.errors import Codes, SynapseError 21from synapse.api.room_versions import EventFormatVersions 22from synapse.config.homeserver import HomeServerConfig 23from synapse.events import EventBase 24from synapse.events.builder import EventBuilder 25from synapse.events.utils import ( 26 CANONICALJSON_MAX_INT, 27 CANONICALJSON_MIN_INT, 28 validate_canonicaljson, 29) 30from synapse.federation.federation_server import server_matches_acl_event 31from synapse.types import EventID, JsonDict, RoomID, UserID 32 33 34class EventValidator: 35 def validate_new(self, event: EventBase, config: HomeServerConfig) -> None: 36 """Validates the event has roughly the right format 37 38 Args: 39 event: The event to validate. 40 config: The homeserver's configuration. 41 """ 42 self.validate_builder(event) 43 44 if event.format_version == EventFormatVersions.V1: 45 EventID.from_string(event.event_id) 46 47 required = [ 48 "auth_events", 49 "content", 50 "hashes", 51 "origin", 52 "prev_events", 53 "sender", 54 "type", 55 ] 56 57 for k in required: 58 if k not in event: 59 raise SynapseError(400, "Event does not have key %s" % (k,)) 60 61 # Check that the following keys have string values 62 event_strings = ["origin"] 63 64 for s in event_strings: 65 if not isinstance(getattr(event, s), str): 66 raise SynapseError(400, "'%s' not a string type" % (s,)) 67 68 # Depending on the room version, ensure the data is spec compliant JSON. 69 if event.room_version.strict_canonicaljson: 70 # Note that only the client controlled portion of the event is 71 # checked, since we trust the portions of the event we created. 72 validate_canonicaljson(event.content) 73 74 if event.type == EventTypes.Aliases: 75 if "aliases" in event.content: 76 for alias in event.content["aliases"]: 77 if len(alias) > MAX_ALIAS_LENGTH: 78 raise SynapseError( 79 400, 80 ( 81 "Can't create aliases longer than" 82 " %d characters" % (MAX_ALIAS_LENGTH,) 83 ), 84 Codes.INVALID_PARAM, 85 ) 86 87 if event.type == EventTypes.Retention: 88 self._validate_retention(event) 89 90 if event.type == EventTypes.ServerACL: 91 if not server_matches_acl_event(config.server.server_name, event): 92 raise SynapseError( 93 400, "Can't create an ACL event that denies the local server" 94 ) 95 96 if event.type == EventTypes.PowerLevels: 97 try: 98 jsonschema.validate( 99 instance=event.content, 100 schema=POWER_LEVELS_SCHEMA, 101 cls=plValidator, 102 ) 103 except jsonschema.ValidationError as e: 104 if e.path: 105 # example: "users_default": '0' is not of type 'integer' 106 message = '"' + e.path[-1] + '": ' + e.message # noqa: B306 107 # jsonschema.ValidationError.message is a valid attribute 108 else: 109 # example: '0' is not of type 'integer' 110 message = e.message # noqa: B306 111 # jsonschema.ValidationError.message is a valid attribute 112 113 raise SynapseError( 114 code=400, 115 msg=message, 116 errcode=Codes.BAD_JSON, 117 ) 118 119 def _validate_retention(self, event: EventBase) -> None: 120 """Checks that an event that defines the retention policy for a room respects the 121 format enforced by the spec. 122 123 Args: 124 event: The event to validate. 125 """ 126 if not event.is_state(): 127 raise SynapseError(code=400, msg="must be a state event") 128 129 min_lifetime = event.content.get("min_lifetime") 130 max_lifetime = event.content.get("max_lifetime") 131 132 if min_lifetime is not None: 133 if not isinstance(min_lifetime, int): 134 raise SynapseError( 135 code=400, 136 msg="'min_lifetime' must be an integer", 137 errcode=Codes.BAD_JSON, 138 ) 139 140 if max_lifetime is not None: 141 if not isinstance(max_lifetime, int): 142 raise SynapseError( 143 code=400, 144 msg="'max_lifetime' must be an integer", 145 errcode=Codes.BAD_JSON, 146 ) 147 148 if ( 149 min_lifetime is not None 150 and max_lifetime is not None 151 and min_lifetime > max_lifetime 152 ): 153 raise SynapseError( 154 code=400, 155 msg="'min_lifetime' can't be greater than 'max_lifetime", 156 errcode=Codes.BAD_JSON, 157 ) 158 159 def validate_builder(self, event: Union[EventBase, EventBuilder]) -> None: 160 """Validates that the builder/event has roughly the right format. Only 161 checks values that we expect a proto event to have, rather than all the 162 fields an event would have 163 """ 164 165 strings = ["room_id", "sender", "type"] 166 167 if hasattr(event, "state_key"): 168 strings.append("state_key") 169 170 for s in strings: 171 if not isinstance(getattr(event, s), str): 172 raise SynapseError(400, "Not '%s' a string type" % (s,)) 173 174 RoomID.from_string(event.room_id) 175 UserID.from_string(event.sender) 176 177 if event.type == EventTypes.Message: 178 strings = ["body", "msgtype"] 179 180 self._ensure_strings(event.content, strings) 181 182 elif event.type == EventTypes.Topic: 183 self._ensure_strings(event.content, ["topic"]) 184 self._ensure_state_event(event) 185 elif event.type == EventTypes.Name: 186 self._ensure_strings(event.content, ["name"]) 187 self._ensure_state_event(event) 188 elif event.type == EventTypes.Member: 189 if "membership" not in event.content: 190 raise SynapseError(400, "Content has not membership key") 191 192 if event.content["membership"] not in Membership.LIST: 193 raise SynapseError(400, "Invalid membership key") 194 195 self._ensure_state_event(event) 196 elif event.type == EventTypes.Tombstone: 197 if "replacement_room" not in event.content: 198 raise SynapseError(400, "Content has no replacement_room key") 199 200 if event.content["replacement_room"] == event.room_id: 201 raise SynapseError( 202 400, "Tombstone cannot reference the room it was sent in" 203 ) 204 205 self._ensure_state_event(event) 206 207 def _ensure_strings(self, d: JsonDict, keys: Iterable[str]) -> None: 208 for s in keys: 209 if s not in d: 210 raise SynapseError(400, "'%s' not in content" % (s,)) 211 if not isinstance(d[s], str): 212 raise SynapseError(400, "'%s' not a string type" % (s,)) 213 214 def _ensure_state_event(self, event: Union[EventBase, EventBuilder]) -> None: 215 if not event.is_state(): 216 raise SynapseError(400, "'%s' must be state events" % (event.type,)) 217 218 219POWER_LEVELS_SCHEMA = { 220 "type": "object", 221 "properties": { 222 "ban": {"$ref": "#/definitions/int"}, 223 "events": {"$ref": "#/definitions/objectOfInts"}, 224 "events_default": {"$ref": "#/definitions/int"}, 225 "invite": {"$ref": "#/definitions/int"}, 226 "kick": {"$ref": "#/definitions/int"}, 227 "notifications": {"$ref": "#/definitions/objectOfInts"}, 228 "redact": {"$ref": "#/definitions/int"}, 229 "state_default": {"$ref": "#/definitions/int"}, 230 "users": {"$ref": "#/definitions/objectOfInts"}, 231 "users_default": {"$ref": "#/definitions/int"}, 232 }, 233 "definitions": { 234 "int": { 235 "type": "integer", 236 "minimum": CANONICALJSON_MIN_INT, 237 "maximum": CANONICALJSON_MAX_INT, 238 }, 239 "objectOfInts": { 240 "type": "object", 241 "additionalProperties": {"$ref": "#/definitions/int"}, 242 }, 243 }, 244} 245 246 247# This could return something newer than Draft 7, but that's the current "latest" 248# validator. 249def _create_power_level_validator() -> jsonschema.Draft7Validator: 250 validator = jsonschema.validators.validator_for(POWER_LEVELS_SCHEMA) 251 252 # by default jsonschema does not consider a frozendict to be an object so 253 # we need to use a custom type checker 254 # https://python-jsonschema.readthedocs.io/en/stable/validate/?highlight=object#validating-with-additional-types 255 type_checker = validator.TYPE_CHECKER.redefine( 256 "object", lambda checker, thing: isinstance(thing, collections.abc.Mapping) 257 ) 258 259 return jsonschema.validators.extend(validator, type_checker=type_checker) 260 261 262plValidator = _create_power_level_validator() 263