155 lines
4.8 KiB
Rust
155 lines
4.8 KiB
Rust
//! Chat access control based on project roles and conversation visibility settings.
|
|
//!
|
|
//! Rules:
|
|
//! - Owner: can see ALL chats, can ask in ALL chats
|
|
//! - Admin: can see admin+member chats; also sees member's chats regardless
|
|
//! - Member: can see only member-visible chats; cannot create project chats
|
|
//!
|
|
//! Each conversation has two visibility fields:
|
|
//! - `access_visibility`: who can VIEW this chat ("owner" | "admin" | "member")
|
|
//! - `can_ask`: who can SEND messages ("owner" | "admin" | "member")
|
|
//!
|
|
//! Hierarchical override: higher roles can always see lower roles' chats:
|
|
//! Owner → always sees admin/member chats
|
|
//! Admin → always sees member chats
|
|
|
|
use models::ai::ai_conversation;
|
|
use models::projects::MemberRole;
|
|
use uuid::Uuid;
|
|
|
|
/// Result of an access check.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum AccessLevel {
|
|
/// Full access — can view and ask
|
|
Full,
|
|
/// View-only — can see but not send messages
|
|
ViewOnly,
|
|
/// No access
|
|
Denied,
|
|
}
|
|
|
|
/// Check whether `user_role` can VIEW a conversation created by `creator_role`
|
|
/// with the given `access_visibility` setting.
|
|
///
|
|
/// Hierarchical override: higher roles always see lower roles' chats.
|
|
pub fn can_view(creator_role: MemberRole, user_role: MemberRole, access_visibility: &str) -> bool {
|
|
// Owner sees everything
|
|
if user_role == MemberRole::Owner {
|
|
return true;
|
|
}
|
|
|
|
// Admin sees member chats (hierarchical) + chats visible to admin
|
|
if user_role == MemberRole::Admin {
|
|
if creator_role == MemberRole::Member {
|
|
return true; // hierarchical: admin always sees member chats
|
|
}
|
|
return access_visibility == "admin" || access_visibility == "member";
|
|
}
|
|
|
|
// Member: only sees member-visible chats
|
|
if user_role == MemberRole::Member {
|
|
return access_visibility == "member";
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
/// Check whether `user_role` can ASK (send messages) in a conversation.
|
|
pub fn can_ask(creator_role: MemberRole, user_role: MemberRole, can_ask_setting: &str) -> bool {
|
|
// Same hierarchy as viewing
|
|
if user_role == MemberRole::Owner {
|
|
return true;
|
|
}
|
|
|
|
if user_role == MemberRole::Admin {
|
|
if creator_role == MemberRole::Member {
|
|
return true;
|
|
}
|
|
return can_ask_setting == "admin" || can_ask_setting == "member";
|
|
}
|
|
|
|
if user_role == MemberRole::Member {
|
|
return can_ask_setting == "member";
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
/// Check whether `user_role` can CREATE a project-scoped chat.
|
|
pub fn can_create(user_role: MemberRole) -> bool {
|
|
matches!(user_role, MemberRole::Owner | MemberRole::Admin)
|
|
}
|
|
|
|
/// Check full access (view + ask) for a conversation.
|
|
pub fn check_access(
|
|
creator_role: MemberRole,
|
|
user_role: MemberRole,
|
|
access_visibility: &str,
|
|
can_ask_setting: &str,
|
|
) -> AccessLevel {
|
|
if !can_view(creator_role, user_role, access_visibility) {
|
|
return AccessLevel::Denied;
|
|
}
|
|
if can_ask(creator_role, user_role, can_ask_setting) {
|
|
AccessLevel::Full
|
|
} else {
|
|
AccessLevel::ViewOnly
|
|
}
|
|
}
|
|
|
|
/// Resolve the project role of a user. Returns None if the user is not a project member.
|
|
pub async fn resolve_project_role(
|
|
db: &db::database::AppDatabase,
|
|
project_id: Uuid,
|
|
user_id: Uuid,
|
|
) -> Result<Option<MemberRole>, crate::error::AppError> {
|
|
use models::projects::project_members;
|
|
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
|
|
|
let member = project_members::Entity::find()
|
|
.filter(project_members::Column::Project.eq(project_id))
|
|
.filter(project_members::Column::User.eq(user_id))
|
|
.one(db.reader())
|
|
.await?;
|
|
|
|
Ok(member.and_then(|m| m.scope_role().ok()))
|
|
}
|
|
|
|
/// Check access for a specific conversation, resolving the user's role from DB.
|
|
pub async fn check_conversation_access(
|
|
db: &db::database::AppDatabase,
|
|
conversation: &ai_conversation::Model,
|
|
user_id: Uuid,
|
|
) -> Result<AccessLevel, crate::error::AppError> {
|
|
let Some(project_id) = conversation.project_id else {
|
|
// Personal chats: only the owner can access
|
|
if conversation.user_id == user_id {
|
|
return Ok(AccessLevel::Full);
|
|
}
|
|
return Ok(AccessLevel::Denied);
|
|
};
|
|
|
|
// Project chat: check project role
|
|
let Some(user_role) = resolve_project_role(db, project_id, user_id).await? else {
|
|
return Ok(AccessLevel::Denied);
|
|
};
|
|
|
|
// Owner sees everything
|
|
if user_role == MemberRole::Owner {
|
|
return Ok(AccessLevel::Full);
|
|
}
|
|
|
|
// Get creator's role
|
|
let Some(creator_role) = resolve_project_role(db, project_id, conversation.user_id).await?
|
|
else {
|
|
return Ok(AccessLevel::Denied);
|
|
};
|
|
|
|
Ok(check_access(
|
|
creator_role,
|
|
user_role,
|
|
&conversation.access_visibility,
|
|
&conversation.can_ask,
|
|
))
|
|
}
|