state-res: Fix restricted joins

This commit is contained in:
Timo Kösters 2022-03-14 15:02:21 +01:00 committed by GitHub
parent 117880524f
commit 83e46b6aea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 197 additions and 91 deletions

View File

@ -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();

View File

@ -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());