Skip to content

Commit

Permalink
TA#59457 [FIX] Smart Button Access (#135)
Browse files Browse the repository at this point in the history
  • Loading branch information
majouda authored Feb 27, 2024
1 parent 6a93439 commit 29334bc
Show file tree
Hide file tree
Showing 12 changed files with 274 additions and 176 deletions.
36 changes: 36 additions & 0 deletions base_extended_security/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,42 @@ However, I can edit existing tasks.

.. image:: static/description/task_edit.png

Action Buttons
--------------
When a user has only read access to a given model because of a `Basic Rule <#defining-basic-rules>`_,
the action buttons on the form view are hidden.

This feature is only esthetic.
It does not prevent users from actually calling a method through xml-rpc
(extended security rules only block CRUD operations).

The reason is that there are a lot of method possibily called from the web interface,
even when accessing an object in read mode.
It would be very risky to block method calls arbitrarily.

Read Access Buttons
~~~~~~~~~~~~~~~~~~~
There is a hook defined by the module that allows to define methods that are
considered ``Read`` access actions.

The buttons bound to these actions are made visible through the web interface even
though the user has read access to the model.

.. code-block:: python
class SaleOrder(models.Model):
_inherit = 'sale.order'
@api.model
def get_read_access_actions(self):
res = super().get_read_access_actions()
res.append("action_draft")
return res
However, if the native Odoo ACL (``ir.model.access``) prevent the user from seeing the button,
this hook will have no effect.

Excluded Queries
----------------
The module ignores some xml-rpc queries.
Expand Down
4 changes: 2 additions & 2 deletions base_extended_security/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@

{
'name': 'Base Extended Security',
'version': "14.0.1.0.0",
'version': '1.2.0',
'author': 'Numigi',
'maintainer': 'Numigi',
'license': 'LGPL-3',
'category': 'Other',
'summary': 'Securize access to records',
'depends': [
'web', 'test_http_request'
'web',
],
'data': [
'security/ir.model.access.csv',
Expand Down
4 changes: 0 additions & 4 deletions base_extended_security/controllers/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,25 +171,21 @@ def _get_related_model(self, relation_name):
def _check_read_rules(model, record_ids):
records = _browse_records(model, record_ids)
records.check_extended_security_read()
records.check_extended_security_all()


def _check_write_rules(model, record_ids):
records = _browse_records(model, record_ids)
records.check_extended_security_write()
records.check_extended_security_all()


def _check_create_rules(model, record_ids):
records = _browse_records(model, record_ids)
records.check_extended_security_create()
records.check_extended_security_all()


def _check_unlink_rules(model, record_ids):
records = _browse_records(model, record_ids)
records.check_extended_security_unlink()
records.check_extended_security_all()


def _browse_records(model, record_ids):
Expand Down
1 change: 0 additions & 1 deletion base_extended_security/controllers/web_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ def base(self, data, token):

if record_ids:
records = request.env[model].browse(record_ids)
records.check_extended_security_all()
records.check_extended_security_read()
else:
search_domain = params.get('domain') or []
Expand Down
31 changes: 20 additions & 11 deletions base_extended_security/models/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# © 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens)
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).

from odoo import models
from odoo import models, api


class BaseWithExtendedSecurity(models.AbstractModel):
Expand All @@ -23,38 +23,47 @@ def get_extended_security_domain(self):
)

def check_extended_security_all(self):
"""Check extended security rules that applies for all CRUD operations.
This method excludes the name_get operation.
The reason is that name_get is much less likely to be a security
issue for most use cases.
Blocking name_get also causes errors in the web.interface,
because of Many2one fields.
"""
"""Check extended security rules that applies for all CRUD operations."""
pass

def check_extended_security_read(self):
"""Check extended security rules for read operations."""
self.env['extended.security.rule'].check_user_access(
model=self._name, access_type='read',
)
self.check_extended_security_all()

def check_extended_security_write(self):
"""Check extended security rules for write operations."""
self.env['extended.security.rule'].check_user_access(
model=self._name, access_type='write',
)
self.check_extended_security_all()

def check_extended_security_create(self):
"""Check extended security rules for create operations."""
self.env['extended.security.rule'].check_user_access(
model=self._name, access_type='create',
)
self.check_extended_security_all()

def check_extended_security_unlink(self):
"""Check extended security rules for unlink operations."""
self.env['extended.security.rule'].check_user_access(
model=self._name, access_type='unlink',
)
self.check_extended_security_all()

@api.model
def get_read_access_actions(self):
"""Get names of actions that should always appear on form views.
By default, when a user has only read access to a model,
the action buttons are hidden.
This method returns a list of method names.
Buttons bound to these method will not be hidden by the
extended security module.
"""
return []
10 changes: 3 additions & 7 deletions base_extended_security/models/extended_security_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,18 @@ class ExtendedSecurityRule(models.Model):
@api.model_create_multi
def create(self, vals_list):
res = super().create(vals_list)
self._flush_and_clear_cache()
self.clear_caches()
return res

def write(self, vals):
res = super().write(vals)
self._flush_and_clear_cache()
self.clear_caches()
return res

def unlink(self):
res = super().unlink()
self._flush_and_clear_cache()
return res

def _flush_and_clear_cache(self):
self.flush()
self.clear_caches()
return res

@api.model
def check_user_access(self, model, access_type):
Expand Down
25 changes: 12 additions & 13 deletions base_extended_security/models/ir_ui_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,23 @@
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).

from lxml import etree
from odoo import models
from odoo import api, models


class ViewWithButtonsHiden(models.Model):

_inherit = 'ir.ui.view'

@api.model
def postprocess_and_fields(self, node, model=None, validate=False):
"""Add custom labels to the view xml.
This method is called in Odoo when generating the final xml of a view.
"""
arch, fields = super().postprocess_and_fields(
node, model=model, validate=validate
)
arch, fields = super().postprocess_and_fields(node, model, validate)
is_nested_view = bool(self._context.get('base_model_name'))
view_model = model or self.model

if not is_nested_view and view_model:
arch = _hide_buttons_with_access_blocked(self.env, view_model, arch)
if not is_nested_view and model:
arch = _hide_buttons_with_access_blocked(self.env, model, arch)
_hide_one2many_view_buttons_with_access_blocked(self.env, fields)

return arch, fields
Expand All @@ -47,7 +44,7 @@ def _hide_buttons_with_access_blocked(env, model, arch):

if not perm_write:
tree.attrib['edit'] = "false"
_remove_object_button_tags(tree)
_remove_write_access_buttons(env, model, tree)

if not perm_create:
tree.attrib['create'] = "false"
Expand All @@ -58,9 +55,12 @@ def _hide_buttons_with_access_blocked(env, model, arch):
return etree.tostring(tree)


def _remove_object_button_tags(tree):
def _remove_write_access_buttons(env, model, tree):
read_access_buttons = env[model].get_read_access_actions()
for button in tree.xpath("//button[@type='object']"):
button.getparent().remove(button)
is_read_access_button = button.attrib.get("name") in read_access_buttons
if not is_read_access_button:
button.getparent().remove(button)


def _hide_one2many_view_buttons_with_access_blocked(env, fields):
Expand All @@ -72,8 +72,7 @@ def _hide_one2many_view_buttons_with_access_blocked(env, fields):
:param env: the Odoo environment
:param fields: the field definitions
"""
one2many_fields = (
f for f in fields.values() if 'type' in f.keys() and f['type'] == 'one2many')
one2many_fields = (f for f in fields.values() if f['type'] == 'one2many')
for field in one2many_fields:
model = field['relation']

Expand Down
8 changes: 7 additions & 1 deletion base_extended_security/tests/common.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

from odoo import models
from odoo import models, api
from odoo.exceptions import AccessError
from odoo.osv.expression import AND
from odoo.tests.common import SavepointCase
Expand Down Expand Up @@ -50,6 +50,12 @@ def check_extended_security_unlink(self):
if partner.customer_rank < 1:
raise AccessError(NON_CUSTOMER_UNLINK_MESSAGE)

@api.model
def get_read_access_actions(self):
res = super().get_read_access_actions()
res.append("create_company")
return res


class ControllerCase(SavepointCase):

Expand Down
Loading

0 comments on commit 29334bc

Please sign in to comment.