gb/.opencode/plans/folders.md

11 KiB

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<String>
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

-- 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

diesel::table! {
    folder_group_access (id) {
        id -> Uuid,
        folder_path -> Text,
        group_id -> Uuid,
        permission_level -> Varchar,
        created_by -> Nullable<Uuid>,
        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)

#[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<Uuid>,
    pub created_at: DateTime<Utc>,
}

#[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<Uuid>,
}

Fase 4: Carregar Grupos do Usuário

Arquivo: botserver/src/core/shared/state.rs

Modificar AppState ou UserContext para popular grupos:

// Nova função em core/kb/permissions.rs
pub async fn load_user_groups(
    db_pool: &DbPool,
    user_id: Uuid,
) -> Result<Vec<String>, 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<String> = 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<Self, String> {
        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

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<bool, String> {
    // 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

use crate::core::kb::permissions::build_qdrant_permission_filter;

async fn think_kb_search(
    // ... parâmetros ...
    user_id: Uuid,
) -> Result<Value, String> {
    // 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)

// 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

// GET /api/rbac/folders/{path}/groups
// Lista grupos com acesso a uma pasta
async fn get_folder_groups(
    Path(folder_path): Path<String>,
    State(state): State<ApiState>,
) -> Result<Json<Vec<GroupInfo>>, 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<FolderAccessPayload>,
) -> Result<Json<FolderGroupAccess>, 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<StatusCode, AppError> {
    // 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:

<!-- Tab de Pastas -->
<div hx-get="/api/admin/groups/{group_id}/folders" 
     hx-target="#group-folders">
  <button class="tab-btn">Pastas</button>
</div>

<div id="group-folders" class="tab-content">
  <!-- Lista de pastas com acesso -->
  <!-- Botão: Adicionar pasta -->
</div>

Arquivo: botui/ui/suite/drive/drive.html

Mostrar cadeado nas pastas protegidas:

<i class="fa-solid fa-lock" title="Acesso restrito a grupos"></i>

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

#[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