Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Add ability for access tokens to belong to one user but grant access to another user. #8616

Merged
merged 14 commits into from
Oct 29, 2020
Merged
1 change: 1 addition & 0 deletions changelog.d/8616.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Change schema to support access tokens belonging to one user but granting access to another.
110 changes: 44 additions & 66 deletions synapse/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS
from synapse.events import EventBase
from synapse.logging import opentracing as opentracing
from synapse.storage.databases.main.registration import TokenLookupResult
from synapse.types import StateMap, UserID
from synapse.util.caches.lrucache import LruCache
from synapse.util.metrics import Measure
Expand Down Expand Up @@ -192,10 +193,6 @@ async def get_user_by_req(

user_id, app_service = await self._get_appservice_user_id(request)
if user_id:
request.authenticated_entity = user_id
opentracing.set_tag("authenticated_entity", user_id)
opentracing.set_tag("appservice_id", app_service.id)

if ip_addr and self._track_appservice_user_ips:
await self.store.insert_client_ip(
user_id=user_id,
Expand All @@ -205,31 +202,39 @@ async def get_user_by_req(
device_id="dummy-device", # stubbed
)

return synapse.types.create_requester(user_id, app_service=app_service)
requester = synapse.types.create_requester(
user_id, app_service=app_service
)

request.requester = user_id
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implies some potential clean-up if each request has a requester on it... (I think we frequently pass the SynapseRequest and Requester into handlers). Nothing to really do here though. Just noting.

opentracing.set_tag("authenticated_entity", user_id)
opentracing.set_tag("user_id", user_id)
opentracing.set_tag("appservice_id", app_service.id)

return requester

user_info = await self.get_user_by_access_token(
access_token, rights, allow_expired=allow_expired
)
user = user_info["user"]
token_id = user_info["token_id"]
is_guest = user_info["is_guest"]
shadow_banned = user_info["shadow_banned"]
user = UserID.from_string(user_info.user_id)
clokep marked this conversation as resolved.
Show resolved Hide resolved
token_id = user_info.token_id
is_guest = user_info.is_guest
shadow_banned = user_info.shadow_banned

# Deny the request if the user account has expired.
if self._account_validity.enabled and not allow_expired:
user_id = user.to_string()
if await self.store.is_account_expired(user_id, self.clock.time_msec()):
if await self.store.is_account_expired(
user_info.user_id, self.clock.time_msec()
):
raise AuthError(
403, "User account has expired", errcode=Codes.EXPIRED_ACCOUNT
)

# device_id may not be present if get_user_by_access_token has been
# stubbed out.
device_id = user_info.get("device_id")
device_id = user_info.device_id

if user and access_token and ip_addr:
if access_token and ip_addr:
await self.store.insert_client_ip(
user_id=user.to_string(),
user_id=user_info.token_owner,
access_token=access_token,
ip=ip_addr,
user_agent=user_agent,
Expand All @@ -243,19 +248,23 @@ async def get_user_by_req(
errcode=Codes.GUEST_ACCESS_FORBIDDEN,
)

request.authenticated_entity = user.to_string()
opentracing.set_tag("authenticated_entity", user.to_string())
if device_id:
opentracing.set_tag("device_id", device_id)

return synapse.types.create_requester(
requester = synapse.types.create_requester(
user,
token_id,
is_guest,
shadow_banned,
device_id,
app_service=app_service,
authenticated_entity=user_info.token_owner,
)

request.requester = requester
opentracing.set_tag("authenticated_entity", user_info.token_owner)
opentracing.set_tag("user_id", user_info.user_id)
if device_id:
opentracing.set_tag("device_id", device_id)

return requester
except KeyError:
raise MissingClientTokenError()

Expand Down Expand Up @@ -286,7 +295,7 @@ async def _get_appservice_user_id(self, request):

async def get_user_by_access_token(
self, token: str, rights: str = "access", allow_expired: bool = False,
) -> dict:
) -> TokenLookupResult:
""" Validate access token and get user_id from it

Args:
Expand All @@ -295,13 +304,7 @@ async def get_user_by_access_token(
allow this
allow_expired: If False, raises an InvalidClientTokenError
if the token is expired
Returns:
dict that includes:
`user` (UserID)
`is_guest` (bool)
`shadow_banned` (bool)
`token_id` (int|None): access token id. May be None if guest
`device_id` (str|None): device corresponding to access token

Raises:
InvalidClientTokenError if a user by that token exists, but the token is
expired
Expand All @@ -311,9 +314,9 @@ async def get_user_by_access_token(

if rights == "access":
# first look in the database
r = await self._look_up_user_by_access_token(token)
r = await self.store.get_user_by_access_token(token)
if r:
valid_until_ms = r["valid_until_ms"]
valid_until_ms = r.valid_until_ms
if (
not allow_expired
and valid_until_ms is not None
Expand All @@ -330,7 +333,6 @@ async def get_user_by_access_token(
# otherwise it needs to be a valid macaroon
try:
user_id, guest = self._parse_and_validate_macaroon(token, rights)
user = UserID.from_string(user_id)

if rights == "access":
if not guest:
Expand All @@ -356,23 +358,17 @@ async def get_user_by_access_token(
raise InvalidClientTokenError(
"Guest access token used for regular user"
)
ret = {
"user": user,
"is_guest": True,
"shadow_banned": False,
"token_id": None,

ret = TokenLookupResult(
user_id=user_id,
is_guest=True,
# all guests get the same device id
"device_id": GUEST_DEVICE_ID,
}
device_id=GUEST_DEVICE_ID,
)
elif rights == "delete_pusher":
# We don't store these tokens in the database
ret = {
"user": user,
"is_guest": False,
"shadow_banned": False,
"token_id": None,
"device_id": None,
}

ret = TokenLookupResult(user_id=user_id, is_guest=False)
else:
raise RuntimeError("Unknown rights setting %s", rights)
return ret
Expand Down Expand Up @@ -481,31 +477,13 @@ def _verify_expiry(self, caveat):
now = self.hs.get_clock().time_msec()
return now < expiry

async def _look_up_user_by_access_token(self, token):
ret = await self.store.get_user_by_access_token(token)
if not ret:
return None

# 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.
user_info = {
"user": UserID.from_string(ret.get("name")),
"token_id": ret.get("token_id", None),
"is_guest": False,
"shadow_banned": ret.get("shadow_banned"),
"device_id": ret.get("device_id"),
"valid_until_ms": ret.get("valid_until_ms"),
}
return user_info

def get_appservice_by_req(self, request):
token = self.get_access_token_from_request(request)
service = self.store.get_app_service_by_token(token)
if not service:
logger.warning("Unrecognised appservice access token.")
raise InvalidClientTokenError()
request.authenticated_entity = service.sender
request.requester = service.sender
clokep marked this conversation as resolved.
Show resolved Hide resolved
return service

async def is_server_admin(self, user: UserID) -> bool:
Expand Down
2 changes: 1 addition & 1 deletion synapse/federation/transport/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ async def authenticate_request(self, request, content):
)

logger.debug("Request from %s", origin)
request.authenticated_entity = origin
request.requester = origin

# If we get a valid signed request from the other side, its probably
# alive
Expand Down
8 changes: 4 additions & 4 deletions synapse/handlers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -984,17 +984,17 @@ async def delete_access_token(self, access_token: str):
# This might return an awaitable, if it does block the log out
# until it completes.
result = provider.on_logged_out(
user_id=str(user_info["user"]),
device_id=user_info["device_id"],
user_id=user_info.user_id,
device_id=user_info.device_id,
access_token=access_token,
)
if inspect.isawaitable(result):
await result

# delete pushers associated with this access token
if user_info["token_id"] is not None:
if user_info.token_id is not None:
await self.hs.get_pusherpool().remove_pushers_by_access_token(
str(user_info["user"]), (user_info["token_id"],)
user_info.user_id, (user_info.token_id,)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any idea what the str here was for?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user_info["user"] object was a UserID object.

)

async def delete_access_tokens_for_user(
Expand Down
7 changes: 5 additions & 2 deletions synapse/handlers/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,10 @@ async def check_username(
400, "User ID already taken.", errcode=Codes.USER_IN_USE
)
user_data = await self.auth.get_user_by_access_token(guest_access_token)
if not user_data["is_guest"] or user_data["user"].localpart != localpart:
if (
not user_data.is_guest
or UserID.from_string(user_data.user_id).localpart != localpart
):
raise AuthError(
403,
"Cannot register taken user ID without valid guest "
Expand Down Expand Up @@ -741,7 +744,7 @@ async def _register_email_threepid(self, user_id, threepid, token):
# up when the access token is saved, but that's quite an
# invasive change I'd rather do separately.
user_tuple = await self.store.get_user_by_access_token(token)
token_id = user_tuple["token_id"]
token_id = user_tuple.token_id

await self.pusher_pool.add_pusher(
user_id=user_id,
Expand Down
30 changes: 23 additions & 7 deletions synapse/http/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import contextlib
import logging
import time
from typing import Optional
from typing import Optional, Union

from twisted.python.failure import Failure
from twisted.web.server import Request, Site
Expand All @@ -23,6 +23,7 @@
from synapse.http import redact_uri
from synapse.http.request_metrics import RequestMetrics, requests_counter
from synapse.logging.context import LoggingContext, PreserveLoggingContext
from synapse.types import Requester

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -54,9 +55,12 @@ def __init__(self, channel, *args, **kw):
Request.__init__(self, channel, *args, **kw)
self.site = channel.site
self._channel = channel # this is used by the tests
self.authenticated_entity = None
self.start_time = 0.0

# The requester, if authenticated. For federation requests this is the
# server name, for client requests this is the Requester object.
self.requester = None # type: Optional[Union[Requester, str]]

# we can't yet create the logcontext, as we don't know the method.
self.logcontext = None # type: Optional[LoggingContext]

Expand Down Expand Up @@ -263,11 +267,23 @@ def _finished_processing(self):
# to the client (nb may be negative)
response_send_time = self.finish_time - self._processing_finished_time

# need to decode as it could be raw utf-8 bytes
# from a IDN servname in an auth header
authenticated_entity = self.authenticated_entity
if authenticated_entity is not None and isinstance(authenticated_entity, bytes):
authenticated_entity = authenticated_entity.decode("utf-8", "replace")
# Convert the requester into a string that we can log
authenticated_entity = None
if isinstance(self.requester, str):
authenticated_entity = self.requester
elif isinstance(self.requester, Requester):
authenticated_entity = self.requester.authenticated_entity

# If this is a request where the target user doesn't match the user who
# authenticated (e.g. and admin is puppetting a user) then we log both.
if self.requester.user.to_string() != authenticated_entity:
authenticated_entity = "{},{}".format(
authenticated_entity, self.requester.user.to_string(),
)
elif self.requester is not None:
# This shouldn't happen, but we log it so we don't lose information
# and can see that we're doing something wrong.
authenticated_entity = repr(self.requester) # type: ignore[unreachable]

# ...or could be raw utf-8 bytes in the User-Agent header.
# N.B. if you don't do this, the logger explodes cryptically
Expand Down
6 changes: 2 additions & 4 deletions synapse/replication/http/membership.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,7 @@ async def _handle_request(self, request, room_id, user_id):

requester = Requester.deserialize(self.store, content["requester"])

if requester.user:
request.authenticated_entity = requester.user.to_string()
request.requester = requester

logger.info("remote_join: %s into room: %s", user_id, room_id)

Expand Down Expand Up @@ -142,8 +141,7 @@ async def _handle_request(self, request, invite_event_id):

requester = Requester.deserialize(self.store, content["requester"])

if requester.user:
request.authenticated_entity = requester.user.to_string()
request.requester = requester

# hopefully we're now on the master, so this won't recurse!
event_id, stream_id = await self.member_handler.remote_reject_invite(
Expand Down
3 changes: 1 addition & 2 deletions synapse/replication/http/send_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,7 @@ async def _handle_request(self, request, event_id):
ratelimit = content["ratelimit"]
extra_users = [UserID.from_string(u) for u in content["extra_users"]]

if requester.user:
request.authenticated_entity = requester.user.to_string()
request.requester = requester

logger.info(
"Got event to send with ID: %s into room: %s", event.event_id, event.room_id
Expand Down
Loading