Holger Brunn
9 years ago
No known key found for this signature in database
GPG Key ID: 1C9760FECA3AE18
8 changed files with 409 additions and 0 deletions
-
118base_mixin_restrict_field_access/README.rst
-
4base_mixin_restrict_field_access/__init__.py
-
15base_mixin_restrict_field_access/__openerp__.py
-
4base_mixin_restrict_field_access/models/__init__.py
-
231base_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
-
33base_mixin_restrict_field_access/tests/test_base_mixin_restrict_field_access.py
@ -0,0 +1,118 @@ |
|||||
|
.. 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. |
||||
|
|
||||
|
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: |
||||
|
|
||||
|
.. 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. |
||||
|
|
||||
|
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,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 models |
@ -0,0 +1,15 @@ |
|||||
|
# -*- 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": [ |
||||
|
'base', |
||||
|
], |
||||
|
} |
@ -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,231 @@ |
|||||
|
# -*- 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 |
||||
|
|
||||
|
|
||||
|
class RestrictFieldAccessMixin(models.AbstractModel): |
||||
|
"""Mixin to restrict access to fields on record level""" |
||||
|
_name = 'restrict.field.access.mixin' |
||||
|
|
||||
|
# TODO: read_group, __export_rows, everything that was forgotten |
||||
|
|
||||
|
@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: |
||||
|
for field in record: |
||||
|
if not self._restrict_field_access_is_field_accessible(field): |
||||
|
record[field] = self._fields[field].convert_to_read( |
||||
|
self._fields[field].null(self.env)) |
||||
|
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): |
||||
|
# 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) |
||||
|
|
||||
|
if view_type == 'search': |
||||
|
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""" |
||||
|
# TODO: this is insecure. in the end, we need something in the lines of |
||||
|
# base_suspend_security's uid-hack |
||||
|
return self.with_context(_restrict_field_access_suspend=True) |
||||
|
|
||||
|
@api.model |
||||
|
def _restrict_field_access_get_is_suspended(self): |
||||
|
"""return True if we shouldn't check for field access restrictions""" |
||||
|
return self.env.context.get('_restrict_field_access_suspend') |
||||
|
|
||||
|
@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""" |
||||
|
if self._restrict_field_access_get_is_suspended() or\ |
||||
|
self.env.user.id == SUPERUSER_ID: |
||||
|
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,33 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2016 Therp BV <http://therp.nl> |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
||||
|
from openerp.tests.common import TransactionCase |
||||
|
from openerp import models |
||||
|
|
||||
|
|
||||
|
class TestBaseMixinRestrictFieldAccess(TransactionCase): |
||||
|
def test_base_mixin_restrict_field_access(self): |
||||
|
# inherit from our mixin |
||||
|
class ResPartner(models.Model): |
||||
|
_inherit = ['restrict.field.access.mixin', 'res.partner'] |
||||
|
_name = 'res.partner' |
||||
|
|
||||
|
# 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 = res_partner.sudo(self.env.ref('base.user_demo').id).create({ |
||||
|
'name': 'testpartner', |
||||
|
}) |
||||
|
partner.copy() |
||||
|
partner.write({ |
||||
|
'name': 'testpartner2', |
||||
|
}) |
||||
|
partner.search([]) |
||||
|
self.assertTrue(partner.restrict_field_access) |
||||
|
partner.fields_view_get() |
||||
|
# TODO: a lot more tests |
Write
Preview
Loading…
Cancel
Save
Reference in new issue