diff --git a/auth_user_role_saml/README.rst b/auth_user_role_saml/README.rst new file mode 100644 index 0000000000..7ff62a8227 --- /dev/null +++ b/auth_user_role_saml/README.rst @@ -0,0 +1,122 @@ +========================================= +360 ERP - Auth User Role SAML glue module +========================================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:2e6c5776e9b6d22e12988aae8809272a36a5ce9ea7810625ecfa0c5028bac251 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--auth-lightgray.png?logo=github + :target: https://github.com/OCA/server-auth/tree/18.0/auth_user_role_saml + :alt: OCA/server-auth +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-auth-18-0/server-auth-18-0-auth_user_role_saml + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/server-auth&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This is a glue module that bridges ``auth_saml`` and ``auth_user_role``. + +It intercepts the standard SAML sign-in process, extracts the identity +payload (attributes) from the SAML response, and passes it to the +generic role evaluation engine. This allows administrators to +automatically provision Odoo roles based on groups or attributes defined +in Azure AD, Keycloak, Okta, or any other SAML 2.0 Identity Provider. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To configure SAML role synchronization: + +1. Ensure your SAML Identity Provider is configured to send user + attributes (e.g., ``groups``, ``roles``, or ``department``) in its + SAML assertions. +2. Configure your attribute mappings in **Settings > Users & Companies > + Identity Role Mappings** (provided by the base ``auth_user_role`` + module). +3. Navigate to **Settings > Users & Companies > SAML Providers** and + open your provider. +4. Under the provider settings, you will find a **Strict Role + Synchronization** checkbox: + + - **Checked (Default):** Strict mode. Odoo becomes a strict mirror of + the IdP. Any roles the user possesses in Odoo that are *not* + explicitly granted by the current SAML payload will be + automatically removed upon login. + - **Unchecked:** Additive mode. Mapped roles are granted, but + manually assigned roles in Odoo are left completely untouched. + +Usage +===== + +Once configured, users simply log in using the SAML Identity Provider +button on the Odoo login screen. + +Upon successful authentication, the module will parse the SAML identity +payload, evaluate it against the active mapping rules, and apply the +roles before the user's Odoo session is fully initialized. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* 360 ERP + +Contributors +------------ + +- Andrea Stirpe + +Other credits +------------- + +The development of this module has been financially supported by: + +- 360 ERP + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/server-auth `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/auth_user_role_saml/__init__.py b/auth_user_role_saml/__init__.py new file mode 100644 index 0000000000..a57845ed57 --- /dev/null +++ b/auth_user_role_saml/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 360ERP () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import models diff --git a/auth_user_role_saml/__manifest__.py b/auth_user_role_saml/__manifest__.py new file mode 100644 index 0000000000..1288d62143 --- /dev/null +++ b/auth_user_role_saml/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2026 360ERP () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "360 ERP - Auth User Role SAML glue module", + "version": "18.0.1.0.0", + "author": "360 ERP, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/server-auth", + "license": "AGPL-3", + "depends": [ + "auth_saml", + "auth_user_role", + ], + "data": [ + "views/auth_saml_provider_views.xml", + ], + "auto_install": True, +} diff --git a/auth_user_role_saml/models/__init__.py b/auth_user_role_saml/models/__init__.py new file mode 100644 index 0000000000..9db23cbf3d --- /dev/null +++ b/auth_user_role_saml/models/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2026 360ERP () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import res_users +from . import auth_saml_provider diff --git a/auth_user_role_saml/models/auth_saml_provider.py b/auth_user_role_saml/models/auth_saml_provider.py new file mode 100644 index 0000000000..e74ee3f7dc --- /dev/null +++ b/auth_user_role_saml/models/auth_saml_provider.py @@ -0,0 +1,30 @@ +# Copyright 2026 360ERP () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class AuthSamlProvider(models.Model): + _inherit = "auth.saml.provider" + + def _default_strict_sync(self): + # Fetch the global parameter, defaulting to 'True' + param = ( + self.env["ir.config_parameter"] + .sudo() + .get_param("auth_user_role.strict_sync", "True") + ) + return param == "True" + + sync_roles_strictly = fields.Boolean( + string="Strict Role Synchronization", + default=_default_strict_sync, + help="If checked, any Odoo roles manually assigned to the user will be removed " + "if they are not explicitly provided by the SAML IdP payload.", + ) + + def _hook_validate_auth_response(self, response, matching_value): + """Extract the identity payload before the response object is destroyed.""" + vals = super()._hook_validate_auth_response(response, matching_value) or {} + vals["saml_identity_payload"] = response.get_identity() + return vals diff --git a/auth_user_role_saml/models/res_users.py b/auth_user_role_saml/models/res_users.py new file mode 100644 index 0000000000..ea4a8acc5f --- /dev/null +++ b/auth_user_role_saml/models/res_users.py @@ -0,0 +1,42 @@ +# Copyright 2026 360ERP () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging + +from odoo import models + +_logger = logging.getLogger(__name__) + + +class ResUser(models.Model): + _inherit = "res.users" + + def _auth_saml_signin(self, provider, validation, saml_response): + """ + Intercept the standard SAML sign-in, allow it to complete, + and then pass the identity payload to the generic role engine. + """ + login = super()._auth_saml_signin(provider, validation, saml_response) + identity_payload = validation.get("saml_identity_payload") + + if identity_payload is not None: + user = self.env["res.users"].sudo().search([("login", "=", login)], limit=1) + + if user: + # Fetch the provider record to check its specific strict_sync setting + provider_record = self.env["auth.saml.provider"].browse(provider) + strict_sync = provider_record.sync_roles_strictly + + # If strict_sync is True but the database has NO mappings, + # applying it will inadvertently wipe all roles from the logging-in user + if strict_sync and not self.env[ + "auth.user.role.mapping" + ].sudo().search_count([], limit=1): + strict_sync = False + + # Pass strict_sync to the evaluation engine + user.sudo().evaluate_and_apply_auth_roles( + identity_payload, strict_sync=strict_sync + ) + + return login diff --git a/auth_user_role_saml/pyproject.toml b/auth_user_role_saml/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/auth_user_role_saml/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/auth_user_role_saml/readme/CONFIGURE.md b/auth_user_role_saml/readme/CONFIGURE.md new file mode 100644 index 0000000000..b5618b7454 --- /dev/null +++ b/auth_user_role_saml/readme/CONFIGURE.md @@ -0,0 +1,8 @@ +To configure SAML role synchronization: + +1. Ensure your SAML Identity Provider is configured to send user attributes (e.g., `groups`, `roles`, or `department`) in its SAML assertions. +2. Configure your attribute mappings in **Settings > Users & Companies > Identity Role Mappings** (provided by the base `auth_user_role` module). +3. Navigate to **Settings > Users & Companies > SAML Providers** and open your provider. +4. Under the provider settings, you will find a **Strict Role Synchronization** checkbox: + * **Checked (Default):** Strict mode. Odoo becomes a strict mirror of the IdP. Any roles the user possesses in Odoo that are *not* explicitly granted by the current SAML payload will be automatically removed upon login. + * **Unchecked:** Additive mode. Mapped roles are granted, but manually assigned roles in Odoo are left completely untouched. diff --git a/auth_user_role_saml/readme/CONTRIBUTORS.md b/auth_user_role_saml/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..0453d2bd32 --- /dev/null +++ b/auth_user_role_saml/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +* Andrea Stirpe diff --git a/auth_user_role_saml/readme/CREDITS.md b/auth_user_role_saml/readme/CREDITS.md new file mode 100644 index 0000000000..e16de803fd --- /dev/null +++ b/auth_user_role_saml/readme/CREDITS.md @@ -0,0 +1,4 @@ +The development of this module has been financially supported by: + +* 360 ERP + diff --git a/auth_user_role_saml/readme/DESCRIPTION.md b/auth_user_role_saml/readme/DESCRIPTION.md new file mode 100644 index 0000000000..599a932ada --- /dev/null +++ b/auth_user_role_saml/readme/DESCRIPTION.md @@ -0,0 +1,4 @@ +This is a glue module that bridges `auth_saml` and `auth_user_role`. + +It intercepts the standard SAML sign-in process, extracts the identity payload (attributes) from the SAML response, and passes it to the generic role evaluation engine. +This allows administrators to automatically provision Odoo roles based on groups or attributes defined in Azure AD, Keycloak, Okta, or any other SAML 2.0 Identity Provider. \ No newline at end of file diff --git a/auth_user_role_saml/readme/USAGE.md b/auth_user_role_saml/readme/USAGE.md new file mode 100644 index 0000000000..1a6b9df38d --- /dev/null +++ b/auth_user_role_saml/readme/USAGE.md @@ -0,0 +1,3 @@ +Once configured, users simply log in using the SAML Identity Provider button on the Odoo login screen. + +Upon successful authentication, the module will parse the SAML identity payload, evaluate it against the active mapping rules, and apply the roles before the user's Odoo session is fully initialized. \ No newline at end of file diff --git a/auth_user_role_saml/static/description/icon.png b/auth_user_role_saml/static/description/icon.png new file mode 100644 index 0000000000..a78ac843dc Binary files /dev/null and b/auth_user_role_saml/static/description/icon.png differ diff --git a/auth_user_role_saml/static/description/index.html b/auth_user_role_saml/static/description/index.html new file mode 100644 index 0000000000..8d95bd0253 --- /dev/null +++ b/auth_user_role_saml/static/description/index.html @@ -0,0 +1,470 @@ + + + + + +360 ERP - Auth User Role SAML glue module + + + +
+

360 ERP - Auth User Role SAML glue module

+ + +

Beta License: AGPL-3 OCA/server-auth Translate me on Weblate Try me on Runboat

+

This is a glue module that bridges auth_saml and auth_user_role.

+

It intercepts the standard SAML sign-in process, extracts the identity +payload (attributes) from the SAML response, and passes it to the +generic role evaluation engine. This allows administrators to +automatically provision Odoo roles based on groups or attributes defined +in Azure AD, Keycloak, Okta, or any other SAML 2.0 Identity Provider.

+

Table of contents

+ +
+

Configuration

+

To configure SAML role synchronization:

+
    +
  1. Ensure your SAML Identity Provider is configured to send user +attributes (e.g., groups, roles, or department) in its +SAML assertions.
  2. +
  3. Configure your attribute mappings in Settings > Users & Companies > +Identity Role Mappings (provided by the base auth_user_role +module).
  4. +
  5. Navigate to Settings > Users & Companies > SAML Providers and +open your provider.
  6. +
  7. Under the provider settings, you will find a Strict Role +Synchronization checkbox:
      +
    • Checked (Default): Strict mode. Odoo becomes a strict mirror of +the IdP. Any roles the user possesses in Odoo that are not +explicitly granted by the current SAML payload will be +automatically removed upon login.
    • +
    • Unchecked: Additive mode. Mapped roles are granted, but +manually assigned roles in Odoo are left completely untouched.
    • +
    +
  8. +
+
+
+

Usage

+

Once configured, users simply log in using the SAML Identity Provider +button on the Odoo login screen.

+

Upon successful authentication, the module will parse the SAML identity +payload, evaluate it against the active mapping rules, and apply the +roles before the user’s Odoo session is fully initialized.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • 360 ERP
  • +
+
+
+

Contributors

+
    +
  • Andrea Stirpe
  • +
+
+
+

Other credits

+

The development of this module has been financially supported by:

+
    +
  • 360 ERP
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/server-auth project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/auth_user_role_saml/tests/__init__.py b/auth_user_role_saml/tests/__init__.py new file mode 100644 index 0000000000..ce4afe37ff --- /dev/null +++ b/auth_user_role_saml/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 360ERP () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_auth_user_roles_saml diff --git a/auth_user_role_saml/tests/test_auth_user_roles_saml.py b/auth_user_role_saml/tests/test_auth_user_roles_saml.py new file mode 100644 index 0000000000..ac181f0058 --- /dev/null +++ b/auth_user_role_saml/tests/test_auth_user_roles_saml.py @@ -0,0 +1,306 @@ +# Copyright 2026 360ERP () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import base64 +from datetime import date, timedelta + +from odoo.tests.common import TransactionCase + +from odoo.addons.auth_saml.tests.fake_idp import DummyResponse + + +class TestAuthUserRolesSaml(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.test_role = cls.env["res.users.role"].create({"name": "Test Global Role"}) + cls.extra_manual_role = cls.env["res.users.role"].create( + {"name": "Extra Manual Role"} + ) + + cls.provider = cls.env["auth.saml.provider"].create( + { + "name": "Test SAML Bridge Provider", + "idp_metadata": "", + "sp_pem_public": base64.b64encode(b"fake_public_key"), + "sp_pem_private": base64.b64encode(b"fake_private_key"), + "matching_attribute": "mail", + "sig_alg": "SIG_RSA_SHA1", + "sync_roles_strictly": False, # Defaults to False + } + ) + + cls.mapping = cls.env["auth.user.role.mapping"].create( + { + "attribute": "eduPersonAffiliation", + "operator": "equals", + "value": "role2", + "role_id": cls.test_role.id, + } + ) + + cls.user = cls.env["res.users"].create( + { + "name": "Test SAML User", + "login": "user2@example.com", + } + ) + cls.env["res.users.saml"].create( + { + "user_id": cls.user.id, + "saml_provider_id": cls.provider.id, + "saml_uid": "user2@example.com", + } + ) + + def _simulate_saml_signin(self, identity_payload): + """ + Simulates the exact two-step SAML login process to prevent false positives. + """ + fake_response = DummyResponse(200, "fake_data") + if identity_payload is not None: + fake_response.set_identity(identity_payload) + + validation_extras = ( + self.provider._hook_validate_auth_response( + fake_response, "user2@example.com" + ) + or {} + ) + + validation = {"user_id": "user2@example.com"} + validation.update(validation_extras) + + self.env["res.users"]._auth_saml_signin( + self.provider.id, validation, "raw_base64_saml_string" + ) + + def test_01_hook_evaluation_equals(self): + """Testing bridge intercepts signin and applies 'equals' mapping""" + self._simulate_saml_signin( + {"mail": "user2@example.com", "eduPersonAffiliation": ["role2"]} + ) + self.assertIn(self.test_role.id, self.user.role_line_ids.mapped("role_id").ids) + + def test_02_hook_evaluation_contains(self): + self.mapping.write({"operator": "contains", "value": "admin"}) + self._simulate_saml_signin( + {"mail": "user2@example.com", "eduPersonAffiliation": ["super_admin_user"]} + ) + self.assertIn(self.test_role.id, self.user.role_line_ids.mapped("role_id").ids) + + def test_03_missing_attribute_clears_roles(self): + """With strict_sync, missing mapped attributes should clear previously + granted roles.""" + self.provider.write({"sync_roles_strictly": True}) + self.user.write({"role_line_ids": [(0, 0, {"role_id": self.test_role.id})]}) + self._simulate_saml_signin({"mail": "user2@example.com"}) + self.assertNotIn( + self.test_role.id, self.user.role_line_ids.mapped("role_id").ids + ) + + def test_04_string_vs_list_attribute(self): + self._simulate_saml_signin( + {"mail": "user2@example.com", "eduPersonAffiliation": "role2"} + ) + self.assertIn(self.test_role.id, self.user.role_line_ids.mapped("role_id").ids) + + def test_05_multiple_mappings(self): + self.env["auth.user.role.mapping"].create( + { + "attribute": "department", + "operator": "equals", + "value": "IT", + "role_id": self.extra_manual_role.id, + } + ) + self._simulate_saml_signin( + { + "mail": "user2@example.com", + "eduPersonAffiliation": ["role2"], + "department": ["IT"], + } + ) + roles_assigned = self.user.role_line_ids.mapped("role_id").ids + self.assertIn(self.test_role.id, roles_assigned) + self.assertIn(self.extra_manual_role.id, roles_assigned) + + def test_06_case_sensitivity(self): + self.mapping.write({"operator": "equals", "value": "role2"}) + self._simulate_saml_signin( + {"mail": "user2@example.com", "eduPersonAffiliation": ["ROLE2"]} + ) + self.assertNotIn( + self.test_role.id, self.user.role_line_ids.mapped("role_id").ids + ) + + def test_07_repeated_login_idempotency(self): + self._simulate_saml_signin( + {"mail": "user2@example.com", "eduPersonAffiliation": ["role2"]} + ) + self._simulate_saml_signin( + {"mail": "user2@example.com", "eduPersonAffiliation": ["role2"]} + ) + role_lines = self.user.role_line_ids.filtered( + lambda r: r.role_id == self.test_role + ) + self.assertEqual(len(role_lines), 1) + + def test_08_multiple_values_same_attribute(self): + role3 = self.env["res.users.role"].create({"name": "Role 3"}) + self.env["auth.user.role.mapping"].create( + { + "attribute": "eduPersonAffiliation", + "operator": "equals", + "value": "role3", + "role_id": role3.id, + } + ) + self._simulate_saml_signin( + {"mail": "user2@example.com", "eduPersonAffiliation": ["role2", "role3"]} + ) + roles_assigned = self.user.role_line_ids.mapped("role_id").ids + self.assertIn(self.test_role.id, roles_assigned) + self.assertIn(role3.id, roles_assigned) + + def test_09_reactivate_expired_role(self): + yesterday = date.today() - timedelta(days=1) + self.user.write( + { + "role_line_ids": [ + (0, 0, {"role_id": self.test_role.id, "date_to": yesterday}) + ] + } + ) + self.assertNotIn( + self.test_role.id, self.user._get_enabled_roles().mapped("role_id").ids + ) + + self._simulate_saml_signin( + {"mail": "user2@example.com", "eduPersonAffiliation": ["role2"]} + ) + + role_line = self.user.role_line_ids.filtered( + lambda r: r.role_id == self.test_role + ) + self.assertEqual(len(role_line), 1) + self.assertFalse(role_line.date_to) + self.assertIn( + self.test_role.id, self.user._get_enabled_roles().mapped("role_id").ids + ) + + def test_10_no_roles_in_validation_bypass(self): + """Test the edge case where the IDP identity payload is explicitly None.""" + self.user.write({"role_line_ids": [(0, 0, {"role_id": self.test_role.id})]}) + + validation = {"user_id": "user2@example.com", "saml_identity_payload": None} + + self.env["res.users"]._auth_saml_signin( + self.provider.id, validation, "raw_base64_saml_string" + ) + + self.assertIn(self.test_role.id, self.user.role_line_ids.mapped("role_id").ids) + + def test_11_duplicate_role_mappings_deduplication(self): + self.env["auth.user.role.mapping"].create( + { + "attribute": "department", + "operator": "equals", + "value": "IT", + "role_id": self.test_role.id, + } + ) + self._simulate_saml_signin( + { + "mail": "user2@example.com", + "eduPersonAffiliation": ["role2"], + "department": ["IT"], + } + ) + + role_lines = self.user.role_line_ids.filtered( + lambda r: r.role_id == self.test_role + ) + self.assertEqual(len(role_lines), 1) + + def test_12_empty_list_attribute_clears_roles(self): + """If a mapped attribute returns an empty list and strict sync is on, + the role should be cleared.""" + self.provider.write({"sync_roles_strictly": True}) + + self.user.write({"role_line_ids": [(0, 0, {"role_id": self.test_role.id})]}) + self._simulate_saml_signin( + {"mail": "user2@example.com", "eduPersonAffiliation": []} + ) + self.assertNotIn( + self.test_role.id, self.user.role_line_ids.mapped("role_id").ids + ) + + def test_13_empty_identity_payload_clears_roles(self): + """If a user has lost all roles in the IdP, the payload is {}, + and Odoo roles must be stripped.""" + self.provider.write({"sync_roles_strictly": True}) + self.user.write({"role_line_ids": [(0, 0, {"role_id": self.test_role.id})]}) + + self._simulate_saml_signin({}) + + self.assertNotIn( + self.test_role.id, self.user.role_line_ids.mapped("role_id").ids + ) + + def test_14_strict_sync_removes_unmapped_roles(self): + """Strict sync MUST remove manually assigned unmapped roles.""" + self.provider.write({"sync_roles_strictly": True}) + self.user.write( + {"role_line_ids": [(0, 0, {"role_id": self.extra_manual_role.id})]} + ) + + self._simulate_saml_signin( + {"mail": "user2@example.com", "eduPersonAffiliation": ["role2"]} + ) + + roles_assigned = self.user.role_line_ids.mapped("role_id").ids + self.assertIn(self.test_role.id, roles_assigned) + self.assertNotIn(self.extra_manual_role.id, roles_assigned) + + def test_15_default_strict_sync_from_config(self): + """Test that a new SAML provider inherits the global strict_sync parameter.""" + + # Ensure the global parameter is missing to test the fallback + self.env["ir.config_parameter"].sudo().search( + [("key", "=", "auth_user_role.strict_sync")] + ).unlink() + + # Create a new provider WITHOUT explicitly setting sync_roles_strictly + provider_true = self.env["auth.saml.provider"].create( + { + "name": "Default True Test Provider", + "idp_metadata": "", + "sp_pem_public": base64.b64encode(b"fake_public_key"), + "sp_pem_private": base64.b64encode(b"fake_private_key"), + "matching_attribute": "mail", + "sig_alg": "SIG_RSA_SHA1", + } + ) + + # It should default to True based on the global parameter + self.assertTrue(provider_true.sync_roles_strictly) + + # Change the global parameter to False + self.env["ir.config_parameter"].set_param("auth_user_role.strict_sync", "False") + + # Create another provider WITHOUT explicitly setting sync_roles_strictly + provider_false = self.env["auth.saml.provider"].create( + { + "name": "Default False Test Provider", + "idp_metadata": "", + "sp_pem_public": base64.b64encode(b"fake_public_key"), + "sp_pem_private": base64.b64encode(b"fake_private_key"), + "matching_attribute": "mail", + "sig_alg": "SIG_RSA_SHA1", + } + ) + + # It should now default to False based on the updated parameter + self.assertFalse(provider_false.sync_roles_strictly) diff --git a/auth_user_role_saml/views/auth_saml_provider_views.xml b/auth_user_role_saml/views/auth_saml_provider_views.xml new file mode 100644 index 0000000000..045af5eb6f --- /dev/null +++ b/auth_user_role_saml/views/auth_saml_provider_views.xml @@ -0,0 +1,16 @@ + + + + auth.saml.provider.form.inherit.strict.sync + auth.saml.provider + + + + + + + diff --git a/test-requirements.txt b/test-requirements.txt index 2cb24f43db..c490b0e640 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1 +1,2 @@ responses +odoo-addon-auth_user_role @ git+https://github.com/OCA/server-auth.git@refs/pull/928/head#subdirectory=auth_user_role