IR_RULES :

- new object to define versatile access rules.
- provide cache to keep fast access.
- associated with user or user group.

The orm layer is modified to use them. This modification and
pre-defined rules in xml data replace the most part of implicit magic
around company_id.

bzr revid: bch-73e82904c54c3ad4f5a7454a446af30572ae156d
This commit is contained in:
bch 2007-05-18 12:29:43 +00:00
parent 570538f860
commit 605fde9553
7 changed files with 256 additions and 48 deletions

View File

@ -59,6 +59,8 @@
<field name="arch" type="xml">
<form string="Groups">
<field name="name" colspan="4" select="1"/>
<field name="rule_ids" />
</form>
</field>
</record>
@ -116,6 +118,7 @@
<newline/>
<field name="groups_id"/>
<field name="roles_id"/>
<field name="rule_ids"/>
</form>
</field>
</record>

View File

@ -39,3 +39,4 @@ import ir_values
import ir_translation
import ir_exports
import workflow
import ir_rule

View File

@ -792,6 +792,48 @@
<field name="view_id" ref="ir_access_view_form"/>
</record>
<menuitem name="Administration/Security/Access Controls" action="ir_access_act" id="menu_ir_access_act"/>
==========================================================
Rules
==========================================================
<record model="ir.ui.view" id="view_rule_form">
<field name="name">Rule</field>
<field name="model">ir.rule</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="rule">
<field name="name"/>
<field name="type" />
<field name="model_id" />
<field name="field_id" on_change="onchange_rule(model_id,field_id, operator, operand)"/>
<field name="operator" on_change="onchange_rule(model_id,field_id, operator, operand)"/>
<field name="operand" on_change="onchange_rule(model_id,field_id, operator, operand)"/>
<field name="domain" colspan="4"/>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_rule_tree">
<field name="name">Rule</field>
<field name="model">ir.rule</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Rules">
<field name="name"/>
<field name="type"/>
<field name="model_id"/>
<field name="domain"/>
</tree>
</field>
</record>
<record model="ir.actions.act_window" id="action_rule">
<field name="name">Rule</field>
<field name="res_model">ir.rule</field>
<field name="view_type">form</field>
<field name="view_id" ref="view_rule_form"/>
</record>
<menuitem name="Administration/Security/Rules" action="action_rule" id="menu_action_rule"/>
</data>
</terp>

View File

@ -0,0 +1,121 @@
##############################################################################
#
# Copyright (c) 2004-2006 TINY SPRL. (http://tiny.be) All Rights Reserved.
# Fabien Pinckaers <fp@tiny.Be>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
from osv import fields,osv
import time
import tools
class ir_rule(osv.osv):
_name = 'ir.rule'
def _operand(self,cr,uid,context):
def get(object, level=3, ending=[], ending_excl=[], recur=[], root_tech='', root=''):
res= []
fields = self.pool.get(object).fields_get(cr,uid)
key = fields.keys()
key.sort()
for k in key:
if (not ending or fields[k]['type'] in ending) and ((not ending_excl) or not (fields[k]['type'] in ending_excl)):
res.append((root_tech+'.'+k,root+'/'+fields[k]['string']))
if fields[k]['type'] in recur:
res.append((root_tech+'.'+k+'.id',root+'/'+fields[k]['string']))
if (fields[k]['type'] in recur) and (level>0):
res.extend(get(fields[k]['relation'], level-1, ending,
ending_excl, recur, root_tech+'.'+k, root+'/'+fields[k]['string']))
return res
res = [("False", "False"),("user.id","User")]+get('res.users', level=1,ending_excl=['one2many','many2one','many2many','reference'],
recur=['many2one'],root_tech='user',root='User')
return res
_columns = {
'name': fields.char('Name',size=128, required=True, select=True),
'type': fields.selection( (('add','Additive'),('sub','Subtractive')),'Type',required=True, select=True),
'model_id': fields.many2one('ir.model', 'Model',select=True, required=True),
'field_id': fields.many2one('ir.model.fields', 'Field',domain= "[('model_id','=',model_id)]",select=True),
'operator':fields.selection( (('=','='),('<>','<>'),('<=','<='),('>=','>=')),'Operator'),
'operand':fields.selection(_operand,'Operand', size=64),
'domain': fields.char('Domain', size=256, required=True)
}
_defaults={
'type': lambda *a : 'add'
}
def domain_get(self, cr, uid, model_name):
# root user above constraint
if uid == 1:
return '', []
cr.execute("select r.id from ir_rule r join ir_model m on (r.model_id = m.id ) where m.model = %s and r.id in ( select rule_id from user_rule_rel where users_id = %d union select rule_id from group_rule_rel g join res_groups_users_rel u on (g.group_id = u.gid) where u.uid = %d )", (model_name,uid,uid))
ids = map(lambda x:x[0], cr.fetchall())
obj = self.pool.get(model_name)
add = []
add_str = []
sub = []
sub_str = []
for rule in self.browse(cr, uid, ids):
dom = eval(rule.domain, {'user': self.pool.get('res.users').browse(cr, uid, uid), 'time':time})
d1,d2 = obj._where_calc(dom)
if rule.type=='add':
add_str += d1
add +=d2
else:
sub_str += d1
sub += d2
add_str = ' or '.join(add_str)
sub_str = ' and '.join(sub_str)
if not (add or sub):
return '', []
if add and sub:
return '((%s) and (%s))' % (add_str, sub_str), add+sub
if add:
return '%s' % (add_str,), add
if sub:
return '%s' % (sub_str,),sub
domain_get = tools.cache()(domain_get)
def onchange_rule(self, cr, uid, context, model_id, field_id, operator, operand):
if not ( field_id and operator and operand): return {}
field_names= self.pool.get('ir.model.fields').read(cr,uid,[field_id], ["name"])
if not field_names : return {}
return {'value':{'domain': "[('%s', '%s', %s)]"%(field_names[0]['name'], operator, operand)}}
def write(self, cr, uid, *args, **argv):
res = super(ir_rule, self).write(cr, uid, *args, **argv)
# Restart the cache on the company_get method
self.domain_get()
return res
ir_rule()

View File

@ -33,7 +33,13 @@ class groups(osv.osv):
_name = "res.groups"
_columns = {
'name': fields.char('Group Name', size=64, required=True),
'rule_ids': fields.many2many('ir.rule', 'group_rule_rel', 'group_id', 'rule_id', 'Acces Rules'),
}
def write(self, cr, uid, *args, **argv):
res = super(groups, self).write(cr, uid, *args, **argv)
# Restart the cache on the company_get method
self.pool.get('ir.rule').domain_get()
return res
groups()
@ -86,6 +92,7 @@ class users(osv.osv):
'groups_id': fields.many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', 'groups'),
'roles_id': fields.many2many('res.roles', 'res_roles_users_rel', 'uid', 'rid', 'Roles'),
'company_id': fields.many2one('res.company', 'Company'),
'rule_ids': fields.many2many('ir.rule', 'user_rule_rel', 'users_id', 'rule_id', 'Acces Rules'),
}
_sql_constraints = [
('login_key', 'UNIQUE (login)', 'You can not have two users with the same login !')
@ -101,8 +108,8 @@ class users(osv.osv):
def write(self, cr, uid, *args, **argv):
res = super(users, self).write(cr, uid, *args, **argv)
# Restart the cache on the company_get method
self.company_get()
self.pool.get('ir.rule').domain_get()
return res
def unlink(self, cr, uid, ids):
@ -120,5 +127,7 @@ class users(osv.osv):
login = self.read(cr, uid, [id], ['login'])[0]['login']
default.update({'login': login+' (copy)'})
return super(users, self).copy(cr, uid, id, default, context)
users()

View File

@ -285,8 +285,11 @@ class many2one(_column):
for id in filter(None, res.values()):
try:
names[id] = dict(obj.name_get(cr, user, [id], context))[id]
except except_orm:
names[id] = "== Access denied =="
except except_orm, e:
if e.name == 'AccessError':
names[id] = "== Access denied =="
else :
raise
for r in res.keys():
if res[r] and res[r] in names:
res[r] = (res[r], names[res[r]])
@ -397,11 +400,12 @@ class many2many(_column):
ids_s = ','.join(map(str,ids))
limit_str = self._limit is not None and ' limit %d' % self._limit or ''
obj = obj.pool.get(self._obj)
if 'company_id' in obj._columns:
compids = tools.get_user_companies(cr, user)
cr.execute('SELECT r.'+self._id2+', r.'+self._id1+' FROM '+self._rel+' AS r, '+obj._table+' AS o WHERE r.'+self._id1+' in ('+ids_s+') AND r.'+self._id2+' = o.id AND (o.company_id IN ('+','.join(map(str,compids))+') OR o.company_id IS NULL)'+limit_str+' OFFSET %d', (offset,))
else:
cr.execute('select '+self._id2+','+self._id1+' from '+self._rel+' where '+self._id1+' in ('+ids_s+')'+limit_str+' offset %d', (offset,))
d1, d2 = obj.pool.get('ir.rule').domain_get(cr, user, obj._name)
if d1:
d1 = ' and '+d1
cr.execute('SELECT '+self._rel+'.'+self._id2+','+self._rel+'.'+self._id1+' FROM '+self._rel+' , '+obj._table+' where '+self._rel+'.'+self._id1+' in ('+ids_s+') AND '+self._rel+'.'+self._id2+' = '+obj._table+'.id ' +d1+limit_str+' offset %d', d2+[offset])
for r in cr.fetchall():
res[r[1]].append(r[0])
return res
@ -425,12 +429,13 @@ class many2many(_column):
elif act[0]==5:
cr.execute('update '+self._rel+' set '+self._id2+'=null where '+self._id2+'=%d', (id,))
elif act[0]==6:
if 'company_id' in obj._columns and not user == 1:
compids = tools.get_user_companies(cr, user)
cr.execute('delete from '+self._rel+' where '+self._id1+'=%d AND '+self._id2+' IN (SELECT r.'+self._id2+' FROM '+self._rel+' AS r, '+obj._table+' AS o WHERE r.'+self._id1+'=%d AND r.'+self._id2+' = o.id AND (o.company_id IN ('+','.join(map(str,compids))+') OR o.company_id IS NULL))', (id, id, ))
else:
cr.execute('delete from '+self._rel+' where '+self._id1+'=%d', (id, ))
for act_nbr in act[2]:
d1, d2 = obj.pool.get('ir.rule').domain_get(cr, user, obj._name)
if d1:
d1 = ' and '+d1
cr.execute('delete from '+self._rel+' where '+self._id1+'=%d AND '+self._id2+' IN (SELECT '+self._rel+'.'+self._id2+' FROM '+self._rel+', '+obj._table+' WHERE '+self._rel+'.'+self._id1+'=%d AND '+self._rel+'.'+self._id2+' = '+obj._table+'.id '+ d1 +')', [id, id]+d2 )
for act_nbr in act[2]: # XXX add clause ? [bch 20070518]
cr.execute('insert into '+self._rel+' ('+self._id1+','+self._id2+') values (%d, %d)', (id, act_nbr))
#

View File

@ -174,12 +174,15 @@ class browse_record(object):
if data[n]:
obj = self._table.pool.get(f._obj)
compids=False
if 'company_id' in obj._columns and not self._uid == 1:
compids = tools.get_user_companies(self._cr, self._uid)
if compids:
self._cr.execute('SELECT id FROM '+obj._table+' where id = %d AND (company_id in ('+','.join(map(str,compids))+') or company_id is null)', (data[n],))
if not self._cr.fetchall():
raise except_orm('BrowseError', 'Object %s (id:%d) is linked to the object %s (id:%d) which is not in your company' %(self._table._description, self._id, obj._description, data[n]))
#
# Removed for performance sake. (Bug may arise) [20070516]
#
# if 'company_id' in obj._columns :
# compids = tools.get_user_companies(self._cr, self._uid)
# if compids:
# self._cr.execute('SELECT id FROM '+obj._table+' where id = %d AND (company_id in ('+','.join(map(str,compids))+') or company_id is null)', (data[n],))
# if not self._cr.fetchall():
# raise except_orm('BrowseError', 'Object %s (id:%d) is linked to the object %s (id:%d) which is not in your company' %(self._table._description, self._id, obj._description, data[n]))
data[n] = browse_record(self._cr, self._uid, data[n], obj, self._cache, context=self._context, list_class=self._list_class)
else:
data[n] = browse_null()
@ -479,7 +482,19 @@ class orm(object):
self._sequence = self._table+'_id_seq'
for k in self._defaults:
assert (k in self._columns) or (k in self._inherit_fields), 'Default function defined in %s but field %s does not exist !' % (self._name, k,)
if self._log_access:
self._columns.update({
'create_uid': fields.many2one('res.users','Creation user',required=True, readonly=True),
'create_date': fields.datetime('Creation date',required=True, readonly=True),
'write_uid': fields.many2one('res.users','Last modification by', readonly=True),
'write_date': fields.datetime('Last modification date', readonly=True),
})
#FIXME : does not work :
# self._defaults.update({
# 'create_uid': lambda self,cr,uid,context : uid,
# 'create_date': lambda *a : time.strftime("%Y-%m-%d %H:%M:%S")
# })
#
# Update objects that uses this one to update their _inherits fields
#
@ -710,25 +725,20 @@ class orm(object):
if fields==None:
fields = self._columns.keys()
# if the object has a field named 'company_id', filter out all
# records which do not concern the current company (the company
# of the current user) or its "childs"
company_clause='true'
compids=False
if 'company_id' in self._columns and not user == 1:
compids = tools.get_user_companies(cr, user)
if compids:
company_clause = '(company_id in ('+','.join(map(str,compids))+') or company_id is null)'
# construct a clause for the rules :
d1, d2 = self.pool.get('ir.rule').domain_get(cr, user, self._name)
# all inherited fields + all non inherited fields for which the attribute whose name is in load is True
fields_pre = filter(lambda x: x in self._columns and getattr(self._columns[x],'_classic_write'), fields) + self._inherits.values()
if len(fields_pre) or compids:
cr.execute('select %s from %s where id in (%s) and %s order by %s' % (','.join(fields_pre + ['id']), self._table, ','.join([str(x) for x in ids]), company_clause, self._order))
uniq_id = []
[uniq_id.append(i) for i in ids if not uniq_id.count(i)]
if not cr.rowcount == len(uniq_id) and compids:
raise except_orm('ReadError', 'You try to read objects (%s) that is not in your company' % self._description)
if len(fields_pre) :
if d1:
cr.execute('select %s from %s where id in (%s) and %s order by %s' % (','.join(fields_pre + ['id']), self._table, ','.join([str(x) for x in ids]), d1, self._order),d2)
if not cr.rowcount == len({}.fromkeys(ids)):
raise except_orm('AccessError', 'You try to bypass an access rule (Document type : %s).' % self._description)
else:
cr.execute('select %s from %s where id in (%s) order by %s' % (','.join(fields_pre + ['id']), self._table, ','.join([str(x) for x in ids]), self._order))
res = cr.dictfetchall()
else:
res = map(lambda x: {'id':x}, ids)
@ -835,19 +845,26 @@ class orm(object):
wf_service.trg_delete(uid, self._name, id, cr)
str_d = string.join(('%d',)*len(ids),',')
cr.execute('select * from '+self._table+' where id in ('+str_d+')', ids)
res = cr.dictfetchall()
#cr.execute('select * from '+self._table+' where id in ('+str_d+')', ids)
#res = cr.dictfetchall()
#for key in self._inherits:
# ids2 = [x[self._inherits[key]] for x in res]
# self.pool.get(key).unlink(cr, uid, ids2)
cr.execute('delete from inherit where (obj_type=%s and obj_id in ('+str_d+')) or (inst_type=%s and inst_id in ('+str_d+'))', (self._name,)+tuple(ids)+(self._name,)+tuple(ids))
cr.execute('delete from '+self._table+' where id in ('+str_d+')', ids)
d1, d2 = self.pool.get('ir.rule').domain_get(cr, uid, self._name)
if d1:
d1 = ' and '+d1
cr.execute('delete from inherit where (obj_type=%s and obj_id in ('+str_d+'))'+d1+' or (inst_type=%s and inst_id in ('+str_d+')'+d1+')', [self._name]+ids+d2+[self._name]+ids+d2)
cr.execute('delete from '+self._table+' where id in ('+str_d+')'+d1, ids+d2)
return True
#
# TODO: Validate
#
def write(self, cr, user, ids, vals, context={}):
if not ids:
return True
delta= context.get('read_delta',False)
if delta and self._log_access:
cr.execute("select (now() - min(write_date)) <= '%s'::interval from %s where id in (%s)"% (delta,self._table,",".join(map(str, ids))) )
@ -856,10 +873,9 @@ class orm(object):
raise except_orm('ConcurrencyException', 'This record was modified in the meanwhile')
self.pool.get('ir.model.access').check(cr, user, self._name, 'write')
#for v in self._inherits.values():
# assert v not in vals, (v, vals)
if not ids:
return
ids_str = string.join(map(str, ids),',')
upd0=[]
upd1=[]
@ -884,7 +900,11 @@ class orm(object):
upd1.append(user)
if len(upd0):
cr.execute('update '+self._table+' set '+string.join(upd0,',')+' where id in ('+ids_str+')', upd1)
d1, d2 = self.pool.get('ir.rule').domain_get(cr, user, self._name)
if d1:
d1 = ' and '+d1
cr.execute('update '+self._table+' set '+string.join(upd0,',')+' where id in ('+ids_str+')'+d1, upd1+ d2)
if totranslate:
for f in direct:
@ -1391,11 +1411,11 @@ class orm(object):
# if the object has a field named 'company_id', filter out all
# records which do not concern the current company (the company
# of the current user) or its "childs"
if 'company_id' in self._columns and not user == 1:
compids = tools.get_user_companies(cr, user)
if compids:
compids.append(False)
args.append(('company_id','in',compids))
# if 'company_id' in self._columns and not user == 1:
# compids = tools.get_user_companies(cr, user)
# if compids:
# compids.append(False)
# args.append(('company_id','in',compids))
i = 0
tables=[self._table]
@ -1497,6 +1517,7 @@ class orm(object):
# compute the where, order by, limit and offset clauses
(qu1,qu2) = self._where_calc(args)
if len(qu1):
qu1 = ' where '+string.join(qu1,' and ')
else:
@ -1505,7 +1526,13 @@ class orm(object):
limit_str = limit and ' limit %d' % limit or ''
offset_str = offset and ' offset %d' % offset or ''
# construct a clause for the rules :
d1, d2 = self.pool.get('ir.rule').domain_get(cr, user, self._name)
if d1:
qu1 = qu1 and qu1+' and '+d1 or ' where '+d1
qu2 += d2
# execute the "main" query to fetch the ids we were searching for
cr.execute('select %s.id from ' % self._table + ','.join(tables) +qu1+' order by '+order_by+limit_str+offset_str, qu2)
res = cr.fetchall()