Update submodules: botserver, botui and project guidelines.
This commit is contained in:
parent
93dc55c47c
commit
fc95cba887
4 changed files with 610 additions and 1302 deletions
429
.opencode/plans/folders.md
Normal file
429
.opencode/plans/folders.md
Normal file
|
|
@ -0,0 +1,429 @@
|
||||||
|
# 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`
|
||||||
|
|
||||||
|
```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<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)
|
||||||
|
|
||||||
|
```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<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:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 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`
|
||||||
|
|
||||||
|
```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<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`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
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)
|
||||||
|
|
||||||
|
```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<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:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- 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:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<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
|
||||||
|
|
||||||
|
```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 |
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 552f37a41c63cb5a86e147a5de3cc2555a521d3f
|
Subproject commit 155d465b14de5d018c7c3eeaf47abe5b3b95fb0c
|
||||||
2
botui
2
botui
|
|
@ -1 +1 @@
|
||||||
Subproject commit 3919a857b2441c472dcdd1bd90b84734f6f10b02
|
Subproject commit 45f56f0f6e4c54c168d73464736fb3fc80f79026
|
||||||
Loading…
Add table
Reference in a new issue