Browse Source
Merge pull request #396 from hbrunn/8.0-base_mixin_restrict_field_access
Merge pull request #396 from hbrunn/8.0-base_mixin_restrict_field_access
[ADD] base_mixin_restrict_field_accesspull/1002/head
Pedro M. Baeza
7 years ago
committed by
GitHub
10 changed files with 595 additions and 0 deletions
-
123base_mixin_restrict_field_access/README.rst
-
5base_mixin_restrict_field_access/__init__.py
-
16base_mixin_restrict_field_access/__openerp__.py
-
4base_mixin_restrict_field_access/controllers/__init__.py
-
21base_mixin_restrict_field_access/controllers/main.py
-
4base_mixin_restrict_field_access/models/__init__.py
-
312base_mixin_restrict_field_access/models/restrict_field_access_mixin.py
-
BINbase_mixin_restrict_field_access/static/description/icon.png
-
4base_mixin_restrict_field_access/tests/__init__.py
-
106base_mixin_restrict_field_access/tests/test_base_mixin_restrict_field_access.py
@ -0,0 +1,123 @@ |
|||||
|
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg |
||||
|
:alt: License: AGPL-3 |
||||
|
|
||||
|
===================== |
||||
|
Restrict field access |
||||
|
===================== |
||||
|
|
||||
|
This module was written to help developers restricting access to fields in a |
||||
|
secure and flexible manner on record level. |
||||
|
|
||||
|
If you're not a developer, this module is not for you as you need to write code |
||||
|
in order to actually use it. |
||||
|
|
||||
|
Usage |
||||
|
===== |
||||
|
|
||||
|
To use this module, you need to inherit this mixin for the model whose fields |
||||
|
you want to restrict, and implement at least the following methods to do |
||||
|
something useful: |
||||
|
|
||||
|
.. code:: python |
||||
|
|
||||
|
class ResPartner(models.Model): |
||||
|
# inherit from the mixin |
||||
|
_inherit = ['restrict.field.access.mixin', 'res.partner'] |
||||
|
_name = 'res.partner' |
||||
|
|
||||
|
@api.multi |
||||
|
def _restrict_field_access_get_field_whitelist(self, action='read'): |
||||
|
# return a whitelist (or a blacklist) of fields, depending on the |
||||
|
# action passed |
||||
|
whitelist = [ |
||||
|
'name', 'parent_id', 'is_company', 'firstname', 'lastname', |
||||
|
'infix', 'initials', |
||||
|
] + super(ResPartner, self)\ |
||||
|
._restrict_field_access_get_field_whitelist(action=action) |
||||
|
if action == 'read': |
||||
|
whitelist.extend(['section_id', 'user_id']) |
||||
|
return whitelist |
||||
|
|
||||
|
@api.multi |
||||
|
def _restrict_field_access_is_field_accessible(self, field_name, |
||||
|
action='read'): |
||||
|
# in case the whitelist is not enough, you can also decide for |
||||
|
# specific records if an action can be carried out on it or not |
||||
|
result = super(ResPartner, self)\ |
||||
|
._restrict_field_access_is_field_accessible( |
||||
|
field_name, action=action) |
||||
|
if result or not self: |
||||
|
return result |
||||
|
return all(this.section_id in self.env.user.section_ids or |
||||
|
this.user_id == self.env.user |
||||
|
for this in self) |
||||
|
|
||||
|
@api.multi |
||||
|
@api.onchange('section_id', 'user_id') |
||||
|
@api.depends('section_id', 'user_id') |
||||
|
def _compute_restrict_field_access(self): |
||||
|
# if your decision depends on other fields, you probably need to |
||||
|
# override this function in order to attach the correct onchange/ |
||||
|
# depends decorators |
||||
|
return super(ResPartner, self)._compute_restrict_field_access() |
||||
|
|
||||
|
@api.model |
||||
|
def _restrict_field_access_inject_restrict_field_access_domain( |
||||
|
self, domain): |
||||
|
# you also might want to decide with a domain expression which |
||||
|
# records are visible in the first place |
||||
|
domain[:] = expression.AND([ |
||||
|
domain, |
||||
|
[ |
||||
|
'|', |
||||
|
('section_id', 'in', self.env.user.section_ids.ids), |
||||
|
('user_id', '=', self.env.user.id), |
||||
|
], |
||||
|
]) |
||||
|
|
||||
|
The example code here will allow only reading a few fields for partners of |
||||
|
which the current user is neither the sales person nor in this partner's sales |
||||
|
team. |
||||
|
|
||||
|
Read the comments of the mixin, that's part of the documentation. Also have a |
||||
|
look at the tests, that's another example on how to use this code. |
||||
|
|
||||
|
For further information, please visit: |
||||
|
|
||||
|
* https://www.odoo.com/forum/help-1 |
||||
|
|
||||
|
Known issues / Roadmap |
||||
|
====================== |
||||
|
|
||||
|
* the code contains some TODOs which should be done |
||||
|
|
||||
|
Bug Tracker |
||||
|
=========== |
||||
|
|
||||
|
Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-tools/issues>`_. |
||||
|
In case of trouble, please check there if your issue has already been reported. |
||||
|
If you spotted it first, help us smashing it by providing a detailed and welcomed feedback |
||||
|
`here <https://github.com/OCA/server-tools/issues/new?body=module:%20base_mixin_restrict_field_access%0Aversion:%208.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_. |
||||
|
|
||||
|
Credits |
||||
|
======= |
||||
|
|
||||
|
Contributors |
||||
|
------------ |
||||
|
|
||||
|
* Holger Brunn <hbrunn@therp.nl> |
||||
|
|
||||
|
Maintainer |
||||
|
---------- |
||||
|
|
||||
|
.. image:: https://odoo-community.org/logo.png |
||||
|
:alt: Odoo Community Association |
||||
|
:target: https://odoo-community.org |
||||
|
|
||||
|
This module is maintained by the OCA. |
||||
|
|
||||
|
OCA, or the Odoo Community Association, is a nonprofit organization whose |
||||
|
mission is to support the collaborative development of Odoo features and |
||||
|
promote its widespread use. |
||||
|
|
||||
|
To contribute to this module, please visit https://odoo-community.org. |
@ -0,0 +1,5 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2016 Therp BV <http://therp.nl> |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
||||
|
from . import models |
||||
|
from . import controllers |
@ -0,0 +1,16 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2016 Therp BV <http://therp.nl> |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
||||
|
{ |
||||
|
"name": "Restrict field access", |
||||
|
"version": "8.0.1.0.0", |
||||
|
"author": "Therp BV,Odoo Community Association (OCA)", |
||||
|
"license": "AGPL-3", |
||||
|
"category": "Hidden/Dependency", |
||||
|
"summary": "Make it simple to restrict read and/or write access to " |
||||
|
"certain fields base on some condition", |
||||
|
"depends": [ |
||||
|
'web', |
||||
|
'base_suspend_security', |
||||
|
], |
||||
|
} |
@ -0,0 +1,4 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2017 Therp BV <http://therp.nl> |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
||||
|
from . import main |
@ -0,0 +1,21 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2017 Therp BV <http://therp.nl> |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
||||
|
from openerp.http import request |
||||
|
from openerp.addons.web.controllers.main import Export |
||||
|
from ..models.restrict_field_access_mixin import RestrictFieldAccessMixin |
||||
|
|
||||
|
|
||||
|
class RestrictedExport(Export): |
||||
|
"""Don't (even offer to) export inaccessible fields""" |
||||
|
def fields_get(self, model): |
||||
|
fields = super(RestrictedExport, self).fields_get(model) |
||||
|
model = request.env[model] |
||||
|
if isinstance(model, RestrictFieldAccessMixin): |
||||
|
sanitised_fields = { |
||||
|
k: fields[k] for k in fields |
||||
|
if model._restrict_field_access_is_field_accessible(k) |
||||
|
} |
||||
|
return sanitised_fields |
||||
|
else: |
||||
|
return fields |
@ -0,0 +1,4 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2016 Therp BV <http://therp.nl> |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
||||
|
from . import restrict_field_access_mixin |
@ -0,0 +1,312 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2016 Therp BV <http://therp.nl> |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
||||
|
import json |
||||
|
from lxml import etree |
||||
|
from openerp import _, api, fields, models, SUPERUSER_ID |
||||
|
from openerp.osv import expression # pylint: disable=W0402 |
||||
|
from openerp.addons.base_suspend_security.base_suspend_security import\ |
||||
|
BaseSuspendSecurityUid |
||||
|
|
||||
|
|
||||
|
class RestrictFieldAccessMixin(models.AbstractModel): |
||||
|
"""Mixin to restrict access to fields on record level""" |
||||
|
_name = 'restrict.field.access.mixin' |
||||
|
|
||||
|
@api.multi |
||||
|
def _compute_restrict_field_access(self): |
||||
|
"""determine if restricted field access is active on records. |
||||
|
If you override _restrict_field_access_is_field_accessible to make |
||||
|
fields accessible depending on some other field values, override this |
||||
|
to in order to append an @api.depends that reflects this""" |
||||
|
result = {} |
||||
|
for this in self: |
||||
|
this['restrict_field_access'] = any( |
||||
|
not this._restrict_field_access_is_field_accessible( |
||||
|
field, 'write') |
||||
|
for field in self._fields) |
||||
|
if this['restrict_field_access']: |
||||
|
result['warning'] = { |
||||
|
'title': _('Warning'), |
||||
|
'message': _( |
||||
|
'You will lose access to fields if you save now!'), |
||||
|
} |
||||
|
return result |
||||
|
|
||||
|
# use this field on your forms to be able to hide gui elements |
||||
|
restrict_field_access = fields.Boolean( |
||||
|
'Field access restricted', compute='_compute_restrict_field_access') |
||||
|
|
||||
|
@api.model |
||||
|
@api.returns('self', lambda x: x.id) |
||||
|
def create(self, vals): |
||||
|
restricted_vals = self._restrict_field_access_filter_vals( |
||||
|
vals, action='create') |
||||
|
return self.browse( |
||||
|
super(RestrictFieldAccessMixin, |
||||
|
# TODO: this allows users to slip in nonallowed |
||||
|
# fields with x2many operations, so we need to reset |
||||
|
# this somewhere, probably just at the beginning of create |
||||
|
self._restrict_field_access_suspend()) |
||||
|
.create(restricted_vals).ids |
||||
|
) |
||||
|
|
||||
|
@api.multi |
||||
|
def copy(self, default=None): |
||||
|
restricted_default = self._restrict_field_access_filter_vals( |
||||
|
default or {}, action='create') |
||||
|
return self.browse( |
||||
|
super(RestrictFieldAccessMixin, |
||||
|
self._restrict_field_access_suspend()) |
||||
|
.copy(default=restricted_default).ids |
||||
|
) |
||||
|
|
||||
|
@api.multi |
||||
|
def read(self, fields=None, load='_classic_read'): |
||||
|
result = super(RestrictFieldAccessMixin, self).read( |
||||
|
fields=fields, load=load) |
||||
|
for record in result: |
||||
|
this = self.browse(record['id']) |
||||
|
for field in record: |
||||
|
if not this._restrict_field_access_is_field_accessible(field): |
||||
|
record[field] = self._fields[field].convert_to_read( |
||||
|
self._fields[field].null(self.env)) |
||||
|
if self._fields[field] in self.env.cache: |
||||
|
self.env.cache[self._fields[field]].pop( |
||||
|
record['id'], False) |
||||
|
return result |
||||
|
|
||||
|
@api.model |
||||
|
def read_group(self, domain, fields, groupby, offset=0, limit=None, |
||||
|
orderby=False, lazy=True): |
||||
|
"""Restrict reading if we read an inaccessible field""" |
||||
|
has_inaccessible_field = False |
||||
|
has_inaccessible_field |= any( |
||||
|
not self._restrict_field_access_is_field_accessible(f) |
||||
|
for f in fields or self._fields.keys() |
||||
|
) |
||||
|
has_inaccessible_field |= any( |
||||
|
expression.is_leaf(term) and |
||||
|
not self._restrict_field_access_is_field_accessible( |
||||
|
term[0].split('.')[0] |
||||
|
) |
||||
|
for term in domain |
||||
|
) |
||||
|
if groupby: |
||||
|
if isinstance(groupby, basestring): |
||||
|
groupby = [groupby] |
||||
|
has_inaccessible_field |= any( |
||||
|
not self._restrict_field_access_is_field_accessible( |
||||
|
f.split(':')[0] |
||||
|
) |
||||
|
for f in groupby |
||||
|
) |
||||
|
if orderby: |
||||
|
has_inaccessible_field |= any( |
||||
|
not self._restrict_field_access_is_field_accessible(f.split()) |
||||
|
for f in orderby.split(',') |
||||
|
) |
||||
|
# just like with search, we restrict read_group to the accessible |
||||
|
# records, because we'd either leak data otherwise or have very wrong |
||||
|
# results |
||||
|
if has_inaccessible_field: |
||||
|
self._restrict_field_access_inject_restrict_field_access_domain( |
||||
|
domain |
||||
|
) |
||||
|
|
||||
|
return super(RestrictFieldAccessMixin, self).read_group( |
||||
|
domain, fields, groupby, offset=offset, limit=limit, |
||||
|
orderby=orderby, lazy=lazy |
||||
|
) |
||||
|
|
||||
|
@api.multi |
||||
|
def _BaseModel__export_rows(self, fields): |
||||
|
"""Null inaccessible fields""" |
||||
|
result = [] |
||||
|
for this in self: |
||||
|
rows = super(RestrictFieldAccessMixin, this)\ |
||||
|
._BaseModel__export_rows(fields) |
||||
|
for row in rows: |
||||
|
for i, path in enumerate(fields): |
||||
|
# we only need to take care of our own fields, super calls |
||||
|
# __export_rows again for x2x exports |
||||
|
if not path or len(path) > 1: |
||||
|
continue |
||||
|
if not this._restrict_field_access_is_field_accessible( |
||||
|
path[0], |
||||
|
) and row[i]: |
||||
|
field = self._fields[path[0]] |
||||
|
row[i] = field.convert_to_export( |
||||
|
field.convert_to_cache( |
||||
|
field.null(self.env), this, validate=False, |
||||
|
), |
||||
|
self.env |
||||
|
) |
||||
|
result.extend(rows) |
||||
|
return result |
||||
|
|
||||
|
@api.multi |
||||
|
def write(self, vals): |
||||
|
for this in self: |
||||
|
# this way, we get the minimal values we can write on all records |
||||
|
vals = this._restrict_field_access_filter_vals( |
||||
|
vals, action='write') |
||||
|
return super(RestrictFieldAccessMixin, self).write(vals) |
||||
|
|
||||
|
@api.model |
||||
|
def _search(self, args, offset=0, limit=None, order=None, count=False, |
||||
|
access_rights_uid=None): |
||||
|
if not args: |
||||
|
return super(RestrictFieldAccessMixin, self)._search( |
||||
|
args, offset=offset, limit=limit, order=order, count=count, |
||||
|
access_rights_uid=access_rights_uid) |
||||
|
args = expression.normalize_domain(args) |
||||
|
has_inaccessible_field = False |
||||
|
for term in args: |
||||
|
if not expression.is_leaf(term): |
||||
|
continue |
||||
|
if not self._restrict_field_access_is_field_accessible( |
||||
|
term[0], 'read'): |
||||
|
has_inaccessible_field = True |
||||
|
break |
||||
|
if has_inaccessible_field: |
||||
|
check_self = self if not access_rights_uid else self.sudo( |
||||
|
access_rights_uid) |
||||
|
check_self\ |
||||
|
._restrict_field_access_inject_restrict_field_access_domain( |
||||
|
args) |
||||
|
return super(RestrictFieldAccessMixin, self)._search( |
||||
|
args, offset=offset, limit=limit, order=order, count=count, |
||||
|
access_rights_uid=access_rights_uid) |
||||
|
|
||||
|
@api.model |
||||
|
def _restrict_field_access_inject_restrict_field_access_domain( |
||||
|
self, domain): |
||||
|
"""inject a proposition to restrict search results to only the ones |
||||
|
where the user may access all fields in the search domain. If you |
||||
|
you override _restrict_field_access_is_field_accessible to make |
||||
|
fields accessible depending on some other field values, override this |
||||
|
in order not to leak information""" |
||||
|
pass |
||||
|
|
||||
|
@api.cr_uid_context |
||||
|
def fields_view_get(self, cr, uid, view_id=None, view_type='form', |
||||
|
context=None, toolbar=False, submenu=False): |
||||
|
# pylint: disable=R8110 |
||||
|
# This needs to be oldstyle because res.partner in base passes context |
||||
|
# as positional argument |
||||
|
result = super(RestrictFieldAccessMixin, self).fields_view_get( |
||||
|
cr, uid, view_id=view_id, view_type=view_type, context=context, |
||||
|
toolbar=toolbar, submenu=submenu) |
||||
|
|
||||
|
# TODO: for editable trees, we'll have to inject this into the |
||||
|
# form the editable list view creates on the fly |
||||
|
if view_type != 'form': |
||||
|
return result |
||||
|
|
||||
|
# inject modifiers to make forbidden fields readonly |
||||
|
arch = etree.fromstring(result['arch']) |
||||
|
for field in arch.xpath('//field'): |
||||
|
field.attrib['modifiers'] = json.dumps( |
||||
|
self._restrict_field_access_adjust_field_modifiers( |
||||
|
cr, uid, |
||||
|
field, |
||||
|
json.loads(field.attrib.get('modifiers', '{}')), |
||||
|
context=context)) |
||||
|
|
||||
|
self._restrict_field_access_inject_restrict_field_access_arch( |
||||
|
cr, uid, arch, result['fields'], context=context) |
||||
|
|
||||
|
result['arch'] = etree.tostring(arch, encoding="utf-8") |
||||
|
return result |
||||
|
|
||||
|
@api.model |
||||
|
def _restrict_field_access_inject_restrict_field_access_arch( |
||||
|
self, arch, fields): |
||||
|
"""inject the field restrict_field_access into arch if not there""" |
||||
|
if 'restrict_field_access' not in fields: |
||||
|
etree.SubElement(arch, 'field', { |
||||
|
'name': 'restrict_field_access', |
||||
|
'modifiers': json.dumps({ |
||||
|
('tree_' if arch.tag == 'tree' else '') + 'invisible': True |
||||
|
}), |
||||
|
}) |
||||
|
fields['restrict_field_access'] =\ |
||||
|
self._fields['restrict_field_access'].get_description(self.env) |
||||
|
|
||||
|
@api.model |
||||
|
def _restrict_field_access_adjust_field_modifiers(self, field_node, |
||||
|
modifiers): |
||||
|
"""inject a readonly modifier to make non-writable fields in a form |
||||
|
readonly""" |
||||
|
# TODO: this can be fooled by embedded views |
||||
|
if not self._restrict_field_access_is_field_accessible( |
||||
|
field_node.attrib['name'], action='write'): |
||||
|
for modifier, value in [('readonly', True), ('required', False)]: |
||||
|
domain = modifiers.get(modifier, []) |
||||
|
if isinstance(domain, list) and domain: |
||||
|
domain = expression.normalize_domain(domain) |
||||
|
elif bool(domain) == value: |
||||
|
# readonly/nonrequired anyways |
||||
|
return modifiers |
||||
|
else: |
||||
|
domain = [] |
||||
|
restrict_domain = [('restrict_field_access', '=', value)] |
||||
|
if domain: |
||||
|
restrict_domain = expression.OR([ |
||||
|
restrict_domain, |
||||
|
domain |
||||
|
]) |
||||
|
modifiers[modifier] = restrict_domain |
||||
|
return modifiers |
||||
|
|
||||
|
@api.multi |
||||
|
def _restrict_field_access_get_field_whitelist(self, action='read'): |
||||
|
"""return whitelisted fields. Those are readable and writable for |
||||
|
everyone, for the rest, it depends on your implementation of |
||||
|
_restrict_field_access_is_field_accessible""" |
||||
|
return models.MAGIC_COLUMNS + [ |
||||
|
self._rec_name, 'display_name', 'restrict_field_access', |
||||
|
] |
||||
|
|
||||
|
@api.model |
||||
|
def _restrict_field_access_suspend(self): |
||||
|
"""set a marker that we don't want to restrict field access""" |
||||
|
return self.suspend_security() |
||||
|
|
||||
|
@api.model |
||||
|
def _restrict_field_access_get_is_suspended(self): |
||||
|
"""return True if we shouldn't check for field access restrictions""" |
||||
|
return isinstance(self.env.uid, BaseSuspendSecurityUid) |
||||
|
|
||||
|
@api.multi |
||||
|
def _restrict_field_access_filter_vals(self, vals, action='read'): |
||||
|
"""remove inaccessible fields from vals""" |
||||
|
assert len(self) <= 1, 'This function needs an empty recordset or '\ |
||||
|
'exactly one record' |
||||
|
this = self.new(dict((self.copy_data()[0] if self else {}), **vals)) |
||||
|
return dict( |
||||
|
filter( |
||||
|
lambda itemtuple: |
||||
|
this._restrict_field_access_is_field_accessible( |
||||
|
itemtuple[0], action=action), |
||||
|
vals.iteritems())) |
||||
|
|
||||
|
@api.multi |
||||
|
def _restrict_field_access_is_field_accessible(self, field_name, |
||||
|
action='read'): |
||||
|
"""return True if the current user can perform specified action on |
||||
|
all records in self. Override for your own logic. |
||||
|
This function is also called with an empty recordset to get a list |
||||
|
of fields which are accessible unconditionally. |
||||
|
Note that this function is called *very* often. Even small things |
||||
|
like saying self.env.user.id instead of self.env.uid will give you a |
||||
|
massive performance penalty""" |
||||
|
if self._restrict_field_access_get_is_suspended() or\ |
||||
|
self.env.uid == SUPERUSER_ID or\ |
||||
|
not self and action == 'read' and\ |
||||
|
self._fields[field_name].required: |
||||
|
return True |
||||
|
whitelist = self._restrict_field_access_get_field_whitelist( |
||||
|
action=action) |
||||
|
return field_name in whitelist |
After Width: 128 | Height: 128 | Size: 9.2 KiB |
@ -0,0 +1,4 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2016 Therp BV <http://therp.nl> |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
||||
|
from . import test_base_mixin_restrict_field_access |
@ -0,0 +1,106 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2016 Therp BV <http://therp.nl> |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
||||
|
from openerp import api, models |
||||
|
from openerp.osv import expression |
||||
|
from openerp.tests.common import TransactionCase |
||||
|
|
||||
|
|
||||
|
class TestBaseMixinRestrictFieldAccess(TransactionCase): |
||||
|
def test_base_mixin_restrict_field_access(self): |
||||
|
# inherit from our mixin. Here we want to restrict access to |
||||
|
# all fields when the partner has a credit limit of less than 42 |
||||
|
# and the current user is not an admin |
||||
|
class ResPartner(models.Model): |
||||
|
_inherit = ['restrict.field.access.mixin', 'res.partner'] |
||||
|
_name = 'res.partner' |
||||
|
|
||||
|
# implement a record specific whitelist: credit limit is only |
||||
|
# visible for normal users if it's below 42 |
||||
|
@api.multi |
||||
|
def _restrict_field_access_is_field_accessible( |
||||
|
self, field_name, action='read' |
||||
|
): |
||||
|
result = super(ResPartner, self)\ |
||||
|
._restrict_field_access_is_field_accessible( |
||||
|
field_name, action=action |
||||
|
) |
||||
|
if not self._restrict_field_access_get_is_suspended() and\ |
||||
|
not self.env.user.has_group('base.group_system') and\ |
||||
|
field_name not in models.MAGIC_COLUMNS and self: |
||||
|
result = all( |
||||
|
this.sudo().credit_limit < 42 for this in self |
||||
|
) |
||||
|
return result |
||||
|
|
||||
|
# and as the documentation says, we need to add a domain to enforce |
||||
|
# this |
||||
|
def _restrict_field_access_inject_restrict_field_access_domain( |
||||
|
self, domain |
||||
|
): |
||||
|
domain[:] = expression.AND([ |
||||
|
expression.normalize_domain(domain), |
||||
|
[('credit_limit', '<', 42)] |
||||
|
]) |
||||
|
# call base-suspend_security's register hook |
||||
|
self.env['ir.rule']._register_hook() |
||||
|
|
||||
|
# setup the model |
||||
|
res_partner = ResPartner._build_model(self.registry, self.cr).browse( |
||||
|
self.cr, self.uid, [], context={}) |
||||
|
res_partner._prepare_setup() |
||||
|
res_partner._setup_base(False) |
||||
|
res_partner._setup_fields() |
||||
|
res_partner._setup_complete() |
||||
|
|
||||
|
# run tests as nonprivileged user |
||||
|
partner_model = res_partner.sudo(self.env.ref('base.user_demo').id) |
||||
|
partner = partner_model.create({ |
||||
|
'name': 'testpartner', |
||||
|
}) |
||||
|
self.assertFalse(partner.restrict_field_access) |
||||
|
partner.sudo().write({'credit_limit': 42}) |
||||
|
partner.invalidate_cache() |
||||
|
self.assertTrue(partner.restrict_field_access) |
||||
|
self.assertFalse(partner.credit_limit) |
||||
|
self.assertTrue(partner.sudo().credit_limit) |
||||
|
# not searching for some restricted field should yield the partner |
||||
|
self.assertIn(partner, partner_model.search([])) |
||||
|
# but searching for it should not |
||||
|
self.assertNotIn( |
||||
|
partner, |
||||
|
partner_model.search([ |
||||
|
('credit_limit', '=', 42) |
||||
|
]) |
||||
|
) |
||||
|
# when we copy stuff, restricted fields should be copied, but still |
||||
|
# be inaccessible |
||||
|
new_partner = partner.copy() |
||||
|
self.assertFalse(new_partner.credit_limit) |
||||
|
self.assertTrue(new_partner.sudo().credit_limit) |
||||
|
# check that our field injection works |
||||
|
fields_view_get = partner.fields_view_get() |
||||
|
self.assertIn('restrict_field_access', fields_view_get['arch']) |
||||
|
# check that the export does null offending values |
||||
|
export = partner._BaseModel__export_rows([['id'], ['credit_limit']]) |
||||
|
self.assertEqual(export[0][1], '0.0') |
||||
|
# but that it does export the value when it's fine |
||||
|
partner.sudo().write({'credit_limit': 41}) |
||||
|
partner.invalidate_cache() |
||||
|
export = partner._BaseModel__export_rows([['id'], ['credit_limit']]) |
||||
|
self.assertEqual(export[0][1], '41.0') |
||||
|
# read_group should behave like search: restrict to records with our |
||||
|
# field accessible if a restricted field is requested, unrestricted |
||||
|
# otherwise |
||||
|
data = partner_model.read_group( |
||||
|
[], [], ['user_id'] |
||||
|
) |
||||
|
self.assertEqual(data[0]['credit_limit'], 41) |
||||
|
# but users with permissions should see the sum for all records |
||||
|
data = partner_model.sudo().read_group( |
||||
|
[], [], ['user_id'] |
||||
|
) |
||||
|
self.assertEqual( |
||||
|
data[0]['credit_limit'], |
||||
|
sum(partner_model.sudo().search([]).mapped('credit_limit')) |
||||
|
) |
Write
Preview
Loading…
Cancel
Save
Reference in new issue