state-res: Add support for room version 8 join restrictions
Co-authored-by: Devin Ragotzy <d6ragotzy@wmich.edu>
This commit is contained in:
parent
57cbe491f0
commit
7290860019
@ -19,6 +19,24 @@ use tracing::{debug, error, info, warn};
|
||||
|
||||
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
|
||||
/// this `content`.
|
||||
///
|
||||
@ -95,11 +113,6 @@ pub fn auth_check<E: Event>(
|
||||
current_third_party_invite: Option<impl Event>,
|
||||
fetch_state: impl Fn(&EventType, &str) -> Option<E>,
|
||||
) -> Result<bool> {
|
||||
#[derive(Deserialize)]
|
||||
struct RoomMemberContentFields {
|
||||
membership: Option<Raw<MembershipState>>,
|
||||
}
|
||||
|
||||
info!(
|
||||
"auth_check beginning for {} ({})",
|
||||
incoming_event.event_id(),
|
||||
@ -221,7 +234,7 @@ pub fn auth_check<E: Event>(
|
||||
};
|
||||
|
||||
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");
|
||||
return Ok(false);
|
||||
}
|
||||
@ -229,6 +242,17 @@ pub fn auth_check<E: Event>(
|
||||
let target_user =
|
||||
<&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(
|
||||
room_version,
|
||||
target_user,
|
||||
@ -240,6 +264,10 @@ pub fn auth_check<E: Event>(
|
||||
current_third_party_invite,
|
||||
power_levels_event.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);
|
||||
}
|
||||
@ -290,11 +318,6 @@ pub fn auth_check<E: Event>(
|
||||
// Allow if and only if sender's current power level is greater than
|
||||
// or equal to the invite level
|
||||
if *incoming_event.event_type() == EventType::RoomThirdPartyInvite {
|
||||
#[derive(Deserialize)]
|
||||
struct PowerLevelsContentInvite {
|
||||
invite: Int,
|
||||
}
|
||||
|
||||
let invite_level = match &power_levels_event {
|
||||
Some(power_levels) => {
|
||||
from_json_str::<PowerLevelsContentInvite>(power_levels.content().get())?.invite
|
||||
@ -376,7 +399,7 @@ pub fn auth_check<E: Event>(
|
||||
/// State.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn valid_membership_change(
|
||||
_room_version: &RoomVersion,
|
||||
room_version: &RoomVersion,
|
||||
target_user: &UserId,
|
||||
target_user_membership_event: Option<impl Event>,
|
||||
sender: &UserId,
|
||||
@ -386,13 +409,9 @@ fn valid_membership_change(
|
||||
current_third_party_invite: Option<impl Event>,
|
||||
power_levels_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> {
|
||||
// FIXME: field extracting could be bundled for `content`
|
||||
#[derive(Deserialize)]
|
||||
struct GetMembership {
|
||||
membership: MembershipState,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GetThirdPartyInvite {
|
||||
third_party_invite: Option<Raw<ThirdPartyInvite>>,
|
||||
@ -443,6 +462,54 @@ fn valid_membership_change(
|
||||
let target_user_membership_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 {
|
||||
MembershipState::Join => {
|
||||
if sender != target_user {
|
||||
@ -455,7 +522,15 @@ fn valid_membership_change(
|
||||
let allow = join_rules == JoinRule::Invite
|
||||
&& (target_user_current_membership == MembershipState::Join
|
||||
|| 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 {
|
||||
warn!(
|
||||
@ -557,17 +632,21 @@ fn valid_membership_change(
|
||||
}
|
||||
}
|
||||
#[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.
|
||||
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
|
||||
} else {
|
||||
// 2. If `sender` does not match `state_key`, reject.
|
||||
// 3. If the `sender`'s current membership is not `ban`, `invite`, or `join`, allow.
|
||||
// 4. Otherwise, reject.
|
||||
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
|
||||
} else if matches!(
|
||||
sender_membership,
|
||||
@ -866,8 +945,21 @@ mod tests {
|
||||
},
|
||||
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;
|
||||
|
||||
// #[cfg(not(feature = "unstable-pre-spec"))]
|
||||
#[test]
|
||||
fn test_ban_pass() {
|
||||
let _ =
|
||||
@ -909,10 +1001,15 @@ mod tests {
|
||||
None::<StateEvent>,
|
||||
fetch_state(EventType::RoomPowerLevels, "".to_owned()),
|
||||
fetch_state(EventType::RoomJoinRules, "".to_owned()),
|
||||
#[cfg(feature = "unstable-pre-spec")]
|
||||
None,
|
||||
#[cfg(feature = "unstable-pre-spec")]
|
||||
None
|
||||
)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
// #[cfg(not(feature = "unstable-pre-spec"))]
|
||||
#[test]
|
||||
fn test_ban_fail() {
|
||||
let _ =
|
||||
@ -954,6 +1051,160 @@ mod tests {
|
||||
None::<StateEvent>,
|
||||
fetch_state(EventType::RoomPowerLevels, "".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());
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user