Fixes new SSO signups landing as Portal users instead of Internal (unconditionally, for any OAuth provider, once installed), and syncs Odoo groups from an OIDC provider's groups claim on every login.
86 lines
4.0 KiB
Python
86 lines
4.0 KiB
Python
import logging
|
|
|
|
from odoo import api, models
|
|
from odoo.addons.auth_signup.models.res_partner import SignupError
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
OIDC_SYNCED_CATEGORY_XMLID = "auth_oidc_group_sync.module_category_oidc_synced"
|
|
TEMPLATE_USER_XMLID = "auth_oidc_group_sync.template_internal_user_id"
|
|
|
|
|
|
class ResUsers(models.Model):
|
|
_inherit = "res.users"
|
|
|
|
def _create_user_from_template(self, values):
|
|
# Applies to any OAuth-based signup.
|
|
if values.get("oauth_provider_id"):
|
|
if not values.get("login"):
|
|
raise ValueError(self.env._("Signup: no login given for new user"))
|
|
values["active"] = True
|
|
try:
|
|
with self.env.cr.savepoint():
|
|
template_user = self.env.ref(TEMPLATE_USER_XMLID)
|
|
return template_user.with_context(no_reset_password=True).copy(values)
|
|
except Exception as e:
|
|
raise SignupError(str(e)) from e
|
|
return super()._create_user_from_template(values)
|
|
|
|
@api.model
|
|
def _auth_oauth_signin(self, provider, validation, params):
|
|
login = super()._auth_oauth_signin(provider, validation, params)
|
|
if login:
|
|
user = self.env["res.users"].sudo().search([("login", "=", login)], limit=1)
|
|
if user:
|
|
user._sync_oidc_groups(validation)
|
|
return login
|
|
|
|
def _sync_oidc_groups(self, validation):
|
|
self.ensure_one()
|
|
if "groups" not in validation:
|
|
# Absent claim (misconfigured scope) must NOT be treated as an empty claim —
|
|
# that would revoke every auto-provisioned group on every login until fixed.
|
|
_logger.warning(
|
|
"OIDC login for %s: no 'groups' claim in token, skipping role sync", self.login
|
|
)
|
|
return
|
|
claim_groups = set(validation.get("groups") or [])
|
|
Groups = self.env["res.groups"].sudo()
|
|
category = self.env.ref(OIDC_SYNCED_CATEGORY_XMLID)
|
|
desired = Groups.browse()
|
|
for name in claim_groups:
|
|
group = Groups.search(
|
|
[("category_id", "=", category.id), ("name", "=", name)], limit=1
|
|
)
|
|
if not group:
|
|
group = Groups.create({"name": name, "category_id": category.id})
|
|
desired |= group
|
|
owned_universe = Groups.search([("category_id", "=", category.id)])
|
|
current_owned = self.groups_id & owned_universe
|
|
to_add = desired - current_owned
|
|
to_remove = current_owned - desired
|
|
if not (to_add or to_remove):
|
|
return
|
|
|
|
# Odoo materializes implied_ids into groups_id at write-time, not just at
|
|
# has_group()-check time, so revoking a group that implies others must also clean
|
|
# up whatever it materialized - but only what's not independently justified.
|
|
# independently_held excludes anything reachable from to_remove itself, so a
|
|
# multi-level chain (A implies B, B implies C) is unwound all the way rather than
|
|
# stopping at the first level once the removed group's own contribution is
|
|
# excluded from the "still granted" check. base.group_user is always kept in the
|
|
# base regardless: _create_user_from_template guarantees it to every SSO user
|
|
# independent of any claimed group, and Odoo has no way to tell that apart from a
|
|
# copy that only happens to also be implied by whatever's being revoked.
|
|
floor = self.env.ref("base.group_user")
|
|
floor_closure = floor | floor.trans_implied_ids
|
|
implied_by_removed = to_remove.trans_implied_ids
|
|
independently_held = self.groups_id - to_remove - implied_by_removed
|
|
final_base = independently_held | to_add | (self.groups_id & floor_closure)
|
|
still_implied = final_base | final_base.trans_implied_ids
|
|
to_unlink = to_remove | (implied_by_removed - still_implied)
|
|
|
|
self.write({
|
|
"groups_id": [(3, g.id) for g in to_unlink] + [(4, g.id) for g in to_add],
|
|
})
|