Replace membership auth with timo's logic
This commit is contained in:
parent
aadccdee64
commit
b846aec94a
@ -1,19 +1,23 @@
|
||||
use std::convert::TryFrom;
|
||||
use std::{collections::BTreeMap, convert::TryFrom};
|
||||
|
||||
use maplit::btreeset;
|
||||
use ruma::{
|
||||
events::{
|
||||
room::{self, join_rules::JoinRule, member::MembershipState},
|
||||
room::{
|
||||
self,
|
||||
join_rules::JoinRule,
|
||||
member::{self, MembershipState},
|
||||
power_levels::{self, PowerLevelsEventContent},
|
||||
},
|
||||
EventType,
|
||||
},
|
||||
identifiers::{RoomVersionId, UserId},
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
room_version::RoomVersion,
|
||||
state_event::{Requester, StateEvent},
|
||||
Result, StateMap,
|
||||
Error, Result, StateMap,
|
||||
};
|
||||
|
||||
/// Represents the 3 event redaction outcomes.
|
||||
@ -223,7 +227,7 @@ pub fn auth_check(
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if !is_membership_change_allowed(event.to_requester(), prev_event, &auth_events)? {
|
||||
if !valid_membership_change(event.to_requester(), prev_event, &auth_events)? {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
@ -298,182 +302,164 @@ pub fn can_federate(auth_events: &StateMap<StateEvent>) -> bool {
|
||||
}
|
||||
|
||||
/// Does the user who sent this member event have required power levels to do so.
|
||||
pub fn is_membership_change_allowed(
|
||||
///
|
||||
/// * `user` - Information about the membership event and user making the request.
|
||||
/// * `prev_event` - The event that occurred immediately before the `user` event or None.
|
||||
/// * `auth_events` - The set of auth events that relate to a membership event.
|
||||
/// this is generated by calling `auth_types_for_event` with the membership event and
|
||||
/// the current State.
|
||||
pub fn valid_membership_change(
|
||||
user: Requester<'_>,
|
||||
prev_event: Option<&StateEvent>,
|
||||
auth_events: &StateMap<StateEvent>,
|
||||
) -> Result<bool> {
|
||||
let state_key = if let Some(s) = user.state_key.as_ref() {
|
||||
s
|
||||
} else {
|
||||
return Err(Error::TempString("State event requires state_key".into()));
|
||||
};
|
||||
|
||||
let content =
|
||||
serde_json::from_str::<room::member::MemberEventContent>(&user.content.to_string())?;
|
||||
|
||||
let membership = content.membership;
|
||||
let target_membership = content.membership;
|
||||
|
||||
// If the only previous event is an m.room.create and the state_key is the creator, allow
|
||||
if user.prev_event_ids.len() == 1 && membership == MembershipState::Join {
|
||||
if let Some(create) = prev_event {
|
||||
if let Ok(create_ev) = create.deserialize_content::<room::create::CreateEventContent>()
|
||||
{
|
||||
if user.state_key == Some(create_ev.creator.to_string())
|
||||
&& create.prev_event_ids().is_empty()
|
||||
{
|
||||
tracing::debug!("m.room.member event allowed via m.room.create");
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let target_user_id = UserId::try_from(user.state_key.as_deref().unwrap()).unwrap();
|
||||
let target_user_id =
|
||||
UserId::try_from(state_key.as_str()).map_err(|e| Error::TempString(format!("{}", e)))?;
|
||||
|
||||
let key = (EventType::RoomMember, Some(user.sender.to_string()));
|
||||
let caller = auth_events.get(&key);
|
||||
|
||||
let caller_in_room = caller.is_some() && check_membership(caller, MembershipState::Join);
|
||||
let caller_invited = caller.is_some() && check_membership(caller, MembershipState::Invite);
|
||||
let sender = auth_events.get(&key);
|
||||
let sender_membership =
|
||||
sender.map_or(Ok::<_, Error>(member::MembershipState::Leave), |pdu| {
|
||||
Ok(pdu
|
||||
.deserialize_content::<room::member::MemberEventContent>()?
|
||||
.membership)
|
||||
})?;
|
||||
|
||||
let key = (EventType::RoomMember, Some(target_user_id.to_string()));
|
||||
let target = auth_events.get(&key);
|
||||
let current = auth_events.get(&key);
|
||||
let current_membership =
|
||||
current.map_or(Ok::<_, Error>(member::MembershipState::Leave), |pdu| {
|
||||
Ok(pdu
|
||||
.deserialize_content::<room::member::MemberEventContent>()?
|
||||
.membership)
|
||||
})?;
|
||||
|
||||
let target_in_room = target.is_some() && check_membership(target, MembershipState::Join);
|
||||
let target_banned = target.is_some() && check_membership(target, MembershipState::Ban);
|
||||
let key = (EventType::RoomPowerLevels, Some("".into()));
|
||||
let power_levels = auth_events.get(&key).map_or_else(
|
||||
|| {
|
||||
Ok::<_, Error>(power_levels::PowerLevelsEventContent {
|
||||
ban: 50.into(),
|
||||
events: BTreeMap::new(),
|
||||
events_default: 0.into(),
|
||||
invite: 50.into(),
|
||||
kick: 50.into(),
|
||||
redact: 50.into(),
|
||||
state_default: 0.into(),
|
||||
users: BTreeMap::new(),
|
||||
users_default: 0.into(),
|
||||
notifications: ruma::events::room::power_levels::NotificationPowerLevels {
|
||||
room: 50.into(),
|
||||
},
|
||||
})
|
||||
},
|
||||
|power_levels| {
|
||||
power_levels
|
||||
.deserialize_content::<PowerLevelsEventContent>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)?;
|
||||
|
||||
let sender_power = power_levels.users.get(&user.sender).map_or_else(
|
||||
|| {
|
||||
if sender_membership != member::MembershipState::Join {
|
||||
None
|
||||
} else {
|
||||
Some(&power_levels.users_default)
|
||||
}
|
||||
},
|
||||
// If it's okay, wrap with Some(_)
|
||||
Some,
|
||||
);
|
||||
let target_power = power_levels.users.get(&target_user_id).map_or_else(
|
||||
|| {
|
||||
if target_membership != member::MembershipState::Join {
|
||||
None
|
||||
} else {
|
||||
Some(&power_levels.users_default)
|
||||
}
|
||||
},
|
||||
// If it's okay, wrap with Some(_)
|
||||
Some,
|
||||
);
|
||||
|
||||
let key = (EventType::RoomJoinRules, Some("".to_string()));
|
||||
let join_rules_event = auth_events.get(&key);
|
||||
|
||||
let mut join_rule = JoinRule::Invite;
|
||||
let mut join_rules = JoinRule::Invite;
|
||||
if let Some(jr) = join_rules_event {
|
||||
join_rule = jr
|
||||
join_rules = jr
|
||||
.deserialize_content::<room::join_rules::JoinRulesEventContent>()?
|
||||
.join_rule;
|
||||
}
|
||||
|
||||
let senders_level = get_user_power_level(user.sender, auth_events);
|
||||
let target_level = get_user_power_level(&target_user_id, auth_events);
|
||||
|
||||
// synapse has a not "what to do for default here 50"
|
||||
let ban_level = get_named_level(auth_events, "ban", 50);
|
||||
|
||||
// TODO clean this up
|
||||
tracing::debug!(
|
||||
"_is_membership_change_allowed: {}",
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"caller_in_room": caller_in_room,
|
||||
"caller_invited": caller_invited,
|
||||
"target_banned": target_banned,
|
||||
"target_in_room": target_in_room,
|
||||
"membership": membership,
|
||||
"join_rule": join_rule,
|
||||
"target_user_id": target_user_id,
|
||||
"event.user_id": user.sender,
|
||||
}))
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
// we already check if the join event was the room creator
|
||||
if membership == MembershipState::Invite && content.third_party_invite.is_some() {
|
||||
// TODO this is unimpled
|
||||
if !verify_third_party_invite(&user, auth_events) {
|
||||
tracing::warn!("not invited to this room",);
|
||||
return Ok(false);
|
||||
if let Some(prev) = prev_event {
|
||||
if prev.kind() == EventType::RoomCreate && prev.prev_event_ids().is_empty() {
|
||||
return Ok(true);
|
||||
}
|
||||
if target_banned {
|
||||
tracing::warn!("banned from this room",);
|
||||
return Ok(false);
|
||||
}
|
||||
tracing::info!("invite succeded");
|
||||
return Ok(true);
|
||||
} else if membership == MembershipState::Join {
|
||||
// If the sender does not match state_key, reject.
|
||||
if user.sender != &target_user_id {
|
||||
tracing::warn!("cannot force another user to join");
|
||||
return Ok(false); // cannot force another user to join
|
||||
} else if target_banned {
|
||||
tracing::warn!("cannot join when banned");
|
||||
return Ok(false); // cannot joined when banned
|
||||
} else if join_rule == JoinRule::Invite {
|
||||
if !caller_in_room && !caller_invited {
|
||||
tracing::warn!("user has not been invited to this room");
|
||||
return Ok(false); // you are not invited to this room
|
||||
}
|
||||
} else if join_rule == JoinRule::Public {
|
||||
tracing::info!("join rule public")
|
||||
// pass
|
||||
} else {
|
||||
tracing::warn!("the join rule is Private or yet to be spec'ed by Matrix");
|
||||
// synapse has 2 TODO's may_join list and private rooms
|
||||
|
||||
// the join_rule is Private or Knock which means it is not yet spec'ed
|
||||
return Ok(false);
|
||||
}
|
||||
} else if membership == MembershipState::Invite {
|
||||
// if senders current membership is not join reject
|
||||
if !caller_in_room {
|
||||
tracing::warn!("invite sender not in room they are inviting user to");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// If the targets current membership is ban or join
|
||||
if target_banned {
|
||||
tracing::warn!("target has been banned");
|
||||
return Ok(false);
|
||||
} else if target_in_room {
|
||||
tracing::warn!("already in room");
|
||||
return Ok(false); // already in room
|
||||
} else {
|
||||
let invite_level = get_named_level(auth_events, "invite", 0);
|
||||
// If the sender's power level is greater than or equal to the invite level, allow.
|
||||
if senders_level < invite_level {
|
||||
tracing::warn!("invite sender does not have power to invite");
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
} else if membership == MembershipState::Leave {
|
||||
if user.sender == &target_user_id && !(caller_in_room || caller_invited) {}
|
||||
// if senders current membership is not join reject
|
||||
if !caller_in_room {
|
||||
tracing::warn!("sender not in room they are leaving");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// If the target user's current membership state is ban, and the sender's power level is less than the ban level, reject
|
||||
if target_banned && senders_level < ban_level {
|
||||
tracing::warn!("not enough power to unban");
|
||||
return Ok(false); // you cannot unban this user
|
||||
|
||||
// If the sender's power level is greater than or equal to the kick level,
|
||||
// and the target user's power level is less than the sender's power level, allow
|
||||
} else if senders_level <= get_named_level(auth_events, "kick", 50)
|
||||
|| target_level < senders_level
|
||||
{
|
||||
tracing::warn!("not enough power to kick user");
|
||||
return Ok(false); // you do not have the power to kick user
|
||||
}
|
||||
} else if membership == MembershipState::Ban {
|
||||
// if senders current membership is not join reject
|
||||
if !caller_in_room {
|
||||
tracing::warn!("ban sender not in room they are banning user from");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
"{} < {} || {} <= {}",
|
||||
senders_level,
|
||||
ban_level,
|
||||
senders_level,
|
||||
target_level
|
||||
);
|
||||
|
||||
if senders_level < ban_level || senders_level <= target_level {
|
||||
tracing::warn!("not enough power to ban");
|
||||
return Ok(false);
|
||||
}
|
||||
} else {
|
||||
tracing::warn!("unknown membership status");
|
||||
// Unknown membership status
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
Ok(if target_membership == MembershipState::Join {
|
||||
if user.sender != &target_user_id {
|
||||
false
|
||||
} else if let MembershipState::Ban = current_membership {
|
||||
false
|
||||
} else {
|
||||
join_rules == JoinRule::Invite
|
||||
&& (current_membership == MembershipState::Join
|
||||
|| current_membership == MembershipState::Invite)
|
||||
|| join_rules == JoinRule::Public
|
||||
}
|
||||
} else if target_membership == MembershipState::Invite {
|
||||
if let Some(_tp_id) = content.third_party_invite {
|
||||
if current_membership == MembershipState::Ban {
|
||||
false
|
||||
} else {
|
||||
// TODO this is not filled out
|
||||
verify_third_party_invite(&user, auth_events)
|
||||
}
|
||||
} else if sender_membership != MembershipState::Join
|
||||
|| current_membership == MembershipState::Join
|
||||
|| current_membership == MembershipState::Ban
|
||||
{
|
||||
false
|
||||
} else {
|
||||
sender_power
|
||||
.filter(|&p| p >= &power_levels.invite)
|
||||
.is_some()
|
||||
}
|
||||
} else if target_membership == MembershipState::Leave {
|
||||
if user.sender == &target_user_id {
|
||||
current_membership == MembershipState::Join
|
||||
|| current_membership == MembershipState::Invite
|
||||
} else if sender_membership != MembershipState::Join
|
||||
|| current_membership == MembershipState::Ban
|
||||
&& sender_power.filter(|&p| p < &power_levels.ban).is_some()
|
||||
{
|
||||
false
|
||||
} else {
|
||||
sender_power.filter(|&p| p >= &power_levels.kick).is_some()
|
||||
&& target_power < sender_power
|
||||
}
|
||||
} else if target_membership == MembershipState::Ban {
|
||||
if sender_membership != MembershipState::Join {
|
||||
false
|
||||
} else {
|
||||
sender_power.filter(|&p| p >= &power_levels.ban).is_some()
|
||||
&& target_power < sender_power
|
||||
}
|
||||
} else {
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
/// Is the event's sender in the room that they sent the event to.
|
||||
|
@ -16,7 +16,7 @@ use serde_json::{json, Value as JsonValue};
|
||||
use state_res::{
|
||||
event_auth::{
|
||||
// auth_check, auth_types_for_event, can_federate, check_power_levels, check_redaction,
|
||||
is_membership_change_allowed,
|
||||
valid_membership_change,
|
||||
},
|
||||
Requester, StateEvent, StateMap, StateStore,
|
||||
};
|
||||
@ -261,7 +261,7 @@ fn test_ban_pass() {
|
||||
sender: &alice(),
|
||||
};
|
||||
|
||||
assert!(is_membership_change_allowed(requester, prev, &auth_events).unwrap())
|
||||
assert!(valid_membership_change(requester, prev, &auth_events).unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -285,5 +285,5 @@ fn test_ban_fail() {
|
||||
sender: &charlie(),
|
||||
};
|
||||
|
||||
assert!(!is_membership_change_allowed(requester, prev, &auth_events).unwrap())
|
||||
assert!(!valid_membership_change(requester, prev, &auth_events).unwrap())
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user