From aba68878a68fac03951815683c352d593c96913e Mon Sep 17 00:00:00 2001 From: Dorin Hongu Date: Wed, 5 Nov 2025 11:07:38 +0200 Subject: [PATCH] =?UTF-8?q?Adaug=C4=83=20protec=C8=9Bii=20pentru=20modele?= =?UTF-8?q?=20de=20configura=C8=9Bie=20=C3=AEn=20produc=C8=9Bie?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A fost implementat un mecanism global de blocare pentru operațiunile de creare, modificare și ștergere pe modele marcate ca fiind de configurație în medii de producție. De asemenea, au fost adăugate câmpuri și vizualizări noi pentru a identifica aceste modele și a asigura funcționarea corectă a sistemului în contextul acestor restricții. --- deltatech_transport_change/__manifest__.py | 4 +- deltatech_transport_change/models/__init__.py | 2 + .../models/config_lock_guard.py | 80 +++++++++++++++++++ deltatech_transport_change/models/ir_model.py | 48 +++++++++++ .../views/ir_model_view.xml | 24 ++++++ 5 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 deltatech_transport_change/models/config_lock_guard.py create mode 100644 deltatech_transport_change/models/ir_model.py create mode 100644 deltatech_transport_change/views/ir_model_view.xml diff --git a/deltatech_transport_change/__manifest__.py b/deltatech_transport_change/__manifest__.py index 043a8936b..f02240089 100644 --- a/deltatech_transport_change/__manifest__.py +++ b/deltatech_transport_change/__manifest__.py @@ -1,6 +1,6 @@ { "name": "DeltaTech Transport Change", - "version": "19.0.0.1.4", + "version": "19.0.0.1.8", "category": "Technical", "summary": "Export configuration changes to CSV and manage transport through Git", "author": "Terrabit, Dorin Hongu", @@ -8,6 +8,7 @@ "depends": [ "base", "mail", + "deltatech_test_system", ], "external_dependencies": { "python": ["GitPython"], @@ -16,6 +17,7 @@ "security/ir.model.access.csv", "views/transport_config_views.xml", "views/transport_repo_views.xml", + "views/ir_model_view.xml", ], "installable": True, "application": False, diff --git a/deltatech_transport_change/models/__init__.py b/deltatech_transport_change/models/__init__.py index 6d8fc04af..b2c3f4e23 100644 --- a/deltatech_transport_change/models/__init__.py +++ b/deltatech_transport_change/models/__init__.py @@ -6,3 +6,5 @@ from . import transport_config from . import transport_repo from . import transport_utils +from . import config_lock_guard +from . import ir_model diff --git a/deltatech_transport_change/models/config_lock_guard.py b/deltatech_transport_change/models/config_lock_guard.py new file mode 100644 index 000000000..209270778 --- /dev/null +++ b/deltatech_transport_change/models/config_lock_guard.py @@ -0,0 +1,80 @@ +# © 2025 Deltatech / Terrabit +# Global guard to block create/write/unlink on selected configuration models in production + +import logging +from functools import wraps + +from odoo import models, tools +from odoo.tools import str2bool + +_logger = logging.getLogger(__name__) + +_PARAM_LOCK_ENABLED = "deltatech_transport_change.config_lock_enabled" + + +class ConfigLockGuard(models.AbstractModel): + _name = "config.lock.guard" + _description = "Configuration Models Lock Guard" + + @tools.ormcache() + def _protected_model_names(self): + """Cached set of model technical names flagged as configuration models.""" + names = self.env["ir.model"].sudo().search([("is_config_model", "=", True)]).mapped("model") + # Return an immutable for hashing across workers + return frozenset(names) + + def _should_block(self, model_name): + """Return True if ops on model_name should be blocked for current user. + Conditions: + - NOT neutralized (production): ir.config_parameter('database.is_neutralized') is False + - Model is flagged on ir.model (is_config_model = True) + - Current user is not superuser (and not bypass, if enabled later) + Additionally, guard is disabled during module install/update to allow data loading. + """ + try: + # Fast-path exits + if self.env.user._is_superuser(): + return False + # Allow during module install/update and data loading + ctx = self.env.context or {} + if ctx.get("install_mode") or ctx.get("module") or ctx.get("loading_modules"): + return False + + # If model is not marked as protected, skip + if model_name not in self._protected_model_names(): + return False + + # Only block in production (non-neutralized) + icp = self.env["ir.config_parameter"].sudo() + neutral = icp.get_param("database.is_neutralized", default=False) + if str2bool(str(neutral)): + return False + + return True + except Exception as e: # Never break core ops if guard fails; just log and allow + _logger.exception("[ConfigLock] Guard check failed: %s", e) + return False + + def _register_hook(self): + # Patch BaseModel methods once per registry load + Base = models.BaseModel + for method_name in ("create", "write", "unlink"): + original = getattr(Base, method_name) + # Prevent double wrapping + if getattr(original, "_config_lock_wrapped", False): # pragma: no cover + continue + wrapped = self._wrap_method(original) + wrapped._config_lock_wrapped = True # type: ignore[attr-defined] + setattr(Base, method_name, wrapped) + return super()._register_hook() + + def _wrap_method(self, original): + @wraps(original) + def wrapper(recordset, *args, **kwargs): + if self._should_block(recordset._name): + from odoo.exceptions import UserError + + raise UserError("This model is protected in production. Direct create/write/delete is not allowed.") + return original(recordset, *args, **kwargs) + + return wrapper diff --git a/deltatech_transport_change/models/ir_model.py b/deltatech_transport_change/models/ir_model.py new file mode 100644 index 000000000..affb5dc22 --- /dev/null +++ b/deltatech_transport_change/models/ir_model.py @@ -0,0 +1,48 @@ +# © 2025 Deltatech / Terrabit +# Extend ir.model to mark configuration models + +from odoo import fields, models + + +class IrModel(models.Model): + _inherit = "ir.model" + + is_config_model = fields.Boolean( + string="Configuration Model", + help=( + "If enabled, this model is considered a configuration model. " + "When the database is NOT neutralized (production), direct create/write/delete " + "operations can be blocked by deltatech_transport_change according to the guard rules." + ), + ) + + def _clear_guard_cache(self): + # Clear the ormcache for protected model names + try: + self.env["config.lock.guard"]._protected_model_names.clear_cache(self.env["config.lock.guard"]) + except Exception: + # Best effort; do not block admin actions + pass + + def write(self, vals): + res = super().write(vals) + if "is_config_model" in vals: + self._clear_guard_cache() + return res + + def create(self, vals_list): + records = super().create(vals_list) + # New models might be flagged as configuration + if isinstance(vals_list, dict): + vals_iter = [vals_list] + else: + vals_iter = vals_list + if any("is_config_model" in v for v in vals_iter): + self._clear_guard_cache() + return records + + def unlink(self): + res = super().unlink() + # Model set changed; clear cache + self._clear_guard_cache() + return res diff --git a/deltatech_transport_change/views/ir_model_view.xml b/deltatech_transport_change/views/ir_model_view.xml new file mode 100644 index 000000000..93d99a157 --- /dev/null +++ b/deltatech_transport_change/views/ir_model_view.xml @@ -0,0 +1,24 @@ + + + + ir.model.form.configuration.flag + ir.model + + + + + + + + + + ir.model.tree.configuration.flag + ir.model + + + + + + + +