diff --git a/base_mixin_restrict_field_access/__init__.py b/base_mixin_restrict_field_access/__init__.py index 7eda98a23..0188fced6 100644 --- a/base_mixin_restrict_field_access/__init__.py +++ b/base_mixin_restrict_field_access/__init__.py @@ -2,3 +2,4 @@ # © 2016 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from . import models +from . import controllers diff --git a/base_mixin_restrict_field_access/__openerp__.py b/base_mixin_restrict_field_access/__openerp__.py index e577a166d..52bbac6cf 100644 --- a/base_mixin_restrict_field_access/__openerp__.py +++ b/base_mixin_restrict_field_access/__openerp__.py @@ -10,6 +10,6 @@ "summary": "Make it simple to restrict read and/or write access to " "certain fields base on some condition", "depends": [ - 'base', + 'web', ], } diff --git a/base_mixin_restrict_field_access/controllers/__init__.py b/base_mixin_restrict_field_access/controllers/__init__.py new file mode 100644 index 000000000..1b21ddd33 --- /dev/null +++ b/base_mixin_restrict_field_access/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# © 2017 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import main diff --git a/base_mixin_restrict_field_access/controllers/main.py b/base_mixin_restrict_field_access/controllers/main.py new file mode 100644 index 000000000..f2632e1bf --- /dev/null +++ b/base_mixin_restrict_field_access/controllers/main.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# © 2017 Therp BV +# 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 diff --git a/base_mixin_restrict_field_access/models/restrict_field_access_mixin.py b/base_mixin_restrict_field_access/models/restrict_field_access_mixin.py index 47fb36cbd..181a2d97b 100644 --- a/base_mixin_restrict_field_access/models/restrict_field_access_mixin.py +++ b/base_mixin_restrict_field_access/models/restrict_field_access_mixin.py @@ -4,8 +4,6 @@ import json from lxml import etree from openerp import _, api, fields, models, SUPERUSER_ID -from openerp.addons.web.controllers.main import Export -from openerp.http import request from openerp.osv import expression # pylint: disable=W0402 @@ -72,84 +70,70 @@ class RestrictFieldAccessMixin(models.AbstractModel): self._fields[field].null(self.env)) return result - def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False, lazy=True): - """ - Remove inaccessible fields from 'fields', 'groupby' and 'orderby'. - - If this removes all 'fields', return no records. - If this removes all 'groupby', group by first remaining field. - If this removes 'orderby', don't specify order. - """ - requested_fields = fields or self._columns.keys() - sanitised_fields = [ - f for f in requested_fields if self._restrict_field_access_is_field_accessible( - cr, uid, [], f + @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 not sanitised_fields: - return [] - - sanitised_groupby = [] - groupby = [groupby] if isinstance(groupby, basestring) else groupby - for groupby_part in groupby: - groupby_field = groupby_part.split(':')[0] - if self._restrict_field_access_is_field_accessible(cr, uid, [], groupby_field): - sanitised_groupby.append(groupby_part) - if not sanitised_groupby: - sanitised_groupby.append(sanitised_fields[0]) - if orderby: - sanitised_orderby = [] - for orderby_part in orderby.split(','): - orderby_field = orderby_part.split()[0] - if self._restrict_field_access_is_field_accessible(cr, uid, [], orderby_field): - sanitised_orderby.append(orderby_part) - sanitised_orderby = sanitised_orderby and ','.join(sanitised_orderby) or False - else: - sanitised_orderby = False + 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 + ) - result = super(RestrictFieldAccessMixin, self).read_group( - cr, - uid, - domain, - sanitised_fields, - sanitised_groupby, - offset=offset, - limit=limit, - context=context, - orderby=sanitised_orderby, - lazy=lazy + return super(RestrictFieldAccessMixin, self).read_group( + domain, fields, groupby, offset=offset, limit=limit, + orderby=orderby, lazy=lazy ) - # Add inaccessible fields back in with null values - inaccessible_fields = [f for f in requested_fields if f not in sanitised_fields] - for field_name in inaccessible_fields: - field = self._columns[field_name] - if lazy: - result.append( - { - '__domain': [(True, '=', True)], - field_name: field.null(self.env), - field_name + '_count': 0L - } - ) - else: - result.append( - { - '__domain': [(True, '=', True)], - field_name: field.null(self.env), - '__count': 0L - } - ) - return result @api.multi def _BaseModel__export_rows(self, fields): - """Don't export inaccessible fields""" - if isinstance(self, RestrictFieldAccessMixin): - sanitised_fields = [f for f in fields if f and self._restrict_field_access_is_field_accessible(f[0])] - return super(RestrictFieldAccessMixin, self)._BaseModel__export_rows(sanitised_fields) - else: - return super(RestrictFieldAccessMixin, self)._BaseModel__export_rows(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]: + row[i] = self._fields[path[0]].convert_to_export( + self._fields[path[0]].null(self.env), self.env + ) + result.extend(rows) + return result @api.multi def write(self, vals): @@ -198,6 +182,7 @@ class RestrictFieldAccessMixin(models.AbstractModel): @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( @@ -301,24 +286,13 @@ class RestrictFieldAccessMixin(models.AbstractModel): 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""" + 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""" if self._restrict_field_access_get_is_suspended() or\ - self.env.user.id == SUPERUSER_ID: + self.env.user.id == SUPERUSER_ID or\ + not self and action == 'read': return True whitelist = self._restrict_field_access_get_field_whitelist( action=action) return field_name in whitelist - - -class RestrictedExport(Export): - """Don't (even offer to) export inaccessible fields""" - def fields_get(self, model): - Model = request.session.model(model) - fields = Model.fields_get(False, request.context) - 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 -