//! 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, 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 { 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, )) }