Skip to content

Commit 96db213

Browse files
committed
[18.0][ADD] auth_user_role
1 parent 963b6c5 commit 96db213

19 files changed

Lines changed: 1128 additions & 0 deletions

auth_user_role/README.rst

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
========================
2+
360 ERP - Auth User Role
3+
========================
4+
5+
..
6+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
7+
!! This file is generated by oca-gen-addon-readme !!
8+
!! changes will be overwritten. !!
9+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
10+
!! source digest: sha256:6b4bcdd74e55d4277803a74f7ac68e4bc51c14d2c4dec736cdfd1dea0ec9ee20
11+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
12+
13+
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
14+
:target: https://odoo-community.org/page/development-status
15+
:alt: Beta
16+
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
17+
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
18+
:alt: License: LGPL-3
19+
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--auth-lightgray.png?logo=github
20+
:target: https://github.com/OCA/server-auth/tree/18.0/auth_user_role
21+
:alt: OCA/server-auth
22+
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
23+
:target: https://translation.odoo-community.org/projects/server-auth-18-0/server-auth-18-0-auth_user_role
24+
:alt: Translate me on Weblate
25+
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
26+
:target: https://runboat.odoo-community.org/builds?repo=OCA/server-auth&target_branch=18.0
27+
:alt: Try me on Runboat
28+
29+
|badge1| |badge2| |badge3| |badge4| |badge5|
30+
31+
This module provides a generic engine to map Identity Provider (IdP)
32+
attributes to Odoo user roles. It acts as an abstraction layer built on
33+
top of the ``base_user_role`` module.
34+
35+
By itself, this module does not handle authentication. Instead, it is
36+
designed to be triggered by specialized "glue" modules (e.g., SAML,
37+
OAuth, LDAP) during the login process. It evaluates incoming identity
38+
payloads against a set of configured global rules and dynamically
39+
provisions or revokes user roles.
40+
41+
**Table of contents**
42+
43+
.. contents::
44+
:local:
45+
46+
Configuration
47+
=============
48+
49+
To configure role mappings:
50+
51+
1. Navigate to **Settings > Users & Companies > Identity Role
52+
Mappings**.
53+
2. Create a new mapping rule.
54+
3. Define the **Identity Attribute**: Enter the exact payload attribute
55+
key provided by your IdP (e.g., ``department``, ``groups``,
56+
``eduPersonAffiliation``).
57+
4. Select the **Operator**:
58+
59+
- **equals**: The payload value must exactly match the defined value.
60+
- **contains**: The payload value must contain the defined value
61+
(useful for comma-separated lists or longer strings).
62+
63+
5. Define the **Value** you expect to receive from the IdP.
64+
6. Select the **Role** (from ``base_user_role``) that should be assigned
65+
when the condition is met.
66+
67+
Usage
68+
=====
69+
70+
There is no direct user interaction required for this module. Once
71+
configured, the evaluation and assignment of roles happen automatically
72+
in the background whenever an integrated authentication provider
73+
triggers the ``evaluate_and_apply_auth_roles`` method during user
74+
sign-in.
75+
76+
All role grants, reactivations, and revocations are automatically logged
77+
in the Odoo server logs for security auditing.
78+
79+
Bug Tracker
80+
===========
81+
82+
Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-auth/issues>`_.
83+
In case of trouble, please check there if your issue has already been reported.
84+
If you spotted it first, help us to smash it by providing a detailed and welcomed
85+
`feedback <https://github.com/OCA/server-auth/issues/new?body=module:%20auth_user_role%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
86+
87+
Do not contact contributors directly about support or help with technical issues.
88+
89+
Credits
90+
=======
91+
92+
Authors
93+
-------
94+
95+
* 360 ERP
96+
97+
Contributors
98+
------------
99+
100+
- Andrea Stirpe
101+
102+
Other credits
103+
-------------
104+
105+
The development of this module has been financially supported by:
106+
107+
- 360 ERP
108+
109+
Maintainers
110+
-----------
111+
112+
This module is maintained by the OCA.
113+
114+
.. image:: https://odoo-community.org/logo.png
115+
:alt: Odoo Community Association
116+
:target: https://odoo-community.org
117+
118+
OCA, or the Odoo Community Association, is a nonprofit organization whose
119+
mission is to support the collaborative development of Odoo features and
120+
promote its widespread use.
121+
122+
This module is part of the `OCA/server-auth <https://github.com/OCA/server-auth/tree/18.0/auth_user_role>`_ project on GitHub.
123+
124+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

auth_user_role/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Copyright 2026 360ERP (<https://www.360erp.com>)
2+
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
3+
4+
from . import models

auth_user_role/__manifest__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright 2026 360ERP (<https://www.360erp.com>)
2+
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
3+
4+
{
5+
"name": "360 ERP - Auth User Role",
6+
"version": "18.0.1.0.0",
7+
"author": "360 ERP, Odoo Community Association (OCA)",
8+
"website": "https://github.com/OCA/server-auth",
9+
"license": "LGPL-3",
10+
"depends": [
11+
"base_user_role",
12+
],
13+
"data": [
14+
"security/ir.model.access.csv",
15+
"views/auth_user_role_mapping_views.xml",
16+
],
17+
}

auth_user_role/models/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Copyright 2026 360ERP (<https://www.360erp.com>)
2+
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
3+
4+
from . import auth_user_role_mapping
5+
from . import res_config_settings
6+
from . import res_users
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Copyright 2026 360ERP (<https://www.360erp.com>)
2+
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
3+
4+
from odoo import api, fields, models, tools
5+
6+
7+
class AuthUserRoleMapping(models.Model):
8+
_name = "auth.user.role.mapping"
9+
_description = "Identity Role Mapping"
10+
_rec_name = "attribute"
11+
_order = "attribute"
12+
13+
attribute = fields.Char(
14+
string="Identity Attribute",
15+
help=(
16+
"The payload attribute to check (e.g., department, "
17+
"groups, eduPersonAffiliation)."
18+
),
19+
required=True,
20+
)
21+
operator = fields.Selection(
22+
selection=[("equals", "equals"), ("contains", "contains")],
23+
default="equals",
24+
required=True,
25+
help="The operator to check the attribute against the value.",
26+
)
27+
value = fields.Char(help="The value to check the attribute against.", required=True)
28+
role_id = fields.Many2one(
29+
"res.users.role",
30+
help="The Odoo role to assign.",
31+
required=True,
32+
ondelete="cascade",
33+
)
34+
35+
@api.model
36+
@tools.ormcache()
37+
def _get_all_mappings_cached(self):
38+
"""Fetch all mappings and cache them as native dicts for fast evaluation."""
39+
mappings = self.sudo().search([])
40+
return [
41+
{
42+
"attribute": m.attribute,
43+
"operator": m.operator,
44+
"value": m.value,
45+
"role_id": m.role_id.id,
46+
}
47+
for m in mappings
48+
]
49+
50+
@api.model_create_multi
51+
def create(self, vals_list):
52+
self.env.registry.clear_cache()
53+
return super().create(vals_list)
54+
55+
def write(self, vals):
56+
self.env.registry.clear_cache()
57+
return super().write(vals)
58+
59+
def unlink(self):
60+
self.env.registry.clear_cache()
61+
return super().unlink()
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright 2026 360ERP (<https://www.360erp.com>)
2+
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
3+
4+
from odoo import fields, models
5+
6+
7+
class ResConfigSettings(models.TransientModel):
8+
_inherit = "res.config.settings"
9+
10+
auth_user_role_strict_sync = fields.Boolean(
11+
string="Strict Identity Role Synchronization",
12+
config_parameter="auth_user_role.strict_sync",
13+
default=True,
14+
help=(
15+
"If enabled globally, any Odoo roles manually assigned to a user will be "
16+
"removed if they are not explicitly provided by the "
17+
"Identity Provider payload."
18+
),
19+
)

auth_user_role/models/res_users.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Copyright 2026 360ERP (<https://www.360erp.com>)
2+
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
3+
4+
import logging
5+
6+
from odoo import models
7+
8+
_logger = logging.getLogger(__name__)
9+
10+
11+
class ResUser(models.Model):
12+
_inherit = "res.users"
13+
14+
def _get_mapped_roles(self, identity_payload):
15+
"""Helper to evaluate the identity payload against role mappings."""
16+
roles_to_add = set()
17+
if not identity_payload:
18+
return roles_to_add
19+
20+
cached_mappings = self.env["auth.user.role.mapping"]._get_all_mappings_cached()
21+
22+
for mapping in cached_mappings:
23+
if mapping["attribute"] not in identity_payload:
24+
continue
25+
26+
attribute_values = identity_payload.get(mapping["attribute"])
27+
if not isinstance(attribute_values, list):
28+
attribute_values = [attribute_values]
29+
30+
for attr_val in attribute_values:
31+
attr_str = str(attr_val)
32+
if mapping["operator"] == "equals" and attr_str == mapping["value"]:
33+
roles_to_add.add(mapping["role_id"])
34+
elif mapping["operator"] == "contains" and mapping["value"] in attr_str:
35+
roles_to_add.add(mapping["role_id"])
36+
37+
return roles_to_add
38+
39+
def evaluate_and_apply_auth_roles(self, identity_payload, strict_sync=None):
40+
"""
41+
Abstraction layer to evaluate an identity payload against global mappings
42+
and apply the resulting roles to the user.
43+
"""
44+
self.ensure_one()
45+
46+
# Fall back to global system parameter if not explicitly overridden
47+
if strict_sync is None:
48+
strict_sync = (
49+
self.env["ir.config_parameter"]
50+
.sudo()
51+
.get_param("auth_user_role.strict_sync", "True")
52+
== "True"
53+
)
54+
55+
roles_to_add = self._get_mapped_roles(identity_payload)
56+
57+
existing_lines = self.role_line_ids
58+
existing_role_ids = set(existing_lines.mapped("role_id").ids)
59+
active_role_ids = set(self._get_enabled_roles().mapped("role_id").ids)
60+
61+
commands = []
62+
roles_removed_log = []
63+
roles_added_log = []
64+
65+
if strict_sync:
66+
roles_to_remove = existing_role_ids - roles_to_add
67+
if roles_to_remove:
68+
lines_to_remove = existing_lines.filtered(
69+
lambda el: el.role_id.id in roles_to_remove
70+
)
71+
for line in lines_to_remove:
72+
commands.append((2, line.id, 0))
73+
roles_removed_log.append(line.role_id.name)
74+
75+
for role_id in roles_to_add:
76+
if role_id not in existing_role_ids:
77+
commands.append((0, 0, {"role_id": role_id}))
78+
role = self.env["res.users.role"].browse(role_id)
79+
roles_added_log.append(role.name)
80+
elif role_id not in active_role_ids:
81+
line_to_activate = existing_lines.filtered(
82+
lambda el, rid=role_id: el.role_id.id == rid
83+
)
84+
if line_to_activate:
85+
commands.append((1, line_to_activate[0].id, {"date_to": False}))
86+
role = self.env["res.users.role"].browse(role_id)
87+
roles_added_log.append(f"{role.name} (Reactivated)")
88+
89+
if commands:
90+
self.write({"role_line_ids": commands})
91+
if roles_removed_log:
92+
_logger.info(
93+
"Identity Sync - Removed roles from user %s: %s",
94+
self.login,
95+
", ".join(roles_removed_log),
96+
)
97+
if roles_added_log:
98+
_logger.info(
99+
"Identity Sync - Granted roles to user %s: %s",
100+
self.login,
101+
", ".join(roles_added_log),
102+
)
103+
104+
if strict_sync:
105+
self.set_groups_from_roles(force=True)
106+
107+
return list(roles_to_add)

auth_user_role/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[build-system]
2+
requires = ["whool"]
3+
build-backend = "whool.buildapi"

auth_user_role/readme/CONFIGURE.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
To configure role mappings:
2+
3+
1. Navigate to **Settings > Users & Companies > Identity Role Mappings**.
4+
2. Create a new mapping rule.
5+
3. Define the **Identity Attribute**: Enter the exact payload attribute key provided by your IdP (e.g., `department`, `groups`, `eduPersonAffiliation`).
6+
4. Select the **Operator**:
7+
* **equals**: The payload value must exactly match the defined value.
8+
* **contains**: The payload value must contain the defined value (useful for comma-separated lists or longer strings).
9+
5. Define the **Value** you expect to receive from the IdP.
10+
6. Select the **Role** (from `base_user_role`) that should be assigned when the condition is met.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* Andrea Stirpe

0 commit comments

Comments
 (0)