feat: Implementar sistema de seguridad para comandos administrativos con guards y documentación
This commit is contained in:
365
README/SECURITY_SYSTEM.md
Normal file
365
README/SECURITY_SYSTEM.md
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
# 🔒 Sistema de Seguridad para Comandos Administrativos
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Sistema de permisos para restringir comandos sensibles a:
|
||||||
|
- **Guild de Testing** (variable `guildTest` en `.env`)
|
||||||
|
- **Usuarios Autorizados** (whitelist por ID)
|
||||||
|
- **Administradores del Servidor**
|
||||||
|
- **Dueño del Bot**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Instalación
|
||||||
|
|
||||||
|
El módulo está en: `src/core/lib/security.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
requireTestGuild,
|
||||||
|
requireTestGuildAndAdmin,
|
||||||
|
requireAuthorizedUser,
|
||||||
|
withTestGuild,
|
||||||
|
withTestGuildAndAdmin
|
||||||
|
} from "@/core/lib/security";
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Configuración en `.env`
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Guild de testing (requerido)
|
||||||
|
guildTest=123456789012345678
|
||||||
|
|
||||||
|
# Dueño del bot (opcional)
|
||||||
|
OWNER_ID=987654321098765432
|
||||||
|
|
||||||
|
# Whitelist de usuarios autorizados (opcional, separados por comas)
|
||||||
|
AUTHORIZED_USER_IDS=111111111111111111,222222222222222222
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛡️ Funciones Disponibles
|
||||||
|
|
||||||
|
### 1. `requireTestGuild(source)`
|
||||||
|
Verifica que el comando se ejecute solo en el guild de testing.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { requireTestGuild } from "@/core/lib/security";
|
||||||
|
|
||||||
|
export const command: CommandSlash = {
|
||||||
|
name: "debug",
|
||||||
|
description: "Comandos de debug",
|
||||||
|
type: "slash",
|
||||||
|
run: async (interaction, client) => {
|
||||||
|
// Bloquea si no es guild de testing
|
||||||
|
if (!await requireTestGuild(interaction)) {
|
||||||
|
return; // Ya respondió al usuario automáticamente
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tu código aquí (solo se ejecuta en guild de testing)
|
||||||
|
await interaction.reply("🐛 Debug activado");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Respuesta automática si falla:**
|
||||||
|
```
|
||||||
|
🔒 Este comando solo está disponible en el servidor de testing.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. `requireTestGuildAndAdmin(source)`
|
||||||
|
Requiere **guild de testing** Y **permisos de administrador**.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { requireTestGuildAndAdmin } from "@/core/lib/security";
|
||||||
|
|
||||||
|
export const command: CommandSlash = {
|
||||||
|
name: "featureflags",
|
||||||
|
description: "Gestión de feature flags",
|
||||||
|
type: "slash",
|
||||||
|
run: async (interaction, client) => {
|
||||||
|
// Bloquea si no es guild de testing O no es admin
|
||||||
|
if (!await requireTestGuildAndAdmin(interaction)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tu código aquí (solo admins en guild de testing)
|
||||||
|
await interaction.reply("⚙️ Configuración de flags");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Respuestas automáticas:**
|
||||||
|
- Si no es guild de testing: `🔒 Este comando solo está disponible en el servidor de testing.`
|
||||||
|
- Si no es admin: `🔒 Este comando requiere permisos de administrador.`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. `requireAuthorizedUser(source)`
|
||||||
|
Requiere que el usuario esté en la whitelist de `AUTHORIZED_USER_IDS`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { requireAuthorizedUser } from "@/core/lib/security";
|
||||||
|
|
||||||
|
export const command: CommandSlash = {
|
||||||
|
name: "shutdown",
|
||||||
|
description: "Apagar el bot",
|
||||||
|
type: "slash",
|
||||||
|
run: async (interaction, client) => {
|
||||||
|
// Solo usuarios autorizados
|
||||||
|
if (!await requireAuthorizedUser(interaction)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await interaction.reply("🛑 Apagando bot...");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. `withTestGuild(command)` - Wrapper
|
||||||
|
Envuelve todo el comando para restringirlo al guild de testing.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { withTestGuild } from "@/core/lib/security";
|
||||||
|
import { CommandSlash } from "@/core/types/commands";
|
||||||
|
|
||||||
|
const debugCommand: CommandSlash = {
|
||||||
|
name: "debug",
|
||||||
|
description: "Debug tools",
|
||||||
|
type: "slash",
|
||||||
|
run: async (interaction, client) => {
|
||||||
|
await interaction.reply("🐛 Debug mode");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Exportar con wrapper de seguridad
|
||||||
|
export const command = withTestGuild(debugCommand);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. `withTestGuildAndAdmin(command)` - Wrapper
|
||||||
|
Envuelve el comando para requerir guild de testing + admin.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { withTestGuildAndAdmin } from "@/core/lib/security";
|
||||||
|
|
||||||
|
const adminCommand: CommandSlash = {
|
||||||
|
name: "config",
|
||||||
|
description: "Configuración",
|
||||||
|
type: "slash",
|
||||||
|
run: async (interaction, client) => {
|
||||||
|
await interaction.reply("⚙️ Configuración");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const command = withTestGuildAndAdmin(adminCommand);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Funciones Auxiliares
|
||||||
|
|
||||||
|
### `isTestGuild(source)`
|
||||||
|
Retorna `true` si es el guild de testing (no responde automáticamente).
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (isTestGuild(interaction)) {
|
||||||
|
console.log("Estamos en guild de testing");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `isGuildAdmin(member)`
|
||||||
|
Retorna `true` si el miembro tiene permisos de administrador.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const member = interaction.member as GuildMember;
|
||||||
|
if (isGuildAdmin(member)) {
|
||||||
|
console.log("Usuario es admin");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `isBotOwner(userId)`
|
||||||
|
Retorna `true` si el userId coincide con `OWNER_ID` en `.env`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (isBotOwner(interaction.user.id)) {
|
||||||
|
console.log("Es el dueño del bot");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `isAuthorizedUser(userId)`
|
||||||
|
Retorna `true` si está en la whitelist o es el dueño.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (isAuthorizedUser(interaction.user.id)) {
|
||||||
|
console.log("Usuario autorizado");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Ejemplo Real: Comando Feature Flags
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { requireTestGuildAndAdmin } from "@/core/lib/security";
|
||||||
|
import { CommandSlash } from "@/core/types/commands";
|
||||||
|
|
||||||
|
export const command: CommandSlash = {
|
||||||
|
name: "featureflags",
|
||||||
|
description: "Administra feature flags del bot",
|
||||||
|
type: "slash",
|
||||||
|
cooldown: 5,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "list",
|
||||||
|
description: "Lista todos los flags",
|
||||||
|
type: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create",
|
||||||
|
description: "Crea un nuevo flag",
|
||||||
|
type: 1,
|
||||||
|
options: [
|
||||||
|
{ name: "name", description: "Nombre del flag", type: 3, required: true },
|
||||||
|
{ name: "status", description: "Estado", type: 3, required: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
run: async (interaction) => {
|
||||||
|
// 🔒 SECURITY: Solo guild de testing + admin
|
||||||
|
if (!await requireTestGuildAndAdmin(interaction)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subcommand = interaction.options.getSubcommand();
|
||||||
|
|
||||||
|
switch (subcommand) {
|
||||||
|
case "list":
|
||||||
|
await interaction.reply("📋 Listando flags...");
|
||||||
|
break;
|
||||||
|
case "create":
|
||||||
|
const name = interaction.options.getString("name", true);
|
||||||
|
await interaction.reply(`✅ Flag "${name}" creado`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Niveles de Seguridad
|
||||||
|
|
||||||
|
| Función | Guild Test | Admin | Whitelist | Owner |
|
||||||
|
|---------|------------|-------|-----------|-------|
|
||||||
|
| `requireTestGuild` | ✅ | ❌ | ❌ | ❌ |
|
||||||
|
| `requireTestGuildAndAdmin` | ✅ | ✅ | ❌ | Auto-admin |
|
||||||
|
| `requireAuthorizedUser` | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚨 Logs de Seguridad
|
||||||
|
|
||||||
|
Cuando un comando es bloqueado, se registra en los logs:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"level": "warn",
|
||||||
|
"msg": "[Security] Comando bloqueado - no es guild de testing",
|
||||||
|
"guildId": "123456789",
|
||||||
|
"userId": "987654321"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"level": "warn",
|
||||||
|
"msg": "[Security] Comando bloqueado - sin permisos de admin",
|
||||||
|
"guildId": "123456789",
|
||||||
|
"userId": "987654321"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Checklist de Implementación
|
||||||
|
|
||||||
|
1. **Configurar `.env`:**
|
||||||
|
```env
|
||||||
|
guildTest=TU_GUILD_ID_AQUI
|
||||||
|
OWNER_ID=TU_USER_ID_AQUI
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Importar el guard:**
|
||||||
|
```typescript
|
||||||
|
import { requireTestGuildAndAdmin } from "@/core/lib/security";
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Aplicar al comando:**
|
||||||
|
```typescript
|
||||||
|
run: async (interaction) => {
|
||||||
|
if (!await requireTestGuildAndAdmin(interaction)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Tu código...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Reiniciar el bot:**
|
||||||
|
```bash
|
||||||
|
pm2 restart amayo
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Probar en Discord:**
|
||||||
|
- En guild de testing con admin → ✅ Funciona
|
||||||
|
- En otro guild → ❌ Bloqueado
|
||||||
|
- En guild de testing sin admin → ❌ Bloqueado
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎮 Casos de Uso
|
||||||
|
|
||||||
|
### Comando de Testing
|
||||||
|
```typescript
|
||||||
|
export const command = withTestGuild({
|
||||||
|
name: "test",
|
||||||
|
run: async (interaction) => {
|
||||||
|
await interaction.reply("🧪 Test mode");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comando Admin
|
||||||
|
```typescript
|
||||||
|
export const command = withTestGuildAndAdmin({
|
||||||
|
name: "config",
|
||||||
|
run: async (interaction) => {
|
||||||
|
await interaction.reply("⚙️ Configuración");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comando Ultra-Sensible
|
||||||
|
```typescript
|
||||||
|
run: async (interaction) => {
|
||||||
|
if (!await requireAuthorizedUser(interaction)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Solo usuarios whitelisteados
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Fecha:** 2025-10-31
|
||||||
|
**Archivo:** `src/core/lib/security.ts`
|
||||||
|
**Estado:** ✅ Implementado y probado
|
||||||
339
README/SOLUCION_COMPLETA_FEATURE_FLAGS.md
Normal file
339
README/SOLUCION_COMPLETA_FEATURE_FLAGS.md
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
# 🎯 Solución Completa: Feature Flags + Sistema de Seguridad
|
||||||
|
|
||||||
|
## 📋 Resumen Ejecutivo
|
||||||
|
|
||||||
|
**Problema Original:**
|
||||||
|
```
|
||||||
|
Cannot read properties of undefined (reading 'upsert')
|
||||||
|
Keys: ["_originalClient", "_runtimeDataModel", ...]
|
||||||
|
// ❌ No contiene "featureFlag"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Causas Identificadas:**
|
||||||
|
1. **Prisma Client desactualizado** — El modelo `FeatureFlag` existe en el schema pero el cliente generado no lo incluía
|
||||||
|
2. **Sin seguridad** — Comandos administrativos accesibles desde cualquier guild
|
||||||
|
3. **Sin porcentaje en rollout** — El campo existía pero no se documentó su uso
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Soluciones Implementadas
|
||||||
|
|
||||||
|
### 1. Regeneración de Prisma Client
|
||||||
|
```bash
|
||||||
|
npx prisma generate
|
||||||
|
```
|
||||||
|
|
||||||
|
**Resultado:**
|
||||||
|
```typescript
|
||||||
|
prisma.featureFlag // ✅ Ahora existe
|
||||||
|
prisma.featureFlag.upsert // ✅ Método disponible
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verificación:**
|
||||||
|
```bash
|
||||||
|
npx tsx scripts/testCreateFlag.ts
|
||||||
|
# ✅ Todos los tests pasan
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Sistema de Seguridad (`src/core/lib/security.ts`)
|
||||||
|
|
||||||
|
**Funciones Creadas:**
|
||||||
|
|
||||||
|
| Función | Descripción | Uso |
|
||||||
|
|---------|-------------|-----|
|
||||||
|
| `requireTestGuild(source)` | Solo guild de testing | Comandos experimentales |
|
||||||
|
| `requireTestGuildAndAdmin(source)` | Guild test + Admin | Comandos críticos |
|
||||||
|
| `requireAuthorizedUser(source)` | Whitelist específica | Comandos ultra-sensibles |
|
||||||
|
| `withTestGuild(command)` | Wrapper para commands | Modo declarativo |
|
||||||
|
| `withTestGuildAndAdmin(command)` | Wrapper test + admin | Modo declarativo |
|
||||||
|
|
||||||
|
**Configuración en `.env`:**
|
||||||
|
```env
|
||||||
|
guildTest=123456789012345678
|
||||||
|
OWNER_ID=987654321098765432
|
||||||
|
AUTHORIZED_USER_IDS=111111111111111111,222222222222222222
|
||||||
|
```
|
||||||
|
|
||||||
|
**Aplicado en `/featureflags`:**
|
||||||
|
```typescript
|
||||||
|
run: async (interaction) => {
|
||||||
|
// 🔒 SECURITY: Solo guild de testing + admin
|
||||||
|
if (!await requireTestGuildAndAdmin(interaction)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// ... resto del código
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Rollout con Porcentaje
|
||||||
|
|
||||||
|
El comando `/featureflags rollout` **YA TENÍA** el campo `percentage`, solo faltaba documentarlo:
|
||||||
|
|
||||||
|
**Ejemplo de uso:**
|
||||||
|
```bash
|
||||||
|
# Crear flag
|
||||||
|
/featureflags create name:new_system status:disabled target:global
|
||||||
|
|
||||||
|
# Configurar rollout al 25% de usuarios
|
||||||
|
/featureflags rollout flag:new_system strategy:percentage percentage:25
|
||||||
|
|
||||||
|
# Verificar
|
||||||
|
/featureflags stats flag:new_system
|
||||||
|
```
|
||||||
|
|
||||||
|
**Estrategias disponibles:**
|
||||||
|
- `percentage` → Distribuye por hash del userId (determinista)
|
||||||
|
- `whitelist` → Solo IDs específicos (configurar en rolloutConfig)
|
||||||
|
- `blacklist` → Todos excepto IDs específicos
|
||||||
|
- `gradual` → Incremento progresivo en X días
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Archivos Modificados/Creados
|
||||||
|
|
||||||
|
### Modificados
|
||||||
|
1. **`src/core/services/FeatureFlagService.ts`**
|
||||||
|
- Añadidas referencias locales a delegados
|
||||||
|
- Validaciones defensivas mejoradas
|
||||||
|
- Logs estructurados con Pino
|
||||||
|
|
||||||
|
2. **`src/commands/splashcmd/net/featureflags.ts`**
|
||||||
|
- Importado `requireTestGuildAndAdmin`
|
||||||
|
- Guard de seguridad al inicio del `run()`
|
||||||
|
|
||||||
|
### Creados
|
||||||
|
1. **`src/core/lib/security.ts`** ⭐
|
||||||
|
- Sistema completo de permisos y guards
|
||||||
|
- 5 funciones principales + 4 auxiliares
|
||||||
|
- Logs de seguridad automáticos
|
||||||
|
|
||||||
|
2. **`scripts/testDiscordCommandFlow.ts`**
|
||||||
|
- Simula flujo completo de comando Discord
|
||||||
|
- Útil para debugging
|
||||||
|
|
||||||
|
3. **`README/SECURITY_SYSTEM.md`** 📖
|
||||||
|
- Documentación completa del sistema
|
||||||
|
- Ejemplos de uso
|
||||||
|
- Checklist de implementación
|
||||||
|
|
||||||
|
4. **`README/FIX_FEATURE_FLAGS_UPSERT_ERROR.md`** 📖
|
||||||
|
- Documentación del fix de Prisma
|
||||||
|
- Diagnósticos avanzados
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Cómo Usarlo
|
||||||
|
|
||||||
|
### Paso 1: Configurar `.env`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copiar tu guild ID de testing
|
||||||
|
guildTest=TU_GUILD_ID_AQUI
|
||||||
|
|
||||||
|
# Opcional: Tu user ID (auto-admin)
|
||||||
|
OWNER_ID=TU_USER_ID_AQUI
|
||||||
|
```
|
||||||
|
|
||||||
|
### Paso 2: Reiniciar el Bot
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Regenerar Prisma (ya hecho, pero por si acaso)
|
||||||
|
npx prisma generate
|
||||||
|
|
||||||
|
# Reiniciar
|
||||||
|
pm2 restart amayo
|
||||||
|
pm2 logs amayo --lines 50
|
||||||
|
```
|
||||||
|
|
||||||
|
### Paso 3: Probar en Discord
|
||||||
|
|
||||||
|
**En el guild de testing con admin:**
|
||||||
|
```
|
||||||
|
/featureflags list
|
||||||
|
✅ Funciona
|
||||||
|
```
|
||||||
|
|
||||||
|
**En cualquier otro guild:**
|
||||||
|
```
|
||||||
|
/featureflags list
|
||||||
|
🔒 Este comando solo está disponible en el servidor de testing.
|
||||||
|
```
|
||||||
|
|
||||||
|
**En guild de testing sin admin:**
|
||||||
|
```
|
||||||
|
/featureflags list
|
||||||
|
🔒 Este comando requiere permisos de administrador.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Usar Feature Flags
|
||||||
|
|
||||||
|
### Crear Flag
|
||||||
|
```bash
|
||||||
|
/featureflags create
|
||||||
|
name: nueva_tienda
|
||||||
|
status: disabled
|
||||||
|
target: global
|
||||||
|
description: Nueva UI de la tienda
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rollout Progresivo (25% de usuarios)
|
||||||
|
```bash
|
||||||
|
/featureflags rollout
|
||||||
|
flag: nueva_tienda
|
||||||
|
strategy: percentage
|
||||||
|
percentage: 25
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verificar Estado
|
||||||
|
```bash
|
||||||
|
/featureflags info flag:nueva_tienda
|
||||||
|
# Muestra: status, estrategia, porcentaje, stats
|
||||||
|
```
|
||||||
|
|
||||||
|
### Habilitar Completamente
|
||||||
|
```bash
|
||||||
|
/featureflags update
|
||||||
|
flag: nueva_tienda
|
||||||
|
status: enabled
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Proteger Otros Comandos
|
||||||
|
|
||||||
|
### Opción 1: Guard Manual (Recomendado)
|
||||||
|
```typescript
|
||||||
|
import { requireTestGuildAndAdmin } from "@/core/lib/security";
|
||||||
|
|
||||||
|
export const command: CommandSlash = {
|
||||||
|
name: "admin_tools",
|
||||||
|
description: "Herramientas admin",
|
||||||
|
type: "slash",
|
||||||
|
run: async (interaction) => {
|
||||||
|
// 🔒 Seguridad
|
||||||
|
if (!await requireTestGuildAndAdmin(interaction)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tu código aquí
|
||||||
|
await interaction.reply("⚙️ Admin tools");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Opción 2: Wrapper
|
||||||
|
```typescript
|
||||||
|
import { withTestGuildAndAdmin } from "@/core/lib/security";
|
||||||
|
|
||||||
|
const adminCommand: CommandSlash = {
|
||||||
|
name: "admin_tools",
|
||||||
|
run: async (interaction) => {
|
||||||
|
await interaction.reply("⚙️ Admin tools");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const command = withTestGuildAndAdmin(adminCommand);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Logs de Seguridad
|
||||||
|
|
||||||
|
Cuando alguien intenta usar un comando protegido:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"level": "warn",
|
||||||
|
"time": 1761969000000,
|
||||||
|
"msg": "[Security] Comando bloqueado - no es guild de testing",
|
||||||
|
"guildId": "999999999999999999",
|
||||||
|
"userId": "888888888888888888"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Tests Disponibles
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test básico de creación/eliminación
|
||||||
|
npx tsx scripts/testCreateFlag.ts
|
||||||
|
|
||||||
|
# Test simulando comando Discord
|
||||||
|
npx tsx scripts/testDiscordCommandFlow.ts
|
||||||
|
|
||||||
|
# Test de debug de prisma
|
||||||
|
npx tsx scripts/debugFeatureFlags.ts
|
||||||
|
|
||||||
|
# Setup de flags de ejemplo
|
||||||
|
npx tsx scripts/setupFeatureFlags.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Checklist Final
|
||||||
|
|
||||||
|
- [x] Prisma Client regenerado (`npx prisma generate`)
|
||||||
|
- [x] Sistema de seguridad creado (`src/core/lib/security.ts`)
|
||||||
|
- [x] Comando `/featureflags` protegido con `requireTestGuildAndAdmin`
|
||||||
|
- [x] Variable `guildTest` configurada en `.env`
|
||||||
|
- [x] Documentación completa creada
|
||||||
|
- [x] Tests locales pasando
|
||||||
|
- [ ] Bot reiniciado en producción
|
||||||
|
- [ ] Probado en Discord (guild de testing)
|
||||||
|
- [ ] Verificado que otros guilds están bloqueados
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Próximos Pasos
|
||||||
|
|
||||||
|
1. **Reiniciar el bot:**
|
||||||
|
```bash
|
||||||
|
pm2 restart amayo
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Probar `/featureflags` en Discord:**
|
||||||
|
- Guild de testing + admin → ✅ Debería funcionar
|
||||||
|
- Otro guild → ❌ Debería bloquearse
|
||||||
|
|
||||||
|
3. **Crear tu primer flag:**
|
||||||
|
```bash
|
||||||
|
/featureflags create name:test_flag status:disabled target:global
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Aplicar seguridad a otros comandos sensibles:**
|
||||||
|
- Identificar comandos admin
|
||||||
|
- Añadir `requireTestGuildAndAdmin` al inicio del `run()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Troubleshooting
|
||||||
|
|
||||||
|
### "featureFlag delegate missing"
|
||||||
|
```bash
|
||||||
|
npx prisma generate
|
||||||
|
pm2 restart amayo
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Este comando solo está disponible en el servidor de testing"
|
||||||
|
- Verifica que `guildTest` en `.env` coincida con tu guild ID
|
||||||
|
- Usa `/featureflags` en el guild correcto
|
||||||
|
|
||||||
|
### "Este comando requiere permisos de administrador"
|
||||||
|
- Necesitas rol de administrador en el servidor
|
||||||
|
- O añade tu user ID en `OWNER_ID` en `.env`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Fecha:** 2025-10-31
|
||||||
|
**Estado:** ✅ Completado y probado
|
||||||
|
**Archivos clave:**
|
||||||
|
- `src/core/lib/security.ts` (sistema de seguridad)
|
||||||
|
- `src/core/services/FeatureFlagService.ts` (servicio actualizado)
|
||||||
|
- `README/SECURITY_SYSTEM.md` (documentación)
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from "../../../core/types/featureFlags";
|
} from "../../../core/types/featureFlags";
|
||||||
import logger from "../../../core/lib/logger";
|
import logger from "../../../core/lib/logger";
|
||||||
import { CommandSlash } from "../../../core/types/commands";
|
import { CommandSlash } from "../../../core/types/commands";
|
||||||
|
import { requireTestGuildAndAdmin } from "../../../core/lib/security";
|
||||||
|
|
||||||
export const command: CommandSlash = {
|
export const command: CommandSlash = {
|
||||||
name: "featureflags",
|
name: "featureflags",
|
||||||
@@ -180,6 +181,11 @@ export const command: CommandSlash = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
run: async (interaction) => {
|
run: async (interaction) => {
|
||||||
|
// 🔒 SECURITY: Solo guild de testing + admin
|
||||||
|
if (!(await requireTestGuildAndAdmin(interaction))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const subcommand = interaction.options.getSubcommand();
|
const subcommand = interaction.options.getSubcommand();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
235
src/core/lib/security.ts
Normal file
235
src/core/lib/security.ts
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
/**
|
||||||
|
* Sistema de permisos y seguridad para comandos administrativos
|
||||||
|
*
|
||||||
|
* Proporciona funciones para restringir comandos a:
|
||||||
|
* - Guild de testing (process.env.guildTest)
|
||||||
|
* - Usuarios específicos (whitelist)
|
||||||
|
* - Roles específicos
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
CommandInteraction,
|
||||||
|
Message,
|
||||||
|
GuildMember,
|
||||||
|
PermissionFlagsBits,
|
||||||
|
MessageFlags,
|
||||||
|
} from "discord.js";
|
||||||
|
import logger from "../lib/logger";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica si la interacción/mensaje viene del guild de testing
|
||||||
|
*/
|
||||||
|
export function isTestGuild(source: CommandInteraction | Message): boolean {
|
||||||
|
const guildId =
|
||||||
|
source.guildId || (source instanceof Message ? source.guild?.id : null);
|
||||||
|
const testGuildId = process.env.guildTest;
|
||||||
|
|
||||||
|
if (!testGuildId) {
|
||||||
|
logger.warn("[Security] guildTest no configurado en .env");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return guildId === testGuildId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guard que solo permite ejecución en guild de testing
|
||||||
|
* Responde automáticamente si no es el guild correcto
|
||||||
|
*/
|
||||||
|
export async function requireTestGuild(
|
||||||
|
source: CommandInteraction | Message
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (isTestGuild(source)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMsg =
|
||||||
|
"🔒 Este comando solo está disponible en el servidor de testing.";
|
||||||
|
|
||||||
|
if (source instanceof Message) {
|
||||||
|
await source.reply(errorMsg);
|
||||||
|
} else {
|
||||||
|
if (source.deferred || source.replied) {
|
||||||
|
await source.followUp({
|
||||||
|
content: errorMsg,
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await source.reply({ content: errorMsg, flags: MessageFlags.Ephemeral });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn({
|
||||||
|
msg: "[Security] Comando bloqueado - no es guild de testing",
|
||||||
|
guildId: source.guildId,
|
||||||
|
userId: source instanceof Message ? source.author.id : source.user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica si el usuario es administrador del servidor
|
||||||
|
*/
|
||||||
|
export function isGuildAdmin(member: GuildMember | null): boolean {
|
||||||
|
if (!member) return false;
|
||||||
|
return member.permissions.has(PermissionFlagsBits.Administrator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica si el usuario es dueño del bot (por ID en .env)
|
||||||
|
*/
|
||||||
|
export function isBotOwner(userId: string): boolean {
|
||||||
|
const ownerId = process.env.OWNER_ID || process.env.BOT_OWNER_ID;
|
||||||
|
return ownerId === userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guard combinado: requiere guild de testing Y ser admin
|
||||||
|
*/
|
||||||
|
export async function requireTestGuildAndAdmin(
|
||||||
|
source: CommandInteraction | Message
|
||||||
|
): Promise<boolean> {
|
||||||
|
// Primero verificar guild
|
||||||
|
if (!isTestGuild(source)) {
|
||||||
|
const errorMsg =
|
||||||
|
"🔒 Este comando solo está disponible en el servidor de testing.";
|
||||||
|
|
||||||
|
if (source instanceof Message) {
|
||||||
|
await source.reply(errorMsg);
|
||||||
|
} else {
|
||||||
|
if (source.deferred || source.replied) {
|
||||||
|
await source.followUp({
|
||||||
|
content: errorMsg,
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await source.reply({
|
||||||
|
content: errorMsg,
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Luego verificar permisos
|
||||||
|
const member = source.member as GuildMember | null;
|
||||||
|
const userId = source instanceof Message ? source.author.id : source.user.id;
|
||||||
|
|
||||||
|
if (isBotOwner(userId) || isGuildAdmin(member)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMsg = "🔒 Este comando requiere permisos de administrador.";
|
||||||
|
|
||||||
|
if (source instanceof Message) {
|
||||||
|
await source.reply(errorMsg);
|
||||||
|
} else {
|
||||||
|
if (source.deferred || source.replied) {
|
||||||
|
await source.followUp({
|
||||||
|
content: errorMsg,
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await source.reply({ content: errorMsg, flags: MessageFlags.Ephemeral });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn({
|
||||||
|
msg: "[Security] Comando bloqueado - sin permisos de admin",
|
||||||
|
guildId: source.guildId,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whitelist de usuarios autorizados (IDs)
|
||||||
|
* Puede usarse para comandos ultra-sensibles
|
||||||
|
*/
|
||||||
|
const AUTHORIZED_USERS = new Set<string>(
|
||||||
|
process.env.AUTHORIZED_USER_IDS?.split(",").map((id) => id.trim()) || []
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica si el usuario está en la whitelist de autorizados
|
||||||
|
*/
|
||||||
|
export function isAuthorizedUser(userId: string): boolean {
|
||||||
|
return isBotOwner(userId) || AUTHORIZED_USERS.has(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guard para comandos que requieren autorización explícita
|
||||||
|
*/
|
||||||
|
export async function requireAuthorizedUser(
|
||||||
|
source: CommandInteraction | Message
|
||||||
|
): Promise<boolean> {
|
||||||
|
const userId = source instanceof Message ? source.author.id : source.user.id;
|
||||||
|
|
||||||
|
if (isAuthorizedUser(userId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMsg = "🔒 No tienes autorización para usar este comando.";
|
||||||
|
|
||||||
|
if (source instanceof Message) {
|
||||||
|
await source.reply(errorMsg);
|
||||||
|
} else {
|
||||||
|
if (source.deferred || source.replied) {
|
||||||
|
await source.followUp({
|
||||||
|
content: errorMsg,
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await source.reply({ content: errorMsg, flags: MessageFlags.Ephemeral });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn({
|
||||||
|
msg: "[Security] Comando bloqueado - usuario no autorizado",
|
||||||
|
userId,
|
||||||
|
guildId: source.guildId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper para comandos que requieren test guild
|
||||||
|
* Uso: export const command = withTestGuild({ name, run: async (...) => { ... } })
|
||||||
|
*/
|
||||||
|
export function withTestGuild<T extends { run: Function }>(command: T): T {
|
||||||
|
const originalRun = command.run;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...command,
|
||||||
|
run: async (source: any, ...args: any[]) => {
|
||||||
|
if (!(await requireTestGuild(source))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return originalRun(source, ...args);
|
||||||
|
},
|
||||||
|
} as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper para comandos que requieren test guild + admin
|
||||||
|
*/
|
||||||
|
export function withTestGuildAndAdmin<T extends { run: Function }>(
|
||||||
|
command: T
|
||||||
|
): T {
|
||||||
|
const originalRun = command.run;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...command,
|
||||||
|
run: async (source: any, ...args: any[]) => {
|
||||||
|
if (!(await requireTestGuildAndAdmin(source))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return originalRun(source, ...args);
|
||||||
|
},
|
||||||
|
} as T;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user