2014-08-12 16:10:52 +02:00
|
|
|
# -*- coding: utf-8 -*-
|
2016-01-05 19:01:18 +01:00
|
|
|
# Copyright 2014 - 2016 OpenMarket Ltd
|
2014-08-12 16:10:52 +02:00
|
|
|
#
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
#
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
#
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
# 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.
|
2016-07-26 17:46:53 +02:00
|
|
|
import logging
|
2020-08-06 14:30:06 +02:00
|
|
|
from typing import List, Optional, Tuple
|
2016-07-26 17:46:53 +02:00
|
|
|
|
|
|
|
import pymacaroons
|
2018-06-28 21:31:53 +02:00
|
|
|
from netaddr import IPAddress
|
2014-08-12 16:10:52 +02:00
|
|
|
|
2020-05-14 17:32:49 +02:00
|
|
|
from twisted.web.server import Request
|
2018-07-09 08:09:20 +02:00
|
|
|
|
2016-07-26 17:46:53 +02:00
|
|
|
import synapse.types
|
2017-01-13 16:07:32 +01:00
|
|
|
from synapse import event_auth
|
2020-05-06 16:54:58 +02:00
|
|
|
from synapse.api.auth_blocking import AuthBlocking
|
|
|
|
from synapse.api.constants import EventTypes, Membership
|
2019-07-11 12:06:23 +02:00
|
|
|
from synapse.api.errors import (
|
|
|
|
AuthError,
|
|
|
|
Codes,
|
|
|
|
InvalidClientTokenError,
|
|
|
|
MissingClientTokenError,
|
|
|
|
)
|
2020-01-28 15:18:29 +01:00
|
|
|
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS
|
2020-02-19 00:13:29 +01:00
|
|
|
from synapse.events import EventBase
|
2020-07-05 17:32:02 +02:00
|
|
|
from synapse.logging import opentracing as opentracing
|
2020-01-16 14:31:22 +01:00
|
|
|
from synapse.types import StateMap, UserID
|
2017-06-29 15:50:18 +02:00
|
|
|
from synapse.util.caches.lrucache import LruCache
|
2016-04-13 12:15:59 +02:00
|
|
|
from synapse.util.metrics import Measure
|
2014-08-12 16:10:52 +02:00
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2015-03-16 01:18:08 +01:00
|
|
|
AuthEventTypes = (
|
2019-06-20 11:32:02 +02:00
|
|
|
EventTypes.Create,
|
|
|
|
EventTypes.Member,
|
|
|
|
EventTypes.PowerLevels,
|
|
|
|
EventTypes.JoinRules,
|
|
|
|
EventTypes.RoomHistoryVisibility,
|
2015-10-01 18:49:52 +02:00
|
|
|
EventTypes.ThirdPartyInvite,
|
2015-03-16 01:18:08 +01:00
|
|
|
)
|
|
|
|
|
2016-11-25 16:25:30 +01:00
|
|
|
# guests always get this device id.
|
|
|
|
GUEST_DEVICE_ID = "guest_device"
|
|
|
|
|
2015-03-16 01:18:08 +01:00
|
|
|
|
2017-06-29 15:50:18 +02:00
|
|
|
class _InvalidMacaroonException(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2020-09-04 12:54:56 +02:00
|
|
|
class Auth:
|
2017-01-10 19:16:54 +01:00
|
|
|
"""
|
|
|
|
FIXME: This class contains a mix of functions for authenticating users
|
|
|
|
of our client-server API and authenticating events added to room graphs.
|
|
|
|
"""
|
2019-06-20 11:32:02 +02:00
|
|
|
|
2017-01-10 19:16:54 +01:00
|
|
|
def __init__(self, hs):
|
|
|
|
self.hs = hs
|
|
|
|
self.clock = hs.get_clock()
|
|
|
|
self.store = hs.get_datastore()
|
|
|
|
self.state = hs.get_state_handler()
|
|
|
|
|
2020-10-16 16:56:39 +02:00
|
|
|
self.token_cache = LruCache(
|
|
|
|
10000, "token_cache"
|
|
|
|
) # type: LruCache[str, Tuple[str, bool]]
|
2017-06-29 15:50:18 +02:00
|
|
|
|
2020-05-06 16:54:58 +02:00
|
|
|
self._auth_blocking = AuthBlocking(self.hs)
|
|
|
|
|
2019-04-08 18:10:55 +02:00
|
|
|
self._account_validity = hs.config.account_validity
|
2020-05-06 16:54:58 +02:00
|
|
|
self._track_appservice_user_ips = hs.config.track_appservice_user_ips
|
|
|
|
self._macaroon_secret_key = hs.config.macaroon_secret_key
|
2019-04-08 18:10:55 +02:00
|
|
|
|
2020-08-06 14:30:06 +02:00
|
|
|
async def check_from_context(
|
|
|
|
self, room_version: str, event, context, do_sig_check=True
|
|
|
|
):
|
|
|
|
prev_state_ids = await context.get_prev_state_ids()
|
|
|
|
auth_events_ids = self.compute_auth_events(
|
2019-06-20 11:32:02 +02:00
|
|
|
event, prev_state_ids, for_verification=True
|
2017-01-10 19:16:54 +01:00
|
|
|
)
|
2020-08-06 14:30:06 +02:00
|
|
|
auth_events = await self.store.get_events(auth_events_ids)
|
2020-06-15 13:03:36 +02:00
|
|
|
auth_events = {(e.type, e.state_key): e for e in auth_events.values()}
|
2020-01-28 15:18:29 +01:00
|
|
|
|
|
|
|
room_version_obj = KNOWN_ROOM_VERSIONS[room_version]
|
2019-10-18 19:43:36 +02:00
|
|
|
event_auth.check(
|
2020-01-28 15:18:29 +01:00
|
|
|
room_version_obj, event, auth_events=auth_events, do_sig_check=do_sig_check
|
2019-01-25 19:31:41 +01:00
|
|
|
)
|
2017-01-10 19:16:54 +01:00
|
|
|
|
2020-08-06 14:30:06 +02:00
|
|
|
async def check_user_in_room(
|
2020-02-19 00:13:29 +01:00
|
|
|
self,
|
|
|
|
room_id: str,
|
|
|
|
user_id: str,
|
|
|
|
current_state: Optional[StateMap[EventBase]] = None,
|
|
|
|
allow_departed_users: bool = False,
|
2020-08-06 14:30:06 +02:00
|
|
|
) -> EventBase:
|
2020-02-19 00:13:29 +01:00
|
|
|
"""Check if the user is in the room, or was at some point.
|
2017-01-10 19:16:54 +01:00
|
|
|
Args:
|
2020-02-19 00:13:29 +01:00
|
|
|
room_id: The room to check.
|
|
|
|
|
|
|
|
user_id: The user to check.
|
|
|
|
|
|
|
|
current_state: Optional map of the current state of the room.
|
2017-01-10 19:16:54 +01:00
|
|
|
If provided then that map is used to check whether they are a
|
|
|
|
member of the room. Otherwise the current membership is
|
|
|
|
loaded from the database.
|
2020-02-19 00:13:29 +01:00
|
|
|
|
|
|
|
allow_departed_users: if True, accept users that were previously
|
|
|
|
members but have now departed.
|
|
|
|
|
2017-01-10 19:16:54 +01:00
|
|
|
Raises:
|
2020-02-19 00:13:29 +01:00
|
|
|
AuthError if the user is/was not in the room.
|
2017-01-10 19:16:54 +01:00
|
|
|
Returns:
|
2020-08-06 14:30:06 +02:00
|
|
|
Membership event for the user if the user was in the
|
|
|
|
room. This will be the join event if they are currently joined to
|
|
|
|
the room. This will be the leave event if they have left the room.
|
2017-01-10 19:16:54 +01:00
|
|
|
"""
|
|
|
|
if current_state:
|
2019-06-20 11:32:02 +02:00
|
|
|
member = current_state.get((EventTypes.Member, user_id), None)
|
2017-01-10 19:16:54 +01:00
|
|
|
else:
|
2020-08-06 14:30:06 +02:00
|
|
|
member = await self.state.get_current_state(
|
|
|
|
room_id=room_id, event_type=EventTypes.Member, state_key=user_id
|
2017-01-10 19:16:54 +01:00
|
|
|
)
|
2015-10-01 18:49:52 +02:00
|
|
|
|
2020-08-06 14:30:06 +02:00
|
|
|
if member:
|
|
|
|
membership = member.membership
|
2016-02-23 16:11:25 +01:00
|
|
|
|
2020-08-06 14:30:06 +02:00
|
|
|
if membership == Membership.JOIN:
|
2020-02-19 00:13:29 +01:00
|
|
|
return member
|
2016-02-23 16:11:25 +01:00
|
|
|
|
2020-08-06 14:30:06 +02:00
|
|
|
# XXX this looks totally bogus. Why do we not allow users who have been banned,
|
|
|
|
# or those who were members previously and have been re-invited?
|
|
|
|
if allow_departed_users and membership == Membership.LEAVE:
|
|
|
|
forgot = await self.store.did_forget(user_id, room_id)
|
|
|
|
if not forgot:
|
|
|
|
return member
|
|
|
|
|
2020-02-19 00:13:29 +01:00
|
|
|
raise AuthError(403, "User %s not in room %s" % (user_id, room_id))
|
2016-02-23 16:11:25 +01:00
|
|
|
|
2020-08-06 14:30:06 +02:00
|
|
|
async def check_host_in_room(self, room_id, host):
|
2017-01-10 19:16:54 +01:00
|
|
|
with Measure(self.clock, "check_host_in_room"):
|
2020-08-06 14:30:06 +02:00
|
|
|
latest_event_ids = await self.store.is_host_joined(room_id, host)
|
2019-07-23 15:00:55 +02:00
|
|
|
return latest_event_ids
|
2015-04-22 15:20:04 +02:00
|
|
|
|
2017-01-10 19:16:54 +01:00
|
|
|
def can_federate(self, event, auth_events):
|
|
|
|
creation_event = auth_events.get((EventTypes.Create, ""))
|
2015-04-21 21:53:23 +02:00
|
|
|
|
2017-01-10 19:16:54 +01:00
|
|
|
return creation_event.content.get("m.federate", True) is True
|
2015-04-21 21:53:23 +02:00
|
|
|
|
2017-01-10 19:16:54 +01:00
|
|
|
def get_public_keys(self, invite_event):
|
2017-01-13 16:07:32 +01:00
|
|
|
return event_auth.get_public_keys(invite_event)
|
2014-10-15 17:06:59 +02:00
|
|
|
|
2020-08-06 14:30:06 +02:00
|
|
|
async def get_user_by_req(
|
2020-05-14 17:32:49 +02:00
|
|
|
self,
|
|
|
|
request: Request,
|
|
|
|
allow_guest: bool = False,
|
|
|
|
rights: str = "access",
|
|
|
|
allow_expired: bool = False,
|
2020-08-06 14:30:06 +02:00
|
|
|
) -> synapse.types.Requester:
|
2014-08-12 16:10:52 +02:00
|
|
|
""" Get a registered user's ID.
|
|
|
|
|
|
|
|
Args:
|
2020-05-14 17:32:49 +02:00
|
|
|
request: An HTTP request with an access_token query parameter.
|
|
|
|
allow_guest: If False, will raise an AuthError if the user making the
|
|
|
|
request is a guest.
|
|
|
|
rights: The operation being performed; the access token must allow this
|
|
|
|
allow_expired: If True, allow the request through even if the account
|
|
|
|
is expired, or session token lifetime has ended. Note that
|
|
|
|
/login will deliver access tokens regardless of expiration.
|
|
|
|
|
2014-08-12 16:10:52 +02:00
|
|
|
Returns:
|
2020-08-06 14:30:06 +02:00
|
|
|
Resolves to the requester
|
2014-08-12 16:10:52 +02:00
|
|
|
Raises:
|
2019-07-11 12:06:23 +02:00
|
|
|
InvalidClientCredentialsError if no user by that token exists or the token
|
|
|
|
is invalid.
|
|
|
|
AuthError if access is denied for the user in the access token
|
2014-08-12 16:10:52 +02:00
|
|
|
"""
|
|
|
|
try:
|
2018-12-04 12:44:41 +01:00
|
|
|
ip_addr = self.hs.get_ip_from_request(request)
|
2020-10-23 18:12:59 +02:00
|
|
|
user_agent = request.get_user_agent("")
|
2018-12-04 12:44:41 +01:00
|
|
|
|
2019-07-11 12:06:23 +02:00
|
|
|
access_token = self.get_access_token_from_request(request)
|
2018-12-04 12:44:41 +01:00
|
|
|
|
2020-08-06 14:30:06 +02:00
|
|
|
user_id, app_service = await self._get_appservice_user_id(request)
|
2016-01-18 17:32:33 +01:00
|
|
|
if user_id:
|
2015-08-18 16:16:28 +02:00
|
|
|
request.authenticated_entity = user_id
|
2019-08-16 17:13:25 +02:00
|
|
|
opentracing.set_tag("authenticated_entity", user_id)
|
2019-09-25 12:59:00 +02:00
|
|
|
opentracing.set_tag("appservice_id", app_service.id)
|
2018-12-04 12:44:41 +01:00
|
|
|
|
2020-05-06 16:54:58 +02:00
|
|
|
if ip_addr and self._track_appservice_user_ips:
|
2020-08-06 14:30:06 +02:00
|
|
|
await self.store.insert_client_ip(
|
2018-12-04 12:44:41 +01:00
|
|
|
user_id=user_id,
|
|
|
|
access_token=access_token,
|
|
|
|
ip=ip_addr,
|
|
|
|
user_agent=user_agent,
|
|
|
|
device_id="dummy-device", # stubbed
|
|
|
|
)
|
|
|
|
|
2019-07-23 15:00:55 +02:00
|
|
|
return synapse.types.create_requester(user_id, app_service=app_service)
|
2015-02-05 16:00:33 +01:00
|
|
|
|
2020-08-06 14:30:06 +02:00
|
|
|
user_info = await self.get_user_by_access_token(
|
2020-05-14 17:32:49 +02:00
|
|
|
access_token, rights, allow_expired=allow_expired
|
|
|
|
)
|
2014-09-29 15:59:52 +02:00
|
|
|
user = user_info["user"]
|
2015-01-28 17:58:23 +01:00
|
|
|
token_id = user_info["token_id"]
|
2015-11-04 18:29:07 +01:00
|
|
|
is_guest = user_info["is_guest"]
|
2020-08-14 18:37:59 +02:00
|
|
|
shadow_banned = user_info["shadow_banned"]
|
2014-09-26 17:36:24 +02:00
|
|
|
|
2019-04-08 18:10:55 +02:00
|
|
|
# Deny the request if the user account has expired.
|
2019-06-05 17:35:05 +02:00
|
|
|
if self._account_validity.enabled and not allow_expired:
|
2019-04-10 18:58:47 +02:00
|
|
|
user_id = user.to_string()
|
2020-09-23 17:06:28 +02:00
|
|
|
if await self.store.is_account_expired(user_id, self.clock.time_msec()):
|
2019-04-08 18:10:55 +02:00
|
|
|
raise AuthError(
|
2019-06-20 11:32:02 +02:00
|
|
|
403, "User account has expired", errcode=Codes.EXPIRED_ACCOUNT
|
2019-04-08 18:10:55 +02:00
|
|
|
)
|
|
|
|
|
2016-07-20 16:25:40 +02:00
|
|
|
# device_id may not be present if get_user_by_access_token has been
|
|
|
|
# stubbed out.
|
|
|
|
device_id = user_info.get("device_id")
|
|
|
|
|
2014-09-26 17:36:24 +02:00
|
|
|
if user and access_token and ip_addr:
|
2020-08-06 14:30:06 +02:00
|
|
|
await self.store.insert_client_ip(
|
2017-06-27 16:53:45 +02:00
|
|
|
user_id=user.to_string(),
|
2014-09-29 15:59:52 +02:00
|
|
|
access_token=access_token,
|
|
|
|
ip=ip_addr,
|
2016-07-20 16:25:40 +02:00
|
|
|
user_agent=user_agent,
|
|
|
|
device_id=device_id,
|
2014-09-29 14:35:15 +02:00
|
|
|
)
|
2014-09-26 17:36:24 +02:00
|
|
|
|
2015-11-04 18:29:07 +01:00
|
|
|
if is_guest and not allow_guest:
|
|
|
|
raise AuthError(
|
2019-06-20 11:32:02 +02:00
|
|
|
403,
|
|
|
|
"Guest access not allowed",
|
|
|
|
errcode=Codes.GUEST_ACCESS_FORBIDDEN,
|
2015-11-04 18:29:07 +01:00
|
|
|
)
|
|
|
|
|
2015-06-15 18:11:44 +02:00
|
|
|
request.authenticated_entity = user.to_string()
|
2019-08-16 17:13:25 +02:00
|
|
|
opentracing.set_tag("authenticated_entity", user.to_string())
|
2019-09-25 12:59:00 +02:00
|
|
|
if device_id:
|
|
|
|
opentracing.set_tag("device_id", device_id)
|
2015-06-15 18:11:44 +02:00
|
|
|
|
2019-07-23 15:00:55 +02:00
|
|
|
return synapse.types.create_requester(
|
2020-08-14 18:37:59 +02:00
|
|
|
user,
|
|
|
|
token_id,
|
|
|
|
is_guest,
|
|
|
|
shadow_banned,
|
|
|
|
device_id,
|
|
|
|
app_service=app_service,
|
2016-10-20 13:07:16 +02:00
|
|
|
)
|
2014-08-12 16:10:52 +02:00
|
|
|
except KeyError:
|
2019-07-11 12:06:23 +02:00
|
|
|
raise MissingClientTokenError()
|
2014-08-12 16:10:52 +02:00
|
|
|
|
2020-08-06 14:30:06 +02:00
|
|
|
async def _get_appservice_user_id(self, request):
|
2016-10-06 10:43:32 +02:00
|
|
|
app_service = self.store.get_app_service_by_token(
|
2019-07-11 12:06:23 +02:00
|
|
|
self.get_access_token_from_request(request)
|
2016-01-18 17:32:33 +01:00
|
|
|
)
|
|
|
|
if app_service is None:
|
2019-08-30 17:28:26 +02:00
|
|
|
return None, None
|
2016-01-18 17:32:33 +01:00
|
|
|
|
2018-06-28 21:31:53 +02:00
|
|
|
if app_service.ip_range_whitelist:
|
|
|
|
ip_address = IPAddress(self.hs.get_ip_from_request(request))
|
|
|
|
if ip_address not in app_service.ip_range_whitelist:
|
2019-08-30 17:28:26 +02:00
|
|
|
return None, None
|
2018-06-28 21:31:53 +02:00
|
|
|
|
2018-08-01 16:54:06 +02:00
|
|
|
if b"user_id" not in request.args:
|
2019-08-30 17:28:26 +02:00
|
|
|
return app_service.sender, app_service
|
2016-01-18 17:32:33 +01:00
|
|
|
|
2019-06-20 11:32:02 +02:00
|
|
|
user_id = request.args[b"user_id"][0].decode("utf8")
|
2016-01-18 17:33:05 +01:00
|
|
|
if app_service.sender == user_id:
|
2019-08-30 17:28:26 +02:00
|
|
|
return app_service.sender, app_service
|
2016-01-18 17:32:33 +01:00
|
|
|
|
|
|
|
if not app_service.is_interested_in_user(user_id):
|
2019-06-20 11:32:02 +02:00
|
|
|
raise AuthError(403, "Application service cannot masquerade as this user.")
|
2020-08-06 14:30:06 +02:00
|
|
|
if not (await self.store.get_user_by_id(user_id)):
|
2019-06-20 11:32:02 +02:00
|
|
|
raise AuthError(403, "Application service has not registered this user")
|
2019-08-30 17:28:26 +02:00
|
|
|
return user_id, app_service
|
2016-01-18 17:32:33 +01:00
|
|
|
|
2020-08-06 14:30:06 +02:00
|
|
|
async def get_user_by_access_token(
|
2020-05-14 17:32:49 +02:00
|
|
|
self, token: str, rights: str = "access", allow_expired: bool = False,
|
2020-08-06 14:30:06 +02:00
|
|
|
) -> dict:
|
2016-12-06 16:31:37 +01:00
|
|
|
""" Validate access token and get user_id from it
|
2014-08-12 16:10:52 +02:00
|
|
|
|
|
|
|
Args:
|
2020-05-14 17:32:49 +02:00
|
|
|
token: The access token to get the user by
|
|
|
|
rights: The operation being performed; the access token must
|
|
|
|
allow this
|
|
|
|
allow_expired: If False, raises an InvalidClientTokenError
|
|
|
|
if the token is expired
|
2014-08-12 16:10:52 +02:00
|
|
|
Returns:
|
2020-08-06 14:30:06 +02:00
|
|
|
dict that includes:
|
2017-11-29 16:41:20 +01:00
|
|
|
`user` (UserID)
|
|
|
|
`is_guest` (bool)
|
2020-08-14 18:37:59 +02:00
|
|
|
`shadow_banned` (bool)
|
2017-11-29 16:41:20 +01:00
|
|
|
`token_id` (int|None): access token id. May be None if guest
|
|
|
|
`device_id` (str|None): device corresponding to access token
|
2014-08-12 16:10:52 +02:00
|
|
|
Raises:
|
2020-05-14 17:32:49 +02:00
|
|
|
InvalidClientTokenError if a user by that token exists, but the token is
|
|
|
|
expired
|
2019-07-11 12:06:23 +02:00
|
|
|
InvalidClientCredentialsError if no user by that token exists or the token
|
2020-05-14 17:32:49 +02:00
|
|
|
is invalid
|
2014-08-12 16:10:52 +02:00
|
|
|
"""
|
2019-01-10 13:41:13 +01:00
|
|
|
|
|
|
|
if rights == "access":
|
|
|
|
# first look in the database
|
2020-08-06 14:30:06 +02:00
|
|
|
r = await self._look_up_user_by_access_token(token)
|
2019-01-10 13:41:13 +01:00
|
|
|
if r:
|
2019-07-12 18:26:02 +02:00
|
|
|
valid_until_ms = r["valid_until_ms"]
|
|
|
|
if (
|
2020-05-14 17:32:49 +02:00
|
|
|
not allow_expired
|
|
|
|
and valid_until_ms is not None
|
2019-07-12 18:26:02 +02:00
|
|
|
and valid_until_ms < self.clock.time_msec()
|
|
|
|
):
|
|
|
|
# there was a valid access token, but it has expired.
|
|
|
|
# soft-logout the user.
|
|
|
|
raise InvalidClientTokenError(
|
|
|
|
msg="Access token has expired", soft_logout=True
|
|
|
|
)
|
|
|
|
|
2019-07-23 15:00:55 +02:00
|
|
|
return r
|
2015-08-26 14:22:23 +02:00
|
|
|
|
2019-01-10 13:41:13 +01:00
|
|
|
# otherwise it needs to be a valid macaroon
|
2015-08-26 14:22:23 +02:00
|
|
|
try:
|
2019-01-10 13:41:13 +01:00
|
|
|
user_id, guest = self._parse_and_validate_macaroon(token, rights)
|
2016-08-08 17:34:07 +02:00
|
|
|
user = UserID.from_string(user_id)
|
2015-11-04 18:29:07 +01:00
|
|
|
|
2019-01-10 13:41:13 +01:00
|
|
|
if rights == "access":
|
|
|
|
if not guest:
|
|
|
|
# non-guest access tokens must be in the database
|
|
|
|
logger.warning("Unrecognised access token - not in store.")
|
2019-07-11 12:06:23 +02:00
|
|
|
raise InvalidClientTokenError()
|
2019-01-10 13:41:13 +01:00
|
|
|
|
2016-12-06 16:31:37 +01:00
|
|
|
# Guest access tokens are not stored in the database (there can
|
|
|
|
# only be one access token per guest, anyway).
|
|
|
|
#
|
|
|
|
# In order to prevent guest access tokens being used as regular
|
|
|
|
# user access tokens (and hence getting around the invalidation
|
|
|
|
# process), we look up the user id and check that it is indeed
|
|
|
|
# a guest user.
|
|
|
|
#
|
|
|
|
# It would of course be much easier to store guest access
|
|
|
|
# tokens in the database as well, but that would break existing
|
|
|
|
# guest tokens.
|
2020-08-06 14:30:06 +02:00
|
|
|
stored_user = await self.store.get_user_by_id(user_id)
|
2016-12-06 16:31:37 +01:00
|
|
|
if not stored_user:
|
2019-07-11 12:06:23 +02:00
|
|
|
raise InvalidClientTokenError("Unknown user_id %s" % user_id)
|
2016-12-06 16:31:37 +01:00
|
|
|
if not stored_user["is_guest"]:
|
2019-07-11 12:06:23 +02:00
|
|
|
raise InvalidClientTokenError(
|
|
|
|
"Guest access token used for regular user"
|
2016-12-06 16:31:37 +01:00
|
|
|
)
|
2015-11-04 18:29:07 +01:00
|
|
|
ret = {
|
|
|
|
"user": user,
|
|
|
|
"is_guest": True,
|
2020-08-14 18:37:59 +02:00
|
|
|
"shadow_banned": False,
|
2015-11-04 18:29:07 +01:00
|
|
|
"token_id": None,
|
2016-11-25 16:25:30 +01:00
|
|
|
# all guests get the same device id
|
|
|
|
"device_id": GUEST_DEVICE_ID,
|
2015-11-04 18:29:07 +01:00
|
|
|
}
|
2016-06-02 18:21:31 +02:00
|
|
|
elif rights == "delete_pusher":
|
|
|
|
# We don't store these tokens in the database
|
|
|
|
ret = {
|
|
|
|
"user": user,
|
|
|
|
"is_guest": False,
|
2020-08-14 18:37:59 +02:00
|
|
|
"shadow_banned": False,
|
2016-06-02 18:21:31 +02:00
|
|
|
"token_id": None,
|
2016-07-20 16:25:40 +02:00
|
|
|
"device_id": None,
|
2016-06-02 18:21:31 +02:00
|
|
|
}
|
2015-11-04 18:29:07 +01:00
|
|
|
else:
|
2019-01-10 13:41:13 +01:00
|
|
|
raise RuntimeError("Unknown rights setting %s", rights)
|
2019-07-23 15:00:55 +02:00
|
|
|
return ret
|
2019-01-10 13:41:13 +01:00
|
|
|
except (
|
|
|
|
_InvalidMacaroonException,
|
|
|
|
pymacaroons.exceptions.MacaroonException,
|
|
|
|
TypeError,
|
|
|
|
ValueError,
|
|
|
|
) as e:
|
|
|
|
logger.warning("Invalid macaroon in auth: %s %s", type(e), e)
|
2019-07-11 12:06:23 +02:00
|
|
|
raise InvalidClientTokenError("Invalid macaroon passed.")
|
2015-08-26 14:22:23 +02:00
|
|
|
|
2017-06-29 15:50:18 +02:00
|
|
|
def _parse_and_validate_macaroon(self, token, rights="access"):
|
|
|
|
"""Takes a macaroon and tries to parse and validate it. This is cached
|
|
|
|
if and only if rights == access and there isn't an expiry.
|
|
|
|
|
|
|
|
On invalid macaroon raises _InvalidMacaroonException
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
(user_id, is_guest)
|
|
|
|
"""
|
|
|
|
if rights == "access":
|
|
|
|
cached = self.token_cache.get(token, None)
|
|
|
|
if cached:
|
|
|
|
return cached
|
|
|
|
|
|
|
|
try:
|
|
|
|
macaroon = pymacaroons.Macaroon.deserialize(token)
|
|
|
|
except Exception: # deserialize can throw more-or-less anything
|
|
|
|
# doesn't look like a macaroon: treat it as an opaque token which
|
|
|
|
# must be in the database.
|
|
|
|
# TODO: it would be nice to get rid of this, but apparently some
|
|
|
|
# people use access tokens which aren't macaroons
|
|
|
|
raise _InvalidMacaroonException()
|
|
|
|
|
|
|
|
try:
|
|
|
|
user_id = self.get_user_id_from_macaroon(macaroon)
|
|
|
|
|
|
|
|
guest = False
|
|
|
|
for caveat in macaroon.caveats:
|
2019-07-30 09:25:02 +02:00
|
|
|
if caveat.caveat_id == "guest = true":
|
2017-06-29 15:50:18 +02:00
|
|
|
guest = True
|
|
|
|
|
2019-07-30 09:25:02 +02:00
|
|
|
self.validate_macaroon(macaroon, rights, user_id=user_id)
|
2017-06-29 15:50:18 +02:00
|
|
|
except (pymacaroons.exceptions.MacaroonException, TypeError, ValueError):
|
2019-07-11 12:06:23 +02:00
|
|
|
raise InvalidClientTokenError("Invalid macaroon passed.")
|
2017-06-29 15:50:18 +02:00
|
|
|
|
2019-07-30 09:25:02 +02:00
|
|
|
if rights == "access":
|
2017-06-29 15:50:18 +02:00
|
|
|
self.token_cache[token] = (user_id, guest)
|
|
|
|
|
|
|
|
return user_id, guest
|
|
|
|
|
2016-08-08 17:34:07 +02:00
|
|
|
def get_user_id_from_macaroon(self, macaroon):
|
|
|
|
"""Retrieve the user_id given by the caveats on the macaroon.
|
|
|
|
|
|
|
|
Does *not* validate the macaroon.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
macaroon (pymacaroons.Macaroon): The macaroon to validate
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
(str) user id
|
|
|
|
|
|
|
|
Raises:
|
2019-07-11 12:06:23 +02:00
|
|
|
InvalidClientCredentialsError if there is no user_id caveat in the
|
|
|
|
macaroon
|
2016-08-08 17:34:07 +02:00
|
|
|
"""
|
|
|
|
user_prefix = "user_id = "
|
|
|
|
for caveat in macaroon.caveats:
|
|
|
|
if caveat.caveat_id.startswith(user_prefix):
|
2019-06-20 11:32:02 +02:00
|
|
|
return caveat.caveat_id[len(user_prefix) :]
|
2019-07-11 12:06:23 +02:00
|
|
|
raise InvalidClientTokenError("No user caveat in macaroon")
|
2016-08-08 17:34:07 +02:00
|
|
|
|
2019-07-30 09:25:02 +02:00
|
|
|
def validate_macaroon(self, macaroon, type_string, user_id):
|
2015-11-19 16:16:25 +01:00
|
|
|
"""
|
|
|
|
validate that a Macaroon is understood by and was signed by this server.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
macaroon(pymacaroons.Macaroon): The macaroon to validate
|
2016-11-30 18:40:18 +01:00
|
|
|
type_string(str): The kind of token required (e.g. "access",
|
2016-06-01 18:40:52 +02:00
|
|
|
"delete_pusher")
|
2016-08-08 17:34:07 +02:00
|
|
|
user_id (str): The user_id required
|
2015-11-19 16:16:25 +01:00
|
|
|
"""
|
2015-08-26 14:22:23 +02:00
|
|
|
v = pymacaroons.Verifier()
|
2016-11-24 13:38:17 +01:00
|
|
|
|
|
|
|
# the verifier runs a test for every caveat on the macaroon, to check
|
|
|
|
# that it is met for the current request. Each caveat must match at
|
|
|
|
# least one of the predicates specified by satisfy_exact or
|
|
|
|
# specify_general.
|
2015-08-26 14:22:23 +02:00
|
|
|
v.satisfy_exact("gen = 1")
|
2015-11-11 12:12:35 +01:00
|
|
|
v.satisfy_exact("type = " + type_string)
|
2016-07-07 17:11:37 +02:00
|
|
|
v.satisfy_exact("user_id = %s" % user_id)
|
2015-11-17 11:58:05 +01:00
|
|
|
v.satisfy_exact("guest = true")
|
2019-07-30 09:25:02 +02:00
|
|
|
v.satisfy_general(self._verify_expiry)
|
2015-11-11 12:12:35 +01:00
|
|
|
|
2016-11-30 18:40:18 +01:00
|
|
|
# access_tokens include a nonce for uniqueness: any value is acceptable
|
2016-11-28 10:55:21 +01:00
|
|
|
v.satisfy_general(lambda c: c.startswith("nonce = "))
|
|
|
|
|
2020-05-06 16:54:58 +02:00
|
|
|
v.verify(macaroon, self._macaroon_secret_key)
|
2015-08-26 14:22:23 +02:00
|
|
|
|
2015-11-19 16:16:25 +01:00
|
|
|
def _verify_expiry(self, caveat):
|
2015-08-26 14:22:23 +02:00
|
|
|
prefix = "time < "
|
|
|
|
if not caveat.startswith(prefix):
|
|
|
|
return False
|
2019-06-20 11:32:02 +02:00
|
|
|
expiry = int(caveat[len(prefix) :])
|
2015-08-26 14:22:23 +02:00
|
|
|
now = self.hs.get_clock().time_msec()
|
|
|
|
return now < expiry
|
|
|
|
|
2020-08-06 14:30:06 +02:00
|
|
|
async def _look_up_user_by_access_token(self, token):
|
|
|
|
ret = await self.store.get_user_by_access_token(token)
|
2015-03-24 18:24:15 +01:00
|
|
|
if not ret:
|
2019-07-23 15:00:55 +02:00
|
|
|
return None
|
2019-01-10 13:41:13 +01:00
|
|
|
|
2016-07-20 16:25:40 +02:00
|
|
|
# we use ret.get() below because *lots* of unit tests stub out
|
|
|
|
# get_user_by_access_token in a way where it only returns a couple of
|
|
|
|
# the fields.
|
2015-03-24 18:24:15 +01:00
|
|
|
user_info = {
|
|
|
|
"user": UserID.from_string(ret.get("name")),
|
|
|
|
"token_id": ret.get("token_id", None),
|
2015-11-04 18:29:07 +01:00
|
|
|
"is_guest": False,
|
2020-08-14 18:37:59 +02:00
|
|
|
"shadow_banned": ret.get("shadow_banned"),
|
2016-07-20 16:25:40 +02:00
|
|
|
"device_id": ret.get("device_id"),
|
2019-07-12 18:26:02 +02:00
|
|
|
"valid_until_ms": ret.get("valid_until_ms"),
|
2015-03-24 18:24:15 +01:00
|
|
|
}
|
2019-07-23 15:00:55 +02:00
|
|
|
return user_info
|
2014-09-01 14:44:19 +02:00
|
|
|
|
2015-02-06 11:57:14 +01:00
|
|
|
def get_appservice_by_req(self, request):
|
2019-07-11 12:06:23 +02:00
|
|
|
token = self.get_access_token_from_request(request)
|
|
|
|
service = self.store.get_app_service_by_token(token)
|
|
|
|
if not service:
|
2019-10-31 11:23:24 +01:00
|
|
|
logger.warning("Unrecognised appservice access token.")
|
2019-07-11 12:06:23 +02:00
|
|
|
raise InvalidClientTokenError()
|
|
|
|
request.authenticated_entity = service.sender
|
2020-08-06 14:30:06 +02:00
|
|
|
return service
|
2015-02-06 11:57:14 +01:00
|
|
|
|
2020-06-05 15:33:49 +02:00
|
|
|
async def is_server_admin(self, user: UserID) -> bool:
|
2017-09-19 17:08:14 +02:00
|
|
|
""" Check if the given user is a local server admin.
|
|
|
|
|
|
|
|
Args:
|
2020-06-05 15:33:49 +02:00
|
|
|
user: user to check
|
2017-09-19 17:08:14 +02:00
|
|
|
|
|
|
|
Returns:
|
2020-06-05 15:33:49 +02:00
|
|
|
True if the user is an admin
|
2017-09-19 17:08:14 +02:00
|
|
|
"""
|
2020-06-05 15:33:49 +02:00
|
|
|
return await self.store.is_server_admin(user)
|
2014-09-29 14:35:38 +02:00
|
|
|
|
2019-12-16 17:59:32 +01:00
|
|
|
def compute_auth_events(
|
2020-01-16 14:31:22 +01:00
|
|
|
self, event, current_state_ids: StateMap[str], for_verification: bool = False,
|
2020-08-06 14:30:06 +02:00
|
|
|
) -> List[str]:
|
2019-12-16 17:59:32 +01:00
|
|
|
"""Given an event and current state return the list of event IDs used
|
|
|
|
to auth an event.
|
2014-11-07 11:42:44 +01:00
|
|
|
|
2019-12-16 17:59:32 +01:00
|
|
|
If `for_verification` is False then only return auth events that
|
|
|
|
should be added to the event's `auth_events`.
|
2014-11-07 11:42:44 +01:00
|
|
|
|
2019-12-16 17:59:32 +01:00
|
|
|
Returns:
|
2020-08-06 14:30:06 +02:00
|
|
|
List of event IDs.
|
2019-12-16 17:59:32 +01:00
|
|
|
"""
|
2014-11-25 12:31:18 +01:00
|
|
|
|
2019-12-16 17:59:32 +01:00
|
|
|
if event.type == EventTypes.Create:
|
2020-08-06 14:30:06 +02:00
|
|
|
return []
|
2019-12-16 17:59:32 +01:00
|
|
|
|
|
|
|
# Currently we ignore the `for_verification` flag even though there are
|
|
|
|
# some situations where we can drop particular auth events when adding
|
|
|
|
# to the event's `auth_events` (e.g. joins pointing to previous joins
|
2020-07-09 15:52:58 +02:00
|
|
|
# when room is publicly joinable). Dropping event IDs has the
|
2019-12-16 17:59:32 +01:00
|
|
|
# advantage that the auth chain for the room grows slower, but we use
|
|
|
|
# the auth chain in state resolution v2 to order events, which means
|
|
|
|
# care must be taken if dropping events to ensure that it doesn't
|
|
|
|
# introduce undesirable "state reset" behaviour.
|
|
|
|
#
|
|
|
|
# All of which sounds a bit tricky so we don't bother for now.
|
2014-11-07 11:42:44 +01:00
|
|
|
|
2019-12-16 17:59:32 +01:00
|
|
|
auth_ids = []
|
|
|
|
for etype, state_key in event_auth.auth_types_for_event(event):
|
|
|
|
auth_ev_id = current_state_ids.get((etype, state_key))
|
|
|
|
if auth_ev_id:
|
|
|
|
auth_ids.append(auth_ev_id)
|
2014-11-07 11:42:44 +01:00
|
|
|
|
2020-08-06 14:30:06 +02:00
|
|
|
return auth_ids
|
2014-11-07 11:42:44 +01:00
|
|
|
|
2020-05-01 16:15:36 +02:00
|
|
|
async def check_can_change_room_list(self, room_id: str, user: UserID):
|
2020-03-04 17:30:46 +01:00
|
|
|
"""Determine whether the user is allowed to edit the room's entry in the
|
2016-03-21 15:03:20 +01:00
|
|
|
published room list.
|
|
|
|
|
|
|
|
Args:
|
2020-02-21 13:18:33 +01:00
|
|
|
room_id
|
|
|
|
user
|
2016-03-21 15:03:20 +01:00
|
|
|
"""
|
|
|
|
|
2020-05-01 16:15:36 +02:00
|
|
|
is_admin = await self.is_server_admin(user)
|
2016-03-21 15:03:20 +01:00
|
|
|
if is_admin:
|
2019-07-23 15:00:55 +02:00
|
|
|
return True
|
2016-03-21 15:03:20 +01:00
|
|
|
|
|
|
|
user_id = user.to_string()
|
2020-05-01 16:15:36 +02:00
|
|
|
await self.check_user_in_room(room_id, user_id)
|
2016-03-21 15:03:20 +01:00
|
|
|
|
|
|
|
# We currently require the user is a "moderator" in the room. We do this
|
|
|
|
# by checking if they would (theoretically) be able to change the
|
2020-02-21 13:18:33 +01:00
|
|
|
# m.room.canonical_alias events
|
2020-05-01 16:15:36 +02:00
|
|
|
power_level_event = await self.state.get_current_state(
|
2016-03-21 15:03:20 +01:00
|
|
|
room_id, EventTypes.PowerLevels, ""
|
|
|
|
)
|
|
|
|
|
|
|
|
auth_events = {}
|
|
|
|
if power_level_event:
|
|
|
|
auth_events[(EventTypes.PowerLevels, "")] = power_level_event
|
|
|
|
|
2017-01-13 16:07:32 +01:00
|
|
|
send_level = event_auth.get_send_level(
|
2020-02-21 13:18:33 +01:00
|
|
|
EventTypes.CanonicalAlias, "", power_level_event
|
2016-03-21 15:03:20 +01:00
|
|
|
)
|
2017-01-13 16:07:32 +01:00
|
|
|
user_level = event_auth.get_user_power_level(user_id, auth_events)
|
2016-03-21 15:03:20 +01:00
|
|
|
|
2020-03-04 17:30:46 +01:00
|
|
|
return user_level >= send_level
|
2016-09-09 17:29:10 +02:00
|
|
|
|
2018-07-13 23:34:49 +02:00
|
|
|
@staticmethod
|
2020-05-14 17:32:49 +02:00
|
|
|
def has_access_token(request: Request):
|
2018-07-13 23:34:49 +02:00
|
|
|
"""Checks if the request has an access_token.
|
2016-09-09 17:29:10 +02:00
|
|
|
|
2018-07-13 23:34:49 +02:00
|
|
|
Returns:
|
|
|
|
bool: False if no access_token was given, True otherwise.
|
|
|
|
"""
|
2018-08-20 15:54:49 +02:00
|
|
|
query_params = request.args.get(b"access_token")
|
2018-07-13 23:34:49 +02:00
|
|
|
auth_headers = request.requestHeaders.getRawHeaders(b"Authorization")
|
|
|
|
return bool(query_params) or bool(auth_headers)
|
2016-09-09 17:29:10 +02:00
|
|
|
|
2018-07-13 23:34:49 +02:00
|
|
|
@staticmethod
|
2020-05-14 17:32:49 +02:00
|
|
|
def get_access_token_from_request(request: Request):
|
2018-07-13 23:34:49 +02:00
|
|
|
"""Extracts the access_token from the request.
|
2016-09-09 19:17:42 +02:00
|
|
|
|
2018-07-13 23:34:49 +02:00
|
|
|
Args:
|
|
|
|
request: The http request.
|
|
|
|
Returns:
|
2018-08-20 15:54:49 +02:00
|
|
|
unicode: The access_token
|
2018-07-13 23:34:49 +02:00
|
|
|
Raises:
|
2019-07-11 12:06:23 +02:00
|
|
|
MissingClientTokenError: If there isn't a single access_token in the
|
|
|
|
request
|
2018-07-13 23:34:49 +02:00
|
|
|
"""
|
|
|
|
|
|
|
|
auth_headers = request.requestHeaders.getRawHeaders(b"Authorization")
|
|
|
|
query_params = request.args.get(b"access_token")
|
|
|
|
if auth_headers:
|
|
|
|
# Try the get the access_token from a "Authorization: Bearer"
|
|
|
|
# header
|
|
|
|
if query_params is not None:
|
2019-07-11 12:06:23 +02:00
|
|
|
raise MissingClientTokenError(
|
|
|
|
"Mixing Authorization headers and access_token query parameters."
|
2018-07-13 23:34:49 +02:00
|
|
|
)
|
|
|
|
if len(auth_headers) > 1:
|
2019-07-11 12:06:23 +02:00
|
|
|
raise MissingClientTokenError("Too many Authorization headers.")
|
2018-08-20 15:54:49 +02:00
|
|
|
parts = auth_headers[0].split(b" ")
|
|
|
|
if parts[0] == b"Bearer" and len(parts) == 2:
|
2019-06-20 11:32:02 +02:00
|
|
|
return parts[1].decode("ascii")
|
2018-07-13 23:34:49 +02:00
|
|
|
else:
|
2019-07-11 12:06:23 +02:00
|
|
|
raise MissingClientTokenError("Invalid Authorization header.")
|
2016-09-09 19:17:42 +02:00
|
|
|
else:
|
2018-07-13 23:34:49 +02:00
|
|
|
# Try to get the access_token from the query params.
|
|
|
|
if not query_params:
|
2019-07-11 12:06:23 +02:00
|
|
|
raise MissingClientTokenError()
|
2016-09-09 17:29:10 +02:00
|
|
|
|
2019-06-20 11:32:02 +02:00
|
|
|
return query_params[0].decode("ascii")
|
2018-07-20 16:30:59 +02:00
|
|
|
|
2020-08-06 14:30:06 +02:00
|
|
|
async def check_user_in_room_or_world_readable(
|
2020-02-19 00:14:57 +01:00
|
|
|
self, room_id: str, user_id: str, allow_departed_users: bool = False
|
2020-08-06 14:30:06 +02:00
|
|
|
) -> Tuple[str, Optional[str]]:
|
2018-07-20 16:30:59 +02:00
|
|
|
"""Checks that the user is or was in the room or the room is world
|
|
|
|
readable. If it isn't then an exception is raised.
|
|
|
|
|
2020-02-19 00:14:57 +01:00
|
|
|
Args:
|
|
|
|
room_id: room to check
|
|
|
|
user_id: user to check
|
|
|
|
allow_departed_users: if True, accept users that were previously
|
|
|
|
members but have now departed
|
|
|
|
|
2018-07-20 16:30:59 +02:00
|
|
|
Returns:
|
2020-08-06 14:30:06 +02:00
|
|
|
Resolves to the current membership of the user in the room and the
|
|
|
|
membership event ID of the user. If the user is not in the room and
|
|
|
|
never has been, then `(Membership.JOIN, None)` is returned.
|
2018-07-20 16:30:59 +02:00
|
|
|
"""
|
|
|
|
|
|
|
|
try:
|
2020-02-19 00:13:29 +01:00
|
|
|
# check_user_in_room will return the most recent membership
|
2018-07-20 16:30:59 +02:00
|
|
|
# event for the user if:
|
|
|
|
# * The user is a non-guest user, and was ever in the room
|
|
|
|
# * The user is a guest user, and has joined the room
|
|
|
|
# else it will throw.
|
2020-08-06 14:30:06 +02:00
|
|
|
member_event = await self.check_user_in_room(
|
2020-02-19 00:14:57 +01:00
|
|
|
room_id, user_id, allow_departed_users=allow_departed_users
|
2020-02-19 00:13:29 +01:00
|
|
|
)
|
2019-08-30 17:28:26 +02:00
|
|
|
return member_event.membership, member_event.event_id
|
2018-07-20 16:30:59 +02:00
|
|
|
except AuthError:
|
2020-08-06 14:30:06 +02:00
|
|
|
visibility = await self.state.get_current_state(
|
|
|
|
room_id, EventTypes.RoomHistoryVisibility, ""
|
2018-07-20 16:30:59 +02:00
|
|
|
)
|
|
|
|
if (
|
2019-06-20 11:32:02 +02:00
|
|
|
visibility
|
|
|
|
and visibility.content["history_visibility"] == "world_readable"
|
2018-07-20 16:30:59 +02:00
|
|
|
):
|
2019-08-30 17:28:26 +02:00
|
|
|
return Membership.JOIN, None
|
2018-07-20 16:30:59 +02:00
|
|
|
raise AuthError(
|
2020-02-19 00:14:57 +01:00
|
|
|
403,
|
|
|
|
"User %s not in room %s, and room previews are disabled"
|
|
|
|
% (user_id, room_id),
|
2018-07-20 16:30:59 +02:00
|
|
|
)
|
2018-08-02 17:57:35 +02:00
|
|
|
|
2020-05-06 16:54:58 +02:00
|
|
|
def check_auth_blocking(self, *args, **kwargs):
|
|
|
|
return self._auth_blocking.check_auth_blocking(*args, **kwargs)
|