1# Copyright 2021 The Matrix.org Foundation C.I.C.
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 logging
15from typing import TYPE_CHECKING, Collection, List, Optional, Union
16
17from synapse import event_auth
18from synapse.api.constants import (
19    EventTypes,
20    JoinRules,
21    Membership,
22    RestrictedJoinRuleTypes,
23)
24from synapse.api.errors import AuthError, Codes, SynapseError
25from synapse.api.room_versions import RoomVersion
26from synapse.event_auth import check_auth_rules_for_event
27from synapse.events import EventBase
28from synapse.events.builder import EventBuilder
29from synapse.events.snapshot import EventContext
30from synapse.types import StateMap, get_domain_from_id
31from synapse.util.metrics import Measure
32
33if TYPE_CHECKING:
34    from synapse.server import HomeServer
35
36logger = logging.getLogger(__name__)
37
38
39class EventAuthHandler:
40    """
41    This class contains methods for authenticating events added to room graphs.
42    """
43
44    def __init__(self, hs: "HomeServer"):
45        self._clock = hs.get_clock()
46        self._store = hs.get_datastore()
47        self._server_name = hs.hostname
48
49    async def check_auth_rules_from_context(
50        self,
51        room_version_obj: RoomVersion,
52        event: EventBase,
53        context: EventContext,
54    ) -> None:
55        """Check an event passes the auth rules at its own auth events"""
56        auth_event_ids = event.auth_event_ids()
57        auth_events_by_id = await self._store.get_events(auth_event_ids)
58        check_auth_rules_for_event(room_version_obj, event, auth_events_by_id.values())
59
60    def compute_auth_events(
61        self,
62        event: Union[EventBase, EventBuilder],
63        current_state_ids: StateMap[str],
64        for_verification: bool = False,
65    ) -> List[str]:
66        """Given an event and current state return the list of event IDs used
67        to auth an event.
68
69        If `for_verification` is False then only return auth events that
70        should be added to the event's `auth_events`.
71
72        Returns:
73            List of event IDs.
74        """
75
76        if event.type == EventTypes.Create:
77            return []
78
79        # Currently we ignore the `for_verification` flag even though there are
80        # some situations where we can drop particular auth events when adding
81        # to the event's `auth_events` (e.g. joins pointing to previous joins
82        # when room is publicly joinable). Dropping event IDs has the
83        # advantage that the auth chain for the room grows slower, but we use
84        # the auth chain in state resolution v2 to order events, which means
85        # care must be taken if dropping events to ensure that it doesn't
86        # introduce undesirable "state reset" behaviour.
87        #
88        # All of which sounds a bit tricky so we don't bother for now.
89        auth_ids = []
90        for etype, state_key in event_auth.auth_types_for_event(
91            event.room_version, event
92        ):
93            auth_ev_id = current_state_ids.get((etype, state_key))
94            if auth_ev_id:
95                auth_ids.append(auth_ev_id)
96
97        return auth_ids
98
99    async def get_user_which_could_invite(
100        self, room_id: str, current_state_ids: StateMap[str]
101    ) -> str:
102        """
103        Searches the room state for a local user who has the power level necessary
104        to invite other users.
105
106        Args:
107            room_id: The room ID under search.
108            current_state_ids: The current state of the room.
109
110        Returns:
111            The MXID of the user which could issue an invite.
112
113        Raises:
114            SynapseError if no appropriate user is found.
115        """
116        power_level_event_id = current_state_ids.get((EventTypes.PowerLevels, ""))
117        invite_level = 0
118        users_default_level = 0
119        if power_level_event_id:
120            power_level_event = await self._store.get_event(power_level_event_id)
121            invite_level = power_level_event.content.get("invite", invite_level)
122            users_default_level = power_level_event.content.get(
123                "users_default", users_default_level
124            )
125            users = power_level_event.content.get("users", {})
126        else:
127            users = {}
128
129        # Find the user with the highest power level.
130        users_in_room = await self._store.get_users_in_room(room_id)
131        # Only interested in local users.
132        local_users_in_room = [
133            u for u in users_in_room if get_domain_from_id(u) == self._server_name
134        ]
135        chosen_user = max(
136            local_users_in_room,
137            key=lambda user: users.get(user, users_default_level),
138            default=None,
139        )
140
141        # Return the chosen if they can issue invites.
142        user_power_level = users.get(chosen_user, users_default_level)
143        if chosen_user and user_power_level >= invite_level:
144            logger.debug(
145                "Found a user who can issue invites  %s with power level %d >= invite level %d",
146                chosen_user,
147                user_power_level,
148                invite_level,
149            )
150            return chosen_user
151
152        # No user was found.
153        raise SynapseError(
154            400,
155            "Unable to find a user which could issue an invite",
156            Codes.UNABLE_TO_GRANT_JOIN,
157        )
158
159    async def check_host_in_room(self, room_id: str, host: str) -> bool:
160        with Measure(self._clock, "check_host_in_room"):
161            return await self._store.is_host_joined(room_id, host)
162
163    async def check_restricted_join_rules(
164        self,
165        state_ids: StateMap[str],
166        room_version: RoomVersion,
167        user_id: str,
168        prev_member_event: Optional[EventBase],
169    ) -> None:
170        """
171        Check whether a user can join a room without an invite due to restricted join rules.
172
173        When joining a room with restricted joined rules (as defined in MSC3083),
174        the membership of rooms must be checked during a room join.
175
176        Args:
177            state_ids: The state of the room as it currently is.
178            room_version: The room version of the room being joined.
179            user_id: The user joining the room.
180            prev_member_event: The current membership event for this user.
181
182        Raises:
183            AuthError if the user cannot join the room.
184        """
185        # If the member is invited or currently joined, then nothing to do.
186        if prev_member_event and (
187            prev_member_event.membership in (Membership.JOIN, Membership.INVITE)
188        ):
189            return
190
191        # This is not a room with a restricted join rule, so we don't need to do the
192        # restricted room specific checks.
193        #
194        # Note: We'll be applying the standard join rule checks later, which will
195        # catch the cases of e.g. trying to join private rooms without an invite.
196        if not await self.has_restricted_join_rules(state_ids, room_version):
197            return
198
199        # Get the rooms which allow access to this room and check if the user is
200        # in any of them.
201        allowed_rooms = await self.get_rooms_that_allow_join(state_ids)
202        if not await self.is_user_in_rooms(allowed_rooms, user_id):
203
204            # If this is a remote request, the user might be in an allowed room
205            # that we do not know about.
206            if get_domain_from_id(user_id) != self._server_name:
207                for room_id in allowed_rooms:
208                    if not await self._store.is_host_joined(room_id, self._server_name):
209                        raise SynapseError(
210                            400,
211                            f"Unable to check if {user_id} is in allowed rooms.",
212                            Codes.UNABLE_AUTHORISE_JOIN,
213                        )
214
215            raise AuthError(
216                403,
217                "You do not belong to any of the required rooms/spaces to join this room.",
218            )
219
220    async def has_restricted_join_rules(
221        self, state_ids: StateMap[str], room_version: RoomVersion
222    ) -> bool:
223        """
224        Return if the room has the proper join rules set for access via rooms.
225
226        Args:
227            state_ids: The state of the room as it currently is.
228            room_version: The room version of the room to query.
229
230        Returns:
231            True if the proper room version and join rules are set for restricted access.
232        """
233        # This only applies to room versions which support the new join rule.
234        if not room_version.msc3083_join_rules:
235            return False
236
237        # If there's no join rule, then it defaults to invite (so this doesn't apply).
238        join_rules_event_id = state_ids.get((EventTypes.JoinRules, ""), None)
239        if not join_rules_event_id:
240            return False
241
242        # If the join rule is not restricted, this doesn't apply.
243        join_rules_event = await self._store.get_event(join_rules_event_id)
244        return join_rules_event.content.get("join_rule") == JoinRules.RESTRICTED
245
246    async def get_rooms_that_allow_join(
247        self, state_ids: StateMap[str]
248    ) -> Collection[str]:
249        """
250        Generate a list of rooms in which membership allows access to a room.
251
252        Args:
253            state_ids: The current state of the room the user wishes to join
254
255        Returns:
256            A collection of room IDs. Membership in any of the rooms in the list grants the ability to join the target room.
257        """
258        # If there's no join rule, then it defaults to invite (so this doesn't apply).
259        join_rules_event_id = state_ids.get((EventTypes.JoinRules, ""), None)
260        if not join_rules_event_id:
261            return ()
262
263        # If the join rule is not restricted, this doesn't apply.
264        join_rules_event = await self._store.get_event(join_rules_event_id)
265
266        # If allowed is of the wrong form, then only allow invited users.
267        allow_list = join_rules_event.content.get("allow", [])
268        if not isinstance(allow_list, list):
269            return ()
270
271        # Pull out the other room IDs, invalid data gets filtered.
272        result = []
273        for allow in allow_list:
274            if not isinstance(allow, dict):
275                continue
276
277            # If the type is unexpected, skip it.
278            if allow.get("type") != RestrictedJoinRuleTypes.ROOM_MEMBERSHIP:
279                continue
280
281            room_id = allow.get("room_id")
282            if not isinstance(room_id, str):
283                continue
284
285            result.append(room_id)
286
287        return result
288
289    async def is_user_in_rooms(self, room_ids: Collection[str], user_id: str) -> bool:
290        """
291        Check whether a user is a member of any of the provided rooms.
292
293        Args:
294            room_ids: The rooms to check for membership.
295            user_id: The user to check.
296
297        Returns:
298            True if the user is in any of the rooms, false otherwise.
299        """
300        if not room_ids:
301            return False
302
303        # Get the list of joined rooms and see if there's an overlap.
304        joined_rooms = await self._store.get_rooms_for_user(user_id)
305
306        # Check each room and see if the user is in it.
307        for room_id in room_ids:
308            if room_id in joined_rooms:
309                return True
310
311        # The user was not in any of the rooms.
312        return False
313