gitdataai/libs/service/chat/access.rs

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,
))
}