mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-10-23 23:02:57 +00:00
refactor: Clean up join rule evaluation logic
This commit is contained in:
4 files changed
+314
-410
No files matched your search
+63
-185
@@ -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<crate::State>,
|
||||
body: Ruma<prepare_join_event::v1::Request>,
|
||||
) -> Result<prepare_join_event::v1::Response> {
|
||||
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<OwnedUserId> = {
|
||||
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<OwnedUserId> {
|
||||
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<bool> {
|
||||
let Ok(join_rules_event_content) = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content::<RoomJoinRulesEventContent>(
|
||||
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."
|
||||
)))
|
||||
}
|
||||
}
|
||||
+46
-10
@@ -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
|
||||
|
||||
+19
-13
@@ -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<Server>,
|
||||
@@ -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
|
||||
|
||||
+186
-202
@@ -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<OwnedRoomId> {
|
||||
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<String>,
|
||||
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<OwnedUserId> = 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<String>,
|
||||
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<bool> {
|
||||
let Ok(join_rules_event_content) = self
|
||||
.services
|
||||
.state_accessor
|
||||
.room_state_get_content::<RoomJoinRulesEventContent>(
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user