diff --git a/muk_autovacuum/__manifest__.py b/muk_autovacuum/__manifest__.py index 74e5c4f..da942ba 100644 --- a/muk_autovacuum/__manifest__.py +++ b/muk_autovacuum/__manifest__.py @@ -20,7 +20,7 @@ { "name": "MuK Autovacuum", "summary": """Configure automatic garbage collection""", - "version": "11.0.2.1.0", + "version": "11.0.2.1.1", "category": "Extra Tools", "license": "AGPL-3", "website": "https://www.mukit.at", diff --git a/muk_autovacuum/models/ir_autovacuum.py b/muk_autovacuum/models/ir_autovacuum.py index ecda219..f262e8c 100644 --- a/muk_autovacuum/models/ir_autovacuum.py +++ b/muk_autovacuum/models/ir_autovacuum.py @@ -62,8 +62,10 @@ class AutoVacuum(models.AbstractModel): if rule.state == 'time': computed_time = datetime.datetime.utcnow() - _types[rule.time_type](rule.time) domain = [(rule.time_field.name, '<', computed_time.strftime(DEFAULT_SERVER_DATETIME_FORMAT))] - if rule.protect_starred and "starred" in rule.model.field_id.mapped("name"): - domain.append(('starred', '=', False)) + if rule.protect_starred: + for field in rule.model.field_id: + if field.name in ['starred', 'favorite', 'is_starred', 'is_favorite']: + domain.append((field.name, '=', False)) if rule.only_inactive and "active" in rule.model.field_id.mapped("name"): domain.append(('active', '=', False)) _logger.info(_("GC domain: %s"), domain) diff --git a/muk_autovacuum/models/rules.py b/muk_autovacuum/models/rules.py index b1f93e1..db8d1b8 100644 --- a/muk_autovacuum/models/rules.py +++ b/muk_autovacuum/models/rules.py @@ -205,7 +205,12 @@ class AutoVacuumRules(models.Model): 'size': [('invisible', True)], 'domain': [('invisible', True)], 'code': [('invisible', True)]}, - help="Do not delete starred records.") + help="""Do not delete starred records. + Checks for the following fields: + - starred + - favorite + - is_starred + - is_favorite""") only_inactive = fields.Boolean( string='Only Archived', diff --git a/muk_security/__init__.py b/muk_security/__init__.py index 3a632f4..ba31b8c 100644 --- a/muk_security/__init__.py +++ b/muk_security/__init__.py @@ -17,4 +17,7 @@ # ################################################################################### -from . import models \ No newline at end of file +from . import models + +def _patch_system(): + from . import base \ No newline at end of file diff --git a/muk_security/__manifest__.py b/muk_security/__manifest__.py index e149e7b..d0bdd36 100644 --- a/muk_security/__manifest__.py +++ b/muk_security/__manifest__.py @@ -20,7 +20,7 @@ { "name": "MuK Security", "summary": """Security Features""", - "version": "11.0.1.0.0", + "version": "11.0.1.1.0", "category": "Extra Tools", "license": "AGPL-3", "website": "http://www.mukit.at", @@ -31,11 +31,14 @@ ], "depends": [ "muk_utils", + "muk_autovacuum", ], "data": [ + "security/security.xml", "security/ir.model.access.csv", "views/lock.xml", "views/groups.xml", + "data/autovacuum.xml", ], "qweb": [ "static/src/xml/*.xml", @@ -50,4 +53,5 @@ "auto_install": True, "application": False, "installable": True, + "post_load": "_patch_system", } \ No newline at end of file diff --git a/muk_security/base/__init__.py b/muk_security/base/__init__.py new file mode 100644 index 0000000..cb2ac54 --- /dev/null +++ b/muk_security/base/__init__.py @@ -0,0 +1,23 @@ +################################################################################### +# +# MuK Document Management System +# +# Copyright (C) 2018 MuK IT GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +################################################################################### + +from . import api +from . import models \ No newline at end of file diff --git a/muk_security/base/api.py b/muk_security/base/api.py new file mode 100644 index 0000000..acd9848 --- /dev/null +++ b/muk_security/base/api.py @@ -0,0 +1,36 @@ +################################################################################### +# +# Copyright (C) 2018 MuK IT GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +################################################################################### + +import logging + +from odoo import models, api, SUPERUSER_ID + +from odoo.addons.muk_utils.tools import patch +from odoo.addons.muk_security.tools import helper + +_logger = logging.getLogger(__name__) + +@api.model +@patch.monkey_patch(api.Environment) +def __call__(self, cr=None, user=None, context=None): + env = __call__.super(self, cr, user, context) + if user and isinstance(user, helper.NoSecurityUid): + env.uid = user + return env + return env \ No newline at end of file diff --git a/muk_security/base/models.py b/muk_security/base/models.py new file mode 100644 index 0000000..ab20304 --- /dev/null +++ b/muk_security/base/models.py @@ -0,0 +1,40 @@ +################################################################################### +# +# Copyright (C) 2018 MuK IT GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +################################################################################### + +import logging + +from odoo import models, api, SUPERUSER_ID + +from odoo.addons.muk_utils.tools import patch +from odoo.addons.muk_security.tools import helper + +_logger = logging.getLogger(__name__) + +@api.model +def suspend_security(self, user=None): + return self.sudo(user=helper.NoSecurityUid(user or self.env.uid)) + +models.BaseModel.suspend_security = suspend_security + +@api.model +@patch.monkey_patch_model(models.BaseModel) +def check_field_access_rights(self, operation, fields): + if isinstance(self.env.uid, helper.NoSecurityUid): + return fields or list(self._fields) + return check_field_access_rights.super(self, operation, fields) \ No newline at end of file diff --git a/muk_security/data/autovacuum.xml b/muk_security/data/autovacuum.xml new file mode 100644 index 0000000..0c6f26e --- /dev/null +++ b/muk_security/data/autovacuum.xml @@ -0,0 +1,35 @@ + + + + + + + + Cleans up locks that have not been removed correctly + code + + +locks = env['muk_security.lock'] +for lock in model.search([]): + if not lock.lock_ref: + locks |= lock +locks.unlink() + + + + \ No newline at end of file diff --git a/muk_security/doc/changelog.rst b/muk_security/doc/changelog.rst index 9ee2b48..5a60853 100644 --- a/muk_security/doc/changelog.rst +++ b/muk_security/doc/changelog.rst @@ -1,3 +1,8 @@ +`1.1.0` +------- + +- Updated dependencies + `1.0.0` ------- diff --git a/muk_security/doc/index.rst b/muk_security/doc/index.rst index 0499d6a..938f5d3 100644 --- a/muk_security/doc/index.rst +++ b/muk_security/doc/index.rst @@ -2,7 +2,7 @@ MuK Security ============ -Technical module to provide some utility features and libraries that can be used +Technical module to provide some utility and security features that can be used in other applications. This module has no direct effect on the running system. Installation @@ -23,7 +23,7 @@ No additional configuration is needed to use this module. Usage ============= -This module has no direct visible effect on the system. It provide utility features. +This module has no direct visible effect on the system. It provide security features. Credits ======= diff --git a/muk_security/models/__init__.py b/muk_security/models/__init__.py index 6e4f8d6..5354471 100644 --- a/muk_security/models/__init__.py +++ b/muk_security/models/__init__.py @@ -20,6 +20,9 @@ from . import lock from . import locking from . import access -from . import groups from . import access_groups +from . import security_groups from . import res_groups +from . import res_users +from . import ir_rule +from . import ir_model_access diff --git a/muk_security/models/access.py b/muk_security/models/access.py index 5ddfdaa..192324a 100644 --- a/muk_security/models/access.py +++ b/muk_security/models/access.py @@ -37,18 +37,22 @@ class BaseModelAccess(models.AbstractModel): permission_read = fields.Boolean( compute='_compute_permissions', + search='_search_permission_read', string="Read Access") permission_create = fields.Boolean( compute='_compute_permissions', + search='_search_permission_create', string="Create Access") permission_write = fields.Boolean( compute='_compute_permissions', + search='_search_permission_write', string="Write Access") permission_unlink = fields.Boolean( compute='_compute_permissions', + search='_search_permission_unlink', string="Delete Access") #---------------------------------------------------------- @@ -76,12 +80,47 @@ class BaseModelAccess(models.AbstractModel): try: access_right = self.check_access_rights(operation, raise_exception) access_rule = self.check_access_rule(operation) == None - return access_right and access_rule + access = access_right and access_rule + if not access and raise_exception: + raise AccessError(_("This operation is forbidden!")) + return access except AccessError: if raise_exception: raise AccessError(_("This operation is forbidden!")) return False + #---------------------------------------------------------- + # Search + #---------------------------------------------------------- + + @api.model + def _search_permission_read(self, operator, operand): + records = self.search([]).filtered(lambda r: r.check_access('read') == True) + if operator == '=' and operand: + return [('id', 'in', records.mapped('id'))] + return [('id', 'not in', records.mapped('id'))] + + @api.model + def _search_permission_create(self, operator, operand): + records = self.search([]).filtered(lambda r: r.check_access('create') == True) + if operator == '=' and operand: + return [('id', 'in', records.mapped('id'))] + return [('id', 'not in', records.mapped('id'))] + + @api.model + def _search_permission_write(self, operator, operand): + records = self.search([]).filtered(lambda r: r.check_access('write') == True) + if operator == '=' and operand: + return [('id', 'in', records.mapped('id'))] + return [('id', 'not in', records.mapped('id'))] + + @api.model + def _search_permission_unlink(self, operator, operand): + records = self.search([]).filtered(lambda r: r.check_access('unlink') == True) + if operator == '=' and operand: + return [('id', 'in', records.mapped('id'))] + return [('id', 'not in', records.mapped('id'))] + #---------------------------------------------------------- # Read, View #---------------------------------------------------------- diff --git a/muk_security/models/access_groups.py b/muk_security/models/access_groups.py index ff7a772..2951f98 100644 --- a/muk_security/models/access_groups.py +++ b/muk_security/models/access_groups.py @@ -23,6 +23,8 @@ from odoo import _, SUPERUSER_ID from odoo import models, api, fields from odoo.exceptions import AccessError +from odoo.addons.muk_security.tools import helper + _logger = logging.getLogger(__name__) class BaseModelAccessGroups(models.AbstractModel): @@ -32,12 +34,18 @@ class BaseModelAccessGroups(models.AbstractModel): _inherit = 'muk_security.access' # Set it to True to enforced security even if no group has been set - _strict_security = False + _strict_security = False + + # If set the group fields are restricted by the access group + _field_groups = None + + # If set the suspend fields are restricted by the access group + _suspend_groups = None #---------------------------------------------------------- - # Function + # Datebase #---------------------------------------------------------- - + @api.model def _add_magic_fields(self): super(BaseModelAccessGroups, self)._add_magic_fields() @@ -45,6 +53,30 @@ class BaseModelAccessGroups(models.AbstractModel): if name not in self._fields: self._add_field(name, field) base, model = self._name.split(".") + add('suspend_security_read', fields.Boolean( + _module=base, + string="Suspend Security for Read", + automatic=True, + default=False, + groups=self._suspend_groups)) + add('suspend_security_create', fields.Boolean( + _module=base, + string="Suspend Security for Create", + automatic=True, + default=False, + groups=self._suspend_groups)) + add('suspend_security_write', fields.Boolean( + _module=base, + string="Suspend Security for Write", + automatic=True, + default=False, + groups=self._suspend_groups)) + add('suspend_security_unlink', fields.Boolean( + _module=base, + string="Suspend Security for Unlink", + automatic=True, + default=False, + groups=self._suspend_groups)) add('groups', fields.Many2many( _module=base, comodel_name='muk_security.groups', @@ -52,7 +84,8 @@ class BaseModelAccessGroups(models.AbstractModel): column1='aid', column2='gid', string="Groups", - automatic=True)) + automatic=True, + groups=self._field_groups)) add('complete_groups', fields.Many2many( _module=base, comodel_name='muk_security.groups', @@ -62,7 +95,32 @@ class BaseModelAccessGroups(models.AbstractModel): string="Complete Groups", compute='_compute_groups', store=True, - automatic=True)) + automatic=True, + groups=self._field_groups)) + + #---------------------------------------------------------- + # Function + #---------------------------------------------------------- + + @api.multi + def trigger_computation(self, fields, *largs, **kwargs): + super(BaseModelAccessGroups, self).trigger_computation(fields, *largs, **kwargs) + if "complete_groups" in fields: + self.suspend_security()._compute_groups() + + @api.model + def check_group_values(self, values): + if any(field in values for field in ['groups']): + return True + return False + + @api.multi + @api.returns('muk_security.groups') + def get_groups(self): + self.ensure_one() + groups = self.env['muk_security.groups'] + groups |= self.groups + return groups @api.model def _get_no_access_ids(self): @@ -83,6 +141,18 @@ class BaseModelAccessGroups(models.AbstractModel): else: return [] + @api.model + def _get_suspended_access_ids(self, operation): + base, model = self._name.split(".") + sql = ''' + SELECT id + FROM %s a + WHERE a.suspend_security_%s = true + ''' % (self._table, operation) + self.env.cr.execute(sql) + fetch = self.env.cr.fetchall() + return len(fetch) > 0 and list(map(lambda x: x[0], fetch)) or [] + @api.model def _get_access_ids(self): base, model = self._name.split(".") @@ -90,7 +160,7 @@ class BaseModelAccessGroups(models.AbstractModel): SELECT r.aid FROM muk_groups_complete_%s_rel r JOIN muk_security_groups g ON r.gid = g.id - JOIN muk_groups_users_rel u ON r.gid = u.gid + JOIN muk_security_groups_users_rel u ON r.gid = u.gid WHERE u.uid = %s AND g.perm_read = true ''' % (model, self.env.user.id) self.env.cr.execute(sql) @@ -99,42 +169,67 @@ class BaseModelAccessGroups(models.AbstractModel): return access_ids @api.model - def _get_complete_access_ids(self): - return self._get_no_access_ids() + self._get_access_ids() + def _get_ids_without_security(self, operation): + no_access_ids = self._get_no_access_ids() + suspended_access_ids = self._get_suspended_access_ids(operation) + return list(set(no_access_ids).union(suspended_access_ids)) + + @api.model + def _get_complete_access_ids(self, operation): + access_ids = self._get_access_ids() + no_access_ids = self._get_no_access_ids() + suspended_access_ids = self._get_suspended_access_ids(operation) + return list(set(access_ids).union(no_access_ids, suspended_access_ids)) @api.multi - def _eval_access_skip(self): + def _eval_access_skip(self, operation): + if isinstance(self.env.uid, helper.NoSecurityUid): + return True return False @api.multi - def check_access_rule(self, operation): - super(BaseModelAccessGroups, self).check_access_rule(operation) - if self.env.user.id == SUPERUSER_ID or self._eval_access_skip(): + def check_access_groups(self, operation): + if self.env.user.id == SUPERUSER_ID or self._eval_access_skip(operation): return None base, model = self._name.split(".") - no_access_ids = self._get_no_access_ids() - for record in self.filtered(lambda rec: rec.id not in no_access_ids): + filter_ids = self._get_ids_without_security(operation) + for record in self.filtered(lambda rec: rec.id not in filter_ids): sql = ''' SELECT perm_%s FROM muk_groups_complete_%s_rel r JOIN muk_security_groups g ON g.id = r.gid - JOIN muk_groups_users_rel u ON u.gid = g.id + JOIN muk_security_groups_users_rel u ON u.gid = g.id WHERE r.aid = %s AND u.uid = %s ''' % (operation, model, record.id, self.env.user.id) self.env.cr.execute(sql) fetch = self.env.cr.fetchall() if not any(list(map(lambda x: x[0], fetch))): raise AccessError(_("This operation is forbidden!")) + + @api.multi + def check_access(self, operation, raise_exception=False): + res = super(BaseModelAccessGroups, self).check_access(operation, raise_exception) + try: + access_groups = self.check_access_groups(operation) == None + access = res and access_groups + if not access and raise_exception: + raise AccessError(_("This operation is forbidden!")) + return access + except AccessError: + if raise_exception: + raise AccessError(_("This operation is forbidden!")) + return False #---------------------------------------------------------- # Read #---------------------------------------------------------- - + + @api.multi def _after_read(self, result, *largs, **kwargs): result = super(BaseModelAccessGroups, self)._after_read(result) - if self.env.user.id == SUPERUSER_ID or self._eval_access_skip(): + if self.env.user.id == SUPERUSER_ID or self._eval_access_skip("read"): return result - access_ids = self._get_complete_access_ids() + access_ids = self._get_complete_access_ids("read") result = [result] if not isinstance(result, list) else result if len(access_ids) > 0: access_result = [] @@ -144,11 +239,12 @@ class BaseModelAccessGroups(models.AbstractModel): return access_result return [] + @api.model def _after_search(self, result, *largs, **kwargs): result = super(BaseModelAccessGroups, self)._after_search(result) - if self.env.user.id == SUPERUSER_ID or self._eval_access_skip(): + if self.env.user.id == SUPERUSER_ID or self._eval_access_skip("read"): return result - access_ids = self._get_complete_access_ids() + access_ids = self._get_complete_access_ids("read") if len(access_ids) > 0: access_result = self.env[self._name] if isinstance(result, int): @@ -161,11 +257,12 @@ class BaseModelAccessGroups(models.AbstractModel): return access_result return self.env[self._name] + @api.model def _after_name_search(self, result, *largs, **kwargs): result = super(BaseModelAccessGroups, self)._after_name_search(result) - if self.env.user.id == SUPERUSER_ID or self._eval_access_skip(): + if self.env.user.id == SUPERUSER_ID or self._eval_access_skip("read"): return result - access_ids = self._get_complete_access_ids() + access_ids = self._get_complete_access_ids("read") if len(access_ids) > 0: access_result = [] for tuple in result: @@ -177,11 +274,35 @@ class BaseModelAccessGroups(models.AbstractModel): #---------------------------------------------------------- # Read, View #---------------------------------------------------------- - + + @api.multi def _compute_groups(self, write=True): if write: for record in self: - record.complete_groups = record.groups + record.complete_groups = record.get_groups() else: self.ensure_one() - return {'complete_groups': [(6, 0, self.groups.mapped('id'))]} \ No newline at end of file + return {'complete_groups': [(6, 0, self.get_groups().mapped('id'))]} + + #---------------------------------------------------------- + # Create, Update, Delete + #---------------------------------------------------------- + + @api.multi + def _before_write(self, vals, *largs, **kwargs): + self.check_access_groups('write') + return super(BaseModelAccessGroups, self)._before_write(vals, *largs, **kwargs) + + @api.multi + def _before_unlink(self, *largs, **kwargs): + self.check_access_groups('unlink') + return super(BaseModelAccessGroups, self)._before_unlink(*largs, **kwargs) + + @api.multi + def _check_recomputation(self, vals, olds, *largs, **kwargs): + super(BaseModelAccessGroups, self)._check_recomputation(vals, olds, *largs, **kwargs) + fields = [] + if self.check_group_values(vals): + fields.extend(['complete_groups']) + if fields: + self.trigger_computation(fields) diff --git a/muk_security/models/groups.py b/muk_security/models/groups.py deleted file mode 100644 index 64d8879..0000000 --- a/muk_security/models/groups.py +++ /dev/null @@ -1,178 +0,0 @@ -################################################################################### -# -# Copyright (C) 2017 MuK IT GmbH -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -################################################################################### - -from odoo import models, fields, api - -class AccessGroups(models.Model): - - _name = 'muk_security.groups' - _description = "Access Groups" - _inherit = 'muk_utils.model' - - _parent_store = True - _parent_name = "parent_group" - _parent_order = 'parent_left' - _order = 'parent_left' - - #---------------------------------------------------------- - # Database - #---------------------------------------------------------- - - name = fields.Char( - string="Group Name", - required=True) - - parent_group = fields.Many2one( - comodel_name='muk_security.groups', - string='Parent Group', - ondelete='cascade', - auto_join=True, - index=True) - - child_groups = fields.One2many( - comodel_name='muk_security.groups', - inverse_name='parent_group', - string='Child Groups') - - parent_left = fields.Integer( - string='Left Parent', - index=True) - - parent_right = fields.Integer( - string='Right Parent', - index=True) - - perm_read = fields.Boolean( - string='Read Access') - - perm_create = fields.Boolean( - string='Create Access') - - perm_write = fields.Boolean( - string='Write Access') - - perm_unlink = fields.Boolean( - string='Unlink Access') - - groups = fields.Many2many( - comodel_name='res.groups', - relation='muk_groups_groups_rel', - column1='gid', - column2='rid', - string='Groups') - - explicit_users = fields.Many2many( - comodel_name='res.users', - relation='muk_groups_explicit_users_rel', - column1='gid', - column2='uid', - string='Explicit Users') - - users = fields.Many2many( - comodel_name='res.users', - relation='muk_groups_users_rel', - column1='gid', - column2='uid', - string='Users', - compute='_compute_users', - store=True) - - count_users = fields.Integer( - compute='_compute_count_users', - string="Users") - - _sql_constraints = [ - ('name_uniq', 'unique (name)', 'The name of the group must be unique!') - ] - - #---------------------------------------------------------- - # Functions - #---------------------------------------------------------- - - def trigger_computation_up(self, fields): - parent_group = self.parent_group - if parent_group: - parent_group.trigger_computation(fields) - - def trigger_computation_down(self, fields): - for child in self.child_groups: - child.with_context(is_subnode=True).trigger_computation(fields) - - def trigger_computation(self, fields): - values = {} - if "users" in fields: - values.update(self._compute_users(write=False)) - if values: - self.write(values); - if "users" in fields: - self.trigger_computation_down(fields) - - #---------------------------------------------------------- - # Read, View - #---------------------------------------------------------- - - @api.model - def check_user_values(self, values): - if any(field in values for field in ['parent_group', 'groups', 'explicit_users']): - return True - return False - - @api.multi - def get_users(self): - self.ensure_one() - users = self.env['res.users'] - if self.parent_group: - users |= self.parent_group.users - users |= self.groups.mapped('users') - users |= self.explicit_users - return users - - def _compute_users(self, write=True): - if write: - for record in self: - record.users = record.get_users() - else: - self.ensure_one() - return {'users': [(6, 0, self.get_users().mapped('id'))]} - - @api.depends('users') - def _compute_count_users(self): - for record in self: - record.count_users = len(record.users) - - #---------------------------------------------------------- - # Create, Write, Delete - #---------------------------------------------------------- - - def _after_create(self, vals): - record = super(AccessGroups, self)._after_create(vals) - record._check_recomputation(vals) - return record - - def _after_write_record(self, vals): - vals = super(AccessGroups, self)._after_write_record(vals) - self._check_recomputation(vals) - return vals - - def _check_recomputation(self, values): - fields = [] - if self.check_user_values(values): - fields.extend(['users']) - if fields: - self.trigger_computation(fields) \ No newline at end of file diff --git a/muk_security/models/ir_model_access.py b/muk_security/models/ir_model_access.py new file mode 100644 index 0000000..a8dad25 --- /dev/null +++ b/muk_security/models/ir_model_access.py @@ -0,0 +1,39 @@ +################################################################################### +# +# Copyright (C) 2017 MuK IT GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +################################################################################### + +import logging + +from odoo import api, fields, models +from odoo import tools, _ +from odoo.exceptions import ValidationError + +from odoo.addons.muk_security.tools import helper + +_logger = logging.getLogger(__name__) + +class ExtendedIrModelAccess(models.Model): + + _inherit = 'ir.model.access' + + @api.model + @tools.ormcache_context('self._uid', 'model', 'mode', 'raise_exception', keys=('lang',)) + def check(self, model, mode='read', raise_exception=True): + if isinstance(self.env.uid, helper.NoSecurityUid): + return True + return super(ExtendedIrModelAccess, self).check(model, mode=mode, raise_exception=raise_exception) \ No newline at end of file diff --git a/muk_security/models/ir_rule.py b/muk_security/models/ir_rule.py new file mode 100644 index 0000000..9acc3eb --- /dev/null +++ b/muk_security/models/ir_rule.py @@ -0,0 +1,39 @@ +################################################################################### +# +# Copyright (C) 2017 MuK IT GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +################################################################################### + +import logging + +from odoo import api, fields, models +from odoo import tools, _ +from odoo.exceptions import ValidationError + +from odoo.addons.muk_security.tools import helper + +_logger = logging.getLogger(__name__) + +class ExtendedIrRule(models.Model): + + _inherit = 'ir.rule' + + @api.model + @tools.ormcache('self._uid', 'model_name', 'mode') + def _compute_domain(self, model_name, mode="read"): + if isinstance(self.env.uid, helper.NoSecurityUid): + return None + return super(ExtendedIrRule, self)._compute_domain(model_name, mode=mode) \ No newline at end of file diff --git a/muk_security/models/lock.py b/muk_security/models/lock.py index f53e9ec..3b79f1d 100644 --- a/muk_security/models/lock.py +++ b/muk_security/models/lock.py @@ -29,22 +29,33 @@ class Lock(models.Model): _description = "Lock" name = fields.Char( - compute='_compute_name', - string="Name") + compute='_compute_lock_ref_data', + string="Name", + store=True) locked_by = fields.Char( string="Locked by", required=True) - locked_by_ref = fields.Reference( - selection=[('res.users', 'User')], - string="User Reference") + locked_by_ref = fields.Many2one( + comodel_name='res.users', + string="Locked by") lock_ref = fields.Reference( selection=[], - string="Object Reference", + string="Reference", required=True) + lock_ref_model = fields.Char( + compute='_compute_lock_ref_data', + string="Reference Model", + store=True) + + lock_ref_id = fields.Char( + compute='_compute_lock_ref_data', + string="Reference ID", + store=True) + token = fields.Char( string="Token") @@ -52,6 +63,10 @@ class Lock(models.Model): string="Operation") @api.depends('lock_ref') - def _compute_name(self): + def _compute_lock_ref_data(self): for record in self: - record.name = "Lock for " + str(record.lock_ref.name) \ No newline at end of file + record.update({ + 'name': "Lock for " + str(record.lock_ref.display_name), + 'lock_ref_model': record.lock_ref._name, + 'lock_ref_id': record.lock_ref.id}) + \ No newline at end of file diff --git a/muk_security/models/locking.py b/muk_security/models/locking.py index fe5761b..03cc3a3 100644 --- a/muk_security/models/locking.py +++ b/muk_security/models/locking.py @@ -20,6 +20,7 @@ import os import hashlib import logging +import itertools from odoo import _ from odoo import models, api, fields @@ -40,12 +41,29 @@ class BaseModelLocking(models.AbstractModel): locked = fields.Many2one( comodel_name='muk_security.lock', compute='_compute_lock', - string="Locked",) + string="Locked") + + locked_state = fields.Boolean( + compute='_compute_lock', + string="Locked") + + locked_by = fields.Many2one( + related='locked.locked_by_ref', + comodel_name='res.users', + string="Locked by") editor = fields.Boolean( compute='_compute_editor', string="Editor") + #---------------------------------------------------------- + # Functions + #---------------------------------------------------------- + + @api.model + def generate_operation_key(self): + return hashlib.sha1(os.urandom(128)).hexdigest() + #---------------------------------------------------------- # Locking #---------------------------------------------------------- @@ -67,7 +85,7 @@ class BaseModelLocking(models.AbstractModel): token = hashlib.sha1(os.urandom(128)).hexdigest() lock = self.env['muk_security.lock'].sudo().create({ 'locked_by': user and user.name or "System", - 'locked_by_ref': user and user._name + ',' + str(user.id) or None, + 'locked_by_ref': user and user.id or None, 'lock_ref': record._name + ',' + str(record.id), 'token': token, 'operation': operation}) @@ -88,8 +106,13 @@ class BaseModelLocking(models.AbstractModel): @api.model def unlock_operation(self, operation, *largs, **kwargs): locks = self.env['muk_security.lock'].sudo().search([('operation', '=', operation)]) + references = [ + list((k, list(map(lambda rec: rec.lock_ref_id, v)))) + for k, v in itertools.groupby( + locks.sorted(key=lambda rec: rec.lock_ref_model), + lambda rec: rec.lock_ref_model)] locks.sudo().unlink() - return True + return references @api.multi def is_locked(self, *largs, **kwargs): @@ -113,7 +136,7 @@ class BaseModelLocking(models.AbstractModel): def _checking_lock_user(self, *largs, **kwargs): for record in self: lock = record.is_locked() - if lock and lock.locked_by_ref and not lock.locked_by_ref != self.env.user: + if lock and lock.locked_by_ref and not lock.locked_by_ref.id != self.env.user.id: raise AccessError(_("The record (%s[%s]) is locked by a user, so it can't be changes or deleted.") % (self._name, self.id)) @@ -123,6 +146,8 @@ class BaseModelLocking(models.AbstractModel): for record in self: lock = record.is_locked() if lock and lock.operation and lock.operation != operation: + print(operation, lock.operation) + raise IOError raise AccessError(_("The record (%s[%s]) is locked, so it can't be changes or deleted.") % (self._name, self.id)) @@ -149,14 +174,19 @@ class BaseModelLocking(models.AbstractModel): raise AccessError(_("The record is already locked by another user. (%s)") % lock.locked_by_ref.name) else: raise AccessError(_("The record is already locked.")) + return True #---------------------------------------------------------- # Read, View #---------------------------------------------------------- - + + @api.multi def _compute_lock(self): for record in self: - record.locked = record.is_locked() + locked = record.is_locked() + record.update({ + 'locked_state': bool(locked), + 'locked': locked}) @api.depends('locked') def _compute_editor(self): @@ -169,51 +199,53 @@ class BaseModelLocking(models.AbstractModel): @api.multi def write(self, vals): - operation = hashlib.sha1(os.urandom(128)).hexdigest() + operation = self.generate_operation_key() vals = self._before_write_operation(vals, operation) - result = super(BaseModelLocking, self).write(vals) + process_operation = self.env.context['operation'] if 'operation' in self.env.context else operation + result = super(BaseModelLocking, self.with_context(operation=process_operation)).write(vals) for record in self: record._after_write_record_operation(vals, operation) result = self._after_write_operation(result, vals, operation) return result + @api.multi def _before_write_operation(self, vals, operation, *largs, **kwargs): if 'operation' in self.env.context: self._checking_lock(self.env.context['operation']) - elif operation: - self._checking_lock(operation) else: - self._checking_lock_user() + self._checking_lock(operation) return vals + @api.multi def _after_write_record_operation(self, vals, operation, *largs, **kwargs): return vals - + + @api.multi def _after_write_operation(self, result, vals, operation, *largs, **kwargs): return result @api.multi def unlink(self): - operation = hashlib.sha1(os.urandom(128)).hexdigest() - operation_info = self._before_unlink_operation(operation) - operation_infos = [] + operation = self.generate_operation_key() + self._before_unlink_operation(operation) for record in self: - operation_infos.append(record._before_unlink_record_operation(operation)) - result = super(BaseModelLocking, self).unlink() - self._after_unlink_operation(result, operation_info, operation_infos, operation) + record._before_unlink_record_operation(operation) + process_operation = self.env.context['operation'] if 'operation' in self.env.context else operation + result = super(BaseModelLocking, self.with_context(operation=process_operation)).unlink() + self._after_unlink_operation(result, operation) return result + @api.multi def _before_unlink_operation(self, operation, *largs, **kwargs): if 'operation' in self.env.context: self._checking_lock(self.env.context['operation']) - elif operation: - self._checking_lock(operation) else: - self._checking_lock_user() - return {} + self._checking_lock(operation) + @api.multi def _before_unlink_record_operation(self, operation, *largs, **kwargs): - return {} - - def _after_unlink_operation(self, result, operation_info, operation_infos, operation, *largs, **kwargs): + pass + + @api.multi + def _after_unlink_operation(self, result, operation, *largs, **kwargs): pass \ No newline at end of file diff --git a/muk_security/models/res_groups.py b/muk_security/models/res_groups.py index b7ab876..64d0b1c 100644 --- a/muk_security/models/res_groups.py +++ b/muk_security/models/res_groups.py @@ -29,36 +29,13 @@ class AccessGroups(models.Model): _inherit = "res.groups" - #---------------------------------------------------------- + #---------------------------------------------------------- # Database #---------------------------------------------------------- - groups = fields.Many2many( + security_groups = fields.Many2many( comodel_name='muk_security.groups', - relation='muk_groups_groups_rel', + relation='muk_security_groups_groups_rel', column1='rid', column2='gid', - string='Groups') - - #---------------------------------------------------------- - # Create, Update, Delete - #---------------------------------------------------------- - - @api.multi - def write(self, vals): - result = super(AccessGroups, self).write(vals) - if any(field in vals for field in ['users']): - for record in self: - for group in record.groups: - group.trigger_computation(['users']) - return result - - @api.multi - def unlink(self): - groups = self.env['muk_security.groups'] - for record in self: - groups |= record.groups - result = super(AccessGroups, self).unlink() - for group in groups: - group.trigger_computation(['users']) - return result \ No newline at end of file + string='Groups') \ No newline at end of file diff --git a/muk_security/models/res_users.py b/muk_security/models/res_users.py new file mode 100644 index 0000000..516dddd --- /dev/null +++ b/muk_security/models/res_users.py @@ -0,0 +1,57 @@ +################################################################################### +# +# Copyright (C) 2017 MuK IT GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +################################################################################### + +import logging + +from odoo import api, fields, models +from odoo import tools, _ +from odoo.exceptions import ValidationError + +from odoo.addons.base.res import res_users + +from odoo.addons.muk_security.tools import helper + +_logger = logging.getLogger(__name__) + +class AccessUser(models.Model): + + _inherit = 'res.users' + + #---------------------------------------------------------- + # Database + #---------------------------------------------------------- + + security_groups = fields.Many2many( + comodel_name='muk_security.groups', + relation='muk_security_groups_explicit_users_rel', + column1='uid', + column2='gid', + string='Groups', + readonly=True) + + #---------------------------------------------------------- + # Functions + #---------------------------------------------------------- + + @classmethod + def _browse(cls, ids, env, prefetch=None): + return super(AccessUser, cls)._browse([ + id if not isinstance(id, helper.NoSecurityUid) + else super(helper.NoSecurityUid, id).__int__() + for id in ids], env, prefetch=prefetch) \ No newline at end of file diff --git a/muk_security/models/security_groups.py b/muk_security/models/security_groups.py new file mode 100644 index 0000000..39f3909 --- /dev/null +++ b/muk_security/models/security_groups.py @@ -0,0 +1,43 @@ +################################################################################### +# +# Copyright (C) 2017 MuK IT GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +################################################################################### + +from odoo import models, fields, api + +class AccessGroups(models.Model): + + _name = 'muk_security.groups' + _description = "Access Groups" + _inherit = 'muk_utils.groups' + + #---------------------------------------------------------- + # Database + #---------------------------------------------------------- + + perm_read = fields.Boolean( + string='Read Access') + + perm_create = fields.Boolean( + string='Create Access') + + perm_write = fields.Boolean( + string='Write Access') + + perm_unlink = fields.Boolean( + string='Unlink Access') + \ No newline at end of file diff --git a/muk_security/security/ir.model.access.csv b/muk_security/security/ir.model.access.csv index ba748a9..b550356 100644 --- a/muk_security/security/ir.model.access.csv +++ b/muk_security/security/ir.model.access.csv @@ -1,4 +1,5 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink -access_security_lock_admin,security_lock_admin,model_muk_security_lock,base.group_erp_manager,1,0,0,1 -access_security_groups_admin,security_groups_admin,model_muk_security_groups,base.group_erp_manager,1,1,1,1 \ No newline at end of file +access_security_groups_user,security_groups_user,model_muk_security_groups,base.group_user,1,1,1,1 + +access_security_lock_admin,security_lock_admin,model_muk_security_lock,base.group_erp_manager,1,0,0,1 \ No newline at end of file diff --git a/muk_security/security/security.xml b/muk_security/security/security.xml new file mode 100644 index 0000000..13798f6 --- /dev/null +++ b/muk_security/security/security.xml @@ -0,0 +1,45 @@ + + + + + + + + User can only edit and delete their own groups. + + + + + + + [('create_uid','=',user.id)] + + + + Admins can edit and delete all groups. + + + + + + + [(1 ,'=', 1)] + + + + diff --git a/muk_security/static/description/index.html b/muk_security/static/description/index.html index 05bbbb6..7a536ac 100644 --- a/muk_security/static/description/index.html +++ b/muk_security/static/description/index.html @@ -11,10 +11,9 @@

Overview

-

Technical module to provide some utility - features. The module is mainly used as a dependency by other modules - and to provide a collection of common libraries. It has no direct - visible effect on the system.

+

Technical module to provide some utility and + security features. The module is mainly used as a dependency by + other modules and has no direct visible effect on the system.

diff --git a/muk_security/tests/__init__.py b/muk_security/tests/__init__.py new file mode 100644 index 0000000..cb6eb81 --- /dev/null +++ b/muk_security/tests/__init__.py @@ -0,0 +1,22 @@ +################################################################################### +# +# MuK Document Management System +# +# Copyright (C) 2018 MuK IT GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +################################################################################### + +from . import test_suspend_security \ No newline at end of file diff --git a/muk_security/tests/test_suspend_security.py b/muk_security/tests/test_suspend_security.py new file mode 100644 index 0000000..7a40283 --- /dev/null +++ b/muk_security/tests/test_suspend_security.py @@ -0,0 +1,50 @@ +################################################################################### +# +# Copyright (C) 2017 MuK IT GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +################################################################################### + +import os +import base64 +import logging + +from odoo import exceptions +from odoo.tests import common + +_path = os.path.dirname(os.path.dirname(__file__)) +_logger = logging.getLogger(__name__) + +class SuspendSecurityTestCase(common.TransactionCase): + + at_install = False + post_install = True + + def setUp(self): + super(SuspendSecurityTestCase, self).setUp() + + def tearDown(self): + super(SuspendSecurityTestCase, self).tearDown() + + def test_suspend_security(self): + user_id = self.env.ref('base.user_demo').id + with self.assertRaises(exceptions.AccessError): + self.env.ref('base.user_root').sudo(user_id).name = 'test' + self.env.ref('base.user_root').sudo(user_id).suspend_security().name = 'test' + self.assertEqual(self.env.ref('base.user_root').name, 'test') + self.assertEqual(self.env.ref('base.user_root').write_uid.id, user_id) + + def test_normalize(self): + self.env['res.users'].browse(self.env['res.users'].suspend_security().env.uid) \ No newline at end of file diff --git a/muk_security/tools/__init__.py b/muk_security/tools/__init__.py new file mode 100644 index 0000000..efc0a9b --- /dev/null +++ b/muk_security/tools/__init__.py @@ -0,0 +1,22 @@ +################################################################################### +# +# MuK Document Management System +# +# Copyright (C) 2018 MuK IT GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +################################################################################### + +from . import helper \ No newline at end of file diff --git a/muk_security/tools/helper.py b/muk_security/tools/helper.py new file mode 100644 index 0000000..1a7fe2f --- /dev/null +++ b/muk_security/tools/helper.py @@ -0,0 +1,36 @@ +################################################################################### +# +# MuK Document Management System +# +# Copyright (C) 2018 MuK IT GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +################################################################################### + +class NoSecurityUid(int): + + def __int__(self): + return self + + def __eq__(self, other): + if isinstance(other, int): + return False + return super(NoSecurityUid, self).__int__() == other + + def __iter__(self): + yield super(NoSecurityUid, self).__int__() + + def __hash__(self): + return super(NoSecurityUid, self).__hash__() \ No newline at end of file diff --git a/muk_security/views/groups.xml b/muk_security/views/groups.xml index ce14ba8..672f48e 100644 --- a/muk_security/views/groups.xml +++ b/muk_security/views/groups.xml @@ -65,7 +65,7 @@ - + @@ -75,7 +75,7 @@ - + @@ -83,7 +83,7 @@ - + diff --git a/muk_utils/__manifest__.py b/muk_utils/__manifest__.py index ec53b4b..8e8630b 100644 --- a/muk_utils/__manifest__.py +++ b/muk_utils/__manifest__.py @@ -20,7 +20,7 @@ { "name": "MuK Utils", "summary": """Utility Features""", - "version": '11.0.1.0.6', + "version": '11.0.1.0.8', "category": 'Extra Tools', "license": "AGPL-3", "website": "https://www.mukit.at", @@ -32,6 +32,9 @@ "depends": [ "base", ], + "data": [ + "data/ir_cron.xml", + ], "qweb": [ "static/src/xml/*.xml", ], diff --git a/muk_utils/data/ir_cron.xml b/muk_utils/data/ir_cron.xml new file mode 100644 index 0000000..f1b8983 --- /dev/null +++ b/muk_utils/data/ir_cron.xml @@ -0,0 +1,41 @@ + + + + + + + + Cron job to update the Groups + + + + 1 + days + -1 + + code + +model_names = model.pool.descendants(['muk_utils.groups'], '_inherit', '_inherits') +for model_name in model_names: + group = model.env[model_name].sudo() + if not group._abstract: + group.update_groups() + + + + diff --git a/muk_utils/models/__init__.py b/muk_utils/models/__init__.py index 9b18ef8..7d46e70 100644 --- a/muk_utils/models/__init__.py +++ b/muk_utils/models/__init__.py @@ -18,3 +18,6 @@ ################################################################################### from . import model +from . import groups +from . import res_groups +from . import res_users diff --git a/muk_utils/models/groups.py b/muk_utils/models/groups.py new file mode 100644 index 0000000..4f96b2f --- /dev/null +++ b/muk_utils/models/groups.py @@ -0,0 +1,180 @@ +################################################################################### +# +# Copyright (C) 2017 MuK IT GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +################################################################################### + +from odoo import models, fields, api + +class Groups(models.AbstractModel): + + _name = 'muk_utils.groups' + _inherit = 'muk_utils.model' + + _parent_store = True + _parent_name = "parent_group" + _parent_order = 'parent_left' + _order = 'parent_left' + + #---------------------------------------------------------- + # Database + #---------------------------------------------------------- + + name = fields.Char( + string="Group Name", + required=True, + translate=True) + + parent_left = fields.Integer( + string='Left Parent', + index=True) + + parent_right = fields.Integer( + string='Right Parent', + index=True) + + count_users = fields.Integer( + compute='_compute_count_users', + string="Users") + + @api.model + def _add_magic_fields(self): + super(Groups, self)._add_magic_fields() + def add(name, field): + if name not in self._fields: + self._add_field(name, field) + base, model = self._name.split(".") + add('parent_group', fields.Many2one( + _module=base, + comodel_name=self._name, + string='Parent Group', + ondelete='cascade', + auto_join=True, + index=True, + automatic=True)) + add('child_groups', fields.One2many( + _module=base, + comodel_name=self._name, + inverse_name='parent_group', + string='Child Groups', + automatic=True)) + add('groups', fields.Many2many( + _module=base, + comodel_name='res.groups', + relation='%s_groups_rel' % (self._table), + column1='gid', + column2='rid', + string='Groups', + automatic=True)) + add('explicit_users', fields.Many2many( + _module=base, + comodel_name='res.users', + relation='%s_explicit_users_rel' % (self._table), + column1='gid', + column2='uid', + string='Explicit Users', + automatic=True)) + add('users', fields.Many2many( + _module=base, + comodel_name='res.users', + relation='%s_users_rel' % (self._table), + column1='gid', + column2='uid', + string='Users', + compute='_compute_users', + store=True, + automatic=True)) + + _sql_constraints = [ + ('name_uniq', 'unique (name)', 'The name of the group must be unique!') + ] + + #---------------------------------------------------------- + # Functions + #---------------------------------------------------------- + + @api.multi + def trigger_computation_up(self, fields, *largs, **kwargs): + parent_groups = self.mapped('parent_group') + if parent_groups.exists(): + parent_groups.with_context(is_parent=True).trigger_computation(fields) + + @api.multi + def trigger_computation_down(self, fields, *largs, **kwargs): + child_groups = self.mapped('child_groups') + if child_groups.exists(): + child_groups.with_context(is_child=True).trigger_computation(fields) + + @api.multi + def trigger_computation(self, fields, *largs, **kwargs): + super(Groups, self).trigger_computation(fields, *largs, **kwargs) + if "users" in fields: + self.suspend_security()._compute_users() + self.suspend_security().trigger_computation_down(fields) + + @api.model + def check_user_values(self, values): + if any(field in values for field in [ + 'parent_group', 'groups', 'explicit_users']): + return True + return False + + @api.multi + @api.returns('res.users') + def get_users(self): + self.ensure_one() + users = self.env['res.users'] + if self.parent_group: + users |= self.parent_group.users + users |= self.groups.mapped('users') + users |= self.explicit_users + return users + + #---------------------------------------------------------- + # Read, View + #---------------------------------------------------------- + + @api.multi + def _compute_users(self): + for record in self: + record.users = record.get_users() + + @api.depends('users') + def _compute_count_users(self): + for record in self: + record.count_users = len(record.users) + + #---------------------------------------------------------- + # Create, Write, Delete + #---------------------------------------------------------- + + @api.multi + def _check_recomputation(self, vals, olds, *largs, **kwargs): + super(Groups, self)._check_recomputation(vals, olds, *largs, **kwargs) + fields = [] + if self.check_user_values(vals): + fields.extend(['users']) + if fields: + self.trigger_computation(fields) + + #---------------------------------------------------------- + # Cron Job Functions + #---------------------------------------------------------- + + @api.model + def update_groups(self, *args, **kwargs): + self.search([]).trigger_computation(['users']) + \ No newline at end of file diff --git a/muk_utils/models/model.py b/muk_utils/models/model.py index acbce6a..32036ec 100644 --- a/muk_utils/models/model.py +++ b/muk_utils/models/model.py @@ -32,6 +32,14 @@ class BaseModelExtension(models.AbstractModel): # Function #---------------------------------------------------------- + @api.multi + def notify_change(self, values, *largs, **kwargs): + pass + + @api.multi + def trigger_computation(self, fields, *largs, **kwargs): + pass + @api.multi def check_existence(self): records = self.exists() @@ -44,15 +52,18 @@ class BaseModelExtension(models.AbstractModel): # Read #---------------------------------------------------------- + @api.model def browse(self, arg=None, prefetch=None): arg = self._before_browse(arg) result = super(BaseModelExtension, self).browse(arg, prefetch) result = self._after_browse(result) return result + @api.model def _before_browse(self, arg, *largs, **kwargs): return arg + @api.model def _after_browse(self, result, *largs, **kwargs): return result @@ -61,16 +72,22 @@ class BaseModelExtension(models.AbstractModel): fields = self._before_read(fields) result = super(BaseModelExtension, self).read(fields, load) for index, record in enumerate(self.exists()): - result[index] = record._after_read_record(result[index]) + try: + result[index] = record._after_read_record(result[index]) + except IndexError: + _logger.exception("Something went wrong!") result = self._after_read(result) return result + @api.multi def _before_read(self, fields, *largs, **kwargs): return fields + @api.multi def _after_read_record(self, values, *largs, **kwargs): return values - + + @api.multi def _after_read(self, result, *largs, **kwargs): return result @@ -84,9 +101,11 @@ class BaseModelExtension(models.AbstractModel): result = self._after_search(result) return result + @api.model def _before_search(self, args, offset, limit, order, count, *largs, **kwargs): return args, offset, limit, order, count + @api.model def _after_search(self, result, *largs, **kwargs): return result @@ -97,9 +116,11 @@ class BaseModelExtension(models.AbstractModel): result = self._after_name_search(result) return result + @api.model def _before_name_search(self, name, args, operator, limit, *largs, **kwargs): return name, args, operator, limit + @api.model def _after_name_search(self, result, *largs, **kwargs): return result @@ -110,9 +131,11 @@ class BaseModelExtension(models.AbstractModel): result = self._after_read_group(result) return result + @api.model def _before_read_group(self, domain, fields, groupby, offset, limit, orderby, lazy, *largs, **kwargs): return domain, fields, groupby, offset, limit, orderby, lazy + @api.model def _after_read_group(self, result, *largs, **kwargs): return result @@ -127,28 +150,39 @@ class BaseModelExtension(models.AbstractModel): result = result._after_create(vals) return result + @api.model def _before_create(self, vals, *largs, **kwargs): return vals + @api.model def _after_create(self, vals, *largs, **kwargs): + self._check_recomputation(vals, []) return self @api.multi def write(self, vals): + olds = [] vals = self._before_write(vals) + if 'track_old_values' in self.env.context: + olds = [{key: record[key] for key in vals} for record in self] result = super(BaseModelExtension, self).write(vals) for record in self: record._after_write_record(vals) - result = self._after_write(result, vals) + result = self._after_write(result, vals, olds) return result + @api.multi def _before_write(self, vals, *largs, **kwargs): return vals + @api.multi def _after_write_record(self, vals, *largs, **kwargs): return vals - def _after_write(self, result, vals, *largs, **kwargs): + @api.multi + def _after_write(self, result, vals, olds, *largs, **kwargs): + self._check_recomputation(vals, olds) + self._check_notification(vals) return result @api.multi @@ -161,11 +195,28 @@ class BaseModelExtension(models.AbstractModel): self._after_unlink(result, info, infos) return result + @api.multi def _before_unlink(self, *largs, **kwargs): return {} + @api.multi def _before_unlink_record(self, *largs, **kwargs): return {} - + + @api.multi def _after_unlink(self, result, info, infos, *largs, **kwargs): + pass + + #---------------------------------------------------------- + # Helper + #---------------------------------------------------------- + + @api.multi + def _check_recomputation(self, vals, olds, *largs, **kwargs): + # self.trigger_computation(fields) + pass + + @api.multi + def _check_notification(self, vals, *largs, **kwargs): + # self.notify_change(change) pass \ No newline at end of file diff --git a/muk_utils/models/res_groups.py b/muk_utils/models/res_groups.py new file mode 100644 index 0000000..32298ac --- /dev/null +++ b/muk_utils/models/res_groups.py @@ -0,0 +1,68 @@ +################################################################################### +# +# Copyright (C) 2017 MuK IT GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +################################################################################### + +import logging + +from collections import defaultdict + +from odoo import api, fields, models +from odoo import tools, _ +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + +class ResGroups(models.Model): + + _inherit = "res.groups" + + #---------------------------------------------------------- + # Create, Update, Delete + #---------------------------------------------------------- + + @api.multi + def write(self, vals): + model_recs = defaultdict(set) + model_names = self.pool.descendants(['muk_utils.groups'], '_inherit', '_inherits') + if any(field in vals for field in ['users']): + for model_name in model_names: + model = self.env[model_name].sudo() + if not model._abstract: + model_recs[model_name] = model.search([['groups', 'in', self.mapped('id')]]) + result = super(ResGroups, self).write(vals) + if any(field in vals for field in ['users']): + for model_name in model_names: + model = self.env[model_name].sudo() + if not model._abstract: + model_recs[model_name] = model_recs[model_name] | model.search([['groups', 'in', self.mapped('id')]]) + for tuple in model_recs.items(): + tuple[1].trigger_computation(['users']) + return result + + @api.multi + def unlink(self): + model_recs = defaultdict(set) + model_names = self.pool.descendants(['muk_utils.groups'], '_inherit', '_inherits') + for model_name in model_names: + model = self.env[model_name].sudo() + if not model._abstract: + model_recs[model_name] = model.search([['groups', 'in', self.mapped('id')]]) + result = super(ResGroups, self).unlink(vals) + for tuple in model_recs.items(): + tuple[1].trigger_computation(['users']) + return result \ No newline at end of file diff --git a/muk_utils/models/res_users.py b/muk_utils/models/res_users.py new file mode 100644 index 0000000..83bb693 --- /dev/null +++ b/muk_utils/models/res_users.py @@ -0,0 +1,85 @@ +################################################################################### +# +# Copyright (C) 2017 MuK IT GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +################################################################################### + +import logging + +from collections import defaultdict + +from odoo import api, fields, models +from odoo import tools, _ +from odoo.exceptions import ValidationError + +from odoo.addons.base.res import res_users + +from odoo.addons.muk_security.tools import helper + +_logger = logging.getLogger(__name__) + +class ResUser(models.Model): + + _inherit = 'res.users' + + #---------------------------------------------------------- + # Create, Update, Delete + #---------------------------------------------------------- + + @api.model + def create(self, values): + result = super(ResUser, self).create(values) + model_recs = defaultdict(set) + model_names = self.pool.descendants(['muk_utils.groups'], '_inherit', '_inherits') + for model_name in model_names: + model = self.env[model_name].sudo() + if not model._abstract: + model_recs[model_name] = model.search([['groups', 'in', self.mapped('groups_id.id')]]) + for tuple in model_recs.items(): + tuple[1].trigger_computation(['users']) + return result + + @api.multi + def write(self, vals): + group_ids = self.mapped('groups_id.id') + result = super(ResUser, self).write(vals) + group_ids += [vals[k] for k in vals if res_users.is_selection_groups(k) and vals[k]] + group_ids += [vals[k] for k in vals if res_users.is_boolean_group(k) and vals[k]] + if any(field in vals for field in ['groups_id']): + group_ids += self.mapped('groups_id.id') + if group_ids: + model_recs = defaultdict(set) + model_names = self.pool.descendants(['muk_utils.groups'], '_inherit', '_inherits') + for model_name in model_names: + model = self.env[model_name].sudo() + if not model._abstract: + model_recs[model_name] = model.search([['groups', 'in', group_ids]]) + for tuple in model_recs.items(): + tuple[1].trigger_computation(['users']) + return result + + @api.multi + def unlink(self): + model_recs = defaultdict(set) + model_names = self.pool.descendants(['muk_utils.groups'], '_inherit', '_inherits') + for model_name in model_names: + model = self.env[model_name].sudo() + if not model._abstract: + model_recs[model_name] = model.search([['groups', 'in', self.mapped('groups_id.id')]]) + result = super(ResUser, self).unlink() + for tuple in model_recs.items(): + tuple[1].trigger_computation(['users']) + return result \ No newline at end of file