docs: enterprise refactor plan with ralph specs
This commit is contained in:
148
.ralph/specs/phase-09-auth-module.md
Normal file
148
.ralph/specs/phase-09-auth-module.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# Phase 9: Auth Module
|
||||
|
||||
## Objetivo
|
||||
Sistema completo de autenticación y autorización para ABE como plataforma.
|
||||
|
||||
## Roles y permisos
|
||||
|
||||
| Role | Sessions | Findings | Reports | Integrations | Org/Users | Settings | License |
|
||||
|---------|----------|----------|---------|--------------|-----------|----------|---------|
|
||||
| owner | CRUD | CRUD | CRUD | CRUD | CRUD | CRUD | CRUD |
|
||||
| admin | CRUD | CRUD | CRUD | CRUD | CRU | CRUD | R |
|
||||
| member | CR | CRU | CR | R | R | R | R |
|
||||
| viewer | R | R | R | R | R | R | R |
|
||||
|
||||
## Better Auth config
|
||||
```typescript
|
||||
import { betterAuth } from 'better-auth';
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: {
|
||||
// Usar Kysely adapter o direct SQLite
|
||||
type: 'sqlite',
|
||||
url: config.db.path,
|
||||
},
|
||||
emailAndPassword: { enabled: true },
|
||||
session: {
|
||||
maxAge: config.auth.sessionMaxAge,
|
||||
updateAge: 60 * 60, // refresh cada hora
|
||||
},
|
||||
// Organization plugin si disponible, sino implementar manual
|
||||
});
|
||||
```
|
||||
|
||||
Si Better Auth no soporta organizaciones directamente, implementar manualmente:
|
||||
- Tabla organizations (id, name, slug, created_at)
|
||||
- Tabla org_members (id, org_id, user_id, role, invited_at, joined_at)
|
||||
|
||||
## CASL AbilityFactory
|
||||
```typescript
|
||||
import { AbilityBuilder, createMongoAbility } from '@casl/ability';
|
||||
|
||||
export function defineAbilityFor(role: string) {
|
||||
const { can, cannot, build } = new AbilityBuilder(createMongoAbility);
|
||||
|
||||
switch (role) {
|
||||
case 'owner':
|
||||
can('manage', 'all');
|
||||
break;
|
||||
case 'admin':
|
||||
can('manage', 'all');
|
||||
cannot('delete', 'Organization');
|
||||
cannot('manage', 'License');
|
||||
can('read', 'License');
|
||||
break;
|
||||
case 'member':
|
||||
can('create', ['Session', 'Finding', 'Report']);
|
||||
can('read', 'all');
|
||||
can('update', 'Finding');
|
||||
break;
|
||||
case 'viewer':
|
||||
can('read', 'all');
|
||||
break;
|
||||
}
|
||||
return build();
|
||||
}
|
||||
```
|
||||
|
||||
## AuthMiddleware — orden de verificación
|
||||
1. Check cookie de session (web UI) via Better Auth
|
||||
2. Check header `Authorization: Bearer <jwt>`
|
||||
3. Check header `X-ABE-API-Key: <key>` (API keys para CI/CD)
|
||||
4. Si ninguno → 401
|
||||
|
||||
## API Key system
|
||||
- POST /api/auth/api-keys — crear key (retorna key UNA vez, después solo hash)
|
||||
- GET /api/auth/api-keys — listar (sin mostrar key, solo nombre + último uso)
|
||||
- DELETE /api/auth/api-keys/:id — revocar
|
||||
- Keys hasheadas con SHA-256 en DB
|
||||
- Cada key tiene: name, permissions (array de roles), expiresAt, lastUsedAt
|
||||
|
||||
## First-run flow
|
||||
1. Backend: si tabla users tiene 0 rows → flag `setupRequired = true`
|
||||
2. GET /api/auth/setup-required → `{ required: boolean }`
|
||||
3. Si required, POST /api/auth/setup con { email, password, name, orgName }
|
||||
4. Crea user con role owner + organization default
|
||||
5. Después de setup, requiere login normal
|
||||
|
||||
## Migraciones
|
||||
```sql
|
||||
-- users (Better Auth maneja su propia tabla, pero añadir campos custom)
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'member',
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS organizations (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
slug TEXT UNIQUE NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS org_members (
|
||||
id TEXT PRIMARY KEY,
|
||||
org_id TEXT NOT NULL REFERENCES organizations(id),
|
||||
user_id TEXT NOT NULL REFERENCES users(id),
|
||||
role TEXT NOT NULL DEFAULT 'member',
|
||||
joined_at INTEGER NOT NULL,
|
||||
UNIQUE(org_id, user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES users(id),
|
||||
org_id TEXT NOT NULL REFERENCES organizations(id),
|
||||
name TEXT NOT NULL,
|
||||
key_hash TEXT NOT NULL,
|
||||
key_prefix TEXT NOT NULL, -- primeros 8 chars para identificar
|
||||
permissions TEXT NOT NULL DEFAULT '["member"]',
|
||||
expires_at INTEGER,
|
||||
last_used_at INTEGER,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS auth_sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES users(id),
|
||||
token TEXT UNIQUE NOT NULL,
|
||||
expires_at INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
## NOTA sobre Better Auth
|
||||
Si Better Auth resulta demasiado complejo de integrar con Express puro
|
||||
o tiene incompatibilidades, implementar auth manualmente:
|
||||
- argon2 para hash passwords
|
||||
- crypto.randomUUID() para session tokens
|
||||
- Cookie httpOnly + secure + sameSite para sessions
|
||||
- Middleware custom que lee cookie → busca en auth_sessions → adjunta user a req
|
||||
|
||||
Esto es PERFECTAMENTE VÁLIDO. No over-engineer la auth.
|
||||
La prioridad es que funcione, sea seguro, y tenga RBAC.
|
||||
Reference in New Issue
Block a user