[MERGE] feature branch with improvements for sharing resources

Contains a few fixes for ir.rule management, tools for combining
domains, related tests, and a few minor fixes.

bzr revid: odo@openerp.com-20110624133529-z4y01gy2fm0x0oj1
This commit is contained in:
Olivier Dony
2011-06-24 15:35:29 +02:00
16 changed files with 245 additions and 83 deletions
+14 -9
View File
@@ -1667,24 +1667,29 @@
<field name="perm_create"/>
<field name="perm_unlink"/>
</group>
<separator colspan="4" string="Domain Setup"/>
<separator colspan="4" string="Rule definition (domain filter)"/>
<group colspan="4">
<field name="domain_force" colspan="4"/>
<field name="domain_force" colspan="4" nolabel="1"/>
</group>
<separator colspan="4" string="Groups (no group = global)"/>
<group colspan="4" expand="1">
<field name="global"/>
<field name="groups" nolabel="1" colspan="4"/>
</group>
<separator colspan="4" string="Combination of rules"/>
<label align="0.0" colspan="4"
<label colspan="4" string=""/>
<separator colspan="4" string="Interaction between rules"/>
<label colspan="4"
string="Global rules (non group-specific) are restrictions, and cannot be bypassed. Group-local rules grant additional permissions, but are constrained within the bounds of global ones. The first group rules restrict further than global rules, but any additional group rule will add more permissions"/>
<label colspan="4"
string="Detailed algorithm:"/>
<label colspan="4"
string="1. Global rules are combined together with a logical AND operator, and with the result of the following steps"/>
<label align="0.0" colspan="4"
string="2. Group-specific rules are combined together with a logical AND operator"/>
<label align="0.0" colspan="4"
<label colspan="4"
string="2. Group-specific rules are combined together with a logical OR operator"/>
<label colspan="4"
string="3. If user belongs to several groups, the results from step 2 are combined with logical OR operator"/>
<label align="0.0" colspan="4"
string="Example: GLOBAL_RULE_1 AND GLOBAL_RULE_2 AND ( (GROUP_A_RULE_1 AND GROUP_A_RULE_2) OR (GROUP_B_RULE_1 AND GROUP_B_RULE_2) )"/>
<label colspan="4"
string="Example: GLOBAL_RULE_1 AND GLOBAL_RULE_2 AND ( (GROUP_A_RULE_1 OR GROUP_A_RULE_2) OR (GROUP_B_RULE_1 OR GROUP_B_RULE_2) )"/>
</form>
</field>
</record>
+18 -11
View File
@@ -21,7 +21,6 @@
import logging
import re
import time
from operator import itemgetter
from osv import fields,osv
import netsvc
@@ -450,6 +449,23 @@ class ir_model_access(osv.osv):
# pass no groups -> no access
return False
def group_names_with_access(self, cr, model_name, access_mode):
"""Returns the names of visible groups which have been granted ``access_mode`` on
the model ``model_name``.
:rtype: list
"""
assert access_mode in ['read','write','create','unlink'], 'Invalid access mode: %s' % access_mode
cr.execute('''SELECT
g.name
FROM
ir_model_access a
JOIN ir_model m ON (a.model_id=m.id)
JOIN res_groups g ON (a.group_id=g.id)
WHERE
m.model=%s AND
a.perm_''' + access_mode, (model_name,))
return [x[0] for x in cr.fetchall()]
def check(self, cr, uid, model, mode='read', raise_exception=True, context=None):
if uid==1:
# User root have all accesses
@@ -493,16 +509,7 @@ class ir_model_access(osv.osv):
r = cr.fetchone()[0]
if not r and raise_exception:
cr.execute('''select
g.name
from
ir_model_access a
left join ir_model m on (a.model_id=m.id)
left join res_groups g on (a.group_id=g.id)
where
m.model=%s and
a.group_id is not null and perm_''' + mode, (model_name, ))
groups = ', '.join(map(lambda x: x[0], cr.fetchall())) or '/'
groups = ', '.join(self.group_names_with_access(cr, model_name, mode)) or '/'
msgs = {
'read': _("You can not read this document (%s) ! Be sure your user belongs to one of these groups: %s."),
'write': _("You can not write in this document (%s) ! Be sure your user belongs to one of these groups: %s."),
+41 -36
View File
@@ -19,25 +19,41 @@
#
##############################################################################
from osv import fields,osv
from osv import fields, osv, expression
import time
from operator import itemgetter
from functools import partial
import tools
from tools.safe_eval import safe_eval as eval
from tools.misc import unquote as unquote
SUPERUSER_UID = 1
class ir_rule(osv.osv):
_name = 'ir.rule'
_order = 'name'
_MODES = ['read', 'write', 'create', 'unlink']
def _eval_context_for_combinations(self):
"""Returns a dictionary to use as evaluation context for
ir.rule domains, when the goal is to obtain python lists
that are easier to parse and combine, but not to
actually execute them."""
return {'user': unquote('user'),
'time': unquote('time')}
def _eval_context(self, cr, uid):
"""Returns a dictionary to use as evaluation context for
ir.rule domains."""
return {'user': self.pool.get('res.users').browse(cr, 1, uid),
'time':time}
def _domain_force_get(self, cr, uid, ids, field_name, arg, context=None):
res = {}
eval_context = self._eval_context(cr, uid)
for rule in self.browse(cr, uid, ids, context):
if rule.domain_force:
eval_user_data = {'user': self.pool.get('res.users').browse(cr, 1, uid),
'time':time}
res[rule.id] = eval(rule.domain_force, eval_user_data)
res[rule.id] = expression.normalize(eval(rule.domain_force, eval_context))
else:
res[rule.id] = []
return res
@@ -83,25 +99,12 @@ class ir_rule(osv.osv):
(_check_model_obj, 'Rules are not supported for osv_memory objects !', ['model_id'])
]
def domain_create(self, cr, uid, rule_ids):
count = 0
dom = []
for rule in self.browse(cr, uid, rule_ids):
if rule.domain:
dom += rule.domain
count += 1
if count:
return ['&'] * (count-1) + dom
return []
@tools.cache()
def _compute_domain(self, cr, uid, model_name, mode="read"):
if mode not in self._MODES:
raise ValueError('Invalid mode: %r' % (mode,))
group_rule = {}
global_rules = []
if uid == 1:
if uid == SUPERUSER_UID:
return None
cr.execute("""SELECT r.id
FROM ir_rule r
@@ -111,26 +114,28 @@ class ir_rule(osv.osv):
AND (r.id IN (SELECT rule_group_id FROM rule_group_rel g_rel
JOIN res_groups_users_rel u_rel ON (g_rel.group_id = u_rel.gid)
WHERE u_rel.uid = %s) OR r.global)""", (model_name, uid))
ids = map(lambda x: x[0], cr.fetchall())
if ids:
for rule in self.browse(cr, uid, ids):
rule_ids = [x[0] for x in cr.fetchall()]
if rule_ids:
# browse user as super-admin root to avoid access errors!
user = self.pool.get('res.users').browse(cr, SUPERUSER_UID, uid)
global_domains = [] # list of domains
group_domains = {} # map: group -> list of domains
for rule in self.browse(cr, SUPERUSER_UID, rule_ids):
# read 'domain' as UID to have the correct eval context for the rule.
rule_domain = self.read(cr, uid, rule.id, ['domain'])['domain']
dom = expression.normalize(rule_domain)
for group in rule.groups:
group_rule.setdefault(group.id, []).append(rule.id)
if group in user.groups_id:
group_domains.setdefault(group, []).append(dom)
if not rule.groups:
global_rules.append(rule.id)
global_domain = self.domain_create(cr, uid, global_rules)
count = 0
group_domains = []
for value in group_rule.values():
group_domain = self.domain_create(cr, uid, value)
if group_domain:
group_domains += group_domain
count += 1
if count and global_domain:
return ['&'] + global_domain + ['|'] * (count-1) + group_domains
if count:
return ['|'] * (count-1) + group_domains
return global_domain
global_domains.append(dom)
# combine global domains and group domains
if group_domains:
group_domain = expression.OR(map(expression.OR, group_domains.values()))
else:
group_domain = []
domain = expression.AND(global_domains + [group_domain])
return domain
return []
def clear_cache(self, cr, uid):
-1
View File
@@ -24,7 +24,6 @@ from lxml import etree
from tools import graph
from tools.safe_eval import safe_eval as eval
import tools
import netsvc
import os
import logging
+2 -2
View File
@@ -581,9 +581,9 @@ class groups2(osv.osv): ##FIXME: Is there a reason to inherit this object ?
if group_users:
user_names = [user.name for user in self.pool.get('res.users').browse(cr, uid, group_users, context=context)]
user_names = list(set(user_names))
if len(user_names) >= 5:
user_names = user_names[:5]
user_names += '...'
user_names = user_names[:5] + ['...']
raise osv.except_osv(_('Warning !'),
_('Group(s) cannot be deleted, because some user(s) still belong to them: %s !') % \
', '.join(user_names))
@@ -61,7 +61,7 @@
"access_res_partner_bank_group_user","res_partner_bank group_user","model_res_partner_bank","group_user",1,0,0,0
"access_res_partner_bank_group_partner_manager","res_partner_bank group_partner_manager","model_res_partner_bank","group_partner_manager",1,1,1,1
"access_res_partner_bank_type_group_partner_manager","res_partner_bank_type group_partner_manager","model_res_partner_bank_type","group_partner_manager",1,1,1,1
"access_res_partner_bank_type_group_user","res_partner_bank_type group_user","model_res_partner_bank_type","group_user",1,0,0,0
"access_res_partner_bank_type_group_user","res_partner_bank_type group_user","model_res_partner_bank_type",,1,0,0,0
"access_res_partner_bank_type_field_group_partner_manager","res_partner_bank_type_field group_partner_manager","model_res_partner_bank_type_field","group_partner_manager",1,1,1,1
"access_res_partner_bank_type_field_group_user","res_partner_bank_type_field group_user","model_res_partner_bank_type_field",,1,0,0,0
"access_res_partner_canal_group_user","res_partner_canal group_user","model_res_partner_canal","group_user",1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
61 access_res_partner_bank_group_user res_partner_bank group_user model_res_partner_bank group_user 1 0 0 0
62 access_res_partner_bank_group_partner_manager res_partner_bank group_partner_manager model_res_partner_bank group_partner_manager 1 1 1 1
63 access_res_partner_bank_type_group_partner_manager res_partner_bank_type group_partner_manager model_res_partner_bank_type group_partner_manager 1 1 1 1
64 access_res_partner_bank_type_group_user res_partner_bank_type group_user model_res_partner_bank_type group_user 1 0 0 0
65 access_res_partner_bank_type_field_group_partner_manager res_partner_bank_type_field group_partner_manager model_res_partner_bank_type_field group_partner_manager 1 1 1 1
66 access_res_partner_bank_type_field_group_user res_partner_bank_type_field group_user model_res_partner_bank_type_field 1 0 0 0
67 access_res_partner_canal_group_user res_partner_canal group_user model_res_partner_canal group_user 1 0 0 0
+43 -1
View File
@@ -132,6 +132,48 @@
!python {model: res.partner }: |
ids = self.search(cr, ref('base.user_demo'), [])
assert ids, "Demo user should see some partner."
-
Modify the ir_rule for employee to have a rule that fordids
seeing any record.
We use a domain with implicit AND operator for later tests
on normalization.
-
!record {model: ir.rule, id: test_rule2}:
domain_force: "[('id','=',False),('name','=',False)]"
-
Check that demo user still sees partners, because group-rules are OR'ed.
-
!python {model: res.partner }: |
ids = self.search(cr, ref('base.user_demo'), [])
assert ids, "Demo user should see some partner."
-
Create a new group with demo user in it, and a complex rule.
Demo should still see partners.
-
!record {model: res.groups, id: test_group}:
name: Test Group
users:
- base.user_demo
-
Add the rule to the new group, with a domain containing an implicit AND operator,
which is more tricky because it will have to be normalized before combining it.
-
!record {model: ir.rule, id: test_rule3}:
model_id: base.model_res_partner
domain_force: "[('name', '!=', False),('id', '!=', False)]"
name: test_rule4
groups:
- test_group
perm_unlink: 1
perm_write: 1
perm_read: 1
perm_create: 1
-
Read the partners again as demo user, which should give results.
-
!python {model: res.partner }: |
ids = self.search(cr, ref('base.user_demo'), [])
assert ids, "Demo user should see partners even with the combined rules."
-
Delete global domains (to combine only group domains).
-
@@ -140,7 +182,7 @@
assert ids, "Demo user should see some partner."
self.unlink(cr, uid, ids)
-
Read as demo user the partners (three 1=1 domains, no global domain).
Read as demo user the partners (several group domains, no global domain).
-
!python {model: res.partner }: |
ids = self.search(cr, ref('base.user_demo'), [])
@@ -177,4 +177,16 @@
res_ids = self.search(cr, uid, [('company_id.partner_id', 'not in', [])])
res_ids.sort()
assert res_ids == all_ids, "Searching against empty set failed, returns %r" % res_ids
-
Verify that normalize_domain() works.
-
!python {model: res.partner}: |
from osv import expression
norm_domain = domain = ['&',(1,'=',1),('a','=','b')]
assert norm_domain == expression.normalize(domain), "Normalized domains should be left untouched"
domain = [('x','in',['y','z']),('a.v','=','e'),'|','|',('a','=','b'),'!',('c','>','d'),('e','!=','f'),('g','=','h')]
norm_domain = ['&','&','&'] + domain
assert norm_domain == expression.normalize(domain), "Non-normalized domains should be properly normalized"
+11
View File
@@ -188,6 +188,16 @@
</rng:element>
</rng:define>
<rng:define name="url">
<rng:element name="url">
<rng:attribute name="id" />
<rng:attribute name="name" />
<rng:attribute name="url" />
<rng:optional><rng:attribute name="target" /></rng:optional>
<rng:empty />
</rng:element>
</rng:define>
<rng:define name="act_window">
<rng:element name="act_window">
<rng:attribute name="id" />
@@ -222,6 +232,7 @@
<rng:ref name="delete" />
<rng:ref name="wizard" />
<rng:ref name="act_window" />
<rng:ref name="url" />
<rng:ref name="assert" />
<rng:ref name="report" />
<rng:ref name="workflow" />
-2
View File
@@ -35,7 +35,6 @@ import socket
import sys
import threading
import time
import warnings
import types
from pprint import pformat
@@ -186,7 +185,6 @@ class ColoredFormatter(DBFormatter):
return DBFormatter.format(self, record)
def init_logger():
import os
from tools.translate import resetlocale
resetlocale()
+76 -9
View File
@@ -23,6 +23,72 @@
from openerp.tools import flatten, reverse_enumerate
import fields
NOT_OPERATOR = '!'
OR_OPERATOR = '|'
AND_OPERATOR = '&'
TRUE_DOMAIN = [(1,'=',1)]
FALSE_DOMAIN = [(0,'=',1)]
def normalize(domain):
"""Returns a normalized version of ``domain_expr``, where all implicit '&' operators
have been made explicit. One property of normalized domain expressions is that they
can be easily combined together as if they were single domain components.
"""
assert isinstance(domain, (list, tuple)), "Domains to normalize must have a 'domain' form: a list or tuple of domain components"
if not domain:
return TRUE_DOMAIN
result = []
expected = 1 # expected number of expressions
op_arity = {NOT_OPERATOR: 1, AND_OPERATOR: 2, OR_OPERATOR: 2}
for token in domain:
if expected == 0: # more than expected, like in [A, B]
result[0:0] = ['&'] # put an extra '&' in front
expected = 1
result.append(token)
if isinstance(token, (list,tuple)): # domain term
expected -= 1
else:
expected += op_arity.get(token, 0) - 1
assert expected == 0
return result
def combine(operator, unit, zero, domains):
"""Returns a new domain expression where all domain components from ``domains``
have been added together using the binary operator ``operator``.
:param unit: the identity element of the domains "set" with regard to the operation
performed by ``operator``, i.e the domain component ``i`` which, when
combined with any domain ``x`` via ``operator``, yields ``x``.
E.g. [(1,'=',1)] is the typical unit for AND_OPERATOR: adding it
to any domain component gives the same domain.
:param zero: the absorbing element of the domains "set" with regard to the operation
performed by ``operator``, i.e the domain component ``z`` which, when
combined with any domain ``x`` via ``operator``, yields ``z``.
E.g. [(1,'=',1)] is the typical zero for OR_OPERATOR: as soon as
you see it in a domain component the resulting domain is the zero.
"""
result = []
count = 0
for domain in domains:
if domain == unit:
continue
if domain == zero:
return zero
if domain:
result += domain
count += 1
result = [operator] * (count - 1) + result
return result
def AND(domains):
""" AND([D1,D2,...]) returns a domain representing D1 and D2 and ... """
return combine(AND_OPERATOR, TRUE_DOMAIN, FALSE_DOMAIN, domains)
def OR(domains):
""" OR([D1,D2,...]) returns a domain representing D1 or D2 or ... """
return combine(OR_OPERATOR, FALSE_DOMAIN, TRUE_DOMAIN, domains)
class expression(object):
"""
@@ -32,10 +98,12 @@ class expression(object):
For more info: http://christophe-simonis-at-tiny.blogspot.com/2008/08/new-new-domain-notation.html
"""
def _is_operator(self, element):
return isinstance(element, (str, unicode)) and element in ['&', '|', '!']
@classmethod
def _is_operator(cls, element):
return isinstance(element, (str, unicode)) and element in [AND_OPERATOR, OR_OPERATOR, NOT_OPERATOR]
def _is_leaf(self, element, internal=False):
@classmethod
def _is_leaf(cls, element, internal=False):
OPS = ('=', '!=', '<>', '<=', '<', '>', '>=', '=?', '=like', '=ilike', 'like', 'not like', 'ilike', 'not ilike', 'in', 'not in', 'child_of')
INTERNAL_OPS = OPS + ('inselect',)
return (isinstance(element, tuple) or isinstance(element, list)) \
@@ -92,8 +160,8 @@ class expression(object):
doms = []
for o in table.browse(cr, uid, ids, context=context):
if doms:
doms.insert(0, '|')
doms += ['&', ('parent_left', '<', o.parent_right), ('parent_left', '>=', o.parent_left)]
doms.insert(0, OR_OPERATOR)
doms += [AND_OPERATOR, ('parent_left', '<', o.parent_right), ('parent_left', '>=', o.parent_left)]
if prefix:
return [(left, 'in', table.search(cr, uid, doms, context=context))]
return doms
@@ -187,7 +255,7 @@ class expression(object):
else:
# we assume that the expression is valid
# we create a dummy leaf for forcing the parsing of the resulting expression
self.__exp[i] = '&'
self.__exp[i] = AND_OPERATOR
self.__exp.insert(i + 1, self.__DUMMY_LEAF)
for j, se in enumerate(subexp):
self.__exp.insert(i + 2 + j, se)
@@ -389,7 +457,6 @@ class expression(object):
]
self.__exp[i] = ('id', 'inselect', (query1, query2))
return self
def __leaf_to_sql(self, leaf, table):
@@ -493,10 +560,10 @@ class expression(object):
params.insert(0, p)
stack.append(q)
else:
if e == '!':
if e == NOT_OPERATOR:
stack.append('(NOT (%s))' % (stack.pop(),))
else:
ops = {'&': ' AND ', '|': ' OR '}
ops = {AND_OPERATOR: ' AND ', OR_OPERATOR: ' OR '}
q1 = stack.pop()
q2 = stack.pop()
stack.append('(%s %s %s)' % (q1, ops[e], q2,))
+12 -3
View File
@@ -936,6 +936,9 @@ class related(function):
if self._type=='many2one':
ids = filter(None, res.values())
if ids:
# name_get as root, as seeing the name of a related
# object depends on access right of source document,
# not target, so user may not have access.
ng = dict(obj.pool.get(self._obj).name_get(cr, 1, ids, context=context))
for r in res:
if res[r]:
@@ -1079,7 +1082,10 @@ class property(function):
value = properties.get_by_record(cr, uid, prop, context=context)
res[prop.res_id.id][prop.fields_id.name] = value or False
if value and (prop.type == 'many2one'):
record_exists = obj.pool.get(value._name).exists(cr, uid, value.id)
# check existence as root, as seeing the name of a related
# object depends on access right of source document,
# not target, so user may not have access.
record_exists = obj.pool.get(value._name).exists(cr, 1, value.id)
if record_exists:
replaces.setdefault(value._name, {})
replaces[value._name][value.id] = True
@@ -1087,8 +1093,11 @@ class property(function):
res[prop.res_id.id][prop.fields_id.name] = False
for rep in replaces:
nids = obj.pool.get(rep).search(cr, uid, [('id','in',replaces[rep].keys())], context=context)
replaces[rep] = dict(obj.pool.get(rep).name_get(cr, uid, nids, context=context))
# search+name_get as root, as seeing the name of a related
# object depends on access right of source document,
# not target, so user may not have access.
nids = obj.pool.get(rep).search(cr, 1, [('id','in',replaces[rep].keys())], context=context)
replaces[rep] = dict(obj.pool.get(rep).name_get(cr, 1, nids, context=context))
for prop in prop_name:
for id in ids:
+6 -3
View File
@@ -972,7 +972,7 @@ class orm_template(object):
if field[len(prefix)]=='id':
try:
data_res_id = _get_id(model_name, line[i], current_module, 'id')
except ValueError, e:
except ValueError:
pass
xml_id = line[i]
continue
@@ -2069,8 +2069,9 @@ class orm_template(object):
class orm_memory(orm_template):
_protected = ['read', 'write', 'create', 'default_get', 'perm_read', 'unlink', 'fields_get', 'fields_view_get', 'search', 'name_get', 'distinct_field_get', 'name_search', 'copy', 'import_data', 'search_count', 'exists']
_max_count = config.get('osv_memory_count_limit')
_max_hours = config.get('osv_memory_age_limit')
_inherit_fields = {}
_max_count = None
_max_hours = None
_check_time = 20
@classmethod
@@ -2082,6 +2083,8 @@ class orm_memory(orm_template):
self.datas = {}
self.next_id = 0
self.check_id = 0
self._max_count = config.get('osv_memory_count_limit')
self._max_hours = config.get('osv_memory_age_limit')
cr.execute('delete from wkf_instance where res_type=%s', (self._name,))
def _check_access(self, uid, object_id, mode):
+7 -2
View File
@@ -45,7 +45,6 @@ import openerp.loglevels as loglevels
import openerp.pooler as pooler
from config import config
from translate import _
from yaml_import import convert_yaml_import
# List of etree._Element subclasses that we choose to ignore when parsing XML.
from misc import SKIPPED_ELEMENT_TYPES
@@ -402,7 +401,7 @@ form: module.record_id""" % (xml_id,)
self._remove_ir_values(cr, string, value, model)
def _tag_url(self, cr, rec, data_node=None):
url = rec.get("string",'').encode('utf8')
url = rec.get("url",'').encode('utf8')
target = rec.get("target",'').encode('utf8')
name = rec.get("name",'').encode('utf8')
xml_id = rec.get('id','').encode('utf8')
@@ -654,6 +653,12 @@ form: module.record_id""" % (xml_id,)
resw = cr.fetchone()
if (not values.get('name', False)) and resw:
values['name'] = resw[0]
elif a_type=='url':
a_id = self.id_get(cr, a_action)
cr.execute('select name from ir_act_url where id=%s', (int(a_id),))
resw = cr.fetchone()
if (not values.get('name')) and resw:
values['name'] = resw[0]
if rec.get('sequence'):
values['sequence'] = int(rec.get('sequence'))
if rec.get('icon'):
+1 -2
View File
@@ -177,7 +177,7 @@ class YamlInterpreter(object):
try:
_, id = self.pool.get('ir.model.data').get_object_reference(self.cr, self.uid, module, checked_xml_id)
self.id_map[xml_id] = id
except ValueError, e:
except ValueError:
raise ValueError("""%s not found when processing %s.
This Yaml file appears to depend on missing data. This often happens for
tests that belong to a module's test suite and depend on each other.""" % (checked_xml_id, self.filename))
@@ -488,7 +488,6 @@ class YamlInterpreter(object):
if self.isnoupdate(function) and self.mode != 'init':
return
model = self.get_model(function.model)
context = self.get_context(function, self.eval_context)
if function.eval:
args = self.process_eval(function.eval)
else:
+1 -1
View File
@@ -32,7 +32,7 @@ class Record(YamlTag):
return '!record {model: %s, id: %s}:' % (str(self.model,), str(self.id,))
class Python(YamlTag):
def __init__(self, model, severity=logging.WARNING, name="", **kwargs):
def __init__(self, model, severity=logging.ERROR, name="", **kwargs):
self.model= model
self.severity = severity
self.name = name