state-res: Add support for room version 8 join restrictions

Co-authored-by: Devin Ragotzy <d6ragotzy@wmich.edu>
This commit is contained in:
Jonas Platte 2021-12-17 18:58:32 +01:00
parent 57cbe491f0
commit 7290860019
No known key found for this signature in database
GPG Key ID: CC154DE0E30B7C67

View File

@ -19,6 +19,24 @@ use tracing::{debug, error, info, warn};
use crate::{room_version::RoomVersion, Error, Event, PowerLevelsContentFields, Result}; use crate::{room_version::RoomVersion, Error, Event, PowerLevelsContentFields, Result};
// FIXME: field extracting could be bundled for `content`
#[derive(Deserialize)]
struct GetMembership {
membership: MembershipState,
}
#[derive(Deserialize)]
struct RoomMemberContentFields {
membership: Option<Raw<MembershipState>>,
#[cfg(feature = "unstable-pre-spec")]
join_authorised_via_users_server: Option<Raw<Box<UserId>>>,
}
#[derive(Deserialize)]
struct PowerLevelsContentInvite {
invite: Int,
}
/// For the given event `kind` what are the relevant auth events that are needed to authenticate /// For the given event `kind` what are the relevant auth events that are needed to authenticate
/// this `content`. /// this `content`.
/// ///
@ -95,11 +113,6 @@ pub fn auth_check<E: Event>(
current_third_party_invite: Option<impl Event>, current_third_party_invite: Option<impl Event>,
fetch_state: impl Fn(&EventType, &str) -> Option<E>, fetch_state: impl Fn(&EventType, &str) -> Option<E>,
) -> Result<bool> { ) -> Result<bool> {
#[derive(Deserialize)]
struct RoomMemberContentFields {
membership: Option<Raw<MembershipState>>,
}
info!( info!(
"auth_check beginning for {} ({})", "auth_check beginning for {} ({})",
incoming_event.event_id(), incoming_event.event_id(),
@ -221,7 +234,7 @@ pub fn auth_check<E: Event>(
}; };
let content: RoomMemberContentFields = from_json_str(incoming_event.content().get())?; let content: RoomMemberContentFields = from_json_str(incoming_event.content().get())?;
if content.membership.and_then(|m| m.deserialize().ok()).is_none() { if content.membership.as_ref().and_then(|m| m.deserialize().ok()).is_none() {
warn!("no valid membership field found for m.room.member event content"); warn!("no valid membership field found for m.room.member event content");
return Ok(false); return Ok(false);
} }
@ -229,6 +242,17 @@ pub fn auth_check<E: Event>(
let target_user = let target_user =
<&UserId>::try_from(state_key).map_err(|e| Error::InvalidPdu(format!("{}", e)))?; <&UserId>::try_from(state_key).map_err(|e| Error::InvalidPdu(format!("{}", e)))?;
#[cfg(feature = "unstable-pre-spec")]
let join_authed_user =
content.join_authorised_via_users_server.as_ref().and_then(|u| u.deserialize().ok());
#[cfg(feature = "unstable-pre-spec")]
let join_authed_user_membership = if let Some(auth_user) = &join_authed_user {
fetch_state(&EventType::RoomMember, auth_user.as_str())
.and_then(|mem| from_json_str::<GetMembership>(mem.content().get()).ok())
.map(|mem| mem.membership)
} else {
None
};
if !valid_membership_change( if !valid_membership_change(
room_version, room_version,
target_user, target_user,
@ -240,6 +264,10 @@ pub fn auth_check<E: Event>(
current_third_party_invite, current_third_party_invite,
power_levels_event.as_ref(), power_levels_event.as_ref(),
fetch_state(&EventType::RoomJoinRules, "").as_ref(), fetch_state(&EventType::RoomJoinRules, "").as_ref(),
#[cfg(feature = "unstable-pre-spec")]
join_authed_user.as_deref(),
#[cfg(feature = "unstable-pre-spec")]
join_authed_user_membership,
)? { )? {
return Ok(false); return Ok(false);
} }
@ -290,11 +318,6 @@ pub fn auth_check<E: Event>(
// Allow if and only if sender's current power level is greater than // Allow if and only if sender's current power level is greater than
// or equal to the invite level // or equal to the invite level
if *incoming_event.event_type() == EventType::RoomThirdPartyInvite { if *incoming_event.event_type() == EventType::RoomThirdPartyInvite {
#[derive(Deserialize)]
struct PowerLevelsContentInvite {
invite: Int,
}
let invite_level = match &power_levels_event { let invite_level = match &power_levels_event {
Some(power_levels) => { Some(power_levels) => {
from_json_str::<PowerLevelsContentInvite>(power_levels.content().get())?.invite from_json_str::<PowerLevelsContentInvite>(power_levels.content().get())?.invite
@ -376,7 +399,7 @@ pub fn auth_check<E: Event>(
/// State. /// State.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn valid_membership_change( fn valid_membership_change(
_room_version: &RoomVersion, room_version: &RoomVersion,
target_user: &UserId, target_user: &UserId,
target_user_membership_event: Option<impl Event>, target_user_membership_event: Option<impl Event>,
sender: &UserId, sender: &UserId,
@ -386,13 +409,9 @@ fn valid_membership_change(
current_third_party_invite: Option<impl Event>, current_third_party_invite: Option<impl Event>,
power_levels_event: Option<impl Event>, power_levels_event: Option<impl Event>,
join_rules_event: Option<impl Event>, join_rules_event: Option<impl Event>,
#[cfg(feature = "unstable-pre-spec")] authed_user_id: Option<&UserId>,
#[cfg(feature = "unstable-pre-spec")] auth_user_membership: Option<MembershipState>,
) -> Result<bool> { ) -> Result<bool> {
// FIXME: field extracting could be bundled for `content`
#[derive(Deserialize)]
struct GetMembership {
membership: MembershipState,
}
#[derive(Deserialize)] #[derive(Deserialize)]
struct GetThirdPartyInvite { struct GetThirdPartyInvite {
third_party_invite: Option<Raw<ThirdPartyInvite>>, third_party_invite: Option<Raw<ThirdPartyInvite>>,
@ -443,6 +462,54 @@ fn valid_membership_change(
let target_user_membership_event_id = let target_user_membership_event_id =
target_user_membership_event.as_ref().map(|e| e.event_id()); target_user_membership_event.as_ref().map(|e| e.event_id());
#[cfg(not(feature = "unstable-pre-spec"))]
let restricted = false;
#[cfg(not(feature = "unstable-pre-spec"))]
let allow_based_on_membership = false;
#[cfg(not(feature = "unstable-pre-spec"))]
let restricted_join_rules_auth = false;
// FIXME: `JoinRule::Restricted(_)` can contain conditions that allow a user to join if
// they are met. So far the spec talks about roomId based auth inheritance, the problem with
// this is that ruma-state-res can only request events from one room at a time :(
#[cfg(feature = "unstable-pre-spec")]
let restricted = matches!(join_rules, JoinRule::Restricted(_));
#[cfg(feature = "unstable-pre-spec")]
let allow_based_on_membership =
matches!(target_user_current_membership, MembershipState::Invite | MembershipState::Join)
|| authed_user_id.is_none();
#[cfg(feature = "unstable-pre-spec")]
let restricted_join_rules_auth = if let Some(authed_user_id) = authed_user_id {
// Is the authorised user allowed to invite users into this rooom
let (auth_user_pl, invite_level) = if let Some(pl) = &power_levels_event {
let invite = match from_json_str::<PowerLevelsContentInvite>(pl.content().get()) {
Ok(power_levels) => power_levels.invite,
_ => int!(50),
};
if let Ok(content) = from_json_str::<PowerLevelsContentFields>(pl.content().get()) {
let user_pl = if let Some(level) = content.users.get(authed_user_id) {
*level
} else {
content.users_default
};
(user_pl, invite)
} else {
(int!(0), invite)
}
} else {
(int!(0), int!(0))
};
(auth_user_membership == Some(MembershipState::Join)) && (auth_user_pl >= invite_level)
} else {
// If the `join_authorised_via_users_server` was empty we treat the target user as invited
true
};
Ok(match target_membership { Ok(match target_membership {
MembershipState::Join => { MembershipState::Join => {
if sender != target_user { if sender != target_user {
@ -455,7 +522,15 @@ fn valid_membership_change(
let allow = join_rules == JoinRule::Invite let allow = join_rules == JoinRule::Invite
&& (target_user_current_membership == MembershipState::Join && (target_user_current_membership == MembershipState::Join
|| target_user_current_membership == MembershipState::Invite) || target_user_current_membership == MembershipState::Invite)
|| join_rules == JoinRule::Public; || join_rules == JoinRule::Public
|| room_version.restricted_join_rules
// 0. room version of 8 ^ and join rule of restricted
&& restricted
// 1. The user's previous membership was invite or join
&& allow_based_on_membership
// 2. The join event has a valid signature from a homeserver whose
// users have the power to issue invites or the field was `None`.
&& restricted_join_rules_auth;
if !allow { if !allow {
warn!( warn!(
@ -557,17 +632,21 @@ fn valid_membership_change(
} }
} }
#[cfg(feature = "unstable-pre-spec")] #[cfg(feature = "unstable-pre-spec")]
MembershipState::Knock if _room_version.allow_knocking => { MembershipState::Knock if room_version.allow_knocking => {
// 1. If the `join_rule` is anything other than `knock`, reject. // 1. If the `join_rule` is anything other than `knock`, reject.
if join_rules != JoinRule::Knock { if join_rules != JoinRule::Knock {
warn!("Join rule is not set to knock"); warn!("Join rule is not set to knock, knocking is not allowed");
false false
} else { } else {
// 2. If `sender` does not match `state_key`, reject. // 2. If `sender` does not match `state_key`, reject.
// 3. If the `sender`'s current membership is not `ban`, `invite`, or `join`, allow. // 3. If the `sender`'s current membership is not `ban`, `invite`, or `join`, allow.
// 4. Otherwise, reject. // 4. Otherwise, reject.
if sender != target_user { if sender != target_user {
warn!("Can't make other user join"); warn!(
?sender,
?target_user,
"Can't make another user join, sender did not match target"
);
false false
} else if matches!( } else if matches!(
sender_membership, sender_membership,
@ -866,8 +945,21 @@ mod tests {
}, },
Event, RoomVersion, StateMap, Event, RoomVersion, StateMap,
}; };
#[cfg(feature = "unstable-pre-spec")]
use {
crate::test_utils::{bob, ella, event_id, room_id},
ruma_events::room::{
join_rules::{
AllowRule, JoinRule, Restricted, RoomJoinRulesEventContent, RoomMembership,
},
member::{MembershipState, RoomMemberEventContent},
},
serde_json::value::to_raw_value as to_raw_json_value,
};
use ruma_events::EventType; use ruma_events::EventType;
// #[cfg(not(feature = "unstable-pre-spec"))]
#[test] #[test]
fn test_ban_pass() { fn test_ban_pass() {
let _ = let _ =
@ -909,10 +1001,15 @@ mod tests {
None::<StateEvent>, None::<StateEvent>,
fetch_state(EventType::RoomPowerLevels, "".to_owned()), fetch_state(EventType::RoomPowerLevels, "".to_owned()),
fetch_state(EventType::RoomJoinRules, "".to_owned()), fetch_state(EventType::RoomJoinRules, "".to_owned()),
#[cfg(feature = "unstable-pre-spec")]
None,
#[cfg(feature = "unstable-pre-spec")]
None
) )
.unwrap()); .unwrap());
} }
// #[cfg(not(feature = "unstable-pre-spec"))]
#[test] #[test]
fn test_ban_fail() { fn test_ban_fail() {
let _ = let _ =
@ -954,6 +1051,160 @@ mod tests {
None::<StateEvent>, None::<StateEvent>,
fetch_state(EventType::RoomPowerLevels, "".to_owned()), fetch_state(EventType::RoomPowerLevels, "".to_owned()),
fetch_state(EventType::RoomJoinRules, "".to_owned()), fetch_state(EventType::RoomJoinRules, "".to_owned()),
#[cfg(feature = "unstable-pre-spec")]
None,
#[cfg(feature = "unstable-pre-spec")]
None
)
.unwrap());
}
#[cfg(feature = "unstable-pre-spec")]
#[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(),
EventType::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::Invite);
member.join_authorized_via_users_server = Some(alice());
events.insert(
event_id("new"),
to_pdu_event(
"new",
bob(),
EventType::RoomMember,
Some(ella().as_str()),
to_raw_json_value(&member).unwrap(),
&["CREATE", "IJR", "IPOWER"],
&["IMC"],
),
);
let prev_event =
events.values().find(|ev| ev.event_id.as_str().contains("IMC")).map(Arc::clone);
let auth_events = events
.values()
.map(|ev| {
((ev.event_type().to_owned(), ev.state_key().unwrap().to_owned()), Arc::clone(ev))
})
.collect::<StateMap<_>>();
let requester = to_pdu_event(
"HELLO",
ella(),
EventType::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(EventType::RoomMember, target_user.to_string()),
&sender,
fetch_state(EventType::RoomMember, sender.to_string()),
requester.content(),
prev_event.clone(),
None::<StateEvent>,
fetch_state(EventType::RoomPowerLevels, "".to_owned()),
fetch_state(EventType::RoomJoinRules, "".to_owned()),
Some(&alice()),
Some(MembershipState::Join),
)
.unwrap());
assert!(!valid_membership_change(
&RoomVersion::V9,
&target_user,
fetch_state(EventType::RoomMember, target_user.to_string()),
&sender,
fetch_state(EventType::RoomMember, sender.to_string()),
requester.content(),
prev_event,
None::<StateEvent>,
fetch_state(EventType::RoomPowerLevels, "".to_owned()),
fetch_state(EventType::RoomJoinRules, "".to_owned()),
Some(&ella()),
Some(MembershipState::Join),
)
.unwrap());
}
#[cfg(feature = "unstable-pre-spec")]
#[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(),
EventType::RoomJoinRules,
Some(""),
to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Knock)).unwrap(),
&["CREATE", "IMA", "IPOWER"],
&["IPOWER"],
);
let prev_event =
events.values().find(|ev| ev.event_id.as_str().contains("IMC")).map(Arc::clone);
let auth_events = events
.values()
.map(|ev| {
((ev.event_type().to_owned(), ev.state_key().unwrap().to_owned()), Arc::clone(ev))
})
.collect::<StateMap<_>>();
let requester = to_pdu_event(
"HELLO",
ella(),
EventType::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(EventType::RoomMember, target_user.to_string()),
&sender,
fetch_state(EventType::RoomMember, sender.to_string()),
requester.content(),
prev_event,
None::<StateEvent>,
fetch_state(EventType::RoomPowerLevels, "".to_owned()),
fetch_state(EventType::RoomJoinRules, "".to_owned()),
None,
None,
) )
.unwrap()); .unwrap());
} }