69f12aa
use std::collections::HashMap;
use models::space::space_bans;
use models::space::space_member_groups;
use models::space::space_members::{self, MemberStatus};
use models::space::spaces;
use models::users::user;
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
QuerySelect, Set, TransactionTrait,
};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use utoipa::ToSchema;
use uuid::Uuid;
use crate::errors::SpaceError;
use crate::services::service::SpaceService;
use crate::services::spaces::{PageQuery, UserDto};
use crate::share::authz::{MANAGE_BANS, require_perm};
use crate::share::lookup::lookup_space_by_name;
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct BanDto {
pub space_id: Uuid,
pub user: Option<UserDto>,
pub reason: Option<String>,
pub banned_by: Option<UserDto>,
pub created_at: OffsetDateTime,
pub updated_at: OffsetDateTime,
pub expires_at: Option<OffsetDateTime>,
pub allow_speak: bool,
pub allow_push_repo: bool,
pub allow_comment_any: bool,
}
#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct BanUserCmd {
pub user_id: i64,
#[serde(default)]
pub reason: Option<String>,
#[serde(
default,
deserialize_with = "export::serde_datetime::deserialize_opt_datetime"
)]
pub expires_at: Option<OffsetDateTime>,
#[serde(default)]
pub allow_speak: Option<bool>,
#[serde(default)]
pub allow_push_repo: Option<bool>,
#[serde(default)]
pub allow_comment_any: Option<bool>,
}
#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct UpdateBanCmd {
#[serde(default, deserialize_with = "export::serde_patch::deserialize_opt_opt")]
pub reason: Option<Option<String>>,
#[serde(
default,
deserialize_with = "export::serde_datetime::deserialize_opt_opt_datetime"
)]
pub expires_at: Option<Option<OffsetDateTime>>,
#[serde(default)]
pub allow_speak: Option<bool>,
#[serde(default)]
pub allow_push_repo: Option<bool>,
#[serde(default)]
pub allow_comment_any: Option<bool>,
}
fn proto_timestamp(value: OffsetDateTime) -> prost_types::Timestamp {
prost_types::Timestamp {
seconds: value.unix_timestamp(),
nanos: value.nanosecond() as i32,
}
}
pub type BanPage = export::pagination::Page<BanDto>;
impl SpaceService {
#[tracing::instrument(name = "space.ban_user", skip_all, fields(caller = %caller, space = %space_name))]
pub async fn ban_user(
&self,
caller: i64,
space_name: &str,
cmd: BanUserCmd,
) -> Result<BanDto, SpaceError> {
let space = lookup_space_by_name(self, space_name).await?;
require_perm(self, &space, caller, MANAGE_BANS).await?;
user::Entity::find_by_id(cmd.user_id)
.one(self.db().read())
.await?
.ok_or(SpaceError::NotFound)?;
let existing = space_bans::Entity::find_by_id((space.id, cmd.user_id))
.one(self.db().read())
.await?;
if existing.is_some() {
return Err(SpaceError::Conflict(
"user is already banned from this space".to_string(),
));
}
let client = self
.state
.kenauth
.as_ref()
.ok_or_else(|| SpaceError::Internal("kenoauth is not configured".to_string()))?;
client
.ban_member(
caller,
&space.id.to_string(),
cmd.user_id,
cmd.reason.as_deref().unwrap_or_default(),
)
.await?;
if cmd.expires_at.is_some()
|| cmd.allow_speak.unwrap_or(false)
|| cmd.allow_push_repo.unwrap_or(false)
|| cmd.allow_comment_any.unwrap_or(false)
{
client
.update_ban(
caller,
&space.id.to_string(),
cmd.user_id,
None,
cmd.expires_at.map(|value| Some(proto_timestamp(value))),
cmd.allow_speak,
cmd.allow_push_repo,
cmd.allow_comment_any,
)
.await?;
}
let member = space_members::Entity::find_by_id((space.id, cmd.user_id))
.one(self.db().read())
.await?;
let now = OffsetDateTime::now_utc();
let ban_row = space_bans::ActiveModel {
space_id: Set(space.id),
user_id: Set(cmd.user_id),
reason: Set(cmd.reason),
banned_by: Set(caller),
created_at: Set(now),
updated_at: Set(now),
expires_at: Set(cmd.expires_at),
allow_speak: Set(cmd.allow_speak.unwrap_or(false)),
allow_push_repo: Set(cmd.allow_push_repo.unwrap_or(false)),
allow_comment_any: Set(cmd.allow_comment_any.unwrap_or(false)),
};
let inserted = self
.db()
.primary()
.transaction::<_, space_bans::Model, SpaceError>(|txn| {
Box::pin(async move {
let ban = ban_row.insert(txn).await?;
if let Some(m) = member {
let was_active = m.status == MemberStatus::Active;
let mut am: space_members::ActiveModel = m.into();
am.status = Set(MemberStatus::Removed);
am.left_at = Set(Some(now));
am.updated_at = Set(Some(now));
am.update(txn).await?;
if was_active {
spaces::Entity::update_many()
.filter(spaces::Column::Id.eq(space.id))
.col_expr(
spaces::Column::NumsMembers,
sea_orm::sea_query::Expr::cust("GREATEST(nums_members - 1, 0)"),
)
.exec(txn)
.await?;
}
}
space_member_groups::Entity::delete_many()
.filter(space_member_groups::Column::SpaceId.eq(space.id))
.filter(space_member_groups::Column::UserId.eq(cmd.user_id))
.exec(txn)
.await?;
Ok(ban)
})
})
.await?;
let user_map: HashMap<i64, UserDto> = self.load_user_briefs(&[inserted.user_id, caller]).await?;
let user_brief = user_map.get(&inserted.user_id).cloned();
let actor_brief = user_map.get(&caller).cloned();
Ok(BanDto {
space_id: inserted.space_id,
user: user_brief,
reason: inserted.reason,
banned_by: actor_brief,
created_at: inserted.created_at,
updated_at: inserted.updated_at,
expires_at: inserted.expires_at,
allow_speak: inserted.allow_speak,
allow_push_repo: inserted.allow_push_repo,
allow_comment_any: inserted.allow_comment_any,
})
}
pub async fn get_ban(
&self,
caller: i64,
space_name: &str,
uid: i64,
) -> Result<BanDto, SpaceError> {
let space = lookup_space_by_name(self, space_name).await?;
require_perm(self, &space, caller, MANAGE_BANS).await?;
let ban = space_bans::Entity::find_by_id((space.id, uid))
.one(self.db().read())
.await?
.ok_or(SpaceError::NotFound)?;
let user_map: HashMap<i64, UserDto> = self.load_user_briefs(&[ban.user_id, ban.banned_by]).await?;
let user_brief = user_map.get(&ban.user_id).cloned();
let actor_brief = user_map.get(&ban.banned_by).cloned();
Ok(BanDto {
space_id: ban.space_id,
user: user_brief,
reason: ban.reason,
banned_by: actor_brief,
created_at: ban.created_at,
updated_at: ban.updated_at,
expires_at: ban.expires_at,
allow_speak: ban.allow_speak,
allow_push_repo: ban.allow_push_repo,
allow_comment_any: ban.allow_comment_any,
})
}
#[tracing::instrument(name = "space.update_ban", skip_all, fields(caller = %caller, space = %space_name, uid = %uid))]
pub async fn update_ban(
&self,
caller: i64,
space_name: &str,
uid: i64,
cmd: UpdateBanCmd,
) -> Result<BanDto, SpaceError> {
let space = lookup_space_by_name(self, space_name).await?;
require_perm(self, &space, caller, MANAGE_BANS).await?;
let ban = space_bans::Entity::find_by_id((space.id, uid))
.one(self.db().read())
.await?
.ok_or(SpaceError::NotFound)?;
let client = self
.state
.kenauth
.as_ref()
.ok_or_else(|| SpaceError::Internal("kenoauth is not configured".to_string()))?;
client
.update_ban(
caller,
&space.id.to_string(),
uid,
cmd.reason.clone(),
cmd.expires_at.map(|value| value.map(proto_timestamp)),
cmd.allow_speak,
cmd.allow_push_repo,
cmd.allow_comment_any,
)
.await?;
let mut am: space_bans::ActiveModel = ban.into();
if let Some(reason) = cmd.reason {
am.reason = Set(reason);
}
if let Some(expires_at) = cmd.expires_at {
am.expires_at = Set(expires_at);
}
if let Some(v) = cmd.allow_speak {
am.allow_speak = Set(v);
}
if let Some(v) = cmd.allow_push_repo {
am.allow_push_repo = Set(v);
}
if let Some(v) = cmd.allow_comment_any {
am.allow_comment_any = Set(v);
}
let now = OffsetDateTime::now_utc();
am.updated_at = Set(now);
let updated = am.update(self.db().write()).await?;
let user_map: HashMap<i64, UserDto> = self
.load_user_briefs(&[updated.user_id, updated.banned_by])
.await?;
let user_brief = user_map.get(&updated.user_id).cloned();
let actor_brief = user_map.get(&updated.banned_by).cloned();
Ok(BanDto {
space_id: updated.space_id,
user: user_brief,
reason: updated.reason,
banned_by: actor_brief,
created_at: updated.created_at,
updated_at: updated.updated_at,
expires_at: updated.expires_at,
allow_speak: updated.allow_speak,
allow_push_repo: updated.allow_push_repo,
allow_comment_any: updated.allow_comment_any,
})
}
#[tracing::instrument(name = "space.unban_user", skip_all, fields(caller = %caller, space = %space_name, uid = %uid))]
pub async fn unban_user(
&self,
caller: i64,
space_name: &str,
uid: i64,
) -> Result<(), SpaceError> {
let space = lookup_space_by_name(self, space_name).await?;
require_perm(self, &space, caller, MANAGE_BANS).await?;
let client = self
.state
.kenauth
.as_ref()
.ok_or_else(|| SpaceError::Internal("kenoauth is not configured".to_string()))?;
client
.unban_member(caller, &space.id.to_string(), uid)
.await?;
let res = space_bans::Entity::delete_by_id((space.id, uid))
.exec(self.db().write())
.await?;
if res.rows_affected == 0 {
return Err(SpaceError::NotFound);
}
Ok(())
}
pub async fn list_bans(
&self,
caller: i64,
space_name: &str,
q: PageQuery,
) -> Result<BanPage, SpaceError> {
let space = lookup_space_by_name(self, space_name).await?;
require_perm(self, &space, caller, MANAGE_BANS).await?;
let max_pagination = self.config().space.max_pagination;
let default_pagination = self.config().space.default_pagination;
let limit = q
.limit
.unwrap_or(default_pagination)
.clamp(1, max_pagination);
let offset = q
.offset
.unwrap_or(0)
.clamp(0, export::pagination::MAX_OFFSET);
let select = space_bans::Entity::find()
.filter(space_bans::Column::SpaceId.eq(space.id))
.order_by_desc(space_bans::Column::CreatedAt);
let total = select.clone().count(self.db().read()).await? as i64;
let rows = select
.limit(limit)
.offset(offset)
.all(self.db().read())
.await?;
let mut uids: Vec<i64> = Vec::new();
for b in &rows {
uids.push(b.user_id);
uids.push(b.banned_by);
}
let user_map: HashMap<i64, UserDto> = self.load_user_briefs(&uids).await?;
let items = rows
.into_iter()
.map(|b| {
let user_brief = user_map.get(&b.user_id).cloned();
let actor_brief = user_map.get(&b.banned_by).cloned();
BanDto {
space_id: b.space_id,
user: user_brief,
reason: b.reason,
banned_by: actor_brief,
created_at: b.created_at,
updated_at: b.updated_at,
expires_at: b.expires_at,
allow_speak: b.allow_speak,
allow_push_repo: b.allow_push_repo,
allow_comment_any: b.allow_comment_any,
}
})
.collect();
Ok(BanPage {
items,
total,
limit,
offset,
})
}
}