use std::{
borrow::Borrow,
collections::{BTreeMap, BTreeSet, HashSet},
};
use js_int::Int;
use ruma_common::{
EventId, OwnedUserId, UserId, room::JoinRuleKind, room_version_rules::AuthorizationRules,
};
use ruma_events::{
StateEventType, TimelineEventType,
room::{member::MembershipState, power_levels::UserPowerLevel},
};
use serde_json::value::RawValue as RawJsonValue;
use tracing::{debug, info, instrument, warn};
mod room_member;
#[cfg(test)]
mod tests;
use self::room_member::check_room_member;
use crate::{
Event,
events::{
RoomCreateEvent, RoomJoinRulesEvent, RoomMemberEvent, RoomPowerLevelsEvent,
RoomThirdPartyInviteEvent,
member::{RoomMemberEventContent, RoomMemberEventOptionExt},
power_levels::{RoomPowerLevelsEventOptionExt, RoomPowerLevelsIntField},
},
utils::RoomIdExt,
};
pub fn auth_types_for_event(
event_type: &TimelineEventType,
sender: &UserId,
state_key: Option<&str>,
content: &RawJsonValue,
rules: &AuthorizationRules,
) -> Result<Vec<(StateEventType, String)>, String> {
if event_type == &TimelineEventType::RoomCreate {
return Ok(vec![]);
}
let mut auth_types = vec![
(StateEventType::RoomPowerLevels, "".to_owned()),
(StateEventType::RoomMember, sender.to_string()),
];
if !rules.room_create_event_id_as_room_id {
auth_types.push((StateEventType::RoomCreate, "".to_owned()));
}
if event_type == &TimelineEventType::RoomMember {
let Some(state_key) = state_key else {
return Err("missing `state_key` field for `m.room.member` event".to_owned());
};
let key = (StateEventType::RoomMember, state_key.to_owned());
if !auth_types.contains(&key) {
auth_types.push(key);
}
let content = RoomMemberEventContent::new(content);
let membership = content.membership()?;
if matches!(
membership,
MembershipState::Join | MembershipState::Invite | MembershipState::Knock
) {
let key = (StateEventType::RoomJoinRules, "".to_owned());
if !auth_types.contains(&key) {
auth_types.push(key);
}
}
if membership == MembershipState::Invite {
let third_party_invite = content.third_party_invite()?;
if let Some(third_party_invite) = third_party_invite {
let token = third_party_invite.token()?.to_owned();
let key = (StateEventType::RoomThirdPartyInvite, token);
if !auth_types.contains(&key) {
auth_types.push(key);
}
}
}
if membership == MembershipState::Join && rules.restricted_join_rule {
let join_authorised_via_users_server = content.join_authorised_via_users_server()?;
if let Some(user_id) = join_authorised_via_users_server {
let key = (StateEventType::RoomMember, user_id.to_string());
if !auth_types.contains(&key) {
auth_types.push(key);
}
}
}
}
Ok(auth_types)
}
#[instrument(skip_all, fields(event_id = incoming_event.event_id().borrow().as_str()))]
pub fn check_state_independent_auth_rules<E: Event>(
rules: &AuthorizationRules,
incoming_event: impl Event,
fetch_event: impl Fn(&EventId) -> Option<E>,
) -> Result<(), String> {
debug!("starting state-independent auth check");
if *incoming_event.event_type() == TimelineEventType::RoomCreate {
let room_create_event = RoomCreateEvent::new(incoming_event);
return check_room_create(room_create_event, rules);
}
let expected_auth_types = auth_types_for_event(
incoming_event.event_type(),
incoming_event.sender(),
incoming_event.state_key(),
incoming_event.content(),
rules,
)?
.into_iter()
.map(|(event_type, state_key)| (TimelineEventType::from(event_type), state_key))
.collect::<HashSet<_>>();
let Some(room_id) = incoming_event.room_id() else {
return Err("missing `room_id` field for event".to_owned());
};
let mut seen_auth_types: HashSet<(TimelineEventType, String)> =
HashSet::with_capacity(expected_auth_types.len());
for auth_event_id in incoming_event.auth_events() {
let event_id = auth_event_id.borrow();
let Some(auth_event) = fetch_event(event_id) else {
return Err(format!("failed to find auth event {event_id}"));
};
if auth_event.room_id().is_none_or(|auth_room_id| auth_room_id != room_id) {
return Err(format!("auth event {event_id} not in the same room"));
}
let event_type = auth_event.event_type();
let state_key = auth_event
.state_key()
.ok_or_else(|| format!("auth event {event_id} has no `state_key`"))?;
let key = (event_type.clone(), state_key.to_owned());
if seen_auth_types.contains(&key) {
return Err(format!(
"duplicate auth event {event_id} for ({event_type}, {state_key}) pair"
));
}
if !expected_auth_types.contains(&key) {
return Err(format!(
"unexpected auth event {event_id} with ({event_type}, {state_key}) pair"
));
}
if auth_event.rejected() {
return Err(format!("rejected auth event {event_id}"));
}
seen_auth_types.insert(key);
}
if !rules.room_create_event_id_as_room_id
&& !seen_auth_types
.iter()
.any(|(event_type, _)| *event_type == TimelineEventType::RoomCreate)
{
return Err("no `m.room.create` event in auth events".to_owned());
}
if rules.room_create_event_id_as_room_id {
let room_create_event_id = room_id.room_create_event_id().map_err(|error| {
format!("could not construct `m.room.create` event ID from room ID: {error}")
})?;
let room_create_event = fetch_event(&room_create_event_id).ok_or_else(|| {
format!("failed to find `m.room.create` event {room_create_event_id}")
})?;
if room_create_event.rejected() {
return Err(format!("rejected `m.room.create` event {room_create_event_id}"));
}
}
Ok(())
}
#[instrument(skip_all, fields(event_id = incoming_event.event_id().borrow().as_str()))]
pub fn check_state_dependent_auth_rules<E: Event>(
rules: &AuthorizationRules,
incoming_event: impl Event,
fetch_state: impl Fn(&StateEventType, &str) -> Option<E>,
) -> Result<(), String> {
debug!("starting state-dependent auth check");
if *incoming_event.event_type() == TimelineEventType::RoomCreate {
debug!("allowing `m.room.create` event");
return Ok(());
}
let room_create_event = fetch_state.room_create_event()?;
let federate = room_create_event.federate()?;
if !federate
&& room_create_event.sender().server_name() != incoming_event.sender().server_name()
{
return Err("\
room is not federated and event's sender domain \
does not match `m.room.create` event's sender domain"
.to_owned());
}
let sender = incoming_event.sender();
if rules.special_case_room_aliases && *incoming_event.event_type() == "m.room.aliases".into() {
debug!("starting m.room.aliases check");
if incoming_event.state_key() != Some(sender.server_name().as_str()) {
return Err("\
server name of the `state_key` of `m.room.aliases` event \
does not match the server name of the sender"
.to_owned());
}
info!("`m.room.aliases` event was allowed");
return Ok(());
}
if *incoming_event.event_type() == TimelineEventType::RoomMember {
let room_member_event = RoomMemberEvent::new(incoming_event);
return check_room_member(room_member_event, rules, room_create_event, fetch_state);
}
let sender_membership = fetch_state.user_membership(sender)?;
tracing::info!(?sender_membership);
if sender_membership != MembershipState::Join {
return Err("sender's membership is not `join`".to_owned());
}
let creators = room_create_event.creators(rules)?;
let current_room_power_levels_event = fetch_state.room_power_levels_event();
let sender_power_level =
current_room_power_levels_event.user_power_level(sender, &creators, rules)?;
if *incoming_event.event_type() == TimelineEventType::RoomThirdPartyInvite {
let invite_power_level = current_room_power_levels_event
.get_as_int_or_default(RoomPowerLevelsIntField::Invite, rules)?;
if sender_power_level < invite_power_level {
return Err("sender does not have enough power to send invites in this room".to_owned());
}
info!("`m.room.third_party_invite` event was allowed");
return Ok(());
}
let event_type_power_level = current_room_power_levels_event.event_power_level(
incoming_event.event_type(),
incoming_event.state_key(),
rules,
)?;
if sender_power_level < event_type_power_level {
return Err(format!(
"sender does not have enough power to send event of type `{}`",
incoming_event.event_type()
));
}
if incoming_event.state_key().is_some_and(|k| k.starts_with('@'))
&& incoming_event.state_key() != Some(incoming_event.sender().as_str())
{
return Err(
"sender cannot send event with `state_key` matching another user's ID".to_owned()
);
}
if *incoming_event.event_type() == TimelineEventType::RoomPowerLevels {
let room_power_levels_event = RoomPowerLevelsEvent::new(incoming_event);
return check_room_power_levels(
room_power_levels_event,
current_room_power_levels_event,
rules,
sender_power_level,
&creators,
);
}
if rules.special_case_room_redaction
&& *incoming_event.event_type() == TimelineEventType::RoomRedaction
{
return check_room_redaction(
incoming_event,
current_room_power_levels_event,
rules,
sender_power_level,
);
}
info!("allowing event passed all checks");
Ok(())
}
fn check_room_create(
room_create_event: RoomCreateEvent<impl Event>,
rules: &AuthorizationRules,
) -> Result<(), String> {
debug!("start `m.room.create` check");
if room_create_event.prev_events().next().is_some() {
return Err("`m.room.create` event cannot have previous events".into());
}
if rules.room_create_event_id_as_room_id {
if room_create_event.room_id().is_some() {
return Err("`m.room.create` event cannot have a `room_id` field".into());
}
} else {
let Some(room_id) = room_create_event.room_id() else {
return Err("missing `room_id` field in `m.room.create` event".into());
};
let Some(room_id_server_name) = room_id.server_name() else {
return Err(
"invalid `room_id` field in `m.room.create` event: could not parse server name"
.into(),
);
};
if room_id_server_name != room_create_event.sender().server_name() {
return Err("invalid `room_id` field in `m.room.create` event: server name does not match sender's server name".into());
}
}
if !rules.use_room_create_sender && !room_create_event.has_creator()? {
return Err("missing `creator` field in `m.room.create` event".into());
}
room_create_event.additional_creators(rules)?;
info!("`m.room.create` event was allowed");
Ok(())
}
fn check_room_power_levels(
room_power_levels_event: RoomPowerLevelsEvent<impl Event>,
current_room_power_levels_event: Option<RoomPowerLevelsEvent<impl Event>>,
rules: &AuthorizationRules,
sender_power_level: UserPowerLevel,
room_creators: &HashSet<OwnedUserId>,
) -> Result<(), String> {
debug!("starting m.room.power_levels check");
let new_int_fields = room_power_levels_event.int_fields_map(rules)?;
let new_events = room_power_levels_event.events(rules)?;
let new_notifications = room_power_levels_event.notifications(rules)?;
let new_users = room_power_levels_event.users(rules)?;
if rules.explicitly_privilege_room_creators
&& new_users.is_some_and(|new_users| {
room_creators.iter().any(|creator| new_users.contains_key(creator))
})
{
return Err("creator user IDs are not allowed in the `users` field".to_owned());
}
debug!("validation of power event finished");
let Some(current_room_power_levels_event) = current_room_power_levels_event else {
info!("initial m.room.power_levels event allowed");
return Ok(());
};
for field in RoomPowerLevelsIntField::ALL {
let current_power_level = current_room_power_levels_event.get_as_int(*field, rules)?;
let new_power_level = new_int_fields.get(field).copied();
if current_power_level == new_power_level {
continue;
}
let current_power_level_too_big =
current_power_level.unwrap_or_else(|| field.default_value()) > sender_power_level;
let new_power_level_too_big =
new_power_level.unwrap_or_else(|| field.default_value()) > sender_power_level;
if current_power_level_too_big || new_power_level_too_big {
return Err(format!(
"sender does not have enough power to change the power level of `{field}`"
));
}
}
let current_events = current_room_power_levels_event.events(rules)?;
check_power_level_maps(
current_events.as_ref(),
new_events.as_ref(),
&sender_power_level,
|_, current_power_level| {
current_power_level > sender_power_level
},
|ev_type| {
format!(
"sender does not have enough power to change the `{ev_type}` event type power level"
)
},
)?;
if rules.limit_notifications_power_levels {
let current_notifications = current_room_power_levels_event.notifications(rules)?;
check_power_level_maps(
current_notifications.as_ref(),
new_notifications.as_ref(),
&sender_power_level,
|_, current_power_level| {
current_power_level > sender_power_level
},
|key| {
format!(
"sender does not have enough power to change the `{key}` notification power level"
)
},
)?;
}
let current_users = current_room_power_levels_event.users(rules)?;
check_power_level_maps(
current_users,
new_users,
&sender_power_level,
|user_id, current_power_level| {
user_id != room_power_levels_event.sender() && current_power_level >= sender_power_level
},
|user_id| format!("sender does not have enough power to change `{user_id}`'s power level"),
)?;
info!("m.room.power_levels event allowed");
Ok(())
}
fn check_power_level_maps<K: Ord>(
current: Option<&BTreeMap<K, Int>>,
new: Option<&BTreeMap<K, Int>>,
sender_power_level: &UserPowerLevel,
reject_current_power_level_change_fn: impl FnOnce(&K, Int) -> bool + Copy,
error_fn: impl FnOnce(&K) -> String,
) -> Result<(), String> {
let keys_to_check = current
.iter()
.flat_map(|m| m.keys())
.chain(new.iter().flat_map(|m| m.keys()))
.collect::<BTreeSet<_>>();
for key in keys_to_check {
let current_power_level = current.as_ref().and_then(|m| m.get(key));
let new_power_level = new.as_ref().and_then(|m| m.get(key));
if current_power_level == new_power_level {
continue;
}
let current_power_level_change_rejected = current_power_level
.is_some_and(|power_level| reject_current_power_level_change_fn(key, *power_level));
let new_power_level_too_big = new_power_level.is_some_and(|pl| pl > sender_power_level);
if current_power_level_change_rejected || new_power_level_too_big {
return Err(error_fn(key));
}
}
Ok(())
}
fn check_room_redaction(
room_redaction_event: impl Event,
current_room_power_levels_event: Option<RoomPowerLevelsEvent<impl Event>>,
rules: &AuthorizationRules,
sender_level: UserPowerLevel,
) -> Result<(), String> {
let redact_level = current_room_power_levels_event
.get_as_int_or_default(RoomPowerLevelsIntField::Redact, rules)?;
if sender_level >= redact_level {
info!("`m.room.redaction` event allowed via power levels");
return Ok(());
}
if room_redaction_event.event_id().borrow().server_name()
== room_redaction_event.redacts().as_ref().and_then(|&id| id.borrow().server_name())
{
info!("`m.room.redaction` event allowed via room version 1 rules");
return Ok(());
}
Err("`m.room.redaction` event did not pass any of the allow rules".to_owned())
}
trait FetchStateExt<E: Event> {
fn room_create_event(&self) -> Result<RoomCreateEvent<E>, String>;
fn user_membership(&self, user_id: &UserId) -> Result<MembershipState, String>;
fn room_power_levels_event(&self) -> Option<RoomPowerLevelsEvent<E>>;
fn join_rule(&self) -> Result<JoinRuleKind, String>;
fn room_third_party_invite_event(&self, token: &str) -> Option<RoomThirdPartyInviteEvent<E>>;
}
impl<E, F> FetchStateExt<E> for F
where
F: Fn(&StateEventType, &str) -> Option<E>,
E: Event,
{
fn room_create_event(&self) -> Result<RoomCreateEvent<E>, String> {
self(&StateEventType::RoomCreate, "")
.map(RoomCreateEvent::new)
.ok_or_else(|| "no `m.room.create` event in current state".to_owned())
}
fn user_membership(&self, user_id: &UserId) -> Result<MembershipState, String> {
self(&StateEventType::RoomMember, user_id.as_str()).map(RoomMemberEvent::new).membership()
}
fn room_power_levels_event(&self) -> Option<RoomPowerLevelsEvent<E>> {
self(&StateEventType::RoomPowerLevels, "").map(RoomPowerLevelsEvent::new)
}
fn join_rule(&self) -> Result<JoinRuleKind, String> {
self(&StateEventType::RoomJoinRules, "")
.map(RoomJoinRulesEvent::new)
.ok_or_else(|| "no `m.room.join_rules` event in current state".to_owned())?
.join_rule()
}
fn room_third_party_invite_event(&self, token: &str) -> Option<RoomThirdPartyInviteEvent<E>> {
self(&StateEventType::RoomThirdPartyInvite, token).map(RoomThirdPartyInviteEvent::new)
}
}