Files
agents_and_robots/dev/issues/completed/0010-access-control.md
T
egutierrez 6542e7d51c chore: cerrar issue 0010 — access control completado
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>
2026-03-07 19:02:26 +00:00

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

  • SecurityCfg ya existe en internal/config/schema.go con Roles (map de rol -> users + actions)
  • FiltersCfg ya tiene ignore_users (blocklist) y min_power_level
  • shouldHandle() en shell/matrix/listener.go filtra por room y blocklist, pero NO por allowlist
  • Auto-join de invites es incondicional (acepta cualquier invite)
  • MatchMinPowerLevel() en pkg/decision/engine.go existe pero powerLevel siempre 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.roles no 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:

  1. Nivel 1 — Allowlist de usuarios (quien puede hablar con el bot)
  2. Nivel 2 — Invite gating (quien puede invitar al bot a una sala)
  3. 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_users esta vacio → sin restriccion (como ahora)
  • Si security.roles esta 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:

  1. Silent (default): ignorar mensajes de usuarios no autorizados (como si el bot no existiera)
  2. 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 []string a FiltersCfg en internal/config/schema.go
  • Agregar UnauthorizedResponse string a FiltersCfg (silent | explicit)
  • Implementar check de allowlist en shouldHandle() de shell/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 StateMember invite en listener.go
  • Verificar invitante contra allowed_users antes 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.go New() desde cfg.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.md la 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 !acl para admins: ver roles activos, verificar permisos de un usuario