Add plugin APIs for implementations of custom event rules.
parent
6bac9ca6d7
commit
f874b16b2e
|
@ -0,0 +1 @@
|
||||||
|
Allow server admins to define implementations of extra rules for allowing or denying incoming events.
|
|
@ -1351,3 +1351,16 @@ password_config:
|
||||||
# alias: "*"
|
# alias: "*"
|
||||||
# room_id: "*"
|
# room_id: "*"
|
||||||
# action: allow
|
# action: allow
|
||||||
|
|
||||||
|
|
||||||
|
# Server admins can define a Python module that implements extra rules for
|
||||||
|
# allowing or denying incoming events. In order to work, this module needs to
|
||||||
|
# override the methods defined in synapse/events/third_party_rules.py.
|
||||||
|
#
|
||||||
|
# This feature is designed to be used in closed federations only, where each
|
||||||
|
# participating server enforces the same rules.
|
||||||
|
#
|
||||||
|
#third_party_event_rules:
|
||||||
|
# module: "my_custom_project.SuperRulesSet"
|
||||||
|
# config:
|
||||||
|
# example_option: 'things'
|
||||||
|
|
|
@ -38,6 +38,7 @@ from .server import ServerConfig
|
||||||
from .server_notices_config import ServerNoticesConfig
|
from .server_notices_config import ServerNoticesConfig
|
||||||
from .spam_checker import SpamCheckerConfig
|
from .spam_checker import SpamCheckerConfig
|
||||||
from .stats import StatsConfig
|
from .stats import StatsConfig
|
||||||
|
from .third_party_event_rules import ThirdPartyRulesConfig
|
||||||
from .tls import TlsConfig
|
from .tls import TlsConfig
|
||||||
from .user_directory import UserDirectoryConfig
|
from .user_directory import UserDirectoryConfig
|
||||||
from .voip import VoipConfig
|
from .voip import VoipConfig
|
||||||
|
@ -73,5 +74,6 @@ class HomeServerConfig(
|
||||||
StatsConfig,
|
StatsConfig,
|
||||||
ServerNoticesConfig,
|
ServerNoticesConfig,
|
||||||
RoomDirectoryConfig,
|
RoomDirectoryConfig,
|
||||||
|
ThirdPartyRulesConfig,
|
||||||
):
|
):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
#
|
||||||
|
# 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 synapse.util.module_loader import load_module
|
||||||
|
|
||||||
|
from ._base import Config
|
||||||
|
|
||||||
|
|
||||||
|
class ThirdPartyRulesConfig(Config):
|
||||||
|
def read_config(self, config):
|
||||||
|
self.third_party_event_rules = None
|
||||||
|
|
||||||
|
provider = config.get("third_party_event_rules", None)
|
||||||
|
if provider is not None:
|
||||||
|
self.third_party_event_rules = load_module(provider)
|
||||||
|
|
||||||
|
def default_config(self, **kwargs):
|
||||||
|
return """\
|
||||||
|
# Server admins can define a Python module that implements extra rules for
|
||||||
|
# allowing or denying incoming events. In order to work, this module needs to
|
||||||
|
# override the methods defined in synapse/events/third_party_rules.py.
|
||||||
|
#
|
||||||
|
# This feature is designed to be used in closed federations only, where each
|
||||||
|
# participating server enforces the same rules.
|
||||||
|
#
|
||||||
|
#third_party_event_rules:
|
||||||
|
# module: "my_custom_project.SuperRulesSet"
|
||||||
|
# config:
|
||||||
|
# example_option: 'things'
|
||||||
|
"""
|
|
@ -0,0 +1,62 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
#
|
||||||
|
# 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 twisted.internet import defer
|
||||||
|
|
||||||
|
|
||||||
|
class ThirdPartyEventRules(object):
|
||||||
|
"""Allows server admins to provide a Python module implementing an extra set of rules
|
||||||
|
to apply when processing events.
|
||||||
|
|
||||||
|
This is designed to help admins of closed federations with enforcing custom
|
||||||
|
behaviours.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, hs):
|
||||||
|
self.third_party_rules = None
|
||||||
|
|
||||||
|
self.store = hs.get_datastore()
|
||||||
|
|
||||||
|
module = None
|
||||||
|
config = None
|
||||||
|
if hs.config.third_party_event_rules:
|
||||||
|
module, config = hs.config.third_party_event_rules
|
||||||
|
|
||||||
|
if module is not None:
|
||||||
|
self.third_party_rules = module(config=config)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def check_event_allowed(self, event, context):
|
||||||
|
"""Check if a provided event should be allowed in the given context.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event (synapse.events.EventBase): The event to be checked.
|
||||||
|
context (synapse.events.snapshot.EventContext): The context of the event.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
defer.Deferred(bool), True if the event should be allowed, False if not.
|
||||||
|
"""
|
||||||
|
if self.third_party_rules is None:
|
||||||
|
defer.returnValue(True)
|
||||||
|
|
||||||
|
prev_state_ids = yield context.get_prev_state_ids(self.store)
|
||||||
|
|
||||||
|
# Retrieve the state events from the database.
|
||||||
|
state_events = {}
|
||||||
|
for key, event_id in prev_state_ids.items():
|
||||||
|
state_events[key] = yield self.store.get_event(event_id, allow_none=True)
|
||||||
|
|
||||||
|
ret = yield self.third_party_rules.check_event_allowed(event, state_events)
|
||||||
|
defer.returnValue(ret)
|
|
@ -1,6 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014-2016 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
# Copyright 2018 New Vector Ltd
|
# Copyright 2017-2018 New Vector Ltd
|
||||||
|
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
#
|
#
|
||||||
# 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.
|
||||||
|
@ -33,6 +34,7 @@ from synapse.api.constants import EventTypes, Membership, RejectedReason
|
||||||
from synapse.api.errors import (
|
from synapse.api.errors import (
|
||||||
AuthError,
|
AuthError,
|
||||||
CodeMessageException,
|
CodeMessageException,
|
||||||
|
Codes,
|
||||||
FederationDeniedError,
|
FederationDeniedError,
|
||||||
FederationError,
|
FederationError,
|
||||||
RequestSendFailed,
|
RequestSendFailed,
|
||||||
|
@ -127,6 +129,8 @@ class FederationHandler(BaseHandler):
|
||||||
self.room_queues = {}
|
self.room_queues = {}
|
||||||
self._room_pdu_linearizer = Linearizer("fed_room_pdu")
|
self._room_pdu_linearizer = Linearizer("fed_room_pdu")
|
||||||
|
|
||||||
|
self.third_party_event_rules = hs.get_third_party_event_rules()
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def on_receive_pdu(
|
def on_receive_pdu(
|
||||||
self, origin, pdu, sent_to_us_directly=False,
|
self, origin, pdu, sent_to_us_directly=False,
|
||||||
|
@ -1258,6 +1262,15 @@ class FederationHandler(BaseHandler):
|
||||||
logger.warn("Failed to create join %r because %s", event, e)
|
logger.warn("Failed to create join %r because %s", event, e)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
event_allowed = yield self.third_party_event_rules.check_event_allowed(
|
||||||
|
event, context,
|
||||||
|
)
|
||||||
|
if not event_allowed:
|
||||||
|
logger.info("Creation of join %s forbidden by third-party rules", event)
|
||||||
|
raise SynapseError(
|
||||||
|
403, "This event is not allowed in this context", Codes.FORBIDDEN,
|
||||||
|
)
|
||||||
|
|
||||||
# The remote hasn't signed it yet, obviously. We'll do the full checks
|
# The remote hasn't signed it yet, obviously. We'll do the full checks
|
||||||
# when we get the event back in `on_send_join_request`
|
# when we get the event back in `on_send_join_request`
|
||||||
yield self.auth.check_from_context(
|
yield self.auth.check_from_context(
|
||||||
|
@ -1300,6 +1313,15 @@ class FederationHandler(BaseHandler):
|
||||||
origin, event
|
origin, event
|
||||||
)
|
)
|
||||||
|
|
||||||
|
event_allowed = yield self.third_party_event_rules.check_event_allowed(
|
||||||
|
event, context,
|
||||||
|
)
|
||||||
|
if not event_allowed:
|
||||||
|
logger.info("Sending of join %s forbidden by third-party rules", event)
|
||||||
|
raise SynapseError(
|
||||||
|
403, "This event is not allowed in this context", Codes.FORBIDDEN,
|
||||||
|
)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"on_send_join_request: After _handle_new_event: %s, sigs: %s",
|
"on_send_join_request: After _handle_new_event: %s, sigs: %s",
|
||||||
event.event_id,
|
event.event_id,
|
||||||
|
@ -1458,6 +1480,15 @@ class FederationHandler(BaseHandler):
|
||||||
builder=builder,
|
builder=builder,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
event_allowed = yield self.third_party_event_rules.check_event_allowed(
|
||||||
|
event, context,
|
||||||
|
)
|
||||||
|
if not event_allowed:
|
||||||
|
logger.warning("Creation of leave %s forbidden by third-party rules", event)
|
||||||
|
raise SynapseError(
|
||||||
|
403, "This event is not allowed in this context", Codes.FORBIDDEN,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# The remote hasn't signed it yet, obviously. We'll do the full checks
|
# The remote hasn't signed it yet, obviously. We'll do the full checks
|
||||||
# when we get the event back in `on_send_leave_request`
|
# when we get the event back in `on_send_leave_request`
|
||||||
|
@ -1484,10 +1515,19 @@ class FederationHandler(BaseHandler):
|
||||||
|
|
||||||
event.internal_metadata.outlier = False
|
event.internal_metadata.outlier = False
|
||||||
|
|
||||||
yield self._handle_new_event(
|
context = yield self._handle_new_event(
|
||||||
origin, event
|
origin, event
|
||||||
)
|
)
|
||||||
|
|
||||||
|
event_allowed = yield self.third_party_event_rules.check_event_allowed(
|
||||||
|
event, context,
|
||||||
|
)
|
||||||
|
if not event_allowed:
|
||||||
|
logger.info("Sending of leave %s forbidden by third-party rules", event)
|
||||||
|
raise SynapseError(
|
||||||
|
403, "This event is not allowed in this context", Codes.FORBIDDEN,
|
||||||
|
)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"on_send_leave_request: After _handle_new_event: %s, sigs: %s",
|
"on_send_leave_request: After _handle_new_event: %s, sigs: %s",
|
||||||
event.event_id,
|
event.event_id,
|
||||||
|
@ -2550,6 +2590,18 @@ class FederationHandler(BaseHandler):
|
||||||
builder=builder
|
builder=builder
|
||||||
)
|
)
|
||||||
|
|
||||||
|
event_allowed = yield self.third_party_event_rules.check_event_allowed(
|
||||||
|
event, context,
|
||||||
|
)
|
||||||
|
if not event_allowed:
|
||||||
|
logger.info(
|
||||||
|
"Creation of threepid invite %s forbidden by third-party rules",
|
||||||
|
event,
|
||||||
|
)
|
||||||
|
raise SynapseError(
|
||||||
|
403, "This event is not allowed in this context", Codes.FORBIDDEN,
|
||||||
|
)
|
||||||
|
|
||||||
event, context = yield self.add_display_name_to_third_party_invite(
|
event, context = yield self.add_display_name_to_third_party_invite(
|
||||||
room_version, event_dict, event, context
|
room_version, event_dict, event, context
|
||||||
)
|
)
|
||||||
|
@ -2598,6 +2650,18 @@ class FederationHandler(BaseHandler):
|
||||||
builder=builder,
|
builder=builder,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
event_allowed = yield self.third_party_event_rules.check_event_allowed(
|
||||||
|
event, context,
|
||||||
|
)
|
||||||
|
if not event_allowed:
|
||||||
|
logger.warning(
|
||||||
|
"Exchange of threepid invite %s forbidden by third-party rules",
|
||||||
|
event,
|
||||||
|
)
|
||||||
|
raise SynapseError(
|
||||||
|
403, "This event is not allowed in this context", Codes.FORBIDDEN,
|
||||||
|
)
|
||||||
|
|
||||||
event, context = yield self.add_display_name_to_third_party_invite(
|
event, context = yield self.add_display_name_to_third_party_invite(
|
||||||
room_version, event_dict, event, context
|
room_version, event_dict, event, context
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 - 2016 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
# Copyright 2017 - 2018 New Vector Ltd
|
# Copyright 2017-2018 New Vector Ltd
|
||||||
|
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
#
|
#
|
||||||
# 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.
|
||||||
|
@ -248,6 +249,7 @@ class EventCreationHandler(object):
|
||||||
self.action_generator = hs.get_action_generator()
|
self.action_generator = hs.get_action_generator()
|
||||||
|
|
||||||
self.spam_checker = hs.get_spam_checker()
|
self.spam_checker = hs.get_spam_checker()
|
||||||
|
self.third_party_event_rules = hs.get_third_party_event_rules()
|
||||||
|
|
||||||
self._block_events_without_consent_error = (
|
self._block_events_without_consent_error = (
|
||||||
self.config.block_events_without_consent_error
|
self.config.block_events_without_consent_error
|
||||||
|
@ -658,6 +660,14 @@ class EventCreationHandler(object):
|
||||||
else:
|
else:
|
||||||
room_version = yield self.store.get_room_version(event.room_id)
|
room_version = yield self.store.get_room_version(event.room_id)
|
||||||
|
|
||||||
|
event_allowed = yield self.third_party_event_rules.check_event_allowed(
|
||||||
|
event, context,
|
||||||
|
)
|
||||||
|
if not event_allowed:
|
||||||
|
raise SynapseError(
|
||||||
|
403, "This event is not allowed in this context", Codes.FORBIDDEN,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield self.auth.check_from_context(room_version, event, context)
|
yield self.auth.check_from_context(room_version, event, context)
|
||||||
except AuthError as err:
|
except AuthError as err:
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014-2016 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
|
# Copyright 2017-2018 New Vector Ltd
|
||||||
|
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
#
|
#
|
||||||
# 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.
|
||||||
|
@ -35,6 +37,7 @@ from synapse.crypto import context_factory
|
||||||
from synapse.crypto.keyring import Keyring
|
from synapse.crypto.keyring import Keyring
|
||||||
from synapse.events.builder import EventBuilderFactory
|
from synapse.events.builder import EventBuilderFactory
|
||||||
from synapse.events.spamcheck import SpamChecker
|
from synapse.events.spamcheck import SpamChecker
|
||||||
|
from synapse.events.third_party_rules import ThirdPartyEventRules
|
||||||
from synapse.events.utils import EventClientSerializer
|
from synapse.events.utils import EventClientSerializer
|
||||||
from synapse.federation.federation_client import FederationClient
|
from synapse.federation.federation_client import FederationClient
|
||||||
from synapse.federation.federation_server import (
|
from synapse.federation.federation_server import (
|
||||||
|
@ -178,6 +181,7 @@ class HomeServer(object):
|
||||||
'groups_attestation_renewer',
|
'groups_attestation_renewer',
|
||||||
'secrets',
|
'secrets',
|
||||||
'spam_checker',
|
'spam_checker',
|
||||||
|
'third_party_event_rules',
|
||||||
'room_member_handler',
|
'room_member_handler',
|
||||||
'federation_registry',
|
'federation_registry',
|
||||||
'server_notices_manager',
|
'server_notices_manager',
|
||||||
|
@ -483,6 +487,9 @@ class HomeServer(object):
|
||||||
def build_spam_checker(self):
|
def build_spam_checker(self):
|
||||||
return SpamChecker(self)
|
return SpamChecker(self)
|
||||||
|
|
||||||
|
def build_third_party_event_rules(self):
|
||||||
|
return ThirdPartyEventRules(self)
|
||||||
|
|
||||||
def build_room_member_handler(self):
|
def build_room_member_handler(self):
|
||||||
if self.config.worker_app:
|
if self.config.worker_app:
|
||||||
return RoomMemberWorkerHandler(self)
|
return RoomMemberWorkerHandler(self)
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
#
|
||||||
|
# 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 synapse.rest import admin
|
||||||
|
from synapse.rest.client.v1 import login, room
|
||||||
|
|
||||||
|
from tests import unittest
|
||||||
|
|
||||||
|
|
||||||
|
class ThirdPartyRulesTestModule(object):
|
||||||
|
def __init__(self, config):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def check_event_allowed(self, event, context):
|
||||||
|
if event.type == "foo.bar.forbidden":
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_config(config):
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
class ThirdPartyRulesTestCase(unittest.HomeserverTestCase):
|
||||||
|
servlets = [
|
||||||
|
admin.register_servlets,
|
||||||
|
login.register_servlets,
|
||||||
|
room.register_servlets,
|
||||||
|
]
|
||||||
|
|
||||||
|
def make_homeserver(self, reactor, clock):
|
||||||
|
config = self.default_config()
|
||||||
|
config["third_party_event_rules"] = {
|
||||||
|
"module": "tests.rest.client.third_party_rules.ThirdPartyRulesTestModule",
|
||||||
|
"config": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
self.hs = self.setup_test_homeserver(config=config)
|
||||||
|
return self.hs
|
||||||
|
|
||||||
|
def test_third_party_rules(self):
|
||||||
|
"""Tests that a forbidden event is forbidden from being sent, but an allowed one
|
||||||
|
can be sent.
|
||||||
|
"""
|
||||||
|
user_id = self.register_user("kermit", "monkey")
|
||||||
|
tok = self.login("kermit", "monkey")
|
||||||
|
|
||||||
|
room_id = self.helper.create_room_as(user_id, tok=tok)
|
||||||
|
|
||||||
|
request, channel = self.make_request(
|
||||||
|
"PUT",
|
||||||
|
"/_matrix/client/r0/rooms/%s/send/foo.bar.allowed/1" % room_id,
|
||||||
|
{},
|
||||||
|
access_token=tok,
|
||||||
|
)
|
||||||
|
self.render(request)
|
||||||
|
self.assertEquals(channel.result["code"], b"200", channel.result)
|
||||||
|
|
||||||
|
request, channel = self.make_request(
|
||||||
|
"PUT",
|
||||||
|
"/_matrix/client/r0/rooms/%s/send/foo.bar.forbidden/1" % room_id,
|
||||||
|
{},
|
||||||
|
access_token=tok,
|
||||||
|
)
|
||||||
|
self.render(request)
|
||||||
|
self.assertEquals(channel.result["code"], b"403", channel.result)
|
Loading…
Reference in New Issue