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