diff --git a/src/api/server/make_join.rs b/src/api/server/make_join.rs index 411a322f5..f9776933c 100644 --- a/src/api/server/make_join.rs +++ b/src/api/server/make_join.rs @@ -1,29 +1,19 @@ -use std::{borrow::ToOwned, pin::pin}; - use axum::extract::State; -use conduwuit::{ - Err, Error, Result, debug, debug_info, info, matrix::pdu::PartialPdu, utils, warn, -}; -use conduwuit_service::Services; -use futures::StreamExt; +use conduwuit::{Err, Error, Result, info, matrix::pdu::PartialPdu, utils}; use ruma::{ - OwnedUserId, RoomId, UserId, + OwnedUserId, api::{ error::{ErrorKind, IncompatibleRoomVersionErrorData}, federation::membership::prepare_join_event, }, assign, - events::{ - StateEventType, - room::{ - join_rules::{AllowRule, JoinRule, RoomJoinRulesEventContent}, - member::{MembershipState, RoomMemberEventContent}, - }, + events::room::{ + join_rules::JoinRule, + member::{MembershipState, RoomMemberEventContent}, }, }; use serde_json::value::to_raw_value; -use service::rooms::state::RoomMutexGuard; -use tokio::join; +use service::rooms::membership::JoinConditionResult; use crate::Ruma; @@ -35,9 +25,6 @@ pub(crate) async fn create_join_event_template_route( State(services): State, body: Ruma, ) -> Result { - if !services.rooms.metadata.exists(&body.room_id).await { - return Err!(Request(NotFound("Room is unknown to this server."))); - } if !services .rooms .state_cache @@ -63,18 +50,6 @@ pub(crate) async fn create_join_event_template_route( .acl_check(&body.identity, &body.room_id) .await?; - if services - .moderation - .is_remote_server_forbidden(&body.identity) - { - warn!( - "Server {} for remote user {} tried joining room ID {} which has a server name that \ - is globally forbidden. Rejecting.", - body.identity, &body.user_id, &body.room_id, - ); - return Err!(Request(Forbidden("Server is banned on this homeserver."))); - } - let room_version = services.rooms.state.get_room_version(&body.room_id).await?; let room_version_rules = room_version.rules().unwrap(); if !body.ver.contains(&room_version) { @@ -87,41 +62,74 @@ pub(crate) async fn create_join_event_template_route( } let state_lock = services.rooms.state.mutex.lock(body.room_id.as_str()).await; - let (is_invited, is_joined) = join!( - services - .rooms - .state_cache - .is_invited(&body.user_id, &body.room_id), - services - .rooms - .state_cache - .is_joined(&body.user_id, &body.room_id) - ); + + let current_membership = services + .rooms + .state_accessor + .get_member(&body.room_id, &body.user_id) + .await + .ok() + .map_or_else(|| MembershipState::Leave, |content| content.membership); + let join_authorized_via_users_server: Option = { - if is_joined || is_invited { - // User is already joined or invited and consequently does not need - // an authorising user + if matches!( + current_membership, + MembershipState::Join | MembershipState::Invite | MembershipState::Knock + ) { + // User is already joined, invited, or knocked, and consequently + // does not need an authorising user None } else if !room_version_rules.authorization.restricted_join_rule { - // room version does not support restricted join rules + // Room version does not support restricted join rules None - } else if user_can_perform_restricted_join(&services, &body.user_id, &body.room_id) - .await? - { - Some( - select_authorising_user(&services, &body.room_id, &body.user_id, &state_lock) - .await?, - ) } else { - None + let join_rules = services + .rooms + .state_accessor + .get_join_rules(&body.room_id) + .await; + + if let JoinRule::Restricted(conditions) | JoinRule::KnockRestricted(conditions) = + join_rules + { + match services + .rooms + .membership + .evaluate_join_conditions(&body.room_id, &body.user_id, &conditions) + .await + { + | JoinConditionResult::Satisfied => { + // Try to find an authorizing user + services + .rooms + .membership + .select_authorizing_user(&body.room_id, &body.user_id, &state_lock) + .await + .map(Some)? + }, + | JoinConditionResult::Unsatisfied => { + return Err!(Request(Forbidden( + "You do not satisfy this room's join conditions." + ))); + }, + | JoinConditionResult::UnableToAuthorizeJoin => { + return Err!(Request(UnableToAuthorizeJoin( + "This server cannot check this room's join conditions." + ))); + }, + } + } else { + None + } } }; - if services.antispam.check_all_joins() && join_authorized_via_users_server.is_none() { + + if services.antispam.check_all_joins() { if services .antispam .meowlnir_accept_make_join(body.room_id.clone(), body.user_id.clone()) .await - .is_err() + != JoinConditionResult::Satisfied { return Err!(Request(Forbidden("Antispam rejected join request."))); } @@ -153,133 +161,3 @@ pub(crate) async fn create_join_event_template_route( }), ) } - -/// Attempts to find a user who is able to issue an invite in the target room. -pub(crate) async fn select_authorising_user<'a>( - services: &Services, - room_id: &'a RoomId, - user_id: &'a UserId, - state_lock: &'a RoomMutexGuard, -) -> Result { - let candidates = services.rooms.state_cache.local_users_in_room(room_id); - - let mut candidates = pin!(candidates); - - while let Some(candidate) = candidates.next().await { - if services - .rooms - .state_accessor - .user_can_invite(room_id, &candidate, user_id, state_lock) - .await - { - return Ok(candidate); - } - } - - Err!(Request(UnableToGrantJoin( - "No user on this server is able to assist in joining." - ))) -} - -/// Checks whether the given user can join the given room via a restricted join. -pub(crate) async fn user_can_perform_restricted_join( - services: &Services, - user_id: &UserId, - room_id: &RoomId, -) -> Result { - let Ok(join_rules_event_content) = services - .rooms - .state_accessor - .room_state_get_content::( - room_id, - &StateEventType::RoomJoinRules, - "", - ) - .await - else { - // No join rules means there's nothing to authorise (defaults to invite) - return Ok(false); - }; - - let (JoinRule::Restricted(r) | JoinRule::KnockRestricted(r)) = - join_rules_event_content.join_rule - else { - // This is not a restricted room - return Ok(false); - }; - - if r.allow.is_empty() { - // This will never be authorisable, return forbidden. - return Err!(Request(Forbidden("You are not invited to this room."))); - } - - let mut could_satisfy = true; - for allow_rule in &r.allow { - match allow_rule { - | AllowRule::RoomMembership(membership) => { - if !services - .rooms - .state_cache - .server_in_room(services.globals.server_name(), &membership.room_id) - .await - { - // Since we can't check this room, mark could_satisfy as - // false so that we can return - // M_UNABLE_TO_AUTHORIZE_JOIN later. - could_satisfy = false; - continue; - } - - if services - .rooms - .state_cache - .is_joined(user_id, &membership.room_id) - .await - { - debug!( - "User {} is allowed to join room {} via membership in room {}", - user_id, room_id, membership.room_id - ); - return Ok(true); - } - }, - | other if other.rule_type() == "fi.mau.spam_checker" => - return match services - .antispam - .meowlnir_accept_make_join(room_id.to_owned(), user_id.to_owned()) - .await - { - | Ok(()) => Ok(true), - | Err(_) => Err!(Request(Forbidden("Antispam rejected join request."))), - }, - | _ => { - // We don't recognise this join rule, so we cannot satisfy the - // request. - could_satisfy = false; - debug_info!( - "Unsupported allow rule in restricted join for room {}: {:?}", - room_id, - allow_rule - ); - }, - } - } - - if could_satisfy { - // We were able to check all the restrictions and can be certain that - // the prospective member is not permitted to join. - Err!(Request(Forbidden( - "You do not belong to any of the rooms or spaces required to join this room." - ))) - } else { - // We were unable to check all the restrictions. This usually means we - // aren't in one of the rooms this one is restricted to, ergo can't - // check its state for the user's membership, and consequently the - // user *might* be able to join if they ask another server. - Err!(Request(UnableToAuthorizeJoin( - "You do not belong to any of the recognised rooms or spaces required to join this \ - room, but this server is unable to verify every requirement. You may be able to \ - join via another server." - ))) - } -} diff --git a/src/api/server/send_join.rs b/src/api/server/send_join.rs index dd9b12e14..aa91957d0 100644 --- a/src/api/server/send_join.rs +++ b/src/api/server/send_join.rs @@ -16,9 +16,11 @@ TimelineEventType, room::member::{MembershipState, RoomMemberEventContent}, }, + room::JoinRule, room_version_rules::RoomVersionRules, }; use serde_json::value::to_raw_value; +use service::rooms::membership::JoinConditionResult; use crate::{Ruma, server::utils::validate_any_membership_event}; @@ -108,18 +110,52 @@ async fn create_join_event( they cannot authorise your join." ))); } - if !super::user_can_perform_restricted_join(services, &sender, room_id).await? { - return Err!(Request(UnableToAuthorizeJoin( - "Joining user did not pass restricted room's rules." + + // If the user is invited or joined we don't need to check the join + // conditions, but in that case the requesting server shouldn't + // have set `join_authorized_via_users_server` in the first place. + + let join_rules = services.rooms.state_accessor.get_join_rules(room_id).await; + + if let JoinRule::Restricted(conditions) | JoinRule::KnockRestricted(conditions) = + join_rules + { + match services + .rooms + .membership + .evaluate_join_conditions(room_id, &sender, &conditions) + .await + { + | JoinConditionResult::Satisfied => { + // Sign the join to prove that we checked the conditions. + + services + .server_keys + .hash_and_sign_event(&mut pdu, room_version_rules) + .map_err(|e| { + err!(Request(InvalidParam(warn!( + "Failed to sign send_join event: {e}" + )))) + })?; + }, + | JoinConditionResult::Unsatisfied => { + return Err!(Request(Forbidden( + "You do not satisfy this room's join conditions." + ))); + }, + | JoinConditionResult::UnableToAuthorizeJoin => { + return Err!(Request(UnableToAuthorizeJoin( + "This server cannot check this room's join conditions." + ))); + }, + } + } else { + // We can't sign this, and if we don't sign it our event will fail + // the auth check, so we have to bail out here. + return Err!(Request(InvalidParam( + "join_authorized_via_users_server was supplied for a room that is not restricted" ))); } - - services - .server_keys - .hash_and_sign_event(&mut pdu, room_version_rules) - .map_err(|e| { - err!(Request(InvalidParam(warn!("Failed to sign send_join event: {e}")))) - })?; } let mutex_lock = services diff --git a/src/service/antispam/mod.rs b/src/service/antispam/mod.rs index 3e235e264..93eda539b 100644 --- a/src/service/antispam/mod.rs +++ b/src/service/antispam/mod.rs @@ -8,7 +8,7 @@ }; use ruminuwuity::{draupnir_antispam, meowlnir_antispam}; -use crate::{client, config, sending, service::Dep}; +use crate::{client, config, rooms::membership::JoinConditionResult, sending, service::Dep}; struct Services { server: Arc, @@ -103,7 +103,7 @@ pub async fn user_may_invite( } else { Ok(()) }; - self.record("user_may_invite", &result); + self.record("user_may_invite", if result.is_ok() { "allowed" } else { "denied" }); return result; } Ok(()) @@ -149,7 +149,7 @@ pub async fn user_may_join_room( } else { Ok(()) }; - self.record("user_may_join_room", &result); + self.record("user_may_join_room", if result.is_ok() { "allowed" } else { "denied" }); return result; } Ok(()) @@ -161,10 +161,10 @@ pub async fn meowlnir_accept_make_join( &self, room_id: OwnedRoomId, user_id: OwnedUserId, - ) -> Result<()> { + ) -> JoinConditionResult { if let Some(Antispam { meowlnir: Some(meowlnir), .. }) = &self.services.config.antispam { debug!(?room_id, ?user_id, "Asking meowlnir for accept_make_join"); - let result = self + match self .send_antispam_request( meowlnir.base_url.as_str(), &meowlnir.secret, @@ -175,18 +175,24 @@ pub async fn meowlnir_accept_make_join( ), ) .await - .inspect(|_| debug!("meowlnir allowed the make_join")) - .inspect_err(|e| debug!("meowlnir denied the make_join: {e:?}")) - .map(|_| ()); - self.record("accept_make_join", &result); - result + { + | Ok(_) => { + debug!("meowlnir allowed the make_join"); + self.record("accept_make_join", "allowed"); + JoinConditionResult::Satisfied + }, + | Err(err) => { + debug!("meowlnir denied the make_join: {err:?}"); + self.record("accept_make_join", "denied"); + JoinConditionResult::Unsatisfied + }, + } } else { - Ok(()) + JoinConditionResult::UnableToAuthorizeJoin } } - fn record(&self, check: &'static str, result: &Result) { - let outcome = if result.is_ok() { "allowed" } else { "denied" }; + fn record(&self, check: &'static str, outcome: &'static str) { self.services .server .metrics diff --git a/src/service/rooms/membership/mod.rs b/src/service/rooms/membership/mod.rs index 2f9a926c5..b8a2ed299 100644 --- a/src/service/rooms/membership/mod.rs +++ b/src/service/rooms/membership/mod.rs @@ -2,7 +2,6 @@ use conduwuit::{ Err, Event, Pdu, Result, Server, debug, debug_info, debug_warn, defer, err, error, info, - is_true, matrix::{ StateKey, event::{gen_event_id, gen_event_id_canonical_json}, @@ -24,12 +23,9 @@ canonical_json::to_canonical_value, events::{ StateEventType, StaticEventContent, - room::{ - join_rules::RoomJoinRulesEventContent, - member::{MembershipState, RoomMemberEventContent}, - }, + room::member::{MembershipState, RoomMemberEventContent}, }, - room::{AllowRule, JoinRule}, + room::{AllowRule, JoinRule, Restricted, RoomMembership}, }; use crate::{ @@ -44,6 +40,18 @@ sending, server_keys, sync, users, }; +#[derive(PartialEq, Eq)] +pub enum JoinConditionResult { + /// The local server verified that the user satisfies at least one join + /// condition. + Satisfied, + /// The local server verified that the user does not satisfy any join + /// conditions. + Unsatisfied, + /// The local server could not check all join conditions. + UnableToAuthorizeJoin, +} + pub struct Service { services: Services, } @@ -110,16 +118,36 @@ pub async fn join_room( ) -> Result { assert!(self.services.globals.user_is_local(sender_user), "user should be local"); + let servers: Vec<_> = servers + .iter() + .filter(|server| !self.services.globals.server_is_ours(server)) + .collect(); + let state_lock = self.services.state.mutex.lock(room_id).await; - if self + let server_in_room = self .services .state_cache - .is_joined(sender_user, room_id) - .await - { - debug_warn!("{sender_user} is already joined in {room_id}"); + .server_in_room(self.services.globals.server_name(), room_id) + .await; + + // Only check our known membership if we're already in the room. + // See: https://forgejo.ellis.link/continuwuation/continuwuity/issues/855 + let current_membership = if server_in_room { + self.services + .state_accessor + .get_member(room_id, sender_user) + .await + .ok() + .map_or_else(|| MembershipState::Leave, |content| content.membership) + } else { + MembershipState::Leave + }; + + if current_membership == MembershipState::Join { return Ok(room_id.to_owned()); + } else if current_membership == MembershipState::Ban { + return Err!(Request(Forbidden("You are banned from this room."))); } if let Err(e) = self @@ -137,31 +165,16 @@ pub async fn join_room( { warn!("Antispam prevented user {} from joining room {}: {}", sender_user, room_id, e); return Err!(Request(Forbidden("You are not allowed to join this room."))); - } - - let server_in_room = self - .services - .state_cache - .server_in_room(self.services.globals.server_name(), room_id) - .await; - - // Only check our known membership if we're already in the room. - // See: https://forgejo.ellis.link/continuwuation/continuwuity/issues/855 - let membership = if server_in_room { - self.services - .state_accessor - .get_member(room_id, sender_user) + } else if self.services.antispam.check_all_joins() { + if self + .services + .antispam + .meowlnir_accept_make_join(room_id.to_owned(), sender_user.to_owned()) .await - } else { - debug!("Ignoring local state for join {room_id}, we aren't in the room yet."); - Ok(RoomMemberEventContent::new(MembershipState::Leave)) - }; - - if let Ok(m) = membership { - if m.membership == MembershipState::Ban { - debug_warn!("{sender_user} is banned from {room_id} but attempted to join"); - // TODO: return reason - return Err!(Request(Forbidden("You are banned from the room."))); + != JoinConditionResult::Satisfied + { + warn!("Antispam prevented user {} from joining room {}", sender_user, room_id); + return Err!(Request(Forbidden("Antispam rejected join request."))); } } @@ -172,31 +185,38 @@ pub async fn join_room( ))); } - if self.services.antispam.check_all_joins() { - if let Err(e) = self - .services - .antispam - .meowlnir_accept_make_join(room_id.to_owned(), sender_user.to_owned()) + if server_in_room { + // Try to join the room locally. + match self + .join_local_room( + sender_user, + room_id, + current_membership, + reason.clone(), + &state_lock, + ) + .boxed() .await { - warn!( - "Antispam prevented user {} from joining room {}: {}", - sender_user, room_id, e - ); - return Err!(Request(Forbidden("Antispam rejected join request."))); + | Ok(()) => { + // We were able to authorize a local join. + info!("Joined room locally"); + return Ok(room_id.to_owned()); + }, + | Err(err) => { + // We couldn't authorize a local join, probably because it's + // restricted and we couldn't satisfy + // the join rules ourselves. + info!(?err, "Failed to join room locally, trying over federation instead"); + }, } } - if server_in_room { - self.join_local_room(sender_user, room_id, reason, servers, state_lock) - .boxed() - .await?; - } else { - // Ask a remote server if we are not participating in this room - self.join_remote_room(sender_user, room_id, reason, servers, state_lock) - .boxed() - .await?; - } + // If we aren't participating in the room, or failed to join locally, + // try to join over federation. + self.join_remote_room(sender_user, room_id, reason, &servers, &state_lock) + .boxed() + .await?; Ok(room_id.to_owned()) } @@ -206,39 +226,39 @@ async fn join_local_room( &self, sender_user: &UserId, room_id: &RoomId, + current_membership: MembershipState, reason: Option, - servers: &[OwnedServerName], - state_lock: RoomMutexGuard, + state_lock: &RoomMutexGuard, ) -> Result { info!("Joining room locally"); - let (room_version, join_rules, is_invited) = join!( + let (room_version, join_rules) = join!( self.services.state.get_room_version(room_id), self.services.state_accessor.get_join_rules(room_id), - self.services.state_cache.is_invited(sender_user, room_id) ); let room_version = room_version?; let room_version_rules = room_version.rules().unwrap(); let mut auth_user: Option = None; - if !is_invited - && matches!(join_rules, JoinRule::Restricted(_) | JoinRule::KnockRestricted(_)) + + if !matches!(current_membership, MembershipState::Invite | MembershipState::Knock) + && let JoinRule::Restricted(conditions) | JoinRule::KnockRestricted(conditions) = + &join_rules { if room_version_rules.authorization.restricted_join_rule { // This is a restricted room, check if we can complete the join // requirements locally. - let needs_auth_user = self - .user_can_perform_restricted_join(sender_user, room_id) - .await; - if needs_auth_user.is_ok_and(is_true!()) { - // If there was an error or the value is false, we'll try - // joining over federation. Since it's Ok(true), we can - // authorise this locally. If we can't select a local - // user, this will remain None, the join will fail, and - // we'll fall back to federation. + if self + .evaluate_join_conditions(room_id, sender_user, conditions) + .await + == JoinConditionResult::Satisfied + { + // Try to find an authorizing user. If we can't find one, + // the local join will fail to authorize and we'll fall back + // to federation. auth_user = self - .select_authorising_user(room_id, sender_user, &state_lock) + .select_authorizing_user(room_id, sender_user, state_lock) .await .ok(); } @@ -251,54 +271,27 @@ async fn join_local_room( content.reason.clone_from(&reason); content.join_authorized_via_users_server = auth_user; - // Try normal join first - let Err(error) = self - .services + self.services .timeline .build_and_append_pdu( PartialPdu::state(sender_user.to_string(), &content), sender_user, Some(room_id), - &state_lock, + state_lock, ) - .await - else { - info!("Joined room locally"); - return Ok(()); - }; + .await?; - if servers.is_empty() - || servers.len() == 1 && self.services.globals.server_is_ours(&servers[0]) - { - if !self.services.metadata.exists(room_id).await { - return Err!(Request( - Unknown( - "Room was not found locally and no servers were found to help us \ - discover it" - ), - NOT_FOUND - )); - } - - return Err(error); - } - - info!( - ?error, - remote_servers = %servers.len(), - "Could not join room locally, attempting remote join", - ); - Box::pin(self.join_remote_room(sender_user, room_id, reason, servers, state_lock)).await + Ok(()) } #[tracing::instrument(skip_all, fields(%sender_user, %room_id), name = "join_remote_room", level = "info")] - pub async fn join_remote_room( + async fn join_remote_room( &self, sender_user: &UserId, room_id: &RoomId, reason: Option, - servers: &[OwnedServerName], - state_lock: RoomMutexGuard, + servers: &[&OwnedServerName], + state_lock: &RoomMutexGuard, ) -> Result { // public so the admin command force-join-room-remotely works info!("Joining {room_id} over federation."); @@ -657,7 +650,7 @@ pub async fn join_remote_room( debug!("Forcing state for new room"); self.services .state - .force_state(room_id, statehash_before_join, added, removed, &state_lock) + .force_state(room_id, statehash_before_join, added, removed, state_lock) .await?; debug!("Updating joined counts for new room"); @@ -679,7 +672,7 @@ pub async fn join_remote_room( &parsed_join_pdu, join_event, once(parsed_join_pdu.event_id.as_ref()), - &state_lock, + state_lock, room_id, ) .await?; @@ -694,7 +687,7 @@ pub async fn join_remote_room( // exist self.services .state - .set_room_state(room_id, statehash_after_join, &state_lock); + .set_room_state(room_id, statehash_after_join, state_lock); if !resident_before { // NOTE: We replace local extremities for this room if we were not a // resident before. We might be doing a remote join to satisfy @@ -704,7 +697,7 @@ pub async fn join_remote_room( info!("Replacing local forward extremities"); self.services .state - .set_forward_extremities(room_id, once(parsed_join_pdu.event_id()), &state_lock) + .set_forward_extremities(room_id, once(parsed_join_pdu.event_id()), state_lock) .await; } drop(cork); @@ -722,7 +715,7 @@ async fn make_join_request( &self, sender_user: &UserId, room_id: &RoomId, - servers: &[OwnedServerName], + servers: &[&OwnedServerName], ) -> Result<(federation::membership::prepare_join_event::v1::Response, OwnedServerName)> { let mut make_join_counter: usize = 1; @@ -762,7 +755,7 @@ async fn make_join_request( warn!("make_join response from {remote_server} failed validation: {e}"); continue; } - return Ok((response, remote_server.clone())); + return Ok((response, (*remote_server).clone())); }, | Err(e) => match e.kind() { | ErrorKind::UnableToAuthorizeJoin => { @@ -810,7 +803,7 @@ async fn make_join_request( /// Attempts to find a user who is able to issue an invite in the target /// room. - pub async fn select_authorising_user<'a>( + pub async fn select_authorizing_user<'a>( &self, room_id: &'a RoomId, user_id: &'a UserId, @@ -838,108 +831,99 @@ pub async fn select_authorising_user<'a>( /// Checks whether the given user can join the given room via a restricted /// join. - pub(crate) async fn user_can_perform_restricted_join( + pub async fn evaluate_join_conditions( &self, - user_id: &UserId, room_id: &RoomId, - ) -> Result { - let Ok(join_rules_event_content) = self - .services - .state_accessor - .room_state_get_content::( - room_id, - &StateEventType::RoomJoinRules, - "", - ) - .await - else { - // No join rules means there's nothing to authorise (defaults to - // invite) - return Ok(false); - }; - - let (JoinRule::Restricted(r) | JoinRule::KnockRestricted(r)) = - join_rules_event_content.join_rule - else { - // This is not a restricted room - return Ok(false); - }; - - if r.allow.is_empty() { + user_id: &UserId, + conditions: &Restricted, + ) -> JoinConditionResult { + if conditions.allow.is_empty() { // This will never be authorisable, return forbidden. - return Err!(Request(Forbidden("You are not invited to this room."))); + return JoinConditionResult::Unsatisfied; } - let mut could_satisfy = true; - for allow_rule in &r.allow { - match allow_rule { - | AllowRule::RoomMembership(membership) => { - if !self - .services - .state_cache - .server_in_room(self.services.globals.server_name(), &membership.room_id) - .await - { - // Since we can't check this room, mark could_satisfy as - // false so that we can return - // M_UNABLE_TO_AUTHORIZE_JOIN later. - could_satisfy = false; - continue; - } + let mut unable_to_authorize = false; - if self - .services - .state_cache - .is_joined(user_id, &membership.room_id) - .await - { - debug!( - "User {} is allowed to join room {} via membership in room {}", - user_id, room_id, membership.room_id - ); - return Ok(true); - } + for rule in &conditions.allow { + match self.evaluate_allow_rule(room_id, user_id, rule).await { + | JoinConditionResult::Satisfied => { + // This allow rule is satisfied, the user can join. + return JoinConditionResult::Satisfied; }, - | other if other.rule_type() == "fi.mau.spam_checker" => - return match self - .services - .antispam - .meowlnir_accept_make_join(room_id.to_owned(), user_id.to_owned()) - .await - { - | Ok(()) => Ok(true), - | Err(_) => Err!(Request(Forbidden("Antispam rejected join request."))), - }, - | _ => { - // We don't recognise this join rule, so we cannot satisfy - // the request. - could_satisfy = false; - debug_info!( - "Unsupported allow rule in restricted join for room {}: {:?}", - room_id, - allow_rule - ); + | JoinConditionResult::Unsatisfied => { + // This allow rule is not satisfied, but the user might be + // able to join if they satisfy another + // rule. Keep checking. + continue; + }, + | JoinConditionResult::UnableToAuthorizeJoin => { + // We couldn't check this allow rule, but the user might be + // able to join if they satisfy another + // rule. Keep checking. + unable_to_authorize = true; + continue; }, } } - if could_satisfy { - // We were able to check all the restrictions and can be certain - // that the prospective member is not permitted to join. - Err!(Request(Forbidden( - "You do not belong to any of the rooms or spaces required to join this room." - ))) + if unable_to_authorize { + // We couldn't conclusively determine if the user satisfies some + // rules. + JoinConditionResult::UnableToAuthorizeJoin } else { - // We were unable to check all the restrictions. This usually means - // we aren't in one of the rooms this one is restricted to, ergo - // can't check its state for the user's membership, and - // consequently the user *might* be able to join if - // they ask another server. - Err!(Request(UnableToAuthorizeJoin( - "You do not belong to any of the recognised rooms or spaces required to join \ - this room, but this server is unable to verify every requirement. You may be \ - able to join via another server." - ))) + // We determined that the user does not satisfy any rules. + JoinConditionResult::Unsatisfied + } + } + + async fn evaluate_allow_rule( + &self, + room_id: &RoomId, + user_id: &UserId, + rule: &AllowRule, + ) -> JoinConditionResult { + match rule { + | AllowRule::RoomMembership(RoomMembership { room_id: condition_room, .. }) => { + if self + .services + .state_cache + .server_in_room(self.services.globals.server_name(), condition_room) + .await + { + if self + .services + .state_cache + .is_joined(user_id, condition_room) + .await + { + debug!( + ?user_id, + ?room_id, + ?condition_room, + "User may join via membership in {condition_room}" + ); + JoinConditionResult::Satisfied + } else { + JoinConditionResult::Unsatisfied + } + } else { + JoinConditionResult::UnableToAuthorizeJoin + } + }, + | other if other.rule_type() == "fi.mau.spam_checker" => + self.services + .antispam + .meowlnir_accept_make_join(room_id.to_owned(), user_id.to_owned()) + .await, + | unknown => { + debug!( + ?user_id, + ?room_id, + rule_type = unknown.rule_type(), + "Could not check unknown allow rule" + ); + JoinConditionResult::UnableToAuthorizeJoin + }, } } }