mirror of
https://github.com/element-hq/synapse.git
synced 2024-11-22 01:25:44 +03:00
Make the api.auth.Auth a Protocol
This commit is contained in:
parent
5d8c659373
commit
e2c8458bba
7 changed files with 464 additions and 248 deletions
175
synapse/api/auth/__init__.py
Normal file
175
synapse/api/auth/__init__.py
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
# Copyright 2023 The Matrix.org Foundation.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
from typing_extensions import Protocol
|
||||||
|
|
||||||
|
from twisted.web.server import Request
|
||||||
|
|
||||||
|
from synapse.appservice import ApplicationService
|
||||||
|
from synapse.http.site import SynapseRequest
|
||||||
|
from synapse.types import Requester
|
||||||
|
|
||||||
|
# guests always get this device id.
|
||||||
|
GUEST_DEVICE_ID = "guest_device"
|
||||||
|
|
||||||
|
|
||||||
|
class Auth(Protocol):
|
||||||
|
"""The interface that an auth provider must implement."""
|
||||||
|
|
||||||
|
async def check_user_in_room(
|
||||||
|
self,
|
||||||
|
room_id: str,
|
||||||
|
requester: Requester,
|
||||||
|
allow_departed_users: bool = False,
|
||||||
|
) -> Tuple[str, Optional[str]]:
|
||||||
|
"""Check if the user is in the room, or was at some point.
|
||||||
|
Args:
|
||||||
|
room_id: The room to check.
|
||||||
|
|
||||||
|
user_id: The user to check.
|
||||||
|
|
||||||
|
current_state: Optional map of the current state of the room.
|
||||||
|
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.
|
||||||
|
|
||||||
|
allow_departed_users: if True, accept users that were previously
|
||||||
|
members but have now departed.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AuthError if the user is/was not in the room.
|
||||||
|
Returns:
|
||||||
|
The current membership of the user in the room and the
|
||||||
|
membership event ID of the user.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def get_user_by_req(
|
||||||
|
self,
|
||||||
|
request: SynapseRequest,
|
||||||
|
allow_guest: bool = False,
|
||||||
|
allow_expired: bool = False,
|
||||||
|
) -> Requester:
|
||||||
|
"""Get a registered user's ID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Resolves to the requester
|
||||||
|
Raises:
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def validate_appservice_can_control_user_id(
|
||||||
|
self, app_service: ApplicationService, user_id: str
|
||||||
|
) -> None:
|
||||||
|
"""Validates that the app service is allowed to control
|
||||||
|
the given user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app_service: The app service that controls the user
|
||||||
|
user_id: The author MXID that the app service is controlling
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AuthError: If the application service is not allowed to control the user
|
||||||
|
(user namespace regex does not match, wrong homeserver, etc)
|
||||||
|
or if the user has not been registered yet.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def get_user_by_access_token(
|
||||||
|
self,
|
||||||
|
token: str,
|
||||||
|
allow_expired: bool = False,
|
||||||
|
) -> Requester:
|
||||||
|
"""Validate access token and get user_id from it
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token: The access token to get the user by
|
||||||
|
allow_expired: If False, raises an InvalidClientTokenError
|
||||||
|
if the token is expired
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
InvalidClientTokenError if a user by that token exists, but the token is
|
||||||
|
expired
|
||||||
|
InvalidClientCredentialsError if no user by that token exists or the token
|
||||||
|
is invalid
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def is_server_admin(self, requester: Requester) -> bool:
|
||||||
|
"""Check if the given user is a local server admin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
requester: user to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the user is an admin
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def check_can_change_room_list(
|
||||||
|
self, room_id: str, requester: Requester
|
||||||
|
) -> bool:
|
||||||
|
"""Determine whether the user is allowed to edit the room's entry in the
|
||||||
|
published room list.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room_id
|
||||||
|
user
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def has_access_token(request: Request) -> bool:
|
||||||
|
"""Checks if the request has an access_token.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
False if no access_token was given, True otherwise.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_access_token_from_request(request: Request) -> str:
|
||||||
|
"""Extracts the access_token from the request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: The http request.
|
||||||
|
Returns:
|
||||||
|
The access_token
|
||||||
|
Raises:
|
||||||
|
MissingClientTokenError: If there isn't a single access_token in the
|
||||||
|
request
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def check_user_in_room_or_world_readable(
|
||||||
|
self, room_id: str, requester: Requester, allow_departed_users: bool = False
|
||||||
|
) -> Tuple[str, Optional[str]]:
|
||||||
|
"""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.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
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.
|
||||||
|
"""
|
273
synapse/api/auth/base.py
Normal file
273
synapse/api/auth/base.py
Normal file
|
@ -0,0 +1,273 @@
|
||||||
|
# Copyright 2023 The Matrix.org Foundation.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
import logging
|
||||||
|
from typing import TYPE_CHECKING, Optional, Tuple
|
||||||
|
|
||||||
|
from twisted.web.server import Request
|
||||||
|
|
||||||
|
from synapse import event_auth
|
||||||
|
from synapse.api.constants import EventTypes, HistoryVisibility, Membership
|
||||||
|
from synapse.api.errors import (
|
||||||
|
AuthError,
|
||||||
|
Codes,
|
||||||
|
MissingClientTokenError,
|
||||||
|
UnstableSpecAuthError,
|
||||||
|
)
|
||||||
|
from synapse.appservice import ApplicationService
|
||||||
|
from synapse.logging.opentracing import trace
|
||||||
|
from synapse.types import Requester
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from synapse.server import HomeServer
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseAuth:
|
||||||
|
"""Common base class for all auth implementations."""
|
||||||
|
|
||||||
|
def __init__(self, hs: "HomeServer"):
|
||||||
|
self.hs = hs
|
||||||
|
self.store = hs.get_datastores().main
|
||||||
|
self._storage_controllers = hs.get_storage_controllers()
|
||||||
|
|
||||||
|
async def check_user_in_room(
|
||||||
|
self,
|
||||||
|
room_id: str,
|
||||||
|
requester: Requester,
|
||||||
|
allow_departed_users: bool = False,
|
||||||
|
) -> Tuple[str, Optional[str]]:
|
||||||
|
"""Check if the user is in the room, or was at some point.
|
||||||
|
Args:
|
||||||
|
room_id: The room to check.
|
||||||
|
|
||||||
|
requester: The user making the request, according to the access token.
|
||||||
|
|
||||||
|
current_state: Optional map of the current state of the room.
|
||||||
|
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.
|
||||||
|
|
||||||
|
allow_departed_users: if True, accept users that were previously
|
||||||
|
members but have now departed.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AuthError if the user is/was not in the room.
|
||||||
|
Returns:
|
||||||
|
The current membership of the user in the room and the
|
||||||
|
membership event ID of the user.
|
||||||
|
"""
|
||||||
|
|
||||||
|
user_id = requester.user.to_string()
|
||||||
|
(
|
||||||
|
membership,
|
||||||
|
member_event_id,
|
||||||
|
) = await self.store.get_local_current_membership_for_user_in_room(
|
||||||
|
user_id=user_id,
|
||||||
|
room_id=room_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if membership:
|
||||||
|
if membership == Membership.JOIN:
|
||||||
|
return membership, member_event_id
|
||||||
|
|
||||||
|
# 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 membership, member_event_id
|
||||||
|
raise UnstableSpecAuthError(
|
||||||
|
403,
|
||||||
|
"User %s not in room %s" % (user_id, room_id),
|
||||||
|
errcode=Codes.NOT_JOINED,
|
||||||
|
)
|
||||||
|
|
||||||
|
@trace
|
||||||
|
async def check_user_in_room_or_world_readable(
|
||||||
|
self, room_id: str, requester: Requester, allow_departed_users: bool = False
|
||||||
|
) -> Tuple[str, Optional[str]]:
|
||||||
|
"""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.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
# check_user_in_room will return the most recent membership
|
||||||
|
# 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.
|
||||||
|
return await self.check_user_in_room(
|
||||||
|
room_id, requester, allow_departed_users=allow_departed_users
|
||||||
|
)
|
||||||
|
except AuthError:
|
||||||
|
visibility = await self._storage_controllers.state.get_current_state_event(
|
||||||
|
room_id, EventTypes.RoomHistoryVisibility, ""
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
visibility
|
||||||
|
and visibility.content.get("history_visibility")
|
||||||
|
== HistoryVisibility.WORLD_READABLE
|
||||||
|
):
|
||||||
|
return Membership.JOIN, None
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"User %r not in room %s, and room previews are disabled"
|
||||||
|
% (requester.user, room_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def validate_appservice_can_control_user_id(
|
||||||
|
self, app_service: ApplicationService, user_id: str
|
||||||
|
) -> None:
|
||||||
|
"""Validates that the app service is allowed to control
|
||||||
|
the given user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app_service: The app service that controls the user
|
||||||
|
user_id: The author MXID that the app service is controlling
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AuthError: If the application service is not allowed to control the user
|
||||||
|
(user namespace regex does not match, wrong homeserver, etc)
|
||||||
|
or if the user has not been registered yet.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# It's ok if the app service is trying to use the sender from their registration
|
||||||
|
if app_service.sender == user_id:
|
||||||
|
pass
|
||||||
|
# Check to make sure the app service is allowed to control the user
|
||||||
|
elif not app_service.is_interested_in_user(user_id):
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"Application service cannot masquerade as this user (%s)." % user_id,
|
||||||
|
)
|
||||||
|
# Check to make sure the user is already registered on the homeserver
|
||||||
|
elif not (await self.store.get_user_by_id(user_id)):
|
||||||
|
raise AuthError(
|
||||||
|
403, "Application service has not registered this user (%s)" % user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
async def is_server_admin(self, requester: Requester) -> bool:
|
||||||
|
"""Check if the given user is a local server admin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
requester: user to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the user is an admin
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
async def check_can_change_room_list(
|
||||||
|
self, room_id: str, requester: Requester
|
||||||
|
) -> bool:
|
||||||
|
"""Determine whether the user is allowed to edit the room's entry in the
|
||||||
|
published room list.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room_id
|
||||||
|
user
|
||||||
|
"""
|
||||||
|
|
||||||
|
is_admin = await self.is_server_admin(requester)
|
||||||
|
if is_admin:
|
||||||
|
return True
|
||||||
|
|
||||||
|
await self.check_user_in_room(room_id, requester)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# m.room.canonical_alias events
|
||||||
|
|
||||||
|
power_level_event = (
|
||||||
|
await self._storage_controllers.state.get_current_state_event(
|
||||||
|
room_id, EventTypes.PowerLevels, ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
auth_events = {}
|
||||||
|
if power_level_event:
|
||||||
|
auth_events[(EventTypes.PowerLevels, "")] = power_level_event
|
||||||
|
|
||||||
|
send_level = event_auth.get_send_level(
|
||||||
|
EventTypes.CanonicalAlias, "", power_level_event
|
||||||
|
)
|
||||||
|
user_level = event_auth.get_user_power_level(
|
||||||
|
requester.user.to_string(), auth_events
|
||||||
|
)
|
||||||
|
|
||||||
|
return user_level >= send_level
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def has_access_token(request: Request) -> bool:
|
||||||
|
"""Checks if the request has an access_token.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
False if no access_token was given, True otherwise.
|
||||||
|
"""
|
||||||
|
# This will always be set by the time Twisted calls us.
|
||||||
|
assert request.args is not None
|
||||||
|
|
||||||
|
query_params = request.args.get(b"access_token")
|
||||||
|
auth_headers = request.requestHeaders.getRawHeaders(b"Authorization")
|
||||||
|
return bool(query_params) or bool(auth_headers)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_access_token_from_request(request: Request) -> str:
|
||||||
|
"""Extracts the access_token from the request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: The http request.
|
||||||
|
Returns:
|
||||||
|
The access_token
|
||||||
|
Raises:
|
||||||
|
MissingClientTokenError: If there isn't a single access_token in the
|
||||||
|
request
|
||||||
|
"""
|
||||||
|
# This will always be set by the time Twisted calls us.
|
||||||
|
assert request.args is not None
|
||||||
|
|
||||||
|
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:
|
||||||
|
raise MissingClientTokenError(
|
||||||
|
"Mixing Authorization headers and access_token query parameters."
|
||||||
|
)
|
||||||
|
if len(auth_headers) > 1:
|
||||||
|
raise MissingClientTokenError("Too many Authorization headers.")
|
||||||
|
parts = auth_headers[0].split(b" ")
|
||||||
|
if parts[0] == b"Bearer" and len(parts) == 2:
|
||||||
|
return parts[1].decode("ascii")
|
||||||
|
else:
|
||||||
|
raise MissingClientTokenError("Invalid Authorization header.")
|
||||||
|
else:
|
||||||
|
# Try to get the access_token from the query params.
|
||||||
|
if not query_params:
|
||||||
|
raise MissingClientTokenError()
|
||||||
|
|
||||||
|
return query_params[0].decode("ascii")
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright 2014 - 2016 OpenMarket Ltd
|
# Copyright 2023 The Matrix.org Foundation.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
@ -12,113 +12,49 @@
|
||||||
# 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 logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Optional, Tuple
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
import pymacaroons
|
import pymacaroons
|
||||||
from netaddr import IPAddress
|
from netaddr import IPAddress
|
||||||
|
|
||||||
from twisted.web.server import Request
|
from twisted.web.server import Request
|
||||||
|
|
||||||
from synapse import event_auth
|
|
||||||
from synapse.api.constants import EventTypes, HistoryVisibility, Membership
|
|
||||||
from synapse.api.errors import (
|
from synapse.api.errors import (
|
||||||
AuthError,
|
AuthError,
|
||||||
Codes,
|
Codes,
|
||||||
InvalidClientTokenError,
|
InvalidClientTokenError,
|
||||||
MissingClientTokenError,
|
MissingClientTokenError,
|
||||||
UnstableSpecAuthError,
|
|
||||||
)
|
)
|
||||||
from synapse.appservice import ApplicationService
|
|
||||||
from synapse.http import get_request_user_agent
|
from synapse.http import get_request_user_agent
|
||||||
from synapse.http.site import SynapseRequest
|
from synapse.http.site import SynapseRequest
|
||||||
from synapse.logging.opentracing import (
|
from synapse.logging.opentracing import active_span, force_tracing, start_active_span
|
||||||
active_span,
|
|
||||||
force_tracing,
|
|
||||||
start_active_span,
|
|
||||||
trace,
|
|
||||||
)
|
|
||||||
from synapse.types import Requester, create_requester
|
from synapse.types import Requester, create_requester
|
||||||
from synapse.util.cancellation import cancellable
|
from synapse.util.cancellation import cancellable
|
||||||
|
|
||||||
|
from . import GUEST_DEVICE_ID
|
||||||
|
from .base import BaseAuth
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from synapse.server import HomeServer
|
from synapse.server import HomeServer
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# guests always get this device id.
|
class InternalAuth(BaseAuth):
|
||||||
GUEST_DEVICE_ID = "guest_device"
|
|
||||||
|
|
||||||
|
|
||||||
class Auth:
|
|
||||||
"""
|
"""
|
||||||
This class contains functions for authenticating users of our client-server API.
|
This class contains functions for authenticating users of our client-server API.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, hs: "HomeServer"):
|
def __init__(self, hs: "HomeServer"):
|
||||||
self.hs = hs
|
super().__init__(hs)
|
||||||
self.clock = hs.get_clock()
|
self.clock = hs.get_clock()
|
||||||
self.store = hs.get_datastores().main
|
|
||||||
self._account_validity_handler = hs.get_account_validity_handler()
|
self._account_validity_handler = hs.get_account_validity_handler()
|
||||||
self._storage_controllers = hs.get_storage_controllers()
|
|
||||||
self._macaroon_generator = hs.get_macaroon_generator()
|
self._macaroon_generator = hs.get_macaroon_generator()
|
||||||
|
|
||||||
self._track_appservice_user_ips = hs.config.appservice.track_appservice_user_ips
|
self._track_appservice_user_ips = hs.config.appservice.track_appservice_user_ips
|
||||||
self._track_puppeted_user_ips = hs.config.api.track_puppeted_user_ips
|
self._track_puppeted_user_ips = hs.config.api.track_puppeted_user_ips
|
||||||
self._force_tracing_for_users = hs.config.tracing.force_tracing_for_users
|
self._force_tracing_for_users = hs.config.tracing.force_tracing_for_users
|
||||||
|
|
||||||
async def check_user_in_room(
|
|
||||||
self,
|
|
||||||
room_id: str,
|
|
||||||
requester: Requester,
|
|
||||||
allow_departed_users: bool = False,
|
|
||||||
) -> Tuple[str, Optional[str]]:
|
|
||||||
"""Check if the user is in the room, or was at some point.
|
|
||||||
Args:
|
|
||||||
room_id: The room to check.
|
|
||||||
|
|
||||||
requester: The user making the request, according to the access token.
|
|
||||||
|
|
||||||
current_state: Optional map of the current state of the room.
|
|
||||||
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.
|
|
||||||
|
|
||||||
allow_departed_users: if True, accept users that were previously
|
|
||||||
members but have now departed.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
AuthError if the user is/was not in the room.
|
|
||||||
Returns:
|
|
||||||
The current membership of the user in the room and the
|
|
||||||
membership event ID of the user.
|
|
||||||
"""
|
|
||||||
|
|
||||||
user_id = requester.user.to_string()
|
|
||||||
(
|
|
||||||
membership,
|
|
||||||
member_event_id,
|
|
||||||
) = await self.store.get_local_current_membership_for_user_in_room(
|
|
||||||
user_id=user_id,
|
|
||||||
room_id=room_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
if membership:
|
|
||||||
if membership == Membership.JOIN:
|
|
||||||
return membership, member_event_id
|
|
||||||
|
|
||||||
# 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 membership, member_event_id
|
|
||||||
raise UnstableSpecAuthError(
|
|
||||||
403,
|
|
||||||
"User %s not in room %s" % (user_id, room_id),
|
|
||||||
errcode=Codes.NOT_JOINED,
|
|
||||||
)
|
|
||||||
|
|
||||||
@cancellable
|
@cancellable
|
||||||
async def get_user_by_req(
|
async def get_user_by_req(
|
||||||
self,
|
self,
|
||||||
|
@ -253,37 +189,6 @@ class Auth:
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise MissingClientTokenError()
|
raise MissingClientTokenError()
|
||||||
|
|
||||||
async def validate_appservice_can_control_user_id(
|
|
||||||
self, app_service: ApplicationService, user_id: str
|
|
||||||
) -> None:
|
|
||||||
"""Validates that the app service is allowed to control
|
|
||||||
the given user.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
app_service: The app service that controls the user
|
|
||||||
user_id: The author MXID that the app service is controlling
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
AuthError: If the application service is not allowed to control the user
|
|
||||||
(user namespace regex does not match, wrong homeserver, etc)
|
|
||||||
or if the user has not been registered yet.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# It's ok if the app service is trying to use the sender from their registration
|
|
||||||
if app_service.sender == user_id:
|
|
||||||
pass
|
|
||||||
# Check to make sure the app service is allowed to control the user
|
|
||||||
elif not app_service.is_interested_in_user(user_id):
|
|
||||||
raise AuthError(
|
|
||||||
403,
|
|
||||||
"Application service cannot masquerade as this user (%s)." % user_id,
|
|
||||||
)
|
|
||||||
# Check to make sure the user is already registered on the homeserver
|
|
||||||
elif not (await self.store.get_user_by_id(user_id)):
|
|
||||||
raise AuthError(
|
|
||||||
403, "Application service has not registered this user (%s)" % user_id
|
|
||||||
)
|
|
||||||
|
|
||||||
@cancellable
|
@cancellable
|
||||||
async def _get_appservice_user(self, request: Request) -> Optional[Requester]:
|
async def _get_appservice_user(self, request: Request) -> Optional[Requester]:
|
||||||
"""
|
"""
|
||||||
|
@ -462,141 +367,3 @@ class Auth:
|
||||||
True if the user is an admin
|
True if the user is an admin
|
||||||
"""
|
"""
|
||||||
return await self.store.is_server_admin(requester.user)
|
return await self.store.is_server_admin(requester.user)
|
||||||
|
|
||||||
async def check_can_change_room_list(
|
|
||||||
self, room_id: str, requester: Requester
|
|
||||||
) -> bool:
|
|
||||||
"""Determine whether the user is allowed to edit the room's entry in the
|
|
||||||
published room list.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
room_id: The room to check.
|
|
||||||
requester: The user making the request, according to the access token.
|
|
||||||
"""
|
|
||||||
|
|
||||||
is_admin = await self.is_server_admin(requester)
|
|
||||||
if is_admin:
|
|
||||||
return True
|
|
||||||
|
|
||||||
await self.check_user_in_room(room_id, requester)
|
|
||||||
|
|
||||||
# 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
|
|
||||||
# m.room.canonical_alias events
|
|
||||||
|
|
||||||
power_level_event = (
|
|
||||||
await self._storage_controllers.state.get_current_state_event(
|
|
||||||
room_id, EventTypes.PowerLevels, ""
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
auth_events = {}
|
|
||||||
if power_level_event:
|
|
||||||
auth_events[(EventTypes.PowerLevels, "")] = power_level_event
|
|
||||||
|
|
||||||
send_level = event_auth.get_send_level(
|
|
||||||
EventTypes.CanonicalAlias, "", power_level_event
|
|
||||||
)
|
|
||||||
user_level = event_auth.get_user_power_level(
|
|
||||||
requester.user.to_string(), auth_events
|
|
||||||
)
|
|
||||||
|
|
||||||
return user_level >= send_level
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def has_access_token(request: Request) -> bool:
|
|
||||||
"""Checks if the request has an access_token.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
False if no access_token was given, True otherwise.
|
|
||||||
"""
|
|
||||||
# This will always be set by the time Twisted calls us.
|
|
||||||
assert request.args is not None
|
|
||||||
|
|
||||||
query_params = request.args.get(b"access_token")
|
|
||||||
auth_headers = request.requestHeaders.getRawHeaders(b"Authorization")
|
|
||||||
return bool(query_params) or bool(auth_headers)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
@cancellable
|
|
||||||
def get_access_token_from_request(request: Request) -> str:
|
|
||||||
"""Extracts the access_token from the request.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request: The http request.
|
|
||||||
Returns:
|
|
||||||
The access_token
|
|
||||||
Raises:
|
|
||||||
MissingClientTokenError: If there isn't a single access_token in the
|
|
||||||
request
|
|
||||||
"""
|
|
||||||
# This will always be set by the time Twisted calls us.
|
|
||||||
assert request.args is not None
|
|
||||||
|
|
||||||
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:
|
|
||||||
raise MissingClientTokenError(
|
|
||||||
"Mixing Authorization headers and access_token query parameters."
|
|
||||||
)
|
|
||||||
if len(auth_headers) > 1:
|
|
||||||
raise MissingClientTokenError("Too many Authorization headers.")
|
|
||||||
parts = auth_headers[0].split(b" ")
|
|
||||||
if parts[0] == b"Bearer" and len(parts) == 2:
|
|
||||||
return parts[1].decode("ascii")
|
|
||||||
else:
|
|
||||||
raise MissingClientTokenError("Invalid Authorization header.")
|
|
||||||
else:
|
|
||||||
# Try to get the access_token from the query params.
|
|
||||||
if not query_params:
|
|
||||||
raise MissingClientTokenError()
|
|
||||||
|
|
||||||
return query_params[0].decode("ascii")
|
|
||||||
|
|
||||||
@trace
|
|
||||||
async def check_user_in_room_or_world_readable(
|
|
||||||
self, room_id: str, requester: Requester, allow_departed_users: bool = False
|
|
||||||
) -> Tuple[str, Optional[str]]:
|
|
||||||
"""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.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
room_id: The room to check.
|
|
||||||
requester: The user making the request, according to the access token.
|
|
||||||
allow_departed_users: If True, accept users that were previously
|
|
||||||
members but have now departed.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
# check_user_in_room will return the most recent membership
|
|
||||||
# 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.
|
|
||||||
return await self.check_user_in_room(
|
|
||||||
room_id, requester, allow_departed_users=allow_departed_users
|
|
||||||
)
|
|
||||||
except AuthError:
|
|
||||||
visibility = await self._storage_controllers.state.get_current_state_event(
|
|
||||||
room_id, EventTypes.RoomHistoryVisibility, ""
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
visibility
|
|
||||||
and visibility.content.get("history_visibility")
|
|
||||||
== HistoryVisibility.WORLD_READABLE
|
|
||||||
):
|
|
||||||
return Membership.JOIN, None
|
|
||||||
raise UnstableSpecAuthError(
|
|
||||||
403,
|
|
||||||
"User %s not in room %s, and room previews are disabled"
|
|
||||||
% (requester.user, room_id),
|
|
||||||
errcode=Codes.NOT_JOINED,
|
|
||||||
)
|
|
|
@ -31,6 +31,7 @@ from twisted.web.iweb import IPolicyForHTTPS
|
||||||
from twisted.web.resource import Resource
|
from twisted.web.resource import Resource
|
||||||
|
|
||||||
from synapse.api.auth import Auth
|
from synapse.api.auth import Auth
|
||||||
|
from synapse.api.auth.internal import InternalAuth
|
||||||
from synapse.api.auth_blocking import AuthBlocking
|
from synapse.api.auth_blocking import AuthBlocking
|
||||||
from synapse.api.filtering import Filtering
|
from synapse.api.filtering import Filtering
|
||||||
from synapse.api.ratelimiting import Ratelimiter, RequestRatelimiter
|
from synapse.api.ratelimiting import Ratelimiter, RequestRatelimiter
|
||||||
|
@ -427,7 +428,7 @@ class HomeServer(metaclass=abc.ABCMeta):
|
||||||
|
|
||||||
@cache_in_self
|
@cache_in_self
|
||||||
def get_auth(self) -> Auth:
|
def get_auth(self) -> Auth:
|
||||||
return Auth(self)
|
return InternalAuth(self)
|
||||||
|
|
||||||
@cache_in_self
|
@cache_in_self
|
||||||
def get_auth_blocking(self) -> AuthBlocking:
|
def get_auth_blocking(self) -> AuthBlocking:
|
||||||
|
|
|
@ -18,7 +18,7 @@ import pymacaroons
|
||||||
|
|
||||||
from twisted.test.proto_helpers import MemoryReactor
|
from twisted.test.proto_helpers import MemoryReactor
|
||||||
|
|
||||||
from synapse.api.auth import Auth
|
from synapse.api.auth.internal import InternalAuth
|
||||||
from synapse.api.auth_blocking import AuthBlocking
|
from synapse.api.auth_blocking import AuthBlocking
|
||||||
from synapse.api.constants import UserTypes
|
from synapse.api.constants import UserTypes
|
||||||
from synapse.api.errors import (
|
from synapse.api.errors import (
|
||||||
|
@ -48,7 +48,7 @@ class AuthTestCase(unittest.HomeserverTestCase):
|
||||||
# have been called by the HomeserverTestCase machinery.
|
# have been called by the HomeserverTestCase machinery.
|
||||||
hs.datastores.main = self.store # type: ignore[union-attr]
|
hs.datastores.main = self.store # type: ignore[union-attr]
|
||||||
hs.get_auth_handler().store = self.store
|
hs.get_auth_handler().store = self.store
|
||||||
self.auth = Auth(hs)
|
self.auth = InternalAuth(hs)
|
||||||
|
|
||||||
# AuthBlocking reads from the hs' config on initialization. We need to
|
# AuthBlocking reads from the hs' config on initialization. We need to
|
||||||
# modify its config instead of the hs'
|
# modify its config instead of the hs'
|
||||||
|
|
|
@ -17,7 +17,7 @@ from unittest.mock import Mock
|
||||||
|
|
||||||
from twisted.test.proto_helpers import MemoryReactor
|
from twisted.test.proto_helpers import MemoryReactor
|
||||||
|
|
||||||
from synapse.api.auth import Auth
|
from synapse.api.auth.internal import InternalAuth
|
||||||
from synapse.api.constants import UserTypes
|
from synapse.api.constants import UserTypes
|
||||||
from synapse.api.errors import (
|
from synapse.api.errors import (
|
||||||
CodeMessageException,
|
CodeMessageException,
|
||||||
|
@ -683,7 +683,7 @@ class RegistrationTestCase(unittest.HomeserverTestCase):
|
||||||
request = Mock(args={})
|
request = Mock(args={})
|
||||||
request.args[b"access_token"] = [token.encode("ascii")]
|
request.args[b"access_token"] = [token.encode("ascii")]
|
||||||
request.requestHeaders.getRawHeaders = mock_getRawHeaders()
|
request.requestHeaders.getRawHeaders = mock_getRawHeaders()
|
||||||
auth = Auth(self.hs)
|
auth = InternalAuth(self.hs)
|
||||||
requester = self.get_success(auth.get_user_by_req(request))
|
requester = self.get_success(auth.get_user_by_req(request))
|
||||||
|
|
||||||
self.assertTrue(requester.shadow_banned)
|
self.assertTrue(requester.shadow_banned)
|
||||||
|
|
|
@ -28,7 +28,7 @@ from unittest.mock import Mock
|
||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.api.auth import Auth
|
from synapse.api.auth.internal import InternalAuth
|
||||||
from synapse.api.constants import EventTypes, Membership
|
from synapse.api.constants import EventTypes, Membership
|
||||||
from synapse.api.room_versions import RoomVersions
|
from synapse.api.room_versions import RoomVersions
|
||||||
from synapse.events import EventBase, make_event_from_dict
|
from synapse.events import EventBase, make_event_from_dict
|
||||||
|
@ -240,7 +240,7 @@ class StateTestCase(unittest.TestCase):
|
||||||
hs.get_macaroon_generator.return_value = MacaroonGenerator(
|
hs.get_macaroon_generator.return_value = MacaroonGenerator(
|
||||||
clock, "tesths", b"verysecret"
|
clock, "tesths", b"verysecret"
|
||||||
)
|
)
|
||||||
hs.get_auth.return_value = Auth(hs)
|
hs.get_auth.return_value = InternalAuth(hs)
|
||||||
hs.get_state_resolution_handler = lambda: StateResolutionHandler(hs)
|
hs.get_state_resolution_handler = lambda: StateResolutionHandler(hs)
|
||||||
hs.get_storage_controllers.return_value = storage_controllers
|
hs.get_storage_controllers.return_value = storage_controllers
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue