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