forked from MirrorHub/synapse
Merge pull request #4814 from matrix-org/erikj/soft_fail_impl
Implement soft fail
This commit is contained in:
commit
39e57f9728
5 changed files with 113 additions and 1 deletions
1
changelog.d/4814.feature
Normal file
1
changelog.d/4814.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Add checks to incoming events over federation for events evading auth (aka "soft fail").
|
|
@ -77,6 +77,20 @@ class _EventInternalMetadata(object):
|
|||
"""
|
||||
return getattr(self, "recheck_redaction", False)
|
||||
|
||||
def is_soft_failed(self):
|
||||
"""Whether the event has been soft failed.
|
||||
|
||||
Soft failed events should be handled as usual, except:
|
||||
1. They should not go down sync or event streams, or generally
|
||||
sent to clients.
|
||||
2. They should not be added to the forward extremities (and
|
||||
therefore not to current state).
|
||||
|
||||
Returns:
|
||||
bool
|
||||
"""
|
||||
return getattr(self, "soft_failed", False)
|
||||
|
||||
|
||||
def _event_dict_property(key):
|
||||
# We want to be able to use hasattr with the event dict properties.
|
||||
|
|
|
@ -45,6 +45,7 @@ from synapse.api.errors import (
|
|||
SynapseError,
|
||||
)
|
||||
from synapse.crypto.event_signing import compute_event_signature
|
||||
from synapse.event_auth import auth_types_for_event
|
||||
from synapse.events.validator import EventValidator
|
||||
from synapse.replication.http.federation import (
|
||||
ReplicationCleanRoomRestServlet,
|
||||
|
@ -1628,6 +1629,7 @@ class FederationHandler(BaseHandler):
|
|||
origin, event,
|
||||
state=state,
|
||||
auth_events=auth_events,
|
||||
backfilled=backfilled,
|
||||
)
|
||||
|
||||
# reraise does not allow inlineCallbacks to preserve the stacktrace, so we
|
||||
|
@ -1672,6 +1674,7 @@ class FederationHandler(BaseHandler):
|
|||
event,
|
||||
state=ev_info.get("state"),
|
||||
auth_events=ev_info.get("auth_events"),
|
||||
backfilled=backfilled,
|
||||
)
|
||||
defer.returnValue(res)
|
||||
|
||||
|
@ -1794,7 +1797,7 @@ class FederationHandler(BaseHandler):
|
|||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _prep_event(self, origin, event, state=None, auth_events=None):
|
||||
def _prep_event(self, origin, event, state, auth_events, backfilled):
|
||||
"""
|
||||
|
||||
Args:
|
||||
|
@ -1802,6 +1805,7 @@ class FederationHandler(BaseHandler):
|
|||
event:
|
||||
state:
|
||||
auth_events:
|
||||
backfilled (bool)
|
||||
|
||||
Returns:
|
||||
Deferred, which resolves to synapse.events.snapshot.EventContext
|
||||
|
@ -1843,11 +1847,99 @@ class FederationHandler(BaseHandler):
|
|||
|
||||
context.rejected = RejectedReason.AUTH_ERROR
|
||||
|
||||
if not context.rejected:
|
||||
yield self._check_for_soft_fail(event, state, backfilled)
|
||||
|
||||
if event.type == EventTypes.GuestAccess and not context.rejected:
|
||||
yield self.maybe_kick_guest_users(event)
|
||||
|
||||
defer.returnValue(context)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _check_for_soft_fail(self, event, state, backfilled):
|
||||
"""Checks if we should soft fail the event, if so marks the event as
|
||||
such.
|
||||
|
||||
Args:
|
||||
event (FrozenEvent)
|
||||
state (dict|None): The state at the event if we don't have all the
|
||||
event's prev events
|
||||
backfilled (bool): Whether the event is from backfill
|
||||
|
||||
Returns:
|
||||
Deferred
|
||||
"""
|
||||
# For new (non-backfilled and non-outlier) events we check if the event
|
||||
# passes auth based on the current state. If it doesn't then we
|
||||
# "soft-fail" the event.
|
||||
do_soft_fail_check = not backfilled and not event.internal_metadata.is_outlier()
|
||||
if do_soft_fail_check:
|
||||
extrem_ids = yield self.store.get_latest_event_ids_in_room(
|
||||
event.room_id,
|
||||
)
|
||||
|
||||
extrem_ids = set(extrem_ids)
|
||||
prev_event_ids = set(event.prev_event_ids())
|
||||
|
||||
if extrem_ids == prev_event_ids:
|
||||
# If they're the same then the current state is the same as the
|
||||
# state at the event, so no point rechecking auth for soft fail.
|
||||
do_soft_fail_check = False
|
||||
|
||||
if do_soft_fail_check:
|
||||
room_version = yield self.store.get_room_version(event.room_id)
|
||||
|
||||
# Calculate the "current state".
|
||||
if state is not None:
|
||||
# If we're explicitly given the state then we won't have all the
|
||||
# prev events, and so we have a gap in the graph. In this case
|
||||
# we want to be a little careful as we might have been down for
|
||||
# a while and have an incorrect view of the current state,
|
||||
# however we still want to do checks as gaps are easy to
|
||||
# maliciously manufacture.
|
||||
#
|
||||
# So we use a "current state" that is actually a state
|
||||
# resolution across the current forward extremities and the
|
||||
# given state at the event. This should correctly handle cases
|
||||
# like bans, especially with state res v2.
|
||||
|
||||
state_sets = yield self.store.get_state_groups(
|
||||
event.room_id, extrem_ids,
|
||||
)
|
||||
state_sets = list(state_sets.values())
|
||||
state_sets.append(state)
|
||||
current_state_ids = yield self.state_handler.resolve_events(
|
||||
room_version, state_sets, event,
|
||||
)
|
||||
current_state_ids = {
|
||||
k: e.event_id for k, e in iteritems(current_state_ids)
|
||||
}
|
||||
else:
|
||||
current_state_ids = yield self.state_handler.get_current_state_ids(
|
||||
event.room_id, latest_event_ids=extrem_ids,
|
||||
)
|
||||
|
||||
# Now check if event pass auth against said current state
|
||||
auth_types = auth_types_for_event(event)
|
||||
current_state_ids = [
|
||||
e for k, e in iteritems(current_state_ids)
|
||||
if k in auth_types
|
||||
]
|
||||
|
||||
current_auth_events = yield self.store.get_events(current_state_ids)
|
||||
current_auth_events = {
|
||||
(e.type, e.state_key): e for e in current_auth_events.values()
|
||||
}
|
||||
|
||||
try:
|
||||
self.auth.check(room_version, event, auth_events=current_auth_events)
|
||||
except AuthError as e:
|
||||
logger.warn(
|
||||
"Failed current state auth resolution for %r because %s",
|
||||
event, e,
|
||||
)
|
||||
event.internal_metadata.soft_failed = True
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_query_auth(self, origin, event_id, room_id, remote_auth_chain, rejects,
|
||||
missing):
|
||||
|
|
|
@ -537,6 +537,7 @@ class EventsStore(StateGroupWorkerStore, EventFederationStore, EventsWorkerStore
|
|||
new_events = [
|
||||
event for event, ctx in event_contexts
|
||||
if not event.internal_metadata.is_outlier() and not ctx.rejected
|
||||
and not event.internal_metadata.is_soft_failed()
|
||||
]
|
||||
|
||||
# start with the existing forward extremities
|
||||
|
|
|
@ -67,6 +67,10 @@ def filter_events_for_client(store, user_id, events, is_peeking=False,
|
|||
Returns:
|
||||
Deferred[list[synapse.events.EventBase]]
|
||||
"""
|
||||
# Filter out events that have been soft failed so that we don't relay them
|
||||
# to clients.
|
||||
events = list(e for e in events if not e.internal_metadata.is_soft_failed())
|
||||
|
||||
types = (
|
||||
(EventTypes.RoomHistoryVisibility, ""),
|
||||
(EventTypes.Member, user_id),
|
||||
|
|
Loading…
Reference in a new issue