# Folders.md: Sistema de Permissões de Pastas (Estilo Windows ACL) ## Visão Geral Implementar controle de acesso a pastas baseado em grupos RBAC, permitindo que `USE KB` inclua seletivamente pastas conforme os grupos do usuário. ## Arquitetura Atual ### Já Existe ✅ | Componente | Arquivo | Estado | |------------|---------|--------| | `KbPermissions` | `core/kb/permissions.rs` | Completo - `AccessLevel::GroupBased`, `FolderPermission` | | `UserContext` | `core/kb/permissions.rs` | Tem `groups: Vec` | | `build_qdrant_permission_filter()` | `core/kb/permissions.rs` | Gera filtros Qdrant por grupo | | `rbac_groups` | Schema core.rs | Tabela existe | | `rbac_user_groups` | Schema core.rs | Tabela existe (user → group) | | `file_shares` | migrations drive | Tem `shared_with_group` | ### Falta Integrar ❌ | Componente | Descrição | |------------|-----------| | `folder_group_access` | Tabela para link pasta → grupo | | `UserContext.groups` | Popular grupos do BD na sessão | | `USE KB` permission check | Verificar grupos antes de adicionar | | UI Admin | Atribuir grupos a pastas | | `USE FOLDER` keyword | BASIC keyword para pastas | --- ## Estrutura de Permissões (Windows-style) ``` Organização ├── Gestores (grupo RBAC) │ ├── Pasta: /relatorios/financeiros │ │ └── Permissão: Gestores (ler/escrever) │ ├── Pasta: /strategic │ │ └── Permissão: Gestores (ler) │ └── Pasta: /publico │ └── Permissão: Todos (ler) │ ├── RH (grupo RBAC) │ ├── Pasta: /rh/documentos │ │ └── Permissão: RH (ler/escrever) │ └── Pasta: /relatorios/financeiros │ └── Permissão: RH (ler) │ └── Todos (grupo implícito) ├── Pasta: /publico │ └── Permissão: Todos (ler) └── Pasta: /intranet └── Permissão: Autenticados (ler) ``` --- ## Plano de Implementação ### Fase 1: Database (Migration) **Arquivo:** `botserver/migrations/6.2.0-02-folder-access/up.sql` ```sql -- Tabela principal: pasta ↔ grupo CREATE TABLE folder_group_access ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), folder_path TEXT NOT NULL, -- Ex: "work/bot1/pasta-protegida" group_id UUID NOT NULL REFERENCES rbac_groups(id) ON DELETE CASCADE, permission_level TEXT NOT NULL DEFAULT 'read', -- read|write|admin created_by UUID REFERENCES users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(folder_path, group_id) ); -- Índice para busca rápida CREATE INDEX idx_folder_group_access_path ON folder_group_access(folder_path); CREATE INDEX idx_folder_group_access_group ON folder_group_access(group_id); -- Adicionar coluna de permissões em kb_collections ALTER TABLE kb_collections ADD COLUMN IF NOT EXISTS access_level TEXT DEFAULT 'authenticated'; COMMENT ON TABLE folder_group_access IS 'Windows-style ACL: pasta ↔ grupo RBAC'; ``` --- ### Fase 2: Schema Diesel **Arquivo:** `botserver/src/core/shared/schema/research.rs` ```rust diesel::table! { folder_group_access (id) { id -> Uuid, folder_path -> Text, group_id -> Uuid, permission_level -> Varchar, created_by -> Nullable, created_at -> Timestamptz, } } diesel::joinable!(folder_group_access -> rbac_groups (group_id)); // Adicionar em kb_collections: access_level -> Varchar, // all|authenticated|role_based|group_based ``` --- ### Fase 3: Modelos Rust **Arquivo:** `botserver/src/core/kb/models.rs` (novo) ```rust #[derive(Debug, Clone, Queryable, Selectable)] #[diesel(table_name = folder_group_access)] pub struct FolderGroupAccess { pub id: Uuid, pub folder_path: String, pub group_id: Uuid, pub permission_level: String, pub created_by: Option, pub created_at: DateTime, } #[derive(Debug, Clone, Insertable)] #[diesel(table_name = folder_group_access)] pub struct NewFolderGroupAccess { pub folder_path: String, pub group_id: Uuid, pub permission_level: String, pub created_by: Option, } ``` --- ### Fase 4: Carregar Grupos do Usuário **Arquivo:** `botserver/src/core/shared/state.rs` Modificar `AppState` ou `UserContext` para popular grupos: ```rust // Nova função em core/kb/permissions.rs pub async fn load_user_groups( db_pool: &DbPool, user_id: Uuid, ) -> Result, String> { use crate::core::shared::schema::core::rbac_groups::dsl::*; use crate::core::shared::schema::core::rbac_user_groups::dsl::*; let mut conn = db_pool.get().map_err(|e| e.to_string())?; let group_names: Vec = rbac_user_groups .inner_join(rbac_groups) .filter(user_id.eq(user_id)) .select(name) .load(&mut conn) .map_err(|e| e.to_string())?; Ok(group_names) } // Em UserContext, adicionar método: impl UserContext { pub async fn with_db_groups(mut self, db_pool: &DbPool) -> Result { let groups = load_user_groups(db_pool, self.user_id).await?; self.groups = groups; Ok(self) } } ``` --- ### Fase 5: Modificar USE KB **Arquivo:** `botserver/src/basic/keywords/use_kb.rs` ```rust use crate::core::kb::permissions::{KbPermissionParser, FolderPermission, AccessLevel}; fn add_kb_to_session( conn_pool: DbPool, session_id: Uuid, bot_id: Uuid, user_id: Uuid, // Adicionar kb_name: &str, ) -> Result<(), String> { // ... código existente ... // NOVO: Verificar permissões de grupo let user_groups = load_user_groups(&conn_pool, user_id)?; let has_access = check_folder_group_access( &conn_pool, &kb_folder_path, &user_groups, )?; if !has_access { return Err(format!( "Acesso negado: KB '{}' requer grupo específico", kb_name )); } // ... resto do código ... } fn check_folder_group_access( conn_pool: &DbPool, folder_path: &str, user_groups: &[String], ) -> Result { // Buscar grupos associados à pasta // Se pasta é "pública" (sem grupos) → permitir // Se usuário está em algum grupo da pasta → permitir // Caso contrário → negar } ``` --- ### Fase 6: Modificar THINK KB (Filtro Qdrant) **Arquivo:** `botserver/src/basic/keywords/think_kb.rs` ```rust use crate::core::kb::permissions::build_qdrant_permission_filter; async fn think_kb_search( // ... parâmetros ... user_id: Uuid, ) -> Result { // Carregar contexto do usuário com grupos let user_groups = load_user_groups(&db_pool, user_id).await?; let user_context = UserContext::authenticated( user_id, Some(email), org_id, ).with_groups(user_groups); // Filtrar resultados do Qdrant com base nos grupos let qdrant_filter = build_qdrant_permission_filter(&user_context); // Buscar no Qdrant com filtro // ... } ``` --- ### Fase 7: Novo Keyword USE FOLDER **Arquivo:** `botserver/src/basic/keywords/use_folder.rs` (novo) ```rust // USE FOLDER "caminho/da/pasta" [READ|WRITE|ADMIN] engine.register_custom_syntax( ["USE", "FOLDER", "$expr$", "($expr$)", "($expr$)", "($expr$)"], true, move |context, inputs| { let folder_path = context.eval_expression_tree(&inputs[0])?.to_string(); // Verificar acesso, adicionar à sessão }, ); ``` --- ### Fase 8: API Endpoints **Arquivo:** `botserver/src/api/routes/rbac.rs` ```rust // GET /api/rbac/folders/{path}/groups // Lista grupos com acesso a uma pasta async fn get_folder_groups( Path(folder_path): Path, State(state): State, ) -> Result>, AppError> { // Query folder_group_access } // POST /api/rbac/folders/{path}/groups/{group_id} async fn add_folder_group( Path((folder_path, group_id)): Path<(String, Uuid)>, Json(payload): Json, ) -> Result, AppError> { // INSERT folder_group_access } // DELETE /api/rbac/folders/{path}/groups/{group_id} async fn remove_folder_group( Path((folder_path, group_id)): Path<(String, Uuid)>, ) -> Result { // DELETE folder_group_access } // GET /api/rbac/users/{user_id}/accessible-folders async fn get_user_accessible_folders( // Lista pastas que o usuário pode acessar ) ``` --- ### Fase 9: UI Admin **Arquivo:** `botui/ui/suite/admin/groups.html` Adicionar aba "Pastas" na visualização do grupo: ```html
``` **Arquivo:** `botui/ui/suite/drive/drive.html` Mostrar cadeado nas pastas protegidas: ```html ``` --- ## Fluxo Completo ``` 1. Usuário executa: USE KB "relatorios-financeiros" 2. Sistema carrega: - user_id da sessão - grupos do usuário (rbac_user_groups → rbac_groups) - grupos da pasta (folder_group_access) 3. Verificação: - Se pasta não tem restrições (pública) → OK - Se usuário está em algum grupo da pasta → OK - Caso contrário → ERRO "Acesso negado" 4. Se OK: - Adiciona KB em session_kb_associations - THINK KB agora busca no Qdrant com filtro de grupos 5. THINK KB retorna: - Apenas documentos de pastas que o usuário tem acesso ``` --- ## Testes ```rust #[test] fn test_group_access_allowed() { let groups = vec!["gestores".to_string()]; let folder_path = "work/bot/financeiro"; // Gestor tem acesso assert!(check_folder_group_access(folder_path, &groups).unwrap()); } #[test] fn test_group_access_denied() { let groups = vec!["rh".to_string()]; let folder_path = "work/bot/financeiro"; // RH não tem acesso a financeiro assert!(!check_folder_group_access(folder_path, &groups).unwrap()); } #[test] fn test_public_folder_access() { let groups = vec![]; let folder_path = "work/bot/publico"; // Pasta pública permite todos assert!(check_folder_group_access(folder_path, &groups).unwrap()); } ``` --- ## Prioridades de Implementação | # | Tarefa | Prioridade | Complexidade | |---|--------|------------|--------------| | 1 | Migration folder_group_access | Alta | Baixa | | 2 | Schema Diesel | Alta | Baixa | | 3 | load_user_groups() | Alta | Média | | 4 | check_folder_group_access() | Alta | Média | | 5 | Modificar USE KB | Alta | Média | | 6 | Modificar THINK KB (Qdrant filter) | Alta | Média | | 7 | API endpoints | Média | Média | | 8 | UI Admin | Média | Alta | | 9 | USE FOLDER keyword | Baixa | Média | --- ## Arquivos a Modificar | Arquivo | Ação | |---------|------| | `migrations/6.2.0-02-folder-access/up.sql` | Criar | | `migrations/6.2.0-02-folder-access/down.sql` | Criar | | `src/core/shared/schema/research.rs` | Modificar | | `src/core/kb/permissions.rs` | Modificar (load_user_groups) | | `src/core/kb/models.rs` | Criar | | `src/basic/keywords/use_kb.rs` | Modificar | | `src/basic/keywords/think_kb.rs` | Modificar | | `src/api/routes/rbac.rs` | Modificar | | `botui/ui/suite/admin/groups.html` | Modificar | | `botui/ui/suite/drive/drive.html` | Modificar |