feat(spaces): add admin commands for space role management
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c8f39ca6ff
commit
673813f5ae
4 changed files with 455 additions and 0 deletions
|
|
@ -11,6 +11,7 @@ use crate::{
|
|||
query::{self, QueryCommand},
|
||||
room::{self, RoomCommand},
|
||||
server::{self, ServerCommand},
|
||||
space::{self, SpaceCommand},
|
||||
token::{self, TokenCommand},
|
||||
user::{self, UserCommand},
|
||||
};
|
||||
|
|
@ -34,6 +35,10 @@ pub enum AdminCommand {
|
|||
/// Commands for managing rooms
|
||||
Rooms(RoomCommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
/// Commands for managing space permissions
|
||||
Spaces(SpaceCommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
/// Commands for managing federation
|
||||
Federation(FederationCommand),
|
||||
|
|
@ -81,6 +86,10 @@ pub(super) async fn process(command: AdminCommand, context: &Context<'_>) -> Res
|
|||
token::process(command, context).await
|
||||
},
|
||||
| Rooms(command) => room::process(command, context).await,
|
||||
| Spaces(command) => {
|
||||
context.bail_restricted()?;
|
||||
space::process(command, context).await
|
||||
},
|
||||
| Federation(command) => federation::process(command, context).await,
|
||||
| Server(command) => server::process(command, context).await,
|
||||
| Debug(command) => debug::process(command, context).await,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ pub(crate) mod media;
|
|||
pub(crate) mod query;
|
||||
pub(crate) mod room;
|
||||
pub(crate) mod server;
|
||||
pub(crate) mod space;
|
||||
pub(crate) mod token;
|
||||
pub(crate) mod user;
|
||||
|
||||
|
|
|
|||
15
src/admin/space/mod.rs
Normal file
15
src/admin/space/mod.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
pub(super) mod roles;
|
||||
|
||||
use clap::Subcommand;
|
||||
use conduwuit::Result;
|
||||
|
||||
use self::roles::SpaceRolesCommand;
|
||||
use crate::admin_command_dispatch;
|
||||
|
||||
#[admin_command_dispatch]
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum SpaceCommand {
|
||||
#[command(subcommand)]
|
||||
/// Manage space roles and permissions
|
||||
Roles(SpaceRolesCommand),
|
||||
}
|
||||
430
src/admin/space/roles.rs
Normal file
430
src/admin/space/roles.rs
Normal file
|
|
@ -0,0 +1,430 @@
|
|||
use clap::Subcommand;
|
||||
use conduwuit::{Err, Result};
|
||||
use conduwuit_core::matrix::space_roles::{
|
||||
RoleDefinition, SpaceRoleMemberEventContent, SpaceRoleRoomEventContent,
|
||||
SpaceRolesEventContent,
|
||||
};
|
||||
use ruma::{OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId, events::StateEventType};
|
||||
use serde_json::value::to_raw_value;
|
||||
|
||||
use conduwuit::matrix::pdu::PduBuilder;
|
||||
|
||||
use crate::{admin_command, admin_command_dispatch};
|
||||
|
||||
macro_rules! custom_state_pdu {
|
||||
($event_type:expr, $state_key:expr, $content:expr) => {
|
||||
PduBuilder {
|
||||
event_type: $event_type.to_owned().into(),
|
||||
content: to_raw_value($content)
|
||||
.expect("Failed to serialize custom state event content"),
|
||||
state_key: Some($state_key.to_owned().into()),
|
||||
..PduBuilder::default()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[admin_command_dispatch]
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum SpaceRolesCommand {
|
||||
/// List all roles defined in a space
|
||||
List {
|
||||
space: OwnedRoomOrAliasId,
|
||||
},
|
||||
/// Add a new role to a space
|
||||
Add {
|
||||
space: OwnedRoomOrAliasId,
|
||||
role_name: String,
|
||||
#[arg(long)]
|
||||
description: Option<String>,
|
||||
#[arg(long)]
|
||||
power_level: Option<i64>,
|
||||
},
|
||||
/// Remove a role from a space
|
||||
Remove {
|
||||
space: OwnedRoomOrAliasId,
|
||||
role_name: String,
|
||||
},
|
||||
/// Assign a role to a user
|
||||
Assign {
|
||||
space: OwnedRoomOrAliasId,
|
||||
user_id: OwnedUserId,
|
||||
role_name: String,
|
||||
},
|
||||
/// Revoke a role from a user
|
||||
Revoke {
|
||||
space: OwnedRoomOrAliasId,
|
||||
user_id: OwnedUserId,
|
||||
role_name: String,
|
||||
},
|
||||
/// Require a role for a room
|
||||
Require {
|
||||
space: OwnedRoomOrAliasId,
|
||||
room_id: OwnedRoomId,
|
||||
role_name: String,
|
||||
},
|
||||
/// Remove a role requirement from a room
|
||||
Unrequire {
|
||||
space: OwnedRoomOrAliasId,
|
||||
room_id: OwnedRoomId,
|
||||
role_name: String,
|
||||
},
|
||||
/// Show a user's roles in a space
|
||||
User {
|
||||
space: OwnedRoomOrAliasId,
|
||||
user_id: OwnedUserId,
|
||||
},
|
||||
/// Show a room's role requirements in a space
|
||||
Room {
|
||||
space: OwnedRoomOrAliasId,
|
||||
room_id: OwnedRoomId,
|
||||
},
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn list(&self, space: OwnedRoomOrAliasId) -> Result {
|
||||
let space_id = self.services.rooms.alias.resolve(&space).await?;
|
||||
let roles_event_type = StateEventType::from("m.space.roles".to_owned());
|
||||
|
||||
let content: SpaceRolesEventContent = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content(&space_id, &roles_event_type, "")
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
if content.roles.is_empty() {
|
||||
return self.write_str("No roles defined in this space.").await;
|
||||
}
|
||||
|
||||
let mut msg = format!("Roles in {}:\n```\n", space_id);
|
||||
for (name, def) in &content.roles {
|
||||
let pl = def
|
||||
.power_level
|
||||
.map(|p| format!(" (power_level: {p})"))
|
||||
.unwrap_or_default();
|
||||
msg.push_str(&format!("- {name}: {}{pl}\n", def.description));
|
||||
}
|
||||
msg.push_str("```");
|
||||
|
||||
self.write_str(&msg).await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn add(
|
||||
&self,
|
||||
space: OwnedRoomOrAliasId,
|
||||
role_name: String,
|
||||
description: Option<String>,
|
||||
power_level: Option<i64>,
|
||||
) -> Result {
|
||||
let space_id = self.services.rooms.alias.resolve(&space).await?;
|
||||
let roles_event_type = StateEventType::from("m.space.roles".to_owned());
|
||||
|
||||
let mut content: SpaceRolesEventContent = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content(&space_id, &roles_event_type, "")
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
if content.roles.contains_key(&role_name) {
|
||||
return Err!("Role '{role_name}' already exists in this space.");
|
||||
}
|
||||
|
||||
content.roles.insert(role_name.clone(), RoleDefinition {
|
||||
description: description.unwrap_or_else(|| role_name.clone()),
|
||||
power_level,
|
||||
});
|
||||
|
||||
let state_lock = self.services.rooms.state.mutex.lock(&space_id).await;
|
||||
let server_user = &self.services.globals.server_user;
|
||||
|
||||
self.services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
custom_state_pdu!("m.space.roles", "", &content),
|
||||
server_user,
|
||||
Some(&space_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.write_str(&format!("Added role '{role_name}' to space {space_id}."))
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn remove(&self, space: OwnedRoomOrAliasId, role_name: String) -> Result {
|
||||
let space_id = self.services.rooms.alias.resolve(&space).await?;
|
||||
let roles_event_type = StateEventType::from("m.space.roles".to_owned());
|
||||
|
||||
let mut content: SpaceRolesEventContent = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content(&space_id, &roles_event_type, "")
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
if content.roles.remove(&role_name).is_none() {
|
||||
return Err!("Role '{role_name}' does not exist in this space.");
|
||||
}
|
||||
|
||||
let state_lock = self.services.rooms.state.mutex.lock(&space_id).await;
|
||||
let server_user = &self.services.globals.server_user;
|
||||
|
||||
self.services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
custom_state_pdu!("m.space.roles", "", &content),
|
||||
server_user,
|
||||
Some(&space_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.write_str(&format!("Removed role '{role_name}' from space {space_id}."))
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn assign(
|
||||
&self,
|
||||
space: OwnedRoomOrAliasId,
|
||||
user_id: OwnedUserId,
|
||||
role_name: String,
|
||||
) -> Result {
|
||||
let space_id = self.services.rooms.alias.resolve(&space).await?;
|
||||
let member_event_type = StateEventType::from("m.space.role.member".to_owned());
|
||||
|
||||
let mut content: SpaceRoleMemberEventContent = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content(&space_id, &member_event_type, user_id.as_str())
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
if content.roles.contains(&role_name) {
|
||||
return Err!("User {user_id} already has role '{role_name}' in this space.");
|
||||
}
|
||||
|
||||
content.roles.push(role_name.clone());
|
||||
|
||||
let state_lock = self.services.rooms.state.mutex.lock(&space_id).await;
|
||||
let server_user = &self.services.globals.server_user;
|
||||
|
||||
self.services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
custom_state_pdu!("m.space.role.member", user_id.as_str(), &content),
|
||||
server_user,
|
||||
Some(&space_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.write_str(&format!(
|
||||
"Assigned role '{role_name}' to {user_id} in space {space_id}."
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn revoke(
|
||||
&self,
|
||||
space: OwnedRoomOrAliasId,
|
||||
user_id: OwnedUserId,
|
||||
role_name: String,
|
||||
) -> Result {
|
||||
let space_id = self.services.rooms.alias.resolve(&space).await?;
|
||||
let member_event_type = StateEventType::from("m.space.role.member".to_owned());
|
||||
|
||||
let mut content: SpaceRoleMemberEventContent = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content(&space_id, &member_event_type, user_id.as_str())
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let original_len = content.roles.len();
|
||||
content.roles.retain(|r| r != &role_name);
|
||||
|
||||
if content.roles.len() == original_len {
|
||||
return Err!("User {user_id} does not have role '{role_name}' in this space.");
|
||||
}
|
||||
|
||||
let state_lock = self.services.rooms.state.mutex.lock(&space_id).await;
|
||||
let server_user = &self.services.globals.server_user;
|
||||
|
||||
self.services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
custom_state_pdu!("m.space.role.member", user_id.as_str(), &content),
|
||||
server_user,
|
||||
Some(&space_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.write_str(&format!(
|
||||
"Revoked role '{role_name}' from {user_id} in space {space_id}."
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn require(
|
||||
&self,
|
||||
space: OwnedRoomOrAliasId,
|
||||
room_id: OwnedRoomId,
|
||||
role_name: String,
|
||||
) -> Result {
|
||||
let space_id = self.services.rooms.alias.resolve(&space).await?;
|
||||
let room_event_type = StateEventType::from("m.space.role.room".to_owned());
|
||||
|
||||
let mut content: SpaceRoleRoomEventContent = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content(&space_id, &room_event_type, room_id.as_str())
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
if content.required_roles.contains(&role_name) {
|
||||
return Err!("Room {room_id} already requires role '{role_name}' in this space.");
|
||||
}
|
||||
|
||||
content.required_roles.push(role_name.clone());
|
||||
|
||||
let state_lock = self.services.rooms.state.mutex.lock(&space_id).await;
|
||||
let server_user = &self.services.globals.server_user;
|
||||
|
||||
self.services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
custom_state_pdu!("m.space.role.room", room_id.as_str(), &content),
|
||||
server_user,
|
||||
Some(&space_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.write_str(&format!(
|
||||
"Room {room_id} now requires role '{role_name}' in space {space_id}."
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn unrequire(
|
||||
&self,
|
||||
space: OwnedRoomOrAliasId,
|
||||
room_id: OwnedRoomId,
|
||||
role_name: String,
|
||||
) -> Result {
|
||||
let space_id = self.services.rooms.alias.resolve(&space).await?;
|
||||
let room_event_type = StateEventType::from("m.space.role.room".to_owned());
|
||||
|
||||
let mut content: SpaceRoleRoomEventContent = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content(&space_id, &room_event_type, room_id.as_str())
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let original_len = content.required_roles.len();
|
||||
content.required_roles.retain(|r| r != &role_name);
|
||||
|
||||
if content.required_roles.len() == original_len {
|
||||
return Err!("Room {room_id} does not require role '{role_name}' in this space.");
|
||||
}
|
||||
|
||||
let state_lock = self.services.rooms.state.mutex.lock(&space_id).await;
|
||||
let server_user = &self.services.globals.server_user;
|
||||
|
||||
self.services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
custom_state_pdu!("m.space.role.room", room_id.as_str(), &content),
|
||||
server_user,
|
||||
Some(&space_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.write_str(&format!(
|
||||
"Removed role requirement '{role_name}' from room {room_id} in space {space_id}."
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn user(&self, space: OwnedRoomOrAliasId, user_id: OwnedUserId) -> Result {
|
||||
let space_id = self.services.rooms.alias.resolve(&space).await?;
|
||||
|
||||
let user_roles = self.services.rooms.roles.user_roles.read().await;
|
||||
let roles = user_roles
|
||||
.get(&space_id)
|
||||
.and_then(|space_users| space_users.get(&user_id));
|
||||
|
||||
match roles {
|
||||
| Some(roles) if !roles.is_empty() => {
|
||||
let roles_list: Vec<&String> = roles.iter().collect();
|
||||
self.write_str(&format!(
|
||||
"Roles for {user_id} in space {space_id}:\n```\n{}\n```",
|
||||
roles_list
|
||||
.iter()
|
||||
.map(|r| format!("- {r}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
))
|
||||
.await
|
||||
},
|
||||
| _ => {
|
||||
self.write_str(&format!("User {user_id} has no roles in space {space_id}."))
|
||||
.await
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn room(&self, space: OwnedRoomOrAliasId, room_id: OwnedRoomId) -> Result {
|
||||
let space_id = self.services.rooms.alias.resolve(&space).await?;
|
||||
|
||||
let room_reqs = self.services.rooms.roles.room_requirements.read().await;
|
||||
let requirements = room_reqs
|
||||
.get(&space_id)
|
||||
.and_then(|space_rooms| space_rooms.get(&room_id));
|
||||
|
||||
match requirements {
|
||||
| Some(reqs) if !reqs.is_empty() => {
|
||||
let reqs_list: Vec<&String> = reqs.iter().collect();
|
||||
self.write_str(&format!(
|
||||
"Required roles for room {room_id} in space {space_id}:\n```\n{}\n```",
|
||||
reqs_list
|
||||
.iter()
|
||||
.map(|r| format!("- {r}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
))
|
||||
.await
|
||||
},
|
||||
| _ => {
|
||||
self.write_str(&format!(
|
||||
"Room {room_id} has no role requirements in space {space_id}."
|
||||
))
|
||||
.await
|
||||
},
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue