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};
|
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());
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user