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

Commit f84da3c

Browse files
authored
Add a cache around server ACL checking (#16360)
* Pre-compiles the server ACLs onto an object per room and invalidates them when new events come in. * Converts the server ACL checking into Rust.
1 parent 17800a0 commit f84da3c

File tree

11 files changed

+235
-85
lines changed

11 files changed

+235
-85
lines changed

changelog.d/16360.misc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Cache server ACL checking.

rust/src/acl/mod.rs

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright 2023 The Matrix.org Foundation C.I.C.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//! An implementation of Matrix server ACL rules.
16+
17+
use std::net::Ipv4Addr;
18+
use std::str::FromStr;
19+
20+
use anyhow::Error;
21+
use pyo3::prelude::*;
22+
use regex::Regex;
23+
24+
use crate::push::utils::{glob_to_regex, GlobMatchType};
25+
26+
/// Called when registering modules with python.
27+
pub fn register_module(py: Python<'_>, m: &PyModule) -> PyResult<()> {
28+
let child_module = PyModule::new(py, "acl")?;
29+
child_module.add_class::<ServerAclEvaluator>()?;
30+
31+
m.add_submodule(child_module)?;
32+
33+
// We need to manually add the module to sys.modules to make `from
34+
// synapse.synapse_rust import acl` work.
35+
py.import("sys")?
36+
.getattr("modules")?
37+
.set_item("synapse.synapse_rust.acl", child_module)?;
38+
39+
Ok(())
40+
}
41+
42+
#[derive(Debug, Clone)]
43+
#[pyclass(frozen)]
44+
pub struct ServerAclEvaluator {
45+
allow_ip_literals: bool,
46+
allow: Vec<Regex>,
47+
deny: Vec<Regex>,
48+
}
49+
50+
#[pymethods]
51+
impl ServerAclEvaluator {
52+
#[new]
53+
pub fn py_new(
54+
allow_ip_literals: bool,
55+
allow: Vec<&str>,
56+
deny: Vec<&str>,
57+
) -> Result<Self, Error> {
58+
let allow = allow
59+
.iter()
60+
.map(|s| glob_to_regex(s, GlobMatchType::Whole))
61+
.collect::<Result<_, _>>()?;
62+
let deny = deny
63+
.iter()
64+
.map(|s| glob_to_regex(s, GlobMatchType::Whole))
65+
.collect::<Result<_, _>>()?;
66+
67+
Ok(ServerAclEvaluator {
68+
allow_ip_literals,
69+
allow,
70+
deny,
71+
})
72+
}
73+
74+
pub fn server_matches_acl_event(&self, server_name: &str) -> bool {
75+
// first of all, check if literal IPs are blocked, and if so, whether the
76+
// server name is a literal IP
77+
if !self.allow_ip_literals {
78+
// check for ipv6 literals. These start with '['.
79+
if server_name.starts_with('[') {
80+
return false;
81+
}
82+
83+
// check for ipv4 literals. We can just lift the routine from std::net.
84+
if Ipv4Addr::from_str(server_name).is_ok() {
85+
return false;
86+
}
87+
}
88+
89+
// next, check the deny list
90+
if self.deny.iter().any(|e| e.is_match(server_name)) {
91+
return false;
92+
}
93+
94+
// then the allow list.
95+
if self.allow.iter().any(|e| e.is_match(server_name)) {
96+
return true;
97+
}
98+
99+
// everything else should be rejected.
100+
false
101+
}
102+
}

rust/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use lazy_static::lazy_static;
22
use pyo3::prelude::*;
33
use pyo3_log::ResetHandle;
44

5+
pub mod acl;
56
pub mod push;
67

78
lazy_static! {
@@ -38,6 +39,7 @@ fn synapse_rust(py: Python<'_>, m: &PyModule) -> PyResult<()> {
3839
m.add_function(wrap_pyfunction!(get_rust_file_digest, m)?)?;
3940
m.add_function(wrap_pyfunction!(reset_logging_config, m)?)?;
4041

42+
acl::register_module(py, m)?;
4143
push::register_module(py, m)?;
4244

4345
Ok(())

stubs/synapse/synapse_rust/acl.pyi

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright 2023 The Matrix.org Foundation C.I.C.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from typing import List
16+
17+
class ServerAclEvaluator:
18+
def __init__(
19+
self, allow_ip_literals: bool, allow: List[str], deny: List[str]
20+
) -> None: ...
21+
def server_matches_acl_event(self, server_name: str) -> bool: ...

synapse/events/validator.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@
3939
CANONICALJSON_MIN_INT,
4040
validate_canonicaljson,
4141
)
42-
from synapse.federation.federation_server import server_matches_acl_event
4342
from synapse.http.servlet import validate_json_object
4443
from synapse.rest.models import RequestBodyModel
44+
from synapse.storage.controllers.state import server_acl_evaluator_from_event
4545
from synapse.types import EventID, JsonDict, RoomID, StrCollection, UserID
4646

4747

@@ -106,7 +106,10 @@ def validate_new(self, event: EventBase, config: HomeServerConfig) -> None:
106106
self._validate_retention(event)
107107

108108
elif event.type == EventTypes.ServerACL:
109-
if not server_matches_acl_event(config.server.server_name, event):
109+
server_acl_evaluator = server_acl_evaluator_from_event(event)
110+
if not server_acl_evaluator.server_matches_acl_event(
111+
config.server.server_name
112+
):
110113
raise SynapseError(
111114
400, "Can't create an ACL event that denies the local server"
112115
)

synapse/federation/federation_server.py

+6-70
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,8 @@
2929
Union,
3030
)
3131

32-
from matrix_common.regex import glob_to_regex
3332
from prometheus_client import Counter, Gauge, Histogram
3433

35-
from twisted.internet.abstract import isIPAddress
3634
from twisted.python import failure
3735

3836
from synapse.api.constants import (
@@ -1324,75 +1322,13 @@ async def check_server_matches_acl(self, server_name: str, room_id: str) -> None
13241322
Raises:
13251323
AuthError if the server does not match the ACL
13261324
"""
1327-
acl_event = await self._storage_controllers.state.get_current_state_event(
1328-
room_id, EventTypes.ServerACL, ""
1325+
server_acl_evaluator = (
1326+
await self._storage_controllers.state.get_server_acl_for_room(room_id)
13291327
)
1330-
if not acl_event or server_matches_acl_event(server_name, acl_event):
1331-
return
1332-
1333-
raise AuthError(code=403, msg="Server is banned from room")
1334-
1335-
1336-
def server_matches_acl_event(server_name: str, acl_event: EventBase) -> bool:
1337-
"""Check if the given server is allowed by the ACL event
1338-
1339-
Args:
1340-
server_name: name of server, without any port part
1341-
acl_event: m.room.server_acl event
1342-
1343-
Returns:
1344-
True if this server is allowed by the ACLs
1345-
"""
1346-
logger.debug("Checking %s against acl %s", server_name, acl_event.content)
1347-
1348-
# first of all, check if literal IPs are blocked, and if so, whether the
1349-
# server name is a literal IP
1350-
allow_ip_literals = acl_event.content.get("allow_ip_literals", True)
1351-
if not isinstance(allow_ip_literals, bool):
1352-
logger.warning("Ignoring non-bool allow_ip_literals flag")
1353-
allow_ip_literals = True
1354-
if not allow_ip_literals:
1355-
# check for ipv6 literals. These start with '['.
1356-
if server_name[0] == "[":
1357-
return False
1358-
1359-
# check for ipv4 literals. We can just lift the routine from twisted.
1360-
if isIPAddress(server_name):
1361-
return False
1362-
1363-
# next, check the deny list
1364-
deny = acl_event.content.get("deny", [])
1365-
if not isinstance(deny, (list, tuple)):
1366-
logger.warning("Ignoring non-list deny ACL %s", deny)
1367-
deny = []
1368-
for e in deny:
1369-
if _acl_entry_matches(server_name, e):
1370-
# logger.info("%s matched deny rule %s", server_name, e)
1371-
return False
1372-
1373-
# then the allow list.
1374-
allow = acl_event.content.get("allow", [])
1375-
if not isinstance(allow, (list, tuple)):
1376-
logger.warning("Ignoring non-list allow ACL %s", allow)
1377-
allow = []
1378-
for e in allow:
1379-
if _acl_entry_matches(server_name, e):
1380-
# logger.info("%s matched allow rule %s", server_name, e)
1381-
return True
1382-
1383-
# everything else should be rejected.
1384-
# logger.info("%s fell through", server_name)
1385-
return False
1386-
1387-
1388-
def _acl_entry_matches(server_name: str, acl_entry: Any) -> bool:
1389-
if not isinstance(acl_entry, str):
1390-
logger.warning(
1391-
"Ignoring non-str ACL entry '%s' (is %s)", acl_entry, type(acl_entry)
1392-
)
1393-
return False
1394-
regex = glob_to_regex(acl_entry)
1395-
return bool(regex.match(server_name))
1328+
if server_acl_evaluator and not server_acl_evaluator.server_matches_acl_event(
1329+
server_name
1330+
):
1331+
raise AuthError(code=403, msg="Server is banned from room")
13961332

13971333

13981334
class FederationHandlerRegistry:

synapse/handlers/federation_event.py

+6
Original file line numberDiff line numberDiff line change
@@ -2342,6 +2342,12 @@ async def _notify_persisted_event(
23422342
# TODO retrieve the previous state, and exclude join -> join transitions
23432343
self._notifier.notify_user_joined_room(event.event_id, event.room_id)
23442344

2345+
# If this is a server ACL event, clear the cache in the storage controller.
2346+
if event.type == EventTypes.ServerACL:
2347+
self._state_storage_controller.get_server_acl_for_room.invalidate(
2348+
(event.room_id,)
2349+
)
2350+
23452351
def _sanity_check_event(self, ev: EventBase) -> None:
23462352
"""
23472353
Do some early sanity checks of a received event

synapse/handlers/message.py

+5
Original file line numberDiff line numberDiff line change
@@ -1730,6 +1730,11 @@ async def persist_and_notify_client_events(
17301730
event.event_id, event.room_id
17311731
)
17321732

1733+
if event.type == EventTypes.ServerACL:
1734+
self._storage_controllers.state.get_server_acl_for_room.invalidate(
1735+
(event.room_id,)
1736+
)
1737+
17331738
await self._maybe_kick_guest_users(event, context)
17341739

17351740
if event.type == EventTypes.CanonicalAlias:

synapse/replication/tcp/client.py

+6
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,12 @@ async def on_rdata(
205205
self.notifier.notify_user_joined_room(
206206
row.data.event_id, row.data.room_id
207207
)
208+
209+
# If this is a server ACL event, clear the cache in the storage controller.
210+
if row.data.type == EventTypes.ServerACL:
211+
self._state_storage_controller.get_server_acl_for_room.invalidate(
212+
(row.data.room_id,)
213+
)
208214
elif stream_name == UnPartialStatedRoomStream.NAME:
209215
for row in rows:
210216
assert isinstance(row, UnPartialStatedRoomStreamRow)

synapse/storage/controllers/state.py

+59
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
PartialCurrentStateTracker,
3838
PartialStateEventsTracker,
3939
)
40+
from synapse.synapse_rust.acl import ServerAclEvaluator
4041
from synapse.types import MutableStateMap, StateMap, get_domain_from_id
4142
from synapse.types.state import StateFilter
4243
from synapse.util.async_helpers import Linearizer
@@ -501,6 +502,31 @@ async def get_canonical_alias_for_room(self, room_id: str) -> Optional[str]:
501502

502503
return event.content.get("alias")
503504

505+
@cached()
506+
async def get_server_acl_for_room(
507+
self, room_id: str
508+
) -> Optional[ServerAclEvaluator]:
509+
"""Get the server ACL evaluator for room, if any
510+
511+
This does up-front parsing of the content to ignore bad data and pre-compile
512+
regular expressions.
513+
514+
Args:
515+
room_id: The room ID
516+
517+
Returns:
518+
The server ACL evaluator, if any
519+
"""
520+
521+
acl_event = await self.get_current_state_event(
522+
room_id, EventTypes.ServerACL, ""
523+
)
524+
525+
if not acl_event:
526+
return None
527+
528+
return server_acl_evaluator_from_event(acl_event)
529+
504530
@trace
505531
@tag_args
506532
async def get_current_state_deltas(
@@ -760,3 +786,36 @@ async def _get_joined_hosts(
760786
cache.state_group = object()
761787

762788
return frozenset(cache.hosts_to_joined_users)
789+
790+
791+
def server_acl_evaluator_from_event(acl_event: EventBase) -> "ServerAclEvaluator":
792+
"""
793+
Create a ServerAclEvaluator from a m.room.server_acl event's content.
794+
795+
This does up-front parsing of the content to ignore bad data. It then creates
796+
the ServerAclEvaluator which will pre-compile regular expressions from the globs.
797+
"""
798+
799+
# first of all, parse if literal IPs are blocked.
800+
allow_ip_literals = acl_event.content.get("allow_ip_literals", True)
801+
if not isinstance(allow_ip_literals, bool):
802+
logger.warning("Ignoring non-bool allow_ip_literals flag")
803+
allow_ip_literals = True
804+
805+
# next, parse the deny list by ignoring any non-strings.
806+
deny = acl_event.content.get("deny", [])
807+
if not isinstance(deny, (list, tuple)):
808+
logger.warning("Ignoring non-list deny ACL %s", deny)
809+
deny = []
810+
else:
811+
deny = [s for s in deny if isinstance(s, str)]
812+
813+
# then the allow list.
814+
allow = acl_event.content.get("allow", [])
815+
if not isinstance(allow, (list, tuple)):
816+
logger.warning("Ignoring non-list allow ACL %s", allow)
817+
allow = []
818+
else:
819+
allow = [s for s in allow if isinstance(s, str)]
820+
821+
return ServerAclEvaluator(allow_ip_literals, allow, deny)

0 commit comments

Comments
 (0)