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