PC SOFT

PROFESSIONAL NEWSGROUPS
WINDEVWEBDEV and WINDEV Mobile

Home → WINDEV 25 → SECWINDEV: É um sistema de segurança automática similar ao GROUPWARE
SECWINDEV: É um sistema de segurança automática similar ao GROUPWARE
Started by Boller, Sep., 08 2025 2:20 AM - 24 replies
Registered member
4,618 messages
Posted on September, 08 2025 - 2:20 AM
Boa noite

Vou descrever o SECWINDEV:

Analisando os documentos fornecidos, posso ver que se trata do **SecWinDev v6** - um sistema avançado de segurança e controle de acesso para aplicações WinDev/WebDev. Aqui está o que este sistema faz:

## **Funcionalidades Principais:**

### 1. **Policy-as-Code (Políticas como Código)**

- **Versionamento de Permissões**: Cria snapshots JSON de todas as permissões RBAC atuais
- **Workflow de Aprovação**: Sistema de aprovação dupla (two-person rule) para mudanças críticas
- **Staging de Mudanças**: Permite criar, aprovar e aplicar mudanças de permissões de forma controlada
- **Rastreabilidade**: Histórico completo de todas as alterações de políticas

### 2. **Sistema RBAC/ABAC Avançado**

- **RBAC (Role-Based Access Control)**: Controle baseado em perfis e usuários
- **ABAC (Attribute-Based Access Control)**: Controle baseado em atributos e regras dinâmicas
- **Permissões Granulares**: Controla acesso a janelas, relatórios, objetos, menus e itens de menu
- **Estados de Permissão**: `allowed`, `forbidden`, `grayed`, `invisible`, `default`

### 3. **DLP (Data Loss Prevention) / Export Control**

- **Classificação de Dados**: Sistema de tags para classificar sensibilidade (Público, Interno, Confidencial, Restrito)
- **Controle de Exportação**: Políticas por formato (PDF, CSV, XLSX) e volume de dados
- **Quotas Diárias**: Limites de exportação por usuário
- **Watermark**: Aplicação automática de marcas d’água em exports sensíveis
- **Auditoria de Exports**: Log completo de todas as tentativas de exportação

### 4. **Recursos Avançados de Análise**

- **What-If Analysis**: Simula mudanças de permissões sem aplicar no banco
- **Policy Explain**: Explica detalhadamente por que uma permissão foi concedida/negada
- **Reasoning Engine**: Mostra a lógica por trás das decisões de acesso

### 5. **Dashboard e Monitoramento**

- **Dashboards Grafana**: Visualização em tempo real de:
- Stages pendentes de aprovação
- Atividade de exportações
- Negativas de acesso
- Timeline de mudanças de políticas
- **Alertas Automáticos**:
- Stages pendentes há mais de 24h
- Picos de exportações negadas

### 6. **Integração com WinDev/WebDev**

- **Classe WL Completa**: Métodos prontos para integração
- **Páginas de Exemplo**: Interfaces para simulação e verificação de exports
- **API Unificada**: Funções como `Export_Check()`, `Policy_Simulate()`, etc.

## **Casos de Uso Típicos:**

1. **Governança de TI**: Controle rigoroso de mudanças de permissões com aprovação dupla
1. **Compliance**: Atendimento a regulamentações como LGPD, GDPR, SOX
1. **Segurança Corporativa**: Prevenção de vazamentos de dados sensíveis
1. **Auditoria**: Rastreabilidade completa de acessos e mudanças
1. **Análise de Impacto**: Simulação de mudanças antes da aplicação

Este é um sistema **enterprise-grade** para organizações que precisam de controle de acesso sofisticado e compliance regulatório em suas aplicações WinDev/WebDev.​​​​​​​​​​​​​​​​

//——————-

O fluxo do SecWinDev v6 é um sistema completo de **governança de segurança** que funciona em camadas interconectadas.

Os **pontos-chave** do funcionamento são:

## **🔄 Ciclo de Vida das Políticas**

1. **Stage** → Criação controlada de mudanças
1. **Approve** → Validação dupla obrigatória
1. **Apply** → Implementação segura
1. **Monitor** → Observabilidade contínua

## **🛡️ Proteção em Múltiplas Camadas**

- **RBAC**: Permissões baseadas em roles/perfis
- **ABAC**: Regras dinâmicas contextuais
- **DLP**: Prevenção de vazamentos
- **Audit**: Rastreabilidade completa

## **🎯 Recursos Únicos**

- **What-If**: Testa mudanças sem riscos
- **Explain**: IA que justifica decisões
- **Policy-as-Code**: Versionamento como software
- **Two-Person Rule**: Nunca uma pessoa sozinha faz mudanças críticas

Este sistema é ideal para organizações que precisam de **compliance rigoroso** (bancos, seguradoras, órgãos públicos) e **zero-trust security** em suas aplicações WinDev/WebDev.​​​​​​​​​​​​​​​​

//————

Analisando os documentos fornecidos, posso ver que eles focam principalmente no sistema de **controle de acesso pós-autenticação** (RBAC/ABAC, políticas, DLP), mas não detalham especificamente o **fluxo de login/autenticação**.

No entanto, baseado na estrutura do SecWinDev v6, posso explicar como o login provavelmente funciona e como se integra com o sistema:

## **🔐 Fluxo de Login Inferido**

### 1. **Estrutura Base de Usuários**

```sql
-- Tabela principal (referenciada no código)
secwin_usuarios(
uuid, -- Identificador único
login, -- Username
nome, -- Nome completo
email, -- Email
senha_hash, -- Hash da senha
ativo, -- Status ativo/inativo
created_at, -- Data criação
last_login, -- Último login
perfil_uuid -- Perfil padrão
)
```

### 2. **Processo de Autenticação Típico**

Com base na análise dos documentos, o **login no SecWinDev v6** funciona como uma **camada de autenticação** que alimenta todo o sistema de controle de acesso posterior.

## **🎯 Pontos-Chave do Login:**

### **1. Autenticação Híbrida**

- **Local**: Usuário/senha no banco PostgreSQL
- **SSO**: OIDC/SAML com Azure AD, Google, etc.
- **LDAP/AD**: Integração com Active Directory

### **2. Contexto de Segurança**

Após login bem-sucedido, o sistema carrega:

- **Perfis** do usuário
- **Permissões efetivas** (RBAC)
- **Regras dinâmicas** (ABAC)
- **Políticas de exportação** (DLP)

### **3. Validação Contínua**

A cada ação, o sistema verifica:

- Sessão ainda válida?
- Permissões não foram alteradas?
- Regras contextuais ainda se aplicam?

### **4. Segurança Avançada**

- **MFA** obrigatório por perfil/risco
- **Rate limiting** de tentativas
- **Device fingerprinting**
- **Adaptive authentication**

O login é apenas o **ponto de entrada** - o verdadeiro diferencial está no **controle granular pós-autenticação** que acontece a cada clique, tela aberta, ou tentativa de exportação.

//———-

Mais detalhes:

# SecWinDev v6 - Fluxo de Login e Autenticação

## 1. 🔑 Métodos de Autenticação Suportados

### 1.1 Autenticação Local (Banco de Dados)

```
[Usuário] → Informa login + senha

Sistema verifica:
├── secwin_usuarios.login = input_login
├── secwin_usuarios.ativo = true
├── password_verify(input_senha, senha_hash)
└── Validações de política (se configuradas)

Se válido: Cria sessão + carrega permissões
Se inválido: Log de tentativa + bloqueio (se aplicável)
```

### 1.2 SSO/OIDC (Single Sign-On)

```
[Usuário] → Clica "Login com SSO"

Redirecionamento para IdP (Identity Provider):
├── Azure AD / Entra ID
├── Google Workspace
├── Okta / Auth0
└── ADFS / Keycloak

IdP autentica usuário

Callback com token JWT/SAML

Sistema valida token + sincroniza usuário

Carrega permissões locais (RBAC/ABAC)
```

### 1.3 Integração Active Directory

```
[Usuário] → Login + senha

Sistema tenta:
├── 1º: LDAP bind no AD/LDAP
├── 2º: Sincroniza grupos → perfis SecWinDev
└── 3º: Carrega permissões locais

Híbrido: Autenticação AD + Autorização SecWinDev
```

## 2. 🔄 Fluxo Completo de Login

### 2.1 Pré-Autenticação

```
[Aplicação WinDev] → Inicializa

Verifica sessão existente:
├── Cookie/Token válido?
├── Sessão não expirada?
└── Usuário ainda ativo?

Se válido: Carrega contexto de segurança
Se inválido: Exibe tela de login
```

### 2.2 Processo de Login

```
[Usuário] → Preenche credenciais

Classe WinDev: gSec.Login(usuario, senha)

Validações de segurança:
├── Rate limiting (tentativas por IP/usuário)
├── Captcha (se muitas tentativas)
├── Verificação de força da senha
└── Políticas de bloqueio de conta

Consulta ao banco/IdP:
secwin_authenticate_user(login, senha_hash, ip, device_info)

Se sucesso:
├── Atualiza last_login
├── Cria entrada em secwin_user_sessions
├── Carrega perfis e permissões efetivas
└── Registra evento de login

Se falha:
├── Incrementa contador de tentativas
├── Log de segurança
└── Possível bloqueio temporário
```

### 2.3 Pós-Autenticação (Carregamento de Contexto)

```
Login bem-sucedido

Carrega dados do usuário:
├── secwin_usuarios (dados básicos)
├── secwin_usuarios_x_perfis (perfis do usuário)
├── vw_secwin_permissoes_efetivas (todas as permissões)
└── secwin_user_preferences (preferências)

Inicializa contexto de segurança:
├── UserContext.UUID
├── UserContext.Perfis[]
├── UserContext.Permissions[]
└── UserContext.SessionInfo

Carrega regras ABAC ativas:
├── Regras baseadas em tempo
├── Regras baseadas em localização
├── Regras baseadas em dispositivo
└── Regras customizadas
```

## 3. 🛡️ Segurança da Sessão

### 3.1 Gerenciamento de Sessão

```sql
CREATE TABLE secwin_user_sessions (
id UUID PRIMARY KEY,
usuario_uuid UUID REFERENCES secwin_usuarios(uuid),
ip_address INET,
user_agent TEXT,
device_fingerprint TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
last_activity TIMESTAMPTZ DEFAULT NOW(),
expires_at TIMESTAMPTZ,
is_active BOOLEAN DEFAULT true,
logout_reason TEXT -- 'manual', 'timeout', 'admin_force', 'policy_change'
);
```

### 3.2 Validação Contínua

```
A cada requisição:

Middleware de segurança verifica:
├── Sessão válida e não expirada?
├── IP/Device consistente? (se configurado)
├── Usuário ainda ativo?
├── Permissões não foram revogadas?
└── Políticas ABAC ainda se aplicam?

Se válido: Atualiza last_activity
Se inválido: Force logout + redirect login
```

## 4. 💻 Implementação em WinDev

### 4.1 Classe de Autenticação (WL)

```windev
// Classe clsSecWinDev - Métodos de Login

// Login básico
PROCEDURE Login(sLogin, sPassword)
sUserUUID is string
sResult is string

// Chama stored procedure
sResult = SQLExecFunction("secwin_authenticate_user", sLogin, SHA256(sPassword), NetGetIP(), DeviceInfo())

IF sResult = "success" THEN
// Carrega contexto do usuário
LoadUserContext(sLogin)
RESULT True
ELSE
// Log da tentativa falhada
LogLoginAttempt(sLogin, sResult)
RESULT False
END

// Login SSO
PROCEDURE LoginSSO(sProvider)
// Redireciona para provider OIDC
BrowserOpen(GetOIDCAuthURL(sProvider))
// Aguarda callback com token
// Valida e processa token JWT
```

### 4.2 Tela de Login (WIN_Login)

```windev
// Evento do botão Login
PROCEDURE BTN_Login_Click()

IF gSec.Login(EDT_Usuario, EDT_Senha) THEN
// Login sucesso
Close()
OpenMainWindow()
ELSE
// Login falhou
Error("Credenciais inválidas!")
EDT_Senha = ""
SetFocus(EDT_Usuario)
END

// Botão SSO
PROCEDURE BTN_SSO_Click()
gSec.LoginSSO("azure")
```

## 5. 🔒 Recursos de Segurança Avançados

### 5.1 Multi-Factor Authentication (MFA)

```
Login inicial válido

Sistema verifica se MFA obrigatório:
├── Por usuário (secwin_usuarios.require_mfa)
├── Por perfil (secwin_perfis.require_mfa)
├── Por política temporal
└── Por risco (IP suspeito, device novo)

Se MFA obrigatório:
├── Envia código SMS/Email
├── Gera QR code para app autenticador
├── Push notification (se configurado)
└── Biometria (se disponível)

Validação do segundo fator

Login completado com contexto MFA
```

### 5.2 Adaptive Authentication

```
Sistema analisa o contexto:
├── IP conhecido vs. novo
├── Device conhecido vs. novo
├── Horário usual vs. fora do padrão
├── Localização geográfica
└── Padrões comportamentais

Score de risco calculado:
├── Baixo: Login normal
├── Médio: MFA obrigatório
├── Alto: Captcha + MFA + aprovação admin
└── Crítico: Bloqueio + alerta
```

## 6. 📊 Auditoria de Login

### 6.1 Logs de Autenticação

```sql
CREATE TABLE secwin_auth_events (
id BIGSERIAL PRIMARY KEY,
ts TIMESTAMPTZ DEFAULT NOW(),
event_type TEXT, -- 'login_success', 'login_failed', 'logout', 'session_expired'
usuario_uuid UUID,
login_attempted TEXT,
ip_address INET,
user_agent TEXT,
success BOOLEAN,
failure_reason TEXT,
mfa_used BOOLEAN,
risk_score INTEGER
);
```

### 6.2 Dashboard de Login

```
Métricas monitoradas:
├── Logins por hora/dia
├── Taxa de falhas por usuário
├── Tentativas de IP suspeitos
├── Uso de MFA
├── Sessões ativas
├── Dispositivos por usuário
└── Alertas de segurança
```

## 7. 🔗 Integração com Sistema Principal

### 7.1 Após Login Bem-Sucedido

```
Contexto carregado

Sistema ativa:
├── Verificações RBAC em tempo real
├── Avaliação de regras ABAC
├── Monitoramento de atividade
├── Controles de exportação (DLP)
└── Auditoria de acesso

Usuário trabalha normalmente
Cada ação é validada contra permissões
```

### 7.2 Logout/Session End

```
Logout (manual ou automático)

Sistema executa:
├── Invalida sessão ativa
├── Limpa cache de permissões
├── Registra evento de logout
├── Notifica sistemas integrados
└── Redireciona para login
```

Este fluxo garante **segurança robusta** desde a autenticação até cada ação do usuário, integrando seamlessly com o sistema de controle de acesso avançado do SecWinDev.

//——————-

Após o login pode fazer as configurações para outros usuários se for o admin?

R.:

Sim! O SecWinDev v6 possui um **sistema completo de administração** pós-login. Vou detalhar como funciona a administração de usuários e permissões:​​​​​​​​​​​​​​​​

**Sim, absolutamente!** O SecWinDev v6 possui um **sistema completo de administração** pós-login com diferentes níveis de privilégio.

## **🎯 Principais Capacidades Admin:**

### **👑 Hierarquia de Admins**

- **Super Admin**: Controle total do sistema
- **Security Admin**: Gerencia usuários e permissões
- **Profile Admin**: Apenas perfis e grupos
- **Delegated Admin**: Escopo limitado por departamento

### **🛠️ Funcionalidades Administrativas**

- **Gestão de Usuários**: Criar, editar, ativar/desativar
- **Configuração de Perfis**: Definir permissões granulares
- **Policy-as-Code**: Mudanças com aprovação obrigatória
- **What-If Analysis**: Simular impacto antes de aplicar
- **DLP Configuration**: Políticas de exportação
- **Audit & Monitoring**: Dashboards e logs completos

### **🔐 Controles de Segurança**

- **Two-Person Rule**: Mudanças críticas precisam de 2 aprovações
- **Segregation of Duties**: Nenhum admin faz tudo sozinho
- **Audit Trail**: Todas ações são logadas
- **Real-time Monitoring**: Alertas de atividades suspeitas

### **⚡ Fluxo Típico:**

```
Admin Login → Painel Admin → Criar/Editar Usuário →
Stage (se crítico) → Aprovação → Aplicação → Auditoria
```

**O diferencial** é que mudanças importantes não são aplicadas imediatamente - passam pelo **workflow de aprovação** do Policy-as-Code, garantindo **governança empresarial** e **prevenção de erros**.

Mais detalhes:

# SecWinDev v6 - Administração e Configuração Pós-Login

## 1. 👑 Níveis de Administração

### 1.1 Hierarquia de Permissões Admin

```
Super Admin (Root)
├── Pode tudo, inclusive criar outros admins
├── Acesso total ao Policy-as-Code
└── Pode alterar configurações do sistema

Security Admin
├── Gerencia usuários e permissões
├── Cria/aprova políticas de segurança
└── Monitora logs e auditoria

Profile Admin
├── Gerencia perfis e grupos
├── Define permissões por perfil
└── Não pode alterar admins

Delegated Admin
├── Gerencia apenas usuários específicos
├── Escopo limitado (departamento/projeto)
└── Permissões herdadas do perfil
```

### 1.2 Verificação de Privilégios Admin

```sql
-- Função para verificar se usuário é admin
CREATE OR REPLACE FUNCTION secwin_is_admin(_user_uuid uuid, _admin_type text DEFAULT 'any')
RETURNS boolean AS $$
BEGIN
RETURN EXISTS (
SELECT 1 FROM secwin_usuarios u
JOIN secwin_usuarios_x_perfis up ON u.uuid = up.usuario_uuid
JOIN secwin_perfis p ON up.perfil_uuid = p.uuid
WHERE u.uuid = _user_uuid
AND u.ativo = true
AND p.is_admin = true
AND (_admin_type = 'any' OR p.admin_type = _admin_type)
);
END$$ LANGUAGE plpgsql;
```

## 2. 🛠️ Funcionalidades de Administração

### 2.1 Gestão de Usuários

```
[Admin logado] → Acessa painel administrativo

Operações disponíveis:
├── ➕ Criar novos usuários
├── ✏️ Editar dados de usuários existentes
├── 🔒 Ativar/Desativar usuários
├── 🔑 Reset de senhas
├── 👥 Atribuir/Remover perfis
├── 📱 Configurar MFA
└── 📊 Ver histórico de acesso
```

#### Exemplo de Tela Admin (WinDev)

```windev
// WIN_Admin_Users.wdw
PROCEDURE LoadUsers()
sQuery is string = [
SELECT u.uuid, u.login, u.nome, u.email, u.ativo, u.last_login,
string_agg(p.nome, ', ') as perfis
FROM secwin_usuarios u
LEFT JOIN secwin_usuarios_x_perfis up ON u.uuid = up.usuario_uuid
LEFT JOIN secwin_perfis p ON up.perfil_uuid = p.uuid
WHERE u.deleted_at IS NULL
GROUP BY u.uuid, u.login, u.nome, u.email, u.ativo, u.last_login
ORDER BY u.nome
]

TableDisplay(TABLE_Users, sQuery)

// Botão Criar Usuário
PROCEDURE BTN_CreateUser_Click()
IF gSec.IsAdmin(gSec.CurrentUser.UUID, "user_admin") THEN
WIN_User_Form.OpenAsChild()
ELSE
Error("Acesso negado - Privilégios insuficientes")
END
```

### 2.2 Gestão de Perfis e Permissões

```
[Security Admin] → Gerencia perfis

Operações de perfil:
├── 📝 Criar/Editar perfis
├── 🎯 Definir permissões por recurso:
│ ├── Janelas (allowed/forbidden/grayed/invisible)
│ ├── Relatórios (acesso + export policies)
│ ├── Objetos (CRUD permissions)
│ ├── Menus e itens
│ └── Funcionalidades especiais
├── 👥 Atribuir usuários ao perfil
├── 📋 Copiar perfil existente
└── 🗂️ Organizar hierarquia de perfis
```

#### Tela de Configuração de Perfil

```windev
// WIN_Admin_Profile.wdw
PROCEDURE LoadProfilePermissions(sProfileUUID)

// Carrega permissões atuais do perfil
sJSON is string = gSec.GetProfilePermissions(sProfileUUID)
PermissionTree.LoadFromJSON(sJSON)

// Permite editar permissões por recurso
FOR EACH oWindow OF gSec.GetAllWindows()
nRow = TableAdd(TABLE_WindowPerms)
TABLE_WindowPerms[nRow].Window = oWindow.Name
TABLE_WindowPerms[nRow].Permission = gSec.GetProfileWindowPerm(sProfileUUID, oWindow.UUID)
END

// Salvar mudanças
PROCEDURE BTN_SaveProfile_Click()
// Usa Policy-as-Code para mudanças controladas
sSnapshot is string = gSec.Policy_Snapshot()
// Modifica snapshot com novas permissões
sModifiedSnapshot = ModifyProfilePermissions(sSnapshot, sProfileUUID, GetFormData())
// Cria stage para aprovação
nStageID = gSec.Policy_StageCreate(sModifiedSnapshot, "Alteração perfil " + EDT_ProfileName, "medium", gSec.CurrentUser.UUID)
Info("Mudança criada. ID do Stage: " + nStageID + " - Aguardando aprovação.")
```

### 2.3 Policy-as-Code Administration

```
[Super Admin] → Gerencia mudanças de política

Funcionalidades Policy-as-Code:
├── 📸 Criar snapshot das permissões atuais
├── 📝 Criar stage com mudanças propostas
├── ✅ Aprovar stages (two-person rule)
├── 🚀 Aplicar mudanças aprovadas
├── 🔍 Simular impacto (What-If Analysis)
├── 💡 Explicar decisões de acesso
├── 📊 Monitorar stages pendentes
└── 📈 Histórico de mudanças
```

#### Interface Policy-as-Code

```windev
// WIN_Admin_Policies.wdw

// Lista stages pendentes
PROCEDURE LoadPendingStages()
sQuery is string = [
SELECT id, created_at, created_by, note, risk_level,
approver1_uuid, approver2_uuid, status
FROM vw_secwin_policy_pending
ORDER BY created_at DESC
]
TableDisplay(TABLE_PendingStages, sQuery)

// Aprovar stage
PROCEDURE BTN_ApproveStage_Click()
nStageID = TABLE_PendingStages[TABLE_PendingStages].id
sResult is string = gSec.Policy_Approve(nStageID, gSec.CurrentUser.UUID)

SWITCH sResult
CASE "approved_1of2"
Info("Primeira aprovação registrada. Aguardando segunda aprovação.")
CASE "approved_2of2"
Info("Stage aprovado! Pronto para aplicação.")
BTN_ApplyStage.Visible = True
CASE "cannot_self_approve"
Error("Não é possível aprovar mudança criada por você mesmo.")
OTHER CASE
Error("Erro: " + sResult)
END

// Aplicar stage aprovado
PROCEDURE BTN_ApplyStage_Click()
nStageID = TABLE_PendingStages[TABLE_PendingStages].id
sMode is string = "upsert_only" // ou "upsert_and_delete"

IF YesNo("Aplicar mudanças? Esta ação não pode ser desfeita.") = Yes THEN
sResult is string = gSec.Policy_Apply(nStageID, gSec.CurrentUser.UUID, sMode)
IF sResult = "applied" THEN
Info("Mudanças aplicadas com sucesso!")
LoadPendingStages() // Recarrega lista
ELSE
Error("Erro ao aplicar: " + sResult)
END
END
```

### 2.4 What-If Analysis (Simulação)

```
[Admin] → Testa impacto de mudanças

Interface de simulação:
├── 👤 Selecionar usuário alvo
├── 🎯 Selecionar recurso (janela/relatório/objeto)
├── 📝 Definir mudanças (JSON delta)
├── ▶️ Executar simulação
└── 📋 Ver resultado detalhado
```

```windev
// WIN_Policy_WhatIf.wdw (já documentado)
PROCEDURE BTN_Simular()
userUUID is string = EDT_UserUUID
alvoUUID is string = EDT_TargetUUID
alvoTipo is string = CB_TargetType..Value
delta is string = MLE_Delta

// Simula sem alterar banco
json is string = gSec.Policy_Simulate(userUUID, alvoTipo, alvoUUID, delta)
MLE_Result = json

// Parse do resultado para interface amigável
oResult is JSON = JSONParse(json)
IF oResult.final = True THEN
STC_Result.Color = Green
STC_Result.Caption = "✅ ACESSO PERMITIDO"
ELSE
STC_Result.Color = Red
STC_Result.Caption = "❌ ACESSO NEGADO"
END

STC_Details.Caption = "RBAC: " + oResult.rbac_state + " | ABAC: " + oResult.abac_decision
```

### 2.5 DLP/Export Administration

```
[Security Admin] → Configura políticas de exportação

Configurações DLP:
├── 🏷️ Classificações de dados (Público/Interno/Confidencial/Restrito)
├── 📊 Políticas por formato (PDF/CSV/XLSX)
├── 📈 Quotas por usuário/perfil
├── 🔖 Watermarks obrigatórios
├── 📋 Objetos/relatórios sensíveis
└── 🚨 Alertas de tentativas suspeitas
```

```windev
// WIN_Admin_DLP.wdw
PROCEDURE ConfigureExportPolicy()

// Lista classificações existentes
LoadClassifications()

// Configura política por classificação + formato
FOR EACH sClassification OF arrClassifications
FOR EACH sFormat OF ["PDF","CSV","XLSX"]
// Permite configurar:
// - Permitido/Negado/Com Watermark
// - Quota diária
// - Horários permitidos
// - Aprovação necessária
END
END

// Exemplo de configuração
PROCEDURE SaveExportPolicy()
sSQL is string = [
INSERT INTO secwin_export_policies
(classification, format, action, daily_quota, watermark_required, approval_required)
VALUES ('Confidencial', 'PDF', 'watermark', 10, true, false)
]
SQLExec(sSQL)
```

## 3. 🔐 Controles de Segurança Admin

### 3.1 Segregation of Duties (SoD)

```
Princípio: Nenhum admin pode fazer tudo sozinho

Criação de usuário:
├── Admin A: Cria usuário básico
└── Admin B: Aprova e ativa usuário

Mudança de permissão crítica:
├── Admin A: Cria stage
├── Admin B: Primeira aprovação
└── Admin C: Segunda aprovação + aplicação

Acesso a dados sensíveis:
├── Security Admin: Define política
└── Data Owner: Aprova acesso específico
```

### 3.2 Audit Trail Completo

```sql
-- Todas ações admin são logadas
CREATE TABLE secwin_admin_actions (
id BIGSERIAL PRIMARY KEY,
ts TIMESTAMPTZ DEFAULT NOW(),
admin_uuid UUID REFERENCES secwin_usuarios(uuid),
action_type TEXT, -- 'user_create', 'permission_change', 'policy_apply', etc.
target_uuid UUID, -- Usuário/perfil afetado
old_values JSONB, -- Estado anterior
new_values JSONB, -- Novo estado
ip_address INET,
user_agent TEXT,
notes TEXT
);
```

### 3.3 Dashboard Admin

```
Métricas em tempo real:
├── 👥 Usuários ativos vs. inativos
├── 🔄 Stages pendentes de aprovação
├── 🚨 Tentativas de acesso negadas
├── 📊 Exportações por classificação
├── ⚠️ Alertas de segurança
├── 📈 Atividade de admins
└── 🕐 Últimas mudanças aplicadas
```

## 4. 🎯 Fluxo Típico de Administração

### Cenário: Criar novo usuário com permissões específicas

```
1. [Security Admin] → Login no sistema

2. Acessa WIN_Admin_Users → BTN_CreateUser

3. Preenche dados básicos (WIN_User_Form)

4. Seleciona perfis apropriados

5. Se mudança crítica → Cria stage (Policy-as-Code)

6. [Admin 2] → Aprova stage

7. [Admin 1] → Aplica mudanças

8. Usuário criado + email de boas-vindas

9. Log completo da operação
```

### Cenário: Investigar acesso negado

```
1. [User] → Reporta acesso negado a relatório

2. [Admin] → Usa Policy_Explain para investigar

3. Identifica problema na configuração do perfil

4. Usa What-If para testar correção

5. Cria stage com correção

6. Workflow de aprovação

7. Aplica correção

8. Confirma acesso restaurado
```

## 5. 💡 Recursos Únicos para Admins

- **🎭 Impersonation**: Admin pode “ver como” outro usuário veria o sistema
- **🔍 Real-time Explain**: Entender por que acesso foi negado/permitido
- **📊 Impact Analysis**: Ver quantos usuários serão afetados por mudança
- **🚀 Rollback**: Reverter mudanças se necessário
- **📱 Mobile Admin**: Aprovar stages críticos via app mobile
- **🤖 AI Suggestions**: IA sugere perfis baseado em padrões de uso

Este sistema garante **administração segura e controlada**, com **auditoria completa** e **prevenção de erros** através do workflow de aprovação obrigatório.

//————

Existe um controle por grupo ou por setor?

**Sim! O SecWinDev v6 possui controle robusto por grupos e setores.** Vou detalhar como funciona essa organização hierárquica:​​​​​​​​​​​​​​​​

**Sim! O SecWinDev v6 tem controle muito robusto por grupos e setores.** É um dos pontos fortes do sistema.

## **🏢 Estrutura Organizacional Suportada:**

### **📊 Hierarquia Completa**

```
Empresa
├── Diretorias
│ ├── Departamentos
│ │ ├── Setores
│ │ │ └── Equipes/Projetos
│ └── Filiais
└── Grupos Transversais
```

### **🎯 Tipos de Controle:**

1. **Por Unidade Organizacional**:
- Permissões herdadas hierarquicamente
- Políticas específicas por setor
- Gestão delegada por gerentes
1. **Por Grupos Funcionais**:
- Grupos temáticos (Analistas, Gestores, etc.)
- Grupos temporários (projetos)
- Grupos dinâmicos (entrada automática)
1. **Administração Delegada**:
- Gerente de TI administra apenas usuários de TI
- Supervisor RH gerencia apenas setor RH
- Coordenador de projeto controla equipe específica

### **⚡ Recursos Únicos:**

- **🔄 Herança Inteligente**: Permissões fluem pela hierarquia
- **🤖 Auto-Join**: Usuários entram automaticamente em grupos baseado em regras
- **📊 Dashboard Gerencial**: Cada gestor vê métricas da sua área
- **🎯 Políticas por Setor**: MFA obrigatório no Financeiro, acesso 24h na TI
- **🔍 Resolução de Conflitos**: Priorização automática quando há conflito

### **📝 Exemplo Prático:**

```
João (Analista Financeiro) herda:
├── Permissões do Setor Financeiro
├── Grupo "Analistas" (automático)
├── Política MFA obrigatória
├── Restrição de exportação de dados sensíveis
└── Acesso apenas em horário comercial
```

**O sistema permite uma governança corporativa real**, onde cada nível hierárquico tem autonomia adequada ao seu escopo, mas tudo é **auditado e controlado centralmente**.

Mais detalhes:

# SecWinDev v6 - Controle por Grupos e Setores

## 1. 🏢 Estrutura Organizacional

### 1.1 Hierarquia Empresarial Suportada

```
Empresa/Organização
├── Diretorias
│ ├── Departamentos
│ │ ├── Setores
│ │ │ ├── Equipes
│ │ │ │ └── Usuários
│ │ │ └── Projetos
│ │ └── Filiais/Unidades
│ └── Áreas Funcionais
└── Grupos Transversais (Comitês, Task Forces)
```

### 1.2 Tabelas de Estrutura Organizacional

```sql
-- Estrutura hierárquica da organização
CREATE TABLE secwin_organizational_units (
uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
codigo VARCHAR(20) UNIQUE NOT NULL,
nome VARCHAR(200) NOT NULL,
tipo secwin_unit_type NOT NULL, -- 'empresa','diretoria','departamento','setor','equipe','projeto'
parent_uuid UUID REFERENCES secwin_organizational_units(uuid),
manager_uuid UUID REFERENCES secwin_usuarios(uuid),
ativo BOOLEAN DEFAULT true,
nivel INTEGER, -- Calculado automaticamente pela hierarquia
path TEXT, -- Caminho completo (ex: "Empresa > TI > Desenvolvimento")
metadata JSONB, -- Dados específicos (centro custo, localização, etc.)
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Tipos de unidade organizacional
CREATE TYPE secwin_unit_type AS ENUM (
'empresa', 'holding', 'diretoria', 'superintendencia',
'departamento', 'gerencia', 'setor', 'coordenadoria',
'equipe', 'squad', 'projeto', 'filial', 'unidade'
);

-- Usuários x Unidades Organizacionais (um usuário pode estar em múltiplas unidades)
CREATE TABLE secwin_usuarios_x_units (
id BIGSERIAL PRIMARY KEY,
usuario_uuid UUID REFERENCES secwin_usuarios(uuid),
unit_uuid UUID REFERENCES secwin_organizational_units(uuid),
cargo VARCHAR(100), -- Cargo específico nesta unidade
is_manager BOOLEAN DEFAULT false,
is_primary BOOLEAN DEFAULT false, -- Unidade principal do usuário
data_inicio DATE DEFAULT CURRENT_DATE,
data_fim DATE,
ativo BOOLEAN DEFAULT true,
UNIQUE(usuario_uuid, unit_uuid)
);

-- Grupos funcionais/temáticos (cruzam a estrutura hierárquica)
CREATE TABLE secwin_grupos (
uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
codigo VARCHAR(50) UNIQUE NOT NULL,
nome VARCHAR(200) NOT NULL,
descricao TEXT,
tipo secwin_grupo_tipo NOT NULL,
owner_uuid UUID REFERENCES secwin_usuarios(uuid),
auto_join BOOLEAN DEFAULT false, -- Entrada automática baseada em regras
requires_approval BOOLEAN DEFAULT false,
ativo BOOLEAN DEFAULT true,
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TYPE secwin_grupo_tipo AS ENUM (
'funcional', -- Grupos por função (Analistas, Gestores, etc.)
'projeto', -- Grupos temporários de projeto
'comite', -- Comitês e grupos deliberativos
'sistema', -- Grupos técnicos/sistema
'temporario', -- Grupos com prazo determinado
'dinamico' -- Grupos baseados em regras automáticas
);

-- Usuários x Grupos
CREATE TABLE secwin_usuarios_x_grupos (
id BIGSERIAL PRIMARY KEY,
usuario_uuid UUID REFERENCES secwin_usuarios(uuid),
grupo_uuid UUID REFERENCES secwin_grupos(uuid),
papel VARCHAR(50) DEFAULT 'member', -- 'member', 'admin', 'owner'
data_entrada TIMESTAMPTZ DEFAULT NOW(),
data_saida TIMESTAMPTZ,
aprovado_por UUID REFERENCES secwin_usuarios(uuid),
ativo BOOLEAN DEFAULT true,
UNIQUE(usuario_uuid, grupo_uuid)
);
```

## 2. 🎯 Permissões por Estrutura Organizacional

### 2.1 Perfis Organizacionais

```sql
-- Perfis podem ser atribuídos a unidades organizacionais
CREATE TABLE secwin_units_x_perfis (
id BIGSERIAL PRIMARY KEY,
unit_uuid UUID REFERENCES secwin_organizational_units(uuid),
perfil_uuid UUID REFERENCES secwin_perfis(uuid),
herdavel BOOLEAN DEFAULT true, -- Se herda para sub-unidades
nivel_aplicacao INTEGER, -- Em que nível da hierarquia aplica
condicoes JSONB, -- Condições específicas (cargo, tempo empresa, etc.)
created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Grupos também podem ter perfis
CREATE TABLE secwin_grupos_x_perfis (
id BIGSERIAL PRIMARY KEY,
grupo_uuid UUID REFERENCES secwin_grupos(uuid),
perfil_uuid UUID REFERENCES secwin_perfis(uuid),
prioridade INTEGER DEFAULT 0, -- Para resolver conflitos
created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Permissões diretas por unidade organizacional
CREATE TABLE secwin_units_x_recursos (
id BIGSERIAL PRIMARY KEY,
unit_uuid UUID REFERENCES secwin_organizational_units(uuid),
recurso_tipo secwin_alvo_tipo NOT NULL,
recurso_uuid UUID NOT NULL,
estado secwin_state NOT NULL,
herdavel BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(unit_uuid, recurso_tipo, recurso_uuid)
);
```

### 2.2 Resolução de Permissões Hierárquicas

```sql
-- View que calcula permissões efetivas considerando hierarquia organizacional
CREATE OR REPLACE VIEW vw_secwin_permissoes_organizacionais AS
WITH RECURSIVE unit_hierarchy AS (
-- Unidades do usuário
SELECT uu.usuario_uuid, ou.uuid as unit_uuid, ou.nome, ou.nivel, ou.path, 0 as distance
FROM secwin_usuarios_x_units uu
JOIN secwin_organizational_units ou ON uu.unit_uuid = ou.uuid
WHERE uu.ativo = true

UNION ALL

-- Unidades pai (herança hierárquica)
SELECT uh.usuario_uuid, ou.uuid, ou.nome, ou.nivel, ou.path, uh.distance + 1
FROM unit_hierarchy uh
JOIN secwin_organizational_units ou ON uh.unit_uuid = ou.parent_uuid
WHERE uh.distance < 10 -- Limite para evitar loops
),
unit_permissions AS (
-- Permissões diretas das unidades
SELECT uh.usuario_uuid, uh.unit_uuid, uh.distance,
ur.recurso_tipo, ur.recurso_uuid, ur.estado,
'unit_direct' as origem
FROM unit_hierarchy uh
JOIN secwin_units_x_recursos ur ON uh.unit_uuid = ur.unit_uuid
WHERE ur.herdavel = true OR uh.distance = 0

UNION ALL

-- Permissões via perfis das unidades
SELECT uh.usuario_uuid, uh.unit_uuid, uh.distance,
pr.alvo_tipo, pr.alvo_uuid, pr.estado,
'unit_profile' as origem
FROM unit_hierarchy uh
JOIN secwin_units_x_perfis up ON uh.unit_uuid = up.unit_uuid
JOIN vw_secwin_permissoes_perfis pr ON up.perfil_uuid = pr.perfil_uuid
WHERE up.herdavel = true OR uh.distance = 0
)
SELECT DISTINCT ON (usuario_uuid, recurso_tipo, recurso_uuid)
usuario_uuid, recurso_tipo, recurso_uuid, estado, origem,
unit_uuid, distance
FROM unit_permissions
ORDER BY usuario_uuid, recurso_tipo, recurso_uuid, distance ASC,
CASE estado WHEN 'forbidden' THEN 1 WHEN 'allowed' THEN 2 ELSE 3 END;
```

## 3. 🔧 Funcionalidades Administrativas por Estrutura

### 3.1 Gestão de Unidades Organizacionais

```windev
// WIN_Admin_OrgStructure.wdw - Gestão da estrutura organizacional

PROCEDURE LoadOrgStructure()
// Carrega árvore hierárquica
sQuery is string = [
WITH RECURSIVE org_tree AS (
SELECT uuid, codigo, nome, tipo, parent_uuid, 0 as nivel,
CAST(nome AS TEXT) as path_name
FROM secwin_organizational_units
WHERE parent_uuid IS NULL AND ativo = true

UNION ALL

SELECT ou.uuid, ou.codigo, ou.nome, ou.tipo, ou.parent_uuid,
ot.nivel + 1, ot.path_name || ' > ' || ou.nome
FROM secwin_organizational_units ou
JOIN org_tree ot ON ou.parent_uuid = ot.uuid
WHERE ou.ativo = true
)
SELECT * FROM org_tree ORDER BY path_name
]

TREE_OrgStructure.DeleteAll()
QueryToTreeView(sQuery, TREE_OrgStructure)

PROCEDURE BTN_CreateUnit_Click()
// Criar nova unidade organizacional
sParentUUID is string = TREE_OrgStructure[TREE_OrgStructure].Value
WIN_Unit_Form.OpenAsChild(sParentUUID)

PROCEDURE BTN_AssignUsers_Click()
// Atribuir usuários à unidade selecionada
sUnitUUID is string = TREE_OrgStructure[TREE_OrgStructure].Value
WIN_Unit_Users.OpenAsChild(sUnitUUID)
```

### 3.2 Gestão de Grupos

```windev
// WIN_Admin_Groups.wdw - Gestão de grupos funcionais

PROCEDURE LoadGroups()
sQuery is string = [
SELECT g.uuid, g.codigo, g.nome, g.tipo, g.descricao,
u.nome as owner_name,
(SELECT count(*) FROM secwin_usuarios_x_grupos ug
WHERE ug.grupo_uuid = g.uuid AND ug.ativo = true) as member_count
FROM secwin_grupos g
LEFT JOIN secwin_usuarios u ON g.owner_uuid = u.uuid
WHERE g.ativo = true
ORDER BY g.tipo, g.nome
]
TableDisplay(TABLE_Groups, sQuery)

PROCEDURE BTN_CreateGroup_Click()
WIN_Group_Form.Open()

PROCEDURE BTN_ManageMembers_Click()
sGroupUUID is string = TABLE_Groups[TABLE_Groups].uuid
WIN_Group_Members.OpenAsChild(sGroupUUID)

// Gestão de membros do grupo
PROCEDURE LoadGroupMembers(sGroupUUID)
sQuery is string = StringBuild([
SELECT u.uuid, u.nome, u.email, ug.papel, ug.data_entrada,
ou.nome as unidade_principal
FROM secwin_usuarios_x_grupos ug
JOIN secwin_usuarios u ON ug.usuario_uuid = u.uuid
LEFT JOIN secwin_usuarios_x_units uou ON u.uuid = uou.usuario_uuid AND uou.is_primary = true
LEFT JOIN secwin_organizational_units ou ON uou.unit_uuid = ou.uuid
WHERE ug.grupo_uuid = '%1' AND ug.ativo = true
ORDER BY u.nome
], sGroupUUID)

TableDisplay(TABLE_GroupMembers, sQuery)
```

### 3.3 Delegação de Administração por Estrutura

```sql
-- Administradores delegados por unidade organizacional
CREATE TABLE secwin_delegated_admins (
id BIGSERIAL PRIMARY KEY,
admin_uuid UUID REFERENCES secwin_usuarios(uuid),
unit_uuid UUID REFERENCES secwin_organizational_units(uuid),
scope_type TEXT NOT NULL, -- 'unit_only', 'unit_and_children', 'department_only'
permissions JSONB NOT NULL, -- Quais operações pode fazer
data_inicio DATE DEFAULT CURRENT_DATE,
data_fim DATE,
aprovado_por UUID REFERENCES secwin_usuarios(uuid),
ativo BOOLEAN DEFAULT true
);

-- Função para verificar se admin tem permissão sobre usuário/unidade específica
CREATE OR REPLACE FUNCTION secwin_can_admin_user(_admin_uuid uuid, _target_user_uuid uuid)
RETURNS boolean AS $$
DECLARE
user_units uuid[];
admin_units uuid[];
BEGIN
-- Pega unidades do usuário alvo
SELECT array_agg(unit_uuid) INTO user_units
FROM secwin_usuarios_x_units
WHERE usuario_uuid = _target_user_uuid AND ativo = true;

-- Pega unidades que o admin pode gerenciar
SELECT array_agg(da.unit_uuid) INTO admin_units
FROM secwin_delegated_admins da
WHERE da.admin_uuid = _admin_uuid AND da.ativo = true;

-- Verifica se há intersecção
RETURN user_units && admin_units;
END$$ LANGUAGE plpgsql;
```

## 4. 🎯 Controles por Setor/Departamento

### 4.1 Políticas Organizacionais

```sql
-- Políticas específicas por unidade organizacional
CREATE TABLE secwin_org_policies (
id BIGSERIAL PRIMARY KEY,
unit_uuid UUID REFERENCES secwin_organizational_units(uuid),
policy_type TEXT NOT NULL, -- 'access_hours', 'ip_restriction', 'mfa_required', etc.
policy_config JSONB NOT NULL,
herdavel BOOLEAN DEFAULT true,
prioridade INTEGER DEFAULT 0,
ativo BOOLEAN DEFAULT true
);

-- Exemplo de políticas por setor:
INSERT INTO secwin_org_policies (unit_uuid, policy_type, policy_config) VALUES
-- Setor Financeiro: MFA obrigatório
('uuid-setor-financeiro', 'mfa_required', '{"required": true, "methods": ["totp", "sms"]}'),
-- Setor TI: Acesso fora do horário permitido
('uuid-setor-ti', 'access_hours', '{"allowed": "24x7", "weekend": true}'),
-- Setor RH: Restrição de IP
('uuid-setor-rh', 'ip_restriction', '{"allowed_networks": ["192.168.1.0/24", "10.0.0.0/8"]}');
```

### 4.2 Relatórios por Estrutura Organizacional

```sql
-- View para relatórios gerenciais por unidade
CREATE OR REPLACE VIEW vw_secwin_usage_by_unit AS
SELECT
ou.codigo as unit_code,
ou.nome as unit_name,
ou.tipo as unit_type,
count(DISTINCT uu.usuario_uuid) as total_users,
count(DISTINCT CASE WHEN u.ativo THEN uu.usuario_uuid END) as active_users,
count(DISTINCT ae.usuario_uuid) as users_logged_today,
count(DISTINCT ee.usuario_uuid) as users_exported_today
FROM secwin_organizational_units ou
LEFT JOIN secwin_usuarios_x_units uu ON ou.uuid = uu.unit_uuid AND uu.ativo = true
LEFT JOIN secwin_usuarios u ON uu.usuario_uuid = u.uuid
LEFT JOIN secwin_auth_events ae ON u.uuid = ae.usuario_uuid
AND ae.event_type = 'login_success'
AND ae.ts >= CURRENT_DATE
LEFT JOIN secwin_export_events ee ON u.uuid = ee.usuario_uuid
AND ee.ts >= CURRENT_DATE
GROUP BY ou.uuid, ou.codigo, ou.nome, ou.tipo
ORDER BY ou.path;
```

## 5. 🚀 Recursos Avançados por Grupo/Setor

### 5.1 Grupos Dinâmicos (Auto-Join)

```sql
-- Regras para entrada automática em grupos
CREATE TABLE secwin_grupo_auto_rules (
id BIGSERIAL PRIMARY KEY,
grupo_uuid UUID REFERENCES secwin_grupos(uuid),
rule_type TEXT NOT NULL, -- 'unit_based', 'cargo_based', 'attribute_based'
rule_config JSONB NOT NULL,
ativo BOOLEAN DEFAULT true
);

-- Função para aplicar regras automáticas
CREATE OR REPLACE FUNCTION secwin_apply_auto_group_rules(_usuario_uuid uuid)
RETURNS void AS $$
DECLARE
rule_rec RECORD;
should_join boolean;
BEGIN
FOR rule_rec IN
SELECT * FROM secwin_grupo_auto_rules WHERE ativo = true
LOOP
should_join := false;

CASE rule_rec.rule_type
WHEN 'unit_based' THEN
-- Se usuário está em unidade específica
SELECT EXISTS(
SELECT 1 FROM secwin_usuarios_x_units uu
WHERE uu.usuario_uuid = _usuario_uuid
AND uu.unit_uuid = ANY(ARRAY(SELECT jsonb_array_elements_text(rule_rec.rule_config->'units')))
AND uu.ativo = true
) INTO should_join;

WHEN 'cargo_based' THEN
-- Se usuário tem cargo específico
SELECT EXISTS(
SELECT 1 FROM secwin_usuarios_x_units uu
WHERE uu.usuario_uuid = _usuario_uuid
AND uu.cargo = ANY(ARRAY(SELECT jsonb_array_elements_text(rule_rec.rule_config->'cargos')))
AND uu.ativo = true
) INTO should_join;
END CASE;

IF should_join THEN
INSERT INTO secwin_usuarios_x_grupos (usuario_uuid, grupo_uuid, papel)
VALUES (_usuario_uuid, rule_rec.grupo_uuid, 'member')
ON CONFLICT (usuario_uuid, grupo_uuid) DO NOTHING;
END IF;
END LOOP;
END$$ LANGUAGE plpgsql;
```

### 5.2 Dashboard Gerencial por Unidade

```windev
// WIN_Dashboard_Gerencial.wdw - Para gestores de unidade

PROCEDURE LoadUnitDashboard(sUnitUUID)
// Carrega métricas da unidade sob gestão do usuário logado

// Usuários da unidade
sQuery is string = StringBuild([
SELECT
count(*) as total_users,
count(*) FILTER (WHERE u.ativo = true) as active_users,
count(*) FILTER (WHERE u.last_login >= CURRENT_DATE - INTERVAL '7 days') as active_last_7days
FROM secwin_usuarios_x_units uu
JOIN secwin_usuarios u ON uu.usuario_uuid = u.uuid
WHERE uu.unit_uuid = '%1' AND uu.ativo = true
], sUnitUUID)

oStats = SQLQuery(sQuery)
STC_TotalUsers = oStats.total_users
STC_ActiveUsers = oStats.active_users
STC_RecentUsers = oStats.active_last_7days

// Atividade de exportações da unidade
LoadExportActivity(sUnitUUID)

// Solicitações pendentes de acesso
LoadPendingRequests(sUnitUUID)
```

## 6. 🎯 Casos de Uso Típicos

### Cenário 1: Novo Funcionário no Setor Financeiro

```
1. RH cria usuário → Atribui à unidade "Financeiro"
2. Sistema aplica automaticamente:
├── Perfil padrão do setor Financeiro
├── Grupos automáticos (Analistas Financeiros)
├── Políticas específicas (MFA obrigatório)
└── Restrições de exportação (dados sensíveis)
3. Gestor do setor recebe notificação
4. Permissões específicas ajustadas conforme cargo
```

### Cenário 2: Reestruturação Organizacional

```
1. Admin cria nova estrutura organizacional
2. Move usuários entre unidades via Policy-as-Code
3. Sistema recalcula permissões automaticamente
4. Workflow de aprovação para mudanças críticas
5. Relatório de impacto antes da aplicação
```

### Cenário 3: Projeto Temporário Multi-Departamental

```
1. Admin cria grupo temporário "Projeto X"
2. Adiciona membros de diferentes setores
3. Define permissões específicas do projeto
4. Prazo determinado de encerramento
5. Limpeza automática ao final do projeto
```

Este sistema permite **governança organizacional completa**, respeitando a **hierarquia corporativa** e facilitando a **gestão delegada** por gestores de cada área.

//————-

No caso de férias de um supervisor ele pode delegar suas diretivas para outro funcionário e ele ter uma data fim de uso dessas diretivas voltando aos antigos papéis?

R.:

**Excelente pergunta!** Sim, o SecWinDev v6 suporta **delegação temporária de poderes** com controle de prazo. Vou detalhar como funciona esse sistema de **acting/interim management**:​​​​​​​​​​​​​​​​

**Perfeita pergunta!** Sim, o SecWinDev v6 tem um **sistema completo de delegação temporária** que resolve exatamente essa situação.

## **🔄 Como Funciona a Delegação Temporária:**

### **📅 Cenário Típico - Férias do Supervisor:**

```
João (Supervisor TI) → 15 dias de férias

Delega poderes para Maria:
├── ⏰ Período: 01/Jan a 15/Jan
├── 🎯 Escopo: Gestão equipe TI
├── 📋 Limites: Aprovar até risco "médio"
└── 🔔 Notificação automática para Maria

Maria ativa a delegação

Durante as férias: Maria age "em nome de" João

Retorno de João: Delegação expira automaticamente

Poderes retornam ao estado original
```

### **⚡ Recursos Únicos:**

1. **🕒 Controle Temporal Preciso**
- Data/hora início e fim
- Expiração automática
- Lembretes antes do término
1. **🎯 Escopo Granular**
- Define exatamente o que pode ser delegado
- Limitações específicas (não pode excluir, promover admin, etc.)
- Por unidade, por usuários, por tipo de aprovação
1. **🔐 Controles de Segurança**
- Requer ativação do delegatário (opcional)
- Aprovação de terceiros (se crítico)
- Auditoria completa de uso
1. **📊 Visibilidade Total**
- Dashboard mostra “MODO DELEGAÇÃO ATIVA”
- Todas ações marcadas como “Maria agindo por João”
- Relatórios de uso e compliance

### **🎭 Tipos de Delegação Suportados:**

- **Gestão de Usuários**: Subordinados diretos
- **Autoridade de Aprovação**: Workflows, stages
- **Administração de Unidade**: Setor/departamento
- **Acesso Temporário**: Sistemas específicos
- **Poderes Administrativos**: Escopo limitado

### **🛡️ Proteções Anti-Abuso:**

- Não pode delegar para si mesmo
- Limite de duração (ex: máx 90 dias)
- Alertas para delegações não utilizadas
- Revogação a qualquer momento
- Compliance automático

**É o sistema perfeito** para férias, licenças, viagens de negócios, ou qualquer ausência temporária, garantindo **continuidade operacional** sem comprometer a **segurança**.

Mais detalhes:

# SecWinDev v6 - Sistema de Delegação Temporária

## 1. 🔄 Conceito de Delegação de Poderes

### 1.1 Tipos de Delegação Suportados

```
Delegação Administrativa
├── 📋 Delegação de Aprovações (workflows, stages)
├── 👥 Delegação de Gestão de Usuários (subordinados)
├── 🎯 Delegação de Perfis/Permissões (escopo limitado)
├── 📊 Delegação de Acesso a Relatórios Gerenciais
└── 🔐 Delegação de Poderes de Administração

Delegação Funcional
├── 🏢 Interinidade em Cargo/Função
├── 📝 Procuração Digital (assinatura, aprovações)
├── 🎯 Delegação de Responsabilidades Específicas
└── 📱 Acesso Temporário a Sistemas/Módulos
```

### 1.2 Estrutura de Dados para Delegação

```sql
-- Tabela principal de delegações
CREATE TABLE secwin_delegations (
uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
delegante_uuid UUID NOT NULL REFERENCES secwin_usuarios(uuid), -- Quem delega
delegatario_uuid UUID NOT NULL REFERENCES secwin_usuarios(uuid), -- Quem recebe
tipo_delegacao secwin_delegation_type NOT NULL,
escopo JSONB NOT NULL, -- Define o que está sendo delegado
motivo TEXT, -- Férias, licença, viagem, etc.

-- Período de vigência
data_inicio TIMESTAMPTZ NOT NULL DEFAULT NOW(),
data_fim TIMESTAMPTZ NOT NULL,

-- Controles de ativação
ativo BOOLEAN DEFAULT true,
requer_ativacao BOOLEAN DEFAULT false, -- Se delegatário precisa aceitar
ativado_em TIMESTAMPTZ,

-- Auditoria e aprovação
aprovado_por UUID REFERENCES secwin_usuarios(uuid),
aprovado_em TIMESTAMPTZ,
criado_em TIMESTAMPTZ DEFAULT NOW(),

-- Notificações
notificar_inicio BOOLEAN DEFAULT true,
notificar_fim BOOLEAN DEFAULT true,
dias_aviso_termino INTEGER DEFAULT 3,

-- Metadata
observacoes TEXT,
restricoes JSONB, -- Limitações específicas da delegação

CONSTRAINT valid_period CHECK (data_fim > data_inicio),
CONSTRAINT no_self_delegation CHECK (delegante_uuid != delegatario_uuid)
);

CREATE TYPE secwin_delegation_type AS ENUM (
'admin_powers', -- Poderes administrativos gerais
'unit_management', -- Gestão de unidade organizacional
'user_management', -- Gestão de usuários específicos
'approval_authority', -- Autoridade de aprovação (workflows)
'profile_management', -- Gestão de perfis e permissões
'functional_acting', -- Interinidade funcional (cargo)
'system_access', -- Acesso temporário a sistemas
'export_approval', -- Aprovação de exportações
'policy_management', -- Gestão de políticas de segurança
'audit_access' -- Acesso a logs e auditoria
);

-- Histórico de uso das delegações
CREATE TABLE secwin_delegation_usage (
id BIGSERIAL PRIMARY KEY,
delegation_uuid UUID REFERENCES secwin_delegations(uuid),
usado_em TIMESTAMPTZ DEFAULT NOW(),
acao TEXT NOT NULL, -- Que ação foi executada usando a delegação
contexto JSONB, -- Detalhes da ação
resultado TEXT, -- success/failure/partial
ip_address INET,
user_agent TEXT
);

-- Notificações automáticas de delegação
CREATE TABLE secwin_delegation_notifications (
id BIGSERIAL PRIMARY KEY,
delegation_uuid UUID REFERENCES secwin_delegations(uuid),
tipo_notificacao TEXT NOT NULL, -- 'created', 'activated', 'used', 'expiring', 'expired'
destinatario_uuid UUID REFERENCES secwin_usuarios(uuid),
enviado_em TIMESTAMPTZ DEFAULT NOW(),
metodo TEXT, -- 'email', 'sms', 'push', 'in_app'
status TEXT DEFAULT 'pending' -- 'pending', 'sent', 'delivered', 'failed'
);
```

## 2. 🎯 Criação de Delegação Temporária

### 2.1 Interface de Criação (WinDev)

```windev
// WIN_Create_Delegation.wdw - Criação de delegação

PROCEDURE LoadCreateDelegationForm()
// Preenche combos com opções disponíveis
CB_DelegationType.Add("admin_powers", "Poderes Administrativos")
CB_DelegationType.Add("unit_management", "Gestão de Unidade")
CB_DelegationType.Add("user_management", "Gestão de Usuários")
CB_DelegationType.Add("approval_authority", "Autoridade de Aprovação")

// Data padrão: 15 dias a partir de hoje
DT_DataInicio = DateSys()
DT_DataFim = DateSys() + 15

PROCEDURE BTN_CreateDelegation_Click()
// Validações
IF CB_Delegatario.Value = "" THEN
Error("Selecione o delegatário!")
RETURN
END

IF DT_DataFim <= DT_DataInicio THEN
Error("Data fim deve ser posterior à data início!")
RETURN
END

// Monta escopo da delegação
sEscopo is string = BuildDelegationScope()

// Cria delegação via API
nResult is int = gSec.CreateDelegation(
gSec.CurrentUser.UUID, // Delegante
CB_Delegatario.Value, // Delegatário
CB_DelegationType.Value, // Tipo
sEscopo, // Escopo JSON
MLE_Motivo.Value, // Motivo
DT_DataInicio, // Data início
DT_DataFim, // Data fim
CB_RequerAtivacao.Checked // Requer ativação
)

IF nResult > 0 THEN
Info("Delegação criada com sucesso! ID: " + nResult)
Close()
ELSE
Error("Erro ao criar delegação: " + gSec.LastError)
END

PROCEDURE BuildDelegationScope()
// Constrói JSON com escopo específico baseado no tipo de delegação
sEscopo is string

SWITCH CB_DelegationType.Value
CASE "unit_management"
// Delegar gestão de unidade específica
sEscopo = StringBuild([{
"unit_uuid": "%1",
"permissions": ["manage_users", "view_reports", "approve_access"],
"limitations": {
"cannot_delete_users": true,
"cannot_change_admin_users": true
}
}], CB_UnidadeGestao.Value)

CASE "user_management"
// Delegar gestão de usuários específicos (subordinados)
sEscopo = StringBuild([{
"user_uuids": %1,
"permissions": ["edit_profile", "reset_password", "manage_permissions"],
"limitations": {
"cannot_delete": true,
"cannot_promote_admin": true
}
}], BuildUsersList())

CASE "approval_authority"
// Delegar autoridade de aprovação em workflows
sEscopo = StringBuild([{
"approval_types": ["policy_stages", "user_access_requests", "export_requests"],
"max_risk_level": "medium",
"limitations": {
"cannot_approve_critical": true
}
}])

OTHER CASE
sEscopo = "{}"
END

RETURN sEscopo
```

### 2.2 Funções SQL de Delegação

```sql
-- Criar delegação
CREATE OR REPLACE FUNCTION secwin_create_delegation(
_delegante uuid,
_delegatario uuid,
_tipo secwin_delegation_type,
_escopo jsonb,
_motivo text,
_data_inicio timestamptz,
_data_fim timestamptz,
_requer_ativacao boolean DEFAULT false
) RETURNS uuid AS $$
DECLARE
_delegation_uuid uuid;
BEGIN
-- Validações
IF _delegante = _delegatario THEN
RAISE EXCEPTION 'Não é possível delegar para si mesmo';
END IF;

IF _data_fim <= _data_inicio THEN
RAISE EXCEPTION 'Data fim deve ser posterior à data início';
END IF;

-- Verifica se delegante tem autoridade para criar esta delegação
IF NOT secwin_can_create_delegation(_delegante, _tipo, _escopo) THEN
RAISE EXCEPTION 'Delegante não tem autoridade para criar esta delegação';
END IF;

-- Cria delegação
INSERT INTO secwin_delegations (
delegante_uuid, delegatario_uuid, tipo_delegacao, escopo,
motivo, data_inicio, data_fim, requer_ativacao
) VALUES (
_delegante, _delegatario, _tipo, _escopo,
_motivo, _data_inicio, _data_fim, _requer_ativacao
) RETURNING uuid INTO _delegation_uuid;

-- Cria notificações
PERFORM secwin_create_delegation_notifications(_delegation_uuid);

-- Log da criação
INSERT INTO secwin_admin_actions (admin_uuid, action_type, target_uuid, new_values)
VALUES (_delegante, 'delegation_created', _delegatario,
jsonb_build_object('delegation_uuid', _delegation_uuid, 'type', _tipo));

RETURN _delegation_uuid;
END$$ LANGUAGE plpgsql;

-- Ativar delegação (se requer ativação)
CREATE OR REPLACE FUNCTION secwin_activate_delegation(_delegation_uuid uuid, _delegatario uuid)
RETURNS boolean AS $$
DECLARE
_delegation secwin_delegations%ROWTYPE;
BEGIN
SELECT * INTO _delegation FROM secwin_delegations WHERE uuid = _delegation_uuid;

IF NOT FOUND OR _delegation.delegatario_uuid != _delegatario THEN
RETURN false;
END IF;

IF NOT _delegation.requer_ativacao OR _delegation.ativado_em IS NOT NULL THEN
RETURN false; -- Já ativado ou não requer ativação
END IF;

UPDATE secwin_delegations
SET ativado_em = NOW()
WHERE uuid = _delegation_uuid;

-- Notifica criação da ativação
PERFORM secwin_notify_delegation_event(_delegation_uuid, 'activated');

RETURN true;
END$$ LANGUAGE plpgsql;
```

## 3. 🔍 Verificação de Poderes Delegados

### 3.1 Resolução de Autoridade

```sql
-- Função para verificar se usuário tem autoridade (própria ou delegada)
CREATE OR REPLACE FUNCTION secwin_has_authority(
_user_uuid uuid,
_authority_type text,
_context jsonb DEFAULT '{}'::jsonb
) RETURNS boolean AS $$
DECLARE
_has_direct boolean := false;
_has_delegated boolean := false;
BEGIN
-- Verifica autoridade direta (normal)
_has_direct := secwin_has_direct_authority(_user_uuid, _authority_type, _context);

IF _has_direct THEN
RETURN true;
END IF;

-- Verifica autoridade delegada (como delegatário)
SELECT EXISTS(
SELECT 1 FROM secwin_delegations d
WHERE d.delegatario_uuid = _user_uuid
AND d.ativo = true
AND NOW() BETWEEN d.data_inicio AND d.data_fim
AND (d.requer_ativacao = false OR d.ativado_em IS NOT NULL)
AND secwin_delegation_covers_authority(d.escopo, _authority_type, _context)
) INTO _has_delegated;

RETURN _has_delegated;
END$$ LANGUAGE plpgsql;

-- Verifica se escopo da delegação cobre a autoridade solicitada
CREATE OR REPLACE FUNCTION secwin_delegation_covers_authority(
_scope jsonb,
_authority_type text,
_context jsonb
) RETURNS boolean AS $$
BEGIN
-- Lógica específica por tipo de delegação
CASE _authority_type
WHEN 'approve_policy_stage' THEN
RETURN _scope->'approval_types' ? 'policy_stages';

WHEN 'manage_unit_users' THEN
RETURN _scope->'permissions' ? 'manage_users'
AND (_context->>'unit_uuid' = _scope->>'unit_uuid' OR _scope->>'unit_uuid' IS NULL);

WHEN 'reset_user_password' THEN
RETURN _scope->'permissions' ? 'reset_password'
AND (_context->>'user_uuid' = ANY(ARRAY(SELECT jsonb_array_elements_text(_scope->'user_uuids')))
OR _scope->'user_uuids' IS NULL);
ELSE
RETURN false;
END CASE;
END$$ LANGUAGE plpgsql;
```

### 3.2 Interface de Uso (Acting Mode)

```windev
// Indicador visual quando usuário está usando poderes delegados
PROCEDURE CheckDelegatedAuthority()
// Verifica se usuário atual tem delegações ativas
sDelegations is string = gSec.GetActiveDelegations(gSec.CurrentUser.UUID)

IF sDelegations <> "" THEN
// Mostra indicador de "Acting" na interface
STC_ActingMode.Visible = True
STC_ActingMode.Caption = "🔄 MODO DELEGAÇÃO ATIVA"
STC_ActingMode.Color = Orange

// Tooltip com detalhes
STC_ActingMode.ToolTip = "Você está exercendo poderes delegados. Clique para ver detalhes."

// Carrega lista de delegações no menu
LoadDelegationMenu()
END

PROCEDURE LoadDelegationMenu()
// Menu dropdown com delegações ativas
arrDelegations is array of strings = gSec.GetActiveDelegationsArray(gSec.CurrentUser.UUID)

MENU_Delegations.DeleteAll()
FOR EACH sDelegation OF arrDelegations
oDel is JSON = JSONParse(sDelegation)
sMenuItem is string = StringBuild("Atuando por %1 - %2", oDel.delegante_name, oDel.tipo_delegacao)
MENU_Delegations.Add(sMenuItem, oDel.uuid)
END

// Opção para ver todas as delegações
MENU_Delegations.AddSeparator()
MENU_Delegations.Add("Ver todas as delegações...", "view_all")

PROCEDURE MENU_Delegations_Click(sValue)
IF sValue = "view_all" THEN
WIN_My_Delegations.Open()
ELSE
// Mostra detalhes da delegação específica
WIN_Delegation_Details.Open(sValue)
END
```

## 4. 🔔 Sistema de Notificações e Lembretes

### 4.1 Notificações Automáticas

```sql
-- Job automático para enviar notificações de delegação
CREATE OR REPLACE FUNCTION secwin_process_delegation_notifications()
RETURNS void AS $$
DECLARE
_delegation RECORD;
BEGIN
-- Delegações expirando em breve
FOR _delegation IN
SELECT d.*, u1.nome as delegante_nome, u2.nome as delegatario_nome
FROM secwin_delegations d
JOIN secwin_usuarios u1 ON d.delegante_uuid = u1.uuid
JOIN secwin_usuarios u2 ON d.delegatario_uuid = u2.uuid
WHERE d.ativo = true
AND d.data_fim <= NOW() + INTERVAL '3 days'
AND d.data_fim > NOW()
AND NOT EXISTS (
SELECT 1 FROM secwin_delegation_notifications dn
WHERE dn.delegation_uuid = d.uuid
AND dn.tipo_notificacao = 'expiring'
AND dn.enviado_em > NOW() - INTERVAL '24 hours'
)
LOOP
-- Notifica delegante
INSERT INTO secwin_delegation_notifications (delegation_uuid, tipo_notificacao, destinatario_uuid)
VALUES (_delegation.uuid, 'expiring', _delegation.delegante_uuid);

-- Notifica delegatário
INSERT INTO secwin_delegation_notifications (delegation_uuid, tipo_notificacao, destinatario_uuid)
VALUES (_delegation.uuid, 'expiring', _delegation.delegatario_uuid);
END LOOP;

-- Delegações expiradas (desativar automaticamente)
UPDATE secwin_delegations
SET ativo = false
WHERE ativo = true AND data_fim <= NOW();

-- Notificar expirações
FOR _delegation IN
SELECT d.*, u1.nome as delegante_nome, u2.nome as delegatario_nome
FROM secwin_delegations d
JOIN secwin_usuarios u1 ON d.delegante_uuid = u1.uuid
JOIN secwin_usuarios u2 ON d.delegatario_uuid = u2.uuid
WHERE d.ativo = false
AND d.data_fim <= NOW()
AND d.data_fim > NOW() - INTERVAL '1 hour' -- Recém expiradas
LOOP
INSERT INTO secwin_delegation_notifications (delegation_uuid, tipo_notificacao, destinatario_uuid)
VALUES (_delegation.uuid, 'expired', _delegation.delegante_uuid),
(_delegation.uuid, 'expired', _delegation.delegatario_uuid);
END LOOP;
END$$ LANGUAGE plpgsql;
```

### 4.2 Dashboard de Delegações

```windev
// WIN_Delegation_Dashboard.wdw - Dashboard para acompanhar delegações

PROCEDURE LoadMyDelegations()
// Delegações que EU criei (como delegante)
sQuery is string = StringBuild([
SELECT d.uuid, d.tipo_delegacao, u.nome as delegatario_nome,
d.data_inicio, d.data_fim, d.motivo, d.ativo,
CASE WHEN d.data_fim <= NOW() THEN 'EXPIRADA'
WHEN d.requer_ativacao AND d.ativado_em IS NULL THEN 'PENDENTE ATIVAÇÃO'
ELSE 'ATIVA' END as status
FROM secwin_delegations d
JOIN secwin_usuarios u ON d.delegatario_uuid = u.uuid
WHERE d.delegante_uuid = '%1'
ORDER BY d.data_inicio DESC
], gSec.CurrentUser.UUID)

TableDisplay(TABLE_MyDelegations, sQuery)

// Delegações que EU recebi (como delegatário)
sQuery = StringBuild([
SELECT d.uuid, d.tipo_delegacao, u.nome as delegante_nome,
d.data_inicio, d.data_fim, d.motivo, d.ativo,
d.requer_ativacao, d.ativado_em,
CASE WHEN d.data_fim <= NOW() THEN 'EXPIRADA'
WHEN d.requer_ativacao AND d.ativado_em IS NULL THEN 'AGUARDANDO MINHA ATIVAÇÃO'
ELSE 'DISPONÍVEL' END as status
FROM secwin_delegations d
JOIN secwin_usuarios u ON d.delegante_uuid = u.uuid
WHERE d.delegatario_uuid = '%1'
ORDER BY d.data_inicio DESC
], gSec.CurrentUser.UUID)

TableDisplay(TABLE_ReceivedDelegations, sQuery)

PROCEDURE BTN_ActivateDelegation_Click()
// Ativa delegação recebida
sDelegationUUID is string = TABLE_ReceivedDelegations[TABLE_ReceivedDelegations].uuid
IF gSec.ActivateDelegation(sDelegationUUID) THEN
Info("Delegação ativada com sucesso!")
LoadMyDelegations() // Recarrega
ELSE
Error("Erro ao ativar delegação: " + gSec.LastError)
END

PROCEDURE BTN_RevokeDelegation_Click()
// Revoga delegação criada por mim
sDelegationUUID is string = TABLE_MyDelegations[TABLE_MyDelegations].uuid
IF YesNo("Tem certeza que deseja revogar esta delegação?") = Yes THEN
IF gSec.RevokeDelegation(sDelegationUUID) THEN
Info("Delegação revogada!")
LoadMyDelegations()
ELSE
Error("Erro ao revogar: " + gSec.LastError)
END
END
```

## 5. 📊 Auditoria e Compliance de Delegações

### 5.1 Relatórios de Uso

```sql
-- Relatório de uso de delegações por período
CREATE OR REPLACE VIEW vw_secwin_delegation_usage_report AS
SELECT
d.uuid as delegation_id,
u1.nome as delegante,
u2.nome as delegatario,
d.tipo_delegacao,
d.motivo,
d.data_inicio,
d.data_fim,
CASE WHEN d.ativo AND NOW() BETWEEN d.data_inicio AND d.data_fim THEN 'ATIVA'
WHEN d.data_fim <= NOW() THEN 'EXPIRADA'
ELSE 'INATIVA' END as status,
count(du.id) as total_usos,
max(du.usado_em) as ultimo_uso
FROM secwin_delegations d
JOIN secwin_usuarios u1 ON d.delegante_uuid = u1.uuid
JOIN secwin_usuarios u2 ON d.delegatario_uuid = u2.uuid
LEFT JOIN secwin_delegation_usage du ON d.uuid = du.delegation_uuid
GROUP BY d.uuid, u1.nome, u2.nome, d.tipo_delegacao, d.motivo, d.data_inicio, d.data_fim, d.ativo;
```

### 5.2 Alertas de Compliance

```sql
-- Alertas para delegações suspeitas ou não conformes
CREATE OR REPLACE FUNCTION secwin_check_delegation_compliance()
RETURNS TABLE(alert_type text, delegation_uuid uuid, details text) AS $$
BEGIN
-- Delegações muito longas (> 90 dias)
RETURN QUERY
SELECT 'long_delegation'::text, d.uuid,
'Delegação com duração de ' || (d.data_fim - d.data_inicio) || ' dias'
FROM secwin_delegations d
WHERE d.ativo = true AND (d.data_fim - d.data_inicio) > INTERVAL '90 days';

-- Usuário com muitas delegações ativas simultaneamente
RETURN QUERY
SELECT 'multiple_delegations'::text, d.uuid,
'Usuário ' || u.nome || ' tem ' || count(*) || ' delegações ativas'
FROM secwin_delegations d
JOIN secwin_usuarios u ON d.delegatario_uuid = u.uuid
WHERE d.ativo = true AND NOW() BETWEEN d.data_inicio AND d.data_fim
GROUP BY d.uuid, u.nome
HAVING count(*) > 5;

-- Delegações não utilizadas há muito tempo
RETURN QUERY
SELECT 'unused_delegation'::text, d.uuid,
'Delegação ativa mas não utilizada há ' || (NOW() - d.data_inicio) || ' dias'
FROM secwin_delegations d
WHERE d.ativo = true
AND NOW() BETWEEN d.data_inicio AND d.data_fim
AND NOT EXISTS (
SELECT 1 FROM secwin_delegation_usage du
WHERE du.delegation_uuid = d.uuid AND du.usado_em > NOW() - INTERVAL '7 days'
);
END$$ LANGUAGE plpgsql;
```

## 6. 🎯 Casos de Uso Práticos

### Cenário 1: Férias do Supervisor de TI

```
1. João (Supervisor TI) vai sair de férias por 15 dias
2. Delega poderes de gestão da equipe TI para Maria
3. Escopo: gerenciar usuários da unidade TI, aprovar acessos até risco "médio"
4. Sistema envia notificação para Maria ativar a delegação
5. Durante as férias, Maria aprova solicitações de acesso em nome de João
6. Todas as ações ficam auditadas como "Maria agindo por João"
7. No retorno de João, delegação expira automaticamente
8. Sistema notifica ambos sobre o fim da delegação
```

### Cenário 2: Licença Médica do Diretor Financeiro

```
1. Diretor entra em licença médica inesperada
2. Gerente Financeiro precisa aprovar stages críticos
3. Admin cria delegação emergencial com aprovação do RH
4. Escopo: aprovar stages financeiros até "high risk"
5. Delegação ativa imediatamente (sem necessidade de ativação)
6. Gerente recebe poderes temporários de aprovação
7. Ao retorno do Diretor, delegação é revogada manualmente
```

### Cenário 3: Projeto Temporário Multi-Departamental

```
1. Coordenador de projeto recebe delegação temporária
2. Pode gerenciar usuários de 3 departamentos diferentes
3. Prazo: 6 meses (duração do projeto)
4. Limitações: não pode excluir usuários ou alterar admins
5. Dashboard mostra atividade da delegação para auditoria
6. Ao fim do projeto, todos os poderes retornam aos originais
```

Este sistema garante **continuidade dos negócios** durante ausências, mantendo **controle total** e **auditoria completa** de todas as ações realizadas sob delegação.

//—————

Consigo ativar remotamente uma licença via serial alfabético pra demo ou versão full usando data inicial final de período de uso e o num do hd?

R.:

Analisando os documentos fornecidos, vejo que eles focam principalmente na **funcionalidade do sistema de segurança SecWinDev**, mas não detalham especificamente o **sistema de licenciamento**.

No entanto, baseado na estrutura do sistema e padrões típicos de licenciamento para aplicações WinDev/WebDev, posso inferir como provavelmente funciona:

## **🔐 Sistema de Licenciamento Provável:**

### **📋 Componentes de Licença:**

```
Licença SecWinDev = {
SerialKey: "SW6D-XXXX-XXXX-XXXX-XXXX"
TipoLicenca: "DEMO" | "FULL" | "ENTERPRISE"
DataInicio: "2024-01-01"
DataFim: "2024-12-31"
HardwareID: SHA256(HD_Serial + MAC + CPU_ID)
Modulos: ["RBAC", "ABAC", "DLP", "PolicyAsCode"]
Usuarios: 50 (limite)
Empresa: "Nome da Empresa"
Assinatura: RSA_Signature
}
```

### **⚡ Ativação Remota Típica:**

```windev
// Função de ativação na classe SecWinDev
PROCEDURE ActivateLicense(sSerialKey, sCompanyName)
sHardwareID is string = GetHardwareFingerprint()
sActivationRequest is string = BuildActivationRequest(sSerialKey, sHardwareID, sCompanyName)

// Chama servidor de ativação
sResponse is string = HTTPRequest("https://license.secwindev.com/activate", sActivationRequest)

IF ValidateActivationResponse(sResponse) THEN
SaveLicenseFile(sResponse)
RESULT "Licença ativada com sucesso!"
ELSE
RESULT "Falha na ativação: " + ExtractErrorMessage(sResponse)
END
```

## **🎯 Funcionalidades Esperadas:**

### **1. Ativação Online/Offline**

- **Online**: Conecta ao servidor de licenças
- **Offline**: Usa arquivo de licença pré-gerado
- **Híbrida**: Sincroniza periodicamente

### **2. Controle por Hardware**

- **HD Serial**: Identificação do disco rígido
- **MAC Address**: Placa de rede
- **CPU ID**: Identificador do processador
- **Fingerprint**: Hash único da máquina

### **3. Modalidades de Licença**

```
DEMO:
├── 30 dias de uso
├── Até 10 usuários
├── Todas as funcionalidades
└── Watermark "VERSÃO DEMO"

FULL:
├── 1 ano de uso
├── Usuários conforme contrato
├── Todas as funcionalidades
└── Suporte incluso

ENTERPRISE:
├── Perpétua ou anual
├── Usuários ilimitados
├── Módulos premium (SSO, SCIM)
└── Suporte prioritário
```

## **📞 Para Ativação Real:**

**Recomendo entrar em contato diretamente com:**

- **Desenvolvedor/Fornecedor** do SecWinDev
- **Suporte técnico** oficial
- **Canal de vendas** autorizado

**Informações necessárias:**

- Serial da licença adquirida
- Dados da empresa
- Hardware ID da máquina
- Tipo de licença desejada (Demo/Full)

## **⚠️ Observação Importante:**

Como não tenho acesso ao **sistema real de licenciamento** do SecWinDev, não posso realizar ativações. O sistema mostrado nos documentos é a **funcionalidade técnica** (controle de acesso, políticas, etc.), mas o **licenciamento** seria um módulo separado.

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,618 messages
Posted on September, 08 2025 - 2:23 AM
// ========== WIN_Policy_WhatIf.events.wl ==========
// Botão "Simular Acesso" (BTN_Simular)
// Campos de entrada: EDT_UserUUID, EDT_TargetUUID, CB_TargetType (window/page/report/object)
// Editor multiline: MLE_Delta (JSON opcional), MLE_Result

PROCEDURE BTN_Simular()
userUUID is string = EDT_UserUUID
alvoUUID is string = EDT_TargetUUID
alvoTipo is string = CB_TargetType..Value // "objeto","janela","relatorio","menu","menu_item"
delta is string = MLE_Delta

json is string = gSec.Policy_Simulate(userUUID, alvoTipo, alvoUUID, delta)
MLE_Result = json

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,618 messages
Posted on September, 08 2025 - 2:25 AM
-- =====================================================================
-- SecWinDev v6 — Policy-as-Code + What-if + Explain (r16)
-- Requer: FULL INSTALL v6 + RLS ALL (opcional)
-- =====================================================================

-- 1) Snapshot atual das permissões (RBAC) -> JSON
CREATE OR REPLACE FUNCTION secwin_policy_snapshot_current()
RETURNS jsonb
LANGUAGE plpgsql AS $$
DECLARE j jsonb;
BEGIN
j := jsonb_build_object(
'usuarios_x_janelas', (SELECT coalesce(jsonb_agg(to_jsonb(t)), '[]'::jsonb) FROM (SELECT * FROM secwin_usuarios_x_janelas) t),
'usuarios_x_relatorios', (SELECT coalesce(jsonb_agg(to_jsonb(t)), '[]'::jsonb) FROM (SELECT * FROM secwin_usuarios_x_relatorios) t),
'usuarios_x_objetos', (SELECT coalesce(jsonb_agg(to_jsonb(t)), '[]'::jsonb) FROM (SELECT * FROM secwin_usuarios_x_objetos) t),
'usuarios_x_menus', (SELECT coalesce(jsonb_agg(to_jsonb(t)), '[]'::jsonb) FROM (SELECT * FROM secwin_usuarios_x_menus) t),
'usuarios_x_menu_itens', (SELECT coalesce(jsonb_agg(to_jsonb(t)), '[]'::jsonb) FROM (SELECT * FROM secwin_usuarios_x_menu_itens) t),
'perfis_x_janelas', (SELECT coalesce(jsonb_agg(to_jsonb(t)), '[]'::jsonb) FROM (SELECT * FROM secwin_perfis_x_janelas) t),
'perfis_x_relatorios', (SELECT coalesce(jsonb_agg(to_jsonb(t)), '[]'::jsonb) FROM (SELECT * FROM secwin_perfis_x_relatorios) t),
'perfis_x_objetos', (SELECT coalesce(jsonb_agg(to_jsonb(t)), '[]'::jsonb) FROM (SELECT * FROM secwin_perfis_x_objetos) t),
'perfis_x_menus', (SELECT coalesce(jsonb_agg(to_jsonb(t)), '[]'::jsonb) FROM (SELECT * FROM secwin_perfis_x_menus) t),
'perfis_x_menu_itens', (SELECT coalesce(jsonb_agg(to_jsonb(t)), '[]'::jsonb) FROM (SELECT * FROM secwin_perfis_x_menu_itens) t)
);
RETURN j;
END$$;

-- 2) Tabelas de "policy as code" (staging + approvals + commits)
CREATE TABLE IF NOT EXISTS secwin_policy_stage(
id bigserial PRIMARY KEY,
created_at timestamptz NOT NULL DEFAULT now(),
created_by uuid REFERENCES secwin_usuarios(uuid),
note text,
risk_level text, -- low/medium/high/critical
snapshot jsonb NOT NULL,
status text NOT NULL DEFAULT 'staged' CHECK (status IN ('staged','approved','applied','rejected','canceled')),
approver1_uuid uuid REFERENCES secwin_usuarios(uuid),
approver1_at timestamptz,
approver2_uuid uuid REFERENCES secwin_usuarios(uuid),
approver2_at timestamptz
);

-- 3) Criar um stage a partir de JSON (ou do snapshot atual)
CREATE OR REPLACE FUNCTION secwin_policy_stage_create(_snapshot jsonb, _note text, _risk text, _by uuid)
RETURNS bigint
LANGUAGE sql AS $$
INSERT INTO secwin_policy_stage(snapshot, note, risk_level, created_by)
VALUES (_snapshot, _note, _risk, _by)
RETURNING id;
$$;

CREATE OR REPLACE FUNCTION secwin_policy_stage_from_current(_note text, _risk text, _by uuid)
RETURNS bigint
LANGUAGE sql AS $$
INSERT INTO secwin_policy_stage(snapshot, note, risk_level, created_by)
VALUES (secwin_policy_snapshot_current(), _note, _risk, _by)
RETURNING id;
$$;

-- 4) Aprovação dupla (two-person rule)
CREATE OR REPLACE FUNCTION secwin_policy_approve(_stage_id bigint, _approver uuid)
RETURNS text
LANGUAGE plpgsql AS $$
DECLARE r secwin_policy_stage%ROWTYPE;
BEGIN
SELECT * INTO r FROM secwin_policy_stage WHERE id=_stage_id FOR UPDATE;
IF NOT FOUND THEN RETURN 'not_found'; END IF;
IF r.status <> 'staged' THEN RETURN 'invalid_status'; END IF;
IF r.created_by = _approver THEN RETURN 'cannot_self_approve'; END IF;

IF r.approver1_uuid IS NULL THEN
UPDATE secwin_policy_stage SET approver1_uuid=_approver, approver1_at=now() WHERE id=_stage_id;
RETURN 'approved_1of2';
ELSIF r.approver1_uuid <> _approver AND r.approver2_uuid IS NULL THEN
UPDATE secwin_policy_stage SET approver2_uuid=_approver, approver2_at=now(), status='approved' WHERE id=_stage_id;
RETURN 'approved_2of2';
ELSE
RETURN 'already_approved';
END IF;
END$$;

-- 5) Funções de UPSERT/DELETE para aplicar mudanças (usadas abaixo)
-- Usuário x * (delete helpers)
CREATE OR REPLACE FUNCTION secwin_del_user_window(_user uuid, _jan uuid) RETURNS void
LANGUAGE sql SECURITY DEFINER SET search_path=public AS $$
DELETE FROM secwin_usuarios_x_janelas WHERE usuario_uuid=_user AND janela_uuid=_jan;
$$;
CREATE OR REPLACE FUNCTION secwin_del_user_report(_user uuid, _rep uuid) RETURNS void
LANGUAGE sql SECURITY DEFINER SET search_path=public AS $$
DELETE FROM secwin_usuarios_x_relatorios WHERE usuario_uuid=_user AND relatorio_uuid=_rep;
$$;
CREATE OR REPLACE FUNCTION secwin_del_user_object(_user uuid, _obj uuid) RETURNS void
LANGUAGE sql SECURITY DEFINER SET search_path=public AS $$
DELETE FROM secwin_usuarios_x_objetos WHERE usuario_uuid=_user AND objeto_uuid=_obj;
$$;
CREATE OR REPLACE FUNCTION secwin_del_user_menu(_user uuid, _menu uuid) RETURNS void
LANGUAGE sql SECURITY DEFINER SET search_path=public AS $$
DELETE FROM secwin_usuarios_x_menus WHERE usuario_uuid=_user AND menu_uuid=_menu;
$$;
CREATE OR REPLACE FUNCTION secwin_del_user_menu_item(_user uuid, _item uuid) RETURNS void
LANGUAGE sql SECURITY DEFINER SET search_path=public AS $$
DELETE FROM secwin_usuarios_x_menu_itens WHERE usuario_uuid=_user AND menu_item_uuid=_item;
$$;

-- Perfil x * (delete helpers)
CREATE OR REPLACE FUNCTION secwin_del_profile_window(_perfil uuid, _jan uuid) RETURNS void
LANGUAGE sql SECURITY DEFINER SET search_path=public AS $$
DELETE FROM secwin_perfis_x_janelas WHERE perfil_uuid=_perfil AND janela_uuid=_jan;
$$;
CREATE OR REPLACE FUNCTION secwin_del_profile_report(_perfil uuid, _rep uuid) RETURNS void
LANGUAGE sql SECURITY DEFINER SET search_path=public AS $$
DELETE FROM secwin_perfis_x_relatorios WHERE perfil_uuid=_perfil AND relatorio_uuid=_rep;
$$;
CREATE OR REPLACE FUNCTION secwin_del_profile_object(_perfil uuid, _obj uuid) RETURNS void
LANGUAGE sql SECURITY DEFINER SET search_path=public AS $$
DELETE FROM secwin_perfis_x_objetos WHERE perfil_uuid=_perfil AND objeto_uuid=_obj;
$$;
CREATE OR REPLACE FUNCTION secwin_del_profile_menu(_perfil uuid, _menu uuid) RETURNS void
LANGUAGE sql SECURITY DEFINER SET search_path=public AS $$
DELETE FROM secwin_perfis_x_menus WHERE perfil_uuid=_perfil AND menu_uuid=_menu;
$$;
CREATE OR REPLACE FUNCTION secwin_del_profile_menu_item(_perfil uuid, _item uuid) RETURNS void
LANGUAGE sql SECURITY DEFINER SET search_path=public AS $$
DELETE FROM secwin_perfis_x_menu_itens WHERE perfil_uuid=_perfil AND menu_item_uuid=_item;
$$;

-- 6) Aplicar stage (somente itens presentes no JSON são upsertados/deletados sob sinalizador)
-- _mode: 'upsert_only' | 'upsert_and_delete'
CREATE OR REPLACE FUNCTION secwin_policy_apply(_stage_id bigint, _applied_by uuid, _mode text DEFAULT 'upsert_only')
RETURNS text
LANGUAGE plpgsql SECURITY DEFINER SET search_path=public AS $$
DECLARE r secwin_policy_stage%ROWTYPE;
DECLARE j jsonb;
DECLARE rec jsonb;
BEGIN
SELECT * INTO r FROM secwin_policy_stage WHERE id=_stage_id FOR UPDATE;
IF NOT FOUND THEN RETURN 'not_found'; END IF;
IF r.status <> 'approved' THEN RETURN 'need_two_approvals'; END IF;
j := r.snapshot;

-- USUÁRIO x JANELA
FOR rec IN SELECT * FROM jsonb_array_elements(COALESCE(j->'usuarios_x_janelas','[]'::jsonb)) LOOP
PERFORM secwin_set_user_window((rec->>'usuario_uuid')::uuid,(rec->>'janela_uuid')::uuid,(rec->>'estado')::secwin_state);
END LOOP;
IF _mode='upsert_and_delete' THEN
DELETE FROM secwin_usuarios_x_janelas t
WHERE NOT EXISTS (
SELECT 1 FROM jsonb_array_elements(COALESCE(j->'usuarios_x_janelas','[]'::jsonb)) e
WHERE (e->>'usuario_uuid')::uuid=t.usuario_uuid AND (e->>'janela_uuid')::uuid=t.janela_uuid
);
END IF;

-- USUÁRIO x RELATÓRIO
FOR rec IN SELECT * FROM jsonb_array_elements(COALESCE(j->'usuarios_x_relatorios','[]'::jsonb)) LOOP
PERFORM secwin_set_user_report((rec->>'usuario_uuid')::uuid,(rec->>'relatorio_uuid')::uuid,(rec->>'estado')::secwin_state);
END LOOP;
IF _mode='upsert_and_delete' THEN
DELETE FROM secwin_usuarios_x_relatorios t
WHERE NOT EXISTS (
SELECT 1 FROM jsonb_array_elements(COALESCE(j->'usuarios_x_relatorios','[]'::jsonb)) e
WHERE (e->>'usuario_uuid')::uuid=t.usuario_uuid AND (e->>'relatorio_uuid')::uuid=t.relatorio_uuid
);
END IF;

-- USUÁRIO x OBJETO
FOR rec IN SELECT * FROM jsonb_array_elements(COALESCE(j->'usuarios_x_objetos','[]'::jsonb)) LOOP
PERFORM secwin_set_user_object((rec->>'usuario_uuid')::uuid,(rec->>'objeto_uuid')::uuid,(rec->>'estado')::secwin_state);
END LOOP;
IF _mode='upsert_and_delete' THEN
DELETE FROM secwin_usuarios_x_objetos t
WHERE NOT EXISTS (
SELECT 1 FROM jsonb_array_elements(COALESCE(j->'usuarios_x_objetos','[]'::jsonb)) e
WHERE (e->>'usuario_uuid')::uuid=t.usuario_uuid AND (e->>'objeto_uuid')::uuid=t.objeto_uuid
);
END IF;

-- USUÁRIO x MENU
FOR rec IN SELECT * FROM jsonb_array_elements(COALESCE(j->'usuarios_x_menus','[]'::jsonb)) LOOP
PERFORM secwin_set_user_menu((rec->>'usuario_uuid')::uuid,(rec->>'menu_uuid')::uuid,(rec->>'estado')::secwin_state);
END LOOP;
IF _mode='upsert_and_delete' THEN
DELETE FROM secwin_usuarios_x_menus t
WHERE NOT EXISTS (
SELECT 1 FROM jsonb_array_elements(COALESCE(j->'usuarios_x_menus','[]'::jsonb)) e
WHERE (e->>'usuario_uuid')::uuid=t.usuario_uuid AND (e->>'menu_uuid')::uuid=t.menu_uuid
);
END IF;

-- USUÁRIO x MENU_ITEM
FOR rec IN SELECT * FROM jsonb_array_elements(COALESCE(j->'usuarios_x_menu_itens','[]'::jsonb)) LOOP
PERFORM secwin_set_user_menu_item((rec->>'usuario_uuid')::uuid,(rec->>'menu_item_uuid')::uuid,(rec->>'estado')::secwin_state);
END LOOP;
IF _mode='upsert_and_delete' THEN
DELETE FROM secwin_usuarios_x_menu_itens t
WHERE NOT EXISTS (
SELECT 1 FROM jsonb_array_elements(COALESCE(j->'usuarios_x_menu_itens','[]'::jsonb)) e
WHERE (e->>'usuario_uuid')::uuid=t.usuario_uuid AND (e->>'menu_item_uuid')::uuid=t.menu_item_uuid
);
END IF;

-- PERFIL x *
FOR rec IN SELECT * FROM jsonb_array_elements(COALESCE(j->'perfis_x_janelas','[]'::jsonb)) LOOP
PERFORM secwin_set_profile_window((rec->>'perfil_uuid')::uuid,(rec->>'janela_uuid')::uuid,(rec->>'estado')::secwin_state);
END LOOP;
IF _mode='upsert_and_delete' THEN
DELETE FROM secwin_perfis_x_janelas t
WHERE NOT EXISTS (
SELECT 1 FROM jsonb_array_elements(COALESCE(j->'perfis_x_janelas','[]'::jsonb)) e
WHERE (e->>'perfil_uuid')::uuid=t.perfil_uuid AND (e->>'janela_uuid')::uuid=t.janela_uuid
);
END IF;

FOR rec IN SELECT * FROM jsonb_array_elements(COALESCE(j->'perfis_x_relatorios','[]'::jsonb)) LOOP
PERFORM secwin_set_profile_report((rec->>'perfil_uuid')::uuid,(rec->>'relatorio_uuid')::uuid,(rec->>'estado')::secwin_state);
END LOOP;
IF _mode='upsert_and_delete' THEN
DELETE FROM secwin_perfis_x_relatorios t
WHERE NOT EXISTS (
SELECT 1 FROM jsonb_array_elements(COALESCE(j->'perfis_x_relatorios','[]'::jsonb)) e
WHERE (e->>'perfil_uuid')::uuid=t.perfil_uuid AND (e->>'relatorio_uuid')::uuid=t.relatorio_uuid
);
END IF;

FOR rec IN SELECT * FROM jsonb_array_elements(COALESCE(j->'perfis_x_objetos','[]'::jsonb)) LOOP
PERFORM secwin_set_profile_object((rec->>'perfil_uuid')::uuid,(rec->>'objeto_uuid')::uuid,(rec->>'estado')::secwin_state);
END LOOP;
IF _mode='upsert_and_delete' THEN
DELETE FROM secwin_perfis_x_objetos t
WHERE NOT EXISTS (
SELECT 1 FROM jsonb_array_elements(COALESCE(j->'perfis_x_objetos','[]'::jsonb)) e
WHERE (e->>'perfil_uuid')::uuid=t.perfil_uuid AND (e->>'objeto_uuid')::uuid=t.objeto_uuid
);
END IF;

FOR rec IN SELECT * FROM jsonb_array_elements(COALESCE(j->'perfis_x_menus','[]'::jsonb)) LOOP
PERFORM secwin_set_profile_menu((rec->>'perfil_uuid')::uuid,(rec->>'menu_uuid')::uuid,(rec->>'estado')::secwin_state);
END LOOP;
IF _mode='upsert_and_delete' THEN
DELETE FROM secwin_perfis_x_menus t
WHERE NOT EXISTS (
SELECT 1 FROM jsonb_array_elements(COALESCE(j->'perfis_x_menus','[]'::jsonb)) e
WHERE (e->>'perfil_uuid')::uuid=t.perfil_uuid AND (e->>'menu_uuid')::uuid=t.menu_uuid
);
END IF;

FOR rec IN SELECT * FROM jsonb_array_elements(COALESCE(j->'perfis_x_menu_itens','[]'::jsonb)) LOOP
PERFORM secwin_set_profile_menu_item((rec->>'perfil_uuid')::uuid,(rec->>'menu_item_uuid')::uuid,(rec->>'estado')::secwin_state);
END LOOP;
IF _mode='upsert_and_delete' THEN
DELETE FROM secwin_perfis_x_menu_itens t
WHERE NOT EXISTS (
SELECT 1 FROM jsonb_array_elements(COALESCE(j->'perfis_x_menu_itens','[]'::jsonb)) e
WHERE (e->>'perfil_uuid')::uuid=t.perfil_uuid AND (e->>'menu_item_uuid')::uuid=t.menu_item_uuid
);
END IF;

UPDATE secwin_policy_stage SET status='applied' WHERE id=_stage_id;
INSERT INTO secwin_policy_versions(created_at, created_by, note, snapshot)
VALUES (now(), _applied_by, concat('apply stage #',_stage_id), j);

RETURN 'applied';
END$$;

-- 7) What-if & Explain
CREATE TABLE IF NOT EXISTS secwin_policy_explain_log(
id bigserial primary key,
ts timestamptz not null default now(),
usuario_uuid uuid,
alvo_tipo secwin_alvo_tipo,
alvo_uuid uuid,
result boolean,
reason jsonb
);

-- Explicação (usa RBAC efetivo + ABAC hook)
CREATE OR REPLACE FUNCTION secwin_policy_explain(_user uuid, _alvo secwin_alvo_tipo, _alvo_uuid uuid)
RETURNS jsonb
LANGUAGE plpgsql AS $$
DECLARE rbac_state secwin_state;
DECLARE abac_dec boolean;
DECLARE final boolean;
DECLARE reason jsonb := '{}'::jsonb;
BEGIN
-- RBAC efetivo: procura em usuarios_x_* e perfis_x_* (simplificado para objetos; estenda para outros)
IF _alvo='objeto' THEN
SELECT estado INTO rbac_state
FROM vw_secwin_permissoes_efetivas
WHERE usuario_uuid=_user AND alvo_tipo='objeto' AND alvo_uuid=_alvo_uuid
LIMIT 1;
ELSE
rbac_state := 'default';
END IF;

abac_dec := secwin_eval_rules(_user, _alvo, _alvo_uuid);
final := CASE
WHEN rbac_state IN ('forbidden','invisible') THEN false
WHEN rbac_state IN ('allowed','grayed') THEN COALESCE(abac_dec, true)
ELSE COALESCE(abac_dec, true)
END;

reason := jsonb_build_object(
'rbac_state', coalesce(rbac_state::text,'default'),
'abac_decision', abac_dec,
'final', final
);

INSERT INTO secwin_policy_explain_log(usuario_uuid, alvo_tipo, alvo_uuid, result, reason)
VALUES (_user, _alvo, _alvo_uuid, final, reason);

RETURN reason;
END$$;

-- What-if: aplica um "delta" RBAC na memória (JSON) e reavalia
-- O delta aceita as mesmas chaves do snapshot, mas só os itens que quer sobrepor.
CREATE OR REPLACE FUNCTION secwin_policy_simulate(_user uuid, _alvo secwin_alvo_tipo, _alvo_uuid uuid, _delta jsonb DEFAULT '{}'::jsonb)
RETURNS jsonb
LANGUAGE plpgsql AS $$
DECLARE base jsonb := secwin_policy_snapshot_current();
DECLARE merged jsonb := base;
DECLARE reason jsonb;
BEGIN
-- Merge raso por chave (arrays do delta substituem as arrays do base)
merged := jsonb_strip_nulls(base || _delta);

-- Heurística: injeta temporariamente os pares do delta nas tabelas *temporárias* e usa explain()
-- Para simplicidade, devolvemos o JSON de delta + explain (sem aplicar no BD).
reason := jsonb_build_object('note','simulation only','delta',_delta) || secwin_policy_explain(_user,_alvo,_alvo_uuid);

RETURN reason;
END$$;

-- 8) Views para dashboard de políticas
CREATE OR REPLACE VIEW vw_secwin_policy_activity AS
SELECT id, created_at, created_by, note, risk_level, status, approver1_uuid, approver1_at, approver2_uuid, approver2_at
FROM secwin_policy_stage;

CREATE OR REPLACE VIEW vw_secwin_policy_pending AS
SELECT * FROM secwin_policy_stage WHERE status='staged';

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,618 messages
Posted on September, 08 2025 - 2:26 AM
-- =====================================================================
-- SecWinDev v6 — DLP / Controle de Exportação (r16)
-- =====================================================================

CREATE TABLE IF NOT EXISTS secwin_classifications(
id serial primary key,
name text unique not null,
level int not null check (level between 0 and 100) -- 0 público, 100 ultra confidencial
);

CREATE TABLE IF NOT EXISTS secwin_object_tags(
alvo_tipo secwin_alvo_tipo not null,
alvo_uuid uuid not null,
classification_id int not null references secwin_classifications(id),
tag text,
primary key(alvo_tipo, alvo_uuid, classification_id)
);

CREATE TABLE IF NOT EXISTS secwin_export_policies(
id serial primary key,
classification_id int not null references secwin_classifications(id),
allowed_formats text[] not null default array['PDF','CSV','XLSX'],
max_rows_per_day int default 10000,
watermark_template text default 'User={{login}} • {{now}} • IP={{ip}}',
mask_strategy text default 'none', -- none|partial|full
requires_mfa boolean default true,
requires_business_hours boolean default false
);

CREATE TABLE IF NOT EXISTS secwin_export_user_quota(
usuario_uuid uuid not null references secwin_usuarios(uuid) on delete cascade,
classification_id int not null references secwin_classifications(id),
day date not null,
used_rows int not null default 0,
primary key(usuario_uuid, classification_id, day)
);

CREATE TABLE IF NOT EXISTS secwin_export_events(
id bigserial primary key,
ts timestamptz not null default now(),
usuario_uuid uuid references secwin_usuarios(uuid),
classification_id int references secwin_classifications(id),
format text,
rows_count int,
context jsonb, -- window/report/object info
decision text, -- allowed|denied
reason text,
watermark text
);

-- Helpers
CREATE OR REPLACE FUNCTION secwin_dlp_get_classification(_alvo secwin_alvo_tipo, _uuid uuid)
RETURNS int
LANGUAGE sql AS $$
SELECT classification_id FROM secwin_object_tags
WHERE alvo_tipo=_alvo AND alvo_uuid=_uuid
ORDER BY classification_id DESC LIMIT 1;
$$;

CREATE OR REPLACE FUNCTION secwin_export_can(
_usuario uuid,
_alvo secwin_alvo_tipo,
_alvo_uuid uuid,
_format text,
_rows int
) RETURNS TABLE(allowed boolean, message text, watermark text)
LANGUAGE plpgsql AS $$
DECLARE c_id int;
DECLARE pol secwin_export_policies%ROWTYPE;
DECLARE ip inet; v_country text;
DECLARE bh record; v_dow int := extract(dow from now());
DECLARE login_txt text;
BEGIN
SELECT u.login INTO login_txt FROM secwin_usuarios u WHERE u.uuid=_usuario;
c_id := secwin_dlp_get_classification(_alvo, _alvo_uuid);
IF c_id IS NULL THEN
RETURN QUERY SELECT true, 'no_classification_policy', NULL;
RETURN;
END IF;

SELECT * INTO pol FROM secwin_export_policies WHERE classification_id=c_id;
IF NOT FOUND THEN
RETURN QUERY SELECT true, 'no_policy_for_class', NULL;
RETURN;
END IF;

-- formato
IF NOT (_format = ANY (pol.allowed_formats)) THEN
RETURN QUERY SELECT false, 'format_not_allowed', NULL;
RETURN;
END IF;

-- mfa
IF pol.requires_mfa THEN
PERFORM 1 FROM secwin_usuarios WHERE uuid=_usuario AND mfa_enrolled_at IS NOT NULL;
IF NOT FOUND THEN
RETURN QUERY SELECT false, 'mfa_required', NULL;
RETURN;
END IF;
END IF;

-- horário comercial
IF pol.requires_business_hours THEN
SELECT * INTO bh FROM secwin_abac_business_hours WHERE dow=v_dow;
IF NOT FOUND OR now()::time < bh.start_time OR now()::time > bh.end_time THEN
RETURN QUERY SELECT false, 'outside_business_hours', NULL;
RETURN;
END IF;
END IF;

-- quota
PERFORM 1
FROM secwin_export_user_quota
WHERE usuario_uuid=_usuario AND classification_id=c_id AND day=current_date AND used_rows + _rows <= pol.max_rows_per_day;
IF NOT FOUND THEN
RETURN QUERY SELECT false, 'quota_exceeded', NULL;
RETURN;
END IF;

-- watermark
SELECT s.ip INTO ip FROM secwin_sessions s WHERE s.usuario_uuid=_usuario ORDER BY created_at DESC LIMIT 1;
RETURN QUERY SELECT true,
'allowed',
replace(replace(replace(pol.watermark_template,'{{login}}',coalesce(login_txt,'')),'{{now}}',now()::text),'{{ip}}',coalesce(ip::text,''));

END$$;

CREATE OR REPLACE FUNCTION secwin_export_register(
_usuario uuid,
_alvo secwin_alvo_tipo,
_alvo_uuid uuid,
_format text,
_rows int,
_context jsonb,
_decision text,
_reason text,
_watermark text
) RETURNS void
LANGUAGE plpgsql AS $$
DECLARE c_id int := secwin_dlp_get_classification(_alvo, _alvo_uuid);
BEGIN
INSERT INTO secwin_export_events(usuario_uuid, classification_id, format, rows_count, context, decision, reason, watermark)
VALUES (_usuario, c_id, _format, _rows, _context, _decision, _reason, _watermark);

IF _decision='allowed' AND c_id IS NOT NULL THEN
INSERT INTO secwin_export_user_quota(usuario_uuid, classification_id, day, used_rows)
VALUES (_usuario, c_id, current_date, _rows)
ON CONFLICT (usuario_uuid, classification_id, day)
DO UPDATE SET used_rows = secwin_export_user_quota.used_rows + EXCLUDED.used_rows;
END IF;
END$$;

-- Views para dashboard
CREATE OR REPLACE VIEW vw_secwin_export_activity_7d AS
SELECT date_trunc('hour', ts) as ts_hour,
count(*) as total,
count(*) filter (where decision='allowed') as allowed,
count(*) filter (where decision='denied') as denied
FROM secwin_export_events
WHERE ts > now() - interval '7 days'
GROUP BY 1 ORDER BY 1;

CREATE OR REPLACE VIEW vw_secwin_export_top_users_7d AS
SELECT u.login, count(*) filter (where e.decision='allowed') as ok, count(*) filter (where e.decision='denied') as deny
FROM secwin_export_events e
LEFT JOIN secwin_usuarios u ON u.uuid=e.usuario_uuid
WHERE ts > now() - interval '7 days'
GROUP BY u.login ORDER BY ok desc;

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,618 messages
Posted on September, 08 2025 - 2:26 AM
# SecWinDev v6 — GO ALL (r16)
Inclui **Policy-as-Code** (stage/approve/apply + what-if/explain) e **DLP/Export** (políticas por classificação, quota diária, watermark). Também entrega **dashboard & alertas** de políticas/exportações e **patch WL** da classe.

## 1) Policy-as-Code
### SQL
- `secwin_policy_snapshot_current()` → JSON com todas as permissões RBAC.
- `secwin_policy_stage_create(snapshot, note, risk, by)` → cria um stage.
- `secwin_policy_approve(stage_id, approver)` → **two-person rule**.
- `secwin_policy_apply(stage_id, applied_by, mode)` → aplica (`upsert_only` ou `upsert_and_delete`).
- `secwin_policy_explain(user, alvo_tipo, alvo_uuid)` → JSON com `rbac_state`, `abac_decision`, `final`.
- `secwin_policy_simulate(user, alvo_tipo, alvo_uuid, delta)` → what-if (não altera BD).
- Views: `vw_secwin_policy_activity`, `vw_secwin_policy_pending`.

> **Atenção**: `upsert_and_delete` remove vínculos **não listados** no snapshot. Use com cautela.

### Classe (WL) – patch pronto
- `Policy_Snapshot()`
- `Policy_StageCreate(snapshotJSON, note, riskLevel, byUUID)`
- `Policy_Approve(stageId, approverUUID)`
- `Policy_Apply(stageId, appliedByUUID, mode)`
- `Policy_Explain(userUUID, alvoTipo, alvoUUID)`
- `Policy_Simulate(userUUID, alvoTipo, alvoUUID, deltaJSON)`

Exemplo WinDev: `WIN_Policy_WhatIf.events.wl`

## 2) DLP / Export
### SQL
- Tabelas: `secwin_classifications`, `secwin_object_tags`, `secwin_export_policies`, `secwin_export_user_quota`, `secwin_export_events`.
- Funções:
- `secwin_export_can(user, alvo_tipo, alvo_uuid, format, rows)` → `allowed/message/watermark`.
- `secwin_export_register(user, alvo_tipo, alvo_uuid, format, rows, context, decision, reason, watermark)`.
- Views: `vw_secwin_export_activity_7d`, `vw_secwin_export_top_users_7d`.

### Classe (WL) – patch pronto
- `Export_Check(userUUID, alvoTipo, alvoUUID, format, rows)` → JSON.
- `Export_Register(userUUID, alvoTipo, alvoUUID, format, rows, contextJSON, decision, reason, watermark)`.

Exemplo WebDev: `PAGE_Export_Check.events.wl`

## 3) Dashboard & alertas (Policies & DLP)
- **Grafana**: `Grafana_SecWinDev_Policies.json`
- **Alerting**: `Grafana_Alerts_Policies_DLP.yaml`
- Stage pendente > 24h (severity: high)
- Pico de export denied (15m) (severity: medium)

## 4) Passos de instalação
```bash
psql -h <host> -U <user> -d <db> -f secwindev_v6_policy_as_code_r16.sql
psql -h <host> -U <user> -d <db> -f secwindev_v6_DLP_export_r16.sql
```
- Importe o dashboard e os alertas no Grafana (troque `YOUR_POSTGRES_UID`).
- Aplique o **patch WL** no final da sua classe `clsSecWinDev`.

## 5) Notas & próximos
- Preencha `secwin_classifications` (ex.: Público, Interno, Confidencial, Restrito).
- Relacione objetos/relatórios sensíveis em `secwin_object_tags`.
- Configure `secwin_export_policies` (formatos, quotas, watermark).
- Integre seu fluxo de **exportação** chamando `Export_Check` → se `allowed`, gere o arquivo e chame `Export_Register`.
- Para SSO/SCIM/IdP: posso gerar os conectores OIDC/SAML/SCIM.

Dúvidas? Só chamar. Posso também criar **telas WL** de *Diff/Aprovação* e *Console de Exportações* com 1 clique.

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,618 messages
Posted on September, 08 2025 - 2:27 AM
// ========== PAGE_Export_Check.events.wl ==========
// Campos: EDT_UserUUID, EDT_TargetUUID, CB_TargetType, CB_Format, EDT_Rows, BTN_Check, BTN_Export
// Área de saída: STC_Result

PROCEDURE BTN_Check_Click()
userUUID is string = EDT_UserUUID
alvoUUID is string = EDT_TargetUUID
alvoTipo is string = CB_TargetType..Value
fmt is string = CB_Format..Value // "PDF","CSV","XLSX"
rows is int = EDT_Rows

res is string = gSec.Export_Check(userUUID, alvoTipo, alvoUUID, fmt, rows)
STC_Result = res

// Decodifique JSON conforme necessário para bloquear UI/mostrar watermark antes da exportação

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,618 messages
Posted on September, 08 2025 - 2:28 AM
{
"title": "SecWinDev \u2022 Pol\u00edticas & Mudan\u00e7as",
"tags": [
"SecWinDev",
"Policies"
],
"timezone": "browser",
"schemaVersion": 38,
"refresh": "30s",
"time": {
"from": "now-30d",
"to": "now"
},
"panels": [
{
"type": "stat",
"title": "Stages pendentes",
"gridPos": {
"h": 4,
"w": 6,
"x": 0,
"y": 0
},
"datasource": {
"type": "postgres",
"uid": "YOUR_POSTGRES_UID"
},
"targets": [
{
"refId": "A",
"format": "table",
"rawSql": "select count(*) as pending from vw_secwin_policy_pending;"
}
]
},
{
"type": "stat",
"title": "Aplicadas (24h)",
"gridPos": {
"h": 4,
"w": 6,
"x": 6,
"y": 0
},
"datasource": {
"type": "postgres",
"uid": "YOUR_POSTGRES_UID"
},
"targets": [
{
"refId": "A",
"format": "table",
"rawSql": "select count(*) as applied_24h from secwin_policy_stage where status='applied' and created_at>now()-interval '24 hour';"
}
]
},
{
"type": "stat",
"title": "Export denies (24h)",
"gridPos": {
"h": 4,
"w": 6,
"x": 12,
"y": 0
},
"datasource": {
"type": "postgres",
"uid": "YOUR_POSTGRES_UID"
},
"targets": [
{
"refId": "A",
"format": "table",
"rawSql": "select count(*) from secwin_export_events where decision='denied' and ts>now()-interval '24 hour';"
}
]
},
{
"type": "timeseries",
"title": "Stages por status",
"gridPos": {
"h": 10,
"w": 24,
"x": 0,
"y": 4
},
"datasource": {
"type": "postgres",
"uid": "YOUR_POSTGRES_UID"
},
"targets": [
{
"refId": "A",
"format": "time_series",
"rawSql": "\nselect $__timeGroup(created_at, $__interval) as time,\n count(*) filter (where status='staged') as staged,\n count(*) filter (where status='approved') as approved,\n count(*) filter (where status='applied') as applied\nfrom secwin_policy_stage\nwhere $__timeFilter(created_at)\ngroup by 1 order by 1;\n"
}
]
},
{
"type": "timeseries",
"title": "Exporta\u00e7\u00f5es (allowed vs denied)",
"gridPos": {
"h": 10,
"w": 24,
"x": 0,
"y": 14
},
"datasource": {
"type": "postgres",
"uid": "YOUR_POSTGRES_UID"
},
"targets": [
{
"refId": "A",
"format": "time_series",
"rawSql": "select ts_hour as time, allowed, denied from vw_secwin_export_activity_7d;"
}
]
},
{
"type": "table",
"title": "\u00daltimas mudan\u00e7as de pol\u00edtica",
"gridPos": {
"h": 10,
"w": 24,
"x": 0,
"y": 24
},
"datasource": {
"type": "postgres",
"uid": "YOUR_POSTGRES_UID"
},
"targets": [
{
"refId": "A",
"format": "table",
"rawSql": "select id, created_at, created_by, note, risk_level, status, approver1_uuid, approver2_uuid from vw_secwin_policy_activity order by created_at desc limit 200;"
}
]
}
]
}

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,618 messages
Posted on September, 08 2025 - 2:29 AM
apiVersion: 1
groups:
- orgId: 1
name: SecWinDev Policy & DLP Alerts
folder: SecWinDev
interval: 1m
rules:
- uid: secwin_stage_pending_24h
title: Policy Stage Pending > 24h
condition: C
data:
- refId: A
datasourceUid: YOUR_POSTGRES_UID
relativeTimeRange: { from: 0, to: 0 }
model:
format: table
rawSql: |
select now() as time, count(*) as value
from secwin_policy_stage
where status='staged' and created_at < now() - interval '24 hour';
- refId: B
datasourceUid: __expr__
model: { type: reduce, reducer: last, expression: A }
- refId: C
datasourceUid: __expr__
model: { type: threshold, expression: B, threshold: 0, outside: true } # > 0
noDataState: NoData
execErrState: Error
for: 0s
labels: { severity: high }
annotations:
summary: "Existem policy stages pendentes há mais de 24 horas."

- uid: secwin_export_denied_spike_15m
title: Export Denied Spike (15m)
condition: C
data:
- refId: A
datasourceUid: YOUR_POSTGRES_UID
relativeTimeRange: { from: 900, to: 0 }
model:
format: table
rawSql: |
select now() as time, count(*) as value
from secwin_export_events
where decision='denied' and ts between $__timeFrom() and $__timeTo();
- refId: B
datasourceUid: __expr__
model: { type: reduce, reducer: last, expression: A }
- refId: C
datasourceUid: __expr__
model: { type: threshold, expression: B, threshold: 10, outside: true } # >10 negações em 15m
noDataState: NoData
execErrState: Error
for: 0s
labels: { severity: medium }
annotations:
summary: "Negativas de exportação acima do limite nos últimos 15 minutos."

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,618 messages
Posted on September, 08 2025 - 2:30 AM
// ===================== Policy-as-Code (WL) =====================

// Snapshot atual (JSON)
PROCEDURE Policy_Snapshot() : string
sSQL is string = "select secwin_policy_snapshot_current()::text"
RESULT _ExecScalar(sSQL)

// Criar Stage a partir de JSON
PROCEDURE Policy_StageCreate(snapshotJSON is string, note is string, riskLevel is string, byUUID is string) : int
sSQL is string = StringBuild("select secwin_policy_stage_create(%1,%2,%3,%4)",
_Q(snapshotJSON), _Q(note), _Q(riskLevel), _Q(byUUID))
RESULT _ExecScalarInt(sSQL)

// Aprovar (two-person rule)
PROCEDURE Policy_Approve(stageId is int, approverUUID is string) : string
sSQL is string = StringBuild("select secwin_policy_approve(%1,%2)", stageId, _Q(approverUUID))
RESULT _ExecScalar(sSQL)

// Aplicar stage (modo: 'upsert_only' ou 'upsert_and_delete')
PROCEDURE Policy_Apply(stageId is int, appliedByUUID is string, mode is string = "upsert_only") : string
sSQL is string = StringBuild("select secwin_policy_apply(%1,%2,%3)", stageId, _Q(appliedByUUID), _Q(mode))
RESULT _ExecScalar(sSQL)

// Explain/Simulate
PROCEDURE Policy_Explain(userUUID is string, alvoTipo is string, alvoUUID is string) : string
sSQL is string = StringBuild("select secwin_policy_explain(%1,%2,%3)::text", _Q(userUUID), _Q(alvoTipo), _Q(alvoUUID))
RESULT _ExecScalar(sSQL)

PROCEDURE Policy_Simulate(userUUID is string, alvoTipo is string, alvoUUID is string, deltaJSON is string) : string
IF deltaJSON = "" THEN deltaJSON = "{}"
sSQL is string = StringBuild("select secwin_policy_simulate(%1,%2,%3,%4)::text", _Q(userUUID), _Q(alvoTipo), _Q(alvoUUID), _Q(deltaJSON))
RESULT _ExecScalar(sSQL)

// ===================== DLP / Export =====================

// Validação antes de exportar (retorna JSON com allowed/message/watermark)
PROCEDURE Export_Check(userUUID is string, alvoTipo is string, alvoUUID is string, format is string, rows is int) : string
sSQL is string = StringBuild("select row_to_json(t)::text from (select * from secwin_export_can(%1,%2,%3,%4,%5)) t",
_Q(userUUID), _Q(alvoTipo), _Q(alvoUUID), _Q(format), rows)
RESULT _ExecScalar(sSQL)

// Registrar evento de exportação
PROCEDURE Export_Register(userUUID is string, alvoTipo is string, alvoUUID is string, format is string, rows is int, contextJSON is string, decision is string, reason is string, watermark is string) : boolean
sSQL is string = StringBuild("select secwin_export_register(%1,%2,%3,%4,%5,%6,%7,%8,%9)",
_Q(userUUID), _Q(alvoTipo), _Q(alvoUUID), _Q(format), rows, _Q(contextJSON), _Q(decision), _Q(reason), _Q(watermark))
RESULT _ExecNonQuery(sSQL)

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,618 messages
Posted on September, 08 2025 - 2:50 AM
show! gerei a versão do servidor REST já chamando diretamente os helpers da sua classe gSec.

Download
• WebDev REST (WLanguage) usando gSec._ExecScalar/_ExecNonQuery/_Q:
webdev_license_rest_gSec.wl

Como usar
1. Importe esse .wl no seu projeto WebDev REST.
2. Exponha as procedures como endpoints:
• REST_v1_auth → POST /v1/auth
• REST_v1_verify → GET /v1/verify
• REST_v1_lease_renew → POST /v1/lease/renew
• REST_v1_revoke → POST /v1/revoke
• REST_v1_heartbeat → POST /v1/heartbeat
3. Garanta que gSec (sua instância da classe) esteja inicializada e com conexão válida ao Postgres.
4. Publique em HTTPS.

Se quiser que eu adicione o bearer check via middleware ou inclua refresh-token endpoint, eu já deixo no arquivo também.

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,618 messages
Posted on September, 08 2025 - 2:51 AM
// ===================================================
// SecWinDev License Server (WebDev REST) — gSec helpers
// ===================================================
// Este arquivo usa diretamente os helpers da sua classe gSec:
// gSec._ExecScalar(), gSec._ExecNonQuery(), gSec._Q()
// Exponha as PROCEDUREs abaixo como endpoints REST no WebDev.

// Helpers ---------------------------------------------------------
PROCEDURE _BodyJSON() : string
buf is Buffer = HTTPRequest..Content
RESULT BufferToString(buf,charsetANSI) // ajuste para utf8 se necessário

PROCEDURE _IP() : string
RESULT RequestIPAddress()

PROCEDURE _UA() : string
RESULT BrowserUserAgent()

PROCEDURE _JsonOk(obj as string) : string
HTTPHeader["Content-Type"] = "application/json;charset=utf-8"
RESULT obj

PROCEDURE _JsonError(code is int, msg is string) : string
HTTPStatus(code)
RESULT _JsonOk([{"ok":false,"error":"] + msg + ["}])

// Endpoint: POST /v1/auth ----------------------------------------
PROCEDURE REST_v1_auth() : string
IF HTTPRequest.Method <> httpPost THEN RETURN _JsonError(405,"method_not_allowed")
body is string = _BodyJSON()
IF body = "" THEN RETURN _JsonError(400,"empty_body")

j is Variant = JSONToVariant(body)
IF j.license_key = Null OR j.device_id = Null THEN RETURN _JsonError(400,"missing_fields")

sSQL is string = [
with act as (
select * from secwin_license_activation_upsert(%1,%2,%3,%4,%5)
)
select secwin_license_issue_tokens(
(select license_uuid from act),
(select activation_uuid from act),
%4,%5
)::text
]
res is string = gSec._ExecScalar(StringBuild(sSQL, gSec._Q(j.license_key), gSec._Q(j.device_id), gSec._Q(j.hw_hash), gSec._Q(_IP()), gSec._Q(_UA())))
IF res = "" THEN RETURN _JsonError(403,"license_invalid_or_limit")
RETURN _JsonOk(res)

// Endpoint: GET /v1/verify ---------------------------------------
PROCEDURE REST_v1_verify() : string
tok is string = ExtractString(HTTPHeader["Authorization"],2," ")
IF tok = "" THEN RETURN _JsonError(401,"missing_token")
sSQL is string = [
select row_to_json(t)::text from (
select true as ok,
license_uuid, activation_uuid, expires_at
from secwin_license_verify_token(%1,'access')
) t
]
res is string = gSec._ExecScalar(StringBuild(sSQL, gSec._Q(tok)))
IF res = "" THEN RETURN _JsonError(401,"invalid_or_expired")
RETURN _JsonOk(res)

// Endpoint: POST /v1/lease/renew ---------------------------------
PROCEDURE REST_v1_lease_renew() : string
tok is string = ExtractString(HTTPHeader["Authorization"],2," ")
IF tok = "" THEN RETURN _JsonError(401,"missing_token")
sSQL is string = "select secwin_license_lease_renew(" + gSec._Q(tok) + "," + gSec._Q(_IP()) + "," + gSec._Q(_UA()) + ")::text"
res is string = gSec._ExecScalar(sSQL)
IF res = "" THEN RETURN _JsonError(401,"invalid_or_expired")
RETURN _JsonOk(res)

// Endpoint: POST /v1/revoke --------------------------------------
PROCEDURE REST_v1_revoke() : string
IF HTTPRequest.Method <> httpPost THEN RETURN _JsonError(405,"method_not_allowed")
body is string = _BodyJSON()
j is Variant = JSONToVariant(body)
IF j.token = Null THEN RETURN _JsonError(400,"missing_token")
sSQL is string = "select secwin_license_revoke_token(" + gSec._Q(j.token) + ")"
ok is boolean = (gSec._ExecScalar(sSQL) = "t") OR (gSec._ExecScalar(sSQL) = "1")
RETURN _JsonOk([{"ok":] + (ok ? "true" : "false") + ["}])

// Endpoint: POST /v1/heartbeat ----------------------------------
PROCEDURE REST_v1_heartbeat() : string
tok is string = ExtractString(HTTPHeader["Authorization"],2," ")
IF tok = "" THEN RETURN _JsonError(401,"missing_token")
sSQL is string = [
with x as (select * from secwin_license_verify_token(%1,'access'))
insert into secwin_license_audit(event, license_uuid, activation_uuid, details, ip, ua)
select 'heartbeat', license_uuid, activation_uuid, jsonb_build_object('note','hb'), %2, %3 from x
returning 'ok'::text
]
ok is string = gSec._ExecScalar(StringBuild(sSQL, gSec._Q(tok), gSec._Q(_IP()), gSec._Q(_UA())))
IF ok = "" THEN RETURN _JsonError(401,"invalid_or_expired")
RETURN _JsonOk([{"ok":true}])

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,618 messages
Posted on September, 08 2025 - 2:52 AM
# SecWinDev • License Server (REST)

## Instalação
1) Banco: `psql -f secwin_license_schema.sql`
2) WebDev: importe `webdev_license_rest.wl`, exponha as procedures REST, publique em HTTPS.
3) Teste com cURL (veja README anterior).

## Endpoints
- POST /v1/auth
- GET /v1/verify
- POST /v1/lease/renew
- POST /v1/revoke
- POST /v1/heartbeat

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,618 messages
Posted on September, 08 2025 - 2:53 AM
// SecWinDev License Server (WebDev REST) — baseline helpers

PROCEDURE _BodyJSON() : string
buf is Buffer = HTTPRequest..Content
RESULT BufferToString(buf,charsetANSI)

PROCEDURE _IP() : string
RESULT RequestIPAddress()

PROCEDURE _UA() : string
RESULT BrowserUserAgent()

PROCEDURE _JsonOk(obj as string) : string
HTTPHeader["Content-Type"] = "application/json;charset=utf-8"
RESULT obj

PROCEDURE _JsonError(code is int, msg is string) : string
HTTPStatus(code)
RESULT _JsonOk([{"ok":false,"error":"] + msg + ["}])

PROCEDURE REST_v1_auth() : string
IF HTTPRequest.Method <> httpPost THEN RETURN _JsonError(405,"method_not_allowed")
body is string = _BodyJSON()
IF body = "" THEN RETURN _JsonError(400,"empty_body")
j is Variant = JSONToVariant(body)
IF j.license_key = Null OR j.device_id = Null THEN RETURN _JsonError(400,"missing_fields")

sSQL is string = [
with act as (
select * from secwin_license_activation_upsert(%1,%2,%3,%4,%5)
)
select secwin_license_issue_tokens(
(select license_uuid from act),
(select activation_uuid from act),
%4,%5
)::text
]
res is string = _ExecScalar(StringBuild(sSQL, _Q(j.license_key), _Q(j.device_id), _Q(j.hw_hash), _Q(_IP()), _Q(_UA())))
IF res = "" THEN RETURN _JsonError(403,"license_invalid_or_limit")
RETURN _JsonOk(res)

PROCEDURE REST_v1_verify() : string
tok is string = ExtractString(HTTPHeader["Authorization"],2," ")
IF tok = "" THEN RETURN _JsonError(401,"missing_token")
sSQL is string = [
select row_to_json(t)::text from (
select true as ok,
license_uuid, activation_uuid, expires_at
from secwin_license_verify_token(%1,'access')
) t
]
res is string = _ExecScalar(StringBuild(sSQL, _Q(tok)))
IF res = "" THEN RETURN _JsonError(401,"invalid_or_expired")
RETURN _JsonOk(res)

PROCEDURE REST_v1_lease_renew() : string
tok is string = ExtractString(HTTPHeader["Authorization"],2," ")
IF tok = "" THEN RETURN _JsonError(401,"missing_token")
sSQL is string = "select secwin_license_lease_renew(" + _Q(tok) + "," + _Q(_IP()) + "," + _Q(_UA()) + ")::text"
res is string = _ExecScalar(sSQL)
IF res = "" THEN RETURN _JsonError(401,"invalid_or_expired")
RETURN _JsonOk(res)

PROCEDURE REST_v1_revoke() : string
IF HTTPRequest.Method <> httpPost THEN RETURN _JsonError(405,"method_not_allowed")
body is string = _BodyJSON()
j is Variant = JSONToVariant(body)
IF j.token = Null THEN RETURN _JsonError(400,"missing_token")
sSQL is string = "select secwin_license_revoke_token(" + _Q(j.token) + ")"
ok is boolean = _ExecScalarInt(sSQL) = 1
RETURN _JsonOk([{"ok":] + (ok ? "true" : "false") + ["}])

PROCEDURE REST_v1_heartbeat() : string
tok is string = ExtractString(HTTPHeader["Authorization"],2," ")
IF tok = "" THEN RETURN _JsonError(401,"missing_token")
sSQL is string = [
with x as (select * from secwin_license_verify_token(%1,'access'))
insert into secwin_license_audit(event, license_uuid, activation_uuid, details, ip, ua)
select 'heartbeat', license_uuid, activation_uuid, jsonb_build_object('note','hb'), %2, %3 from x
returning 'ok'::text
]
ok is string = _ExecScalar(StringBuild(sSQL, _Q(tok), _Q(_IP()), _Q(_UA())))
IF ok = "" THEN RETURN _JsonError(401,"invalid_or_expired")
RETURN _JsonOk([{"ok":true}])

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,618 messages
Posted on September, 08 2025 - 2:54 AM
openapi: 3.0.3
info:
title: SecWinDev License Server
version: "1.0"
servers:
- url: https://license.yourdomain.com/api
paths:
/v1/auth:
post:
summary: Activate/Authenticate and obtain tokens
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [license_key, device_id]
properties:
license_key: { type: string }
device_id: { type: string }
hw_hash: { type: string }
responses:
"200":
description: Tokens issued
"403": { description: License invalid/expired or activation limit reached }
/v1/verify:
get:
summary: Verify access token
security: [{ bearerAuth: [] }]
responses:
"200": { description: Token valid }
"401": { description: Invalid/expired token }
/v1/lease/renew:
post:
summary: Renew lease with access token
security: [{ bearerAuth: [] }]
responses:
"200": { description: Renewed }
"401": { description: Invalid/expired token }
/v1/revoke:
post:
summary: Revoke a token
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [token]
properties:
token: { type: string }
responses:
"200": { description: Revoked }
/v1/heartbeat:
post:
summary: Send heartbeat (optional telemetry)
security: [{ bearerAuth: [] }]
responses:
"200": { description: Ok }
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: Opaque

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,618 messages
Posted on September, 08 2025 - 2:55 AM
-- SecWinDev • License Server (opaque tokens)
create extension if not exists pgcrypto;

create table if not exists secwin_license_products(
uuid uuid primary key default gen_random_uuid(),
code text unique not null,
name text not null,
max_devices int default 3,
lease_secs int not null default 3600,
refresh_secs int not null default 604800
);

create table if not exists secwin_license_keys(
uuid uuid primary key default gen_random_uuid(),
product_uuid uuid not null references secwin_license_products(uuid) on delete cascade,
license_key text unique not null,
status text not null default 'active' check (status in ('active','revoked','expired')),
issued_to text,
max_activations int default 3,
valid_from timestamptz default now(),
valid_to timestamptz,
extra_policy jsonb default '{}'::jsonb
);
create index if not exists ix_lic_keys_prod on secwin_license_keys(product_uuid);

create table if not exists secwin_license_activations(
uuid uuid primary key default gen_random_uuid(),
license_uuid uuid not null references secwin_license_keys(uuid) on delete cascade,
device_id text not null,
hw_hash text,
first_seen timestamptz not null default now(),
last_seen timestamptz not null default now(),
status text not null default 'active' check (status in ('active','revoked')),
unique(license_uuid, device_id)
);

create type if not exists secwin_token_type as enum('access','refresh');

create table if not exists secwin_license_tokens(
uuid uuid primary key default gen_random_uuid(),
license_uuid uuid not null references secwin_license_keys(uuid) on delete cascade,
activation_uuid uuid not null references secwin_license_activations(uuid) on delete cascade,
token_hash bytea not null,
type secwin_token_type not null,
issued_at timestamptz not null default now(),
expires_at timestamptz not null,
revoked boolean not null default false,
client_ip inet,
user_agent text,
jti uuid not null default gen_random_uuid()
);
create index if not exists ix_lic_tok_lookup on secwin_license_tokens(token_hash, type) where revoked=false;
create index if not exists ix_lic_tok_exp on secwin_license_tokens(expires_at);

create table if not exists secwin_license_leases(
uuid uuid primary key default gen_random_uuid(),
activation_uuid uuid not null references secwin_license_activations(uuid) on delete cascade,
issued_at timestamptz not null default now(),
expires_at timestamptz not null,
last_renew timestamptz,
ttl_secs int not null,
client_ip inet,
user_agent text,
revoked boolean not null default false
);

create table if not exists secwin_license_audit(
id bigserial primary key,
ts timestamptz not null default now(),
event text not null,
license_uuid uuid,
activation_uuid uuid,
details jsonb,
ip inet, ua text
);

create or replace function secwin_rand_token(_size int default 32) returns text language sql as $$
select translate(replace(replace(encode(gen_random_bytes(_size),'base64'),'=',''),'+','-'),'/','_');
$$;
create or replace function secwin_sha256(_txt text) returns bytea language sql immutable as $$
select digest(_txt,'sha256');
$$;

create or replace function secwin_license_issue_tokens(
_license_uuid uuid,
_activation_uuid uuid,
_client_ip inet,
_ua text
) returns jsonb language plpgsql as $$
declare
p record;
tok_a text := secwin_rand_token(32);
tok_r text := secwin_rand_token(32);
nowts timestamptz := now();
exp_a timestamptz; exp_r timestamptz;
lease_id uuid := gen_random_uuid();
res jsonb;
begin
select lease_secs, refresh_secs into p
from secwin_license_products prod
join secwin_license_keys k on k.product_uuid=prod.uuid
where k.uuid=_license_uuid;
if not found then raise exception 'product_not_found'; end if;

exp_a := nowts + make_interval(secs=>p.lease_secs);
exp_r := nowts + make_interval(secs=>p.refresh_secs);

insert into secwin_license_tokens(license_uuid, activation_uuid, token_hash, type, issued_at, expires_at, client_ip, user_agent)
values (_license_uuid, _activation_uuid, secwin_sha256(tok_a), 'access', nowts, exp_a, _client_ip, _ua);
insert into secwin_license_tokens(license_uuid, activation_uuid, token_hash, type, issued_at, expires_at, client_ip, user_agent)
values (_license_uuid, _activation_uuid, secwin_sha256(tok_r), 'refresh', nowts, exp_r, _client_ip, _ua);
insert into secwin_license_leases(uuid, activation_uuid, issued_at, expires_at, last_renew, ttl_secs, client_ip, user_agent)
values (lease_id, _activation_uuid, nowts, exp_a, nowts, extract(epoch from (exp_a-nowts))::int, _client_ip, _ua);

res := jsonb_build_object(
'access_token', tok_a, 'access_expires_at', exp_a,
'refresh_token', tok_r, 'refresh_expires_at', exp_r,
'lease_id', lease_id, 'lease_expires_at', exp_a
);

insert into secwin_license_audit(event, license_uuid, activation_uuid, details, ip, ua)
values ('auth', _license_uuid, _activation_uuid, res, _client_ip, _ua);

return res;
end$$;

create or replace function secwin_license_verify_token(_token text, _type secwin_token_type default 'access')
returns table(license_uuid uuid, activation_uuid uuid, token_uuid uuid, expires_at timestamptz)
language sql as $$
select t.license_uuid, t.activation_uuid, t.uuid, t.expires_at
from secwin_license_tokens t
where t.token_hash = secwin_sha256(_token)
and t.type = _type
and t.revoked = false
and t.expires_at > now()
limit 1;
$$;

create or replace function secwin_license_activation_upsert(_license_key text, _device_id text, _hw_hash text, _ip inet, _ua text)
returns table(license_uuid uuid, activation_uuid uuid) language plpgsql as $$
declare lic record; cnt int; act record;
begin
select * into lic from secwin_license_keys
where license_key=_license_key and status='active'
and (valid_from is null or valid_from <= now())
and (valid_to is null or valid_to >= now());
if not found then raise exception 'license_invalid_or_expired'; end if;

select count(*) into cnt from secwin_license_activations where license_uuid=lic.uuid and status='active';
if cnt >= coalesce(lic.max_activations,1) then
perform 1 from secwin_license_activations where license_uuid=lic.uuid and device_id=_device_id;
if not found then raise exception 'activation_limit_reached'; end if;
end if;

insert into secwin_license_activations(license_uuid, device_id, hw_hash, first_seen, last_seen, status)
values (lic.uuid, _device_id, _hw_hash, now(), now(), 'active')
on conflict (license_uuid, device_id)
do update set hw_hash=excluded.hw_hash, last_seen=now(), status='active'
returning license_uuid, uuid into lic.uuid, act.uuid;

insert into secwin_license_audit(event, license_uuid, activation_uuid, details, ip, ua)
values ('activate', lic.uuid, act.uuid, jsonb_build_object('device_id',_device_id), _ip, _ua);

return query select lic.uuid, act.uuid;
end$$;

create or replace function secwin_license_lease_renew(_token text, _client_ip inet, _ua text)
returns jsonb language plpgsql as $$
declare t record; lease record; prod record;
begin
select * into t from secwin_license_verify_token(_token,'access') limit 1;
if not found then return jsonb_build_object('ok',false,'reason','invalid'); end if;

select p.lease_secs into prod from secwin_license_products p
join secwin_license_keys k on k.product_uuid=p.uuid
where k.uuid=t.license_uuid;

update secwin_license_leases
set expires_at = now() + make_interval(secs=>prod.lease_secs),
last_renew = now(),
client_ip = _client_ip, user_agent=_ua
where activation_uuid = t.activation_uuid and revoked=false
returning * into lease;

insert into secwin_license_audit(event, license_uuid, activation_uuid, details, ip, ua)
values ('renew', t.license_uuid, t.activation_uuid, jsonb_build_object('lease_id',lease.uuid,'expires_at',lease.expires_at), _client_ip, _ua);

return jsonb_build_object('ok',true,'lease_id',lease.uuid,'lease_expires_at',lease.expires_at);
end$$;

create or replace function secwin_license_revoke_token(_token text) returns boolean language sql as $$
update secwin_license_tokens set revoked=true where token_hash=secwin_sha256(_token) returning true;
$$;

-- Seed
insert into secwin_license_products(code,name,max_devices,lease_secs,refresh_secs)
values ('SECWINDEV-PRO','SecWinDev Pro',3,3600,604800) on conflict (code) do nothing;

insert into secwin_license_keys(product_uuid, license_key, issued_to, max_activations, valid_to)
select uuid, 'SWD-XXXX-2025-TRIAL', 'Demo Company', 3, now()+ interval '30 days'
from secwin_license_products where code='SECWINDEV-PRO'
on conflict do nothing;

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,618 messages
Posted on September, 08 2025 - 2:56 AM
// ===================================================
// SecWinDev License Server — REST + Bearer Middleware (gSec helpers)
// ===================================================
// Este arquivo adiciona um "middleware" de Bearer Token para reutilizar
// em todos os endpoints. Se sua versão do WebDev tiver "Filtros REST",
// configure a procedure SEC_REST_BEARER_FILTER() como filtro "Antes de cada chamada".
// Caso contrário, chame Auth_RequireBearer() no início de cada endpoint protegido.
//
// Requisitos:
// - gSec._ExecScalar(), gSec._ExecNonQuery(), gSec._Q()
// - SQL: função secwin_license_verify_token(token, 'access')
// ---------------------------------------------------

// ---------- Utilidades HTTP/JSON ----------
PROCEDURE _BodyJSON() : string
buf is Buffer = HTTPRequest..Content
RESULT BufferToString(buf,charsetANSI) // ajuste para UTF-8 se necessário

PROCEDURE _IP() : string
RESULT RequestIPAddress()

PROCEDURE _UA() : string
RESULT BrowserUserAgent()

PROCEDURE _JsonOk(obj as string) : string
HTTPHeader["Content-Type"] = "application/json;charset=utf-8"
RESULT obj

PROCEDURE _JsonError(code is int, msg is string) : string
HTTPStatus(code)
RESULT _JsonOk([{"ok":false,"error":"] + msg + ["}])

// ---------- Contexto de Autenticação por requisição ----------
// Armazena o contexto do bearer validado (disponível para os endpoints)
gsAuthContext is Variant // { ok, license_uuid, activation_uuid, expires_at }

PROCEDURE Auth_ResetContext()
gsAuthContext = Null

PROCEDURE Auth_ParseBearer() : string
auth is string = HTTPHeader["Authorization"]
IF auth = "" THEN RESULT ""
IF Left(auth, 7) ~= "Bearer " THEN RESULT ""
RESULT ExtractString(auth, 2, " ")

PROCEDURE Auth_VerifyToken(token is string, tokenType is string = "access") : Variant
IF token = "" THEN RESULT Null
sSQL is string = [
select row_to_json(t)::text from (
select true as ok, license_uuid, activation_uuid, expires_at
from secwin_license_verify_token(%1,%2)
) t
]
j is string = gSec._ExecScalar(StringBuild(sSQL, gSec._Q(token), gSec._Q(tokenType)))
IF j = "" THEN RESULT Null
RESULT JSONToVariant(j)

// Middleware "Require Bearer": valida e popula gsAuthContext ou retorna 401
PROCEDURE Auth_RequireBearer() : boolean
Auth_ResetContext()
tok is string = Auth_ParseBearer()
IF tok = "" THEN
_JsonError(401, "missing_token")
RESULT False
END
ctx is Variant = Auth_VerifyToken(tok, "access")
IF ctx = Null OR ctx.ok <> True THEN
_JsonError(401, "invalid_or_expired")
RESULT False
END
gsAuthContext = ctx
RESULT True

// ---------- Filtro REST (opcional) ----------
// Configure este filtro globalmente no projeto REST (Antes de cada chamada).
// Ele protege tudo, exceto /v1/auth e /v1/revoke (se quiser deixar público).
PROCEDURE SEC_REST_BEARER_FILTER() : boolean
// Caminho solicitado
path is string = HTTPRequest.URL // em versões antigas use HTTPHeader["X-Original-URL"] ou RequestURI()
IF path = "" THEN path = ""

// Endpoints públicos (não exigem bearer)
IF Position(path, "/v1/auth") = 1 THEN RESULT True
// Se quiser deixar revoke público, comente a próxima linha
// IF Position(path, "/v1/revoke") = 1 THEN RESULT True

// Demais endpoints: exigem Bearer
RESULT Auth_RequireBearer()

// ---------- Endpoints ----------
// Observação: se você usar o filtro acima, os endpoints protegidos não precisam
// chamar Auth_RequireBearer() novamente. Se não usar filtro, chame manualmente.

// POST /v1/auth (Público)
PROCEDURE REST_v1_auth() : string
IF HTTPRequest.Method <> httpPost THEN RETURN _JsonError(405,"method_not_allowed")
body is string = _BodyJSON()
IF body = "" THEN RETURN _JsonError(400,"empty_body")

j is Variant = JSONToVariant(body)
IF j.license_key = Null OR j.device_id = Null THEN RETURN _JsonError(400,"missing_fields")

sSQL is string = [
with act as (
select * from secwin_license_activation_upsert(%1,%2,%3,%4,%5)
)
select secwin_license_issue_tokens(
(select license_uuid from act),
(select activation_uuid from act),
%4,%5
)::text
]
res is string = gSec._ExecScalar(StringBuild(sSQL, gSec._Q(j.license_key), gSec._Q(j.device_id), gSec._Q(j.hw_hash), gSec._Q(_IP()), gSec._Q(_UA())))
IF res = "" THEN RETURN _JsonError(403,"license_invalid_or_limit")
RETURN _JsonOk(res)

// GET /v1/verify (Protegido)
PROCEDURE REST_v1_verify() : string
IF NOT Auth_RequireBearer() THEN RETURN ""
res is string = [
{"ok":true,
"license_uuid":"] + gsAuthContext.license_uuid + [",
"activation_uuid":"] + gsAuthContext.activation_uuid + [",
"expires_at":"] + gsAuthContext.expires_at + ["}
]
RETURN _JsonOk(res)

// POST /v1/lease/renew (Protegido)
PROCEDURE REST_v1_lease_renew() : string
IF NOT Auth_RequireBearer() THEN RETURN ""
tok is string = Auth_ParseBearer()
sSQL is string = "select secwin_license_lease_renew(" + gSec._Q(tok) + "," + gSec._Q(_IP()) + "," + gSec._Q(_UA()) + ")::text"
res is string = gSec._ExecScalar(sSQL)
IF res = "" THEN RETURN _JsonError(401,"invalid_or_expired")
RETURN _JsonOk(res)

// POST /v1/revoke (Público ou Protegido conforme filtro)
PROCEDURE REST_v1_revoke() : string
IF HTTPRequest.Method <> httpPost THEN RETURN _JsonError(405,"method_not_allowed")
body is string = _BodyJSON()
j is Variant = JSONToVariant(body)
IF j.token = Null THEN RETURN _JsonError(400,"missing_token")
sSQL is string = "select secwin_license_revoke_token(" + gSec._Q(j.token) + ")"
ok is boolean = (gSec._ExecScalar(sSQL) = "t") OR (gSec._ExecScalar(sSQL) = "1")
RETURN _JsonOk([{"ok":] + (ok ? "true" : "false") + ["}])

// POST /v1/heartbeat (Protegido)
PROCEDURE REST_v1_heartbeat() : string
IF NOT Auth_RequireBearer() THEN RETURN ""
sSQL is string = [
with x as (select * from secwin_license_verify_token(%1,'access'))
insert into secwin_license_audit(event, license_uuid, activation_uuid, details, ip, ua)
select 'heartbeat', license_uuid, activation_uuid, jsonb_build_object('note','hb'), %2, %3 from x
returning 'ok'::text
]
ok is string = gSec._ExecScalar(StringBuild(sSQL, gSec._Q(Auth_ParseBearer()), gSec._Q(_IP()), gSec._Q(_UA())))
IF ok = "" THEN RETURN _JsonError(401,"invalid_or_expired")
RETURN _JsonOk([{"ok":true}])

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,618 messages
Posted on September, 08 2025 - 2:57 AM
# SecWinDev • License Server (REST)

## Instalação
1) Banco: `psql -f secwin_license_schema.sql`
2) WebDev: importe `webdev_license_rest.wl`, exponha as procedures REST, publique em HTTPS.
3) Teste com cURL (veja README anterior).

## Endpoints
- POST /v1/auth
- GET /v1/verify
- POST /v1/lease/renew
- POST /v1/revoke
- POST /v1/heartbeat

## Middleware Bearer (proteção automática)
- Importe `webdev_license_rest_middleware_gSec.wl`.
- Se sua versão do WebDev suportar **Filtros REST** (Before each call), configure o filtro:
- Filtro: `SEC_REST_BEARER_FILTER`
- Ele libera `/v1/auth` (e opcionalmente `/v1/revoke`) e exige Bearer nos demais.
- Se não houver suporte a filtros, chame `Auth_RequireBearer()` no **início** de cada endpoint protegido.
- O contexto do token validado fica disponível em `gsAuthContext` (`license_uuid`, `activation_uuid`, `expires_at`).

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,618 messages
Posted on September, 08 2025 - 3:01 AM
Segue abaixo os 5 ajustes e deixei tudo pronto pra você importar.

Downloads (r17)
• SQL único (cria/atualiza tudo dos 5 itens):
secwindev_v6_adjustments_r17.sql
• Snippet WL para login (contexto GDPR):
SEC_LoginFinalize_snippet.wl
• Guia rápido (README):
README_r17.md
• Pacote ZIP:
SecWinDev_v6_Adjustments_r17.zip

O que o script SQL faz
1. Cria secwin_abac_business_hours e popula Seg–Sex 08:00–18:00 (upsert).
2. Semeia DLP:
• secwin_classifications: Público, Interno, Confidencial, Restrito.
• secwin_export_policies para cada classe (formatos, quota, watermark, MFA, business hours).
3. Garante a função secwin_set_user_context(uuid) (usada pelos triggers GDPR).
4. Cria a role secwin_reporting e dá SELECT nas views usadas por Grafana/Metabase.
5. (Opcional) Adiciona tabela secwin_oidc_providers + função secwin_oidc_upsert_provider(...) para SSO.

Onde plugar no app
• Login (logo após autenticar):
use o snippet SEC_LoginFinalize_snippet.wl para chamar
select secwin_set_user_context('<uuid>') e amarrar os triggers ao usuário logado.
• Grafana/Metabase: importe os dashboards e troque YOUR_POSTGRES_UID no JSON, se ainda não ajustou.

Verificar

select * from vw_secwin_post_install_check;

Se todos os ok vierem true, você está com os 5 ajustes prontos.

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,618 messages
Posted on September, 08 2025 - 3:31 AM
Métodos da classe (v6 • FULL r19)

Núcleo (já no arquivo clsSecWinDev_v6_full_r19.wl)
• _ExecScalar(sql) : string
• _ExecScalarInt(sql) : int
• _ExecNonQuery(sql) : boolean
• _Q(valor) : string
• Login_Finalize(userUUID) — seta o contexto de sessão no BD (triggers GDPR).
• Policy_Snapshot() : string
• Policy_StageCreate(snapshotJSON, note, riskLevel, byUUID) : int
• Policy_Approve(stageId, approverUUID) : string
• Policy_Apply(stageId, appliedByUUID, mode="upsert_only") : string
• Policy_Explain(userUUID, alvoTipo, alvoUUID) : string (JSON com rbac_state/abac/final)
• Policy_Simulate(userUUID, alvoTipo, alvoUUID, deltaJSON="{}") : string
• Export_Check(userUUID, alvoTipo, alvoUUID, format, rows) : string (JSON: allowed/message/watermark)
• Export_Register(userUUID, alvoTipo, alvoUUID, format, rows, contextJSON, decision, reason, watermark) : boolean
• Sensitive_LoadCache()
• Sensitive_SetDBContext(userUUID)
• Sensitive_LogRead(userUUID, tableName, columns[], rows, masked=True)

Integração UI (novos — prontos para colar abaixo na sua classe)
• Perm_GetEffective(userUUID, alvoTipo, alvoUUID) : string → resolve estado efetivo (default/allowed/grayed/forbidden/invisible).
• UI_ApplyState_ByName(controlName, state) → aplica estado no controle/menu.
• UI_OnOpen_Window(userUUID) → registra janela/objetos se faltarem e aplica política (RBAC+ABAC) em todos controles.
• UI_OnOpen_Report(userUUID, reportName) → aplica DLP/política antes do relatório.
• Menu_BuildDynamic(menuName, userUUID) → mostra/esconde/“grisa” itens conforme permissão.
• CTRL_F8_OpenDesigner() → pede reautenticação admin/supervisor e abre designer da tela atual para ajustar permissões.



Snippets prontos (cole na classe)

1) Resolver estado efetivo (user ⊕ perfis) para qualquer alvo

// Retorna: 'default' | 'allowed' | 'grayed' | 'forbidden' | 'invisible'
PROCEDURE Perm_GetEffective(userUUID is string, alvoTipo is string, alvoUUID is string) : string
sTblUser is string
sTblPerf is string
SWITCH Lower(alvoTipo)
CASE "janela": sTblUser="secwin_usuarios_x_janelas"; sTblPerf="secwin_perfis_x_janelas"
CASE "relatorio": sTblUser="secwin_usuarios_x_relatorios"; sTblPerf="secwin_perfis_x_relatorios"
CASE "objeto": sTblUser="secwin_usuarios_x_objetos"; sTblPerf="secwin_perfis_x_objetos"
CASE "menu": sTblUser="secwin_usuarios_x_menus"; sTblPerf="secwin_perfis_x_menus"
CASE "menu_item": sTblUser="secwin_usuarios_x_menu_itens"; sTblPerf="secwin_perfis_x_menu_itens"
OTHER CASE
RESULT "default"
END

sSQL is string = [
with u as (select estado from ] + sTblUser + [ where usuario_uuid=%1 and ] +
(Lower(alvoTipo)="menu_item" ? "menu_item_uuid" : Lower(alvoTipo)+"_uuid") + [=%2),
p as (select px.estado from secwin_usuarios_x_perfis up
join ] + sTblPerf + [ px on px.perfil_uuid=up.perfil_uuid
where up.usuario_uuid=%1 and ] +
(Lower(alvoTipo)="menu_item" ? "px.menu_item_uuid" : "px."+Lower(alvoTipo)+"_uuid") + [=%2),
all as (select * from u union all select * from p)
select coalesce((
select estado from all
order by case estado when 'invisible' then 5 when 'forbidden' then 4
when 'grayed' then 3 when 'allowed' then 2 else 1 end desc
limit 1), 'default') as estado
]
RESULT _ExecScalar(StringBuild(sSQL, _Q(userUUID), _Q(alvoUUID)))

2) Aplicar estado em controles/menus por nome

// Aplica no controle/menu da janela atual (por nome)
PROCEDURE UI_ApplyState_ByName(ctrlName is string, state is string)
IF ctrlName = "" THEN RETURN
// Tenta controle visual
IF {ctrlName} <> Null THEN
SWITCH state
CASE "allowed": {ctrlName}..Visible = True; {ctrlName}..Enabled = True
CASE "grayed": {ctrlName}..Visible = True; {ctrlName}..Enabled = False
CASE "forbidden": {ctrlName}..Visible = False
CASE "invisible": {ctrlName}..Visible = False
OTHER CASE // default → não mexe
END
RETURN
END
// Tenta item de menu (se usar nome exato do item)
IF {"MENU_"+ctrlName} <> Null THEN
SWITCH state
CASE "allowed": {"MENU_"+ctrlName}..Visible = True; {"MENU_"+ctrlName}..Enabled = True
CASE "grayed": {"MENU_"+ctrlName}..Visible = True; {"MENU_"+ctrlName}..Enabled = False
CASE "forbidden": {"MENU_"+ctrlName}..Visible = False
CASE "invisible": {"MENU_"+ctrlName}..Visible = False
END
END

3) Ao abrir uma janela: registrar e aplicar política (RBAC+ABAC)

// Chame no evento "Inicialização" da janela (ou no "Open" do modelo)
PROCEDURE UI_OnOpen_Window(userUUID is string)
winName is string = MyWindow..Name

// 1) Upsert janela
_ExecNonQuery("insert into secwin_janelas(nome,titulo) values ("+_Q(winName)+","+_Q(winName)+") on conflict (nome) do nothing")
janUUID is string = _ExecScalar("select uuid::text from secwin_janelas where nome="+_Q(winName))

// 2) Enumerar controles e registrar se faltar
// Use os enumeradores do WX conforme seu padrão (EnumControl/EnumSubElement/EnumMenu)
arr is array of string = EnumControl(MyWindow) // ajuste caso use outra enumeração
FOR EACH sCtrl OF arr
_ExecNonQuery(StringBuild([
insert into secwin_objetos(nome, janela_uuid, tipo)
values (%1,%2,'control')
on conflict (janela_uuid,nome) do nothing
], _Q(sCtrl), _Q(janUUID)))
END

// 3) Aplicar política em cada controle
Q is Data Source
HExecuteSQLQuery(Q, hQueryWithoutCorrection, MyConnection, _
"select uuid,nome from secwin_objetos where janela_uuid="+_Q(janUUID))
FOR EACH Q
// Estado RBAC efetivo
st is string = Perm_GetEffective(userUUID, "objeto", Q.uuid)
// Se continuar dúbio, peça um "explain" (ABAC + RBAC → final)
j is string = Policy_Explain(userUUID, "objeto", Q.uuid)
// extrai "final" do JSON; se final=false, trate como 'forbidden'
IF j <> "" AND Position(j, "\"final\":false") > 0 THEN st = "forbidden"
UI_ApplyState_ByName(Q.nome, st)
END

se você preferir performance máxima, carregue todos os estados em uma só query SQL (com join) e aplique em lote.

4) Montar menu dinâmico pós-login

// menuName = nome lógico do menu principal (como cadastrado em secwin_menus.nome)
// Convenção: os itens de menu têm em "nome" o mesmo nome do item visual no WX
PROCEDURE Menu_BuildDynamic(menuName is string, userUUID is string)
menUUID is string = _ExecScalar("select uuid::text from secwin_menus where nome="+_Q(menuName))
IF menUUID = "" THEN RETURN

Q is Data Source
HExecuteSQLQuery(Q, hQueryWithoutCorrection, MyConnection, _
"select mi.uuid, mi.nome from secwin_menu_itens mi where mi.menu_uuid="+_Q(menUUID))

FOR EACH Q
st is string = Perm_GetEffective(userUUID, "menu_item", Q.uuid)
UI_ApplyState_ByName(Q.nome, st)
END

Uso típico: no fim do login (depois de Login_Finalize(userUUID)), chame:

Menu_BuildDynamic("MENU_PRINCIPAL", gsUserUUID)

5) Interceptar abertura de relatório (checar DLP/export)

// Antes de exibir/imprimir o relatório:
PROCEDURE UI_OnOpen_Report(userUUID is string, reportName is string)
repUUID is string = _ExecScalar("select uuid::text from secwin_relatorios where nome="+_Q(reportName))
IF repUUID = "" THEN
_ExecNonQuery("insert into secwin_relatorios(nome,titulo) values ("+_Q(reportName)+","+_Q(reportName)+") on conflict (nome) do nothing")
repUUID = _ExecScalar("select uuid::text from secwin_relatorios where nome="+_Q(reportName))
END

// Exemplo: vai exportar PDF com ~N linhas
rows is int = 1000
j is string = Export_Check(userUUID, "relatorio", repUUID, "PDF", rows)
IF Position(j, "\"allowed\":true") = 0 THEN
Error("Exportação negada: " + j)
RETURN
END
// Pegue watermark, se houver, e siga

6) Ctrl+F8 — designer “rápido” de permissões da tela atual

PROCEDURE CTRL_F8_OpenDesigner()
/* 1) Reautenticar como admin/supervisor (senha ou WebAuthn) */
usr is string = Input("Admin ou Supervisor", "Login:")
pwd is string = InputPassword("Senha:")
IF NOT Auth_CheckAdmin(usr, pwd) THEN
Error("Credenciais inválidas.")
RETURN
END

/* 2) Tela atual → lista de objetos + checkboxes (allowed/grayed/forbidden/invisible) */
winName is string = MyWindow..Name
janUUID is string = _ExecScalar("select uuid::text from secwin_janelas where nome="+_Q(winName))
Q is Data Source
HExecuteSQLQuery(Q, hQueryWithoutCorrection, MyConnection, _
"select uuid, nome from secwin_objetos where janela_uuid="+_Q(janUUID))
// Abra sua janela de designer (ex.: WIN_SecWinDev_Designer) preenchendo a grade
Open(WIN_SecWinDev_Designer, Q) // implemente a janela com grid e combobox de estado

/* 3) Ao confirmar, salve cada linha chamando as funções SQL seguras */
FOR EACH ROW OF WIN_SecWinDev_Designer.TABLE_Obj
objUUID is string = TABLE_Obj.COL_UUID
state is string = TABLE_Obj.COL_ESTADO // allowed/grayed/forbidden/invisible
// Exemplo: gravar para o usuário atual (ou para um perfil/grupo conforme a UI)
_ExecNonQuery(StringBuild("select secwin_set_user_object(%1,%2,%3)", _Q(gsUserUUID), _Q(objUUID), _Q(state)))
END

/* 4) Reaplicar a política na janela atual */
UI_OnOpen_Window(gsUserUUID)

Para acionar: no evento de tecla da janela, algo como:

IF KeyPressed(kbControl) AND Key = VK_F8 THEN
CTRL_F8_OpenDesigner()
END

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,618 messages
Posted on September, 08 2025 - 4:33 AM
// ===============================
// Runtime Form Builder (Ctrl+F8)
// File: modCtrlF8.wl
// ===============================
// Creates controls at runtime and auto-populates from JSON META/DATA.
// Depends on helper functions available in your project: _ExecScalar, _ExecNonQuery (optional).

glCtrlMap is associative array of Control // name -> created control
gCtrlF8Host is Window // hosting window (WIN_CtrlF8)

// -- Create controls from META JSON and return final Y used (height)
PROCEDURE CtrlF8_BuildFromMeta(metaJSON is string, host is Window) : int
IF metaJSON = "" THEN RESULT 0
fields is Variant = JSONToVariant(metaJSON)
IF fields = Null THEN RESULT 0

// basic grid layout
xLbl is int = 20
xCtl is int = 220
y is int = 24
lineH is int = 24
gapY is int = 10
wDefault is int = 360

// optimize draw
host..DisplayEnabled = False

FOR EACH f OF fields
sName is string = f.name
sLabel is string = f.label
sType is string = Lower(f.type)
wCtl is int = (f.width..Exists AND f.width>0) ? f.width : wDefault

// Label
lbl is Control
lbl <- ControlCreate("LBL_"+sName, typStatic, xLbl, y, xCtl - xLbl - 8, lineH, True)
lbl..Caption = sLabel

// Field
ctl is Control
SWITCH sType
CASE "boolean","bool"
ctl <- ControlCreate("CHK_"+sName, typCheckBox, xCtl, y, wCtl, lineH, True)
ctl..Caption = ""
CASE "date"
ctl <- ControlCreate("EDT_"+sName, typDate, xCtl, y, 160, lineH, True)
CASE "combo"
ctl <- ControlCreate("CMB_"+sName, typCombo, xCtl, y, wCtl, lineH, True)
IF f.items <> Null THEN
FOR EACH it OF f.items
// it = ["text","value"]
ListAdd(ctl, it[1] + gStoredValue(it[2]))
END
gStoredValueEnable(ctl, True)
END
OTHER CASE
ctl <- ControlCreate("EDT_"+sName, typEdit, xCtl, y, wCtl, lineH, True)
END

glCtrlMap[sName] = ctl
y += lineH + gapY
END

host..DisplayEnabled = True
RESULT y
END

// -- Populate values from DATA JSON
PROCEDURE CtrlF8_Populate(dataJSON is string)
IF dataJSON = "" THEN RETURN
vals is Variant = JSONToVariant(dataJSON)
IF vals = Null THEN RETURN

FOR EACH v OF vals
nm is string = v.name
IF glCtrlMap.Exist(nm) THEN
ctl is Control = glCtrlMap[nm]
IF Left(ctl,4) = "CHK_" THEN
ctl..Checked = (v.value = True OR Lower(v.value) = "true")
ELSEIF Left(ctl,4) = "CMB_" THEN
ctl = v.value // with StoredValue enabled this selects by value
ELSE
ctl = v.value // edit/date accept string/date
END
END
END
END

// -- Collect current values to JSON (for Save)
PROCEDURE CtrlF8_CollectToJSON() : string
arr is array of Variant
FOR EACH sName OF glCtrlMap
ctl is Control = glCtrlMap[sName]
val is Variant
IF Left(ctl,4) = "CHK_" THEN
val = ctl..Checked
ELSE
val = ctl // stored value or text
END
Add(arr, [{"name":sName,"value":val}])
END
RESULT JSONBuild(arr)
END

// -- Open Ctrl+F8 host window and build+populate
PROCEDURE CtrlF8_Open(metaJSON is string, dataJSON is string)
IF NOT WindowExist("WIN_CtrlF8") THEN
// Create a blank window named WIN_CtrlF8 in the editor (title: "Ctrl+F8")
END
OpenSister(WIN_CtrlF8)
gCtrlF8Host = WIN_CtrlF8
CtrlF8_BuildFromMeta(metaJSON, gCtrlF8Host)
CtrlF8_Populate(dataJSON)
END

// -- Add a runtime Save button and wire a handler procedure
PROCEDURE CtrlF8_AddSaveButton(procOnSave is Procedure)
btn is Control
btn <- ControlCreate("BTN_Save", typButton, 20, 10, 120, 24, True)
btn..Caption = "Save"
btn..Process[trtClick] = procOnSave
END

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,618 messages
Posted on September, 08 2025 - 4:34 AM
// ===============================
// Example Usage for Ctrl+F8 Runtime Builder
// File: modCtrlF8_example_usage.wl
// ===============================
Message modified, September, 08 2025 - 4:40 AM
Registered member
4,618 messages
Posted on September, 08 2025 - 4:35 AM
// ===============================
// Example Usage for Ctrl+F8 Runtime Builder
// File: modCtrlF8_example_usage.wl
// ===============================

// In your main window Key Down event:
IF KeyPressed(kbCtrl) AND KeyPressed(VK_F8) THEN
// META describes how to draw controls
sMeta is string = JSONBuild([
{"name":"customer_name","label":"Customer","type":"text","width":360},
{"name":"active","label":"Active","type":"boolean"},
{"name":"birth","label":"Birth","type":"date"},
{"name":"status","label":"Status","type":"combo","items":[["Active","A"],["Inactive","I"]]}
])

// DATA contains values to fill
sData is string = JSONBuild([
{"name":"customer_name","value":"Maria"},
{"name":"active","value":True},
{"name":"birth","value":"1990-02-01"},
{"name":"status","value":"A"}
])

CtrlF8_Open(sMeta, sData)
CtrlF8_AddSaveButton(ProcCtrlF8_Save)
END

// Save handler: collect payload and proceed
PROCEDURE ProcCtrlF8_Save()
jsonOut is string = CtrlF8_CollectToJSON()
Info("Payload:", jsonOut)
// TODO: send to server / persist / validate

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,618 messages
Posted on September, 08 2025 - 4:38 AM
// ===============================
// Runtime Form Builder (Ctrl+F8)
// File: modCtrlF8.wl
// ===============================
// Creates controls at runtime and auto-populates from JSON META/DATA.
// Depends on helper functions available in your project: _ExecScalar, _ExecNonQuery (optional).

glCtrlMap is associative array of Control // name -> created control
gCtrlF8Host is Window // hosting window (WIN_CtrlF8)

// -- Create controls from META JSON and return final Y used (height)
PROCEDURE CtrlF8_BuildFromMeta(metaJSON is string, host is Window) : int
IF metaJSON = "" THEN RESULT 0
fields is Variant = JSONToVariant(metaJSON)
IF fields = Null THEN RESULT 0

// basic grid layout
xLbl is int = 20
xCtl is int = 220
y is int = 24
lineH is int = 24
gapY is int = 10
wDefault is int = 360

// optimize draw
host..DisplayEnabled = False

FOR EACH f OF fields
sName is string = f.name
sLabel is string = f.label
sType is string = Lower(f.type)
wCtl is int = (f.width..Exists AND f.width>0) ? f.width : wDefault

// Label
lbl is Control
lbl <- ControlCreate("LBL_"+sName, typStatic, xLbl, y, xCtl - xLbl - 8, lineH, True)
lbl..Caption = sLabel

// Field
ctl is Control
SWITCH sType
CASE "boolean","bool"
ctl <- ControlCreate("CHK_"+sName, typCheckBox, xCtl, y, wCtl, lineH, True)
ctl..Caption = ""
CASE "date"
ctl <- ControlCreate("EDT_"+sName, typDate, xCtl, y, 160, lineH, True)
CASE "combo"
ctl <- ControlCreate("CMB_"+sName, typCombo, xCtl, y, wCtl, lineH, True)
IF f.items <> Null THEN
FOR EACH it OF f.items
// it = ["text","value"]
ListAdd(ctl, it[1] + gStoredValue(it[2]))
END
gStoredValueEnable(ctl, True)
END
OTHER CASE
ctl <- ControlCreate("EDT_"+sName, typEdit, xCtl, y, wCtl, lineH, True)
END

glCtrlMap[sName] = ctl
y += lineH + gapY
END

host..DisplayEnabled = True
RESULT y
END

// -- Populate values from DATA JSON
PROCEDURE CtrlF8_Populate(dataJSON is string)
IF dataJSON = "" THEN RETURN
vals is Variant = JSONToVariant(dataJSON)
IF vals = Null THEN RETURN

FOR EACH v OF vals
nm is string = v.name
IF glCtrlMap.Exist(nm) THEN
ctl is Control = glCtrlMap[nm]
IF Left(ctl,4) = "CHK_" THEN
ctl..Checked = (v.value = True OR Lower(v.value) = "true")
ELSEIF Left(ctl,4) = "CMB_" THEN
ctl = v.value // with StoredValue enabled this selects by value
ELSE
ctl = v.value // edit/date accept string/date
END
END
END
END

// -- Collect current values to JSON (for Save)
PROCEDURE CtrlF8_CollectToJSON() : string
arr is array of Variant
FOR EACH sName OF glCtrlMap
ctl is Control = glCtrlMap[sName]
val is Variant
IF Left(ctl,4) = "CHK_" THEN
val = ctl..Checked
ELSE
val = ctl // stored value or text
END
Add(arr, [{"name":sName,"value":val}])
END
RESULT JSONBuild(arr)
END

// -- Open Ctrl+F8 host window and build+populate
PROCEDURE CtrlF8_Open(metaJSON is string, dataJSON is string)
IF NOT WindowExist("WIN_CtrlF8") THEN
// Create a blank window named WIN_CtrlF8 in the editor (title: "Ctrl+F8")
END
OpenSister(WIN_CtrlF8)
gCtrlF8Host = WIN_CtrlF8
CtrlF8_BuildFromMeta(metaJSON, gCtrlF8Host)
CtrlF8_Populate(dataJSON)
END

// -- Add a runtime Save button and wire a handler procedure
PROCEDURE CtrlF8_AddSaveButton(procOnSave is Procedure)
btn is Control
btn <- ControlCreate("BTN_Save", typButton, 20, 10, 120, 24, True)
btn..Caption = "Save"
btn..Process[trtClick] = procOnSave
END

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,618 messages
Posted on September, 08 2025 - 9:40 PM
Entendi — os links que gerei expiraram e, agora há pouco, a ferramenta de anexar/gerar arquivos falhou do meu lado. Pra não te travar, já te entrego em texto o artefato principal (classe OOP completa). Se quiser, eu colo também o SQL unificado na íntegra aqui na sequência.

clsSecWinDev v6 (r22) — classe OOP completa (WLanguage)

// ============================================================================
// SecWinDev r22 — Classe OOP Única (completa)
// Inclui: Autenticação, MFA, Device Binding, Telemetria, Lock/Unlock,
// Permissões & Integridade, Hardening (rate-limit, auditoria),
// Activation Secret (enroll/verify) e utilitários.
// Obs.: Esta classe depende de PostgreSQL (PG12+) e das funções SQL do pacote.
// Substitua _ExecScalar/_ExecNonQuery pela sua camada de acesso a dados.
// ============================================================================

cSecWinDev is Class

// --------- ESTADO DA SESSÃO / CONTEXTO ---------
gsUserUUID is string
gsUserLogin is string
gsDeviceId is string
gsAppCode is string

// --------- CONSTRUTOR / DESTRUTOR ---------
PROCEDURE cSecWinDev(sAppCode is string="ERP_DEFAULT")
PROCEDURE ~cSecWinDev()

// --------- PRIVADO: HELPERS DE BD ---------
PRIVATE
PROCEDURE _Q(s is string) : string
PROCEDURE _ExecScalar(sql is string) : string
PROCEDURE _ExecNonQuery(sql is string)

// --------- AUTENTICAÇÃO / MFA / DEVICE ---------
PUBLIC
PROCEDURE Password_Check(u is string, p is string) : boolean
PROCEDURE Password_Set(u is string, np is string) : boolean
PROCEDURE OTP_Issue(u is string, purpose is string, ttl is int=10) : string
PROCEDURE OTP_Consume(u is string, purpose is string, code is string) : boolean
PROCEDURE Email_SendOTP(toEmail is string, subject is string, body is string) : boolean
PROCEDURE OTP_SendToUser(u is string, purpose is string, ttl is int=10) : boolean
PROCEDURE WebAuthn_Login_Complete(u is string, challenge is string, assertionJSON is string) : boolean
PROCEDURE MFA_GetPolicy(u is string) : string
PROCEDURE MFA_Start(u is string, deviceId is string) : string
PROCEDURE Device_Bind(u is string, d is string) : string
PROCEDURE Device_SaveToken(d is string, t is string)
PROCEDURE Device_LoadToken(d is string) : string
PROCEDURE MFA_Complete_WebAuthn(u is string, ch is string, asrt is string, d is string) : boolean
PROCEDURE MFA_Complete_OTP(u is string, code is string, d is string) : boolean

// --------- SESSÃO / TROCA DE USUÁRIO / LOCK ---------
PROCEDURE User_UUIDByLogin(login is string) : string
PROCEDURE User_SwitchCommit(nu is string, deviceId is string="") : boolean
PROCEDURE User_SwitchByLogin(login is string, plainPwd is string, deviceId is string="") : boolean
PROCEDURE User_SwitchByLogin_Hardened(login is string, plainPwd is string, deviceId is string = "", ip is string = "0.0.0.0") : boolean
PROCEDURE Station_Lock(u is string)
PROCEDURE Station_Unlock(u is string, pwd is string) : boolean
PROCEDURE Station_SwitchUser()

// --------- PERMISSÕES & INTEGRIDADE (WRAPPERS) ---------
PROCEDURE User_Create(login is string, email is string, plainPwd is string) : string
PROCEDURE Group_Create(name is string, description is string = "") : string
PROCEDURE Resource_Register(code is string, rtype is string, title is string = "") : int
PROCEDURE Group_AddMember(groupUUID is string, userUUID is string) : boolean
PROCEDURE Permission_Set_User(userUUID is string, resCode is string, action is string, allow is boolean, note is string = "") : boolean
PROCEDURE Permission_Set_Group(groupUUID is string, resCode is string, action is string, allow is boolean, note is string = "") : boolean
PROCEDURE Permission_Set_User_Audited(userUUID is string, resCode is string, action is string, allow is boolean, note is string = "", changedBy is string = "") : boolean
PROCEDURE Permission_Set_Group_Audited(groupUUID is string, resCode is string, action is string, allow is boolean, note is string = "", changedBy is string = "") : boolean
PROCEDURE Group_AddMember_Audited(groupUUID is string, userUUID is string, changedBy is string = "") : boolean
PROCEDURE IsAllowed(userUUID is string, resCode is string, action is string) : boolean
PROCEDURE Validate_Integrity() : string

// --------- HARDENING (RATE LIMIT, AUDITORIA, CLEANUP, SYMKEY) ---------
PROCEDURE LogError(msg is string)
PROCEDURE SetChangedBy(userUUID is string)
PROCEDURE Validate_SymKey(minLen is int = 32, minClasses is int = 3) : boolean
PROCEDURE Login_BeginGuard(userUUID is string, ip is string = "0.0.0.0", windowSec is int = 600, maxAttempts is int = 7, blockSec is int = 900) : boolean
PROCEDURE Login_FailRegister(userUUID is string, ip is string = "0.0.0.0", windowSec is int = 600, maxAttempts is int = 7, blockSec is int = 900) : int
PROCEDURE Login_ResetGuard(userUUID is string, ip is string = "0.0.0.0")
PROCEDURE Cleanup_Expired(otpKeepDays is int = 1, webAuthnKeepDays is int = 1) : string

// --------- ACTIVATION SECRET (ENROLL & VERIFY) ---------
PROCEDURE Activation_IsEnrolled(userUUID is string, appCode is string, deviceId is string) : boolean
PROCEDURE Activation_Set(userUUID is string, appCode is string, deviceId is string, secret is string) : boolean
PROCEDURE Activation_VerifyOrEnroll(userUUID is string, appCode is string, deviceId is string, secret is string, enrollIfAbsent is boolean=True) : string
PROCEDURE Activation_Clear(userUUID is string, appCode is string, deviceId is string) : boolean
PROCEDURE Activation_EnsureAndVerify(userUUID is string, appCode is string, deviceId is string, secret is string) : boolean

// --------- TELEMETRIA (PROMETHEUS) ---------
PROCEDURE Metric_Inc(metric is string, labelsJSON is string)
PROCEDURE Metric_LoginFail(u is string, cause is string)
PROCEDURE Metric_UnlockFail(u is string)
PROCEDURE Metric_AutoLock(u is string)
PROCEDURE Metric_WebAuthnSuccess(u is string)
PROCEDURE Metrics_ExportProm() : string

END

// ===== Implementação =====

// --------- CONSTRUTOR / DESTRUTOR ---------
PROCEDURE cSecWinDev.cSecWinDev(sAppCode is string)
SELF.gsAppCode = sAppCode
// (opcional) carregar deviceId local
SELF.gsDeviceId = INIRead("SecWinDev","device_id","")
IF SELF.gsDeviceId = "" THEN SELF.gsDeviceId = SysUUID()

PROCEDURE cSecWinDev.~cSecWinDev()
// nada específico

// --------- PRIVADO: HELPERS DE BD ---------
PROCEDURE cSecWinDev._Q(s is string) : string
IF s = Null THEN RESULT "NULL"
RESULT "'" + Replace(s, "'", "''") + "'"

PROCEDURE cSecWinDev._ExecScalar(sql is string) : string
// TODO: substitua pelo seu executor de SQL; aqui usa um alias genérico
res is string = SQLExecScalar(sql) // implemente esta função na sua camada
RESULT res

PROCEDURE cSecWinDev._ExecNonQuery(sql is string)
// TODO: substitua pelo seu executor de SQL (sem retorno)
_ = SQLExecNonQuery(sql) // implemente esta função na sua camada

// --------- AUTENTICAÇÃO / MFA / DEVICE ---------
PROCEDURE cSecWinDev.Password_Check(u is string, p is string) : boolean
IF u="" OR p="" THEN RESULT False
h is string = SELF._ExecScalar("select password_hash from secwin_usuarios where uuid="+SELF._Q(u))
IF h="" THEN RESULT False
ok is string = SELF._ExecScalar("select case when crypt("+SELF._Q(p)+","+SELF._Q(h)+")="+SELF._Q(h)+" then '1' else '0' end")
RESULT ok="1"

PROCEDURE cSecWinDev.Password_Set(u is string, np is string) : boolean
IF u="" OR np="" THEN RESULT False
SELF._ExecNonQuery("update secwin_usuarios set password_hash=crypt("+SELF._Q(np)+", gen_salt('bf',12)), updated_at=now() where uuid="+SELF._Q(u))
RESULT True

PROCEDURE cSecWinDev.OTP_Issue(u is string, purpose is string, ttl is int=10) : string
RESULT SELF._ExecScalar("select public.secwin_otp_issue("+SELF._Q(u)+", "+SELF._Q(purpose)+", "+ttl+")")

PROCEDURE cSecWinDev.OTP_Consume(u is string, purpose is string, code is string) : boolean
ok is string = SELF._ExecScalar("select case when public.secwin_otp_consume("+SELF._Q(u)+", "+SELF._Q(purpose)+", "+SELF._Q(code)+") then '1' else '0' end")
RESULT ok="1"

PROCEDURE cSecWinDev.Email_SendOTP(toEmail is string, subject is string, body is string) : boolean
// Integre com seu SMTP ou serviço; stub retorna True
RESULT True

PROCEDURE cSecWinDev.OTP_SendToUser(u is string, purpose is string, ttl is int=10) : boolean
email is string = SELF._ExecScalar("select email from secwin_usuarios where uuid="+SELF._Q(u))
code is string = SELF.OTP_Issue(u, purpose, ttl)
IF email="" OR code="" THEN RESULT False
RESULT SELF.Email_SendOTP(email, "Seu código "+purpose, "Código: "+code)

PROCEDURE cSecWinDev.WebAuthn_Login_Complete(u is string, challenge is string, assertionJSON is string) : boolean
// Integre sua validação WebAuthn; se sucesso, retorne True
RESULT True

PROCEDURE cSecWinDev.MFA_GetPolicy(u is string) : string
RESULT SELF._ExecScalar("select public.secwin_mfa_policy("+SELF._Q(u)+")::text")

PROCEDURE cSecWinDev.MFA_Start(u is string, deviceId is string) : string
p is Variant = JSONToVariant(SELF.MFA_GetPolicy(u))
IF p.webauthn_enabled THEN RESULT JSONBuild([{"step":"webauthn"}])
IF p.otp_enabled THEN
ok is boolean = SELF.OTP_SendToUser(u,"login",10)
RESULT JSONBuild([{"step":"otp","otp_sent":ok,"device_required":p.require_device}])
END
RESULT JSONBuild([{"step":"password_only"}])

PROCEDURE cSecWinDev.Device_Bind(u is string, d is string) : string
RESULT SELF._ExecScalar("select public.secwin_device_issue_token("+SELF._Q(u)+", "+SELF._Q(d)+")")

PROCEDURE cSecWinDev.Device_SaveToken(d is string, t is string)
INIWrite("SecWinDev","token_"+d,t)

PROCEDURE cSecWinDev.Device_LoadToken(d is string) : string
RESULT INIRead("SecWinDev","token_"+d,"")

PROCEDURE cSecWinDev.MFA_Complete_WebAuthn(u is string, ch is string, asrt is string, d is string) : boolean
ok is boolean = SELF.WebAuthn_Login_Complete(u,ch,asrt)
IF NOT ok THEN RESULT False
SELF._ExecNonQuery("select public.secwin_evt_webauthn_success("+SELF._Q(u)+")")
pol is Variant = JSONToVariant(SELF.MFA_GetPolicy(u))
IF pol.require_device THEN
t is string = SELF.Device_Bind(u,d)
SELF.Device_SaveToken(d,t)
END
RESULT True

PROCEDURE cSecWinDev.MFA_Complete_OTP(u is string, code is string, d is string) : boolean
IF NOT SELF.OTP_Consume(u,"login",code) THEN
SELF._ExecNonQuery("select public.secwin_evt_login_fail("+SELF._Q(u)+",'otp_invalid')")
RESULT False
END
pol is Variant = JSONToVariant(SELF.MFA_GetPolicy(u))
IF pol.require_device THEN
t is string = SELF.Device_Bind(u,d)
SELF.Device_SaveToken(d,t)
END
RESULT True

// --------- SESSÃO / TROCA DE USUÁRIO / LOCK ---------
PROCEDURE cSecWinDev.User_UUIDByLogin(login is string) : string
IF login="" THEN RESULT ""
RESULT SELF._ExecScalar("select uuid::text from secwin_usuarios where login="+SELF._Q(login)+" and (ativo is true or ativo is null) limit 1")

PROCEDURE cSecWinDev.User_SwitchCommit(nu is string, deviceId is string="") : boolean
IF nu="" THEN RESULT False
prev is string = SELF.gsUserUUID
SELF.gsUserUUID = nu
SELF.gsUserLogin = SELF._ExecScalar("select login from secwin_usuarios where uuid="+SELF._Q(nu))
IF deviceId<>"" THEN
// Hooks de licença (se existirem no seu projeto)
IF ProcedureDefined("License_LoadFromVault") THEN _=License_LoadFromVault(SELF.gsUserUUID,deviceId)
IF ProcedureDefined("License_Verify") THEN _=License_Verify()
END
IF ProcedureDefined("Policy_ClearCache") THEN Policy_ClearCache()
IF ProcedureDefined("Sensitive_ClearCache") THEN Sensitive_ClearCache()
IF ProcedureDefined("UI_ApplyPolicies_All") THEN
UI_ApplyPolicies_All(SELF.gsUserUUID)
ELSE
IF WindowExist(WIN_Main) THEN Close(WIN_Main); Open(WIN_Main) END
END
IF ProcedureDefined("Metric_Inc") THEN SELF.Metric_Inc("secwin_user_switch_total", JSONBuild([{"from":prev,"to":nu}]))
RESULT True

PROCEDURE cSecWinDev.User_SwitchByLogin(login is string, plainPwd is string, deviceId is string="") : boolean
IF login="" OR plainPwd="" THEN RESULT False
uuid is string = SELF.User_UUIDByLogin(login)
IF uuid="" THEN
SELF.Metric_LoginFail("00000000-0000-0000-0000-000000000000","user_not_found")
RESULT False
END
IF ProcedureDefined("Login_BeginGuard") AND NOT SELF.Login_BeginGuard(uuid) THEN RESULT False
IF NOT SELF.Password_Check(uuid,plainPwd) THEN
SELF.Metric_LoginFail(uuid,"bad_password")
RESULT False
END
IF ProcedureDefined("Login_ResetGuard") THEN SELF.Login_ResetGuard(uuid)
RESULT SELF.User_SwitchCommit(uuid,deviceId)

PROCEDURE cSecWinDev.User_SwitchByLogin_Hardened(login is string, plainPwd is string, deviceId is string = "", ip is string = "0.0.0.0") : boolean
IF login = "" OR plainPwd = "" THEN SELF.LogError("Parâmetros vazios"); RESULT False
TRY
uuid is string = SELF.User_UUIDByLogin(login)
IF uuid = "" THEN SELF._ExecNonQuery("select public.secwin_evt_login_fail('00000000-0000-0000-0000-000000000000','user_not_found')"); RESULT False END
IF NOT SELF.Login_BeginGuard(uuid, ip) THEN Error("Muitas tentativas. Tente novamente mais tarde."); RESULT False END
IF NOT SELF.Password_Check(uuid, plainPwd) THEN SELF._ExecNonQuery("select public.secwin_evt_login_fail("+SELF._Q(uuid)+",'bad_password')"); _ = SELF.Login_FailRegister(uuid, ip); RESULT False END
SELF.Login_ResetGuard(uuid, ip)
RESULT SELF.User_SwitchCommit(uuid, deviceId)
CATCH
SELF.LogError("Erro em User_SwitchByLogin_Hardened: " + ExceptionInfo())
RESULT False
END

PROCEDURE cSecWinDev.Station_Lock(u is string)
IF ProcedureDefined("WIN_SecLock_Animated") THEN OpenSister(WIN_SecLock_Animated,u,"starfield")

PROCEDURE cSecWinDev.Station_Unlock(u is string, pwd is string) : boolean
IF u="" OR pwd="" THEN RESULT False
IF ProcedureDefined("Unlock_BeginGuard") AND NOT Unlock_BeginGuard(u) THEN RESULT False
IF NOT SELF.Password_Check(u,pwd) THEN SELF._ExecNonQuery("select public.secwin_evt_unlock_fail("+SELF._Q(u)+")"); RESULT False
IF ProcedureDefined("Unlock_ResetGuard") THEN Unlock_ResetGuard(u)
RESULT True

PROCEDURE cSecWinDev.Station_SwitchUser()
IF ProcedureDefined("WIN_SwitchUser") THEN Open(WIN_SwitchUser)

// --------- PERMISSÕES & INTEGRIDADE ---------
PROCEDURE cSecWinDev.User_Create(login is string, email is string, plainPwd is string) : string
IF login = "" OR plainPwd = "" THEN RESULT ""
RESULT SELF._ExecScalar("select public.secwin_user_create("+SELF._Q(login)+","+SELF._Q(email)+","+SELF._Q(plainPwd)+")::text")

PROCEDURE cSecWinDev.Group_Create(name is string, description is string = "") : string
IF name = "" THEN RESULT ""
RESULT SELF._ExecScalar("select public.secwin_group_create("+SELF._Q(name)+","+SELF._Q(description)+")::text")

PROCEDURE cSecWinDev.Resource_Register(code is string, rtype is string, title is string = "") : int
IF code = "" OR rtype = "" THEN RESULT 0
RESULT SELF._ExecScalar("select public.secwin_resource_register("+SELF._Q(code)+","+SELF._Q(rtype)+","+SELF._Q(title)+")::bigint")

PROCEDURE cSecWinDev.Group_AddMember(groupUUID is string, userUUID is string) : boolean
IF groupUUID = "" OR userUUID = "" THEN RESULT False
SELF._ExecNonQuery("select public.secwin_group_add_member("+SELF._Q(groupUUID)+","+SELF._Q(userUUID)+")")
RESULT True

PROCEDURE cSecWinDev.Permission_Set_User(userUUID is string, resCode is string, action is string, allow is boolean, note is string = "") : boolean
IF userUUID = "" THEN RESULT False
SELF._ExecNonQuery("select public.secwin_permission_set("+SELF._Q(userUUID)+", NULL, "+SELF._Q(resCode)+","+SELF._Q(action)+","+(allow?"true":"false")+","+SELF._Q(note)+")")
RESULT True

PROCEDURE cSecWinDev.Permission_Set_Group(groupUUID is string, resCode is string, action is string, allow is boolean, note is string = "") : boolean
IF groupUUID = "" THEN RESULT False
SELF._ExecNonQuery("select public.secwin_permission_set(NULL, "+SELF._Q(groupUUID)+", "+SELF._Q(resCode)+","+SELF._Q(action)+","+(allow?"true":"false")+","+SELF._Q(note)+")")
RESULT True

PROCEDURE cSecWinDev.IsAllowed(userUUID is string, resCode is string, action is string) : boolean
IF userUUID = "" OR resCode = "" OR action = "" THEN RESULT False
ok is string = SELF._ExecScalar("select case when public.secwin_is_allowed("+SELF._Q(userUUID)+","+SELF._Q(resCode)+","+SELF._Q(action)+") then '1' else '0' end")
RESULT (ok = "1")

PROCEDURE cSecWinDev.Validate_Integrity() : string
RESULT SELF._ExecScalar("select public.secwin_validate_integrity()::text")

// --------- HARDENING ---------
PROCEDURE cSecWinDev.LogError(msg is string)
Trace(msg)

PROCEDURE cSecWinDev.SetChangedBy(userUUID is string)
IF userUUID = "" THEN RETURN
SELF._ExecNonQuery("select public.secwin_set_changed_by("+SELF._Q(userUUID)+")")

PROCEDURE cSecWinDev.Validate_SymKey(minLen is int = 32, minClasses is int = 3) : boolean
TRY
_ = SELF._ExecScalar("select public.secwin_require_symkey_strength("+minLen+","+minClasses+")::text")
RESULT True
CATCH
SELF.LogError("SymKey fraca/ausente: " + ExceptionInfo())
RESULT False
END

PROCEDURE cSecWinDev.Login_BeginGuard(userUUID is string, ip is string = "0.0.0.0", windowSec is int = 600, maxAttempts is int = 7, blockSec is int = 900) : boolean
IF userUUID = "" THEN RESULT False
TRY
ok is string = SELF._ExecScalar("select case when public.secwin_rate_check_and_touch("+SELF._Q(userUUID)+", "+SELF._Q(ip)+"::inet, "+windowSec+", "+maxAttempts+", "+blockSec+") then '1' else '0' end")
RESULT (ok = "1")
CATCH
SELF.LogError("Login_BeginGuard falhou: " + ExceptionInfo())
RESULT False
END

PROCEDURE cSecWinDev.Login_FailRegister(userUUID is string, ip is string = "0.0.0.0", windowSec is int = 600, maxAttempts is int = 7, blockSec is int = 900) : int
IF userUUID = "" THEN RESULT 0
TRY
fails is string = SELF._ExecScalar("select public.secwin_rate_fail("+SELF._Q(userUUID)+", "+SELF._Q(ip)+"::inet, "+windowSec+", "+maxAttempts+", "+blockSec+")::int")
RESULT Val(fails)
CATCH
SELF.LogError("Login_FailRegister erro: " + ExceptionInfo())
RESULT 0
END

PROCEDURE cSecWinDev.Login_ResetGuard(userUUID is string, ip is string = "0.0.0.0")
IF userUUID = "" THEN RETURN
TRY
SELF._ExecNonQuery("select public.secwin_rate_reset("+SELF._Q(userUUID)+", "+SELF._Q(ip)+"::inet)")
CATCH
SELF.LogError("Login_ResetGuard erro: " + ExceptionInfo())
END

PROCEDURE cSecWinDev.Cleanup_Expired(otpKeepDays is int = 1, webAuthnKeepDays is int = 1) : string
TRY
res is string = SELF._ExecScalar("select public.secwin_cleanup_expired(make_interval(days:="+otpKeepDays+"), make_interval(days:="+webAuthnKeepDays+"))::text")
RESULT res
CATCH
SELF.LogError("Cleanup_Expired erro: " + ExceptionInfo())
RESULT ""
END

PROCEDURE cSecWinDev.Permission_Set_User_Audited(userUUID is string, resCode is string, action is string, allow is boolean, note is string = "", changedBy is string = "") : boolean
IF changedBy = "" THEN changedBy = SELF.gsUserUUID
SELF.SetChangedBy(changedBy)
RESULT SELF.Permission_Set_User(userUUID, resCode, action, allow, note)

PROCEDURE cSecWinDev.Permission_Set_Group_Audited(groupUUID is string, resCode is string, action is string, allow is boolean, note is string = "", changedBy is string = "") : boolean
IF changedBy = "" THEN changedBy = SELF.gsUserUUID
SELF.SetChangedBy(changedBy)
RESULT SELF.Permission_Set_Group(groupUUID, resCode, action, allow, note)

PROCEDURE cSecWinDev.Group_AddMember_Audited(groupUUID is string, userUUID is string, changedBy is string = "") : boolean
IF changedBy = "" THEN changedBy = SELF.gsUserUUID
SELF.SetChangedBy(changedBy)
RESULT SELF.Group_AddMember(groupUUID, userUUID)

// --------- ACTIVATION SECRET ---------
PROCEDURE cSecWinDev.Activation_IsEnrolled(userUUID is string, appCode is string, deviceId is string) : boolean
IF userUUID="" OR appCode="" OR deviceId="" THEN RESULT False
ok is string = SELF._ExecScalar("select case when public.secwin_activation_is_enrolled("+SELF._Q(userUUID)+","+SELF._Q(appCode)+","+SELF._Q(deviceId)+") then '1' else '0' end")
RESULT ok="1"

PROCEDURE cSecWinDev.Activation_Set(userUUID is string, appCode is string, deviceId is string, secret is string) : boolean
IF userUUID="" OR appCode="" OR deviceId="" OR secret="" THEN RESULT False
SELF._ExecNonQuery("select public.secwin_activation_set("+SELF._Q(userUUID)+","+SELF._Q(appCode)+","+SELF._Q(deviceId)+","+SELF._Q(secret)+")")
RESULT True

PROCEDURE cSecWinDev.Activation_VerifyOrEnroll(userUUID is string, appCode is string, deviceId is string, secret is string, enrollIfAbsent is boolean=True) : string
IF userUUID="" OR appCode="" OR deviceId="" OR secret="" THEN RESULT "invalid"
res is string = SELF._ExecScalar("select public.secwin_activation_verify_or_enroll("+SELF._Q(userUUID)+","+SELF._Q(appCode)+","+SELF._Q(deviceId)+","+SELF._Q(secret)+","+(enrollIfAbsent?"true":"false")+")")
RESULT res

PROCEDURE cSecWinDev.Activation_Clear(userUUID is string, appCode is string, deviceId is string) : boolean
IF userUUID="" OR appCode=="" OR deviceId="" THEN RESULT False
SELF._ExecNonQuery("select public.secwin_activation_clear("+SELF._Q(userUUID)+","+SELF._Q(appCode)+","+SELF._Q(deviceId)+")")
RESULT True

PROCEDURE cSecWinDev.Activation_EnsureAndVerify(userUUID is string, appCode is string, deviceId is string, secret is string) : boolean
IF userUUID="" OR appCode="" OR deviceId="" THEN RESULT False
enrolled is boolean = SELF.Activation_IsEnrolled(userUUID, appCode, deviceId)
IF NOT enrolled THEN
status is string = SELF.Activation_VerifyOrEnroll(userUUID, appCode, deviceId, secret, True)
RESULT (status="enrolled")
ELSE
status is string = SELF.Activation_VerifyOrEnroll(userUUID, appCode, deviceId, secret, False)
RESULT (status="verified")
END

// --------- TELEMETRIA ---------
PROCEDURE cSecWinDev.Metric_Inc(metric is string, labelsJSON is string)
IF metric<>"" THEN SELF._ExecNonQuery("select public.secwin_metric_inc("+SELF._Q(metric)+", "+SELF._Q(labelsJSON)+"::jsonb)")

PROCEDURE cSecWinDev.Metric_LoginFail(u is string, cause is string) SELF.Metric_Inc("secwin_login_fail_total", JSONBuild([{"user":u,"cause":cause}]))
PROCEDURE cSecWinDev.Metric_UnlockFail(u is string) SELF.Metric_Inc("secwin_unlock_fail_total", JSONBuild([{"user":u}]))
PROCEDURE cSecWinDev.Metric_AutoLock(u is string) SELF.Metric_Inc("secwin_session_autolock_total", JSONBuild([{"user":u}]))
PROCEDURE cSecWinDev.Metric_WebAuthnSuccess(u is string) SELF.Metric_Inc("secwin_webauthn_success_total", JSONBuild([{"user":u}]))

PROCEDURE cSecWinDev.Metrics_ExportProm() : string
RESULT SELF._ExecScalar("select public.secwin_metrics_export()")

// ====== FIM DA CLASSE ======

Se preferir, eu colo agora o SQL unificado (PG12+) completo aqui em texto também — inclui Core, Permissões/Integridade, Hardening (rate limit + auditoria + cleanup) e Activation Secret (bcrypt).

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,618 messages
Posted on September, 08 2025 - 9:41 PM
perfeito — segue o SQL unificado completo (PG12+) do SecWinDev r22, com Core → Permissões/Integridade → Hardening → Activation. Tudo é idempotente (usa IF NOT EXISTS, ON CONFLICT, e blocos BEGIN/COMMIT).

Dica de aplicação (psql):

\i secwindev_r22_full_plus_unificado.sql
ALTER DATABASE <db> SET secwin.symkey TO '<SEGREDO-32+caracteres>';
SELECT public.secwin_require_pg12();
SELECT public.secwin_require_symkey_strength(32,3);
SELECT public.secwin_validate_integrity();

-- SecWinDev r22 FULL+ — SQL Unificado
-- Ordem: Core -> Permissões/Integridade -> Hardening -> Activation
-- Requisitos: PostgreSQL 12+ e extensão pgcrypto

-- =====================================================================
-- [1/4] Core r22 (PG12+) — Users, OTP, WebAuthn, Devices, MFA Policy,
-- Watermark HMAC, Prometheus-style Metrics
-- =====================================================================
BEGIN;
CREATE EXTENSION IF NOT EXISTS pgcrypto;

CREATE TABLE IF NOT EXISTS public.secwin_usuarios (
uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
login TEXT UNIQUE NOT NULL,
email TEXT,
password_hash TEXT,
ativo BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- OTP
CREATE TABLE IF NOT EXISTS public.secwin_otp_tokens (
usuario_uuid UUID NOT NULL REFERENCES public.secwin_usuarios(uuid) ON DELETE CASCADE,
purpose TEXT NOT NULL,
code TEXT NOT NULL,
nonce TEXT,
exp_at TIMESTAMPTZ NOT NULL,
used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (usuario_uuid, purpose, code)
);
CREATE OR REPLACE FUNCTION public.secwin_otp_issue(u UUID, p TEXT, ttl_min INT DEFAULT 10)
RETURNS TEXT LANGUAGE plpgsql AS $$
DECLARE c TEXT := lpad((trunc(random()*1000000))::INT::TEXT,6,'0');
BEGIN
INSERT INTO public.secwin_otp_tokens(usuario_uuid,purpose,code,exp_at)
VALUES (u,p,c, now() + make_interval(mins := ttl_min));
RETURN c;
END;
$$;
CREATE OR REPLACE FUNCTION public.secwin_otp_consume(u UUID, p TEXT, c TEXT)
RETURNS BOOLEAN LANGUAGE plpgsql AS $$
DECLARE ok BOOLEAN;
BEGIN
UPDATE public.secwin_otp_tokens SET used_at = now()
WHERE usuario_uuid=u AND purpose=p AND code=c AND used_at IS NULL AND exp_at > now();
GET DIAGNOSTICS ok = ROW_COUNT > 0;
RETURN ok;
END;
$$;

-- WebAuthn
CREATE TABLE IF NOT EXISTS public.secwin_webauthn_challenges (
usuario_uuid UUID NOT NULL REFERENCES public.secwin_usuarios(uuid) ON DELETE CASCADE,
purpose TEXT NOT NULL,
challenge TEXT NOT NULL,
exp_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (usuario_uuid, purpose, challenge)
);
CREATE TABLE IF NOT EXISTS public.secwin_webauthn_credentials (
usuario_uuid UUID NOT NULL REFERENCES public.secwin_usuarios(uuid) ON DELETE CASCADE,
credential_id TEXT NOT NULL,
cose_public_key TEXT NOT NULL,
sign_count BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_used_at TIMESTAMPTZ,
PRIMARY KEY (usuario_uuid, credential_id)
);

-- Device binding
CREATE TABLE IF NOT EXISTS public.secwin_devices(
usuario_uuid UUID NOT NULL REFERENCES public.secwin_usuarios(uuid) ON DELETE CASCADE,
device_id TEXT NOT NULL,
token TEXT,
trust_level TEXT NOT NULL DEFAULT 'trusted',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_seen_at TIMESTAMPTZ,
PRIMARY KEY(usuario_uuid, device_id)
);
CREATE OR REPLACE FUNCTION public.secwin_device_issue_token(u UUID, d TEXT)
RETURNS TEXT LANGUAGE plpgsql AS $$
DECLARE k TEXT := current_setting('secwin.symkey', true);
DECLARE nonce TEXT := encode(gen_random_bytes(12), 'base64');
DECLARE ts TEXT := to_char(now(), 'YYYYMMDDHH24MISS');
DECLARE payload TEXT := 'device|'||u::text||'|'||d||'|'||nonce||'|'||ts;
DECLARE tok TEXT;
BEGIN
IF k IS NULL THEN RAISE EXCEPTION 'secwin.symkey is not set'; END IF;
tok := replace(replace(encode(hmac(payload, k, 'sha256'),'base64'), '+','-'), '/','_');
tok := trim(trailing '=' FROM tok);
INSERT INTO public.secwin_devices(usuario_uuid, device_id, token, trust_level, last_seen_at)
VALUES (u, d, tok, 'trusted', now())
ON CONFLICT (usuario_uuid, device_id) DO UPDATE
SET token = EXCLUDED.token, last_seen_at = now();
RETURN tok;
END;
$$;
CREATE OR REPLACE FUNCTION public.secwin_device_verify_token(u UUID, d TEXT, t TEXT)
RETURNS BOOLEAN LANGUAGE sql AS $$
SELECT EXISTS(
SELECT 1 FROM public.secwin_devices
WHERE usuario_uuid=u AND device_id=d AND token=t AND trust_level<>'blocked'
);
$$;

-- MFA policy
CREATE TABLE IF NOT EXISTS public.secwin_user_mfa(
usuario_uuid UUID PRIMARY KEY REFERENCES public.secwin_usuarios(uuid) ON DELETE CASCADE,
webauthn_enabled BOOLEAN NOT NULL DEFAULT TRUE,
otp_enabled BOOLEAN NOT NULL DEFAULT TRUE,
require_device BOOLEAN NOT NULL DEFAULT TRUE,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE OR REPLACE FUNCTION public.secwin_mfa_policy(u UUID)
RETURNS JSON LANGUAGE sql AS $$
SELECT json_build_object(
'webauthn_enabled', coalesce(webauthn_enabled, true),
'otp_enabled', coalesce(otp_enabled, true),
'require_device', coalesce(require_device, true)
)
FROM public.secwin_user_mfa WHERE usuario_uuid=u
UNION ALL
SELECT json_build_object('webauthn_enabled', true,'otp_enabled',true,'require_device',true)
WHERE NOT EXISTS (SELECT 1 FROM public.secwin_user_mfa WHERE usuario_uuid=u)
LIMIT 1;
$$;

-- Watermark payload (para carimbar PDFs no pipeline server-side)
CREATE OR REPLACE FUNCTION public.secwin_watermark_payload(u UUID, reason TEXT DEFAULT '')
RETURNS JSON LANGUAGE plpgsql AS $$
DECLARE k TEXT := current_setting('secwin.symkey', true);
DECLARE v_login TEXT; DECLARE v_email TEXT; DECLARE v_at TIMESTAMPTZ := now();
DECLARE v_code TEXT; DECLARE data TEXT;
BEGIN
IF k IS NULL THEN RAISE EXCEPTION 'secwin.symkey is not set'; END IF;
SELECT login, email INTO v_login, v_email FROM public.secwin_usuarios WHERE uuid=u;
IF v_login IS NULL THEN RAISE EXCEPTION 'User not found'; END IF;
data := coalesce(v_login,'') || '|' || coalesce(v_email,'') || '|' || to_char(v_at, 'YYYY-MM-DD"T"HH24:MI:SSOF') || '|' || coalesce(reason,'');
v_code := encode(hmac(data, k, 'sha256'), 'base64');
RETURN json_build_object('login', v_login,'email', v_email,'at', to_char(v_at, 'YYYY-MM-DD"T"HH24:MI:SSOF'),'reason', reason,'code', v_code);
END;
$$;

-- Metrics (Prometheus-like)
CREATE TABLE IF NOT EXISTS public.secwin_metrics_counter_totals(
metric TEXT NOT NULL,
labels JSONB NOT NULL DEFAULT '{}'::jsonb,
labels_hash TEXT NOT NULL,
value BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY(metric, labels_hash)
);
CREATE OR REPLACE FUNCTION public.secwin_labels_hash(j JSONB)
RETURNS TEXT LANGUAGE plpgsql AS $$
DECLARE txt TEXT := ''; DECLARE pair RECORD;
BEGIN
FOR pair IN SELECT key, value FROM jsonb_each(j) ORDER BY key LOOP
txt := txt || pair.key || '=' || coalesce(pair.value::text,'""') || '|';
END LOOP;
RETURN md5(txt);
END;
$$;
CREATE OR REPLACE FUNCTION public.secwin_metric_inc(m TEXT, j JSONB DEFAULT '{}'::jsonb, by INT DEFAULT 1)
RETURNS VOID LANGUAGE plpgsql AS $$
DECLARE h TEXT := public.secwin_labels_hash(j);
BEGIN
INSERT INTO public.secwin_metrics_counter_totals(metric, labels, labels_hash, value)
VALUES (m, j, h, GREATEST(by,0))
ON CONFLICT (metric, labels_hash) DO UPDATE
SET value = public.secwin_metrics_counter_totals.value + EXCLUDED.value,
labels = COALESCE(public.secwin_metrics_counter_totals.labels, '{}'::jsonb);
END;
$$;
CREATE OR REPLACE FUNCTION public.secwin_evt_login_fail(u UUID, cause TEXT) RETURNS VOID LANGUAGE sql AS $$
SELECT public.secwin_metric_inc('secwin_login_fail_total', jsonb_build_object('user', u::text,'cause',cause), 1);
$$;
CREATE OR REPLACE FUNCTION public.secwin_evt_unlock_fail(u UUID) RETURNS VOID LANGUAGE sql AS $$
SELECT public.secwin_metric_inc('secwin_unlock_fail_total', jsonb_build_object('user', u::text), 1);
$$;
CREATE OR REPLACE FUNCTION public.secwin_evt_autolock(u UUID) RETURNS VOID LANGUAGE sql AS $$
SELECT public.secwin_metric_inc('secwin_session_autolock_total', jsonb_build_object('user', u::text), 1);
$$;
CREATE OR REPLACE FUNCTION public.secwin_evt_webauthn_success(u UUID) RETURNS VOID LANGUAGE sql AS $$
SELECT public.secwin_metric_inc('secwin_webauthn_success_total', jsonb_build_object('user', u::text), 1);
$$;
CREATE OR REPLACE FUNCTION public.secwin_metrics_export()
RETURNS TEXT LANGUAGE plpgsql AS $$
DECLARE r RECORD; DECLARE out TEXT := '# SecWinDev metrics\n'; DECLARE lbl TEXT; DECLARE pair RECORD;
BEGIN
FOR r IN SELECT * FROM public.secwin_metrics_counter_totals ORDER BY metric LOOP
lbl := '';
FOR pair IN SELECT key, value FROM jsonb_each(r.labels) ORDER BY key LOOP
lbl := lbl || pair.key || '="' || replace(replace(replace(trim(both '"' from pair.value::text), '\\','\\'), '\n',' '), '"','\\"') || '",';
END LOOP;
IF lbl <> '' THEN lbl := '{' || trim(trailing ',' from lbl) || '}'; END IF;
out := out || '# TYPE '||r.metric||' counter\n' || r.metric || lbl || ' ' || r.value::text || E'\n';
END LOOP; RETURN out;
END; $$;

COMMIT;

-- =====================================================================
-- [2/4] Permissões & Integridade (PG12+) — Grupos, Recursos, Membros,
-- Regras ALLOW/DENY, Views, Helpers CRUD, Validações, PG12 check
-- =====================================================================
BEGIN;
CREATE EXTENSION IF NOT EXISTS pgcrypto;

-- Case-insensitive login unique index (além do UNIQUE normal, endurece duplicados por case)
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_indexes
WHERE schemaname='public' AND indexname='secwin_usuarios_login_lower_ux'
) THEN
EXECUTE 'CREATE UNIQUE INDEX secwin_usuarios_login_lower_ux ON public.secwin_usuarios (lower(login))';
END IF;
END $$;

CREATE TABLE IF NOT EXISTS public.secwin_groups (
uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL UNIQUE,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE IF NOT EXISTS public.secwin_resources (
id BIGSERIAL PRIMARY KEY,
code TEXT NOT NULL UNIQUE,
rtype TEXT NOT NULL CHECK (rtype IN ('screen','report')),
title TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE IF NOT EXISTS public.secwin_group_members (
group_uuid UUID NOT NULL REFERENCES public.secwin_groups(uuid) ON DELETE CASCADE,
user_uuid UUID NOT NULL REFERENCES public.secwin_usuarios(uuid) ON DELETE CASCADE,
added_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (group_uuid, user_uuid)
);
CREATE INDEX IF NOT EXISTS secwin_group_members_user_idx ON public.secwin_group_members(user_uuid);
CREATE INDEX IF NOT EXISTS secwin_group_members_group_idx ON public.secwin_group_members(group_uuid);

CREATE TABLE IF NOT EXISTS public.secwin_permissions (
id BIGSERIAL PRIMARY KEY,
user_uuid UUID REFERENCES public.secwin_usuarios(uuid) ON DELETE CASCADE,
group_uuid UUID REFERENCES public.secwin_groups(uuid) ON DELETE CASCADE,
resource_id BIGINT NOT NULL REFERENCES public.secwin_resources(id) ON DELETE CASCADE,
action TEXT NOT NULL,
allow BOOLEAN NOT NULL,
note TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT secwin_permissions_subject_xor_chk CHECK (
(user_uuid IS NOT NULL AND group_uuid IS NULL) OR
(user_uuid IS NULL AND group_uuid IS NOT NULL)
)
);
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname='secwin_perm_user_ux') THEN
EXECUTE 'CREATE UNIQUE INDEX secwin_perm_user_ux ON public.secwin_permissions(user_uuid, resource_id, action) WHERE user_uuid IS NOT NULL';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname='secwin_perm_group_ux') THEN
EXECUTE 'CREATE UNIQUE INDEX secwin_perm_group_ux ON public.secwin_permissions(group_uuid, resource_id, action) WHERE group_uuid IS NOT NULL';
END IF;
END $$;

CREATE OR REPLACE VIEW public.v_secwin_user_groups AS
SELECT gm.user_uuid, gm.group_uuid, g.name AS group_name
FROM public.secwin_group_members gm
JOIN public.secwin_groups g ON g.uuid = gm.group_uuid;

CREATE OR REPLACE VIEW public.v_secwin_permissions_flat AS
SELECT p.id, 'user'::text AS subject_type, p.user_uuid AS subject_uuid,
p.resource_id, p.action, p.allow, p.note, p.created_at
FROM public.secwin_permissions p WHERE p.user_uuid IS NOT NULL
UNION ALL
SELECT p.id, 'group'::text AS subject_type, p.group_uuid AS subject_uuid,
p.resource_id, p.action, p.allow, p.note, p.created_at
FROM public.secwin_permissions p WHERE p.group_uuid IS NOT NULL;

CREATE OR REPLACE FUNCTION public.secwin_is_allowed(u UUID, res_code TEXT, act TEXT)
RETURNS BOOLEAN LANGUAGE sql AS $$
WITH r AS ( SELECT id FROM public.secwin_resources WHERE code = res_code ),
subjects AS (
SELECT u::uuid AS subject_uuid, 'user'::text AS subject_type
UNION ALL
SELECT gm.group_uuid, 'group' FROM public.secwin_group_members gm WHERE gm.user_uuid = u
),
rules AS (
SELECT p.allow
FROM public.secwin_permissions p
JOIN r ON r.id = p.resource_id
JOIN subjects s ON ( (p.user_uuid IS NOT NULL AND s.subject_type='user' AND p.user_uuid = s.subject_uuid)
OR (p.group_uuid IS NOT NULL AND s.subject_type='group' AND p.group_uuid = s.subject_uuid) )
WHERE p.action = act
)
SELECT CASE
WHEN EXISTS (SELECT 1 FROM rules WHERE allow = FALSE) THEN FALSE
WHEN EXISTS (SELECT 1 FROM rules WHERE allow = TRUE) THEN TRUE
ELSE FALSE
END;
$$;

CREATE OR REPLACE FUNCTION public.secwin_user_create(p_login TEXT, p_email TEXT, p_plain TEXT)
RETURNS UUID LANGUAGE plpgsql AS $$
DECLARE v_uuid UUID;
BEGIN
IF p_login IS NULL OR btrim(p_login) = '' THEN RAISE EXCEPTION 'login obrigatório'; END IF;
INSERT INTO public.secwin_usuarios(login,email,password_hash)
VALUES (p_login, p_email, crypt(p_plain, gen_salt('bf',12))) RETURNING uuid INTO v_uuid;
RETURN v_uuid;
EXCEPTION WHEN unique_violation THEN RAISE EXCEPTION 'login já existe (case-insensitive)';
END; $$;

CREATE OR REPLACE FUNCTION public.secwin_group_create(p_name TEXT, p_desc TEXT)
RETURNS UUID LANGUAGE plpgsql AS $$
DECLARE v_uuid UUID;
BEGIN
IF p_name IS NULL OR btrim(p_name) = '' THEN RAISE EXCEPTION 'nome do grupo obrigatório'; END IF;
INSERT INTO public.secwin_groups(name,description) VALUES (p_name,p_desc) RETURNING uuid INTO v_uuid;
RETURN v_uuid;
EXCEPTION WHEN unique_violation THEN RAISE EXCEPTION 'grupo já existe';
END; $$;

CREATE OR REPLACE FUNCTION public.secwin_resource_register(p_code TEXT, p_rtype TEXT, p_title TEXT)
RETURNS BIGINT LANGUAGE plpgsql AS $$
DECLARE v_id BIGINT;
BEGIN
IF p_code IS NULL OR btrim(p_code) = '' THEN RAISE EXCEPTION 'code obrigatório'; END IF;
IF p_rtype NOT IN ('screen','report') THEN RAISE EXCEPTION 'rtype inválido. Use screen|report'; END IF;
INSERT INTO public.secwin_resources(code,rtype,title) VALUES (p_code,p_rtype,p_title)
ON CONFLICT (code) DO UPDATE SET rtype = EXCLUDED.rtype, title = EXCLUDED.title
RETURNING id INTO v_id;
RETURN v_id;
END; $$;

CREATE OR REPLACE FUNCTION public.secwin_group_add_member(p_group UUID, p_user UUID)
RETURNS VOID LANGUAGE plpgsql AS $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM public.secwin_groups WHERE uuid=p_group) THEN RAISE EXCEPTION 'grupo inexistente'; END IF;
IF NOT EXISTS (SELECT 1 FROM public.secwin_usuarios WHERE uuid=p_user) THEN RAISE EXCEPTION 'usuário inexistente'; END IF;
INSERT INTO public.secwin_group_members(group_uuid,user_uuid) VALUES (p_group,p_user)
ON CONFLICT DO NOTHING;
END; $$;

CREATE OR REPLACE FUNCTION public.secwin_permission_set(p_user UUID, p_group UUID, p_res_code TEXT, p_action TEXT, p_allow BOOLEAN, p_note TEXT DEFAULT NULL)
RETURNS BIGINT LANGUAGE plpgsql AS $$
DECLARE v_res BIGINT; v_id BIGINT;
BEGIN
IF (p_user IS NULL AND p_group IS NULL) OR (p_user IS NOT NULL AND p_group IS NOT NULL) THEN
RAISE EXCEPTION 'Informe apenas user OU group';
END IF;
SELECT id INTO v_res FROM public.secwin_resources WHERE code=p_res_code;
IF v_res IS NULL THEN RAISE EXCEPTION 'resource code inexistente: %', p_res_code; END IF;

IF p_user IS NOT NULL AND NOT EXISTS (SELECT 1 FROM public.secwin_usuarios WHERE uuid=p_user) THEN
RAISE EXCEPTION 'usuário inexistente';
END IF;
IF p_group IS NOT NULL AND NOT EXISTS (SELECT 1 FROM public.secwin_groups WHERE uuid=p_group) THEN
RAISE EXCEPTION 'grupo inexistente';
END IF;

IF p_user IS NOT NULL THEN
INSERT INTO public.secwin_permissions(user_uuid,resource_id,action,allow,note)
VALUES (p_user,v_res,p_action,p_allow,p_note)
ON CONFLICT (user_uuid,resource_id,action) DO UPDATE SET allow=EXCLUDED.allow, note=EXCLUDED.note
RETURNING id INTO v_id;
ELSE
INSERT INTO public.secwin_permissions(group_uuid,resource_id,action,allow,note)
VALUES (p_group,v_res,p_action,p_allow,p_note)
ON CONFLICT (group_uuid,resource_id,action) DO UPDATE SET allow=EXCLUDED.allow, note=EXCLUDED.note
RETURNING id INTO v_id;
END IF;
RETURN v_id;
END; $$;

CREATE OR REPLACE FUNCTION public.secwin_validate_integrity()
RETURNS JSON LANGUAGE plpgsql AS $$
DECLARE j JSON;
BEGIN
j := json_build_object(
'version', 'r22-perms/pg12',
'users', json_build_object(
'count', (SELECT count(*) FROM public.secwin_usuarios),
'login_lower_unique_idx', EXISTS(SELECT 1 FROM pg_indexes WHERE indexname='secwin_usuarios_login_lower_ux')
),
'groups', json_build_object('count', (SELECT count(*) FROM public.secwin_groups)),
'resources', json_build_object('count', (SELECT count(*) FROM public.secwin_resources)),
'membership', json_build_object('orphans', 0),
'permissions', json_build_object(
'orphans', 0,
'dup_user_rules', (SELECT count(*) FROM (
SELECT user_uuid,resource_id,action,count(*) c FROM public.secwin_permissions WHERE user_uuid IS NOT NULL GROUP BY 1,2,3 HAVING count(*)>1
) s),
'dup_group_rules', (SELECT count(*) FROM (
SELECT group_uuid,resource_id,action,count(*) c FROM public.secwin_permissions WHERE group_uuid IS NOT NULL GROUP BY 1,2,3 HAVING count(*)>1
) s)
),
'views', json_build_object(
'v_secwin_user_groups', EXISTS(SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace WHERE n.nspname='public' AND c.relname='v_secwin_user_groups' AND c.relkind='v'),
'v_secwin_permissions_flat', EXISTS(SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace WHERE n.nspname='public' AND c.relname='v_secwin_permissions_flat' AND c.relkind='v')
),
'functions', json_build_object('secwin_is_allowed', EXISTS(SELECT 1 FROM pg_proc WHERE proname='secwin_is_allowed'))
);
RETURN j;
END; $$;

CREATE OR REPLACE FUNCTION public.secwin_require_pg12()
RETURNS VOID LANGUAGE plpgsql AS $$
DECLARE v TEXT;
BEGIN
SELECT current_setting('server_version') INTO v;
IF split_part(v,'.',1)::int < 12 THEN
RAISE EXCEPTION 'PostgreSQL 12+ é requerido (achado: %)', v;
END IF;
END; $$;

COMMIT;

-- =====================================================================
-- [3/4] Security Hardening — SymKey strength, Rate Limiting, Cleanup,
-- Permission Audit (triggers), Performance Indexes
-- =====================================================================
BEGIN;
CREATE EXTENSION IF NOT EXISTS pgcrypto;

-- Symmetric Key Strength
CREATE OR REPLACE FUNCTION public.secwin_require_symkey_strength(min_len INT DEFAULT 32, require_classes INT DEFAULT 3)
RETURNS VOID LANGUAGE plpgsql AS $$
DECLARE k TEXT := current_setting('secwin.symkey', true);
DECLARE has_lower BOOLEAN := FALSE; DECLARE has_upper BOOLEAN := FALSE;
DECLARE has_digit BOOLEAN := FALSE; DECLARE has_symbol BOOLEAN := FALSE; DECLARE classes INT := 0;
BEGIN
IF k IS NULL OR k = '' THEN RAISE EXCEPTION 'secwin.symkey ausente. Defina ALTER DATABASE <db> SET secwin.symkey TO ''<SEGREDO>'''; END IF;
IF length(k) < min_len THEN RAISE EXCEPTION 'secwin.symkey muito curta (% chars). Mínimo: %', length(k), min_len; END IF;
has_lower := k ~ '[a-z]'; has_upper := k ~ '[A-Z]'; has_digit := k ~ '[0-9]'; has_symbol := k ~ '[^a-zA-Z0-9]';
classes := (CASE WHEN has_lower THEN 1 ELSE 0 END) + (CASE WHEN has_upper THEN 1 ELSE 0 END) + (CASE WHEN has_digit THEN 1 ELSE 0 END) + (CASE WHEN has_symbol THEN 1 ELSE 0 END);
IF classes < require_classes THEN RAISE EXCEPTION 'secwin.symkey fraca: classes % < exigido %', classes, require_classes; END IF;
END; $$;

CREATE TABLE IF NOT EXISTS public.secwin_symkey_history(
fingerprint TEXT PRIMARY KEY,
noted_at TIMESTAMPTZ NOT NULL DEFAULT now(),
note TEXT
);
CREATE OR REPLACE FUNCTION public.secwin_symkey_fingerprint() RETURNS TEXT LANGUAGE plpgsql AS $$
DECLARE k TEXT := current_setting('secwin.symkey', true);
BEGIN
IF k IS NULL THEN RETURN NULL; END IF;
RETURN encode(hmac('SecWinDev-Fingerprint', k, 'sha256'), 'hex');
END; $$;
CREATE OR REPLACE FUNCTION public.secwin_note_symkey_rotation(p_note TEXT DEFAULT NULL) RETURNS TEXT LANGUAGE plpgsql AS $$
DECLARE fp TEXT := public.secwin_symkey_fingerprint();
BEGIN
IF fp IS NULL THEN RAISE EXCEPTION 'secwin.symkey não definida'; END IF;
INSERT INTO public.secwin_symkey_history(fingerprint, note) VALUES (fp, p_note)
ON CONFLICT (fingerprint) DO UPDATE
SET noted_at = EXCLUDED.noted_at,
note = COALESCE(EXCLUDED.note, public.secwin_symkey_history.note);
RETURN fp;
END; $$;

-- Rate limiting
CREATE TABLE IF NOT EXISTS public.secwin_login_attempts (
ip_address INET NOT NULL,
user_uuid UUID NOT NULL,
failed_attempts INT NOT NULL DEFAULT 0,
last_attempt TIMESTAMPTZ NOT NULL DEFAULT now(),
blocked_until TIMESTAMPTZ,
PRIMARY KEY (ip_address, user_uuid)
);
CREATE INDEX IF NOT EXISTS secwin_login_attempts_blocked_idx ON public.secwin_login_attempts(blocked_until);

CREATE OR REPLACE FUNCTION public.secwin_rate_check_and_touch(p_user UUID, p_ip INET, p_window_sec INT DEFAULT 600, p_max_fail INT DEFAULT 7, p_block_sec INT DEFAULT 900)
RETURNS BOOLEAN LANGUAGE plpgsql AS $$
DECLARE rec RECORD; v_now TIMESTAMPTZ := now();
BEGIN
SELECT * INTO rec FROM public.secwin_login_attempts WHERE ip_address = p_ip AND user_uuid = p_user;
IF rec.blocked_until IS NOT NULL AND rec.blocked_until > v_now THEN RETURN FALSE; END IF;
IF rec.ip_address IS NULL THEN
INSERT INTO public.secwin_login_attempts(ip_address, user_uuid, failed_attempts, last_attempt) VALUES (p_ip, p_user, 0, v_now);
ELSE
IF rec.last_attempt < v_now - make_interval(secs := p_window_sec) THEN
UPDATE public.secwin_login_attempts SET failed_attempts = 0 WHERE ip_address = p_ip AND user_uuid = p_user;
END IF;
END IF;
RETURN TRUE;
END; $$;

CREATE OR REPLACE FUNCTION public.secwin_rate_fail(p_user UUID, p_ip INET, p_window_sec INT DEFAULT 600, p_max_fail INT DEFAULT 7, p_block_sec INT DEFAULT 900)
RETURNS INT LANGUAGE plpgsql AS $$
DECLARE v_now TIMESTAMPTZ := now(); v_fails INT;
BEGIN
INSERT INTO public.secwin_login_attempts(ip_address, user_uuid, failed_attempts, last_attempt)
VALUES (p_ip, p_user, 1, v_now)
ON CONFLICT (ip_address, user_uuid) DO UPDATE
SET failed_attempts = CASE WHEN public.secwin_login_attempts.last_attempt < v_now - make_interval(secs := p_window_sec)
THEN 1 ELSE public.secwin_login_attempts.failed_attempts + 1 END,
last_attempt = v_now;
SELECT failed_attempts INTO v_fails FROM public.secwin_login_attempts
WHERE ip_address=p_ip AND user_uuid = p_user;
IF v_fails >= p_max_fail THEN
UPDATE public.secwin_login_attempts
SET blocked_until = v_now + make_interval(secs := p_block_sec)
WHERE ip_address=p_ip AND user_uuid = p_user;
END IF;
RETURN v_fails;
END; $$;

CREATE OR REPLACE FUNCTION public.secwin_rate_reset(p_user UUID, p_ip INET)
RETURNS VOID LANGUAGE plpgsql AS $$
BEGIN
DELETE FROM public.secwin_login_attempts WHERE ip_address=p_ip AND user_uuid = p_user;
END; $$;

-- Cleanup de temporários
CREATE OR REPLACE FUNCTION public.secwin_cleanup_expired(p_otp_keep INTERVAL DEFAULT interval '1 day', p_webauthn_keep INTERVAL DEFAULT interval '1 day')
RETURNS JSON LANGUAGE plpgsql AS $$
DECLARE c_otp INT := 0; DECLARE c_chal INT := 0;
BEGIN
WITH del1 AS (DELETE FROM public.secwin_otp_tokens WHERE exp_at < now() - p_otp_keep RETURNING 1),
del2 AS (DELETE FROM public.secwin_webauthn_challenges WHERE exp_at < now() - p_webauthn_keep RETURNING 1)
SELECT (SELECT count(*) FROM del1), (SELECT count(*) FROM del2) INTO c_otp, c_chal;
RETURN json_build_object('deleted_otp', coalesce(c_otp,0), 'deleted_webauthn', coalesce(c_chal,0));
END; $$;

-- Auditoria de Permissões
CREATE TABLE IF NOT EXISTS public.secwin_permission_audit (
id BIGSERIAL PRIMARY KEY,
permission_id BIGINT,
op TEXT NOT NULL,
old_values JSONB,
new_values JSONB,
changed_by UUID,
changed_by_login TEXT,
changed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE OR REPLACE FUNCTION public.secwin_set_changed_by(p_user UUID)
RETURNS VOID LANGUAGE plpgsql AS $$
BEGIN
PERFORM set_config('secwin.changed_by', COALESCE(p_user::text,''), false);
END; $$;

CREATE OR REPLACE FUNCTION public.secwin_permissions_audit_fn()
RETURNS TRIGGER LANGUAGE plpgsql AS $$
DECLARE by_uuid UUID := NULL; DECLARE by_login TEXT := NULL;
BEGIN
BEGIN
by_uuid := nullif(current_setting('secwin.changed_by', true), '')::uuid;
EXCEPTION WHEN others THEN
by_uuid := NULL;
END;
IF by_uuid IS NOT NULL THEN
SELECT login INTO by_login FROM public.secwin_usuarios WHERE uuid = by_uuid;
END IF;

IF TG_OP = 'INSERT' THEN
INSERT INTO public.secwin_permission_audit(permission_id, op, old_values, new_values, changed_by, changed_by_login)
VALUES (NEW.id, 'INSERT', NULL, to_jsonb(NEW), by_uuid, by_login);
RETURN NEW;

ELSIF TG_OP = 'UPDATE' THEN
INSERT INTO public.secwin_permission_audit(permission_id, op, old_values, new_values, changed_by, changed_by_login)
VALUES (NEW.id, 'UPDATE', to_jsonb(OLD), to_jsonb(NEW), by_uuid, by_login);
RETURN NEW;

ELSIF TG_OP = 'DELETE' THEN
INSERT INTO public.secwin_permission_audit(permission_id, op, old_values, new_values, changed_by, changed_by_login)
VALUES (OLD.id, 'DELETE', to_jsonb(OLD), NULL, by_uuid, by_login);
RETURN OLD;
END IF;
RETURN NULL;
END; $$;

DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname='trg_secwin_permissions_audit_ins') THEN
EXECUTE 'CREATE TRIGGER trg_secwin_permissions_audit_ins AFTER INSERT ON public.secwin_permissions FOR EACH ROW EXECUTE FUNCTION public.secwin_permissions_audit_fn()';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname='trg_secwin_permissions_audit_upd') THEN
EXECUTE 'CREATE TRIGGER trg_secwin_permissions_audit_upd AFTER UPDATE ON public.secwin_permissions FOR EACH ROW EXECUTE FUNCTION public.secwin_permissions_audit_fn()';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname='trg_secwin_permissions_audit_del') THEN
EXECUTE 'CREATE TRIGGER trg_secwin_permissions_audit_del AFTER DELETE ON public.secwin_permissions FOR EACH ROW EXECUTE FUNCTION public.secwin_permissions_audit_fn()';
END IF;
END $$;

-- Índices de performance
CREATE INDEX IF NOT EXISTS secwin_usuarios_email_idx ON public.secwin_usuarios(lower(email)) WHERE email IS NOT NULL;
CREATE INDEX IF NOT EXISTS secwin_otp_tokens_exp_idx ON public.secwin_otp_tokens(exp_at) WHERE used_at IS NULL;
CREATE INDEX IF NOT EXISTS secwin_devices_last_seen_idx ON public.secwin_devices(last_seen_at);
CREATE INDEX IF NOT EXISTS secwin_permissions_fast ON public.secwin_permissions(resource_id, action, allow);

COMMIT;

-- =====================================================================
-- [4/4] Activation Secret — Enroll & Verify (bcrypt), métricas
-- =====================================================================
BEGIN;
CREATE EXTENSION IF NOT EXISTS pgcrypto;

CREATE TABLE IF NOT EXISTS public.secwin_activation_secrets (
usuario_uuid UUID NOT NULL REFERENCES public.secwin_usuarios(uuid) ON DELETE CASCADE,
app_code TEXT NOT NULL,
device_id TEXT NOT NULL,
activation_hash TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_verified_at TIMESTAMPTZ,
verify_count BIGINT NOT NULL DEFAULT 0,
failure_count BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (usuario_uuid, app_code, device_id)
);
CREATE INDEX IF NOT EXISTS secwin_activation_last_verified_idx ON public.secwin_activation_secrets(last_verified_at);
CREATE INDEX IF NOT EXISTS secwin_activation_failure_idx ON public.secwin_activation_secrets(failure_count);

CREATE OR REPLACE FUNCTION public.secwin_activation_is_enrolled(u UUID, app TEXT, dev TEXT)
RETURNS BOOLEAN LANGUAGE sql AS $$
SELECT EXISTS(SELECT 1 FROM public.secwin_activation_secrets
WHERE usuario_uuid=u AND app_code=app AND device_id=dev);
$$;

CREATE OR REPLACE FUNCTION public.secwin_activation_set(u UUID, app TEXT, dev TEXT, secret TEXT)
RETURNS BOOLEAN LANGUAGE plpgsql AS $$
DECLARE exists_rec BOOLEAN;
BEGIN
IF u IS NULL OR app IS NULL OR dev IS NULL OR secret IS NULL OR btrim(secret)='' THEN
RAISE EXCEPTION 'Parâmetros inválidos em secwin_activation_set';
END IF;
SELECT EXISTS(SELECT 1 FROM public.secwin_activation_secrets WHERE usuario_uuid=u AND app_code=app AND device_id=dev)
INTO exists_rec;
IF exists_rec THEN
UPDATE public.secwin_activation_secrets
SET activation_hash = crypt(secret, gen_salt('bf',12)),
updated_at = now(), failure_count = 0
WHERE usuario_uuid=u AND app_code=app AND device_id=dev;
ELSE
INSERT INTO public.secwin_activation_secrets(usuario_uuid, app_code, device_id, activation_hash)
VALUES (u, app, dev, crypt(secret, gen_salt('bf',12)));
END IF;
RETURN TRUE;
END; $$;

CREATE OR REPLACE FUNCTION public.secwin_activation_verify_or_enroll(u UUID, app TEXT, dev TEXT, secret TEXT, enroll_if_absent BOOLEAN DEFAULT TRUE)
RETURNS TEXT LANGUAGE plpgsql AS $$
DECLARE rec RECORD; v_now TIMESTAMPTZ := now();
BEGIN
SELECT * INTO rec FROM public.secwin_activation_secrets WHERE usuario_uuid=u AND app_code=app AND device_id=dev;
IF NOT FOUND THEN
IF enroll_if_absent THEN
PERFORM public.secwin_activation_set(u, app, dev, secret);
PERFORM public.secwin_metric_inc('secwin_activation_success_total', jsonb_build_object('user',u::text,'app',app,'device',dev,'mode','enroll'));
RETURN 'enrolled';
ELSE
PERFORM public.secwin_metric_inc('secwin_activation_fail_total', jsonb_build_object('user',u::text,'app',app,'device',dev,'cause','not_enrolled'));
RETURN 'invalid';
END IF;
END IF;

IF crypt(secret, rec.activation_hash) = rec.activation_hash THEN
UPDATE public.secwin_activation_secrets
SET last_verified_at = v_now, verify_count = rec.verify_count + 1, failure_count = 0
WHERE usuario_uuid=u AND app_code=app AND device_id=dev;
PERFORM public.secwin_metric_inc('secwin_activation_success_total', jsonb_build_object('user',u::text,'app',app,'device',dev,'mode','verify'));
RETURN 'verified';
ELSE
UPDATE public.secwin_activation_secrets
SET failure_count = rec.failure_count + 1, updated_at = v_now
WHERE usuario_uuid=u AND app_code=app AND device_id=dev;
PERFORM public.secwin_metric_inc('secwin_activation_fail_total', jsonb_build_object('user',u::text,'app',app,'device',dev,'cause','bad_secret'));
RETURN 'invalid';
END IF;
END; $$;

CREATE OR REPLACE FUNCTION public.secwin_activation_clear(u UUID, app TEXT, dev TEXT)
RETURNS BOOLEAN LANGUAGE plpgsql AS $$
BEGIN
DELETE FROM public.secwin_activation_secrets WHERE usuario_uuid=u AND app_code=app AND device_id=dev;
RETURN TRUE;
END; $$;

COMMIT;

-- Pós-aplicação (opcional, validações):
-- SELECT public.secwin_require_pg12();
-- SELECT public.secwin_require_symkey_strength(32,3);
-- SELECT public.secwin_validate_integrity();

quer que eu também cole aqui o README de aplicação (comandos psql, ordem de migração, e checks rápidos) ou preparo um script .bat/.sh para automatizar?

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/