diff --git a/spp_drims_sl/README.rst b/spp_drims_sl/README.rst index adb1b9b14..b458e92b2 100644 --- a/spp_drims_sl/README.rst +++ b/spp_drims_sl/README.rst @@ -159,6 +159,15 @@ Dependencies Changelog ========= +19.0.2.0.1 +~~~~~~~~~~ + +- fix(security): archive the 14 default-credential DRIMS-SL demo users + (shared password ``demo``, including ``admin.dmc@drims.gov.lk`` which + holds ``base.group_system``) on a production install via a + self-contained ``post_init_hook``; they stay active only when demo + data is enabled. + 19.0.2.0.0 ~~~~~~~~~~ diff --git a/spp_drims_sl/__init__.py b/spp_drims_sl/__init__.py index 7f8d9f342..7f6bb16a8 100644 --- a/spp_drims_sl/__init__.py +++ b/spp_drims_sl/__init__.py @@ -1,2 +1,65 @@ # Part of OpenSPP. See LICENSE file for full copyright and licensing details. -# Configuration module - no Python imports +import logging + +_logger = logging.getLogger(__name__) + +# res.users records created with the shared, documented password "demo" by +# data/demo_users.xml (including "user_admin_dmc", which holds base.group_system). +# They load via the `data` section, so they exist after any install — including a +# production database installed without demo data, where the well-known +# credentials would be a login vector. This module does not depend on spp_demo, +# so the archiving helper is kept self-contained here. +DEFAULT_DEMO_USER_XMLIDS = [ + "spp_drims_sl.user_admin_dmc", + "spp_drims_sl.user_wh_staff_colombo", + "spp_drims_sl.user_wh_staff_kandy", + "spp_drims_sl.user_wh_colombo", + "spp_drims_sl.user_wh_kandy", + "spp_drims_sl.user_approver_national", + "spp_drims_sl.user_approver_western", + "spp_drims_sl.user_approver_central", + "spp_drims_sl.user_officer_colombo", + "spp_drims_sl.user_officer_gampaha", + "spp_drims_sl.user_officer_kandy", + "spp_drims_sl.user_officer_galle", + "spp_drims_sl.user_viewer_secretary", + "spp_drims_sl.user_viewer_director", +] + + +def demo_data_enabled(env, module_name): + """Return True if demo data was loaded for ``module_name`` on this database.""" + module = env["ir.module.module"].search([("name", "=", module_name)], limit=1) + return bool(module.demo) + + +def deactivate_default_demo_users(env, xmlids, demo_enabled): + """Deactivate default-credential demo users unless demo data is enabled. + + On a database with demo data (an evaluation/demo instance) the accounts are + left active so demos work. On a database WITHOUT demo data (a + production-style install) they are archived so the well-known ``demo`` + password cannot be used to log in. Returns the users that were deactivated. + """ + if demo_enabled: + return env["res.users"].browse() + users = env["res.users"].browse() + for xmlid in xmlids: + user = env.ref(xmlid, raise_if_not_found=False) + if user and user.active: + users |= user + if users: + users.active = False + _logger.warning( + "Demo data is disabled; archived %d default-credential DRIMS-SL demo " + "user(s): %s. Re-activate them deliberately only on a " + "non-production instance.", + len(users), + ", ".join(users.mapped("login")), + ) + return users + + +def post_init_hook(env): + """Neutralize the default-credential demo users on a production install.""" + deactivate_default_demo_users(env, DEFAULT_DEMO_USER_XMLIDS, demo_data_enabled(env, "spp_drims_sl")) diff --git a/spp_drims_sl/__manifest__.py b/spp_drims_sl/__manifest__.py index cf7133664..cf0e53435 100644 --- a/spp_drims_sl/__manifest__.py +++ b/spp_drims_sl/__manifest__.py @@ -5,7 +5,7 @@ "inventory management. Includes geographic hierarchy, government agencies, " "and approval thresholds per DMC requirements.", "category": "OpenSPP/Inventory", - "version": "19.0.2.0.0", + "version": "19.0.2.0.1", "sequence": 1, "author": "OpenSPP.org", "website": "https://github.com/OpenSPP/OpenSPP2", @@ -39,6 +39,7 @@ # Second pass: enable multitier (must load after tiers are created) "data/approval_config_multitier.xml", ], + "post_init_hook": "post_init_hook", "application": False, "installable": True, "auto_install": False, diff --git a/spp_drims_sl/data/demo_users.xml b/spp_drims_sl/data/demo_users.xml index 0220a9998..dcd8bdaec 100644 --- a/spp_drims_sl/data/demo_users.xml +++ b/spp_drims_sl/data/demo_users.xml @@ -26,6 +26,12 @@ - officer.galle@drims.gov.lk - Field Officer - secretary@drims.gov.lk - Secretary (Viewer, for dashboard tutorial) - director@drims.gov.lk - Director (Viewer) + + SECURITY: these users share the well-known password "demo" and load via the + manifest `data` section, so they exist after any install. On a production + database (installed without demo data) they are archived by this module's + post_init_hook (see __init__.py) so the well-known password cannot be used + to log in; on a demo/evaluation database they stay active. --> diff --git a/spp_drims_sl/readme/HISTORY.md b/spp_drims_sl/readme/HISTORY.md index 4aaf9afef..ce5e72f93 100644 --- a/spp_drims_sl/readme/HISTORY.md +++ b/spp_drims_sl/readme/HISTORY.md @@ -1,3 +1,7 @@ +### 19.0.2.0.1 + +- fix(security): archive the 14 default-credential DRIMS-SL demo users (shared password `demo`, including `admin.dmc@drims.gov.lk` which holds `base.group_system`) on a production install via a self-contained `post_init_hook`; they stay active only when demo data is enabled. + ### 19.0.2.0.0 - Initial migration to OpenSPP2 diff --git a/spp_drims_sl/static/description/index.html b/spp_drims_sl/static/description/index.html index bc3c7f646..42988a7c9 100644 --- a/spp_drims_sl/static/description/index.html +++ b/spp_drims_sl/static/description/index.html @@ -572,6 +572,16 @@

Changelog

+

19.0.2.0.1

+ +
+

19.0.2.0.0

+

19.0.2.0.1

+ +
+

19.0.2.0.0

+

19.0.2.0.1

+ +
+

19.0.2.0.0

+

19.0.2.1.4

+ +
+

19.0.2.1.3

-
+

19.0.2.1.2

-
+

19.0.2.1.1

  • feat(demo): adapt the change-request demo generator to the redesigned @@ -556,7 +574,7 @@

    19.0.2.1.1

    Head of Household uses per-member role lines (#873) (#242)
-
+

19.0.2.1.0

  • feat(demo): seed country-appropriate CR document types (≥5 per country @@ -565,7 +583,7 @@

    19.0.2.1.0

    files to a change request without defining them manually (#1102)
-
+

19.0.2.0.0

  • Initial migration to OpenSPP2
  • diff --git a/spp_mis_demo_v2/tests/__init__.py b/spp_mis_demo_v2/tests/__init__.py index 566a572f8..f772fdefb 100644 --- a/spp_mis_demo_v2/tests/__init__.py +++ b/spp_mis_demo_v2/tests/__init__.py @@ -12,3 +12,4 @@ from . import test_registry_variables from . import test_demo_statistics from . import test_story_area_map +from . import test_demo_user_safety diff --git a/spp_mis_demo_v2/tests/test_demo_user_safety.py b/spp_mis_demo_v2/tests/test_demo_user_safety.py new file mode 100644 index 000000000..50e3e55aa --- /dev/null +++ b/spp_mis_demo_v2/tests/test_demo_user_safety.py @@ -0,0 +1,82 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""Safety tests for this module's default-credential demo users. + +``data/demo_users.xml`` creates MIS demo users with the shared password +``demo``. On a production database (installed without demo data) they must be +deactivated; on a demo/evaluation database they must stay active. The module's +``post_init_hook`` composes the demo-variable activation with this archiving. +""" + +from odoo.modules.module import get_manifest +from odoo.tests import TransactionCase, tagged + +from odoo.addons.spp_mis_demo_v2 import ( + DEFAULT_DEMO_USER_XMLIDS, + deactivate_default_demo_users, + demo_data_enabled, + post_init_hook, +) + + +@tagged("post_install", "-at_install") +class TestMisDemoUserSafety(TransactionCase): + def _default_users(self): + users = self.env["res.users"].browse() + for xmlid in DEFAULT_DEMO_USER_XMLIDS: + user = self.env.ref(xmlid, raise_if_not_found=False) + if user: + users |= user + return users + + def test_default_users_deactivated_when_demo_disabled(self): + users = self._default_users() + self.assertTrue(users, "MIS demo users should exist in the test database") + users.active = True + + deactivated = deactivate_default_demo_users(self.env, DEFAULT_DEMO_USER_XMLIDS, demo_enabled=False) + + self.assertEqual(set(deactivated.ids), set(users.ids)) + self.assertFalse( + any(users.mapped("active")), + "all MIS default-credential demo users must be inactive when demo is disabled", + ) + + def test_default_users_active_when_demo_enabled(self): + users = self._default_users() + users.active = True + + deactivate_default_demo_users(self.env, DEFAULT_DEMO_USER_XMLIDS, demo_enabled=True) + + self.assertTrue( + all(users.mapped("active")), + "MIS demo users must remain active when demo data is enabled", + ) + + def test_composed_hook_archives_users_on_production(self): + """The module's own post_init_hook must archive the users when demo is off.""" + users = self._default_users() + users.active = True + module = self.env["ir.module.module"].search([("name", "=", "spp_mis_demo_v2")], limit=1) + + if module.demo: + self.skipTest("demo data enabled on this database; production path not exercised") + + post_init_hook(self.env) + + self.assertFalse( + any(users.mapped("active")), + "the composed post_init_hook must archive default-credential users when demo is disabled", + ) + + def test_mis_demo_not_marked_production_stable(self): + self.assertNotEqual( + get_manifest("spp_mis_demo_v2").get("development_status"), + "Production/Stable", + ) + + def test_demo_data_enabled_matches_module_flag(self): + module = self.env["ir.module.module"].search([("name", "=", "spp_mis_demo_v2")], limit=1) + self.assertEqual(demo_data_enabled(self.env, "spp_mis_demo_v2"), bool(module.demo)) + + def test_manifest_wires_post_init_hook(self): + self.assertEqual(get_manifest("spp_mis_demo_v2").get("post_init_hook"), "post_init_hook") diff --git a/spp_mis_demo_v2/tests/test_mis_demo_generator.py b/spp_mis_demo_v2/tests/test_mis_demo_generator.py index 6dc5d09c8..c85e45c28 100644 --- a/spp_mis_demo_v2/tests/test_mis_demo_generator.py +++ b/spp_mis_demo_v2/tests/test_mis_demo_generator.py @@ -13,6 +13,19 @@ def setUpClass(cls): super().setUpClass() # Create test country for locale cls.test_country = cls.env.ref("base.us") + # The module's post_init_hook archives the default-credential demo users + # on a database without demo data (the test harness installs without + # demo). The demo CR generator drives a tiered approval workflow that can + # only be performed by the designated demo validator users, so reactivate + # them here: running the generator is an explicit demo action, exactly the + # deliberate reactivation the hook's warning describes. + for xmlid in ( + "spp_mis_demo_v2.demo_user_cr_local_validator", + "spp_mis_demo_v2.demo_user_cr_hq_validator", + ): + user = cls.env.ref(xmlid, raise_if_not_found=False) + if user: + user.active = True def test_generator_creation(self): """Test that generator can be created with defaults."""