state-res: Fix restricted joins
This commit is contained in:
parent
117880524f
commit
83e46b6aea
@ -50,7 +50,10 @@ fn allowed_content_keys_for(event_type: &str, version: &RoomVersionId) -> &'stat
|
||||
_ => &["membership"],
|
||||
},
|
||||
"m.room.create" => &["creator"],
|
||||
"m.room.join_rules" => &["join_rule"],
|
||||
"m.room.join_rules" => match version {
|
||||
RoomVersionId::V8 | RoomVersionId::V9 => &["join_rule", "allow"],
|
||||
_ => &["join_rule"],
|
||||
},
|
||||
"m.room.power_levels" => &[
|
||||
"ban",
|
||||
"events",
|
||||
@ -794,6 +797,9 @@ fn object_retain_keys(object: &mut CanonicalJsonObject, keys: &[&str]) {
|
||||
///
|
||||
/// It will return the sender's server (unless it's a third party invite) and the event id server
|
||||
/// (on v1 and v2 room versions)
|
||||
///
|
||||
/// Starting with room version 8, if join_authorised_via_users_server is present, a signature from
|
||||
/// that user is required.
|
||||
fn servers_to_check_signatures(
|
||||
object: &CanonicalJsonObject,
|
||||
version: &RoomVersionId,
|
||||
@ -829,7 +835,28 @@ fn servers_to_check_signatures(
|
||||
return Err(JsonError::field_missing_from_object("event_id"));
|
||||
}
|
||||
},
|
||||
_ => (),
|
||||
RoomVersionId::V3
|
||||
| RoomVersionId::V4
|
||||
| RoomVersionId::V5
|
||||
| RoomVersionId::V6
|
||||
| RoomVersionId::V7 => {}
|
||||
// TODO: And for all future versions that have join_authorised_via_users_server
|
||||
RoomVersionId::V8 | RoomVersionId::V9 => {
|
||||
if let Some(authorized_user) = object
|
||||
.get("content")
|
||||
.and_then(|c| c.as_object())
|
||||
.and_then(|c| c.get("join_authorised_via_users_server"))
|
||||
{
|
||||
let authorized_user = authorized_user.as_str().ok_or_else(|| {
|
||||
JsonError::not_of_type("join_authorised_via_users_server", JsonType::String)
|
||||
})?;
|
||||
let authorized_user = <&UserId>::try_from(authorized_user)
|
||||
.map_err(|e| Error::from(ParseError::UserId(e)))?;
|
||||
|
||||
servers_to_check.insert(authorized_user.server_name().to_owned());
|
||||
}
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
|
||||
Ok(servers_to_check)
|
||||
@ -968,6 +995,80 @@ mod tests {
|
||||
assert!(matches!(verification, Verified::Signatures));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_event_check_signatures_for_authorized_user() {
|
||||
let key_pair_sender = generate_key_pair();
|
||||
let key_pair_authorized = generate_key_pair();
|
||||
let mut signed_event = serde_json::from_str(
|
||||
r#"{
|
||||
"event_id": "$event_id:domain-event",
|
||||
"auth_events": [],
|
||||
"content": {"join_authorised_via_users_server": "@authorized:domain-authorized"},
|
||||
"depth": 3,
|
||||
"hashes": {
|
||||
"sha256": "5jM4wQpv6lnBo7CLIghJuHdW+s2CMBJPUOGOC89ncos"
|
||||
},
|
||||
"origin": "domain",
|
||||
"origin_server_ts": 1000000,
|
||||
"prev_events": [],
|
||||
"room_id": "!x:domain",
|
||||
"sender": "@name:domain-sender",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age_ts": 1000000
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
sign_json("domain-sender", &key_pair_sender, &mut signed_event).unwrap();
|
||||
sign_json("domain-authorized", &key_pair_authorized, &mut signed_event).unwrap();
|
||||
|
||||
let mut public_key_map = BTreeMap::new();
|
||||
add_key_to_map(&mut public_key_map, "domain-sender", &key_pair_sender);
|
||||
add_key_to_map(&mut public_key_map, "domain-authorized", &key_pair_authorized);
|
||||
|
||||
let verification_result = verify_event(&public_key_map, &signed_event, &RoomVersionId::V9);
|
||||
|
||||
assert!(verification_result.is_ok());
|
||||
let verification = verification_result.unwrap();
|
||||
assert!(matches!(verification, Verified::Signatures));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verification_fails_if_missing_signatures_for_authorized_user() {
|
||||
let key_pair_sender = generate_key_pair();
|
||||
let mut signed_event = serde_json::from_str(
|
||||
r#"{
|
||||
"event_id": "$event_id:domain-event",
|
||||
"auth_events": [],
|
||||
"content": {"join_authorised_via_users_server": "@authorized:domain-authorized"},
|
||||
"depth": 3,
|
||||
"hashes": {
|
||||
"sha256": "5jM4wQpv6lnBo7CLIghJuHdW+s2CMBJPUOGOC89ncos"
|
||||
},
|
||||
"origin": "domain",
|
||||
"origin_server_ts": 1000000,
|
||||
"prev_events": [],
|
||||
"room_id": "!x:domain",
|
||||
"sender": "@name:domain-sender",
|
||||
"type": "X",
|
||||
"unsigned": {
|
||||
"age_ts": 1000000
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
sign_json("domain-sender", &key_pair_sender, &mut signed_event).unwrap();
|
||||
|
||||
let mut public_key_map = BTreeMap::new();
|
||||
add_key_to_map(&mut public_key_map, "domain-sender", &key_pair_sender);
|
||||
|
||||
let verification_result = verify_event(&public_key_map, &signed_event, &RoomVersionId::V9);
|
||||
|
||||
assert!(verification_result.is_err()) // Should be Err(VerificationError::
|
||||
// signature_not_found("domain-authorized")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verification_fails_if_required_keys_are_not_given() {
|
||||
let key_pair_sender = generate_key_pair();
|
||||
|
@ -65,6 +65,7 @@ pub fn auth_types_for_event(
|
||||
struct RoomMemberContentFields {
|
||||
membership: Option<Raw<MembershipState>>,
|
||||
third_party_invite: Option<Raw<ThirdPartyInvite>>,
|
||||
join_authorised_via_users_server: Option<Raw<Box<UserId>>>,
|
||||
}
|
||||
|
||||
if let Some(state_key) = state_key {
|
||||
@ -78,6 +79,15 @@ pub fn auth_types_for_event(
|
||||
if !auth_types.contains(&key) {
|
||||
auth_types.push(key);
|
||||
}
|
||||
|
||||
if let Some(Ok(u)) =
|
||||
content.join_authorised_via_users_server.map(|m| m.deserialize())
|
||||
{
|
||||
let key = (EventType::RoomMember, u.to_string());
|
||||
if !auth_types.contains(&key) {
|
||||
auth_types.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let key = (EventType::RoomMember, state_key.to_owned());
|
||||
@ -174,6 +184,9 @@ pub fn auth_check<E: Event>(
|
||||
}
|
||||
|
||||
/*
|
||||
// TODO: In the past this code caused problems federating with synapse, maybe this has been
|
||||
// resolved already. Needs testing.
|
||||
//
|
||||
// 2. Reject if auth_events
|
||||
// a. auth_events cannot have duplicate keys since it's a BTree
|
||||
// b. All entries are valid auth events according to spec
|
||||
@ -195,34 +208,41 @@ pub fn auth_check<E: Event>(
|
||||
}
|
||||
*/
|
||||
|
||||
// 3. If event does not have m.room.create in auth_events reject
|
||||
let room_create_event = match fetch_state(&EventType::RoomCreate, "") {
|
||||
Some(event) => event,
|
||||
None => {
|
||||
warn!("no m.room.create event in auth chain");
|
||||
|
||||
return Ok(false);
|
||||
}
|
||||
Some(e) => e,
|
||||
};
|
||||
|
||||
// 3. If event does not have m.room.create in auth_events reject
|
||||
if !incoming_event.auth_events().any(|id| id.borrow() == room_create_event.event_id().borrow())
|
||||
{
|
||||
warn!("no m.room.create event in auth events");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// [synapse] checks for federation here
|
||||
|
||||
// 4. If type is m.room.aliases
|
||||
if *incoming_event.event_type() == EventType::RoomAliases
|
||||
&& room_version.special_case_aliases_auth
|
||||
{
|
||||
info!("starting m.room.aliases check");
|
||||
// Only in some room versions 6 and below
|
||||
if room_version.special_case_aliases_auth {
|
||||
// 4. If type is m.room.aliases
|
||||
if *incoming_event.event_type() == EventType::RoomAliases {
|
||||
info!("starting m.room.aliases check");
|
||||
|
||||
// If sender's domain doesn't matches state_key, reject
|
||||
if incoming_event.state_key() != Some(sender.server_name().as_str()) {
|
||||
warn!("state_key does not match sender");
|
||||
return Ok(false);
|
||||
// If sender's domain doesn't matches state_key, reject
|
||||
if incoming_event.state_key() != Some(sender.server_name().as_str()) {
|
||||
warn!("state_key does not match sender");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
info!("m.room.aliases event was allowed");
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
info!("m.room.aliases event was allowed");
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// If type is m.room.member
|
||||
let power_levels_event = fetch_state(&EventType::RoomPowerLevels, "");
|
||||
let sender_member_event = fetch_state(&EventType::RoomMember, sender.as_str());
|
||||
|
||||
@ -245,15 +265,16 @@ pub fn auth_check<E: Event>(
|
||||
let target_user =
|
||||
<&UserId>::try_from(state_key).map_err(|e| Error::InvalidPdu(format!("{}", e)))?;
|
||||
|
||||
let join_authed_user =
|
||||
let user_for_join_auth =
|
||||
content.join_authorised_via_users_server.as_ref().and_then(|u| u.deserialize().ok());
|
||||
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
|
||||
};
|
||||
|
||||
let user_for_join_auth_membership = user_for_join_auth
|
||||
.as_ref()
|
||||
.and_then(|auth_user| fetch_state(&EventType::RoomMember, auth_user.as_str()))
|
||||
.and_then(|mem| from_json_str::<GetMembership>(mem.content().get()).ok())
|
||||
.map(|mem| mem.membership)
|
||||
.unwrap_or(MembershipState::Leave);
|
||||
|
||||
if !valid_membership_change(
|
||||
room_version,
|
||||
target_user,
|
||||
@ -264,8 +285,8 @@ pub fn auth_check<E: Event>(
|
||||
current_third_party_invite,
|
||||
power_levels_event.as_ref(),
|
||||
fetch_state(&EventType::RoomJoinRules, "").as_ref(),
|
||||
join_authed_user.as_deref(),
|
||||
join_authed_user_membership,
|
||||
user_for_join_auth.as_deref(),
|
||||
&user_for_join_auth_membership,
|
||||
room_create_event,
|
||||
)? {
|
||||
return Ok(false);
|
||||
@ -296,6 +317,7 @@ pub fn auth_check<E: Event>(
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// If type is m.room.third_party_invite
|
||||
let sender_power_level = if let Some(pl) = &power_levels_event {
|
||||
if let Ok(content) = from_json_str::<PowerLevelsContentFields>(pl.content().get()) {
|
||||
if let Some(level) = content.users.get(sender) {
|
||||
@ -337,6 +359,7 @@ pub fn auth_check<E: Event>(
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// If type is m.room.power_levels
|
||||
if *incoming_event.event_type() == EventType::RoomPowerLevels {
|
||||
info!("starting m.room.power_levels check");
|
||||
|
||||
@ -406,8 +429,8 @@ fn valid_membership_change(
|
||||
current_third_party_invite: Option<impl Event>,
|
||||
power_levels_event: Option<impl Event>,
|
||||
join_rules_event: Option<impl Event>,
|
||||
authed_user_id: Option<&UserId>,
|
||||
auth_user_membership: Option<MembershipState>,
|
||||
user_for_join_auth: Option<&UserId>,
|
||||
user_for_join_auth_membership: &MembershipState,
|
||||
create_room: impl Event,
|
||||
) -> Result<bool> {
|
||||
#[derive(Deserialize)]
|
||||
@ -455,25 +478,17 @@ fn valid_membership_change(
|
||||
let target_user_membership_event_id =
|
||||
target_user_membership_event.as_ref().map(|e| e.event_id());
|
||||
|
||||
// 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 :(
|
||||
let restricted = matches!(join_rules, JoinRule::Restricted(_));
|
||||
|
||||
let allow_based_on_membership =
|
||||
matches!(target_user_current_membership, MembershipState::Invite | MembershipState::Join)
|
||||
|| authed_user_id.is_none();
|
||||
|
||||
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 user_for_join_auth_is_valid = if let Some(user_for_join_auth) = user_for_join_auth {
|
||||
// Is the authorised user allowed to invite users into this room
|
||||
let (auth_user_pl, invite_level) = if let Some(pl) = &power_levels_event {
|
||||
// TODO Refactor all powerlevel parsing
|
||||
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) {
|
||||
let user_pl = if let Some(level) = content.users.get(user_for_join_auth) {
|
||||
*level
|
||||
} else {
|
||||
content.users_default
|
||||
@ -486,14 +501,16 @@ fn valid_membership_change(
|
||||
} else {
|
||||
(int!(0), int!(0))
|
||||
};
|
||||
(auth_user_membership == Some(MembershipState::Join)) && (auth_user_pl >= invite_level)
|
||||
(user_for_join_auth_membership == &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
|
||||
// No auth user was given
|
||||
false
|
||||
};
|
||||
|
||||
Ok(match target_membership {
|
||||
MembershipState::Join => {
|
||||
// 1. If the only previous event is an m.room.create and the state_key is the creator,
|
||||
// allow
|
||||
let mut prev_events = current_event.prev_events();
|
||||
|
||||
let prev_event_is_create_event = prev_events
|
||||
@ -512,40 +529,40 @@ fn valid_membership_change(
|
||||
}
|
||||
|
||||
if sender != target_user {
|
||||
// If the sender does not match state_key, reject.
|
||||
warn!("Can't make other user join");
|
||||
false
|
||||
} else if let MembershipState::Ban = target_user_current_membership {
|
||||
// If the sender is banned, reject.
|
||||
warn!(?target_user_membership_event_id, "Banned user can't join");
|
||||
false
|
||||
} else {
|
||||
let invite_allowed = (join_rules == JoinRule::Invite
|
||||
} else if (join_rules == JoinRule::Invite
|
||||
|| room_version.allow_knocking && join_rules == JoinRule::Knock)
|
||||
// If the join_rule is invite then allow if membership state is invite or join
|
||||
&& (target_user_current_membership == MembershipState::Join
|
||||
|| target_user_current_membership == MembershipState::Invite);
|
||||
|
||||
if invite_allowed {
|
||||
return Ok(true);
|
||||
|| target_user_current_membership == MembershipState::Invite)
|
||||
{
|
||||
true
|
||||
} else if room_version.restricted_join_rules
|
||||
&& matches!(join_rules, JoinRule::Restricted(_))
|
||||
{
|
||||
// If the join_rule is restricted
|
||||
if matches!(
|
||||
target_user_current_membership,
|
||||
MembershipState::Invite | MembershipState::Join
|
||||
) {
|
||||
// If membership state is join or invite, allow.
|
||||
true
|
||||
} else {
|
||||
// If the join_authorised_via_users_server key in content is not a user with
|
||||
// sufficient permission to invite other users, reject.
|
||||
// Otherwise, allow.
|
||||
user_for_join_auth_is_valid
|
||||
}
|
||||
|
||||
if room_version.restricted_join_rules
|
||||
&& restricted
|
||||
&& allow_based_on_membership
|
||||
&& restricted_join_rules_auth
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
if join_rules == JoinRule::Public {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
warn!(
|
||||
join_rules_event_id = ?join_rules_event.as_ref().map(|e| e.event_id()),
|
||||
?target_user_membership_event_id,
|
||||
"Can't join if join rules is not public and user is not invited / joined",
|
||||
);
|
||||
|
||||
false
|
||||
} else {
|
||||
// If the join_rule is public, allow.
|
||||
// Otherwise, reject.
|
||||
join_rules == JoinRule::Public
|
||||
}
|
||||
}
|
||||
MembershipState::Invite => {
|
||||
@ -965,7 +982,7 @@ mod tests {
|
||||
use crate::{
|
||||
event_auth::valid_membership_change,
|
||||
test_utils::{
|
||||
alice, bob, charlie, ella, event_id, member_content_ban, member_content_join, room_id,
|
||||
alice, charlie, ella, event_id, member_content_ban, member_content_join, room_id,
|
||||
to_pdu_event, StateEvent, INITIAL_EVENTS, INITIAL_EVENTS_CREATE_ROOM,
|
||||
},
|
||||
Event, RoomVersion, StateMap,
|
||||
@ -1009,7 +1026,7 @@ mod tests {
|
||||
fetch_state(EventType::RoomPowerLevels, "".to_owned()),
|
||||
fetch_state(EventType::RoomJoinRules, "".to_owned()),
|
||||
None,
|
||||
None,
|
||||
&MembershipState::Leave,
|
||||
fetch_state(EventType::RoomCreate, "".to_owned()).unwrap(),
|
||||
)
|
||||
.unwrap());
|
||||
@ -1053,7 +1070,7 @@ mod tests {
|
||||
fetch_state(EventType::RoomPowerLevels, "".to_owned()),
|
||||
fetch_state(EventType::RoomJoinRules, "".to_owned()),
|
||||
None,
|
||||
None,
|
||||
&MembershipState::Leave,
|
||||
fetch_state(EventType::RoomCreate, "".to_owned()).unwrap(),
|
||||
)
|
||||
.unwrap());
|
||||
@ -1097,7 +1114,7 @@ mod tests {
|
||||
fetch_state(EventType::RoomPowerLevels, "".to_owned()),
|
||||
fetch_state(EventType::RoomJoinRules, "".to_owned()),
|
||||
None,
|
||||
None,
|
||||
&MembershipState::Leave,
|
||||
fetch_state(EventType::RoomCreate, "".to_owned()).unwrap(),
|
||||
)
|
||||
.unwrap());
|
||||
@ -1141,7 +1158,7 @@ mod tests {
|
||||
fetch_state(EventType::RoomPowerLevels, "".to_owned()),
|
||||
fetch_state(EventType::RoomJoinRules, "".to_owned()),
|
||||
None,
|
||||
None,
|
||||
&MembershipState::Leave,
|
||||
fetch_state(EventType::RoomCreate, "".to_owned()).unwrap(),
|
||||
)
|
||||
.unwrap());
|
||||
@ -1167,20 +1184,8 @@ mod tests {
|
||||
&["IPOWER"],
|
||||
);
|
||||
|
||||
let mut member = RoomMemberEventContent::new(MembershipState::Invite);
|
||||
let mut member = RoomMemberEventContent::new(MembershipState::Join);
|
||||
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 auth_events = events
|
||||
.values()
|
||||
@ -1214,7 +1219,7 @@ mod tests {
|
||||
fetch_state(EventType::RoomPowerLevels, "".to_owned()),
|
||||
fetch_state(EventType::RoomJoinRules, "".to_owned()),
|
||||
Some(&alice()),
|
||||
Some(MembershipState::Join),
|
||||
&MembershipState::Join,
|
||||
fetch_state(EventType::RoomCreate, "".to_owned()).unwrap(),
|
||||
)
|
||||
.unwrap());
|
||||
@ -1230,7 +1235,7 @@ mod tests {
|
||||
fetch_state(EventType::RoomPowerLevels, "".to_owned()),
|
||||
fetch_state(EventType::RoomJoinRules, "".to_owned()),
|
||||
Some(&ella()),
|
||||
Some(MembershipState::Join),
|
||||
&MembershipState::Leave,
|
||||
fetch_state(EventType::RoomCreate, "".to_owned()).unwrap(),
|
||||
)
|
||||
.unwrap());
|
||||
@ -1283,7 +1288,7 @@ mod tests {
|
||||
fetch_state(EventType::RoomPowerLevels, "".to_owned()),
|
||||
fetch_state(EventType::RoomJoinRules, "".to_owned()),
|
||||
None,
|
||||
None,
|
||||
&MembershipState::Leave,
|
||||
fetch_state(EventType::RoomCreate, "".to_owned()).unwrap(),
|
||||
)
|
||||
.unwrap());
|
||||
|
Loading…
x
Reference in New Issue
Block a user