use conduwuit::{Err, Result, err, is_false}; use conduwuit_service::Services; use futures::{FutureExt, future::OptionFuture, join}; use ruma::{ CanonicalJsonObject, EventId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, ServerName, UserId, events::room::member::MembershipState, room_version_rules::RoomVersionRules, }; pub(super) struct AccessCheck<'a> { pub(super) services: &'a Services, pub(super) origin: &'a ServerName, pub(super) room_id: &'a RoomId, pub(super) event_id: Option<&'a EventId>, } impl AccessCheck<'_> { /// Asserts that the server has access to the room and event (if any). /// If the server is permitted, `Ok(())` is returned. Otherwise, a Forbidden /// error is returned. pub(super) async fn assert(&self) -> Result { let acl_check = self .services .rooms .event_handler .acl_check(self.origin, self.room_id) .map(|result| result.is_ok()); let world_readable = self .services .rooms .state_accessor .is_world_readable(self.room_id); let server_in_room = self .services .rooms .state_cache .server_in_room(self.origin, self.room_id); let server_can_see: OptionFuture<_> = self .event_id .map(|event_id| { self.services.rooms.state_accessor.server_can_see_event( self.origin, self.room_id, event_id, ) }) .into(); let (world_readable, server_in_room, server_can_see, acl_check) = join!(world_readable, server_in_room, server_can_see, acl_check); if !acl_check { return Err!(Request(Forbidden("Server access denied."))); } if !world_readable && !server_in_room { return Err!(Request(Forbidden("Server is not in room."))); } if server_can_see.is_some_and(is_false!()) { return Err!(Request(Forbidden("Server is not allowed to see event."))); } Ok(()) } } /// Performs validation on a membership event that should be run on any event a /// remote is trying to send via us. /// /// ## Checks performed /// /// 1. PDU room ID matches request path room ID /// 2. PDU event ID matches request path event ID /// 3. Signature check /// 4. Event type check /// 5. `sender` field presence (and parsing) /// 6. `state_key` field presence (and parsing) /// 7. PDU room format check (PDU check 1) /// /// ## Returns /// /// A resulting tuple of (PDU JSON, target membership state, sender, recipient). pub(crate) async fn validate_any_membership_event( services: &crate::State, body: &serde_json::value::RawValue, room_version_rules: &RoomVersionRules, create_event_id: OwnedEventId, expected_room_id: OwnedRoomId, expected_event_id: OwnedEventId, ) -> Result<(CanonicalJsonObject, MembershipState, OwnedUserId, OwnedUserId)> { let (template_room_id, template_event_id, mut pdu) = services .rooms .event_handler .parse_incoming_pdu(body, Some(room_version_rules)) .await .map_err(|e| err!(Request(BadJson("Invalid membership PDU: {e}"))))?; if template_room_id != expected_room_id { return Err!(Request(InvalidParam("Membership event does not belong to requested room"))); } if template_event_id != expected_event_id { return Err!(Request(InvalidParam(debug_warn!( %template_event_id, %expected_event_id, "Membership event ID does not match provided event ID" )))); } // Only `join` events carry `join_authorised_via_users_server`; co-sign // restricted joins so verification passes. Authorisation is enforced in // create_join_event. let membership_is_join = pdu .get("content") .and_then(|v| v.as_object()) .and_then(|c| c.get("membership")) .and_then(|v| v.as_str()) .is_some_and(|m| m == "join"); let authorising_user = pdu .get("content") .and_then(|v| v.as_object()) .and_then(|c| c.get("join_authorised_via_users_server")) .and_then(|v| v.as_str()) .map(UserId::parse) .and_then(Result::ok); if room_version_rules.authorization.restricted_join_rule && membership_is_join && let Some(authorising_user) = authorising_user && services.globals.user_is_local(&authorising_user) { services .server_keys .hash_and_sign_event(&mut pdu, room_version_rules) .map_err(|e| { err!(Request(InvalidParam("Failed to sign restricted join event: {e}"))) })?; } services .server_keys .verify_event(&pdu, room_version_rules) .await .map_err(|e| { err!(Request(InvalidParam("Signature verification failed on membership event: {e}"))) })?; // Ensure this is a membership event if pdu .get("type") .expect("event must have a type") .as_str() .expect("type must be a string") != "m.room.member" { return Err!(Request(BadJson( "Not allowed to send non-membership event to this endpoint" ))); } let membership = pdu .get("content") .ok_or_else(|| err!(Request(BadJson("Event missing content property"))))? .as_object() .ok_or_else(|| err!(Request(BadJson("Event content is not an object"))))? .get("membership") .ok_or_else(|| err!(Request(BadJson("Event missing membership property"))))? .as_str() .ok_or_else(|| err!(Request(BadJson("Event is not a string"))))? .to_owned(); let sender_user = pdu .get("sender") .and_then(|v| v.as_str()) .map(UserId::parse) .and_then(Result::ok) .ok_or_else(|| err!(Request(InvalidParam("Invalid sender property"))))?; let recipient_user = pdu .get("state_key") .and_then(|v| v.as_str()) .map(UserId::parse) .and_then(Result::ok) .ok_or_else(|| err!(Request(InvalidParam("Invalid state_key property"))))?; // Do a quick format check. The spec doesn't suggest this, but it's probably // a good idea nonetheless. service::rooms::event_handler::Service::pdu_format_check_1( &pdu, room_version_rules, &create_event_id, ) .map_err(|e| { err!(Request(InvalidParam("Membership event violates the room event format: {e}"))) })?; Ok((pdu, membership.into(), sender_user, recipient_user)) }