Política de permisos¶
Novedades en v1.6.0
- El límite conocido 10 se cierra, salvo un caso. En Odoo 10 a 12, la
sesión vigilada decide con los campos del modelo las claves de un objeto
pasado por posición a un método de negocio: una clave que no es campo del
modelo ni clave de contexto permitida se rechaza (regla
context), y si los campos no se pueden leer, la llamada se rechaza (reglaunverified). Lo que queda: una clave de contexto con el nombre de un campo del modelo se lee como un valor. Ver Límites conocidos. - Nuevo digest:
readonly,standardybuilderrechazan un poco más, y cambia la versión de los datos de niveles que entra en el digest, así que el digest de cada perfil cambia (standardsin archivo:21524d1933f3).fullno cambia de comportamiento. Siguen siendo 28 herramientas. odoo_versionnombra la copia de odoo-tools que responde (bloqueplugin), también con la política bloqueada. Una respuesta sinpolicyviene de una copia anterior a la 1.5.0, sin política de permisos. Ver Qué reportan las herramientas.
Novedades en v1.5.1
- Un contexto pasado por posición, en Odoo 10 a 12: esas series también toman el contexto de una llamada del argumento posicional que sigue a los parámetros del método. Las reglas de contexto se aplican ahora a ese argumento: en la capa A en todas las series y, en 10 a 12, otra vez en la sesión vigilada. Lo que quedaba abierto es el límite conocido 10, que la v1.6.0 cierra salvo un caso.
- Nuevo digest:
readonly,standardybuilderrechazan un poco más, y cambia la versión de los datos de niveles que entra en el digest, así que el digest de cada perfil cambia (standardsin archivo:9f3166860347).fullno cambia de comportamiento. Siguen siendo 28 herramientas. - El hook de inicio de sesión ya no lee las opciones de la política: avisa cuando el último arranque del servidor quedó bloqueado, con su código E, a partir del diagnóstico local del servidor.
Novedades en v1.5.0
- Política de permisos: el servidor MCP aplica un perfil de permisos
encima de los derechos de acceso del usuario de Odoo. Por defecto,
standard: lee, crea, actualiza y corre métodos de negocio, pero no borra, no hace cambios estilo Studio, no envía correo ni SMS (las notas internas sí), no cambia usuarios, permisos, módulos, ajustes, cuentas bancarias, secuencias fiscales ni el historial del chatter, y no lee secretos guardados. - Dos opciones nuevas: Permission profile (
odoo_policy) y Permission policy file (odoo_policy_file), ambas con valor por defecto. - Registro de auditoría: una línea JSON por llamada a herramienta, rechazos incluidos, sin valores ni secretos.
- Para conservar el comportamiento de la 1.4, elige el perfil
full.
El servidor MCP de odoo-tools aplica una política de permisos encima de los derechos de acceso del propio usuario de Odoo. Es un techo: nunca concede lo que Odoo rechaza, solo quita derechos, y nunca es más débil que Read-only mode. Funciona igual en Community, Enterprise y Odoo Online, porque corre en el servidor MCP y no necesita ningún módulo de Odoo. Las reglas de acceso, las reglas de registro y los grupos de campos de Odoo siguen aplicando siempre.
La referencia autoritativa es
docs/policy.md
del repositorio; esta página la resume y la explica.
Las dos opciones¶
| Opción del plugin | Variable de entorno | Valor por defecto | Qué es |
|---|---|---|---|
Permission profile (odoo_policy) |
ODOO_POLICY |
standard |
readonly, standard, builder o full |
Permission policy file (odoo_policy_file) |
ODOO_POLICY_FILE |
vacío | ruta absoluta opcional a un archivo JSON con reglas por modelo encima del perfil |
Se cambian en /plugin → odoo-tools → Configure options. En un
contenedor o en un servidor standalone se usan las variables de entorno.
Para conservar el comportamiento de la 1.4, elige full
El perfil por defecto se endurece dentro de esta versión menor. Si quieres
que el agente conserve todos los derechos que tiene el usuario de Odoo (por
ejemplo, en la instancia de tu propia empresa), pon Permission profile en
full (ODOO_POLICY=full en un contenedor o en una ejecución
standalone). full sigue rechazando la lista corta NUNCA
(cambios de API keys y desinstalación de módulos) y conserva las dos
revisiones de argumentos que la hacen cumplir (ver Perfiles).
La política se lee una sola vez, cuando arranca el servidor. Un cambio necesita
reconectar el servidor (/mcp en Claude Code); el nuevo digest aparece
entonces en odoo_version y en la línea de inicio de la auditoría.
Qué protege y qué no¶
En el modo local (stdio), la política de permisos limita lo que Claude puede hacer a través de las herramientas MCP de odoo-tools. Protege contra errores y contra instrucciones escondidas en los datos que Claude lee. No limita a la persona que tiene la API key, y no es un sandbox para un agente con acceso sin restricciones a la shell del mismo equipo. La frontera que resiste frente a todos es el propio usuario de Odoo: dale al agente un usuario de Odoo dedicado y de mínimo privilegio (no un administrador) y una API key con caducidad. En un futuro modo remoto, donde el servidor guarde la credencial de Odoo, la misma política se volverá una aplicación real de permisos.
Tampoco trabaja a nivel de registro (las reglas de registro son de Odoo) ni es
un sandbox de lo que un método de negocio hace dentro de Odoo (ver
Límites conocidos). odoo_version advierte (W10) cuando el
usuario de Odoo es administrador (base.group_system): los derechos de acceso de
Odoo entonces no acotan nada, y esta política es la única capa que acota.
Operaciones¶
| Operación | Significado | Viene de |
|---|---|---|
read |
Datos de registros: buscar, leer, contar, agrupar, exportar, traducciones, adjuntos de un registro | las herramientas de lectura; métodos de clase lectura por odoo_execute |
create |
Crear registros, incluidos copy, name_create y las importaciones que insertan filas |
odoo_create, las importaciones, métodos de clase creación |
write |
Cambiar registros existentes, incluidos archivar, traducciones e importaciones que actualizan filas | odoo_write, odoo_translate_set, las importaciones, métodos de clase escritura |
unlink |
Borrar registros, incluidos los comandos x2many que borran líneas | odoo_unlink, unlink por odoo_execute, comandos x2many |
call |
Correr un método del modelo por odoo_execute, opcionalmente limitado a una lista de métodos |
solo odoo_execute |
schema |
Un cambio estilo Studio en un modelo (un campo nuevo x_, una automatización sobre el modelo) |
odoo_add_field, odoo_add_automation |
inspect |
Metadatos de campos y revisiones de acceso de un modelo | odoo_fields_get, odoo_api_catalog, odoo_access_check, y las llamadas fields_get propias de las herramientas |
inspectnunca lo concede una regla: se permite en un modelo cuando cualquier otra operación está permitida ahí. Un modelo con todas sus operaciones rechazadas (el nivelsecrets) también oculta su lista de campos.- Exportar es
read: una exportación devuelve lo mismo queodoo_search_read. - Las importaciones necesitan
createywrite, porque elload()de Odoo inserta y actualiza.
Perfiles¶
| Operación por clase de modelo | readonly |
standard (por defecto) |
builder |
full |
|---|---|---|---|---|
| read, modelos de negocio | sí | sí | sí | sí |
| create, write, modelos de negocio | no | sí | sí | sí |
| unlink, modelos de negocio | no | no | no | sí |
| call, métodos de negocio | no | sí | sí | sí |
| call de un método de salida | no | solo notas internas | solo notas internas | sí |
| schema, modelos de negocio | no | no | sí | sí |
read de modelos security, outbound y customization |
sí | sí | sí | sí |
cualquier cambio en modelos security y outbound |
no | no | no | sí |
cualquier cambio en modelos customization |
no | no | sí | sí |
cualquier cosa en modelos secrets |
no | no | no | sí |
| campos con aspecto de credencial | retenidos | retenidos | retenidos (se liberan los campos de código de las automatizaciones) | se devuelven |
| reglas de adjuntos, identidad, enlace de delegación y contexto | activas | activas | activas | apagadas |
| NUNCA, las revisiones de nombres y el sobre del guard | activas | activas | activas | activas |
formas de argumentos y consultas de modelos relacionados (reglas shape y unverified) |
activas | activas | activas | activas |
Dos hechos que conviene tener presentes:
builderequivale afullen confidencialidad e integridad. Deja que el agente cree acciones de servidor y automatizaciones: Python que corre en el servidor de Odoo, consudo()disponible, quizá después como el administrador que lo dispare. Ese código puede leer un parámetro del sistema y copiarlo a un campo legible. Los niveles bajobuilderfrenan errores a través de las herramientas genéricas; no frenan a un agente que escribe código de automatización. Usabuildersolo en sandboxes y staging. Cargarlo emite la advertencia W6.fulles el comportamiento de la 1.4, salvo NUNCA, las revisiones de nombres, el sobre del guard (que solo frena un error del propio odoo-tools) y dos revisiones de argumentos de las que dependen NUNCA y un archivo de política. Por eso unas pocas llamadas que la 1.4 enviaba necesitan otra forma:- se rechazan los argumentos que la política no puede leer con certeza
(regla
shape), por ejemplo más de tres argumentos posicionales aread_groupporodoo_execute(las posiciones posteriores cambian entre series de Odoo), o una palabra clave que no conoce en un método de lectura o escritura (los argumentos de un método de negocio no se leen). Pasa los argumentos por nombre, o usa la herramienta dedicada; - las definiciones de campos detrás de una ruta con puntos, y detrás de un
valor que puede ser un comando x2many (una lista,
falseonullen una escritura), se leen primero confields_get: un comandoapi_key_idsenres.usersllega ares.users.apikeys, que NUNCA cierra. Si Odoo rechaza esa consulta, la llamada se rechaza (reglaunverified). Un nombre de campo simple, o un valor simple, no necesita consulta.
- se rechazan los argumentos que la política no puede leer con certeza
(regla
standard rechaza unlink pero permite call: borrar es el único verbo
genérico irreversible (Odoo no tiene papelera, y archivar es el camino
reversible que sugiere el rechazo), mientras que rechazar call rompería
action_confirm, action_post y los flujos de asistentes. En su lugar se
cierran los métodos de salida. Para producción con un usuario administrador, el
ejemplo endurecido limita call a listas de métodos.
Herramientas que lista cada perfil¶
tools/list oculta una herramienta que la política prohíbe en todos los
modelos, igual que Read-only mode oculta las herramientas de escritura. Ocultar
solo aproxima por exceso: una herramienta listada todavía puede rechazarse para
un modelo concreto, y una herramienta oculta que se llama por su nombre devuelve
un error que explica por qué (nunca una herramienta desconocida). Read-only mode
(odoo_readonly, ODOO_READONLY=1) oculta las 9 herramientas de escritura en
todos los perfiles.
| Perfil | Herramientas no listadas | Listadas |
|---|---|---|
readonly |
odoo_create, odoo_write, odoo_unlink, odoo_execute, odoo_translate_set, odoo_import_preview, odoo_import, odoo_add_field, odoo_add_automation |
19 |
standard |
odoo_unlink, odoo_add_field, odoo_add_automation |
25 |
builder |
ninguna | 28 |
full |
ninguna | 28 |
| cualquier perfil con Read-only mode activo | odoo_create, odoo_write, odoo_unlink, odoo_execute, odoo_translate_set, odoo_import_preview, odoo_import, odoo_add_field, odoo_add_automation |
19 |
Un archivo de política puede ocultar más: una herramienta en tools.disabled
deja de listarse.
Las instrucciones del servidor ganan una línea que nombra el perfil, su digest
y las herramientas ocultas, y dice que un rechazo que nombra la política de
permisos es definitivo. Fuera de full, la descripción de cada herramienta que
recibe un modelo termina con un recordatorio corto del perfil, por ejemplo
[Policy 'standard': no deletes or secrets; users, rights, mail-out and
customization read-only. See odoo_version.].
Niveles sensibles¶
Cuatro niveles (tiers) están cerrados por defecto. Un modelo pertenece a un
nivel por su nombre exacto o por un patrón prefijo.* (que abarca los nombres
bajo el prefijo, no el prefijo mismo), por sus alias y por sus padres de
delegación. Cuando cualquier nivel de un modelo retiene una operación, la
operación se rechaza, y el rechazo nombra el primer nivel en este orden. Los
nombres que no existen en una versión nunca coinciden; los marcados
best-effort se confirman contra bases reales. Las columnas dicen qué permite
cada perfil dentro del nivel; full lo permite todo.
| Nivel | Modelos | readonly |
standard |
builder |
Por qué |
|---|---|---|---|---|---|
secrets |
ir.config_parameter, res.config, res.config.*, ir.mail_server, fetchmail.server, payment.provider, payment.acquirer, payment.token, iap.account, res.users.apikeys.show, res.company.ldap, auth_totp.device (best-effort), l10n_mx_edi.certificate (best-effort), certificate.certificate (best-effort), certificate.key (best-effort), account.online.link (best-effort), account.online.account (best-effort), mail.mail |
nada | nada | nada | Guardan credenciales, o valores que actúan como credenciales (parámetros del sistema, contraseñas de servidores de correo y LDAP, llaves y tokens de proveedores de pago, tokens de IAP, una API key nueva, tokens de sincronización bancaria, certificados y llaves CFDI, correos de restablecimiento en cola). Leerlos ya es el daño. |
security |
res.users, res.users.*, res.groups, res.groups.*, ir.model.access, ir.rule, ir.module.module, base.module.*, change.password.*, portal.wizard, portal.wizard.*, auth_totp.wizard, auth.oauth.provider, res.partner.bank, base.partner.merge.automatic.wizard, mail.message, mail.tracking.value, ir.sequence, ir.sequence.date_range |
lectura | lectura | lectura | Identidad y permisos, redirección del inicio de sesión, cuentas bancarias, fusiones de contactos, historial del chatter y secuencias fiscales: la lectura sigue abierta; los cambios necesitan el perfil full o una entrada que nombre el modelo exacto. |
outbound |
mail.compose.message, mail.followers, mail.template, sms.sms, sms.composer, sms.template, portal.share, mailing.* (best-effort), snailmail.letter, account.move.send, account.move.send.* (best-effort), account.invoice.send, mail.mail |
lectura | lectura | lectura | Envían correo, SMS, cartas o enlaces para compartir, o deciden qué se envía. |
customization |
ir.*, base.automation, base.action.rule, studio.*, base_import.import, base.language.install, base.language.import, excepto ir.attachment |
lectura | lectura | todo | Acciones de servidor, automatizaciones, tareas programadas, vistas, assets, campos manuales, valores por defecto y otros registros ir.* corren código o cambian cómo se comporta Odoo después, y los motores de importación masiva y de idiomas insertan, actualizan o instalan entre modelos; necesitan el perfil builder. |
Por qué se cierran los modelos customization:
ir.actions.server,ir.cronybase.automationcorren Python en disparadores posteriores, como otros usuarios, consudo()disponible;ir.server.object.linesguarda expresiones Python.- El QWeb de
ir.ui.viewse compila a código (un script guardado en el sitio web público está a una escritura de distancia);ir.assetinyecta JavaScript y CSS. - Un
ir.model.fieldsmanual puede llevar códigocompute;ir.model.datareapunta identificadores externos;ir.defaultfija valores por defecto en cualquier modelo;ir.property(hasta la 17) guarda valores por compañía como las cuentas por defecto. ir.filters,ir.embedded.actions,ir.ui.menu,ir.actions.*(redirecciones de URL, reportes impresos) eir.translation(hasta la 15). Como prefijo,ir.*cubre también los modelos que nadie listó.base_import.import(el motor de importación masiva) y los asistentes de idioma están en este nivel y solo se alcanzan por sus herramientas, así queodoo_executeno puede llevar elload()del importador a un modelo cerrado.
Los traslapes son intencionales: ir.config_parameter e ir.mail_server son a
la vez ir.* y secrets, así que builder los mantiene cerrados;
ir.model.access, ir.rule, ir.module.module e ir.sequence son a la vez
ir.* y security, así que builder no puede cambiar permisos, instalar
módulos ni renumerar facturas.
Comportamiento en la v1.5.0: account.journal
En la v1.5.0, account.journal (con su cuenta bancaria y sus secuencias)
es un modelo de negocio: sus escrituras no pertenecen a ningún nivel.
Si esas escrituras se suman a un nivel se decidirá tras la verificación en
vivo de la facturación CFDI con la v1.5.0.
NUNCA¶
Ningún perfil y ninguna regla permiten esto, full incluido. El cargador
rechaza un archivo que lo intente (E18).
| Modelo | Operaciones | Por qué |
|---|---|---|
res.users.apikeys |
create, write, unlink, call | odoo-tools nunca crea, cambia ni revoca API keys |
res.users.apikeys.description |
read, create, write, unlink, call, schema | el asistente de creación de API keys |
ir.module.module |
call (button_immediate_uninstall, button_uninstall, module_uninstall) |
desinstalar un módulo borra sus datos |
base.module.uninstall |
read, create, write, unlink, call, schema | el asistente de desinstalación |
Los métodos privados no necesitan entrada: la revisión del nombre del método
(^[A-Za-z][A-Za-z0-9_]{0,127}$) rechaza un _ inicial en todos los
transportes, igual que Odoo. Los nombres de modelo deben cumplir
^[a-z0-9_.]{1,128}$.
Reglas integradas¶
El perfil full apaga las reglas de enlace de delegación, de ir.attachment,
de identidad, de métodos de salida y de contexto que siguen. Dos partes siguen
activas en todos los perfiles, porque NUNCA y las reglas de un archivo de
política aplican también bajo full: un hijo de delegación sigue tomando la
decisión de su padre, y los registros relacionados se siguen revisando.
Delegación (_inherits)¶
Un modelo hijo que delega en un padre cerrado escribe los campos del padre:
website.page delega en ir.ui.view, así que escribir su arch escribe una
vista QWeb. Por eso un hijo hereda los niveles de su padre para toda operación
excepto read: las escrituras en website.page necesitan builder, mientras
que la lectura sigue abierta. Los hijos son website.page y
website.controller.page (ir.ui.view), ir.cron y base.automation en
11-16 (ir.actions.server), y product.document y documents.document
(ir.attachment).
Comportamiento en la v1.5.0: la decisión del padre incluye las reglas del archivo
La decisión completa del padre aplica también al hijo, reglas del archivo
incluidas: una regla exacta sobre website.page (o ir.cron) no abre por
sí sola sus escrituras. Abre también el padre (ir.ui.view, o
ir.actions.server), o usa el perfil builder.
El campo de enlace de un hijo (view_id, ir_attachment_id, etc.) no se puede
fijar por las herramientas genéricas (regla delegation-link): apuntarlo a un
registro existente expondría o cambiaría ese registro a través del hijo.
ir.attachment queda anclado a su padre¶
ir.attachment guarda los campos binarios de otros modelos, incluidas llaves de
certificados, así que una lectura genérica podría alcanzar el nivel secrets por
la puerta de atrás. Una llamada genérica sobre ir.attachment debe nombrar su
padre:
- Una búsqueda (todo método de lectura del modelo que recibe un dominio de
primer nivel:
search,search_read,search_count,read_group,formatted_read_group,web_search_read,web_read_group,name_search,web_name_search,read_grouping_sets,formatted_read_grouping_sets,search_fetchy los lectores de pivote y panelread_progress_bar,search_panel_select_range,search_panel_select_multi_range) necesita un["res_model", "=", X]o["res_model", "in", [...]]de primer nivel cuyos modelos permitan todosread, y ninguna hoja sobreres_field. El guard agrega["res_field", "=", False], así que una búsqueda nunca ve los archivos guardados en campos. Un lectorsearch_panel_*no lleva un dominio de primer nivel, así que sobreir.attachmentnunca puede nombrar al padre y se rechaza. - Una llamada por ids lee primero
res_modelyres_fieldde esos ids (una lectura interna que el agente no recibe). Ningún id puede tenerres_field, y cadares_modeldebe permitirread(para lecturas) owrite(para cambios). - Una creación (o una escritura de estos valores) no puede ser
publicni fijarres_field; sures_modeldebe permitirwrite. Esto vale también cuando la escritura llega air.attachmentpor un hijo de delegación:product.documentydocuments.documentexponenpublic,res_fieldydatasdel padre, así que crear o volver público un adjunto descargable por cualquiera a través de uno de ellos se rechaza, igual que directamente enir.attachment.
Los adjuntos de campos se siguen leyendo por su modelo padre (odoo_read de
image_1920 en product.template), y odoo_record_documents sigue
funcionando con sus propias llamadas internas ancladas.
Identidad (el correo de contacto de un usuario)¶
El enlace de restablecimiento de contraseña de un usuario va al correo de su
contacto (partner), así que cambiar ese correo por res.partner equivaldría a
tomar la cuenta. Un write sobre res.partner que fija email hace que el
guard lea antes user_ids de esos contactos; si alguno es un usuario
(usuarios de portal incluidos), la llamada necesita también write sobre
res.users, que el nivel security retiene.
Comportamiento en la v1.5.0: importaciones que actualizan contactos
Una importación a res.partner con una columna email y una columna id
o .id (una actualización de contactos existentes) se rechaza, y el
rechazo apunta a odoo_write, que revisa cada contacto.
Métodos de salida¶
Un método de negocio puede enviar datos fuera: message_post con un contacto
que el agente eligió le escribe a quien sea, y message_subscribe reenvía cada
mensaje futuro. Estos métodos se rechazan como call salvo que una entrada que
nombre el modelo exacto los liste, con una razón:
action_send_and_print, action_send_mail, message_mail_with_source,
message_notify, message_post, message_post_with_source,
message_post_with_template, message_post_with_view, message_subscribe,
send_and_print_action, send_mail
Las notas internas están permitidas. message_post pasa como un call
simple cuando sus argumentos con nombre llevan subtype_xmlid
"mail.mt_note" (13+) o subtype "mail.mt_note" (10-12), no hay
partner_ids, message_type falta o es "comment", y no se pasa ningún otro
argumento posicional. Una nota interna no le notifica a nadie por correo.
Claves de contexto¶
Una llamada solo puede llevar estas claves de contexto, más default_<campo>
con un valor simple (texto, número, booleano o nulo) sobre un campo que no esté
retenido:
active_id, active_ids, active_model, active_test, allowed_company_ids,
bin_size, button_validate_picking_ids, lang, location,
mail_create_nosubscribe, mo_ids_to_backorder, picking_ids_not_to_backorder,
read_group_expand, skip_backorder, skip_immediate, tz, uid,
validate_analytic, warehouse
Los asistentes de validación de inventario y manufactura necesitan algunas de
estas claves: sin button_validate_picking_ids o skip_backorder, por ejemplo,
el asistente devuelve True y no valida nada. El botón Confirmar/Publicar de la
factura envía validate_analytic (16+), que account.move._post lee para forzar
la validación de la distribución analítica, y el cliente web de Odoo pone el id
del usuario conectado en uid en cada llamada; es informativo, porque el usuario
que actúa es la sesión autenticada, no context['uid']. Ninguna de las claves
permitidas suprime el seguimiento del chatter. tracking_disable,
mail_notrack y mail_create_nolog faltan a propósito: silencian el
seguimiento del chatter que el nivel security protege como evidencia.
Comportamiento en la v1.5.0: un default_<campo> de contexto debe ser un valor simple
Bajo readonly, standard y builder, un default_<campo> que contiene
una lista o un objeto se rechaza (regla context) antes de enviar nada a
Odoo, para toda herramienta, método y modelo, y dondequiera que esté el
contexto (kwargs["context"], un contexto anidado en una especificación de
lectura, o uno anidado en el fields_spec de onchange): comandos x2many
([(0, 0, {...})], [(4, id)], [(6, 0, ids)]), una lista de ids, valores
anidados, incluso una lista vacía. Odoo aplica un valor por defecto de
contexto a cada registro creado mientras corre la llamada: el que nombra la
llamada, las líneas que crean sus comandos x2many y los registros de otros
modelos que crea un método de negocio, un asistente o una importación
(action_confirm, un asistente de validación, load). Un valor así no se
puede revisar contra el único modelo que nombra la llamada. El rechazo nombra
la clave, nunca su valor. Pasa esos valores en los values del registro,
donde cada comando se decide contra el modelo al que llega, o usa el perfil
full. Por ejemplo, crea el asistente de backorder de manufactura con sus
líneas en values:
{"model": "mrp.production.backorder", "method": "create",
"args": [{"mrp_production_ids": [[6, 0, [5]]],
"mrp_production_backorder_line_ids": [[0, 0, {"mrp_production_id": 5,
"to_backorder": true}]]}]}
en lugar de las claves default_mrp_production_ids y
default_mrp_production_backorder_line_ids de su acción, y el asistente de
backorder de inventario con pick_ids: [(4, id)] en values en lugar de
default_pick_ids.
Además, un default_<campo> simple que nombra un campo estructural se
rechaza por construcción en una llamada que puede crear un registro (no en
una lectura, donde no llena nada): el campo de enlace de una delegación cuyo
padre es sensible (default_ir_attachment_id, default_view_id y los demás
enlaces a ir.attachment, a un nivel o a un modelo NUNCA), y
default_public o default_res_field. Un valor por defecto inocuo
(default_move_type: "out_invoice") sigue corriendo, salvo que su campo esté
retenido. default_res_model y default_res_id siguen permitidos, porque la
programación de actividades y el redactor de correo los usan (ver el límite
conocido 9). full no cambia.
Desde la v1.5.1, estas reglas se aplican también a un objeto pasado por posición, que Odoo 10 a 12 toman como contexto, y desde la v1.6.0 la sesión vigilada decide con los campos del modelo las demás claves de un objeto así: ver el límite conocido 10.
Registros relacionados: comandos x2many y rutas de campos¶
Una escritura se revisa para cada registro que cambia, no solo para el modelo que
nombra. En values (create, write, los valores por defecto de copy,
web_save):
| Comando | one2many | many2many |
|---|---|---|
(0, _, vals) |
create en el comodelo, luego sus vals |
igual |
(1, id, vals) |
write en el comodelo, luego sus vals |
igual |
(2, id) |
unlink en el comodelo | igual |
(3, id), (5,), (6, _, ids), una lista de ids, False |
en un write: unlink en el comodelo (Odoo borra las líneas quitadas cuando el inverso es ondelete='cascade'); en un create: write en el comodelo |
write en el comodelo cuando un nivel, una regla del archivo o NUNCA rechaza write ahí; si no, read |
(4, id) |
write en el comodelo (cambia de padre la línea) | como arriba |
| cualquier otra cosa, profundidad mayor a 4, o un campo que Odoo no reporta mientras lleva comandos | rechazado (shape) |
igual |
Así, en res.partner, user_ids: [(1, uid, {"groups_id": ...})] es una
escritura en res.users y en res.groups, los cambios de bank_ids son
escrituras en res.partner.bank, y order_line: [(5,)] es un borrado bajo
standard.
Comportamiento en la v1.5.0: enlaces many2many a usuarios
Un enlace many2many a un modelo cuya escritura está retenida cuenta como un
cambio en él, y por eso se rechaza bajo standard, por ejemplo los grupos de
un usuario (res.users.groups_id) o los usuarios de una compañía
(res.company.user_ids). Dos enlaces que solo hacen referencia a usuarios
cuentan como una lectura de res.users y corren bajo standard: los
asignados de una tarea (project.task.user_ids) y los miembros de un equipo
de helpdesk (helpdesk.team.member_ids). Cualquier otro many2many hacia
res.users sigue contando como escritura.
Las lecturas se revisan a lo largo de sus rutas: cada salto de una hoja de
dominio (incluidos any y not any), la specification anidada de web_read y
web_search_read, las rutas de export_data, las columnas de importación y los
términos con puntos de groupby y order. Cada modelo alcanzado debe permitir
read, y ningún segmento puede ser un campo retenido. En las columnas de
importación, una ruta hacia un one2many (order_line/product_id) crea y escribe
líneas en el comodelo. Cuando no se pueden leer los metadatos de campos que
necesitan estas revisiones, la llamada se rechaza (regla unverified), en todos
los perfiles.
Campos con aspecto de credencial¶
Fuera de full, un campo cuyo nombre coincide con
(^|_)(password|passwd|pass|pwd|secret|token|apikey|api_key|private_key|privkey|credentials?)($|_),
o que es signup_url, smtp_pass o totp_secret, queda retenido. No lo
está un nombre que denota una relación o un conteo de tokens en lugar de un
valor: los nombres exactos payment_token_id, payment_token_ids y
token_count, y cualquier nombre que termine en token_id, token_ids o
token_count (así token_id, suitable_payment_token_ids y
payment_token_count también). Solo hacen referencia a payment.token, del
nivel secrets, cuyos valores siguen sellados. El code de
ir.actions.server, ir.cron y base.automation, el webhook_url de las
acciones de servidor y el webhook_uuid de las automatizaciones también se
retienen, salvo bajo builder (las integraciones a menudo escriben tokens
ahí).
Un campo retenido se rechaza dondequiera que aparezca: listas de campos, el
field de las herramientas de traducción, claves de values (incluidos los
valores de comandos anidados), columnas de importación y rutas de exportación
(cada segmento), claves de specification a cualquier profundidad, groupby,
los campos origen de los agregados, order y cada segmento de una ruta de
dominio. Un filtro sobre un secreto lo filtraría adivinando de a poco. Las
lecturas pierden las claves retenidas a cualquier profundidad, y el resultado de
la herramienta las nombra en policy_withheld_fields (por ejemplo
["res.company.l10n_mx_edi_pac_password"]), así que nada desaparece en
silencio.
Todo método público que lee o filtra datos de registros pasa por estas
revisiones, no solo search/read: search_fetch, fetch, onchange,
read_progress_bar, search_panel_select_range,
search_panel_select_multi_range, copy_data, el web_name_search del cliente
web, los lectores de pivote read_grouping_sets y
formatted_read_grouping_sets, y filtered_domain son lecturas, así que sus
dominios, conjuntos de agrupación, campos y especificaciones se analizan y se
rechazan como cualquier otro. read_progress_bar agrupa por
progress_bar["field"] (y suma sum_field), una segunda dimensión de
agrupación que se revisa como su group_by; su resultado no se redacta, porque
sus claves son los valores distintos de esos campos (ya revisados), no nombres
de campos.
Un método que sigue siendo un call simple (un verbo de negocio como
action_confirm, o un método que ninguna versión de odoo-tools lista) tiene dos
respaldos. Su resultado se redacta a cualquier profundidad, igual que el de una
lectura. Y sus argumentos se revisan con los metadatos de campos del modelo antes
de enviarse: una cadena que es una ruta de campo del modelo se revisa como una
lectura de esa ruta (se rechaza, por ejemplo, mapped("provider_id.name") hacia
un proveedor de pago, o signup_token:count); un objeto cuyas claves son campos
se revisa como valores de escritura (salvo en onchange, una simulación que no
escribe); y un argumento con forma de dominio pasa por el análisis de rutas de
lectura. Una cadena que no es un campo del modelo (el cuerpo de un mensaje,
un asunto, el nombre de archivo de un adjunto, una palabra de estado) se deja
pasar, así que una nota interna y un verbo de negocio con un argumento de texto
siguen corriendo.
El archivo de política puede cambiar la lista: fields.withhold agrega nombres
(modelo.campo o *.campo, para minimizar datos), y fields.allow libera
nombres exactos modelo.campo, cada uno con una razón. withhold gana sobre
allow, que gana sobre la lista integrada.
Precedencia¶
decide(op, model, method) corre estos pasos en orden, y el primero que decide,
decide:
invalid-name: el modelo o el método no pasa su revisión de nombre.never:<id>: coincide una entrada NUNCA.readonly: Read-only mode está activo y la operación no esreadniinspect.locked: la política no se pudo cargar.tool-disabled: la herramienta está entools.disabled(se revisa por herramienta).file:<modelo>: una entrada exacta y vigente que fija la operación. Paracall,falserechaza, una lista decide por pertenencia (y abre los niveles y los métodos de salida que lista), ytrueabre los niveles del modelo paracallpero no los métodos de salida, que siguen necesitando una lista.tier:<nombre>: un nivel del modelo (el suyo, el de un alias o el de un padre de delegación) retiene la operación bajo el perfil.outbound-method: uncallde un método de salida que no es una nota interna.file:<patrón>: el patrón más específico que fija la operación (primero elprefijo.*más largo, luego*).profile:<nombre>: el valor por defecto del perfil.
Los pasos 7 y 8 nunca ven lo que permite un patrón, así que un patrón nunca
puede abrir un nivel ni un método de salida: solo puede hacerlo una entrada que
nombre el modelo exacto, con una razón. Agregar un false nunca convierte un
rechazo en un permiso.
Alias. Una regla sobre cualquiera de los dos nombres de un par renombrado o
fusionado aplica a ambos, en todas las versiones: account.invoice y
account.move, sus modelos .line, stock.quant.package y stock.package,
base.automation y base.action.rule, y los demás renombres del mapa de
compatibilidad. Un rechazo sobre cualquiera de los dos gana.
En Odoo 10 a 12, una regla sobre account.move (asientos) restringe también
account.invoice, y al revés.
El archivo de política¶
{
"version": 1,
"comment": "Acme production: confirm and post only, no imports",
"models": {
"*": {"call": false},
"sale.order": {"call": ["action_confirm", "action_cancel"]},
"account.move": {"call": ["action_post", "message_post"],
"reason": "Invoices are sent from the chatter by the billing agent"},
"res.partner.bank": {"create": true, "write": true,
"reason": "Ticket 4312: supplier onboarding", "expires": "2026-11-15"},
"hr.*": {"read": false, "create": false, "write": false, "call": false}
},
"fields": {
"withhold": ["hr.employee.private_email", "*.vat"],
"allow": [{"field": "sale.order.access_token", "reason": "Portal links for the sales agent"}]
},
"tools": {"disabled": ["odoo_import", "odoo_import_preview"]}
}
El archivo agrega reglas encima del perfil elegido en la opción y no lleva un
perfil propio. El esquema es estricto: cualquier clave desconocida es un error,
así que una errata no puede quitar un rechazo en silencio. Hay un esquema para
editores en
docs/policy.schema.json
(el servidor valida en código).
| Clave | Regla |
|---|---|
| nivel superior | version (requerida, 1), models, fields, tools, comment |
models |
las claves son un modelo exacto, prefijo.* o *; máximo 1000 entradas |
| una entrada | read, create, write, unlink, schema (true o false); call (true, false, o una lista de 1 a 200 nombres de métodos públicos); reason (de 10 a 200 caracteres); expires (AAAA-MM-DD); comment |
fields |
withhold: una lista de modelo.campo o *.campo; allow: una lista de {"field": "modelo.campo", "reason": "..."} |
tools |
{"disabled": [nombres de herramientas]} |
| lectura | máximo 256 KiB, UTF-8 (se acepta un BOM inicial); una clave duplicada es un error |
- Una entrada fija solo las operaciones que nombra. Las listas de métodos no se
combinan: decide la entrada más específica que fija
call. - Las aperturas necesitan una razón. Una entrada exacta que abre una
operación de un nivel, o que lista un método de salida, necesita un
reasonde al menos 10 caracteres (E15).reasonyexpiressolo aplican a entradas exactas (E19). - La caducidad falla cerrada. Después de su fecha (fin del día, UTC), los
permisos de una entrada (un valor
truey una lista de métodos decallpor igual) se ignoran y solo siguen aplicando sus rechazosfalse, así que la operación cae al perfil. Una lista de métodos caducada no se vuelve un rechazo general decall: los verbos de negocio que nombraba siguen funcionando bajostandard, y una entrada caducada nunca permite más que el mismo archivo sin sus permisos. Las aperturas aparecen enodoo_version(opened_sensitive), en--check-policyy en la línea de inicio de la auditoría. - El digest es un SHA-256 sobre el perfil, las reglas normalizadas, las reglas de campos, las herramientas desactivadas, la marca de Read-only mode y la versión de los datos de niveles, recortado a 12 caracteres hexadecimales. Los comentarios, las razones y el orden de las claves no lo cambian; cualquier valor de operación sí.
Dónde vive el archivo¶
- Solo una ruta absoluta explícita. El servidor no busca en ninguna ubicación por defecto, porque un agente podría crear ahí un archivo y hacer que se cargara en silencio.
- Recomendado:
~/.config/odoo-tools/policy.jsonen macOS y Linux,%APPDATA%\odoo-tools\policy.jsonen Windows. Mantenlo fuera de toda carpeta de proyecto y que solo tú puedas escribirlo (si no, advertencia W3). - En un contenedor:
-v ~/.odoo-tools/policy.json:/policy/policy.json:ro -e ODOO_POLICY_FILE=/policy/policy.json(ver Despliegue local).
Proteger el archivo y las opciones¶
Nada de esto es obligatorio, y en stdio nada de esto frena a un agente con una shell sin restricciones (ver Qué protege y qué no); hace más difícil desabrochar el cinturón:
- Reglas de permisos de Claude Code como
{"permissions": {"deny": ["Edit(~/.config/odoo-tools/**)", "Write(~/.config/odoo-tools/**)"]}}cubren las herramientas de archivos de Claude Code y los comandos de archivos que reconoce en Bash, no subprocesos arbitrarios. Mantén activas las confirmaciones de Bash. - Las opciones de plugin de archivos de proyecto se ignoran: Claude Code lee las
opciones de plugin solo de la configuración de usuario o administrada, así que
un repositorio clonado no puede poner el perfil en
full. - Las instrucciones del servidor le dicen al agente que un rechazo de la política es definitivo y que no debe buscar un rodeo por otra herramienta ni por la shell.
Errores de carga y advertencias¶
El servidor nunca se cae por una política mala. Un perfil desconocido, una ruta
relativa o un archivo inválido lo bloquean (locked): tools/list lista solo
odoo_version, que devuelve el bloque policy con el error sin contactar a
Odoo; cualquier otra herramienta se rechaza con un texto que nombra el error y
la opción que lo corrige, y dice que no se envió nada a Odoo; el log de arranque
y la línea de instrucciones llevan el error, y la línea de inicio de la
auditoría su código E. El diagnóstico local del servidor (server.jsonl) guarda
también ese código E, y en Claude Code el hook SessionStart del plugin lo lee
ahí: cuando el último arranque quedó bloqueado, te avisa con el código E como
máximo una vez al día (no lee las opciones de la política ni el archivo). Ver
Solución de problemas.
Los textos de error y de advertencia se muestran en inglés, tal como los emite el servidor:
| Código | Texto |
|---|---|
| E1 | unknown permission profile '{value}' (use readonly, standard, builder, full) |
| E2 | the policy file path must be absolute (got '{value}') |
| E3 | policy file {path}: cannot be read ({strerror}) |
| E4 | policy file {path}: is larger than 262144 bytes |
| E5 | policy file {path}: is not valid JSON (line {line}, column {column}: {msg}) |
| E6 | policy file {path}: duplicate key '{key}' (a repeated entry could hide a rule) |
| E7 | policy file {path}: the top level must be an object with "version": 1 |
| E8 | policy file {path}: unknown key '{key}' at the top level (use version, models, fields, tools, comment) |
| E9 | policy file {path}: models['{key}']: not a model name, 'prefix.*' or '*' |
| E10 | policy file {path}: models['{key}']: unknown key '{op}' (use read, create, write, unlink, call, schema, reason, expires, comment) |
| E11 | policy file {path}: models['{key}'].{op}: must be true or false |
| E12 | policy file {path}: models['{key}'].call: must be true, false or a list of method names |
| E13 | policy file {path}: models['{key}'].call: '{method}' is not a public method name |
| E14 | policy file {path}: tools.disabled: unknown tool '{name}' |
| E15 | policy file {path}: models['{key}'] opens {ops} on {article} {tier}-tier model and needs "reason" (at least 10 characters): it is shown in odoo_version and written to the audit log |
| E16 | policy file {path}: models['{key}'].expires: '{value}' is not a date (YYYY-MM-DD) |
| E17 | policy file {path}: fields.{list}[{index}]: '{value}' is not model.field (withhold also accepts *.field) |
| E18 | policy file {path}: models['{key}'].{op}: cannot be granted: {reason} |
| E19 | policy file {path}: models['{key}']: "reason" and "expires" only apply to an entry that names one model exactly |
| E20 | policy file {path}: fields.allow[{index}] needs "reason" (at least 10 characters) |
Un archivo cuyo contenido completo es el valor JSON null también se bloquea
con E7.
Las advertencias no bloquean. Aparecen en odoo_version.policy.warnings, en
--check-policy y en la línea de inicio de la auditoría. W10 la emite
odoo_version en tiempo de ejecución, cuando el usuario de Odoo es
administrador.
| Código | Texto |
|---|---|
| W1 | models['{key}'] refuses create/write/unlink but allows call: business methods (e.g. action_post) can still change these records; set "call": false or list the allowed methods |
| W2 | models['{model}'] opens {ops} on '{model}' ({tier} tier){until}: {reason} |
| W3 | the policy file can be changed by other users of this machine (mode {octal}) |
| W4 | models['{key}'] expired on {date}: its allowances are ignored, its refusals still apply |
| W5 | models['{key}'] sets {op} on a pattern; models of the {tiers} tiers are not affected: name a model exactly to open one |
| W6 | profile 'builder' lets the agent create automations and server actions, which run Python on the Odoo server with sudo() available: for confidentiality it equals 'full'. Use it on sandboxes and staging. |
| W7 | fields.allow releases {field}: {reason} |
| W10 | the Odoo user of this connection is an administrator (base.group_system): Odoo's access rights narrow nothing, so this policy is the only narrowing layer. Prefer a dedicated user with the fewest groups that work. |
Revisar un archivo sin conexión: --check-policy¶
python3 "<plugin>/server/run_stdio.py" --check-policy ARCHIVO [--profile standard] \
[--explain MODELO:OP[:MÉTODO] ...] [--json]
Carga el archivo como lo haría el servidor, con Read-only mode apagado, nunca
contacta a Odoo, e imprime el perfil y el digest, las reglas normalizadas, las
aperturas con su razón y su caducidad, las advertencias, las herramientas
ocultas y una línea por cada --explain:
explain: res.users write: REFUSED (tier:security)
explain: sale.order call action_confirm: ALLOWED (file:sale.order)
explain: account.move read: REFUSED (file:account.invoice, alias of account.move)
explain: website.page write: REFUSED (tier:customization, through ir.ui.view, which website.page delegates to)
Códigos de salida: 0 si el archivo es válido, 1 si tiene un error (el servidor
arrancaría bloqueado), 2 por uso incorrecto. Un ARCHIVO relativo se resuelve
contra la carpeta actual para la revisión (la opción misma debe ser absoluta), y
un ARCHIVO vacío (--check-policy "") revisa solo el perfil. --json imprime
el mismo reporte como JSON. El digest cubre la marca de Read-only mode, así que
un servidor que corre con ODOO_READONLY=1 reporta otro digest para el mismo
archivo y perfil. El repositorio de políticas de un equipo puede correrlo en su
CI. Dentro del plugin, <plugin> es ${CLAUDE_PLUGIN_ROOT}.
Qué reportan las herramientas¶
odoo_version devuelve un bloque policy:
"policy": {"profile": "standard", "file": null, "digest": "21524d1933f3", "readonly": false,
"hidden_tools": ["odoo_unlink", "odoo_add_field", "odoo_add_automation"],
"opened_sensitive": [], "released_fields": [], "warnings": [],
"error": null, "error_code": null}
Una entrada de opened_sensitive se ve así: {"model": "res.partner.bank",
"ops": ["create", "write"], "tier": "security", "reason": "...", "expires":
"2026-11-15"}. Cada advertencia es un objeto {"code": ..., "text": ...}.
Con la política bloqueada, odoo_version devuelve solo el bloque plugin, el
bloque policy con error y error_code, y una nota, sin contactar a Odoo.
Desde la v1.6.0, odoo_version devuelve también un bloque plugin: la versión
de odoo-tools que respondió y, en Claude Code, su copia instalada
(plugin@marketplace). Una respuesta sin policy viene de una copia anterior a
la 1.5.0, que no tiene política de permisos: la ruta guiada y
/odoo-tools:odoo-doctor se detienen ahí (ver
Dos copias de odoo-tools).
odoo_access_checkdevuelvepolicy({operación: true o false}) junto a la respuesta propia de Odoo, con una nota cuando Odoo permite lo que la política rechaza.odoo_list_modelsdeja fuera los modelos en los que la política no permite nada y dice cuántos son (hidden_by_policy).odoo_online_profilereporta enunavailableuna sección que la política retiene, en lugar de fallar.
Rechazos¶
Un rechazo es un error de herramienta. Nombra la operación, el modelo, el perfil y la regla que decidió; luego dice dónde se detuvo la llamada, da el remedio y le pide al agente no buscar el mismo resultado con otra herramienta. Solo se repiten nombres validados (modelo, campo, método, operación y clave de contexto), nunca el valor de un argumento.
- Capa A, antes de iniciar sesión (la mayoría de los rechazos): se decide con
los accesos declarados por la herramienta y los argumentos, antes de que exista
una sesión. Termina con
Nothing was sent to Odoo.y no se cuenta como llamada a herramienta. Aquí se rechazan una clave de contexto fuera de la lista y undefault_<campo>que contiene una lista o un objeto (reglacontext). - Capa B, después de iniciar sesión (el guard): cada llamada a Odoo que
hace una herramienta se revisa otra vez, con sus valores y rutas, al enviarse.
Ese rechazo termina con
The call was stopped before Odoo ran it: nothing was changed and no record data was returned.El servidor puede haber leído definiciones de campos para decidir. En Odoo 10 a 12, aquí se rechaza una clave de un objeto pasado por posición a un método de negocio que no es campo del modelo ni clave de contexto permitida (reglacontext, ver el límite conocido 10). - Una llamada fuera del mapeo declarado de una herramienta se detiene como un
error de odoo-tools (regla
envelope), en todos los perfiles; repórtalo a dev@transgenia.org.
Un rechazo empieza así, y sigue con el remedio (aquí, archivar con
odoo_write {"active": false}):
the odoo-tools permission policy refuses 'unlink' on 'res.partner' (profile 'standard',
rule profile:standard). Nothing was sent to Odoo.
El agente debe tratar como definitivo un rechazo que nombra la política de permisos: te lo dice, y tú puedes cambiar el perfil o el archivo de política.
Registro de auditoría¶
Cada llamada a herramienta, rechazos incluidos, deja una línea JSON: la
herramienta, los nombres de modelo y operación que tocó (op:modelo[:método]),
la capa y la regla de un rechazo, conteos, la duración, el nombre de la clase de
excepción de un error y el digest de la política. Nunca guarda valores de
argumentos, dominios, valores de campos, ids, contenido de archivos, la URL,
tokens, llaves, contraseñas ni textos de mensajes de error. La base de datos y
el login de Odoo aparecen solo en la línea start que se escribe al arrancar el
servidor, que también lleva el perfil, la ruta del archivo, las advertencias
(códigos) y las aperturas con sus razones.
{"v":1,"ts":"2026-10-21T15:02:11Z","seq":42,"prev":"9c1e0b7d2a44f1e3","event":"call",
"tool":"odoo_write","layer":"B","decision":"deny","rule":"relational",
"touches":["write:res.partner"],"effective":["write:res.users"],"n":3,"status":"refused",
"ms":41,"policy":"a4f1c0d2e9b7"}
Las claves son v, ts, seq, prev, event (start, call,
chain_reset), tool, layer (tool, A o B), decision, rule,
touches, effective, n, withheld, status (ok, refused, error,
invalid), error, ms, policy, y en la línea de inicio profile,
readonly, version, pid, source, db, login, opened y warnings.
ODOO_AUDIT_DIR |
A dónde van las líneas |
|---|---|
una carpeta (el plugin fija ${CLAUDE_PLUGIN_DATA}/audit) |
un archivo por día UTC, odoo-tools-audit-AAAA-MM-DD.jsonl, carpeta 0700, archivos 0600, nunca a través de un enlace simbólico |
sin definir o stderr |
el logger odoo_mcp.audit en stderr (docker logs en un contenedor) |
off |
nada |
ODOO_AUDIT_RETENTION_DAYS (90 por defecto; 0 conserva los archivos para
siempre) borra los archivos diarios antiguos al arrancar el servidor y en la
primera escritura después de la medianoche UTC; solo se borran archivos normales
con nombre de archivo de auditoría. El plugin fija los 90 días. Claude Code borra
la carpeta de datos del plugin cuando el plugin se desinstala de su último
ámbito. Un fallo al escribir nunca rompe una llamada a herramienta.
Cada prev son los primeros 16 caracteres hexadecimales del SHA-256 de la línea
anterior del mismo archivo (genesis en la primera). Para revisar esa cadena:
Sale con 0 cuando la cadena se sostiene; con 1 en la primera línea cuyo contenido se editó, cuyo orden cambió o que se insertó, o cuando la última línea está incompleta (y la nombra); con 2 cuando el archivo no se puede leer. Como cada línea solo enlaza con la anterior, revisar un solo archivo no detecta por sí mismo líneas completas quitadas del final de un día, ni un archivo de un día borrado entero: el prefijo que sobrevive sigue encadenado. En local esto es evidencia, no prevención: quien puede escribir la carpeta puede reescribir los archivos y recalcular la cadena. Un destino que ancle la cadena entre archivos y reinicios es una adición del modo remoto, no incluida aquí.
Ejemplos¶
El repositorio trae cuatro archivos en
docs/policy-examples/,
y la CI carga cada uno:
| Archivo | Perfil | Para |
|---|---|---|
production-hardened.json |
standard |
producción con un usuario administrador, mientras no exista un usuario de mínimo privilegio: solo verbos seleccionados, sin datos de RR. HH., sin leer el chatter, sin cambios al sitio web, sin importaciones |
sales-team.json |
standard |
un agente de ventas: CRM y cotizaciones, respuestas a clientes desde el chatter, sin cambios contables |
read-only-analyst.json |
readonly |
reportes: agregados y exportaciones sin datos de RR. HH., chatter ni datos de contacto |
sandbox-builder.json |
builder |
un sandbox de entrenamiento: cambios estilo Studio, borrados y usuarios de prueba |
El archivo endurecido, para producción con un usuario administrador:
{
"version": 1,
"comment": "Production hardening: business reads and writes, curated verbs only, no imports",
"models": {
"*": {"call": false},
"res.partner": {"call": ["message_post"], "reason": "Agents reply to customers from the chatter"},
"crm.lead": {"call": ["action_set_won", "action_set_lost"]},
"sale.order": {"call": ["action_confirm", "action_cancel"]},
"account.move": {"call": ["action_post"]},
"hr.*": {"read": false, "create": false, "write": false, "call": false},
"mail.message": {"read": false},
"mail.activity":{"read": false},
"website.*": {"write": false, "create": false}
},
"fields": {"withhold": ["*.vat", "hr.employee.private_email"]},
"tools": {"disabled": ["odoo_import", "odoo_import_preview"]}
}
"*": {"call": false}dejaodoo_executesolo con los verbos seleccionados; sus métodos de lectura también se rechazan, porque necesitancall.- Los rechazos de
hr.*,mail.messageymail.activitymantienen los datos de RR. HH. fuera de alcance también a través del chatter (ver los límites conocidos). - El rechazo de
website.*cierra el canal del contenido publicado.
Para usarlo, copia el archivo fuera de todo proyecto (por ejemplo a
~/.config/odoo-tools/policy.json), revísalo con --check-policy y pon su ruta
absoluta en Permission policy file.
Mapeo de herramientas¶
Cada herramienta declara lo que toca, y una herramienta sin mapeo no se puede registrar, listar ni llamar. La capa A decide lo declarado a partir de los argumentos, antes de que exista una sesión. Las llamadas mecánicas fijas que hace una herramienta son entradas internas, cada una anclada por un predicado sobre la llamada real y con una razón; el guard (capa B) rechaza cualquier llamada que no esté declarada ni sea una interna cuyo predicado se cumple.
| Herramienta | Solo lectura | Declarado (capa A) | Oculta en readonly / standard / builder |
|---|---|---|---|
odoo_access_check |
sí | inspect sobre el modelo pedido | no / no / no |
odoo_add_automation |
no | schema sobre el modelo pedido; create base.automation; create ir.actions.server |
sí / sí / no |
odoo_add_field |
no | schema sobre el modelo pedido; inspect sobre el modelo de relation, si se da |
sí / sí / no |
odoo_api_catalog |
sí | inspect sobre el modelo pedido | no / no / no |
odoo_connections |
sí | ningún modelo | no / no / no |
odoo_create |
no | create sobre el modelo pedido | sí / no / no |
odoo_execute |
no | call del método pedido, más su clase (por ejemplo unlink) | sí / no / no |
odoo_export_records_csv |
sí | read sobre el modelo pedido | no / no / no |
odoo_export_records_json |
sí | read sobre el modelo pedido | no / no / no |
odoo_fields_get |
sí | inspect sobre el modelo pedido | no / no / no |
odoo_import |
no | create y write sobre el modelo pedido; las rutas de las columnas fields |
sí / no / no |
odoo_import_preview |
no | create y write sobre el modelo pedido; las rutas de las columnas fields |
sí / no / no |
odoo_list_models |
sí | read ir.model |
no / no / no |
odoo_module_info |
sí | read ir.module.module |
no / no / no |
odoo_online_profile |
sí | read res.users, res.company, res.users.apikeys e ir.module.module; inspect res.partner; una sección rechazada se reporta y la herramienta sigue |
no / no / no |
odoo_read |
sí | read sobre el modelo pedido | no / no / no |
odoo_read_group |
sí | read sobre el modelo pedido | no / no / no |
odoo_record_documents |
sí | read sobre el modelo pedido; read ir.attachment |
no / no / no |
odoo_report |
sí | nunca llama a Odoo | no / no / no |
odoo_search |
sí | read sobre el modelo pedido | no / no / no |
odoo_search_count |
sí | read sobre el modelo pedido | no / no / no |
odoo_search_read |
sí | read sobre el modelo pedido | no / no / no |
odoo_telemetry_preview |
sí | ningún modelo | no / no / no |
odoo_translate_get |
sí | read sobre el modelo pedido | no / no / no |
odoo_translate_set |
no | write sobre el modelo pedido | sí / no / no |
odoo_unlink |
no | unlink sobre el modelo pedido | sí / sí / no |
odoo_version |
sí | ningún modelo | no / no / no |
odoo_write |
no | write sobre el modelo pedido | sí / no / no |
Para agregar una herramienta al servidor, ver Contribuir.
Límites conocidos¶
- Efectos dentro de los métodos.
action_confirmpuede enviar un correo de confirmación cuando Odoo está configurado así, y las automatizaciones existentes corren con las escrituras permitidas. Odoo los gobierna para el usuario conectado. El archivo endurecido limitacalla listas de métodos. - Las cadenas que devuelven los métodos no se redactan, por ejemplo una URL de portal que lleva un token.
- Los modelos puntero distintos de
ir.attachment(mail.message,mail.activity,mail.followers,rating.rating) importan cuando un archivo rechaza lecturas de modelos de negocio; el ejemplo endurecido los rechaza junto conhr.*. - Campos relacionados y calculados con nombres inofensivos, y los nombres visibles, pueden llevar datos de un modelo retenido. Los grupos de campos de Odoo siguen siendo el control ahí.
- Los enlaces many2one no se revisan como toques. Fijar
partner_bank_ida una cuenta bancaria existente es solo una escritura en el padre. - Los campos públicos son un canal de salida por su efecto, por ejemplo la
descripción publicada en el sitio web. Los archivos endurecidos pueden
rechazar escrituras en
website.*yproduct.template. - La inferencia a partir de conteos y agregados no se impide.
- En stdio, un agente con shell puede rodear la política, y
builderequivale afullen confidencialidad. - Un adjunto anidado se puede ligar a un registro con un valor por defecto de
contexto.
default_res_modelydefault_res_idsiguen permitidos, porque la programación de actividades y el redactor de correo los usan, así que una llamada que crea unir.attachmentanidado (directamente, o por un hijo de delegación comoproduct.document) puede fijar elres_modely elres_idde ese adjunto desde el contexto. Una creación directa del adjunto con esos valores sí se revisa (el padre debe permitirwrite); solo se acepta el camino de la creación anidada, que ninguna revisión contra el modelo principal de la llamada puede seguir.publicyres_fieldse rechazan como valores por defecto, así que esto liga un adjunto, no lo expone. -
Un contexto pasado por posición, en Odoo 10 a 12. Esas series también toman el contexto de una llamada del argumento posicional que sigue a los parámetros propios del método; desde la 13 solo se lee
kwargs["context"]. La política lee ese argumento como contexto: en la capa A en todas las series, antes de enviar nada, porodoo_executecon o sinids, y otra vez en la sesión vigilada en 10 a 12. Esas series construyen el contexto condict(), así que una lista de pares[clave, valor]cuenta abajo como un objeto y se lee como el contexto que construye; una lista de nombres de campos, ids o términos de dominio no construye ninguno.- Con un método cuyos parámetros conoce la política (
search_read,create,writey los demás métodos que lee), un objeto después de los parámetros, o en un parámetro que nunca lleva un objeto (ellimitdesearch_count, que Odoo 10 a 12 no tienen), cumple todas las reglas de contexto. Una llamada con un argumento después de los parámetros se sigue rechazando como antes (reglashape) cuando las reglas de contexto la dejan pasar. - Con cualquier otro método (un método de negocio como
action_confirm), la política no puede saber dónde terminan los parámetros, así que cualquier objeto pasado por posición puede ser el contexto o uno de los valores del método. Sus clavesdefault_<campo>cumplen las reglas de contexto en todas las series (una lista o un objeto, un campo estructural en una llamada que puede crear, un campo retenido), y un objeto de valores como{"default_code": "A-1"}sigue corriendo. Desde la v1.6.0, en Odoo 10 a 12 la sesión vigilada decide también sus demás claves, con los campos del modelo: una clave que es campo del modelo es uno de los valores del método y se revisa como sus demás argumentos (ver Campos con aspecto de credencial); una clave de contexto permitida pasa; cualquier otra, comotracking_disable, solo puede ser una clave de contexto y se rechaza (reglacontext). Los campos son las definiciones que la sesión vigilada lee para sus otras revisiones (fields_get, con caché), y solo se leen cuando hay una clave así; si Odoo rechaza esa consulta, la llamada se rechaza (reglaunverified). Una clave que no es texto, que solo puede traer una lista de pares (enlaces x2many como[4, 5]), no nombra ninguna clave de contexto que Odoo lea y no se toca. Por eso una lista de pares de texto cuyos primeros elementos no son campos del modelo ni claves de contexto permitidas (pares de una selección, una fila de dos columnas) también se rechaza ahí cuando se pasa por posición: pasa los argumentos del método por nombre.
Los rechazos (regla
context) nombran la clave, nunca su valor, y dicen que pases el contexto por nombre, enkwargs["context"], y los demás argumentos del método por nombre, o que pongas los valores en losvaluesdel registro. Lo que queda: en Odoo 10 a 12, una clave de un objeto así que es campo del modelo se lee como un valor, así que una clave de contexto con el nombre de uno de los campos del modelo no se somete a la lista permitida. Las claves que la lista deja fuera a propósito (tracking_disable,mail_notrack,mail_create_nolog) se rechazan salvo que el modelo tenga un campo con ese nombre. Pasa el contexto por nombre, enkwargs["context"]. - Con un método cuyos parámetros conoce la política (