fix(spaces): cascade role removal, validate role names, gate on Space type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
898f4a470c
commit
aa610b055a
3 changed files with 147 additions and 11 deletions
|
|
@ -1,5 +1,5 @@
|
|||
use clap::Subcommand;
|
||||
use conduwuit::{Err, Result};
|
||||
use conduwuit::{Err, Event, Result};
|
||||
use conduwuit_core::matrix::space_roles::{
|
||||
RoleDefinition, SpaceRoleMemberEventContent, SpaceRoleRoomEventContent,
|
||||
SpaceRolesEventContent, SPACE_ROLES_EVENT_TYPE, SPACE_ROLE_MEMBER_EVENT_TYPE,
|
||||
|
|
@ -224,6 +224,89 @@ async fn remove(&self, space: OwnedRoomOrAliasId, role_name: String) -> Result {
|
|||
)
|
||||
.await?;
|
||||
|
||||
// Cascade: remove the role from all users' member events
|
||||
let member_event_type = StateEventType::from(SPACE_ROLE_MEMBER_EVENT_TYPE.to_owned());
|
||||
if let Ok(shortstatehash) = self
|
||||
.services
|
||||
.rooms
|
||||
.state
|
||||
.get_room_shortstatehash(&space_id)
|
||||
.await
|
||||
{
|
||||
use futures::StreamExt;
|
||||
|
||||
let user_entries: Vec<(_, ruma::OwnedEventId)> = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.state_keys_with_ids(shortstatehash, &member_event_type)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
for (state_key, event_id) in user_entries {
|
||||
if let Ok(pdu) = self.services.rooms.timeline.get_pdu(&event_id).await {
|
||||
if let Ok(mut member_content) =
|
||||
pdu.get_content::<SpaceRoleMemberEventContent>()
|
||||
{
|
||||
if member_content.roles.contains(&role_name) {
|
||||
member_content.roles.retain(|r| r != &role_name);
|
||||
self.services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
custom_state_pdu!(
|
||||
SPACE_ROLE_MEMBER_EVENT_TYPE,
|
||||
&state_key,
|
||||
&member_content
|
||||
),
|
||||
server_user,
|
||||
Some(&space_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cascade: remove the role from all rooms' role requirement events
|
||||
let room_event_type = StateEventType::from(SPACE_ROLE_ROOM_EVENT_TYPE.to_owned());
|
||||
|
||||
let room_entries: Vec<(_, ruma::OwnedEventId)> = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.state_keys_with_ids(shortstatehash, &room_event_type)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
for (state_key, event_id) in room_entries {
|
||||
if let Ok(pdu) = self.services.rooms.timeline.get_pdu(&event_id).await {
|
||||
if let Ok(mut room_content) =
|
||||
pdu.get_content::<SpaceRoleRoomEventContent>()
|
||||
{
|
||||
if room_content.required_roles.contains(&role_name) {
|
||||
room_content.required_roles.retain(|r| r != &role_name);
|
||||
self.services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
custom_state_pdu!(
|
||||
SPACE_ROLE_ROOM_EVENT_TYPE,
|
||||
&state_key,
|
||||
&room_content
|
||||
),
|
||||
server_user,
|
||||
Some(&space_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.write_str(&format!("Removed role '{role_name}' from space {space_id}."))
|
||||
.await
|
||||
}
|
||||
|
|
@ -236,6 +319,23 @@ async fn assign(
|
|||
role_name: String,
|
||||
) -> Result {
|
||||
let space_id = resolve_space!(self, space);
|
||||
|
||||
// Read current role definitions to validate the role name
|
||||
let roles_event_type = StateEventType::from(SPACE_ROLES_EVENT_TYPE.to_owned());
|
||||
let role_defs: SpaceRolesEventContent = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content(&space_id, &roles_event_type, "")
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
if !role_defs.roles.contains_key(&role_name) {
|
||||
return self
|
||||
.write_str(&format!("Error: Role '{}' does not exist in this space.", role_name))
|
||||
.await;
|
||||
}
|
||||
|
||||
let member_event_type = StateEventType::from(SPACE_ROLE_MEMBER_EVENT_TYPE.to_owned());
|
||||
|
||||
let mut content: SpaceRoleMemberEventContent = self
|
||||
|
|
@ -325,6 +425,23 @@ async fn require(
|
|||
role_name: String,
|
||||
) -> Result {
|
||||
let space_id = resolve_space!(self, space);
|
||||
|
||||
// Read current role definitions to validate the role name
|
||||
let roles_event_type = StateEventType::from(SPACE_ROLES_EVENT_TYPE.to_owned());
|
||||
let role_defs: SpaceRolesEventContent = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content(&space_id, &roles_event_type, "")
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
if !role_defs.roles.contains_key(&role_name) {
|
||||
return self
|
||||
.write_str(&format!("Error: Role '{}' does not exist in this space.", role_name))
|
||||
.await;
|
||||
}
|
||||
|
||||
let room_event_type = StateEventType::from(SPACE_ROLE_ROOM_EVENT_TYPE.to_owned());
|
||||
|
||||
let mut content: SpaceRoleRoomEventContent = self
|
||||
|
|
|
|||
|
|
@ -68,8 +68,6 @@ struct Services {
|
|||
state_accessor: Dep<rooms::state_accessor::Service>,
|
||||
state_cache: Dep<rooms::state_cache::Service>,
|
||||
state: Dep<rooms::state::Service>,
|
||||
#[allow(dead_code)]
|
||||
spaces: Dep<rooms::spaces::Service>,
|
||||
timeline: Dep<rooms::timeline::Service>,
|
||||
}
|
||||
|
||||
|
|
@ -84,7 +82,6 @@ impl crate::Service for Service {
|
|||
.depend::<rooms::state_accessor::Service>("rooms::state_accessor"),
|
||||
state_cache: args.depend::<rooms::state_cache::Service>("rooms::state_cache"),
|
||||
state: args.depend::<rooms::state::Service>("rooms::state"),
|
||||
spaces: args.depend::<rooms::spaces::Service>("rooms::spaces"),
|
||||
timeline: args.depend::<rooms::timeline::Service>("rooms::timeline"),
|
||||
},
|
||||
server: args.server.clone(),
|
||||
|
|
@ -698,6 +695,23 @@ impl Service {
|
|||
debug_warn!("Failed to sync PLs in {child_room_id}: {e}");
|
||||
}
|
||||
}
|
||||
// Revalidate all space members against all child rooms
|
||||
let space_members: Vec<OwnedUserId> = this
|
||||
.services
|
||||
.state_cache
|
||||
.room_members(&space_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
for member in &space_members {
|
||||
if let Err(e) =
|
||||
this.kick_unqualified_from_rooms(&space_id, member).await
|
||||
{
|
||||
debug_warn!(
|
||||
"Role definition revalidation kick failed for {member}: {e}"
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
| SPACE_ROLE_MEMBER_EVENT_TYPE => {
|
||||
// User's roles changed — auto-join/kick + PL sync
|
||||
|
|
|
|||
|
|
@ -370,13 +370,18 @@ where
|
|||
| SPACE_ROLES_EVENT_TYPE
|
||||
| SPACE_ROLE_MEMBER_EVENT_TYPE
|
||||
| SPACE_ROLE_ROOM_EVENT_TYPE => {
|
||||
let roles: Arc<crate::rooms::roles::Service> =
|
||||
Arc::clone(&*self.services.roles);
|
||||
roles.handle_state_event_change(
|
||||
room_id.to_owned(),
|
||||
event_type_str,
|
||||
state_key.to_string(),
|
||||
);
|
||||
if matches!(
|
||||
self.services.state_accessor.get_room_type(room_id).await,
|
||||
Ok(ruma::room::RoomType::Space)
|
||||
) {
|
||||
let roles: Arc<crate::rooms::roles::Service> =
|
||||
Arc::clone(&*self.services.roles);
|
||||
roles.handle_state_event_change(
|
||||
room_id.to_owned(),
|
||||
event_type_str,
|
||||
state_key.to_string(),
|
||||
);
|
||||
}
|
||||
},
|
||||
| _ => {},
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue