# 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: ```yaml 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`: ```go // internal/config/schema.go type FiltersCfg struct { // ... existente ... AllowedUsers []string `yaml:"allowed_users"` // allowlist (vacio = todos) } ``` Config YAML: ```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()`: ```go // 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: ```go // 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) ```go // 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 } ``` ```go // 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 ```go // agents/runtime.go — en handleEvent, despues de evaluar el comando // Para comandos built-in, verificar accion "command:" 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:" // 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" ```yaml 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