From 605fde9553da882f5d6ddf6ae25ebc848e4938f3 Mon Sep 17 00:00:00 2001 From: bch <> Date: Fri, 18 May 2007 12:29:43 +0000 Subject: [PATCH] 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 --- bin/addons/base/base_update.xml | 3 + bin/addons/base/ir/__init__.py | 1 + bin/addons/base/ir/ir.xml | 42 +++++++++++ bin/addons/base/ir/ir_rule.py | 121 ++++++++++++++++++++++++++++++++ bin/addons/base/res/res_user.py | 11 ++- bin/osv/fields.py | 31 ++++---- bin/osv/orm.py | 95 ++++++++++++++++--------- 7 files changed, 256 insertions(+), 48 deletions(-) create mode 100644 bin/addons/base/ir/ir_rule.py diff --git a/bin/addons/base/base_update.xml b/bin/addons/base/base_update.xml index 4bb2296493f..364f9fb0ee3 100644 --- a/bin/addons/base/base_update.xml +++ b/bin/addons/base/base_update.xml @@ -59,6 +59,8 @@
+ + @@ -116,6 +118,7 @@ + diff --git a/bin/addons/base/ir/__init__.py b/bin/addons/base/ir/__init__.py index 46331d821fd..affb98af7f3 100644 --- a/bin/addons/base/ir/__init__.py +++ b/bin/addons/base/ir/__init__.py @@ -39,3 +39,4 @@ import ir_values import ir_translation import ir_exports import workflow +import ir_rule diff --git a/bin/addons/base/ir/ir.xml b/bin/addons/base/ir/ir.xml index a8f051b8a1c..dc4941e3fcd 100644 --- a/bin/addons/base/ir/ir.xml +++ b/bin/addons/base/ir/ir.xml @@ -792,6 +792,48 @@ + ========================================================== + Rules + ========================================================== + + + Rule + ir.rule + form + +
+ + + + + + + + + +
+ + + Rule + ir.rule + tree + + + + + + + + + + + Rule + ir.rule + form + + + + diff --git a/bin/addons/base/ir/ir_rule.py b/bin/addons/base/ir/ir_rule.py new file mode 100644 index 00000000000..92cb09d8d84 --- /dev/null +++ b/bin/addons/base/ir/ir_rule.py @@ -0,0 +1,121 @@ +############################################################################## +# +# Copyright (c) 2004-2006 TINY SPRL. (http://tiny.be) All Rights Reserved. +# Fabien Pinckaers +# +# 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() diff --git a/bin/addons/base/res/res_user.py b/bin/addons/base/res/res_user.py index 194d7e049c2..45252070252 100644 --- a/bin/addons/base/res/res_user.py +++ b/bin/addons/base/res/res_user.py @@ -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() diff --git a/bin/osv/fields.py b/bin/osv/fields.py index 0e65ea60872..bd37694895b 100644 --- a/bin/osv/fields.py +++ b/bin/osv/fields.py @@ -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)) # diff --git a/bin/osv/orm.py b/bin/osv/orm.py index c449350d5e9..7ccc39aa44c 100644 --- a/bin/osv/orm.py +++ b/bin/osv/orm.py @@ -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()