6542e7d51c
Mover issue a completed/ y actualizar README. Todas las fases implementadas: allowlist, invite gating, RBAC puro, integración en runtime, unauthorized_response explicit, y documentación. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
8.8 KiB
8.8 KiB
Task 10 — Control de acceso a agentes por usuario
Objetivo
Implementar un sistema de control de acceso que permita restringir qué usuarios pueden interactuar con cada agente, usando la infraestructura SecurityCfg ya existente (declarada pero no enforceada).
Contexto actual
SecurityCfgya existe eninternal/config/schema.goconRoles(map de rol -> users + actions)FiltersCfgya tieneignore_users(blocklist) ymin_power_levelshouldHandle()enshell/matrix/listener.gofiltra por room y blocklist, pero NO por allowlist- Auto-join de invites es incondicional (acepta cualquier invite)
MatchMinPowerLevel()enpkg/decision/engine.goexiste peropowerLevelsiempre se pasa como 0- Los configs de agentes ya definen roles pero nadie los verifica:
security: roles: admin: users: ["@admin:matrix-af2f3d.organic-machine.com"] actions: ["*"] user: users: ["*"] actions: ["ask", "help", "summarize"]
Problema
- Cualquier usuario del homeserver (o federado) puede invitar a un bot y hablar con el
- No hay forma de restringir acceso por usuario — los bots son publicos
- Los roles configurados en
security.rolesno se verifican en ningun punto - El auto-join acepta invites de cualquiera sin verificar permisos
- No hay distincion entre acciones permitidas por rol (admin vs user)
Diseno
Arquitectura (pure core / impure shell)
pkg/acl/ -> PURE: tipos AccessList, CheckAccess(), ExtractRole()
shell/matrix/listener.go -> IMPURE: aplica ACL en shouldHandle() y auto-join
agents/runtime.go -> composicion: pasa ACL al listener, verifica roles en comandos
Modelo de acceso
Tres niveles de control, cada uno incrementa la restriccion:
- Nivel 1 — Allowlist de usuarios (quien puede hablar con el bot)
- Nivel 2 — Invite gating (quien puede invitar al bot a una sala)
- Nivel 3 — RBAC por accion (quien puede ejecutar que comandos/acciones)
Nivel 1 — Allowlist en FiltersCfg
Agregar allowed_users a FiltersCfg:
// internal/config/schema.go
type FiltersCfg struct {
// ... existente ...
AllowedUsers []string `yaml:"allowed_users"` // allowlist (vacio = todos)
}
Config YAML:
matrix:
filters:
allowed_users:
- "@admin:matrix-af2f3d.organic-machine.com"
- "@enmanuel:matrix-af2f3d.organic-machine.com"
# vacio o ausente = sin restriccion (todos pueden hablar)
Verificacion en shouldHandle():
// Despues de los filtros existentes, antes de return true
if len(f.AllowedUsers) > 0 {
allowed := false
for _, u := range f.AllowedUsers {
if evt.Sender.String() == u {
allowed = true
break
}
}
if !allowed {
l.logger.Debug("ignoring unauthorized user", "sender", evt.Sender)
return false
}
}
Nivel 2 — Invite gating
Modificar el handler de StateMember invite para verificar quien invita:
// shell/matrix/listener.go — en el handler de invites
if membership != event.MembershipInvite {
return
}
// Verificar si el invitante esta autorizado
if len(l.cfg.Filters.AllowedUsers) > 0 {
allowed := false
for _, u := range l.cfg.Filters.AllowedUsers {
if evt.Sender.String() == u {
allowed = true
break
}
}
if !allowed {
l.logger.Info("rejecting invite from unauthorized user",
"room", evt.RoomID, "inviter", evt.Sender)
// Opcion: leave room o simplemente no joinear
return
}
}
// Auto-join (existente)
l.client.raw.JoinRoom(ctx, evt.RoomID.String(), "", nil)
Nivel 3 — RBAC por accion (conectar SecurityCfg.Roles)
Nuevo paquete pkg/acl/ (puro)
// pkg/acl/types.go
// Role representa un rol con sus usuarios y acciones permitidas.
type Role struct {
Name string
Users []string // Matrix user IDs, "*" = todos
Actions []string // acciones permitidas, "*" = todas
}
// ACL contiene la lista de control de acceso resuelta.
type ACL struct {
Roles []Role
}
// pkg/acl/check.go
// FromConfig construye un ACL desde el mapa de roles del config. Puro.
func FromConfig(roles map[string]config.RoleCfg) ACL { ... }
// RoleFor devuelve el nombre del rol con mayor privilegio para un userID. Puro.
// Prioridad: el primer rol especifico que matchee; si ninguno, busca "*".
// Si no hay match, retorna "" (sin rol = sin acceso si RBAC esta activo).
func (a ACL) RoleFor(userID string) string { ... }
// CanDo verifica si un userID puede ejecutar una accion. Puro.
// Si no hay roles definidos, retorna true (sin RBAC = acceso libre).
// Si hay roles pero el usuario no tiene ninguno, retorna false.
func (a ACL) CanDo(userID string, action string) bool { ... }
// AllowedUsers retorna la lista consolidada de todos los userIDs
// con al menos un rol (excluyendo "*"). Util para allowlist. Puro.
func (a ACL) AllowedUsers() []string { ... }
Integracion en runtime.go
// agents/runtime.go — en handleEvent, despues de evaluar el comando
// Para comandos built-in, verificar accion "command:<name>"
if handler, ok := a.commands[msgCtx.Command]; ok {
if !a.acl.CanDo(msgCtx.SenderID, "command:"+msgCtx.Command) {
a.matrix.SendText(ctx, roomID, "No tienes permisos para este comando.")
return
}
reply := handler(ctx, msgCtx)
// ...
}
// Para tool commands, verificar accion "tool:<name>"
// Para LLM fallback, verificar accion "ask" (o la que corresponda)
Mapeo de acciones
| Accion config | Que protege |
|---|---|
* |
Todo (wildcard) |
ask |
Hablar con el LLM (mensajes de texto libre) |
command:* |
Todos los comandos !xxx |
command:tool |
Ejecutar !tool |
command:clear |
Ejecutar !clear |
tool:* |
Todas las tools via LLM |
tool:ssh_command |
Tool SSH especifica |
help |
Comandos informativos (!help, !info, !status) |
Retrocompatibilidad
- Si
allowed_usersesta vacio → sin restriccion (como ahora) - Si
security.rolesesta vacio → sin RBAC (como ahora) - El comportamiento por defecto NO cambia — todo sigue abierto a menos que se configure
Respuesta a usuarios no autorizados
Dos estrategias configurables:
- Silent (default): ignorar mensajes de usuarios no autorizados (como si el bot no existiera)
- Explicit: responder con "No tienes permisos para interactuar con este agente"
matrix:
filters:
allowed_users: [...]
unauthorized_response: "silent" # silent | explicit
Tareas de implementacion
Fase 1 — Allowlist basica (Nivel 1)
- Agregar
AllowedUsers []stringaFiltersCfgeninternal/config/schema.go - Agregar
UnauthorizedResponse stringaFiltersCfg(silent|explicit) - Implementar check de allowlist en
shouldHandle()deshell/matrix/listener.go - Si
unauthorized_response: explicit, responder antes de retornar false - Tests: shouldHandle con allowlist vacia (pasa todo), con lista (filtra)
Fase 2 — Invite gating (Nivel 2)
- Modificar handler de
StateMemberinvite en listener.go - Verificar invitante contra
allowed_usersantes de auto-join - Si no autorizado: no joinear (y opcionalmente leave/reject)
- Log de invites rechazados
Fase 3 — RBAC puro (Nivel 3)
- Crear
pkg/acl/types.go— tipos Role, ACL - Crear
pkg/acl/check.go— FromConfig, RoleFor, CanDo, AllowedUsers - Crear
pkg/acl/check_test.go— tests exhaustivos del ACL puro - Tests: wildcard "" en users, wildcard "" en actions, sin roles, multiples roles
Fase 4 — Conectar RBAC al runtime
- Construir ACL en
agents/runtime.goNew() desdecfg.Security.Roles - Verificar permisos antes de ejecutar comandos built-in
- Verificar permisos antes de ejecutar tools (via LLM y via !tool)
- Verificar permiso "ask" antes de enviar al LLM
- Respuesta de "sin permisos" respetuosa cuando se deniega
Fase 5 — Config y documentacion
- Actualizar configs de assistant-bot y asistente-2 con ejemplo de allowed_users
- Documentar en
docs/creating-agents.mdla seccion de control de acceso - Verificar que agentes sin security config siguen funcionando (retrocompat)
Fase 6 (futura) — Extensiones
- Audit log: registrar intentos de acceso denegados en audit log
- Patron glob en users:
@*:matrix-af2f3d.organic-machine.com(solo usuarios locales) - Rate limiting por rol (admin sin limite, user con rate limit)
- Comando
!aclpara admins: ver roles activos, verificar permisos de un usuario