Compare commits

..

4 Commits

Author SHA1 Message Date
Jade Ellis
b6c5991e1f chore(deps): Update rand
A couple indirect deps are still on rand_core 0.6 but we can deal
2026-02-20 22:57:45 +00:00
Katie Kloss
efd879fcd8 docs: Add news fragment 2026-02-20 10:13:54 +00:00
Katie Kloss
92a848f74d fix: Crash before starting on OpenBSD
core_affinity doesn't return any cores on OpenBSD, so we try to
clamp(1, 0). This is Less Good than fixing that crate, but at
least allows the server to start up.
2026-02-20 10:13:54 +00:00
Renovate Bot
776b5865ba chore(deps): update sentry-rust monorepo to 0.46.0 2026-02-19 14:56:25 +00:00
21 changed files with 3114 additions and 2142 deletions

767
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -68,7 +68,7 @@ default-features = false
version = "0.1.3"
[workspace.dependencies.rand]
version = "0.8.5"
version = "0.10.0"
# Used for the http request / response body type for Ruma endpoints used with reqwest
[workspace.dependencies.bytes]
@@ -298,7 +298,7 @@ default-features = false
features = ["env", "toml"]
[workspace.dependencies.hickory-resolver]
version = "0.25.1"
version = "0.25.2"
default-features = false
features = [
"serde",
@@ -343,7 +343,7 @@ version = "0.1.2"
[workspace.dependencies.ruma]
git = "https://forgejo.ellis.link/continuwuation/ruwuma"
#branch = "conduwuit-changes"
rev = "3126cb5eea991ec40590e54d8c9d75637650641a"
rev = "e087ff15888156942ca2ffe6097d1b4c3fd27628"
features = [
"compat",
"rand",
@@ -425,7 +425,7 @@ features = ["http", "grpc-tonic", "trace", "logs", "metrics"]
# optional sentry metrics for crash/panic reporting
[workspace.dependencies.sentry]
version = "0.45.0"
version = "0.46.0"
default-features = false
features = [
"backtrace",
@@ -441,9 +441,9 @@ features = [
]
[workspace.dependencies.sentry-tracing]
version = "0.45.0"
version = "0.46.0"
[workspace.dependencies.sentry-tower]
version = "0.45.0"
version = "0.46.0"
# jemalloc usage
[workspace.dependencies.tikv-jemalloc-sys]

1
changelog.d/1421.bugfix Normal file
View File

@@ -0,0 +1 @@
Fixed a startup crash in the sender service if we can't detect the number of CPU cores, even if the `sender_workers' config option is set correctly. Contributed by @katie.

View File

@@ -244,7 +244,7 @@ fn build_report(report: Report) -> RoomMessageEventContent {
/// random delay sending a response per spec suggestion regarding
/// enumerating for potential events existing in our server.
async fn delay_response() {
let time_to_wait = rand::thread_rng().gen_range(2..5);
let time_to_wait = rand::random_range(2..5);
debug_info!(
"Got successful /report request, waiting {time_to_wait} seconds before sending \
successful response."

View File

@@ -40,7 +40,7 @@ pub(crate) async fn get_room_information_route(
servers.sort_unstable();
servers.dedup();
servers.shuffle(&mut rand::thread_rng());
servers.shuffle(&mut rand::rng());
// insert our server as the very first choice if in list
if let Some(server_index) = servers

View File

@@ -86,6 +86,7 @@ libloading.optional = true
log.workspace = true
num-traits.workspace = true
rand.workspace = true
rand_core = { version = "0.6.4", features = ["getrandom"] }
regex.workspace = true
reqwest.workspace = true
ring.workspace = true

View File

@@ -0,0 +1,552 @@
#[cfg(conduwuit_bench)]
extern crate test;
use std::{
borrow::Borrow,
collections::{HashMap, HashSet},
sync::atomic::{AtomicU64, Ordering::SeqCst},
};
use futures::{future, future::ready};
use maplit::{btreemap, hashmap, hashset};
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, RoomVersionId, Signatures, UserId,
events::{
StateEventType, TimelineEventType,
room::{
join_rules::{JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent},
},
},
int, room_id, uint, user_id,
};
use serde_json::{
json,
value::{RawValue as RawJsonValue, to_raw_value as to_raw_json_value},
};
use crate::{
matrix::{Event, Pdu, pdu::EventHash},
state_res::{self as state_res, Error, Result, StateMap},
};
static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
#[cfg(conduwuit_bench)]
#[cfg_attr(conduwuit_bench, bench)]
fn lexico_topo_sort(c: &mut test::Bencher) {
let graph = hashmap! {
event_id("l") => hashset![event_id("o")],
event_id("m") => hashset![event_id("n"), event_id("o")],
event_id("n") => hashset![event_id("o")],
event_id("o") => hashset![], // "o" has zero outgoing edges but 4 incoming edges
event_id("p") => hashset![event_id("o")],
};
c.iter(|| {
let _ = state_res::lexicographical_topological_sort(&graph, &|_| {
future::ok((int!(0), MilliSecondsSinceUnixEpoch(uint!(0))))
});
});
}
#[cfg(conduwuit_bench)]
#[cfg_attr(conduwuit_bench, bench)]
fn resolution_shallow_auth_chain(c: &mut test::Bencher) {
let mut store = TestStore(hashmap! {});
// build up the DAG
let (state_at_bob, state_at_charlie, _) = store.set_up();
c.iter(|| async {
let ev_map = store.0.clone();
let state_sets = [&state_at_bob, &state_at_charlie];
let fetch = |id: OwnedEventId| ready(ev_map.get(&id).map(ToOwned::to_owned));
let exists = |id: OwnedEventId| ready(ev_map.get(&id).is_some());
let auth_chain_sets: Vec<HashSet<_>> = state_sets
.iter()
.map(|map| {
store
.auth_event_ids(room_id(), map.values().cloned().collect())
.unwrap()
})
.collect();
let _ = match state_res::resolve(
&RoomVersionId::V6,
state_sets.into_iter(),
&auth_chain_sets,
&fetch,
&exists,
)
.await
{
| Ok(state) => state,
| Err(e) => panic!("{e}"),
};
});
}
#[cfg(conduwuit_bench)]
#[cfg_attr(conduwuit_bench, bench)]
fn resolve_deeper_event_set(c: &mut test::Bencher) {
let mut inner = INITIAL_EVENTS();
let ban = BAN_STATE_SET();
inner.extend(ban);
let store = TestStore(inner.clone());
let state_set_a = [
inner.get(&event_id("CREATE")).unwrap(),
inner.get(&event_id("IJR")).unwrap(),
inner.get(&event_id("IMA")).unwrap(),
inner.get(&event_id("IMB")).unwrap(),
inner.get(&event_id("IMC")).unwrap(),
inner.get(&event_id("MB")).unwrap(),
inner.get(&event_id("PA")).unwrap(),
]
.iter()
.map(|ev| {
(
(ev.event_type().clone().into(), ev.state_key().unwrap().into()),
ev.event_id().to_owned(),
)
})
.collect::<StateMap<_>>();
let state_set_b = [
inner.get(&event_id("CREATE")).unwrap(),
inner.get(&event_id("IJR")).unwrap(),
inner.get(&event_id("IMA")).unwrap(),
inner.get(&event_id("IMB")).unwrap(),
inner.get(&event_id("IMC")).unwrap(),
inner.get(&event_id("IME")).unwrap(),
inner.get(&event_id("PA")).unwrap(),
]
.iter()
.map(|ev| {
(
(ev.event_type().clone().into(), ev.state_key().unwrap().into()),
ev.event_id().to_owned(),
)
})
.collect::<StateMap<_>>();
c.iter(|| async {
let state_sets = [&state_set_a, &state_set_b];
let auth_chain_sets: Vec<HashSet<_>> = state_sets
.iter()
.map(|map| {
store
.auth_event_ids(room_id(), map.values().cloned().collect())
.unwrap()
})
.collect();
let fetch = |id: OwnedEventId| ready(inner.get(&id).map(ToOwned::to_owned));
let exists = |id: OwnedEventId| ready(inner.get(&id).is_some());
let _ = match state_res::resolve(
&RoomVersionId::V6,
state_sets.into_iter(),
&auth_chain_sets,
&fetch,
&exists,
)
.await
{
| Ok(state) => state,
| Err(_) => panic!("resolution failed during benchmarking"),
};
});
}
//*/////////////////////////////////////////////////////////////////////
//
// IMPLEMENTATION DETAILS AHEAD
//
/////////////////////////////////////////////////////////////////////*/
struct TestStore<E: Event>(HashMap<OwnedEventId, E>);
#[allow(unused)]
impl<E: Event + Clone> TestStore<E> {
fn get_event(&self, room_id: &RoomId, event_id: &EventId) -> Result<E> {
self.0
.get(event_id)
.cloned()
.ok_or_else(|| Error::NotFound(format!("{} not found", event_id)))
}
/// Returns the events that correspond to the `event_ids` sorted in the same
/// order.
fn get_events(&self, room_id: &RoomId, event_ids: &[OwnedEventId]) -> Result<Vec<E>> {
let mut events = vec![];
for id in event_ids {
events.push(self.get_event(room_id, id)?);
}
Ok(events)
}
/// Returns a Vec of the related auth events to the given `event`.
fn auth_event_ids(
&self,
room_id: &RoomId,
event_ids: Vec<OwnedEventId>,
) -> Result<HashSet<OwnedEventId>> {
let mut result = HashSet::new();
let mut stack = event_ids;
// DFS for auth event chain
while !stack.is_empty() {
let ev_id = stack.pop().unwrap();
if result.contains(&ev_id) {
continue;
}
result.insert(ev_id.clone());
let event = self.get_event(room_id, ev_id.borrow())?;
stack.extend(event.auth_events().map(ToOwned::to_owned));
}
Ok(result)
}
/// Returns a vector representing the difference in auth chains of the given
/// `events`.
fn auth_chain_diff(
&self,
room_id: &RoomId,
event_ids: Vec<Vec<OwnedEventId>>,
) -> Result<Vec<OwnedEventId>> {
let mut auth_chain_sets = vec![];
for ids in event_ids {
// TODO state store `auth_event_ids` returns self in the event ids list
// when an event returns `auth_event_ids` self is not contained
let chain = self
.auth_event_ids(room_id, ids)?
.into_iter()
.collect::<HashSet<_>>();
auth_chain_sets.push(chain);
}
if let Some(first) = auth_chain_sets.first().cloned() {
let common = auth_chain_sets
.iter()
.skip(1)
.fold(first, |a, b| a.intersection(b).cloned().collect::<HashSet<_>>());
Ok(auth_chain_sets
.into_iter()
.flatten()
.filter(|id| !common.contains(id))
.collect())
} else {
Ok(vec![])
}
}
}
impl TestStore<Pdu> {
#[allow(clippy::type_complexity)]
fn set_up(
&mut self,
) -> (StateMap<OwnedEventId>, StateMap<OwnedEventId>, StateMap<OwnedEventId>) {
let create_event = to_pdu_event::<&EventId>(
"CREATE",
alice(),
TimelineEventType::RoomCreate,
Some(""),
to_raw_json_value(&json!({ "creator": alice() })).unwrap(),
&[],
&[],
);
let cre = create_event.event_id().to_owned();
self.0.insert(cre.clone(), create_event.clone());
let alice_mem = to_pdu_event(
"IMA",
alice(),
TimelineEventType::RoomMember,
Some(alice().to_string().as_str()),
member_content_join(),
&[cre.clone()],
&[cre.clone()],
);
self.0
.insert(alice_mem.event_id().to_owned(), alice_mem.clone());
let join_rules = to_pdu_event(
"IJR",
alice(),
TimelineEventType::RoomJoinRules,
Some(""),
to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(),
&[cre.clone(), alice_mem.event_id().to_owned()],
&[alice_mem.event_id().to_owned()],
);
self.0
.insert(join_rules.event_id().to_owned(), join_rules.clone());
// Bob and Charlie join at the same time, so there is a fork
// this will be represented in the state_sets when we resolve
let bob_mem = to_pdu_event(
"IMB",
bob(),
TimelineEventType::RoomMember,
Some(bob().to_string().as_str()),
member_content_join(),
&[cre.clone(), join_rules.event_id().to_owned()],
&[join_rules.event_id().to_owned()],
);
self.0
.insert(bob_mem.event_id().to_owned(), bob_mem.clone());
let charlie_mem = to_pdu_event(
"IMC",
charlie(),
TimelineEventType::RoomMember,
Some(charlie().to_string().as_str()),
member_content_join(),
&[cre, join_rules.event_id().to_owned()],
&[join_rules.event_id().to_owned()],
);
self.0
.insert(charlie_mem.event_id().to_owned(), charlie_mem.clone());
let state_at_bob = [&create_event, &alice_mem, &join_rules, &bob_mem]
.iter()
.map(|ev| {
(
(ev.event_type().clone().into(), ev.state_key().unwrap().into()),
ev.event_id().to_owned(),
)
})
.collect::<StateMap<_>>();
let state_at_charlie = [&create_event, &alice_mem, &join_rules, &charlie_mem]
.iter()
.map(|ev| {
(
(ev.event_type().clone().into(), ev.state_key().unwrap().into()),
ev.event_id().to_owned(),
)
})
.collect::<StateMap<_>>();
let expected = [&create_event, &alice_mem, &join_rules, &bob_mem, &charlie_mem]
.iter()
.map(|ev| {
(
(ev.event_type().clone().into(), ev.state_key().unwrap().into()),
ev.event_id().to_owned(),
)
})
.collect::<StateMap<_>>();
(state_at_bob, state_at_charlie, expected)
}
}
fn event_id(id: &str) -> OwnedEventId {
if id.contains('$') {
return id.try_into().unwrap();
}
format!("${}:foo", id).try_into().unwrap()
}
fn alice() -> &'static UserId { user_id!("@alice:foo") }
fn bob() -> &'static UserId { user_id!("@bob:foo") }
fn charlie() -> &'static UserId { user_id!("@charlie:foo") }
fn ella() -> &'static UserId { user_id!("@ella:foo") }
fn room_id() -> &'static RoomId { room_id!("!test:foo") }
fn member_content_ban() -> Box<RawJsonValue> {
to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Ban)).unwrap()
}
fn member_content_join() -> Box<RawJsonValue> {
to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Join)).unwrap()
}
fn to_pdu_event<S>(
id: &str,
sender: &UserId,
ev_type: TimelineEventType,
state_key: Option<&str>,
content: Box<RawJsonValue>,
auth_events: &[S],
prev_events: &[S],
) -> Pdu
where
S: AsRef<str>,
{
// We don't care if the addition happens in order just that it is atomic
// (each event has its own value)
let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst);
let id = if id.contains('$') {
id.to_owned()
} else {
format!("${}:foo", id)
};
let auth_events = auth_events
.iter()
.map(AsRef::as_ref)
.map(event_id)
.collect::<Vec<_>>();
let prev_events = prev_events
.iter()
.map(AsRef::as_ref)
.map(event_id)
.collect::<Vec<_>>();
Pdu {
event_id: id.try_into().unwrap(),
room_id: Some(room_id().to_owned()),
sender: sender.to_owned(),
origin_server_ts: ts.try_into().unwrap(),
state_key: state_key.map(Into::into),
kind: ev_type,
content,
origin: None,
redacts: None,
unsigned: None,
auth_events,
prev_events,
depth: uint!(0),
hashes: EventHash { sha256: String::new() },
signatures: None,
}
}
// all graphs start with these input events
#[allow(non_snake_case)]
fn INITIAL_EVENTS() -> HashMap<OwnedEventId, Pdu> {
vec![
to_pdu_event::<&EventId>(
"CREATE",
alice(),
TimelineEventType::RoomCreate,
Some(""),
to_raw_json_value(&json!({ "creator": alice() })).unwrap(),
&[],
&[],
),
to_pdu_event(
"IMA",
alice(),
TimelineEventType::RoomMember,
Some(alice().as_str()),
member_content_join(),
&["CREATE"],
&["CREATE"],
),
to_pdu_event(
"IPOWER",
alice(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100 } })).unwrap(),
&["CREATE", "IMA"],
&["IMA"],
),
to_pdu_event(
"IJR",
alice(),
TimelineEventType::RoomJoinRules,
Some(""),
to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(),
&["CREATE", "IMA", "IPOWER"],
&["IPOWER"],
),
to_pdu_event(
"IMB",
bob(),
TimelineEventType::RoomMember,
Some(bob().to_string().as_str()),
member_content_join(),
&["CREATE", "IJR", "IPOWER"],
&["IJR"],
),
to_pdu_event(
"IMC",
charlie(),
TimelineEventType::RoomMember,
Some(charlie().to_string().as_str()),
member_content_join(),
&["CREATE", "IJR", "IPOWER"],
&["IMB"],
),
to_pdu_event::<&EventId>(
"START",
charlie(),
TimelineEventType::RoomTopic,
Some(""),
to_raw_json_value(&json!({})).unwrap(),
&[],
&[],
),
to_pdu_event::<&EventId>(
"END",
charlie(),
TimelineEventType::RoomTopic,
Some(""),
to_raw_json_value(&json!({})).unwrap(),
&[],
&[],
),
]
.into_iter()
.map(|ev| (ev.event_id().to_owned(), ev))
.collect()
}
// all graphs start with these input events
#[allow(non_snake_case)]
fn BAN_STATE_SET() -> HashMap<OwnedEventId, Pdu> {
vec![
to_pdu_event(
"PA",
alice(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(),
&["CREATE", "IMA", "IPOWER"], // auth_events
&["START"], // prev_events
),
to_pdu_event(
"PB",
alice(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(),
&["CREATE", "IMA", "IPOWER"],
&["END"],
),
to_pdu_event(
"MB",
alice(),
TimelineEventType::RoomMember,
Some(ella().as_str()),
member_content_ban(),
&["CREATE", "IMA", "PB"],
&["PA"],
),
to_pdu_event(
"IME",
ella(),
TimelineEventType::RoomMember,
Some(ella().as_str()),
member_content_join(),
&["CREATE", "IJR", "PA"],
&["MB"],
),
]
.into_iter()
.map(|ev| (ev.event_id().to_owned(), ev))
.collect()
}

View File

@@ -1,4 +1,3 @@
use ruma::OwnedEventId;
use serde_json::Error as JsonError;
use thiserror::Error;
@@ -15,28 +14,10 @@ pub enum Error {
Unsupported(String),
/// The given event was not found.
#[error("Event not found: {0}")]
#[error("Not found error: {0}")]
NotFound(String),
/// A required event this event depended on could not be fetched,
/// either as it was missing, or because it was invalid
#[error("Failed to fetch required {0} event: {1}")]
DependencyFailed(OwnedEventId, String),
/// Invalid fields in the given PDU.
#[error("Invalid PDU: {0}")]
InvalidPdu(String),
/// This event failed an authorization condition.
#[error("Auth check failed: {0}")]
AuthConditionFailed(String),
/// This event contained multiple auth events of the same type and state
/// key.
#[error("Duplicate auth events: {0}")]
DuplicateAuthEvents(String),
/// This event contains unnecessary auth events.
#[error("Unknown or unnecessary auth events present: {0}")]
UnselectedAuthEvents(String),
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,238 +0,0 @@
//! Auth checks relevant to any event's `auth_events`.
//!
//! See: https://spec.matrix.org/v1.16/rooms/v12/#authorization-rules
use std::collections::{HashMap, HashSet};
use ruma::{
EventId, OwnedEventId, RoomId, UserId,
events::{
StateEventType, TimelineEventType,
room::member::{MembershipState, RoomMemberEventContent, ThirdPartyInvite},
},
};
use crate::{Event, EventTypeExt, Pdu, RoomVersion, matrix::StateKey, state_res::Error, warn};
/// For the given event `kind` what are the relevant auth events that are needed
/// to authenticate this `content`.
///
/// # Errors
///
/// This function will return an error if the supplied `content` is not a JSON
/// object.
pub fn auth_types_for_event(
room_version: &RoomVersion,
event_type: &TimelineEventType,
state_key: Option<&StateKey>,
sender: &UserId,
member_content: Option<RoomMemberEventContent>,
) -> serde_json::Result<Vec<(StateEventType, StateKey)>> {
if event_type == &TimelineEventType::RoomCreate {
// Create events never have auth events
return Ok(vec![]);
}
let mut auth_types = if room_version.room_ids_as_hashes {
vec![
StateEventType::RoomMember.with_state_key(sender.as_str()),
StateEventType::RoomPowerLevels.with_state_key(""),
]
} else {
// For room versions that do not use room IDs as hashes, include the
// RoomCreate event as an auth event.
vec![
StateEventType::RoomMember.with_state_key(sender.as_str()),
StateEventType::RoomPowerLevels.with_state_key(""),
StateEventType::RoomCreate.with_state_key(""),
]
};
if event_type == &TimelineEventType::RoomMember {
let member_content =
member_content.expect("member_content must be provided for RoomMember events");
// Include the target's membership (if available)
auth_types.push((
StateEventType::RoomMember,
state_key
.expect("state_key must be provided for RoomMember events")
.to_owned(),
));
if matches!(
member_content.membership,
MembershipState::Join | MembershipState::Invite | MembershipState::Knock
) {
// Include the join rules
auth_types.push(StateEventType::RoomJoinRules.with_state_key(""));
}
if matches!(member_content.membership, MembershipState::Invite) {
// If this is an invite, include the third party invite if it exists
if let Some(ThirdPartyInvite { signed, .. }) = member_content.third_party_invite {
auth_types
.push(StateEventType::RoomThirdPartyInvite.with_state_key(signed.token));
}
}
if matches!(member_content.membership, MembershipState::Join)
&& room_version.restricted_join_rules
{
// If this is a restricted join, include the authorizing user's membership
if let Some(authorizing_user) = member_content.join_authorized_via_users_server {
auth_types
.push(StateEventType::RoomMember.with_state_key(authorizing_user.as_str()));
}
}
}
Ok(auth_types)
}
/// Checks for duplicate auth events in the `auth_events` field of an event.
/// Note: the caller should already have all of the auth events fetched.
///
/// If there are multiple auth events of the same type and state key, this
/// returns an error. Otherwise, it returns a map of (type, state_key) to the
/// corresponding auth event.
pub async fn check_duplicate_auth_events<FE>(
auth_events: &[OwnedEventId],
fetch_event: FE,
) -> Result<HashMap<(StateEventType, StateKey), Pdu>, Error>
where
FE: AsyncFn(&EventId) -> Result<Option<Pdu>, Error>,
{
let mut seen: HashMap<(StateEventType, StateKey), Pdu> = HashMap::new();
// Considering all of the event's auth events:
for auth_event_id in auth_events {
if let Ok(Some(auth_event)) = fetch_event(auth_event_id).await {
let event_type = auth_event.kind();
// If this is not a state event, reject it.
let Some(state_key) = &auth_event.state_key() else {
return Err(Error::InvalidPdu(format!(
"Auth event {:?} is not a state event",
auth_event_id
)));
};
let type_key_pair: (StateEventType, StateKey) =
event_type.clone().with_state_key(state_key.clone());
// If there are duplicate entries for a given type and state_key pair, reject.
if seen.contains_key(&type_key_pair) {
return Err(Error::DuplicateAuthEvents(format!(
"({:?},\"{:?}\")",
event_type, state_key
)));
}
seen.insert(type_key_pair, auth_event);
} else {
return Err(Error::NotFound(auth_event_id.as_str().to_owned()));
}
}
Ok(seen)
}
// Checks that the event does not refer to any auth events that it does not need
// to.
pub fn check_unnecessary_auth_events(
auth_events: &HashSet<(StateEventType, StateKey)>,
expected: &Vec<(StateEventType, StateKey)>,
) -> Result<(), Error> {
// If there are entries whose type and state_key don't match those specified by
// the auth events selection algorithm described in the server specification,
// reject.
let remaining = auth_events
.iter()
.filter(|key| !expected.contains(key))
.collect::<HashSet<_>>();
if !remaining.is_empty() {
return Err(Error::UnselectedAuthEvents(format!("{:?}", remaining)));
}
Ok(())
}
// Checks that all provided auth events were not rejected previously.
//
// TODO: this is currently a no-op and always returns Ok(()).
pub fn check_all_auth_events_accepted(
_auth_events: &HashMap<(StateEventType, StateKey), Pdu>,
) -> Result<(), Error> {
Ok(())
}
// Checks that all auth events are from the same room as the event being
// validated.
pub fn check_auth_same_room(auth_events: &Vec<Pdu>, room_id: &RoomId) -> bool {
for auth_event in auth_events {
if let Some(auth_room_id) = &auth_event.room_id() {
if auth_room_id.as_str() != room_id.as_str() {
warn!(
auth_event_id=%auth_event.event_id(),
"Auth event room id {} does not match expected room id {}",
auth_room_id,
room_id
);
return false;
}
} else {
warn!(auth_event_id=%auth_event.event_id(), "Auth event has no room_id");
return false;
}
}
true
}
/// Performs all auth event checks for the given event.
pub async fn check_auth_events<FE>(
event: &Pdu,
room_id: &RoomId,
room_version: &RoomVersion,
fetch_event: &FE,
) -> Result<HashMap<(StateEventType, StateKey), Pdu>, Error>
where
FE: AsyncFn(&EventId) -> Result<Option<Pdu>, Error>,
{
// If there are duplicate entries for a given type and state_key pair, reject.
let auth_events_map = check_duplicate_auth_events(&event.auth_events, fetch_event).await?;
let auth_events_set: HashSet<(StateEventType, StateKey)> =
auth_events_map.keys().cloned().collect();
// If there are entries whose type and state_key dont match those specified by
// the auth events selection algorithm described in the server specification,
// reject.
let member_event_content = match event.kind() {
| TimelineEventType::RoomMember =>
Some(event.get_content::<RoomMemberEventContent>().map_err(|e| {
Error::InvalidPdu(format!("Failed to parse m.room.member content: {}", e))
})?),
| _ => None,
};
let expected_auth_events = auth_types_for_event(
room_version,
event.kind(),
event.state_key.as_ref(),
event.sender(),
member_event_content,
)?;
if let Err(e) = check_unnecessary_auth_events(&auth_events_set, &expected_auth_events) {
return Err(e);
}
// If there are entries which were themselves rejected under the checks
// performed on receipt of a PDU, reject.
if let Err(e) = check_all_auth_events_accepted(&auth_events_map) {
return Err(e);
}
// If any event in auth_events has a room_id which does not match that of the
// event being authorised, reject.
let auth_event_refs: Vec<Pdu> = auth_events_map.values().cloned().collect();
if !check_auth_same_room(&auth_event_refs, room_id) {
return Err(Error::InvalidPdu(
"One or more auth events are from a different room".to_owned(),
));
}
Ok(auth_events_map)
}

View File

@@ -1,113 +0,0 @@
//! Context for event authorisation checks
use ruma::{
Int, OwnedUserId, UserId,
events::{
StateEventType,
room::{create::RoomCreateEventContent, power_levels::RoomPowerLevelsEventContent},
},
};
use crate::{Event, EventTypeExt, Pdu, RoomVersion, matrix::StateKey, state_res::Error};
pub enum UserPower {
/// Creator indicates this user should be granted a power level above all.
Creator,
/// Standard indicates power levels should be used to determine rank.
Standard,
}
impl PartialEq for UserPower {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
| (UserPower::Creator, UserPower::Creator) => true,
| (UserPower::Standard, UserPower::Standard) => true,
| _ => false,
}
}
}
/// Get the creators of the room.
/// If this room only supports one creator, a vec of one will be returned.
/// If multiple creators are supported, all will be returned, with the
/// m.room.create sender first.
pub async fn calculate_creators<FS>(
room_version: &RoomVersion,
fetch_state: FS,
) -> Result<Vec<OwnedUserId>, Error>
where
FS: AsyncFn((StateEventType, StateKey)) -> Result<Option<Pdu>, Error>,
{
let create_event = fetch_state(StateEventType::RoomCreate.with_state_key(""))
.await?
.ok_or_else(|| Error::InvalidPdu("Room create event not found".to_owned()))?;
let content = create_event
.get_content::<RoomCreateEventContent>()
.map_err(|e| {
Error::InvalidPdu(format!("Room create event has invalid content: {}", e))
})?;
if room_version.explicitly_privilege_room_creators {
let mut creators = vec![create_event.sender().to_owned()];
if let Some(additional) = content.additional_creators {
for user_id in additional {
if !creators.contains(&user_id) {
creators.push(user_id);
}
}
}
Ok(creators)
} else if room_version.use_room_create_sender {
Ok(vec![create_event.sender().to_owned()])
} else {
// Have to check the event content
#[allow(deprecated)]
if let Some(creator) = content.creator {
Ok(vec![creator])
} else {
Err(Error::InvalidPdu("Room create event missing creator field".to_owned()))
}
}
}
/// Rank fetches the creatorship and power level of the target user
///
/// Returns (UserPower, power_level, Option<RoomPowerLevelsEventContent>)
/// If UserPower::Creator is returned, the power_level and
/// RoomPowerLevelsEventContent will be meaningless and can be ignored.
pub async fn get_rank<FS>(
room_version: &RoomVersion,
fetch_state: &FS,
user_id: &UserId,
) -> Result<(UserPower, Int, Option<RoomPowerLevelsEventContent>), Error>
where
FS: AsyncFn((StateEventType, StateKey)) -> Result<Option<Pdu>, Error>,
{
let creators = calculate_creators(room_version, &fetch_state).await?;
if creators.contains(&user_id.to_owned()) && room_version.explicitly_privilege_room_creators {
return Ok((UserPower::Creator, Int::MAX, None));
}
let power_levels = fetch_state(StateEventType::RoomPowerLevels.with_state_key("")).await?;
if let Some(power_levels) = power_levels {
let power_levels = power_levels
.get_content::<RoomPowerLevelsEventContent>()
.map_err(|e| {
Error::InvalidPdu(format!("m.room.power_levels event has invalid content: {}", e))
})?;
Ok((
UserPower::Standard,
*power_levels
.users
.get(user_id)
.unwrap_or(&power_levels.users_default),
Some(power_levels),
))
} else {
// No power levels event, use defaults
if creators[0] == user_id {
return Ok((UserPower::Creator, Int::MAX, None));
}
Ok((UserPower::Standard, Int::from(0), None))
}
}

View File

@@ -1,97 +0,0 @@
//! Auth checks relevant to the `m.room.create` event specifically.
//!
//! See: https://spec.matrix.org/v1.16/rooms/v12/#authorization-rules
use ruma::{OwnedUserId, RoomVersionId, events::room::create::RoomCreateEventContent};
use serde::Deserialize;
use serde_json::from_str;
use crate::{Event, Pdu, RoomVersion, state_res::Error, trace};
// A raw representation of the create event content, for initial parsing.
// This allows us to extract fields without fully validating the event first.
#[derive(Deserialize)]
struct RawCreateContent {
creator: Option<String>,
room_version: Option<String>,
additional_creators: Option<Vec<String>>,
}
// Check whether an `m.room.create` event is valid.
// This ensures that:
//
// 1. The event has no `prev_events`
// 2. If the version disallows it, the event has no `room_id` present.
// 3. If the room version is present and recognised, otherwise assume invalid.
// 4. If the room version supports it, `additional_creators` is populated with
// valid user IDs.
// 5. If the room version supports it, `creator` is populated AND is a valid
// user ID.
// 6. Otherwise, this event is valid.
//
// The fully deserialized `RoomCreateEventContent` is returned for further calls
// to other checks.
pub fn check_room_create(event: &Pdu) -> Result<RoomCreateEventContent, Error> {
// Check 1: The event has no `prev_events`
if !event.prev_events.is_empty() {
return Err(Error::InvalidPdu("m.room.create event has prev_events".to_owned()));
}
let create_content = from_str::<RawCreateContent>(event.content().get())?;
// Note: Here we attempt to both load the raw room version string and validate
// it, and then cast it to the room features. If either step fails, we return
// an unsupported error. If the room version is missing, it defaults to "1",
// which we also do not support.
//
// This performs check 3, which then allows us to perform check 2.
let room_version = if let Some(raw_room_version) = create_content.room_version {
trace!("Parsing and interpreting room version: {}", raw_room_version);
let room_version_id = RoomVersionId::try_from(raw_room_version.as_str())
.map_err(|_| Error::Unsupported(raw_room_version))?;
RoomVersion::new(&room_version_id)
.map_err(|_| Error::Unsupported(room_version_id.as_str().to_owned()))?
} else {
return Err(Error::Unsupported("1".to_owned()));
};
// Check 2: If the version disallows it, the event has no `room_id` present.
if room_version.room_ids_as_hashes && event.room_id.is_some() {
return Err(Error::InvalidPdu(
"m.room.create event has room_id but room version disallows it".to_owned(),
));
}
// Check 4: If the room version supports it, `additional_creators` is populated
// with valid user IDs.
if room_version.explicitly_privilege_room_creators {
if let Some(additional_creators) = create_content.additional_creators {
for creator in additional_creators {
trace!("Validating additional creator user ID: {}", creator);
if OwnedUserId::parse(&creator).is_err() {
return Err(Error::InvalidPdu(format!(
"Invalid user ID in additional_creators: {creator}"
)));
}
}
}
}
// Check 5: If the room version supports it, `creator` is populated AND is a
// valid user ID.
if !room_version.use_room_create_sender {
if let Some(creator) = create_content.creator {
trace!("Validating creator user ID: {}", creator);
if OwnedUserId::parse(&creator).is_err() {
return Err(Error::InvalidPdu(format!("Invalid user ID in creator: {creator}")));
}
} else {
return Err(Error::InvalidPdu(
"m.room.create event missing creator field".to_owned(),
));
}
}
// Deserialise into the full create event for future checks.
Ok(from_str::<RoomCreateEventContent>(event.content().get())?)
}

View File

@@ -1,650 +0,0 @@
use ruma::{
EventId, OwnedUserId, RoomVersionId,
events::{
StateEventType, TimelineEventType,
room::{create::RoomCreateEventContent, member::MembershipState},
},
int,
serde::Raw,
};
use serde::{Deserialize, de::IgnoredAny};
use serde_json::from_str as from_json_str;
use crate::{
Event, EventTypeExt, Pdu, RoomVersion, debug, error,
matrix::StateKey,
state_res::{
error::Error,
event_auth::{
auth_events::check_auth_events,
context::{UserPower, calculate_creators, get_rank},
create_event::check_room_create,
member_event::check_member_event,
power_levels::check_power_levels,
},
},
trace, warn,
};
// FIXME: field extracting could be bundled for `content`
#[derive(Deserialize)]
struct GetMembership {
membership: MembershipState,
}
#[derive(Deserialize, Debug)]
struct RoomMemberContentFields {
membership: Option<Raw<MembershipState>>,
join_authorised_via_users_server: Option<Raw<OwnedUserId>>,
}
#[derive(Deserialize)]
struct RoomCreateContentFields {
room_version: Option<Raw<RoomVersionId>>,
creator: Option<Raw<IgnoredAny>>,
additional_creators: Option<Vec<Raw<OwnedUserId>>>,
#[serde(rename = "m.federate", default = "ruma::serde::default_true")]
federate: bool,
}
/// Authenticate the incoming `event`.
///
/// The steps of authentication are:
///
/// * check that the event is being authenticated for the correct room
/// * then there are checks for specific event types
///
/// The `fetch_state` closure should gather state from a state snapshot. We need
/// to know if the event passes auth against some state not a recursive
/// collection of auth_events fields.
#[tracing::instrument(
skip_all,
fields(
event_id = incoming_event.event_id().as_str(),
event_type = ?incoming_event.event_type().to_string()
)
)]
#[allow(clippy::suspicious_operation_groupings)]
pub async fn auth_check<FE, FS>(
room_version: &RoomVersion,
incoming_event: &Pdu,
fetch_event: &FE,
fetch_state: &FS,
create_event: Option<&Pdu>,
) -> Result<bool, Error>
where
FE: AsyncFn(&EventId) -> Result<Option<Pdu>, Error>,
FS: AsyncFn((StateEventType, StateKey)) -> Result<Option<Pdu>, Error>,
{
debug!("auth_check beginning");
let sender = incoming_event.sender();
// Since v1, If type is m.room.create:
if *incoming_event.event_type() == TimelineEventType::RoomCreate {
debug!("start m.room.create check");
if let Err(e) = check_room_create(incoming_event) {
warn!("m.room.create event has been rejected: {}", e);
return Ok(false);
}
debug!("m.room.create event was allowed");
return Ok(true);
}
let Some(create_event) = create_event else {
error!("no create event provided for auth check");
return Err(Error::InvalidPdu("missing create event".to_owned()));
};
// TODO: we need to know if events have previously been rejected or soft failed
// For now, we'll just assume the create_event is valid.
let create_content = from_json_str::<RoomCreateEventContent>(create_event.content().get())
.expect("provided create event must be valid");
// Since v12, If the events room_id is not an event ID for an accepted (not
// rejected) m.room.create event, with the sigil ! instead of $, reject.
if room_version.room_ids_as_hashes {
let calculated_room_id = create_event.event_id().as_str().replace('$', "!");
if let Some(claimed_room_id) = create_event.room_id() {
if claimed_room_id.as_str() != calculated_room_id {
warn!(
expected = %calculated_room_id,
received = %claimed_room_id,
"event's room ID does not match the hash of the m.room.create event ID"
);
return Ok(false);
}
} else {
warn!("event is missing a room ID");
return Ok(false);
}
}
let room_id = incoming_event.room_id().expect("event must have a room ID");
let auth_map =
match check_auth_events(incoming_event, room_id, &room_version, fetch_event).await {
| Ok(map) => map,
| Err(e) => {
warn!("event's auth events are invalid: {}", e);
return Ok(false);
},
};
// Considering the event's auth_events
// Since v1, If the content of the m.room.create event in the room state has the
// property m.federate set to false, and the sender domain of the event does
// not match the sender domain of the create event, reject.
if !create_content.federate {
if create_event.sender().server_name() != incoming_event.sender().server_name() {
warn!(
sender = %incoming_event.sender(),
create_sender = %create_event.sender(),
"room is not federated and event's sender domain does not match create event's sender domain"
);
return Ok(false);
}
}
// From v1 to v5, If type is m.room.aliases
if room_version.special_case_aliases_auth
&& *incoming_event.event_type() == TimelineEventType::RoomAliases
{
if let Some(state_key) = incoming_event.state_key() {
// If sender's domain doesn't matches state_key, reject
if state_key != sender.server_name().as_str() {
warn!("state_key does not match sender");
return Ok(false);
}
// Otherwise, allow
return Ok(true);
}
// If event has no state_key, reject.
warn!("m.room.alias event has no state key");
return Ok(false);
}
// From v1, If type is m.room.member
if *incoming_event.event_type() == TimelineEventType::RoomMember {
if let Err(e) =
check_member_event(&room_version, incoming_event, fetch_event, fetch_state).await
{
warn!("m.room.member event has been rejected: {}", e);
return Ok(false);
}
}
// From v1, If the sender's current membership state is not join, reject
let sender_member_event =
match auth_map.get(&StateEventType::RoomMember.with_state_key(sender.as_str())) {
| Some(ev) => ev,
| None => {
warn!(
%sender,
"sender is not joined - no membership event found for sender in auth events"
);
return Ok(false);
},
};
let sender_membership_event_content: RoomMemberContentFields =
from_json_str(sender_member_event.content().get())?;
let Some(membership_state) = sender_membership_event_content.membership else {
warn!(
?sender_membership_event_content,
"Sender membership event content missing membership field"
);
return Err(Error::InvalidPdu("Missing membership field".to_owned()));
};
let membership_state = membership_state.deserialize()?;
if membership_state != MembershipState::Join {
warn!(
%sender,
?membership_state,
"sender cannot send events without being joined to the room"
);
return Ok(false);
}
// From v1, If type is m.room.third_party_invite
let (rank, sender_pl, pl_evt) = get_rank(&room_version, fetch_state, sender).await?;
// Allow if and only if sender's current power level is greater than
// or equal to the invite level
if *incoming_event.event_type() == TimelineEventType::RoomThirdPartyInvite {
if rank == UserPower::Creator {
trace!("sender is room creator, allowing m.room.third_party_invite");
return Ok(true);
}
let invite_level = match &pl_evt {
| Some(power_levels) => power_levels.invite,
| None => int!(0),
};
if sender_pl < invite_level {
warn!(
%sender,
has=%sender_pl,
required=%invite_level,
"sender cannot send invites in this room"
);
return Ok(false);
}
debug!("m.room.third_party_invite event was allowed");
return Ok(true);
}
// Since v1, if the event types required power level is greater than the
// senders power level, reject.
let required_level = match &pl_evt {
| Some(power_levels) => power_levels
.events
.get(incoming_event.kind())
.unwrap_or_else(|| {
if incoming_event.state_key.is_some() {
&power_levels.state_default
} else {
&power_levels.events_default
}
}),
| None => &int!(0),
};
if rank != UserPower::Creator && sender_pl < *required_level {
warn!(
%sender,
has=%sender_pl,
required=%required_level,
"sender does not have enough power level to send this event"
);
return Ok(false);
}
// Since v1, If the event has a state_key that starts with an @ and does not
// match the sender, reject.
if let Some(state_key) = incoming_event.state_key() {
if state_key.starts_with('@') && state_key != sender.as_str() {
warn!(
%sender,
%state_key,
"event's state key starts with @ and does not match sender"
);
return Ok(false);
}
}
// Since v1, If type is m.room.power_levels
if *incoming_event.event_type() == TimelineEventType::RoomPowerLevels {
let creators = calculate_creators(&room_version, fetch_state).await?;
if let Err(e) =
check_power_levels(&room_version, incoming_event, pl_evt.as_ref(), creators).await
{
warn!(
%sender,
"m.room.power_levels event has been rejected: {}", e
);
return Ok(false);
}
}
// From v1 to v2: If type is m.room.redaction:
// If the senders power level is greater than or equal to the redact level,
// allow.
// If the domain of the event_id of the event being redacted is the same as the
// domain of the event_id of the m.room.redaction, allow.
// Otherwise, reject.
if room_version.extra_redaction_checks {
// We'll panic here, since while we don't theoretically support the room
// versions that require this, we don't want to incorrectly permit an event
// that should be rejected in this theoretically impossible scenario.
unreachable!(
"continuwuity does not support room versions that require extra redaction checks"
);
}
debug!("allowing event passed all checks");
Ok(true)
}
#[cfg(test)]
mod tests {
use ruma::events::{
StateEventType, TimelineEventType,
room::{
join_rules::{
AllowRule, JoinRule, Restricted, RoomJoinRulesEventContent, RoomMembership,
},
member::{MembershipState, RoomMemberEventContent},
},
};
use serde_json::value::to_raw_value as to_raw_json_value;
use crate::{
matrix::{Event, EventTypeExt, Pdu as PduEvent},
state_res::{
RoomVersion, StateMap,
event_auth::{
iterative_auth_checks::valid_membership_change, valid_membership_change,
},
test_utils::{
INITIAL_EVENTS, INITIAL_EVENTS_CREATE_ROOM, alice, charlie, ella, event_id,
member_content_ban, member_content_join, room_id, to_pdu_event,
},
},
};
#[test]
fn test_ban_pass() {
let _ = tracing::subscriber::set_default(
tracing_subscriber::fmt().with_test_writer().finish(),
);
let events = INITIAL_EVENTS();
let auth_events = events
.values()
.map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.clone()))
.collect::<StateMap<_>>();
let requester = to_pdu_event(
"HELLO",
alice(),
TimelineEventType::RoomMember,
Some(charlie().as_str()),
member_content_ban(),
&[],
&["IMC"],
);
let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned();
let target_user = charlie();
let sender = alice();
assert!(
valid_membership_change(
&RoomVersion::V6,
target_user,
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
sender,
fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(),
&requester,
None::<&PduEvent>,
fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(),
fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(),
None,
&MembershipState::Leave,
&fetch_state(StateEventType::RoomCreate, "".into()).unwrap(),
)
.unwrap()
);
}
#[test]
fn test_join_non_creator() {
let _ = tracing::subscriber::set_default(
tracing_subscriber::fmt().with_test_writer().finish(),
);
let events = INITIAL_EVENTS_CREATE_ROOM();
let auth_events = events
.values()
.map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.clone()))
.collect::<StateMap<_>>();
let requester = to_pdu_event(
"HELLO",
charlie(),
TimelineEventType::RoomMember,
Some(charlie().as_str()),
member_content_join(),
&["CREATE"],
&["CREATE"],
);
let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned();
let target_user = charlie();
let sender = charlie();
assert!(
!valid_membership_change(
&RoomVersion::V6,
target_user,
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
sender,
fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(),
&requester,
None::<&PduEvent>,
fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(),
fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(),
None,
&MembershipState::Leave,
&fetch_state(StateEventType::RoomCreate, "".into()).unwrap(),
)
.unwrap()
);
}
#[test]
fn test_join_creator() {
let _ = tracing::subscriber::set_default(
tracing_subscriber::fmt().with_test_writer().finish(),
);
let events = INITIAL_EVENTS_CREATE_ROOM();
let auth_events = events
.values()
.map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.clone()))
.collect::<StateMap<_>>();
let requester = to_pdu_event(
"HELLO",
alice(),
TimelineEventType::RoomMember,
Some(alice().as_str()),
member_content_join(),
&["CREATE"],
&["CREATE"],
);
let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned();
let target_user = alice();
let sender = alice();
assert!(
valid_membership_change(
&RoomVersion::V6,
target_user,
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
sender,
fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(),
&requester,
None::<&PduEvent>,
fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(),
fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(),
None,
&MembershipState::Leave,
&fetch_state(StateEventType::RoomCreate, "".into()).unwrap(),
)
.unwrap()
);
}
#[test]
fn test_ban_fail() {
let _ = tracing::subscriber::set_default(
tracing_subscriber::fmt().with_test_writer().finish(),
);
let events = INITIAL_EVENTS();
let auth_events = events
.values()
.map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.clone()))
.collect::<StateMap<_>>();
let requester = to_pdu_event(
"HELLO",
charlie(),
TimelineEventType::RoomMember,
Some(alice().as_str()),
member_content_ban(),
&[],
&["IMC"],
);
let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned();
let target_user = alice();
let sender = charlie();
assert!(
!valid_membership_change(
&RoomVersion::V6,
target_user,
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
sender,
fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(),
&requester,
None::<&PduEvent>,
fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(),
fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(),
None,
&MembershipState::Leave,
&fetch_state(StateEventType::RoomCreate, "".into()).unwrap(),
)
.unwrap()
);
}
#[test]
fn test_restricted_join_rule() {
let _ = tracing::subscriber::set_default(
tracing_subscriber::fmt().with_test_writer().finish(),
);
let mut events = INITIAL_EVENTS();
*events.get_mut(&event_id("IJR")).unwrap() = to_pdu_event(
"IJR",
alice(),
TimelineEventType::RoomJoinRules,
Some(""),
to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Restricted(
Restricted::new(vec![AllowRule::RoomMembership(RoomMembership::new(
room_id().to_owned(),
))]),
)))
.unwrap(),
&["CREATE", "IMA", "IPOWER"],
&["IPOWER"],
);
let mut member = RoomMemberEventContent::new(MembershipState::Join);
member.join_authorized_via_users_server = Some(alice().to_owned());
let auth_events = events
.values()
.map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.clone()))
.collect::<StateMap<_>>();
let requester = to_pdu_event(
"HELLO",
ella(),
TimelineEventType::RoomMember,
Some(ella().as_str()),
to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Join)).unwrap(),
&["CREATE", "IJR", "IPOWER", "new"],
&["new"],
);
let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned();
let target_user = ella();
let sender = ella();
assert!(
valid_membership_change(
&RoomVersion::V9,
target_user,
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
sender,
fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(),
&requester,
None::<&PduEvent>,
fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(),
fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(),
Some(alice()),
&MembershipState::Join,
&fetch_state(StateEventType::RoomCreate, "".into()).unwrap(),
)
.unwrap()
);
assert!(
!valid_membership_change(
&RoomVersion::V9,
target_user,
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
sender,
fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(),
&requester,
None::<&PduEvent>,
fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(),
fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(),
Some(ella()),
&MembershipState::Leave,
&fetch_state(StateEventType::RoomCreate, "".into()).unwrap(),
)
.unwrap()
);
}
#[test]
fn test_knock() {
let _ = tracing::subscriber::set_default(
tracing_subscriber::fmt().with_test_writer().finish(),
);
let mut events = INITIAL_EVENTS();
*events.get_mut(&event_id("IJR")).unwrap() = to_pdu_event(
"IJR",
alice(),
TimelineEventType::RoomJoinRules,
Some(""),
to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Knock)).unwrap(),
&["CREATE", "IMA", "IPOWER"],
&["IPOWER"],
);
let auth_events = events
.values()
.map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.clone()))
.collect::<StateMap<_>>();
let requester = to_pdu_event(
"HELLO",
ella(),
TimelineEventType::RoomMember,
Some(ella().as_str()),
to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Knock)).unwrap(),
&[],
&["IMC"],
);
let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned();
let target_user = ella();
let sender = ella();
assert!(
valid_membership_change(
&RoomVersion::V7,
target_user,
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
sender,
fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(),
&requester,
None::<&PduEvent>,
fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(),
fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(),
None,
&MembershipState::Leave,
&fetch_state(StateEventType::RoomCreate, "".into()).unwrap(),
)
.unwrap()
);
}
}

View File

@@ -1,422 +0,0 @@
//! Auth checks relevant to the `m.room.member` event specifically.
//!
//! See: https://spec.matrix.org/v1.16/rooms/v12/#authorization-rules
use ruma::{
EventId, OwnedUserId, UserId,
events::{
StateEventType,
room::{
join_rules::{JoinRule, RoomJoinRulesEventContent},
third_party_invite::{PublicKey, RoomThirdPartyInviteEventContent},
},
},
serde::Base64,
signatures::{PublicKeyMap, PublicKeySet, verify_json},
};
use crate::{
Event, EventTypeExt, Pdu, RoomVersion,
matrix::StateKey,
state_res::{
Error,
event_auth::context::{UserPower, get_rank},
},
utils::to_canonical_object,
};
#[derive(serde::Deserialize, Default)]
struct PartialMembershipObject {
membership: Option<String>,
join_authorized_via_users_server: Option<OwnedUserId>,
third_party_invite: Option<serde_json::Value>,
}
/// Fetches the membership *content* of the target.
/// If there is not one, an empty leave membership is returned.
async fn fetch_membership<FS>(
fetch_state: &FS,
target: &UserId,
) -> Result<PartialMembershipObject, Error>
where
FS: AsyncFn((StateEventType, StateKey)) -> Result<Option<Pdu>, Error>,
{
fetch_state(StateEventType::RoomMember.with_state_key(target.as_str()))
.await
.map(|pdu| {
if let Some(ev) = pdu {
ev.get_content::<PartialMembershipObject>().map_err(|e| {
Error::InvalidPdu(format!("m.room.member event has invalid content: {}", e))
})
} else {
Ok(PartialMembershipObject {
membership: Some("leave".to_owned()),
..Default::default()
})
}
})?
}
async fn check_join_event<FE, FS>(
room_version: &RoomVersion,
event: &Pdu,
membership: &PartialMembershipObject,
target: &UserId,
fetch_event: &FE,
fetch_state: &FS,
) -> Result<(), Error>
where
FE: AsyncFn(&EventId) -> Result<Option<Pdu>, Error>,
FS: AsyncFn((StateEventType, StateKey)) -> Result<Option<Pdu>, Error>,
{
// 3.1: If the only previous event is an m.room.create and the state_key is the
// sender of the m.room.create, allow.
if event.prev_events.len() == 1 {
let only_prev = fetch_event(&event.prev_events[0]).await?;
if let Some(prev_event) = only_prev {
let k = prev_event.event_type().with_state_key("");
if k.0 == StateEventType::RoomCreate && k.1.as_str() == event.sender().as_str() {
return Ok(());
}
} else {
return Err(Error::DependencyFailed(
event.prev_events[0].to_owned(),
"Previous event not found when checking join event".to_owned(),
));
}
}
// 3.2: If the sender does not match state_key, reject.
if event.sender() != target {
return Err(Error::AuthConditionFailed(
"m.room.member join event sender does not match state_key".to_owned(),
));
}
let prev_membership = if let Some(ev) =
fetch_state(StateEventType::RoomMember.with_state_key(target.as_str())).await?
{
Some(ev.get_content::<PartialMembershipObject>().map_err(|e| {
Error::InvalidPdu(format!("Previous m.room.member event has invalid content: {}", e))
})?)
} else {
None
};
let join_rule_content =
if let Some(jr) = fetch_state(StateEventType::RoomJoinRules.with_state_key("")).await? {
jr.get_content::<RoomJoinRulesEventContent>().map_err(|e| {
Error::InvalidPdu(format!("m.room.join_rules event has invalid content: {}", e))
})?
} else {
// Default to invite if no join rules event is present.
RoomJoinRulesEventContent { join_rule: JoinRule::Private }
};
// 3.3: If the sender is banned, reject.
let prev_member = if let Some(prev_content) = &prev_membership {
if let Some(membership) = &prev_content.membership {
if membership == "ban" {
return Err(Error::AuthConditionFailed(
"m.room.member join event sender is banned".to_owned(),
));
}
membership
} else {
"leave"
}
} else {
"leave"
};
// 3.4: If the join_rule is invite or knock then allow if membership
// state is invite or join.
// 3.5: If the join_rule is restricted or knock_restricted:
// 3.5.1: If membership state is join or invite, allow.
match join_rule_content.join_rule {
| JoinRule::Invite | JoinRule::Knock => {
if prev_member == "invite" || prev_member == "join" {
return Ok(());
}
Err(Error::AuthConditionFailed(
"m.room.member join event not invited under invite/knock join rule".to_owned(),
))
},
| JoinRule::Restricted(_) | JoinRule::KnockRestricted(_) => {
// 3.5.2: If the join_authorised_via_users_server key in content is not a user
// with sufficient permission to invite other users or is not a joined
// member of the room, reject.
if prev_member == "invite" || prev_member == "join" {
return Ok(());
}
let join_authed_by = membership.join_authorized_via_users_server.as_ref();
if let Some(user_id) = join_authed_by {
let rank = get_rank(&room_version, fetch_state, user_id).await?;
if rank.0 == UserPower::Standard {
// This user is not a creator, check that they have
// sufficient power level
if rank.1 < rank.2.unwrap().invite {
return Err(Error::InvalidPdu(
"m.room.member join event join_authorised_via_users_server does not \
have sufficient power level to invite"
.to_owned(),
));
}
}
// Check that the user is a joined member of the room
if let Some(state_event) =
fetch_state(StateEventType::RoomMember.with_state_key(user_id.as_str()))
.await?
{
let state_content = state_event
.get_content::<PartialMembershipObject>()
.map_err(|e| {
Error::InvalidPdu(format!(
"m.room.member event has invalid content: {}",
e
))
})?;
if let Some(state_membership) = &state_content.membership {
if state_membership == "join" {
return Ok(());
}
}
}
} else {
return Err(Error::AuthConditionFailed(
"m.room.member join event missing join_authorised_via_users_server"
.to_owned(),
));
}
// 3.5.3: Otherwise, allow
return Ok(());
},
| JoinRule::Public => return Ok(()),
| _ => Err(Error::AuthConditionFailed(format!(
"unknown join rule: {:?}",
join_rule_content.join_rule
)))?,
}
}
/// Checks a third-party invite is valid.
async fn check_third_party_invite(
target_current_membership: PartialMembershipObject,
raw_third_party_invite: &serde_json::Value,
target: &UserId,
event: &Pdu,
fetch_state: impl AsyncFn((StateEventType, StateKey)) -> Result<Option<Pdu>, Error>,
) -> Result<(), Error> {
// 4.1.1: If target user is banned, reject.
if target_current_membership
.membership
.is_some_and(|m| m == "ban")
{
return Err(Error::AuthConditionFailed("invite target is banned".to_owned()));
}
// 4.1.2: If content.third_party_invite does not have a signed property, reject.
let signed = raw_third_party_invite.get("signed").ok_or_else(|| {
Error::AuthConditionFailed(
"invite event third_party_invite missing signed property".to_owned(),
)
})?;
// 4.2.3: If signed does not have mxid and token properties, reject.
let mxid = signed.get("mxid").and_then(|v| v.as_str()).ok_or_else(|| {
Error::AuthConditionFailed(
"invite event third_party_invite signed missing/invalid mxid property".to_owned(),
)
})?;
let token = signed
.get("token")
.and_then(|v| v.as_str())
.ok_or_else(|| {
Error::AuthConditionFailed(
"invite event third_party_invite signed missing token property".to_owned(),
)
})?;
// 4.2.4: If mxid does not match state_key, reject.
if mxid != target.as_str() {
return Err(Error::AuthConditionFailed(
"invite event third_party_invite signed mxid does not match state_key".to_owned(),
));
}
// 4.2.5: If there is no m.room.third_party_invite event in the room
// state matching the token, reject.
let Some(third_party_invite_event) =
fetch_state(StateEventType::RoomThirdPartyInvite.with_state_key(token)).await?
else {
return Err(Error::AuthConditionFailed(
"invite event third_party_invite token has no matching m.room.third_party_invite"
.to_owned(),
));
};
// 4.2.6: If sender does not match sender of the m.room.third_party_invite,
// reject.
if third_party_invite_event.sender() != event.sender() {
return Err(Error::AuthConditionFailed(
"invite event sender does not match m.room.third_party_invite sender".to_owned(),
));
}
// 4.2.7: If any signature in signed matches any public key in the
// m.room.third_party_invite event, allow. The public keys are in
// content of m.room.third_party_invite as:
// 1. A single public key in the public_key property.
// 2. A list of public keys in the public_keys property.
let tpi_content = third_party_invite_event
.get_content::<RoomThirdPartyInviteEventContent>()
.or_else(|_| {
Err(Error::InvalidPdu(
"m.room.third_party_invite event has invalid content".to_owned(),
))
})?;
let mut public_keys = tpi_content.public_keys.unwrap_or_default();
public_keys.push(PublicKey {
public_key: tpi_content.public_key,
key_validity_url: None,
});
let signatures = signed
.get("signatures")
.and_then(|v| v.as_object())
.ok_or_else(|| {
Error::InvalidPdu(
"invite event third_party_invite signed missing/invalid signatures".to_owned(),
)
})?;
let mut public_key_map = PublicKeyMap::new();
for (server_name, sig_map) in signatures {
let mut pk_set = PublicKeySet::new();
if let Some(sig_map) = sig_map.as_object() {
for (key_id, sig) in sig_map {
let sig_b64 = Base64::parse(sig.as_str().ok_or(Error::InvalidPdu(
"invite event third_party_invite signature is not a string".to_owned(),
))?)
.map_err(|_| {
Error::InvalidPdu(
"invite event third_party_invite signature is not valid Base64"
.to_owned(),
)
})?;
pk_set.insert(key_id.clone(), sig_b64);
}
}
public_key_map.insert(server_name.clone(), pk_set);
}
verify_json(
&public_key_map,
to_canonical_object(signed).expect("signed was already validated"),
)
.map_err(|e| {
Error::AuthConditionFailed(format!(
"invite event third_party_invite signature verification failed: {e}"
))
})?;
// If there was no error, there was a valid signature, so allow.
Ok(())
}
async fn check_invite_event<FS>(
room_version: &RoomVersion,
event: &Pdu,
membership: &PartialMembershipObject,
target: &UserId,
fetch_state: &FS,
) -> Result<(), Error>
where
FS: AsyncFn((StateEventType, StateKey)) -> Result<Option<Pdu>, Error>,
{
let target_current_membership = fetch_membership(fetch_state, target).await?;
// 4.1: If content has a third_party_invite property:
if let Some(raw_third_party_invite) = &membership.third_party_invite {
return check_third_party_invite(
target_current_membership,
raw_third_party_invite,
target,
event,
fetch_state,
)
.await;
}
// 4.2: If the senders current membership state is not join, reject.
let sender_membership = fetch_membership(fetch_state, event.sender()).await?;
if sender_membership.membership.is_none_or(|m| m != "join") {
return Err(Error::AuthConditionFailed("invite sender is not joined".to_owned()));
}
// 4.3: If target users current membership state is join or ban, reject.
if target_current_membership
.membership
.is_some_and(|m| m == "join" || m == "ban")
{
return Err(Error::AuthConditionFailed(
"invite target is already joined or banned".to_owned(),
));
}
// 4.4: If the senders power level is greater than or equal to the invite
// level, allow.
let (rank, pl, pl_evt) = get_rank(&room_version, fetch_state, event.sender()).await?;
if rank == UserPower::Creator || pl >= pl_evt.unwrap_or_default().invite {
return Ok(());
}
// 4.5: Otherwise, reject.
Err(Error::AuthConditionFailed(
"invite sender does not have sufficient power level to invite".to_owned(),
))
}
pub async fn check_member_event<FE, FS>(
room_version: &RoomVersion,
event: &Pdu,
fetch_event: FE,
fetch_state: FS,
) -> Result<(), Error>
where
FE: AsyncFn(&EventId) -> Result<Option<Pdu>, Error>,
FS: AsyncFn((StateEventType, StateKey)) -> Result<Option<Pdu>, Error>,
{
// 1. If there is no state_key property, or no membership property in content,
// reject.
if event.state_key.is_none() {
return Err(Error::InvalidPdu("m.room.member event missing state_key".to_owned()));
}
let target = UserId::parse(event.state_key().unwrap())
.map_err(|_| Error::InvalidPdu("m.room.member event has invalid state_key".to_owned()))?
.to_owned();
let content = event
.get_content::<PartialMembershipObject>()
.map_err(|e| {
Error::InvalidPdu(format!("m.room.member event has invalid content: {}", e))
})?;
if content.membership.is_none() {
return Err(Error::InvalidPdu(
"m.room.member event missing membership in content".to_owned(),
));
}
// 2: If content has a join_authorised_via_users_server key
//
// 2.1: If the event is not validly signed by the homeserver of the user ID
// denoted by the key, reject.
if let Some(_join_auth) = &content.join_authorized_via_users_server {
// We need to check the signature here, but don't have the means to do so yet.
todo!("Implement join_authorised_via_users_server check");
}
match content.membership.as_deref().unwrap() {
| "join" =>
check_join_event(room_version, event, &content, &target, &fetch_event, &fetch_state)
.await?,
| "invite" =>
check_invite_event(room_version, event, &content, &target, &fetch_state).await?,
| _ => {
todo!()
},
};
Ok(())
}

View File

@@ -1,6 +0,0 @@
pub mod auth_events;
mod context;
pub mod create_event;
pub mod iterative_auth_checks;
pub mod member_event;
mod power_levels;

View File

@@ -1,157 +0,0 @@
use ruma::{OwnedUserId, events::room::power_levels::RoomPowerLevelsEventContent};
use crate::{
Event, Pdu, RoomVersion,
state_res::{Error, event_auth::context::UserPower},
};
/// Verifies that a m.room.power_levels event is well-formed according to the
/// Matrix specification.
///
/// Creators must contain the m.room.create sender and any additional creators.
pub async fn check_power_levels(
room_version: &RoomVersion,
event: &Pdu,
current_power_levels: Option<&RoomPowerLevelsEventContent>,
creators: Vec<OwnedUserId>,
) -> Result<(), Error> {
let content = event
.get_content::<RoomPowerLevelsEventContent>()
.map_err(|e| {
Error::InvalidPdu(format!("m.room.power_levels event has invalid content: {}", e))
})?;
// If any of the properties users_default, events_default, state_default, ban,
// redact, kick, or invite in content are present and not an integer, reject.
//
// If either of the properties events or notifications in content are present
// and not an object with values that are integers, reject.
//
// NOTE: Deserialisation fails if this is not the case, so we don't need to
// check these here.
// If the users property in content is not an object with keys that are valid
// user IDs with values that are integers (or a string that is an integer),
// reject.
while let Some(user_id) = content.users.keys().next() {
// NOTE: Deserialisation fails if the power level is not an integer, so we don't
// need to check that here.
if let Err(e) = user_id.validate_historical() {
return Err(Error::InvalidPdu(format!(
"m.room.power_levels event has invalid user ID in users map: {}",
e
)));
}
// Since v12, If the users property in content contains the sender of the
// m.room.create event or any of the additional_creators array (if present)
// from the content of the m.room.create event, reject.
if room_version.explicitly_privilege_room_creators && creators.contains(user_id) {
return Err(Error::InvalidPdu(
"m.room.power_levels event users map contains a room creator".to_string(),
));
}
}
// If there is no previous m.room.power_levels event in the room, allow.
if current_power_levels.is_none() {
return Ok(());
}
let current_power_levels = current_power_levels.unwrap();
// For the properties users_default, events_default, state_default, ban, redact,
// kick, invite check if they were added, changed or removed. For each found
// alteration:
// If the current value is higher than the senders current power level, reject.
// If the new value is higher than the senders current power level, reject.
let sender = event.sender();
let rank = if room_version.explicitly_privilege_room_creators {
if creators.contains(&sender.to_owned()) {
UserPower::Creator
} else {
UserPower::Standard
}
} else {
UserPower::Standard
};
let sender_pl = current_power_levels
.users
.get(sender)
.unwrap_or(&current_power_levels.users_default);
if rank != UserPower::Creator {
let checks = [
("users_default", current_power_levels.users_default, content.users_default),
("events_default", current_power_levels.events_default, content.events_default),
("state_default", current_power_levels.state_default, content.state_default),
("ban", current_power_levels.ban, content.ban),
("redact", current_power_levels.redact, content.redact),
("kick", current_power_levels.kick, content.kick),
("invite", current_power_levels.invite, content.invite),
];
for (name, old_value, new_value) in checks.iter() {
if old_value != new_value {
if *old_value > *sender_pl {
return Err(Error::AuthConditionFailed(format!(
"sender cannot change level for {}",
name
)));
}
if *new_value > *sender_pl {
return Err(Error::AuthConditionFailed(format!(
"sender cannot raise level for {} to {}",
name, new_value
)));
}
}
}
// For each entry being changed in, or removed from, the events
// property:
// If the current value is greater than the senders current power level,
// reject.
for (event_type, new_value) in content.events.iter() {
let old_value = current_power_levels.events.get(event_type);
if old_value != Some(new_value) {
let old_pl = old_value.unwrap_or(&current_power_levels.events_default);
if *old_pl > *sender_pl {
return Err(Error::AuthConditionFailed(format!(
"sender cannot change event level for {}",
event_type
)));
}
if *new_value > *sender_pl {
return Err(Error::AuthConditionFailed(format!(
"sender cannot raise event level for {} to {}",
event_type, new_value
)));
}
}
}
// For each entry being changed in, or removed from, the events or
// notifications properties:
// If the current value is greater than the senders current power
// level, reject.
// If the new value is greater than the senders current power level,
// reject.
// TODO after making ruwuma's notifications value a BTreeMap
// For each entry being added to, or changed in, the users property:
// If the new value is greater than the senders current power level, reject.
for (user_id, new_value) in content.users.iter() {
let old_value = current_power_levels.users.get(user_id);
if old_value != Some(new_value) {
if *new_value > *sender_pl {
return Err(Error::AuthConditionFailed(format!(
"sender cannot raise user level for {} to {}",
user_id, new_value
)));
}
}
}
}
Ok(())
}

View File

@@ -8,6 +8,9 @@
#[cfg(test)]
mod test_utils;
#[cfg(test)]
mod benches;
use std::{
borrow::Borrow,
cmp::{Ordering, Reverse},
@@ -15,31 +18,30 @@
hash::{BuildHasher, Hash},
};
use futures::{Future, FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt};
use itertools::Itertools;
use futures::{Future, FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt, future};
use ruma::{
EventId, Int, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomVersionId,
events::{
room::member::{MembershipState, RoomMemberEventContent}, StateEventType,
TimelineEventType,
}, int, EventId, Int, MilliSecondsSinceUnixEpoch,
OwnedEventId,
RoomVersionId,
StateEventType, TimelineEventType,
room::member::{MembershipState, RoomMemberEventContent},
},
int,
};
use serde_json::from_str as from_json_str;
pub(crate) use self::error::Error;
use self::power_levels::PowerLevelsContentFields;
pub use self::{event_auth::iterative_auth_checks::auth_check, room_version::RoomVersion};
use crate::utils::TryFutureExtExt;
pub use self::{
event_auth::{auth_check, auth_types_for_event},
room_version::RoomVersion,
};
use crate::{
debug, err, error as log_error, matrix::{Event, StateKey},
state_res::{
event_auth::auth_events::auth_types_for_event, room_version::StateResolutionVersion,
},
debug, debug_error, err,
matrix::{Event, StateKey},
state_res::room_version::StateResolutionVersion,
trace,
utils::stream::{BroadbandExt, IterStream, ReadyExt, TryBroadbandExt, WidebandExt},
warn,
Pdu,
};
/// A mapping of event type and state_key to some value `T`, usually an
@@ -73,20 +75,23 @@
/// event is part of the same room.
//#[tracing::instrument(level = "debug", skip(state_sets, auth_chain_sets,
//#[tracing::instrument(level event_fetch))]
pub async fn resolve<'a, Sets, SetIter, Hasher, FE, FR, Exists>(
pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, ExistsFut>(
room_version: &RoomVersionId,
state_sets: Sets,
auth_chain_sets: &'a [HashSet<OwnedEventId, Hasher>],
event_fetch: &FE,
event_fetch: &Fetch,
event_exists: &Exists,
) -> Result<StateMap<OwnedEventId>>
where
FE: Fn(&EventId) -> FR + Sync,
FR: Future<Output = Result<Option<Pdu>, Error>> + Send,
Exists: AsyncFn(OwnedEventId) -> bool + Sync,
Fetch: Fn(OwnedEventId) -> FetchFut + Sync,
FetchFut: Future<Output = Option<Pdu>> + Send,
Exists: Fn(OwnedEventId) -> ExistsFut + Sync,
ExistsFut: Future<Output = bool> + Send,
Sets: IntoIterator<IntoIter = SetIter> + Send,
SetIter: Iterator<Item = &'a StateMap<OwnedEventId>> + Clone + Send,
Hasher: BuildHasher + Send + Sync,
Pdu: Event + Clone + Send + Sync,
for<'b> &'b Pdu: Event + Send,
{
use RoomVersionId::*;
let stateres_version = match room_version {
@@ -164,7 +169,7 @@ pub async fn resolve<'a, Sets, SetIter, Hasher, FE, FR, Exists>(
// Sequentially auth check each control event.
let resolved_control = iterative_auth_check(
&room_version,
sorted_control_levels.iter().stream().map(ToOwned::to_owned),
sorted_control_levels.iter().stream().map(AsRef::as_ref),
initial_state,
&event_fetch,
)
@@ -204,7 +209,7 @@ pub async fn resolve<'a, Sets, SetIter, Hasher, FE, FR, Exists>(
let mut resolved_state = iterative_auth_check(
&room_version,
sorted_left_events.iter().stream(),
sorted_left_events.iter().stream().map(AsRef::as_ref),
resolved_control, // The control events are added to the final resolved state
&event_fetch,
)
@@ -268,12 +273,14 @@ fn separate<'a, Id>(
}
/// Calculate the conflicted subgraph
async fn calculate_conflicted_subgraph<FE>(
async fn calculate_conflicted_subgraph<F, Fut, E>(
conflicted: &StateMap<Vec<OwnedEventId>>,
fetch_event: &FE,
fetch_event: &F,
) -> Option<HashSet<OwnedEventId>>
where
FE: AsyncFn(OwnedEventId) -> Result<Option<Pdu>> + Sync,
F: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Option<E>> + Send,
E: Event + Send + Sync,
{
let conflicted_events: HashSet<_> = conflicted.values().flatten().cloned().collect();
let mut subgraph: HashSet<OwnedEventId> = HashSet::new();
@@ -305,17 +312,7 @@ async fn calculate_conflicted_subgraph<FE>(
continue;
}
trace!(event_id = event_id.as_str(), "fetching event for its auth events");
let evt = fetch_event(event_id.clone())
.await
.inspect_err(|e| {
log_error!(
"error fetching event {} for conflicted state subgraph: {}",
event_id,
e
)
})
.ok()
.flatten();
let evt = fetch_event(event_id.clone()).await;
if evt.is_none() {
err!("could not fetch event {} to calculate conflicted subgraph", event_id);
path.pop();
@@ -362,14 +359,15 @@ fn get_auth_chain_diff<Id, Hasher>(
/// The power level is negative because a higher power level is equated to an
/// earlier (further back in time) origin server timestamp.
#[tracing::instrument(level = "debug", skip_all)]
async fn reverse_topological_power_sort<FE, FR>(
async fn reverse_topological_power_sort<E, F, Fut>(
events_to_sort: Vec<OwnedEventId>,
auth_diff: &HashSet<OwnedEventId>,
fetch_event: &FE,
fetch_event: &F,
) -> Result<Vec<OwnedEventId>>
where
FE: Fn(&EventId) -> FR + Sync,
FR: Future<Output = Result<Option<Pdu>, Error>> + Send,
F: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Option<E>> + Send,
E: Event + Send + Sync,
{
debug!("reverse topological sort of power events");
@@ -406,8 +404,8 @@ async fn reverse_topological_power_sort<FE, FR>(
.get(&event_id)
.ok_or_else(|| Error::NotFound(String::new()))?;
let ev = fetch_event(&event_id)
.await?
let ev = fetch_event(event_id)
.await
.ok_or_else(|| Error::NotFound(String::new()))?;
Ok((pl, ev.origin_server_ts()))
@@ -546,14 +544,18 @@ fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other))
/// Do NOT use this any where but topological sort, we find the power level for
/// the eventId at the eventId's generation (we walk backwards to `EventId`s
/// most recent previous power level event).
async fn get_power_level_for_sender<FE, FR>(event_id: &EventId, fetch_event: &FE) -> Result<Int>
async fn get_power_level_for_sender<E, F, Fut>(
event_id: &EventId,
fetch_event: &F,
) -> serde_json::Result<Int>
where
FE: Fn(&EventId) -> FR + Sync,
FR: Future<Output = Result<Option<Pdu>, Error>> + Send,
F: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Option<E>> + Send,
E: Event + Send,
{
debug!("fetch event ({event_id}) senders power level");
let event = fetch_event(event_id).await?;
let event = fetch_event(event_id.to_owned()).await;
let auth_events = event.as_ref().map(Event::auth_events);
@@ -561,7 +563,7 @@ async fn get_power_level_for_sender<FE, FR>(event_id: &EventId, fetch_event: &FE
.into_iter()
.flatten()
.stream()
.broad_filter_map(|aid| fetch_event(aid).unwrap_or_default())
.broadn_filter_map(5, |aid| fetch_event(aid.to_owned()))
.ready_find(|aev| is_type_and_key(aev, &TimelineEventType::RoomPowerLevels, ""))
.await;
@@ -592,24 +594,27 @@ async fn get_power_level_for_sender<FE, FR>(event_id: &EventId, fetch_event: &FE
/// the the `fetch_event` closure and verify each event using the
/// `event_auth::auth_check` function.
#[tracing::instrument(level = "trace", skip_all)]
async fn iterative_auth_check<FE, FR, S>(
async fn iterative_auth_check<'a, E, F, Fut, S>(
room_version: &RoomVersion,
events_to_check: S,
unconflicted_state: StateMap<OwnedEventId>,
fetch_event: &FE,
fetch_event: &F,
) -> Result<StateMap<OwnedEventId>>
where
FE: Fn(&EventId) -> FR,
FR: Future<Output = Result<Option<Pdu>, Error>> + Send + Sync,
S: Stream<Item = OwnedEventId> + Send,
F: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Option<E>> + Send,
S: Stream<Item = &'a EventId> + Send + 'a,
E: Event + Clone + Send + Sync,
for<'b> &'b E: Event + Send,
{
debug!("starting iterative auth check");
let events_to_check: Vec<_> = events_to_check
.map(Ok::<OwnedEventId, Error>)
.broad_and_then(async |event_id| match fetch_event(&event_id).await {
| Ok(Some(e)) => Ok(e),
| _ => Err(Error::NotFound(format!("could not find {event_id}")))?,
.map(Result::Ok)
.broad_and_then(async |event_id| {
fetch_event(event_id.to_owned())
.await
.ok_or_else(|| Error::NotFound(format!("Failed to find {event_id}")))
})
.try_collect()
.boxed()
@@ -622,20 +627,16 @@ async fn iterative_auth_check<FE, FR, S>(
let auth_event_ids: HashSet<OwnedEventId> = events_to_check
.iter()
.flat_map(|event: &Pdu| event.auth_events().map(ToOwned::to_owned))
.flat_map(|event: &E| event.auth_events().map(ToOwned::to_owned))
.collect();
trace!(set = ?auth_event_ids, "auth event IDs to fetch");
let auth_events: HashMap<OwnedEventId, Pdu> = auth_event_ids
let auth_events: HashMap<OwnedEventId, E> = auth_event_ids
.into_iter()
.stream()
.broad_filter_map(async |event_id| {
fetch_event(&event_id)
.await
.map(|ev_opt| ev_opt.map(|ev| (event_id.clone(), ev)))
.unwrap_or_default()
})
.broad_filter_map(fetch_event)
.map(|auth_event| (auth_event.event_id().to_owned(), auth_event))
.collect()
.boxed()
.await;
@@ -654,23 +655,29 @@ async fn iterative_auth_check<FE, FR, S>(
.state_key()
.ok_or_else(|| Error::InvalidPdu("State event had no state key".to_owned()))?;
let member_event_content = match event.kind() {
| TimelineEventType::RoomMember =>
Some(event.get_content::<RoomMemberEventContent>().map_err(|e| {
Error::InvalidPdu(format!("Failed to parse m.room.member content: {}", e))
})?),
| _ => None,
};
let auth_types = auth_types_for_event(
room_version,
event.kind(),
event.state_key().map(StateKey::from_str).as_ref(),
event.event_type(),
event.sender(),
member_event_content,
Some(state_key),
event.content(),
room_version,
)?;
trace!(list = ?auth_types, event_id = event.event_id().as_str(), "auth types for event");
let mut auth_state = StateMap::with_capacity(event.auth_events.len());
let mut auth_state = StateMap::new();
if room_version.room_ids_as_hashes {
trace!("room version uses hashed IDs, manually fetching create event");
let create_event_id_raw = event.room_id_or_hash().as_str().replace('!', "$");
let create_event_id = EventId::parse(&create_event_id_raw).map_err(|e| {
Error::InvalidPdu(format!(
"Failed to parse create event ID from room ID/hash: {e}"
))
})?;
let create_event = fetch_event(create_event_id.into())
.await
.ok_or_else(|| Error::NotFound("Failed to find create event".into()))?;
auth_state.insert(create_event.event_type().with_state_key(""), create_event);
}
for aid in event.auth_events() {
if let Some(ev) = auth_events.get(aid) {
//TODO: synapse checks "rejected_reason" which is most likely related to
@@ -696,13 +703,7 @@ async fn iterative_auth_check<FE, FR, S>(
if let Some(event) = auth_events.get(ev_id) {
Some((key, event.clone()))
} else {
match fetch_event(ev_id).await {
| Ok(Some(event)) => Some((key, event)),
| _ => {
warn!(event_id = ev_id.as_str(), "unable to fetch auth event");
None
},
}
Some((key, fetch_event(ev_id.clone()).await?))
}
})
.ready_for_each(|(key, event)| {
@@ -714,16 +715,30 @@ async fn iterative_auth_check<FE, FR, S>(
debug!(event_id = event.event_id().as_str(), "Running auth checks");
let fetch_state = async |t: (StateEventType, StateKey)| {
Ok(auth_state
.get(&t.0.with_state_key(t.1.as_str()))
.map(ToOwned::to_owned))
// The key for this is (eventType + a state_key of the signed token not sender)
// so search for it
let current_third_party = auth_state.iter().find_map(|(_, pdu)| {
(*pdu.event_type() == TimelineEventType::RoomThirdPartyInvite).then_some(pdu)
});
let fetch_state = |ty: &StateEventType, key: &str| {
future::ready(
auth_state
.get(&ty.with_state_key(key))
.map(ToOwned::to_owned),
)
};
let create_event = fetch_state((StateEventType::RoomCreate, StateKey::new())).await?;
let auth_result =
auth_check(room_version, &event, fetch_event, &fetch_state, create_event.as_ref())
.await;
let auth_result = auth_check(
room_version,
&event,
current_third_party,
fetch_state,
&fetch_state(&StateEventType::RoomCreate, "")
.await
.expect("create event must exist"),
)
.await;
match auth_result {
| Ok(true) => {
@@ -743,7 +758,7 @@ async fn iterative_auth_check<FE, FR, S>(
warn!("event {} failed the authentication check", event.event_id());
},
| Err(e) => {
log_error!("event {} failed the authentication check: {e}", event.event_id());
debug_error!("event {} failed the authentication check: {e}", event.event_id());
return Err(e);
},
}
@@ -762,14 +777,15 @@ async fn iterative_auth_check<FE, FR, S>(
/// after the most recent are depth 0, the events before (with the first power
/// level as a parent) will be marked as depth 1. depth 1 is "older" than depth
/// 0.
async fn mainline_sort<FE, FR>(
async fn mainline_sort<E, F, Fut>(
to_sort: &[OwnedEventId],
resolved_power_level: Option<OwnedEventId>,
fetch_event: &FE,
fetch_event: &F,
) -> Result<Vec<OwnedEventId>>
where
FE: Fn(&EventId) -> FR + Sync,
FR: Future<Output = Result<Option<Pdu>, Error>> + Send,
F: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Option<E>> + Send,
E: Event + Clone + Send + Sync,
{
debug!("mainline sort of events");
@@ -783,14 +799,14 @@ async fn mainline_sort<FE, FR>(
while let Some(p) = pl {
mainline.push(p.clone());
let event = fetch_event(&p)
.await?
let event = fetch_event(p.clone())
.await
.ok_or_else(|| Error::NotFound(format!("Failed to find {p}")))?;
pl = None;
for aid in event.auth_events() {
let ev = fetch_event(aid)
.await?
let ev = fetch_event(aid.to_owned())
.await
.ok_or_else(|| Error::NotFound(format!("Failed to find {aid}")))?;
if is_type_and_key(&ev, &TimelineEventType::RoomPowerLevels, "") {
@@ -811,11 +827,7 @@ async fn mainline_sort<FE, FR>(
.iter()
.stream()
.broad_filter_map(async |ev_id| {
fetch_event(ev_id)
.await
.ok()
.flatten()
.map(|event| (event, ev_id))
fetch_event(ev_id.clone()).await.map(|event| (event, ev_id))
})
.broad_filter_map(|(event, ev_id)| {
get_mainline_depth(Some(event.clone()), &mainline_map, fetch_event)
@@ -837,14 +849,15 @@ async fn mainline_sort<FE, FR>(
/// Get the mainline depth from the `mainline_map` or finds a power_level event
/// that has an associated mainline depth.
async fn get_mainline_depth<FE, FR>(
mut event: Option<Pdu>,
async fn get_mainline_depth<E, F, Fut>(
mut event: Option<E>,
mainline_map: &HashMap<OwnedEventId, usize>,
fetch_event: &FE,
fetch_event: &F,
) -> Result<usize>
where
FE: Fn(&EventId) -> FR + Sync,
FR: Future<Output = Result<Option<Pdu>, Error>> + Send,
F: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Option<E>> + Send,
E: Event + Send + Sync,
{
while let Some(sort_ev) = event {
debug!(event_id = sort_ev.event_id().as_str(), "mainline");
@@ -856,8 +869,8 @@ async fn get_mainline_depth<FE, FR>(
event = None;
for aid in sort_ev.auth_events() {
let aev = fetch_event(aid)
.await?
let aev = fetch_event(aid.to_owned())
.await
.ok_or_else(|| Error::NotFound(format!("Failed to find {aid}")))?;
if is_type_and_key(&aev, &TimelineEventType::RoomPowerLevels, "") {
@@ -870,19 +883,20 @@ async fn get_mainline_depth<FE, FR>(
Ok(0)
}
async fn add_event_and_auth_chain_to_graph<FE, FR>(
async fn add_event_and_auth_chain_to_graph<E, F, Fut>(
graph: &mut HashMap<OwnedEventId, HashSet<OwnedEventId>>,
event_id: OwnedEventId,
auth_diff: &HashSet<OwnedEventId>,
fetch_event: &FE,
fetch_event: &F,
) where
FE: Fn(&EventId) -> FR + Sync,
FR: Future<Output = Result<Option<Pdu>, Error>> + Send,
F: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Option<E>> + Send,
E: Event + Send + Sync,
{
let mut state = vec![event_id];
while let Some(eid) = state.pop() {
graph.entry(eid.clone()).or_default();
let event = fetch_event(&eid).await.ok().flatten();
let event = fetch_event(eid.clone()).await;
let auth_events = event.as_ref().map(Event::auth_events).into_iter().flatten();
// Prefer the store to event as the store filters dedups the events
@@ -901,13 +915,14 @@ async fn add_event_and_auth_chain_to_graph<FE, FR>(
}
}
async fn is_power_event_id<FE, FR>(event_id: &EventId, fetch: &FE) -> bool
async fn is_power_event_id<E, F, Fut>(event_id: &EventId, fetch: &F) -> bool
where
FE: Fn(&EventId) -> FR + Sync,
FR: Future<Output = Result<Option<Pdu>, Error>> + Send,
F: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Option<E>> + Send,
E: Event + Send,
{
match fetch(event_id).await.as_ref() {
| Ok(Some(state)) => is_power_event(state),
match fetch(event_id.to_owned()).await.as_ref() {
| Some(state) => is_power_event(state),
| _ => false,
}
}
@@ -964,27 +979,26 @@ fn with_state_key(self, state_key: impl Into<StateKey>) -> (StateEventType, Stat
mod tests {
use std::collections::{HashMap, HashSet};
use itertools::Itertools;
use maplit::{hashmap, hashset};
use rand::seq::SliceRandom;
use ruma::{
MilliSecondsSinceUnixEpoch, OwnedEventId, RoomVersionId,
events::{
room::join_rules::{JoinRule, RoomJoinRulesEventContent}, StateEventType,
TimelineEventType,
}, int, uint,
MilliSecondsSinceUnixEpoch,
OwnedEventId, RoomVersionId,
StateEventType, TimelineEventType,
room::join_rules::{JoinRule, RoomJoinRulesEventContent},
},
int, uint,
};
use serde_json::{json, value::to_raw_value as to_raw_json_value};
use super::{
is_power_event, room_version::RoomVersion,
StateMap, is_power_event,
room_version::RoomVersion,
test_utils::{
alice, bob, charlie, do_check, ella, event_id, member_content_ban, member_content_join,
room_id, to_init_pdu_event, to_pdu_event, zara, TestStore,
INITIAL_EVENTS,
INITIAL_EVENTS, TestStore, alice, bob, charlie, do_check, ella, event_id,
member_content_ban, member_content_join, room_id, to_init_pdu_event, to_pdu_event,
zara,
},
StateMap,
};
use crate::{
debug,
@@ -1014,13 +1028,13 @@ async fn test_event_sort() {
.map(|pdu| pdu.event_id.clone())
.collect::<Vec<_>>();
let fetcher = |id| ready(Ok(events.get(id).cloned()));
let fetcher = |id| ready(events.get(&id).cloned());
let sorted_power_events =
super::reverse_topological_power_sort(power_events, &auth_chain, &fetcher)
.await
.unwrap();
let resolved_power = super::auth_check(
let resolved_power = super::iterative_auth_check(
&RoomVersion::V6,
sorted_power_events.iter().map(AsRef::as_ref).stream(),
HashMap::new(), // unconflicted events
@@ -1032,7 +1046,7 @@ async fn test_event_sort() {
// don't remove any events so we know it sorts them all correctly
let mut events_to_sort = events.keys().cloned().collect::<Vec<_>>();
events_to_sort.shuffle(&mut rand::thread_rng());
events_to_sort.shuffle(&mut rand::rng());
let power_level = resolved_power
.get(&(StateEventType::RoomPowerLevels, "".into()))

View File

@@ -28,7 +28,7 @@ fn init_argon() -> Argon2<'static> {
}
pub(super) fn password(password: &str) -> Result<String> {
let salt = SaltString::generate(rand::thread_rng());
let salt = SaltString::generate(rand_core::OsRng);
ARGON
.get_or_init(init_argon)
.hash_password(password.as_bytes(), &salt)

View File

@@ -4,16 +4,16 @@
};
use arrayvec::ArrayString;
use rand::{Rng, seq::SliceRandom, thread_rng};
use rand::{Rng, RngExt, seq::SliceRandom};
pub fn shuffle<T>(vec: &mut [T]) {
let mut rng = thread_rng();
let mut rng = rand::rng();
vec.shuffle(&mut rng);
}
pub fn string(length: usize) -> String {
thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
rand::rng()
.sample_iter(&rand::distr::Alphanumeric)
.take(length)
.map(char::from)
.collect()
@@ -22,8 +22,8 @@ pub fn string(length: usize) -> String {
#[inline]
pub fn string_array<const LENGTH: usize>() -> ArrayString<LENGTH> {
let mut ret = ArrayString::<LENGTH>::new();
thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
rand::rng()
.sample_iter(&rand::distr::Alphanumeric)
.take(LENGTH)
.map(char::from)
.for_each(|c| ret.push(c));
@@ -40,7 +40,4 @@ pub fn time_from_now_secs(range: Range<u64>) -> SystemTime {
}
#[must_use]
pub fn secs(range: Range<u64>) -> Duration {
let mut rng = thread_rng();
Duration::from_secs(rng.gen_range(range))
}
pub fn secs(range: Range<u64>) -> Duration { Duration::from_secs(rand::random_range(range)) }

View File

@@ -100,8 +100,7 @@ async fn worker(self: Arc<Self>) -> Result<()> {
}
let first_check_jitter = {
let mut rng = rand::thread_rng();
let jitter_percent = rng.gen_range(-50.0..=10.0);
let jitter_percent = rand::random_range(-50.0..=10.0);
self.interval.mul_f64(1.0 + jitter_percent / 100.0)
};

View File

@@ -385,11 +385,13 @@ fn num_senders(args: &crate::Args<'_>) -> usize {
const MIN_SENDERS: usize = 1;
// Limit the number of senders to the number of workers threads or number of
// cores, conservatively.
let max_senders = args
.server
.metrics
.num_workers()
.min(available_parallelism());
let mut max_senders = args.server.metrics.num_workers();
// Work around some platforms not returning the number of cores.
let num_cores = available_parallelism();
if num_cores > 0 {
max_senders = max_senders.min(num_cores);
}
// If the user doesn't override the default 0, this is intended to then default
// to 1 for now as multiple senders is experimental.