Row Level Security (RLS) Policies
Sport Tech Club - Isolamento Multi-tenant com PostgreSQL RLS
Visão Geral
Este documento define as políticas de Row Level Security (RLS) para garantir isolamento completo de dados entre tenants na plataforma Sport Tech Club.
1. Arquitetura de Segurança
1.1 Modelo de Multi-tenancy
┌─────────────────────────────────────────────────────────────────┐
│ Application Layer │
├─────────────────────────────────────────────────────────────────┤
│ Connection Pool (PgBouncer) │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Schema: │ │ Schema: │ │ Schema: │ │
│ │ identity │ │ arena │ │ booking │ │
│ │ │ │ │ │ │ │
│ │ - tenants │ │ - arenas │ │ - bookings │ │
│ │ - users │ │ - courts │ │ - payments │ │
│ │ - roles │ │ - products │ │ - check_ins │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ RLS Policies (tenant_id) │
├─────────────────────────────────────────────────────────────────┤
│ PostgreSQL 15+ │
└─────────────────────────────────────────────────────────────────┘1.2 Variáveis de Sessão
sql
-- Variáveis de contexto para RLS
-- Definidas no início de cada transação
-- Tenant ID do usuário autenticado
SET LOCAL app.current_tenant_id = 'uuid-do-tenant';
-- User ID do usuário autenticado
SET LOCAL app.current_user_id = 'uuid-do-usuario';
-- Roles do usuário (JSON array)
SET LOCAL app.current_user_roles = '["ARENA_OWNER", "PLAYER"]';
-- Arena ID atual (para operações com escopo de arena)
SET LOCAL app.current_arena_id = 'uuid-da-arena';2. Funções Auxiliares
2.1 Funções de Contexto
sql
-- Obtém o tenant_id da sessão atual
CREATE OR REPLACE FUNCTION current_tenant_id()
RETURNS UUID AS $$
BEGIN
RETURN NULLIF(current_setting('app.current_tenant_id', true), '')::UUID;
EXCEPTION
WHEN OTHERS THEN
RETURN NULL;
END;
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
-- Obtém o user_id da sessão atual
CREATE OR REPLACE FUNCTION current_user_id()
RETURNS UUID AS $$
BEGIN
RETURN NULLIF(current_setting('app.current_user_id', true), '')::UUID;
EXCEPTION
WHEN OTHERS THEN
RETURN NULL;
END;
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
-- Obtém as roles do usuário atual
CREATE OR REPLACE FUNCTION current_user_roles()
RETURNS TEXT[] AS $$
DECLARE
roles_json TEXT;
BEGIN
roles_json := current_setting('app.current_user_roles', true);
IF roles_json IS NULL OR roles_json = '' THEN
RETURN ARRAY[]::TEXT[];
END IF;
RETURN ARRAY(SELECT jsonb_array_elements_text(roles_json::JSONB));
EXCEPTION
WHEN OTHERS THEN
RETURN ARRAY[]::TEXT[];
END;
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
-- Obtém o arena_id da sessão atual
CREATE OR REPLACE FUNCTION current_arena_id()
RETURNS UUID AS $$
BEGIN
RETURN NULLIF(current_setting('app.current_arena_id', true), '')::UUID;
EXCEPTION
WHEN OTHERS THEN
RETURN NULL;
END;
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
-- Verifica se usuário tem uma role específica
CREATE OR REPLACE FUNCTION has_role(role_name TEXT)
RETURNS BOOLEAN AS $$
BEGIN
RETURN role_name = ANY(current_user_roles());
END;
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
-- Verifica se é super admin (acesso irrestrito)
CREATE OR REPLACE FUNCTION is_super_admin()
RETURNS BOOLEAN AS $$
BEGIN
RETURN has_role('SUPER_ADMIN');
END;
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;2.2 Funções de Verificação
sql
-- Verifica se usuário pertence ao tenant
CREATE OR REPLACE FUNCTION user_belongs_to_tenant(check_tenant_id UUID)
RETURNS BOOLEAN AS $$
BEGIN
-- Super admin tem acesso a todos os tenants
IF is_super_admin() THEN
RETURN TRUE;
END IF;
RETURN check_tenant_id = current_tenant_id();
END;
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
-- Verifica se usuário tem acesso à arena
CREATE OR REPLACE FUNCTION user_has_arena_access(check_arena_id UUID)
RETURNS BOOLEAN AS $$
DECLARE
arena_tenant_id UUID;
BEGIN
-- Super admin tem acesso a todas as arenas
IF is_super_admin() THEN
RETURN TRUE;
END IF;
-- Busca o tenant da arena
SELECT tenant_id INTO arena_tenant_id
FROM arena.arenas
WHERE id = check_arena_id;
-- Verifica se o tenant da arena é o mesmo do usuário
RETURN arena_tenant_id = current_tenant_id();
END;
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
-- Verifica se usuário é dono do recurso
CREATE OR REPLACE FUNCTION is_resource_owner(resource_user_id UUID)
RETURNS BOOLEAN AS $$
BEGIN
RETURN resource_user_id = current_user_id();
END;
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;3. Políticas por Tabela
3.1 Schema: Identity
Tabela: tenants
sql
-- Habilita RLS
ALTER TABLE identity.tenants ENABLE ROW LEVEL SECURITY;
-- Força RLS mesmo para o owner da tabela
ALTER TABLE identity.tenants FORCE ROW LEVEL SECURITY;
-- SELECT: Usuário só vê seu próprio tenant (ou todos se super admin)
CREATE POLICY tenants_select_policy ON identity.tenants
FOR SELECT
USING (
is_super_admin() OR id = current_tenant_id()
);
-- INSERT: Apenas super admin pode criar tenants
CREATE POLICY tenants_insert_policy ON identity.tenants
FOR INSERT
WITH CHECK (is_super_admin());
-- UPDATE: Tenant owner ou super admin
CREATE POLICY tenants_update_policy ON identity.tenants
FOR UPDATE
USING (
is_super_admin() OR (
id = current_tenant_id() AND has_role('ARENA_OWNER')
)
);
-- DELETE: Apenas super admin (soft delete na prática)
CREATE POLICY tenants_delete_policy ON identity.tenants
FOR DELETE
USING (is_super_admin());Tabela: users
sql
ALTER TABLE identity.users ENABLE ROW LEVEL SECURITY;
ALTER TABLE identity.users FORCE ROW LEVEL SECURITY;
-- SELECT: Usuários do mesmo tenant ou o próprio usuário
CREATE POLICY users_select_policy ON identity.users
FOR SELECT
USING (
is_super_admin()
OR tenant_id = current_tenant_id()
OR id = current_user_id()
);
-- INSERT: Arena owner/manager do tenant ou super admin
CREATE POLICY users_insert_policy ON identity.users
FOR INSERT
WITH CHECK (
is_super_admin()
OR (
tenant_id = current_tenant_id()
AND (has_role('ARENA_OWNER') OR has_role('ARENA_MANAGER'))
)
);
-- UPDATE: O próprio usuário ou admin do tenant
CREATE POLICY users_update_policy ON identity.users
FOR UPDATE
USING (
is_super_admin()
OR id = current_user_id()
OR (
tenant_id = current_tenant_id()
AND (has_role('ARENA_OWNER') OR has_role('ARENA_MANAGER'))
)
);
-- DELETE: Admin do tenant ou super admin
CREATE POLICY users_delete_policy ON identity.users
FOR DELETE
USING (
is_super_admin()
OR (
tenant_id = current_tenant_id()
AND has_role('ARENA_OWNER')
AND id != current_user_id() -- Não pode deletar a si mesmo
)
);Tabela: user_roles
sql
ALTER TABLE identity.user_roles ENABLE ROW LEVEL SECURITY;
ALTER TABLE identity.user_roles FORCE ROW LEVEL SECURITY;
-- SELECT: Ver roles de usuários do mesmo tenant
CREATE POLICY user_roles_select_policy ON identity.user_roles
FOR SELECT
USING (
is_super_admin()
OR EXISTS (
SELECT 1 FROM identity.users u
WHERE u.id = user_roles.user_id
AND u.tenant_id = current_tenant_id()
)
);
-- INSERT: Arena owner pode atribuir roles (exceto SUPER_ADMIN)
CREATE POLICY user_roles_insert_policy ON identity.user_roles
FOR INSERT
WITH CHECK (
is_super_admin()
OR (
has_role('ARENA_OWNER')
AND role != 'SUPER_ADMIN'
AND EXISTS (
SELECT 1 FROM identity.users u
WHERE u.id = user_roles.user_id
AND u.tenant_id = current_tenant_id()
)
)
);
-- DELETE: Arena owner pode remover roles
CREATE POLICY user_roles_delete_policy ON identity.user_roles
FOR DELETE
USING (
is_super_admin()
OR (
has_role('ARENA_OWNER')
AND role != 'SUPER_ADMIN'
AND EXISTS (
SELECT 1 FROM identity.users u
WHERE u.id = user_roles.user_id
AND u.tenant_id = current_tenant_id()
)
)
);3.2 Schema: Arena
Tabela: arenas
sql
ALTER TABLE arena.arenas ENABLE ROW LEVEL SECURITY;
ALTER TABLE arena.arenas FORCE ROW LEVEL SECURITY;
-- SELECT: Arenas do tenant (públicas para busca) ou super admin
CREATE POLICY arenas_select_policy ON arena.arenas
FOR SELECT
USING (
is_super_admin()
OR tenant_id = current_tenant_id()
OR status = 'ACTIVE' -- Arenas ativas são públicas para busca
);
-- INSERT: Arena owner do tenant
CREATE POLICY arenas_insert_policy ON arena.arenas
FOR INSERT
WITH CHECK (
is_super_admin()
OR (
tenant_id = current_tenant_id()
AND has_role('ARENA_OWNER')
)
);
-- UPDATE: Arena owner/manager do tenant
CREATE POLICY arenas_update_policy ON arena.arenas
FOR UPDATE
USING (
is_super_admin()
OR (
tenant_id = current_tenant_id()
AND (has_role('ARENA_OWNER') OR has_role('ARENA_MANAGER'))
)
);
-- DELETE: Arena owner do tenant
CREATE POLICY arenas_delete_policy ON arena.arenas
FOR DELETE
USING (
is_super_admin()
OR (
tenant_id = current_tenant_id()
AND has_role('ARENA_OWNER')
)
);Tabela: courts
sql
ALTER TABLE arena.courts ENABLE ROW LEVEL SECURITY;
ALTER TABLE arena.courts FORCE ROW LEVEL SECURITY;
-- SELECT: Quadras de arenas acessíveis
CREATE POLICY courts_select_policy ON arena.courts
FOR SELECT
USING (
is_super_admin()
OR EXISTS (
SELECT 1 FROM arena.arenas a
WHERE a.id = courts.arena_id
AND (a.tenant_id = current_tenant_id() OR a.status = 'ACTIVE')
)
);
-- INSERT: Manager da arena
CREATE POLICY courts_insert_policy ON arena.courts
FOR INSERT
WITH CHECK (
is_super_admin()
OR (
user_has_arena_access(arena_id)
AND (has_role('ARENA_OWNER') OR has_role('ARENA_MANAGER'))
)
);
-- UPDATE: Manager da arena
CREATE POLICY courts_update_policy ON arena.courts
FOR UPDATE
USING (
is_super_admin()
OR (
user_has_arena_access(arena_id)
AND (has_role('ARENA_OWNER') OR has_role('ARENA_MANAGER'))
)
);
-- DELETE: Owner da arena
CREATE POLICY courts_delete_policy ON arena.courts
FOR DELETE
USING (
is_super_admin()
OR (
user_has_arena_access(arena_id)
AND has_role('ARENA_OWNER')
)
);Tabela: pricing_rules
sql
ALTER TABLE arena.pricing_rules ENABLE ROW LEVEL SECURITY;
ALTER TABLE arena.pricing_rules FORCE ROW LEVEL SECURITY;
-- SELECT: Regras de preço das arenas acessíveis
CREATE POLICY pricing_rules_select_policy ON arena.pricing_rules
FOR SELECT
USING (
is_super_admin()
OR user_has_arena_access(arena_id)
);
-- INSERT/UPDATE/DELETE: Manager da arena
CREATE POLICY pricing_rules_modify_policy ON arena.pricing_rules
FOR ALL
USING (
is_super_admin()
OR (
user_has_arena_access(arena_id)
AND (has_role('ARENA_OWNER') OR has_role('ARENA_MANAGER'))
)
);3.3 Schema: Booking
Tabela: bookings
sql
ALTER TABLE booking.bookings ENABLE ROW LEVEL SECURITY;
ALTER TABLE booking.bookings FORCE ROW LEVEL SECURITY;
-- SELECT: Próprias reservas ou reservas da arena (para staff)
CREATE POLICY bookings_select_policy ON booking.bookings
FOR SELECT
USING (
is_super_admin()
-- Próprias reservas
OR user_id = current_user_id()
-- Staff da arena vê todas as reservas
OR (
user_has_arena_access(arena_id)
AND (
has_role('ARENA_OWNER')
OR has_role('ARENA_MANAGER')
OR has_role('RECEPTIONIST')
)
)
);
-- INSERT: Usuário autenticado pode criar reservas
CREATE POLICY bookings_insert_policy ON booking.bookings
FOR INSERT
WITH CHECK (
is_super_admin()
OR (
-- Deve ser para uma arena acessível
EXISTS (
SELECT 1 FROM arena.arenas a
WHERE a.id = bookings.arena_id
AND a.status = 'ACTIVE'
)
-- E o usuário criador deve ser o atual
AND created_by_id = current_user_id()
)
);
-- UPDATE: Própria reserva ou staff da arena
CREATE POLICY bookings_update_policy ON booking.bookings
FOR UPDATE
USING (
is_super_admin()
-- Própria reserva (apenas antes do horário)
OR (
user_id = current_user_id()
AND start_time > NOW()
AND status NOT IN ('COMPLETED', 'CANCELLED')
)
-- Staff da arena
OR (
user_has_arena_access(arena_id)
AND (
has_role('ARENA_OWNER')
OR has_role('ARENA_MANAGER')
OR has_role('RECEPTIONIST')
)
)
);
-- DELETE: Apenas soft delete via UPDATE (status = CANCELLED)
-- Sem política de DELETE realTabela: payments
sql
ALTER TABLE booking.payments ENABLE ROW LEVEL SECURITY;
ALTER TABLE booking.payments FORCE ROW LEVEL SECURITY;
-- SELECT: Ver pagamentos de reservas acessíveis
CREATE POLICY payments_select_policy ON booking.payments
FOR SELECT
USING (
is_super_admin()
OR EXISTS (
SELECT 1 FROM booking.bookings b
WHERE b.id = payments.booking_id
AND (
b.user_id = current_user_id()
OR (
user_has_arena_access(b.arena_id)
AND (has_role('ARENA_OWNER') OR has_role('ARENA_MANAGER'))
)
)
)
);
-- INSERT: Sistema ou manager da arena
CREATE POLICY payments_insert_policy ON booking.payments
FOR INSERT
WITH CHECK (
is_super_admin()
OR EXISTS (
SELECT 1 FROM booking.bookings b
WHERE b.id = payments.booking_id
AND user_has_arena_access(b.arena_id)
AND (has_role('ARENA_OWNER') OR has_role('ARENA_MANAGER'))
)
);
-- UPDATE: Apenas sistema (gateway callbacks)
CREATE POLICY payments_update_policy ON booking.payments
FOR UPDATE
USING (is_super_admin());3.4 Schema: Public
Tabela: audit_logs
sql
ALTER TABLE public.audit_logs ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.audit_logs FORCE ROW LEVEL SECURITY;
-- SELECT: Ver logs do próprio tenant ou super admin
CREATE POLICY audit_logs_select_policy ON public.audit_logs
FOR SELECT
USING (
is_super_admin()
OR tenant_id = current_tenant_id()
);
-- INSERT: Sistema pode inserir logs
CREATE POLICY audit_logs_insert_policy ON public.audit_logs
FOR INSERT
WITH CHECK (
is_super_admin()
OR tenant_id = current_tenant_id()
);
-- UPDATE/DELETE: Ninguém pode modificar audit logs
-- (Sem políticas = bloqueia por padrão)4. Bypass para Serviços
4.1 Role de Serviço
sql
-- Cria role para serviços internos (workers, migrations)
CREATE ROLE app_service_role NOLOGIN;
-- Concede permissões completas
GRANT ALL ON ALL TABLES IN SCHEMA identity TO app_service_role;
GRANT ALL ON ALL TABLES IN SCHEMA arena TO app_service_role;
GRANT ALL ON ALL TABLES IN SCHEMA booking TO app_service_role;
GRANT ALL ON ALL TABLES IN SCHEMA public TO app_service_role;
-- Bypass RLS para serviços
ALTER ROLE app_service_role BYPASSRLS;
-- Cria usuário de serviço
CREATE USER app_worker WITH PASSWORD 'secure-password';
GRANT app_service_role TO app_worker;4.2 Função de Contexto para Serviços
sql
-- Função para executar como outro tenant (apenas serviços)
CREATE OR REPLACE FUNCTION execute_as_tenant(
p_tenant_id UUID,
p_query TEXT
)
RETURNS VOID AS $$
BEGIN
-- Apenas app_service_role pode usar esta função
IF NOT pg_has_role(current_user, 'app_service_role', 'MEMBER') THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- Define o contexto do tenant
PERFORM set_config('app.current_tenant_id', p_tenant_id::TEXT, true);
-- Executa a query
EXECUTE p_query;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;5. Middleware de Aplicação
5.1 NestJS Interceptor
typescript
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { PrismaService } from './prisma.service';
@Injectable()
export class TenantContextInterceptor implements NestInterceptor {
constructor(private prisma: PrismaService) {}
async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any>> {
const request = context.switchToHttp().getRequest();
const user = request.user;
if (user) {
// Define variáveis de sessão no PostgreSQL
await this.prisma.$executeRawUnsafe(`
SET LOCAL app.current_tenant_id = '${user.tenantId}';
SET LOCAL app.current_user_id = '${user.id}';
SET LOCAL app.current_user_roles = '${JSON.stringify(user.roles)}';
`);
}
return next.handle();
}
}5.2 Prisma Middleware
typescript
import { Prisma } from '@prisma/client';
export function tenantMiddleware(): Prisma.Middleware {
return async (params, next) => {
// Adiciona tenant_id automaticamente em creates
if (params.action === 'create' || params.action === 'createMany') {
const tenantId = getCurrentTenantId();
if (tenantId && hasTenantIdField(params.model)) {
if (params.action === 'create') {
params.args.data.tenantId = tenantId;
} else {
params.args.data = params.args.data.map((d: any) => ({
...d,
tenantId,
}));
}
}
}
// Adiciona filtro de tenant automaticamente em reads
if (['findMany', 'findFirst', 'findUnique', 'count'].includes(params.action)) {
const tenantId = getCurrentTenantId();
if (tenantId && hasTenantIdField(params.model)) {
params.args.where = {
...params.args.where,
tenantId,
};
}
}
return next(params);
};
}6. Testes de Segurança
6.1 Testes de Isolamento
sql
-- Teste: Usuário do Tenant A não pode ver dados do Tenant B
DO $$
DECLARE
tenant_a_id UUID := 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
tenant_b_id UUID := 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb';
count_result INT;
BEGIN
-- Setup: Simula usuário do Tenant A
PERFORM set_config('app.current_tenant_id', tenant_a_id::TEXT, true);
PERFORM set_config('app.current_user_roles', '["ARENA_OWNER"]', true);
-- Tenta contar arenas do Tenant B
SELECT COUNT(*) INTO count_result
FROM arena.arenas
WHERE tenant_id = tenant_b_id;
-- Deve retornar 0 (RLS bloqueia)
IF count_result > 0 THEN
RAISE EXCEPTION 'SECURITY VIOLATION: Tenant A can see Tenant B data!';
END IF;
RAISE NOTICE 'TEST PASSED: Tenant isolation working correctly';
END $$;6.2 Testes de Roles
sql
-- Teste: Receptionist não pode deletar quadras
DO $$
DECLARE
test_court_id UUID := 'cccccccc-cccc-cccc-cccc-cccccccccccc';
BEGIN
-- Setup: Simula receptionist
PERFORM set_config('app.current_user_roles', '["RECEPTIONIST"]', true);
-- Tenta deletar quadra
BEGIN
DELETE FROM arena.courts WHERE id = test_court_id;
RAISE EXCEPTION 'SECURITY VIOLATION: Receptionist could delete court!';
EXCEPTION
WHEN insufficient_privilege THEN
RAISE NOTICE 'TEST PASSED: Receptionist correctly blocked from delete';
END;
END $$;7. Monitoramento
7.1 Query para Auditoria de RLS
sql
-- Verifica políticas ativas
SELECT
schemaname,
tablename,
policyname,
permissive,
roles,
cmd,
qual,
with_check
FROM pg_policies
WHERE schemaname IN ('identity', 'arena', 'booking', 'public')
ORDER BY schemaname, tablename, policyname;7.2 Alertas de Violação
sql
-- Log de tentativas de acesso bloqueadas
CREATE OR REPLACE FUNCTION log_rls_violation()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.security_logs (
event_type,
user_id,
tenant_id,
table_name,
attempted_action,
details,
created_at
) VALUES (
'RLS_VIOLATION',
current_user_id(),
current_tenant_id(),
TG_TABLE_NAME,
TG_OP,
jsonb_build_object(
'row_id', COALESCE(NEW.id, OLD.id)
),
NOW()
);
RETURN NULL; -- Bloqueia a operação
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;8. Manutenção
8.1 Script de Verificação
bash
#!/bin/bash
# verify-rls-policies.sh
echo "Verificando políticas RLS..."
psql -d sporttechclub -c "
SELECT
schemaname || '.' || tablename as table,
CASE WHEN rowsecurity THEN 'ENABLED' ELSE 'DISABLED' END as rls_status,
COUNT(policyname) as policy_count
FROM pg_tables t
LEFT JOIN pg_policies p ON t.tablename = p.tablename
WHERE t.schemaname IN ('identity', 'arena', 'booking')
GROUP BY schemaname, tablename, rowsecurity
ORDER BY schemaname, tablename;
"8.2 Rollback de Políticas
sql
-- Em caso de emergência: desabilita RLS temporariamente
-- ATENÇÃO: Use apenas em situações críticas!
-- Desabilita RLS em todas as tabelas
DO $$
DECLARE
r RECORD;
BEGIN
FOR r IN SELECT schemaname, tablename
FROM pg_tables
WHERE schemaname IN ('identity', 'arena', 'booking')
LOOP
EXECUTE format('ALTER TABLE %I.%I DISABLE ROW LEVEL SECURITY',
r.schemaname, r.tablename);
END LOOP;
END $$;
-- Para reabilitar:
DO $$
DECLARE
r RECORD;
BEGIN
FOR r IN SELECT schemaname, tablename
FROM pg_tables
WHERE schemaname IN ('identity', 'arena', 'booking')
LOOP
EXECUTE format('ALTER TABLE %I.%I ENABLE ROW LEVEL SECURITY',
r.schemaname, r.tablename);
END LOOP;
END $$;9. Checklist de Implementação
- [ ] Funções de contexto criadas
- [ ] RLS habilitado em todas as tabelas
- [ ] Políticas SELECT implementadas
- [ ] Políticas INSERT implementadas
- [ ] Políticas UPDATE implementadas
- [ ] Políticas DELETE implementadas
- [ ] Role de serviço configurada
- [ ] Middleware de aplicação implementado
- [ ] Testes de isolamento executados
- [ ] Testes de roles executados
- [ ] Monitoramento configurado
- [ ] Documentação atualizada