Files
tryton/modules/user_role/res.py
T
Cédric Krier eeb2b75b4c Forbid to set role to admin user
The links between admin user and groups are managed by XML record.
But the sync task may delete such link if a role is assigned to admin because
the task is run as root which skip the XML check.

Closes #15086
(grafted from 0512f49869883b95b74387a292753e9e43375f35)

--HG--
branch : 8.0
2026-09-10 16:07:26 +02:00

140 lines
4.5 KiB
Python

# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import datetime as dt
from trytond.model import ModelSQL, ModelView, fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id, If
class Role(ModelSQL, ModelView):
__name__ = 'res.role'
name = fields.Char("Name", required=True, translate=True)
groups = fields.Many2Many('res.role-res.group', 'role', 'group', "Groups")
users = fields.One2Many('res.user.role', 'role', "Users")
@classmethod
def on_modification(cls, mode, roles, field_names=None):
pool = Pool()
User = pool.get('res.user')
super().on_modification(mode, roles, field_names=field_names)
if mode == 'write':
users = {u.user for r in roles for u in r.users}
User.sync_roles(User.browse(users))
@classmethod
def on_delete(cls, roles):
pool = Pool()
User = pool.get('res.user')
callback = super().on_delete(roles)
if users := {u.user for r in roles for u in r.users}:
callback.append(lambda: User.sync_roles(
User.browse(users), clear=True))
return callback
@classmethod
def copy(cls, roles, default=None):
default = default.copy() if default is not None else {}
default.setdefault('users')
return super().copy(roles, default=default)
class RoleGroup(ModelSQL):
__name__ = 'res.role-res.group'
role = fields.Many2One(
'res.role', "Role", ondelete='CASCADE', required=True)
group = fields.Many2One(
'res.group', "Group", ondelete='CASCADE', required=True)
class User(metaclass=PoolMeta):
__name__ = 'res.user'
roles = fields.One2Many(
'res.user.role', 'user', "Roles",
states={
'invisible': Eval('id', -1) == Id('res', 'user_admin'),
})
@classmethod
def __setup__(cls):
super().__setup__()
group_readonly = Bool(Eval('roles', []))
if 'readonly' in cls.groups.states:
group_readonly |= cls.groups.states['readonly']
cls.groups.states['readonly'] = group_readonly
cls._context_fields.append('roles')
@classmethod
def on_modification(cls, mode, users, field_names=None):
super().on_modification(mode, users, field_names=field_names)
if mode in {'create', 'write'}:
cls.sync_roles(users)
@classmethod
def sync_roles(cls, users=None, date=None, clear=False):
pool = Pool()
ModelData = pool.get('ir.model.data')
admin_id = ModelData.get_id('res', 'user_admin')
if date is None:
date = dt.datetime.now()
if users is None:
users = cls.search([])
to_write = []
for user in users:
if user.id == admin_id:
continue
if not user.roles and not clear:
continue
new = {g.id for r in user.roles for g in r.role.groups
if r.valid(date)}
old = {g.id for g in user.groups}
if new != old:
to_write.append([user])
to_write.append({'groups': [
('add', new - old),
('remove', old - new),
]})
if to_write:
cls.write(*to_write)
class UserRole(ModelSQL, ModelView):
__name__ = 'res.user.role'
user = fields.Many2One(
'res.user', "User", ondelete='CASCADE', required=True,
domain=[
If(Eval('user', -1) == Id('res', 'user_admin'),
('id', '=', -1),
()),
])
role = fields.Many2One(
'res.role', "Role", ondelete='CASCADE', required=True)
from_date = fields.DateTime(
"From Date",
domain=[
If(Eval('from_date') & Eval('to_date'),
('from_date', '<=', Eval('to_date', None)),
()),
])
to_date = fields.DateTime(
"To Date",
domain=[
If(Eval('from_date') & Eval('to_date'),
('to_date', '>=', Eval('from_date', None)),
()),
])
@classmethod
def __setup__(cls):
super().__setup__()
cls._order.insert(0, ('from_date', 'DESC NULLS FIRST'))
cls._order.insert(1, ('to_date', 'ASC NULLS FIRST'))
def valid(self, date):
from_date = self.from_date or dt.datetime.min
to_date = self.to_date or dt.datetime.max
return from_date <= date <= to_date