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