odoo12 用户(users) 权限管理界面分析

本文深入探讨Odoo的权限管理机制,详细解析了用户权限如何动态生成,包括组视图的自定义字段引入及权限代码背后的逻辑。通过分析源代码,揭示了Odoo如何在后台动态创建和更新权限界面。

起因:

由于需要了解 odoo的权限管理,去看了下 odoo 是如何给用户赋权限的。发现好多不能理解。因此,打算从 user 的xml开始。看里面到底是什么意思

第一步,肯定查看user的xml。找user源码

odoo/odoo/addons/base/views/res_users_views.xml

  <record id="view_users_form" model="ir.ui.view">
            <field name="name">res.users.form</field>
            <field name="model">res.users</field>
            <field name="arch" type="xml">
                <form string="Users">
                    <header>
                    </header>
                    <sheet>
                        由于只看权限,这里全部省略....
                        <notebook colspan="4">
                            <page name="access_rights" string="Access Rights">
                                <group string="Multi Companies" attrs="{'invisible': [('companies_count', '&lt;=', 1)]}">
                                    <field string="Allowed Companies" name="company_ids" widget="many2many_tags" options="{'no_create': True}"/>
                                    <field string="Current Company" name="company_id" context="{'user_preference': 0}"/>
                                    <field string="Companies count" name="companies_count" invisible="1"/>
                                </group>
                                <field name="groups_id"/>
                            </page>
                            <page string="Preferences">
                                省略......
                            </page>
                        </notebook>
                    </sheet>
                </form>
            </field>
        </record>

发现。里面根本没有 权限部分的代码。。然后找了继承view_users_form ,发现也没有。想想也对,既然权限是动态生成的,肯定要在后台完成,不会在 前台写固定的。

由于增删改都需要更新界面。所以 查看user的后台代码。找到

 @api.model
    def _update_user_groups_view(self):
        """ Modify the view with xmlid ``base.user_groups_view``, which inherits
            the user form view, and introduces the reified group fields.
        """

        # remove the language to avoid translations, it will be handled at the view level
        self = self.with_context(lang=None)

        # We have to try-catch this, because at first init the view does not
        # exist but we are already creating some basic groups.
        # 获取 base.user_groups_view 组view 的xml
        view = self.env.ref('base.user_groups_view', raise_if_not_found=False)
        if view and view.exists() and view._name == 'ir.ui.view':
            group_no_one = view.env.ref('base.group_no_one')
            group_employee = view.env.ref('base.group_user')
            xml1, xml2, xml3 = [], [], []
            # 这两个固定
            xml1.append(E.separator(string='User Type', colspan="2", groups='base.group_no_one'))
            xml2.append(E.separator(string='Application Accesses', colspan="2"))

            user_type_field_name = ''
            for app, kind, gs in self.get_groups_by_application():
                attrs = {}
                # hide groups in categories 'Hidden' and 'Extra' (except for group_no_one)
                if app.xml_id in ('base.module_category_hidden', 'base.module_category_extra', 'base.module_category_usability'):
                    attrs['groups'] = 'base.group_no_one'

                # User type (employee, portal or public) is a separated group. This is the only 'selection'
                # group of res.groups without implied groups (with each other).
                if app.xml_id == 'base.module_category_user_type':
                    # application name with a selection field
                    field_name = name_selection_groups(gs.ids)
                    user_type_field_name = field_name
                    attrs['widget'] = 'radio'
                    attrs['groups'] = 'base.group_no_one'
                    xml1.append(E.field(name=field_name, **attrs))
                    xml1.append(E.newline())

                elif kind == 'selection':
                    # 只要属于selection 的就放在Application Accesses 下
                    # application name with a selection field
                    field_name = name_selection_groups(gs.ids)
                    xml2.append(E.field(name=field_name, **attrs))
                    xml2.append(E.newline())
                else:
                    # application separator with boolean fields
                    # 不属于那两个的则为 app_name
                    app_name = app.name or 'Other'
                    xml3.append(E.separator(string=app_name, colspan="4", **attrs))
                    for g in gs:
                        field_name = name_boolean_group(g.id)
                        if g == group_no_one:
                            # make the group_no_one invisible in the form view
                            xml3.append(E.field(name=field_name, invisible="1", **attrs))
                        else:
                            xml3.append(E.field(name=field_name, **attrs))

            xml3.append({'class': "o_label_nowrap"})
            if user_type_field_name:
                user_type_attrs = {'invisible': [(user_type_field_name, '!=', group_employee.id)]}
            else:
                user_type_attrs = {}

            xml = E.field(
                E.group(*(xml1), col="2"),
                E.group(*(xml2), col="2", attrs=str(user_type_attrs)),
                E.group(*(xml3), col="4", attrs=str(user_type_attrs)), name="groups_id", position="replace")
            xml.addprevious(etree.Comment("GENERATED AUTOMATICALLY BY GROUPS"))
            xml_content = etree.tostring(xml, pretty_print=True, encoding="unicode")

            new_context = dict(view._context)
            new_context.pop('install_mode_data', None)  # don't set arch_fs for this computed view
            new_context['lang'] = None
            # 写入base.user_groups_view
            view.with_context(new_context).write({'arch': xml_content})

可以看到。完全在后台给生成了。。都有注释。只说大概原理

<record id="user_groups_view" model="ir.ui.view">
            <field name="name">res.users.groups</field>
            <field name="model">res.users</field>
            <field name="inherit_id" ref="view_users_form"/>
            <field name="arch" type="xml">
                <!-- dummy, will be modified by groups -->
                <field name="groups_id" position="after"/>
            </field>
        </record>

1、先获取user_groups_view,的view。。而此view 继承 users_form ,而且after

2、根据逻辑生成 content 。然后 写入数据库的 arch里面就ok了

 

还有个疑问,如何 确定  selection

    def get_groups_by_application(self):
        """ Return all groups classified by application (module category), as a list::

                [(app, kind, groups), ...],

            where ``app`` and ``groups`` are recordsets, and ``kind`` is either
            ``'boolean'`` or ``'selection'``. Applications are given in sequence
            order.  If ``kind`` is ``'selection'``, ``groups`` are given in
            reverse implication order.
        """
        #  app 分类  gs 组
        def linearize(app, gs):
            # 'User Type' is an exception 特例  app.xml_id 是计算字段
            if app.xml_id == 'base.module_category_user_type':
                return (app, 'selection', gs.sorted('id'))
            # determine sequence order: a group appears after its implied groups  自己继承的组
            order = {g: len(g.trans_implied_ids & gs) for g in gs}
            # check whether order is total, i.e., sequence orders are distinct
            #判断当前group 是否有平级关系,没有平级关系的话,res_groups_implied_rel 肯定无数据,set{g:0,g:0}
            # /set{g:1,g:2,g:1} 肯定不等于gs(组对象)
            if len(set(order.values())) == len(gs):
                return (app, 'selection', gs.sorted(key=order.get))
            else:
                return (app, 'boolean', gs)

        # classify all groups by application
        by_app, others = defaultdict(self.browse), self.browse()
        # 遍历所有group
        for g in self.get_application_groups([]):
            if g.category_id:
                #如果有 category
                by_app[g.category_id] += g
            else:
                others += g
        # build the result
        res = []
        for app, gs in sorted(by_app.items(), key=lambda it: it[0].sequence or 0):
            #app 分类  gs 组
            res.append(linearize(app, gs))
        if others:
            res.append((self.env['ir.module.category'], 'boolean', others))
        return res

关键点在  order = {g: len(g.trans_implied_ids & gs) for g in gs}

trans_implied_ids = fields.Many2many('res.groups', string='Transitively inherits',
    compute='_compute_trans_implied')

@api.depends('implied_ids.trans_implied_ids')
def _compute_trans_implied(self):
    # Compute the transitive closure recursively. Note that the performance
    # is good, because the record cache behaves as a memo (the field is
    # never computed twice on a given group.)
    for g in self:
        g.trans_implied_ids = g.implied_ids | g.mapped('implied_ids.trans_implied_ids')

可以看出。trans_implied_ids 其实就是只想自己的 也就是 res_groups_implied_rel 。

只有当前 category 的组 和 trans_implied_ids  不重复的id 相等,才会显示selection

a -----b

---------c

平级 则不会

 

 

### Odoo 用户权限管理概述 Odoo 提供了一套全面而灵活的权限管理系统,用于满足不同企业的需求。以下是关于用户权限设置的核心概念和技术实现。 #### 权限管理基础 Odoo权限管理基于 **用户(Group)** 和多层级的设计理念。一个用户可以属于多个用户组,而每个用户组则定义了一系列可访问的功能和资源[^4]。通过这种方式,管理员能够高效地分配权限并简化复杂的权限配置过程。 #### 多层次权限体系 Odoo 将权限划分为以下几个主要级别: 1. **菜单级权限** 控制特定菜单项是否对某类用户可见。这种级别的权限仅影响 UI 显示效果,并不能完全阻止未经授权的操作。如果用户尝试绕过前端逻辑并通过 URL 访问受保护区域,则仍需依赖更深层次的安全措施来拦截请求[^5]。 2. **对象级权限** 这是最核心也是最严格的权限控制层面之一。它决定了哪些数据库模型及其关联的数据记录可供指定群体查看、新增、编辑或者移除。具体而言,在 `ir.model.access` 表中存储着每种动作(如读取、写入等)对应于各 Model 的许可状态[^3]。 3. **视图级权限** 针对某些敏感信息(例如产品成本价),可能希望将其屏蔽给普通员工但保留给管理层人员查阅的机会;此时便需要用到此类型的设定——即限定谁能够在界面上看见这些特殊组件/控件等内容。 4. **字段级权限** 更加细致化的一种形式,允许开发者针对单独属性制定规则说明谁能触及它们以及怎样操作才是被允许的行为模式等等细节方面的要求。 #### 动态更新用户权限实例 当业务需求发生变化时,可以通过编程手段快速调整现有用户的授权情况。下面给出一段 Python 示例代码展示如何实现这一目标: ```python from odoo import models, fields, api class ResUsers(models.Model): _inherit = 'res.users' @api.multi def add_to_group(self, group_xml_id): """Add current user(s) to specified group.""" group = self.env.ref(group_xml_id) if not isinstance(group, models.BaseModel): # Ensure we got a valid Group object. raise ValueError(f"Invalid XML ID provided: {group_xml_id}") for rec in self: rec.write({'groups_id': [(4, group.id)]}) @api.multi def remove_from_group(self, group_xml_id): """Remove current user(s) from specified group.""" group = self.env.ref(group_xml_id) if not isinstance(group, models.BaseModel): raise ValueError(f"Invalid XML ID provided: {group_xml_id}") for rec in self: rec.write({'groups_id': [(3, group.id)]}) ``` 上述函数分别实现了向选定用户添加或移除所属群组功能的方法调用接口[^1]。 #### 创建自定义权限组步骤概览 为了更好地适应实际应用场景中的复杂要求,有时还需要构建全新的角色类别以便进一步细化分工协作关系。以下是建立新分组的大致流程描述: 1. 定义一个新的安全文件夹路径下的 CSV 文件用来声明新的 Access Rights 设置; 2. 编辑相应的 .py 源码片段引入刚才提到的那个 csv 中所列举出来的各项权利项目; 3. 更新模块结构使之生效即可完成整个定制化进程。 --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值