44# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
55
66
7+ import functools
78from logging import getLogger
89
9- from odoo import SUPERUSER_ID , api , fields , models
10+ from odoo import api , fields , models
1011from odoo .exceptions import AccessError
1112from odoo .fields import Domain
1213from odoo .tools import SQL
@@ -197,34 +198,25 @@ def _get_domain_by_access_groups(self, operation):
197198 return result
198199
199200 @api .model
200- def _get_permission_domain (self , operator , value , operation ):
201- """Abstract logic for searching computed permission fields."""
202- _self = self
203- # HACK ir.rule domains are evaluated in superuser mode while env.uid
204- # stays the acting user, so `su` together with a non-root uid means we
205- # are resolving the `permission_<op> = user.id` rule on that user's
206- # behalf. The Domain engine coerces that sentinel to this Boolean
207- # field's type before we get here, so we rely on env.uid (used by
208- # _get_access_groups_query) rather than the value to build the domain.
209- if self .env .su and self .env .uid != SUPERUSER_ID :
210- _self = self .sudo (False )
211- value = bool (value )
212- # Tricky one, to know if you want to search
213- # positive or negative access
214- positive = (operator not in Domain .NEGATIVE_OPERATORS ) == bool (value )
215- if _self .env .su :
216- # You're SUPERUSER_ID
217- return Domain .TRUE if positive else Domain .FALSE
218-
219- result = Domain .OR (
201+ def _get_dms_access_domain (self , operation ):
202+ """Domain matching the records the current user may access for
203+ ``operation`` through DMS access groups or inheritance."""
204+ return Domain .OR (
220205 [
221- _self ._get_domain_by_access_groups (operation ),
222- _self ._get_domain_by_inheritance (operation ),
206+ self ._get_domain_by_access_groups (operation ),
207+ self ._get_domain_by_inheritance (operation ),
223208 ]
224209 )
225- if not positive :
226- result = ~ Domain (result )
227- return result
210+
211+ @api .model
212+ def _get_permission_domain (self , operator , value , operation ):
213+ """Search implementation of the computed ``permission_<op>`` fields,
214+ used by field domains (e.g. ``directory_id``'s create filter)."""
215+ positive = (operator not in Domain .NEGATIVE_OPERATORS ) == bool (value )
216+ if self .env .su :
217+ return Domain .TRUE if positive else Domain .FALSE
218+ result = self ._get_dms_access_domain (operation )
219+ return result if positive else ~ result
228220
229221 @api .model
230222 def _search_permission_create (self , operator , value ):
@@ -242,49 +234,57 @@ def _search_permission_unlink(self, operator, value):
242234 def _search_permission_write (self , operator , value ):
243235 return self ._get_permission_domain (operator , value , "write" )
244236
245- def filtered_domain (self , domain ):
246- """This method is needed to inhibit the behavior when called from the
247- _check_access() method with sudo() https://github.com/odoo/odoo/blob/fc737a147b9aefbd6ae5d111835ce3f4f7b4240a/odoo/models.py#L4465.
248- It would cause the error that multiple records are not accessed to be
249- displayed.
250- The _filtered_access() method is also overwritten to prevent this sudo()
251- specific behavior and to be able to access only the appropriate records.
252- """
253- if self .env .su :
254- return self
255- return super ().filtered_domain (domain )
256-
257- def _filtered_access_no_recursion (self , operation : str ):
258- """This method is just the same as _filtered_access
259- but it can not be called withoud super due to
260- recursion error.
261- """
262- if self and not self .env .su and (result := self ._check_access (operation )):
263- return self - result [0 ]
264- return self
237+ def _search (
238+ self ,
239+ domain ,
240+ offset = 0 ,
241+ limit = None ,
242+ order = None ,
243+ * ,
244+ bypass_access = False ,
245+ ** kwargs ,
246+ ):
247+ """Restrict searches to the records the current user may read through
248+ DMS access groups or inheritance (mirrors ``mail.message._search``)."""
249+ if self .env .su or bypass_access :
250+ return super ()._search (
251+ domain , offset , limit , order , bypass_access = bypass_access , ** kwargs
252+ )
253+ domain = Domain .AND ([Domain (domain ), self ._get_dms_access_domain ("read" )])
254+ return super ()._search (domain , offset , limit , order , ** kwargs )
265255
266- def _filtered_access (self , operation ):
267- # Only kept to not break inheritance; see next comment
268- result = super ()._filtered_access (operation )
269- # HACK Always fall back to applying rules by SQL.
270- # Upstream `_filtered_access()` doesn't use computed fields
271- # search methods. Thus, it will take the `[('permission_{operation}',
272- # '=', user.id)]` rule literally. Obviously that will always fail
273- # because `self[f"permission_{operation}"]` will always be a `bool`,
274- # while `user.id` will always be an `int`.
275- result |= self ._filtered_access_no_recursion (operation )
256+ def _check_access (self , operation ):
257+ """Add the DMS access-group / inheritance restriction to the
258+ record-level access check (mirrors ``mail.message._check_access``)."""
259+ result = super ()._check_access (operation )
260+ if self .env .su or not any (self ._ids ):
261+ return result
262+ records = self - result [0 ] if result else self
263+ forbidden = records ._get_forbidden_dms_access (operation )
264+ if forbidden :
265+ if result :
266+ return result [0 ] + forbidden , result [1 ]
267+ Rule = self .env ["ir.rule" ]
268+ return forbidden , functools .partial (
269+ Rule ._make_access_error , operation , forbidden
270+ )
276271 return result
277272
278- def _check_access_dms_record (self , operation : str ) -> tuple | None :
279- """Specific method "similar" to _check_access() but with a different
280- behavior: check if you do not really have access to any of the records
281- in to avoid performing the corresponding create/write/unlink action."""
282- if any (self ._ids ) and not self .env .su :
283- Rule = self .env ["ir.rule" ]
284- domain = Rule ._compute_domain (self ._name , operation )
285- items = self .with_context (active_test = False ).search (domain )
286- if any (x_id not in items .ids for x_id in self .ids ):
287- raise Rule ._make_access_error (operation , (self - items ))
273+ def _get_forbidden_dms_access (self , operation ):
274+ """Return the subset of ``self`` the current user cannot access for
275+ ``operation`` under the DMS access-group / inheritance rules. The
276+ domain carries an SQL sub-query, so it is resolved by search (not by
277+ ``filtered_domain``), bypassing access to evaluate exactly this domain."""
278+ domain = Domain .AND (
279+ [
280+ Domain ([("id" , "in" , self .ids )]),
281+ self ._get_dms_access_domain (operation ),
282+ ]
283+ )
284+ allowed = self .browse (
285+ self .with_context (active_test = False )._search (domain , bypass_access = True )
286+ )
287+ return self - allowed
288288
289289 @api .model_create_multi
290290 def create (self , vals_list ):
@@ -296,13 +296,5 @@ def create(self, vals_list):
296296 res .flush_recordset ()
297297 # Go back to the original sudo state and check we really had creation permission
298298 res = res .sudo (self .env .su )
299- res ._check_access_dms_record ("create" )
299+ res .check_access ("create" )
300300 return res
301-
302- def write (self , vals ):
303- self ._check_access_dms_record ("write" )
304- return super ().write (vals )
305-
306- def unlink (self ):
307- self ._check_access_dms_record ("unlink" )
308- return super ().unlink ()
0 commit comments