Summary
The permission_* record rules in dms rely on a search-method trick that Odoo 19's ORM domain rewrite silently defeats, causing _get_permission_domain to return TRUE_DOMAIN for every user. When dms is run on Odoo 19, DMS access groups stop enforcing entirely: any user with the DMS User group gets read/write/unlink on every dms.file and dms.directory, regardless of dms.access.group membership.
This is not a bug in released 18.0 (see "Affected versions" below) — it's a latent hazard that detonates on 19.0. I'm filing it as a heads-up for the eventual 19.0 port, since a ported module would look locked down while enforcing nothing.
Root cause
permission_read/write/create/unlink are computed Boolean fields with a custom search= (dms/models/dms_security_mixin.py). The shipped rules (dms/security/security.xml) read:
[('permission_read', '=', user.id)]
_get_permission_domain recovers the intent from that user.id value via a documented HACK (dms_security_mixin.py#L204-L217):
def _get_permission_domain(self, operator, value, operation):
_self = self
# HACK ir.rule domain is always computed with sudo, so if this check is
# true, we can assume safely that you're checking permissions
if self.env.su and value == self.env.uid:
_self = self.sudo(False)
value = bool(value)
...
if _self.env.su:
# You're SUPERUSER_ID
return TRUE_DOMAIN if positive else FALSE_DOMAIN
On Odoo 18 the value reaches the search method unchanged (odoo/osv/expression.py, field.determine_domain(model, operator, right) — no coercion), so value == self.env.uid holds, _self drops sudo, and the restrictive domain is built. Correct.
On Odoo 19 the new ORM domain layer normalizes a Boolean leaf before dispatching to the search method. odoo/orm/domains.py _optimize_boolean_in:
@field_type_optimization(['boolean']) # BASIC level — runs BEFORE search-method dispatch
def _optimize_boolean_in(condition, model):
...
value = {str2bool(...) if isinstance(v, str) else bool(v) for v in value} # 70 -> True
# when comparing boolean values, always compare to [True] if possible
# it eases the implementation of search methods
So the leaf becomes ('permission_read', 'in', [True]) and _get_permission_domain receives value=True, never the uid. The guard value == self.env.uid (True == 70) is false, the env stays sudo, and execution falls to return TRUE_DOMAIN — every record matches for every user, on read, write and unlink alike.
The comment on the optimizer is the crux: Boolean values are collapsed to [True] specifically "to ease the implementation of search methods" — which is exactly what removes the uid the HACK depends on.
Reproduction (Odoo 19, synthetic)
Two access groups, two root directories with one file each, and three DMS Users:
- A — member of access group A
- B — member of access group B
- C — member of no access group
Expected: A sees only A's file, B only B's, C nothing. Observed on Odoo 19: all three see both files, and can write/unlink both. Confirmed directly that _get_permission_domain('=', <uid>, 'read') builds a restrictive domain, while _get_permission_domain('=', True, 'read') returns TRUE_DOMAIN.
Affected versions
| Version |
Status |
18.0 (18.0.1.1.1) and earlier |
Not affected — no odoo/orm/domains.py; domain values reach the search method raw, so the HACK works as intended. |
19.0 |
Affected — the ORM domain rewrite coerces the Boolean value before dispatch. (No 19.0 port exists yet; this is a forward-looking report.) |
Suggested fix
ir.rule domains are never evaluated for SUPERUSER_ID, so a sudo env carrying a non-superuser uid is, by definition, a permission check. Keying the guard on that instead of on the (now-normalized) value survives the coercion:
from odoo import SUPERUSER_ID
...
if self.env.su and self.env.uid != SUPERUSER_ID:
_self = self.sudo(False)
value = bool(value)
I've verified this one-line change on Odoo 19 against the fixture above: A sees only A, B only B, C nothing; cross-group content reads and writes are denied; a genuine SUPERUSER_ID env still bypasses (TRUE_DOMAIN branch preserved). Happy to open a PR against the 19.0 branch once a port exists, if that's useful.
Note for other OCA modules
Any module that relies on a computed Boolean field with a custom search= that smuggles a uid (or other non-boolean sentinel) through the domain value will hit this same Odoo 19 coercion. The failure is silent — no error, just an over-permissive domain — so it's worth grepping for during 19.0 ports.
Summary
The
permission_*record rules indmsrely on a search-method trick that Odoo 19's ORM domain rewrite silently defeats, causing_get_permission_domainto returnTRUE_DOMAINfor every user. Whendmsis run on Odoo 19, DMS access groups stop enforcing entirely: any user with the DMS User group gets read/write/unlink on everydms.fileanddms.directory, regardless ofdms.access.groupmembership.This is not a bug in released
18.0(see "Affected versions" below) — it's a latent hazard that detonates on19.0. I'm filing it as a heads-up for the eventual 19.0 port, since a ported module would look locked down while enforcing nothing.Root cause
permission_read/write/create/unlinkare computed Boolean fields with a customsearch=(dms/models/dms_security_mixin.py). The shipped rules (dms/security/security.xml) read:_get_permission_domainrecovers the intent from thatuser.idvalue via a documented HACK (dms_security_mixin.py#L204-L217):On Odoo 18 the value reaches the search method unchanged (
odoo/osv/expression.py,field.determine_domain(model, operator, right)— no coercion), sovalue == self.env.uidholds,_selfdrops sudo, and the restrictive domain is built. Correct.On Odoo 19 the new ORM domain layer normalizes a Boolean leaf before dispatching to the search method.
odoo/orm/domains.py_optimize_boolean_in:So the leaf becomes
('permission_read', 'in', [True])and_get_permission_domainreceivesvalue=True, never the uid. The guardvalue == self.env.uid(True == 70) is false, the env stays sudo, and execution falls toreturn TRUE_DOMAIN— every record matches for every user, on read, write and unlink alike.The comment on the optimizer is the crux: Boolean values are collapsed to
[True]specifically "to ease the implementation of search methods" — which is exactly what removes the uid the HACK depends on.Reproduction (Odoo 19, synthetic)
Two access groups, two root directories with one file each, and three DMS Users:
Expected: A sees only A's file, B only B's, C nothing. Observed on Odoo 19: all three see both files, and can write/unlink both. Confirmed directly that
_get_permission_domain('=', <uid>, 'read')builds a restrictive domain, while_get_permission_domain('=', True, 'read')returnsTRUE_DOMAIN.Affected versions
18.0(18.0.1.1.1) and earlierodoo/orm/domains.py; domain values reach the search method raw, so the HACK works as intended.19.019.0port exists yet; this is a forward-looking report.)Suggested fix
ir.ruledomains are never evaluated forSUPERUSER_ID, so a sudo env carrying a non-superuser uid is, by definition, a permission check. Keying the guard on that instead of on the (now-normalized) value survives the coercion:I've verified this one-line change on Odoo 19 against the fixture above: A sees only A, B only B, C nothing; cross-group content reads and writes are denied; a genuine
SUPERUSER_IDenv still bypasses (TRUE_DOMAINbranch preserved). Happy to open a PR against the 19.0 branch once a port exists, if that's useful.Note for other OCA modules
Any module that relies on a computed Boolean field with a custom
search=that smuggles a uid (or other non-boolean sentinel) through the domain value will hit this same Odoo 19 coercion. The failure is silent — no error, just an over-permissive domain — so it's worth grepping for during 19.0 ports.