Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/16258.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Revert MSC3861 introspection cache, admin impersonation and account lock.
91 changes: 6 additions & 85 deletions synapse/api/auth/msc3861_delegated.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
from synapse.api.auth.base import BaseAuth
from synapse.api.errors import (
AuthError,
Codes,
HttpResponseException,
InvalidClientTokenError,
OAuthInsufficientScopeError,
Expand All @@ -40,7 +39,6 @@
from synapse.types import Requester, UserID, create_requester
from synapse.util import json_decoder
from synapse.util.caches.cached_call import RetryOnExceptionCachedCall
from synapse.util.caches.expiringcache import ExpiringCache

if TYPE_CHECKING:
from synapse.server import HomeServer
Expand Down Expand Up @@ -109,20 +107,13 @@ def __init__(self, hs: "HomeServer"):
assert self._config.client_id, "No client_id provided"
assert auth_method is not None, "Invalid client_auth_method provided"

self._clock = hs.get_clock()
self._http_client = hs.get_proxied_http_client()
self._hostname = hs.hostname
self._admin_token = self._config.admin_token

self._issuer_metadata = RetryOnExceptionCachedCall(self._load_metadata)

self._clock = hs.get_clock()
self._token_cache: ExpiringCache[str, IntrospectionToken] = ExpiringCache(
cache_name="introspection_token_cache",
clock=self._clock,
max_len=10000,
expiry_ms=5 * 60 * 1000,
)

if isinstance(auth_method, PrivateKeyJWTWithKid):
# Use the JWK as the client secret when using the private_key_jwt method
assert self._config.jwk, "No JWK provided"
Expand Down Expand Up @@ -161,20 +152,6 @@ async def _introspect_token(self, token: str) -> IntrospectionToken:
Returns:
The introspection response
"""
# check the cache before doing a request
introspection_token = self._token_cache.get(token, None)

if introspection_token:
# check the expiration field of the token (if it exists)
exp = introspection_token.get("exp", None)
if exp:
time_now = self._clock.time()
expired = time_now > exp
if not expired:
return introspection_token
else:
return introspection_token

metadata = await self._issuer_metadata.get()
introspection_endpoint = metadata.get("introspection_endpoint")
raw_headers: Dict[str, str] = {
Expand All @@ -188,10 +165,7 @@ async def _introspect_token(self, token: str) -> IntrospectionToken:

# Fill the body/headers with credentials
uri, raw_headers, body = self._client_auth.prepare(
method="POST",
uri=introspection_endpoint,
headers=raw_headers,
body=body,
method="POST", uri=introspection_endpoint, headers=raw_headers, body=body
)
headers = Headers({k: [v] for (k, v) in raw_headers.items()})

Expand Down Expand Up @@ -233,20 +207,10 @@ async def _introspect_token(self, token: str) -> IntrospectionToken:
"The introspection endpoint returned an invalid JSON response."
)

expiration = resp.get("exp", None)
if expiration:
if self._clock.time() > expiration:
raise InvalidClientTokenError("Token is expired.")

introspection_token = IntrospectionToken(**resp)

# add token to cache
self._token_cache[token] = introspection_token

return introspection_token
return IntrospectionToken(**resp)

async def is_server_admin(self, requester: Requester) -> bool:
return SCOPE_SYNAPSE_ADMIN in requester.scope
return "urn:synapse:admin:*" in requester.scope

async def get_user_by_req(
self,
Expand All @@ -263,36 +227,6 @@ async def get_user_by_req(
# so that we don't provision the user if they don't have enough permission:
requester = await self.get_user_by_access_token(access_token, allow_expired)

# Allow impersonation by an admin user using `_oidc_admin_impersonate_user_id` query parameter
if request.args is not None:
user_id_params = request.args.get(b"_oidc_admin_impersonate_user_id")
if user_id_params:
if await self.is_server_admin(requester):
user_id_str = user_id_params[0].decode("ascii")
impersonated_user_id = UserID.from_string(user_id_str)
logging.info(f"Admin impersonation of user {user_id_str}")
requester = create_requester(
user_id=impersonated_user_id,
scope=[SCOPE_MATRIX_API],
authenticated_entity=requester.user.to_string(),
)
else:
raise AuthError(
401,
"Impersonation not possible by a non admin user",
)

# Deny the request if the user account is locked.
if not allow_locked and await self.store.get_user_locked_status(
requester.user.to_string()
):
raise AuthError(
401,
"User account has been locked",
errcode=Codes.USER_LOCKED,
additional_fields={"soft_logout": True},
)

if not allow_guest and requester.is_guest:
raise OAuthInsufficientScopeError([SCOPE_MATRIX_API])

Expand All @@ -309,14 +243,14 @@ async def get_user_by_access_token(
# XXX: This is a temporary solution so that the admin API can be called by
# the OIDC provider. This will be removed once we have OIDC client
# credentials grant support in matrix-authentication-service.
logging.info("Admin token used")
logging.info("Admin toked used")
# XXX: that user doesn't exist and won't be provisioned.
# This is mostly fine for admin calls, but we should also think about doing
# requesters without a user_id.
admin_user = UserID("__oidc_admin", self._hostname)
return create_requester(
user_id=admin_user,
scope=[SCOPE_SYNAPSE_ADMIN],
scope=["urn:synapse:admin:*"],
)

try:
Expand Down Expand Up @@ -438,16 +372,3 @@ async def get_user_by_access_token(
scope=scope,
is_guest=(has_guest_scope and not has_user_scope),
)

def invalidate_cached_tokens(self, keys: List[str]) -> None:
"""
Invalidate the entry(s) in the introspection token cache corresponding to the given key
"""
for key in keys:
self._token_cache.invalidate(key)

def invalidate_token_cache(self) -> None:
"""
Invalidate the entire token cache.
"""
self._token_cache.invalidate_all()
12 changes: 0 additions & 12 deletions synapse/replication/tcp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
from synapse.metrics.background_process_metrics import run_as_background_process
from synapse.replication.tcp.streams import (
AccountDataStream,
CachesStream,
DeviceListsStream,
PushersStream,
PushRulesStream,
Expand Down Expand Up @@ -76,7 +75,6 @@ def __init__(self, hs: "HomeServer"):
self._instance_name = hs.get_instance_name()
self._typing_handler = hs.get_typing_handler()
self._state_storage_controller = hs.get_storage_controllers().state
self.auth = hs.get_auth()

self._notify_pushers = hs.config.worker.start_pushers
self._pusher_pool = hs.get_pusherpool()
Expand Down Expand Up @@ -224,16 +222,6 @@ async def on_rdata(
self._state_storage_controller.notify_event_un_partial_stated(
row.event_id
)
# invalidate the introspection token cache
elif stream_name == CachesStream.NAME:
for row in rows:
if row.cache_func == "introspection_token_invalidation":
if row.keys[0] is None:
# invalidate the whole cache
# mypy ignore - the token cache is defined on MSC3861DelegatedAuth
self.auth.invalidate_token_cache() # type: ignore[attr-defined]
else:
self.auth.invalidate_cached_tokens(row.keys) # type: ignore[attr-defined]

await self._presence_handler.process_replication_rows(
stream_name, instance_name, token, rows
Expand Down
3 changes: 0 additions & 3 deletions synapse/rest/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
ListDestinationsRestServlet,
)
from synapse.rest.admin.media import ListMediaInRoom, register_servlets_for_media_repo
from synapse.rest.admin.oidc import OIDCTokenRevocationRestServlet
from synapse.rest.admin.registration_tokens import (
ListRegistrationTokensRestServlet,
NewRegistrationTokenRestServlet,
Expand Down Expand Up @@ -298,8 +297,6 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
BackgroundUpdateRestServlet(hs).register(http_server)
BackgroundUpdateStartJobRestServlet(hs).register(http_server)
ExperimentalFeaturesRestServlet(hs).register(http_server)
if hs.config.experimental.msc3861.enabled:
OIDCTokenRevocationRestServlet(hs).register(http_server)


def register_servlets_for_client_rest_resource(
Expand Down
55 changes: 0 additions & 55 deletions synapse/rest/admin/oidc.py

This file was deleted.

13 changes: 0 additions & 13 deletions synapse/storage/databases/main/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,19 +584,6 @@ def get_cache_stream_token_for_writer(self, instance_name: str) -> int:
else:
return 0

async def stream_introspection_token_invalidation(
self, key: Tuple[Optional[str]]
) -> None:
"""
Stream an invalidation request for the introspection token cache to workers

Args:
key: token_id of the introspection token to remove from the cache
"""
await self.send_invalidation_to_replication(
"introspection_token_invalidation", key
)

@wrap_as_background_process("clean_up_old_cache_invalidations")
async def _clean_up_cache_invalidation_wrapper(self) -> None:
"""
Expand Down
9 changes: 0 additions & 9 deletions synapse/storage/databases/main/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@

from synapse.api.constants import EduTypes
from synapse.api.errors import Codes, StoreError
from synapse.config.homeserver import HomeServerConfig
from synapse.logging.opentracing import (
get_active_span_text_map,
set_tag,
Expand Down Expand Up @@ -1664,7 +1663,6 @@ def __init__(
self.device_id_exists_cache: LruCache[
Tuple[str, str], Literal[True]
] = LruCache(cache_name="device_id_exists", max_size=10000)
self.config: HomeServerConfig = hs.config

async def store_device(
self,
Expand Down Expand Up @@ -1786,13 +1784,6 @@ def _delete_devices_txn(txn: LoggingTransaction) -> None:
for device_id in device_ids:
self.device_id_exists_cache.invalidate((user_id, device_id))

# TODO: don't nuke the entire cache once there is a way to associate
# device_id -> introspection_token
if self.config.experimental.msc3861.enabled:
# mypy ignore - the token cache is defined on MSC3861DelegatedAuth
self.auth._token_cache.invalidate_all() # type: ignore[attr-defined]
await self.stream_introspection_token_invalidation((None,))

async def update_device(
self, user_id: str, device_id: str, new_display_name: Optional[str] = None
) -> None:
Expand Down
22 changes: 0 additions & 22 deletions synapse/util/caches/expiringcache.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,20 +140,6 @@ def pop(self, key: KT, default: T = SENTINEL) -> Union[VT, T]:

return value.value

def invalidate(self, key: KT) -> None:
"""
Remove the given key from the cache.
"""

value = self._cache.pop(key, None)
if value:
if self.iterable:
self.metrics.inc_evictions(
EvictionReason.invalidation, len(value.value)
)
else:
self.metrics.inc_evictions(EvictionReason.invalidation)

def __contains__(self, key: KT) -> bool:
return key in self._cache

Expand Down Expand Up @@ -207,14 +193,6 @@ async def _prune_cache(self) -> None:
len(self),
)

def invalidate_all(self) -> None:
"""
Remove all items from the cache.
"""
keys = set(self._cache.keys())
for key in keys:
self._cache.pop(key)

def __len__(self) -> int:
if self.iterable:
return sum(len(entry.value) for entry in self._cache.values())
Expand Down
Loading