mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 01:28:34 +08:00
feat(G2-03): role-based RLS on config tables — migration 0030 [gov-loop]
Triple migration: crm_pipelines/crm_stages write manager+, conversations write agent+ (viewer read-only), SELECTs unchanged; old ALL policies dropped. Ratchet flip: 2 GAP(G2) it.fails in tests/invariants/gov-1-rbac.test.ts flipped to normal tests (DESKCOMM_GOV_INVARIANTS_EDIT=1, legit exception). Verified-by: gov-verifier PASS 2026-07-16T23:07:32-0300
This commit is contained in:
@@ -257,6 +257,36 @@ Notas:
|
||||
Enforcement em **duas camadas obrigatórias**: RLS (fronteira) + helper único de
|
||||
rota (`require-role`, G2-01) — nunca só UI (anti-padrão 3).
|
||||
|
||||
### 4.1 Auditoria de policies RLS por role (G2-03)
|
||||
|
||||
Auditoria mecânica do `supabase/baseline.sql` (gov/G2, 2026-07-16): tabela →
|
||||
policy de escrita → role mínimo efetivo. **Org-flat** = qualquer membro da org
|
||||
(incl. viewer) escreve. Tabelas fora da matriz §4 (ai_*, channel_sessions,
|
||||
contacts operacionais etc.) não são "config" e ficam fora do alvo desta fase.
|
||||
|
||||
| Tabela | Policy de escrita (baseline) | Role mínimo | Org-flat? | Config §4? | Ação G2-03 |
|
||||
|---|---|---|---|---|---|
|
||||
| api_tokens | `api_tokens_admin_only` | admin | não | sim | manter |
|
||||
| lgpd_requests | `lgpd_requests_admin_write` | admin | não | — | manter |
|
||||
| merge_queue | `merge_queue_manager_write` | manager | não | — | manter |
|
||||
| tenant_integrations | `tenant_integrations_admin_write` | manager | não | sim (integrações) | manter |
|
||||
| user_organizations (team) | `user_orgs_insert/update/delete` | admin | não | sim | manter |
|
||||
| organizations (settings) | `orgs_write_platform_admin` | platform admin (tenant escreve via service role + guard de rota) | não | sim | manter |
|
||||
| **crm_pipelines** | `tenant_isolation_crm_pipelines_all` (ALL) | **qualquer membro** | **sim** | **sim (pipelines config)** | **migration 0030: write manager+** |
|
||||
| **crm_stages** | `tenant_isolation_crm_stages_all` (ALL) | **qualquer membro** | **sim** | **sim (config de pipeline, nota 4)** | **migration 0030: write manager+** |
|
||||
| **conversations** | `conversations_tenant_isolation_all` (ALL) | **qualquer membro (incl. viewer)** | **sim** | não é config, mas viewer é read-only (nota 1) | **migration 0030: write agent+; SELECT intocado (escopo own é G4-01)** |
|
||||
| messages | `messages_tenant_isolation_all` (ALL) | qualquer membro | sim | não (operacional; escopo segue conversations) | G4-01 |
|
||||
| contacts | `tenant_isolation_contacts_all` (ALL) | qualquer membro | sim | não (agent tem org:write, nota 3; viewer-write fica pra G4 junto do escopo) | G4 |
|
||||
| crm_leads / crm_lead_activities / crm_lead_links | `tenant_isolation_*` (ALL) | qualquer membro | sim | não (operacional; escopo own é G4-01) | G4-01 |
|
||||
| channel_sessions, ai_*, orders, nuvemshop_products, idempotency_keys, warmup, storage_redaction_queue | `*_tenant_isolation_*` (ALL) | qualquer membro | sim | não classificado na matriz §4 | fora do escopo G2-03 |
|
||||
| api_audit_log | `audit_log_insert_tenant_member` (insert-only, append) | qualquer membro | por design | — | manter (select manager é read, não write) |
|
||||
|
||||
Resultado: as tabelas de **config org-flat** são `crm_pipelines` e `crm_stages`;
|
||||
a migration `20260716120000_0030_config_rls_role_policies.sql` aplica
|
||||
`fn_role_at_least(organization_id, 'manager')` nas write-policies delas e
|
||||
`fn_role_at_least(organization_id, 'agent')` no write de `conversations`
|
||||
(viewer read-only), mantendo todos os SELECTs org-flat.
|
||||
|
||||
## 5. Roteamento (decisões G1-06; G5 implementa)
|
||||
|
||||
- **Modos no MVP** (decisão G1-06b): `manual` (só claim/atribuição humana) e
|
||||
@@ -312,8 +342,8 @@ entre orgs (pré-requisito de tudo) já é coberto por
|
||||
| 1. RBAC | `gov-1-rbac.test.ts` → "fn_user_role_in mapeia viewer→1, agent→2, manager→3, admin→4" | passa |
|
||||
| 1. RBAC | `gov-1-rbac.test.ts` → "RLS impede agent de se auto-promover (user_orgs_update é admin-only)" | passa |
|
||||
| 1. RBAC | `gov-1-rbac.test.ts` → "role de membro é editável via API — PATCH /api/v1/team/[user_id]/role existe" (gap do plano JÁ fechado pelo EPIC-09) | passa |
|
||||
| 1. RBAC | `gov-1-rbac.test.ts` → "agent NÃO escreve config de pipeline (spec 13 §4: manager+)" | GAP G2 |
|
||||
| 1. RBAC | `gov-1-rbac.test.ts` → "viewer NÃO escreve em conversations (spec 13 §4: viewer é read-only)" | GAP G2 |
|
||||
| 1. RBAC | `gov-1-rbac.test.ts` → "agent NÃO escreve config de pipeline (spec 13 §4: manager+)" | passa (fechado por G2-03, migration 0030) |
|
||||
| 1. RBAC | `gov-1-rbac.test.ts` → "viewer NÃO escreve em conversations (spec 13 §4: viewer é read-only)" | passa (fechado por G2-03, migration 0030) |
|
||||
| 2. Atribuição | `gov-2-assignment.test.ts` → "conversations tem assigned_to_user_id + assigned_at, com FK para auth.users" | passa |
|
||||
| 2. Atribuição | `gov-2-assignment.test.ts` → "crm_leads tem owner_user_id" | passa |
|
||||
| 2. Atribuição | `gov-2-assignment.test.ts` → "mudança de owner em crm_leads emite lead.assigned no event_log" | passa |
|
||||
|
||||
+7
-2
@@ -226,8 +226,13 @@
|
||||
"priority": 30,
|
||||
"lane": "core",
|
||||
"kind": "build",
|
||||
"passes": false,
|
||||
"verification": null
|
||||
"passes": true,
|
||||
"verification": {
|
||||
"verdict": "PASS",
|
||||
"by": "gov-verifier",
|
||||
"at": "2026-07-16T23:07:32-0300",
|
||||
"commit": "self"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "G2-04",
|
||||
|
||||
@@ -154,3 +154,20 @@
|
||||
(pré-existente do EPIC-09, race de ms entre 2 admins) — candidata a inbox se
|
||||
o dono quiser fechar com constraint/trigger.
|
||||
- Próxima sessão: G2-03 (RLS por role nas tabelas de config — migration tripla).
|
||||
|
||||
## 2026-07-16 — sessão 11 do loop (core) — G2-03
|
||||
|
||||
- G2-03 (RLS por role): migration 0030 em tripla (migrations/ + apêndice
|
||||
baseline + MANIFEST; types.ts intocado — policies não mudam contrato).
|
||||
crm_pipelines/crm_stages: SELECT org-flat + write manager+; conversations:
|
||||
SELECT byte-idêntico ao antigo (leitura NÃO estreitada — own-scope é G4-01)
|
||||
+ write agent+ (viewer read-only). Policies ALL antigas dropadas (sem OR órfão).
|
||||
- Flip da catraca: 2 it.fails GAP(G2) de gov-1-rbac viraram testes normais
|
||||
(única mudança no arquivo; commit com DESKCOMM_GOV_INVARIANTS_EDIT=1).
|
||||
Novo invariante gov-1-rbac-config-write.test.ts (positivos+negativos).
|
||||
- Auditoria de policies registrada como spec 13 §4.1; Apêndice A: 2 GAP G2 → passa.
|
||||
- gov-verifier: PASS 1ª rodada, hash-check OK. test:db install+update verdes,
|
||||
35/35 invariantes pós-update, 123 unit.
|
||||
- INB-05 aberto (proposal): spec 13 §4 nota 8 prevê api_audit_log SELECT
|
||||
manager+ "aplicada em G2", mas nenhuma feature G2 cobre — decisão do dono.
|
||||
- Próxima sessão: G2-04 (e2e Playwright de papéis) fecha a fase → checkpoint G2.
|
||||
|
||||
@@ -4201,3 +4201,74 @@ revoke all on function public.fn_mark_conversation_message(uuid, text, text, tim
|
||||
grant execute on function public.fn_upsert_wa_contact(uuid, text, text, text, text, text) to service_role;
|
||||
grant execute on function public.fn_upsert_wa_conversation(uuid, uuid, uuid) to service_role;
|
||||
grant execute on function public.fn_mark_conversation_message(uuid, text, text, timestamptz) to service_role;
|
||||
|
||||
-- ---- RLS por role em tabelas de config + viewer read-only (migration 0030) ----
|
||||
-- G2-03: spec 13 §4 — pipelines/stages (config) write manager+; conversations
|
||||
-- write agent+ (viewer read-only). SELECT permanece org-flat (escopo own é G4).
|
||||
-- Idempotente: drop if exists + create (auto-curativo no update.sh de clones).
|
||||
|
||||
drop policy if exists "tenant_isolation_crm_pipelines_all" on public.crm_pipelines;
|
||||
drop policy if exists "crm_pipelines_select" on public.crm_pipelines;
|
||||
drop policy if exists "crm_pipelines_manager_write" on public.crm_pipelines;
|
||||
|
||||
create policy "crm_pipelines_select" on public.crm_pipelines
|
||||
for select using (
|
||||
(organization_id in (select public.fn_user_org_ids()))
|
||||
or public.fn_is_platform_admin()
|
||||
);
|
||||
|
||||
create policy "crm_pipelines_manager_write" on public.crm_pipelines
|
||||
using (
|
||||
public.fn_is_platform_admin()
|
||||
or ((organization_id in (select public.fn_user_org_ids()))
|
||||
and public.fn_role_at_least(organization_id, 'manager'))
|
||||
)
|
||||
with check (
|
||||
public.fn_is_platform_admin()
|
||||
or ((organization_id in (select public.fn_user_org_ids()))
|
||||
and public.fn_role_at_least(organization_id, 'manager'))
|
||||
);
|
||||
|
||||
drop policy if exists "tenant_isolation_crm_stages_all" on public.crm_stages;
|
||||
drop policy if exists "crm_stages_select" on public.crm_stages;
|
||||
drop policy if exists "crm_stages_manager_write" on public.crm_stages;
|
||||
|
||||
create policy "crm_stages_select" on public.crm_stages
|
||||
for select using (
|
||||
(organization_id in (select public.fn_user_org_ids()))
|
||||
or public.fn_is_platform_admin()
|
||||
);
|
||||
|
||||
create policy "crm_stages_manager_write" on public.crm_stages
|
||||
using (
|
||||
public.fn_is_platform_admin()
|
||||
or ((organization_id in (select public.fn_user_org_ids()))
|
||||
and public.fn_role_at_least(organization_id, 'manager'))
|
||||
)
|
||||
with check (
|
||||
public.fn_is_platform_admin()
|
||||
or ((organization_id in (select public.fn_user_org_ids()))
|
||||
and public.fn_role_at_least(organization_id, 'manager'))
|
||||
);
|
||||
|
||||
drop policy if exists "conversations_tenant_isolation_all" on public.conversations;
|
||||
drop policy if exists "conversations_select" on public.conversations;
|
||||
drop policy if exists "conversations_agent_write" on public.conversations;
|
||||
|
||||
create policy "conversations_select" on public.conversations
|
||||
for select using (
|
||||
(organization_id in (select public.fn_user_org_ids()))
|
||||
or public.fn_is_platform_admin()
|
||||
);
|
||||
|
||||
create policy "conversations_agent_write" on public.conversations
|
||||
using (
|
||||
public.fn_is_platform_admin()
|
||||
or ((organization_id in (select public.fn_user_org_ids()))
|
||||
and public.fn_role_at_least(organization_id, 'agent'))
|
||||
)
|
||||
with check (
|
||||
public.fn_is_platform_admin()
|
||||
or ((organization_id in (select public.fn_user_org_ids()))
|
||||
and public.fn_role_at_least(organization_id, 'agent'))
|
||||
);
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
-- 0030_config_rls_role_policies — G2-03: RLS por role nas tabelas de config
|
||||
-- (defesa em profundidade da matriz spec 13 §4; padrão fn_role_at_least já
|
||||
-- usado em api_tokens/lgpd_requests/merge_queue).
|
||||
--
|
||||
-- Alvos (auditoria em docs/specs/13-spec-governanca-atendimento.md §4.1):
|
||||
-- * crm_pipelines / crm_stages — "pipelines (config)": read org-flat,
|
||||
-- WRITE manager+ (era org-flat: qualquer membro escrevia config).
|
||||
-- * conversations — viewer é read-only: WRITE agent+. SELECT permanece
|
||||
-- org-flat intocado (escopo own/unassigned é G4-01, não aqui).
|
||||
--
|
||||
-- Idempotente e auto-curativo: drop policy if exists + create. Sem dados a
|
||||
-- corrigir (policies não invalidam linhas existentes). Sem BEGIN/COMMIT
|
||||
-- (runner envolve em transação). Portável em psql puro.
|
||||
|
||||
-- crm_pipelines: split da policy ALL org-flat em SELECT org + WRITE manager+
|
||||
drop policy if exists "tenant_isolation_crm_pipelines_all" on public.crm_pipelines;
|
||||
drop policy if exists "crm_pipelines_select" on public.crm_pipelines;
|
||||
drop policy if exists "crm_pipelines_manager_write" on public.crm_pipelines;
|
||||
|
||||
create policy "crm_pipelines_select" on public.crm_pipelines
|
||||
for select using (
|
||||
(organization_id in (select public.fn_user_org_ids()))
|
||||
or public.fn_is_platform_admin()
|
||||
);
|
||||
|
||||
create policy "crm_pipelines_manager_write" on public.crm_pipelines
|
||||
using (
|
||||
public.fn_is_platform_admin()
|
||||
or ((organization_id in (select public.fn_user_org_ids()))
|
||||
and public.fn_role_at_least(organization_id, 'manager'))
|
||||
)
|
||||
with check (
|
||||
public.fn_is_platform_admin()
|
||||
or ((organization_id in (select public.fn_user_org_ids()))
|
||||
and public.fn_role_at_least(organization_id, 'manager'))
|
||||
);
|
||||
|
||||
-- crm_stages: mesma regra (stages são config de pipeline — spec 13 §4 nota 4)
|
||||
drop policy if exists "tenant_isolation_crm_stages_all" on public.crm_stages;
|
||||
drop policy if exists "crm_stages_select" on public.crm_stages;
|
||||
drop policy if exists "crm_stages_manager_write" on public.crm_stages;
|
||||
|
||||
create policy "crm_stages_select" on public.crm_stages
|
||||
for select using (
|
||||
(organization_id in (select public.fn_user_org_ids()))
|
||||
or public.fn_is_platform_admin()
|
||||
);
|
||||
|
||||
create policy "crm_stages_manager_write" on public.crm_stages
|
||||
using (
|
||||
public.fn_is_platform_admin()
|
||||
or ((organization_id in (select public.fn_user_org_ids()))
|
||||
and public.fn_role_at_least(organization_id, 'manager'))
|
||||
)
|
||||
with check (
|
||||
public.fn_is_platform_admin()
|
||||
or ((organization_id in (select public.fn_user_org_ids()))
|
||||
and public.fn_role_at_least(organization_id, 'manager'))
|
||||
);
|
||||
|
||||
-- conversations: viewer read-only (spec 13 §4 nota 1). SELECT continua
|
||||
-- org-flat com a MESMA expressão da policy ALL antiga; write vira agent+.
|
||||
drop policy if exists "conversations_tenant_isolation_all" on public.conversations;
|
||||
drop policy if exists "conversations_select" on public.conversations;
|
||||
drop policy if exists "conversations_agent_write" on public.conversations;
|
||||
|
||||
create policy "conversations_select" on public.conversations
|
||||
for select using (
|
||||
(organization_id in (select public.fn_user_org_ids()))
|
||||
or public.fn_is_platform_admin()
|
||||
);
|
||||
|
||||
create policy "conversations_agent_write" on public.conversations
|
||||
using (
|
||||
public.fn_is_platform_admin()
|
||||
or ((organization_id in (select public.fn_user_org_ids()))
|
||||
and public.fn_role_at_least(organization_id, 'agent'))
|
||||
)
|
||||
with check (
|
||||
public.fn_is_platform_admin()
|
||||
or ((organization_id in (select public.fn_user_org_ids()))
|
||||
and public.fn_role_at_least(organization_id, 'agent'))
|
||||
);
|
||||
@@ -33,6 +33,7 @@ Migrations applied to Supabase project `rrydmwnporysaiysiztn` (sa-east-1, Postgr
|
||||
| `20260506100000` | `0025_fix_publish_fn_and_realtime_publication` | Forward-fix: qualifies column refs in fn_publish_ai_agent_version to resolve `agent_id` ambiguity against RETURNS TABLE output params; adds ai_agent_runs/ai_agents/ai_knowledge_sources to the supabase_realtime publication. |
|
||||
| `20260706200000` | `0026_fix_publish_fn_status_case` | Forward-fix: fn_publish_ai_agent_version compared channel_sessions.status against lowercase `'working'`, but the canonical value (channel_sessions_status_check, written by the WAHA webhook handler) is uppercase `'WORKING'`. Publish always raised `channel_session_offline`, even for a genuinely connected session. Bug present since 0024, carried forward unchanged by 0025. |
|
||||
| `20260706210000` | `0027_whatsapp_conversation_unification` | Bugfix de governança de conversas WhatsApp. Causa-raiz: contatos @lid sem unique key + resolução check-then-act, e o WAHA emitindo `message`+`message.any` por mensagem → 1 pessoa virava N contatos/conversas (medido: 1 lid = 12 contatos). Adiciona coluna gerada `contacts.wa_identity` (`phone:+E164`/`lid:<digits>`), faz merge idempotente do histórico duplicado repontando todas as FKs (usa `is_merged_into` como mapa, sem temp tables → portável em psql), cria `uniq_contacts_org_wa_identity` + `uniq_conversations_1to1_per_contact_session` (a antiga unique incluía group_chat_id NULL, que no Postgres não protege 1:1), e as funções de upsert atômico `fn_upsert_wa_contact`/`fn_upsert_wa_conversation`/`fn_mark_conversation_message` que a app passa a usar (lib/waha/ingest.ts) no lugar do check-then-act. Também corrige mapeamento de `type` WAHA→CRM (`chat`→`text` etc.) que fazia mensagens reais violarem messages_type_check e sumirem. |
|
||||
| `20260716120000` | `0030_config_rls_role_policies` | G2-03 (gov-loop): RLS por role nas tabelas de config, fechando no banco o que a G2-01 fechou na API (spec 13 §4; auditoria em §4.1). crm_pipelines/crm_stages: policy ALL org-flat vira SELECT org + WRITE manager+ (`fn_role_at_least`, padrão de api_tokens/merge_queue) — agent deixa de escrever config de pipeline. conversations: WRITE vira agent+ (viewer é read-only); SELECT permanece org-flat com a mesma expressão (escopo own/unassigned é G4-01). NNNN pula 0028/0029 (ocupados nas branches vendaval/F2-*). Sem mudança de contrato — database.types.ts intocado. |
|
||||
|
||||
## Reproducibility
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
GOV_AGENT_A,
|
||||
GOV_CONV_UNASSIGNED,
|
||||
GOV_MANAGER,
|
||||
GOV_PIPELINE,
|
||||
GOV_STAGE,
|
||||
GOV_VIEWER,
|
||||
countAs,
|
||||
seedGov,
|
||||
writeCountAs,
|
||||
} from "./gov-helpers";
|
||||
|
||||
/**
|
||||
* Eixo 1 — RBAC em tabelas de config (G2-03, migration 0030).
|
||||
* spec 13 §4.1: crm_pipelines/crm_stages write manager+; conversations write
|
||||
* agent+ (viewer read-only). SELECT permanece org-flat — os controles de
|
||||
* leitura abaixo garantem que a migration NÃO estreitou visibilidade
|
||||
* (escopo own/unassigned é G4-01, fora daqui).
|
||||
*/
|
||||
|
||||
beforeAll(() => {
|
||||
seedGov();
|
||||
});
|
||||
|
||||
describe("eixo 1 — RBAC de escrita em config (migration 0030)", () => {
|
||||
it("manager ESCREVE config de pipeline (spec 13 §4: manager+)", () => {
|
||||
const updated = writeCountAs(
|
||||
GOV_MANAGER,
|
||||
`update public.crm_pipelines set name = name where id = '${GOV_PIPELINE}'`,
|
||||
);
|
||||
expect(updated).toBe(1);
|
||||
});
|
||||
|
||||
it("agent NÃO escreve em crm_stages (config de pipeline, spec 13 §4 nota 4)", () => {
|
||||
const updated = writeCountAs(
|
||||
GOV_AGENT_A,
|
||||
`update public.crm_stages set name = name where id = '${GOV_STAGE}'`,
|
||||
);
|
||||
expect(updated).toBe(0);
|
||||
});
|
||||
|
||||
it("manager ESCREVE em crm_stages", () => {
|
||||
const updated = writeCountAs(
|
||||
GOV_MANAGER,
|
||||
`update public.crm_stages set name = name where id = '${GOV_STAGE}'`,
|
||||
);
|
||||
expect(updated).toBe(1);
|
||||
});
|
||||
|
||||
it("agent continua escrevendo em conversations (controle positivo do write agent+)", () => {
|
||||
const updated = writeCountAs(
|
||||
GOV_AGENT_A,
|
||||
`update public.conversations set status = status where id = '${GOV_CONV_UNASSIGNED}'`,
|
||||
);
|
||||
expect(updated).toBe(1);
|
||||
});
|
||||
|
||||
it("viewer NÃO faz UPDATE em conversations (complementa o probe de INSERT do gov-1)", () => {
|
||||
const updated = writeCountAs(
|
||||
GOV_VIEWER,
|
||||
`update public.conversations set status = status where id = '${GOV_CONV_UNASSIGNED}'`,
|
||||
);
|
||||
expect(updated).toBe(0);
|
||||
});
|
||||
|
||||
it("SELECT continua org-flat: viewer e agent leem pipelines, stages e conversations", () => {
|
||||
for (const userId of [GOV_VIEWER, GOV_AGENT_A]) {
|
||||
expect(
|
||||
countAs(userId, `select count(*) from public.crm_pipelines where id = '${GOV_PIPELINE}';`),
|
||||
).toBe(1);
|
||||
expect(
|
||||
countAs(userId, `select count(*) from public.crm_stages where id = '${GOV_STAGE}';`),
|
||||
).toBe(1);
|
||||
expect(
|
||||
countAs(
|
||||
userId,
|
||||
`select count(*) from public.conversations where id = '${GOV_CONV_UNASSIGNED}';`,
|
||||
),
|
||||
).toBe(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -88,10 +88,10 @@ describe("eixo 1 — RBAC", () => {
|
||||
expect(readFileSync(route, "utf8")).toContain("export async function PATCH");
|
||||
});
|
||||
|
||||
// GAP(G2): spec 13 §4 — pipelines (config) é manager+:write, agent=none.
|
||||
// Hoje a policy tenant_isolation_crm_pipelines_all é org-flat: qualquer
|
||||
// membro (incl. agent) escreve config de pipeline.
|
||||
it.fails("agent NÃO escreve config de pipeline (spec 13 §4: manager+)", () => {
|
||||
// Corrigido (G2-03, migration 0030): crm_pipelines_manager_write aplica
|
||||
// fn_role_at_least(org, 'manager') — spec 13 §4: pipelines (config) é
|
||||
// manager+:write, agent=none.
|
||||
it("agent NÃO escreve config de pipeline (spec 13 §4: manager+)", () => {
|
||||
const updated = writeCountAs(
|
||||
GOV_AGENT_A,
|
||||
`update public.crm_pipelines set name = name where id = '${GOV_PIPELINE}'`,
|
||||
@@ -99,9 +99,9 @@ describe("eixo 1 — RBAC", () => {
|
||||
expect(updated).toBe(0);
|
||||
});
|
||||
|
||||
// GAP(G2): spec 13 §4 — viewer é read-only em conversations. Hoje a policy
|
||||
// org-flat (WITH CHECK por org) deixa o viewer inserir/escrever.
|
||||
it.fails("viewer NÃO escreve em conversations (spec 13 §4: viewer é read-only)", () => {
|
||||
// Corrigido (G2-03, migration 0030): conversations_agent_write aplica
|
||||
// fn_role_at_least(org, 'agent') — spec 13 §4: viewer é read-only.
|
||||
it("viewer NÃO escreve em conversations (spec 13 §4: viewer é read-only)", () => {
|
||||
const inserted = writeCountAs(
|
||||
GOV_VIEWER,
|
||||
`insert into public.conversations (id, organization_id, contact_id, channel_session_id, status)
|
||||
|
||||
Reference in New Issue
Block a user