Skip to content

dms: access groups silently stop enforcing on Odoo 19 (Boolean domain-value coercion defeats the permission-search HACK) #495

Description

@J-Palomino

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_DOMAINevery 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions