From ef19dfaada508097a5fb58dfba7c2681e9ff61a8 Mon Sep 17 00:00:00 2001 From: Atchuthan Ubendran Date: Tue, 22 Mar 2022 17:43:47 +0530 Subject: [PATCH] Add option to Eliminate user and fields in audit logs --- auditlog/models/rule.py | 79 +++++++++--- auditlog/tests/test_auditlog.py | 199 ++++++++++++++++++++++++++++++- auditlog/views/auditlog_view.xml | 8 ++ 3 files changed, 269 insertions(+), 17 deletions(-) diff --git a/auditlog/models/rule.py b/auditlog/models/rule.py index 8e72bd3f7..ff31c6c86 100644 --- a/auditlog/models/rule.py +++ b/auditlog/models/rule.py @@ -134,6 +134,19 @@ class AuditlogRule(models.Model): capture_record = fields.Boolean( help="Select this if you want to keep track of Unlink Record", ) + users_to_exclude_ids = fields.Many2many( + "res.users", + string="Users to Exclude", + context={"active_test": False}, + states={"subscribed": [("readonly", True)]}, + ) + + fields_to_exclude_ids = fields.Many2many( + "ir.model.fields", + domain="[('model_id', '=', model_id)]", + string="Fields to Exclude", + states={"subscribed": [("readonly", True)]}, + ) _sql_constraints = [ ( @@ -256,6 +269,7 @@ class AuditlogRule(models.Model): """Instanciate a create method that log its calls.""" self.ensure_one() log_type = self.log_type + users_to_exclude = self.mapped("users_to_exclude_ids") @api.model_create_multi @api.returns("self", lambda value: value.id) @@ -277,6 +291,8 @@ class AuditlogRule(models.Model): new_values[new_record.id][fname] = field.convert_to_read( new_record[fname], new_record ) + if self.env.user in users_to_exclude: + return new_records rule_model.sudo().create_logs( self.env.uid, self._name, @@ -298,6 +314,8 @@ class AuditlogRule(models.Model): new_values = {} for vals, new_record in zip(vals_list2, new_records): new_values.setdefault(new_record.id, vals) + if self.env.user in users_to_exclude: + return new_records rule_model.sudo().create_logs( self.env.uid, self._name, @@ -315,6 +333,7 @@ class AuditlogRule(models.Model): """Instanciate a read method that log its calls.""" self.ensure_one() log_type = self.log_type + users_to_exclude = self.mapped("users_to_exclude_ids") def read(self, fields=None, load="_classic_read", **kwargs): result = read.origin(self, fields, load, **kwargs) @@ -334,6 +353,8 @@ class AuditlogRule(models.Model): return result self = self.with_context(auditlog_disabled=True) rule_model = self.env["auditlog.rule"] + if self.env.user in users_to_exclude: + return result rule_model.sudo().create_logs( self.env.uid, self._name, @@ -351,6 +372,7 @@ class AuditlogRule(models.Model): """Instanciate a write method that log its calls.""" self.ensure_one() log_type = self.log_type + users_to_exclude = self.mapped("users_to_exclude_ids") def write_full(self, vals, **kwargs): self = self.with_context(auditlog_disabled=True) @@ -369,6 +391,8 @@ class AuditlogRule(models.Model): .with_context(prefetch_fields=False) .read(fields_list) } + if self.env.user in users_to_exclude: + return result rule_model.sudo().create_logs( self.env.uid, self._name, @@ -391,6 +415,8 @@ class AuditlogRule(models.Model): old_values = {id_: old_vals2 for id_ in self.ids} new_values = {id_: vals2 for id_ in self.ids} result = write_fast.origin(self, vals, **kwargs) + if self.env.user in users_to_exclude: + return result rule_model.sudo().create_logs( self.env.uid, self._name, @@ -408,6 +434,7 @@ class AuditlogRule(models.Model): """Instanciate an unlink method that log its calls.""" self.ensure_one() log_type = self.log_type + users_to_exclude = self.mapped("users_to_exclude_ids") def unlink_full(self, **kwargs): self = self.with_context(auditlog_disabled=True) @@ -419,6 +446,8 @@ class AuditlogRule(models.Model): .with_context(prefetch_fields=False) .read(fields_list) } + if self.env.user in users_to_exclude: + return unlink_full.origin(self, **kwargs) rule_model.sudo().create_logs( self.env.uid, self._name, @@ -433,6 +462,8 @@ class AuditlogRule(models.Model): def unlink_fast(self, **kwargs): self = self.with_context(auditlog_disabled=True) rule_model = self.env["auditlog.rule"] + if self.env.user in users_to_exclude: + return unlink_fast.origin(self, **kwargs) rule_model.sudo().create_logs( self.env.uid, self._name, @@ -466,17 +497,16 @@ class AuditlogRule(models.Model): log_model = self.env["auditlog.log"] http_request_model = self.env["auditlog.http.request"] http_session_model = self.env["auditlog.http.session"] + model_model = self.env[res_model] + model_id = self.pool._auditlog_model_cache[res_model] + auditlog_rule = self.env["auditlog.rule"].search([("model_id", "=", model_id)]) + fields_to_exclude = auditlog_rule.fields_to_exclude_ids.mapped("name") for res_id in res_ids: - model_model = self.env[res_model] name = model_model.browse(res_id).name_get() - model_id = self.pool._auditlog_model_cache[res_model] - auditlog_rule = self.env["auditlog.rule"].search( - [("model_id", "=", model_id)] - ) res_name = name and name[0] and name[0][1] vals = { "name": res_name, - "model_id": self.pool._auditlog_model_cache[res_model], + "model_id": model_id, "res_id": res_id, "method": method, "user_id": uid, @@ -489,18 +519,26 @@ class AuditlogRule(models.Model): new_values.get(res_id, EMPTY_DICT), old_values.get(res_id, EMPTY_DICT) ) if method == "create": - self._create_log_line_on_create(log, diff.added(), new_values) + self._create_log_line_on_create( + log, diff.added(), new_values, fields_to_exclude + ) elif method == "read": self._create_log_line_on_read( - log, list(old_values.get(res_id, EMPTY_DICT).keys()), old_values + log, + list(old_values.get(res_id, EMPTY_DICT).keys()), + old_values, + fields_to_exclude, ) elif method == "write": self._create_log_line_on_write( - log, diff.changed(), old_values, new_values + log, diff.changed(), old_values, new_values, fields_to_exclude ) elif method == "unlink" and auditlog_rule.capture_record: self._create_log_line_on_read( - log, list(old_values.get(res_id, EMPTY_DICT).keys()), old_values + log, + list(old_values.get(res_id, EMPTY_DICT).keys()), + old_values, + fields_to_exclude, ) def _get_field(self, model, field_name): @@ -525,11 +563,14 @@ class AuditlogRule(models.Model): cache[model.model][field_name] = field_data return cache[model.model][field_name] - def _create_log_line_on_read(self, log, fields_list, read_values): + def _create_log_line_on_read( + self, log, fields_list, read_values, fields_to_exclude + ): """Log field filled on a 'read' operation.""" log_line_model = self.env["auditlog.log.line"] + fields_to_exclude = fields_to_exclude + FIELDS_BLACKLIST for field_name in fields_list: - if field_name in FIELDS_BLACKLIST: + if field_name in fields_to_exclude: continue field = self._get_field(log.model_id, field_name) # not all fields have an ir.models.field entry (ie. related fields) @@ -556,11 +597,14 @@ class AuditlogRule(models.Model): vals["old_value_text"] = old_value_text return vals - def _create_log_line_on_write(self, log, fields_list, old_values, new_values): + def _create_log_line_on_write( + self, log, fields_list, old_values, new_values, fields_to_exclude + ): """Log field updated on a 'write' operation.""" log_line_model = self.env["auditlog.log.line"] + fields_to_exclude = fields_to_exclude + FIELDS_BLACKLIST for field_name in fields_list: - if field_name in FIELDS_BLACKLIST: + if field_name in fields_to_exclude: continue field = self._get_field(log.model_id, field_name) # not all fields have an ir.models.field entry (ie. related fields) @@ -605,11 +649,14 @@ class AuditlogRule(models.Model): vals["new_value_text"] = new_value_text return vals - def _create_log_line_on_create(self, log, fields_list, new_values): + def _create_log_line_on_create( + self, log, fields_list, new_values, fields_to_exclude + ): """Log field filled on a 'create' operation.""" log_line_model = self.env["auditlog.log.line"] + fields_to_exclude = fields_to_exclude + FIELDS_BLACKLIST for field_name in fields_list: - if field_name in FIELDS_BLACKLIST: + if field_name in fields_to_exclude: continue field = self._get_field(log.model_id, field_name) # not all fields have an ir.models.field entry (ie. related fields) diff --git a/auditlog/tests/test_auditlog.py b/auditlog/tests/test_auditlog.py index 1d9756dd9..78d5959b9 100644 --- a/auditlog/tests/test_auditlog.py +++ b/auditlog/tests/test_auditlog.py @@ -3,7 +3,7 @@ # © 2021 Stefan Rijnhart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo.modules.migration import load_script -from odoo.tests.common import TransactionCase +from odoo.tests.common import Form, TransactionCase from odoo.addons.base.models.ir_model import MODULE_UNINSTALL_FLAG @@ -379,3 +379,200 @@ class TestAuditlogFullCaptureRecord(TransactionCase, AuditlogCommon): def tearDown(self): self.groups_rule.unlink() super(TestAuditlogFullCaptureRecord, self).tearDown() + + +class AuditLogRuleTestForUserFields(TransactionCase): + @classmethod + def setUpClass(cls): + super(AuditLogRuleTestForUserFields, cls).setUpClass() + # get Contact model id + cls.contact_model_id = ( + cls.env["ir.model"].search([("model", "=", "res.partner")]).id + ) + + # get phone field id + cls.fields_to_exclude_ids = ( + cls.env["ir.model.fields"] + .search([("model", "=", "res.partner"), ("name", "=", "phone")]) + .id + ) + + # get user id + cls.user = ( + cls.env["res.users"] + .with_context(no_reset_password=True, tracking_disable=True) + .create( + { + "name": "Test User", + "login": "testuser", + } + ) + ) + cls.user_2 = ( + cls.env["res.users"] + .with_context(no_reset_password=True, tracking_disable=True) + .create( + { + "name": "Test User2", + "login": "testuser2", + } + ) + ) + + cls.users_to_exclude_ids = cls.user.id + + # creating auditlog.rule + cls.auditlog_rule = ( + cls.env["auditlog.rule"] + .with_context(tracking_disable=True) + .create( + { + "name": "testrule 01", + "model_id": cls.contact_model_id, + "log_read": True, + "log_create": True, + "log_write": True, + "log_unlink": True, + "log_type": "full", + "capture_record": True, + } + ) + ) + + # Updating phone in fields_to_exclude_ids + cls.auditlog_rule.fields_to_exclude_ids = [[4, cls.fields_to_exclude_ids]] + + # Updating users_to_exclude_ids + cls.auditlog_rule.users_to_exclude_ids = [[4, cls.users_to_exclude_ids]] + + # Subscribe auditlog.rule + cls.auditlog_rule.subscribe() + + cls.auditlog_log = cls.env["auditlog.log"] + + # Creating new res.partner + cls.testpartner1 = ( + cls.env["res.partner"] + .with_context(tracking_disable=True) + .create( + { + "name": "testpartner1", + "phone": "123", + } + ) + ) + + # Creating new res.partner from excluded user + cls.testpartner2 = ( + cls.env["res.partner"] + .with_context(tracking_disable=True) + .with_user(cls.user.id) + .create( + { + "name": "testpartner2", + } + ) + ) + + def test_01_AuditlogFull_field_exclude_create_log(self): + # Checking log is created for testpartner1 + create_log_record = self.auditlog_log.search( + [ + ("model_id", "=", self.auditlog_rule.model_id.id), + ("method", "=", "create"), + ("res_id", "=", self.testpartner1.id), + ] + ).ensure_one() + self.assertTrue(create_log_record) + field_names = create_log_record.line_ids.mapped("field_name") + + # Checking log lines not created for phone + self.assertTrue("phone" not in field_names) + + # Removing created log record + create_log_record.unlink() + + def test_02_AuditlogFull_field_exclude_write_log(self): + # Checking fields_to_exclude_ids + self.testpartner1.with_context(tracking_disable=True).write( + { + "phone": "1234567890", + } + ) + # Checking log is created for testpartner1 + write_log_record = self.auditlog_log.search( + [ + ("model_id", "=", self.auditlog_rule.model_id.id), + ("method", "=", "write"), + ("res_id", "=", self.testpartner1.id), + ] + ).ensure_one() + self.assertTrue(write_log_record) + field_names = write_log_record.line_ids.mapped("field_name") + + # Checking log lines not created for phone + self.assertTrue("phone" not in field_names) + + def test_03_AuditlogFull_user_exclude_write_log(self): + # Update email in Form view with excluded user + partner_form = Form( + self.testpartner1.with_user(self.user.id).with_context( + tracking_disable=True + ) + ) + partner_form.email = "vendor@mail.com" + testpartner1 = partner_form.save() + + # Checking write log not created + with self.assertRaises(ValueError): + self.auditlog_log.search( + [ + ("model_id", "=", self.auditlog_rule.model_id.id), + ("method", "=", "write"), + ("res_id", "=", testpartner1.id), + ("user_id", "=", self.user.id), + ] + ).ensure_one() + + def test_04_AuditlogFull_user_exclude_create_log(self): + # Checking create log not created for testpartner2 + with self.assertRaises(ValueError): + self.auditlog_log.search( + [ + ("model_id", "=", self.auditlog_rule.model_id.id), + ("method", "=", "create"), + ("res_id", "=", self.testpartner2.id), + ] + ).ensure_one() + + def test_05_AuditlogFull_user_exclude_unlink_log(self): + # Removing testpartner2 from excluded user + self.testpartner2.with_user(self.user).unlink() + + # Checking delete log not created for testpartner2 + with self.assertRaises(ValueError): + self.auditlog_log.search( + [ + ("model_id", "=", self.auditlog_rule.model_id.id), + ("method", "=", "unlink"), + ("res_id", "=", self.testpartner2.id), + ] + ).ensure_one() + + def test_06_AuditlogFull_unlink_log(self): + # Removing testpartner1 with user_2 + self.testpartner1.with_user(self.user_2).unlink() + delete_log_record = self.auditlog_log.search( + [ + ("model_id", "=", self.auditlog_rule.model_id.id), + ("method", "=", "unlink"), + ("res_id", "=", self.testpartner1.id), + ("user_id", "=", self.user_2.id), + ] + ).ensure_one() + + # Checking log lines are created + self.assertTrue(delete_log_record) + + # Removing auditlog_rule + self.auditlog_rule.unlink() diff --git a/auditlog/views/auditlog_view.xml b/auditlog/views/auditlog_view.xml index d352de4de..6b297cfd6 100644 --- a/auditlog/views/auditlog_view.xml +++ b/auditlog/views/auditlog_view.xml @@ -44,6 +44,14 @@ name="capture_record" attrs="{'invisible':['|' ,('log_type','!=', 'full'), ('log_unlink','!=', True)]}" /> + +