[IMP] access rights: improve error messages for ACLs and record rules

bzr revid: odo@openerp.com-20120615153414-w4p4iczhl6lli50u
This commit is contained in:
Olivier Dony 2012-06-15 17:34:14 +02:00
parent 5eaed9dbd4
commit 34965058ec
2 changed files with 44 additions and 22 deletions

View File

@ -518,15 +518,16 @@ class ir_model_access(osv.osv):
""" """
assert access_mode in ['read','write','create','unlink'], 'Invalid access mode: %s' % access_mode assert access_mode in ['read','write','create','unlink'], 'Invalid access mode: %s' % access_mode
cr.execute('''SELECT cr.execute('''SELECT
g.name c.name, g.name
FROM FROM
ir_model_access a ir_model_access a
JOIN ir_model m ON (a.model_id=m.id) JOIN ir_model m ON (a.model_id=m.id)
JOIN res_groups g ON (a.group_id=g.id) JOIN res_groups g ON (a.group_id=g.id)
LEFT JOIN ir_module_category c ON (c.id=g.category_id)
WHERE WHERE
m.model=%s AND m.model=%s AND
a.perm_''' + access_mode, (model_name,)) a.perm_''' + access_mode, (model_name,))
return [x[0] for x in cr.fetchall()] return [('%s/%s' % x) if x[0] else x[1] for x in cr.fetchall()]
@tools.ormcache() @tools.ormcache()
def check(self, cr, uid, model, mode='read', raise_exception=True, context=None): def check(self, cr, uid, model, mode='read', raise_exception=True, context=None):
@ -570,15 +571,23 @@ class ir_model_access(osv.osv):
r = cr.fetchone()[0] r = cr.fetchone()[0]
if not r and raise_exception: if not r and raise_exception:
groups = ', '.join(self.group_names_with_access(cr, model_name, mode)) or '/' groups = '\n\t'.join('- %s' % g for g in self.group_names_with_access(cr, model_name, mode))
msgs = { msg_heads = {
'read': _("You can not read this document (%s) ! Be sure your user belongs to one of these groups: %s."), # Messages are declared in extenso so they are properly exported in translation terms
'write': _("You can not write in this document (%s) ! Be sure your user belongs to one of these groups: %s."), 'read': _("Sorry, you are not allowed to access this document."),
'create': _("You can not create this document (%s) ! Be sure your user belongs to one of these groups: %s."), 'write': _("Sorry, you are not allowed to modify this document."),
'unlink': _("You can not delete this document (%s) ! Be sure your user belongs to one of these groups: %s."), 'create': _("Sorry, you are not allowed to create this kind of document."),
'unlink': _("Sorry, you are not allowed to delete this document."),
} }
if groups:
raise except_orm(_('AccessError'), msgs[mode] % (model_name, groups) ) msg_tail = _("Only users with the following access level are currently allowed to do that") + ":\n%s\n\n(" + _("Document model") + ": %s)"
msg_params = (groups, model_name)
else:
msg_tail = _("Please contact your system administrator if you think this is an error.") + "\n\n(" + _("Document model") + ": %s)"
msg_params = (model_name,)
_logger.warning('Access Denied by ACLs for operation: %s, uid: %s, model: %s', mode, uid, model_name)
msg = '%s %s' % (msg_heads[mode], msg_tail)
raise except_orm(_('Access Denied'), msg % msg_params)
return r or False return r or False
__cache_clearing_methods = [] __cache_clearing_methods = []

View File

@ -3487,10 +3487,7 @@ class BaseModel(object):
for sub_ids in cr.split_for_in_conditions(ids): for sub_ids in cr.split_for_in_conditions(ids):
if rule_clause: if rule_clause:
cr.execute(query, [tuple(sub_ids)] + rule_params) cr.execute(query, [tuple(sub_ids)] + rule_params)
if cr.rowcount != len(sub_ids): self._check_record_rules_result_count(cr, user, sub_ids, 'read', context=context)
raise except_orm(_('AccessError'),
_('Operation prohibited by access rules, or performed on an already deleted document (Operation: read, Document type: %s).')
% (self._description,))
else: else:
cr.execute(query, (tuple(sub_ids),)) cr.execute(query, (tuple(sub_ids),))
res.extend(cr.dictfetchall()) res.extend(cr.dictfetchall())
@ -3669,6 +3666,26 @@ class BaseModel(object):
# mention the first one only to keep the error message readable # mention the first one only to keep the error message readable
raise except_orm('ConcurrencyException', _('A document was modified since you last viewed it (%s:%d)') % (self._description, res[0])) raise except_orm('ConcurrencyException', _('A document was modified since you last viewed it (%s:%d)') % (self._description, res[0]))
def _check_record_rules_result_count(self, cr, uid, ids, operation, context=None):
"""Verify that number of returned rows after applying record rules matches
the length of `ids`, and raise an appropriate exception if it does not.
"""
if cr.rowcount != len(ids):
# Attempt to distinguish record rule restriction vs deleted records,
# to provide a more specific error message
cr.execute('SELECT id FROM ' + self._table + ' WHERE id IN %s', (tuple(ids),))
if cr.rowcount != len(ids):
if operation == 'unlink':
# no need to warn about deleting an already deleted record!
return
_logger.warning('Failed operation on deleted record(s): %s, uid: %s, model: %s', operation, uid, self._name)
raise except_orm(_('Missing document(s)'),
_('One of the documents you are trying to access has been deleted, please try again after refreshing.'))
_logger.warning('Access Denied by record rules for operation: %s, uid: %s, model: %s', operation, uid, self._name)
raise except_orm(_('Access Denied'),
_('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % \
(self._description, operation))
def check_access_rights(self, cr, uid, operation, raise_exception=True): # no context on purpose. def check_access_rights(self, cr, uid, operation, raise_exception=True): # no context on purpose.
"""Verifies that the operation given by ``operation`` is allowed for the user """Verifies that the operation given by ``operation`` is allowed for the user
according to the access rights.""" according to the access rights."""
@ -3700,7 +3717,7 @@ class BaseModel(object):
if self.is_transient(): if self.is_transient():
# Only one single implicit access rule for transient models: owner only! # Only one single implicit access rule for transient models: owner only!
# This is ok to hardcode because we assert that TransientModels always # This is ok to hardcode because we assert that TransientModels always
# have log_access enabled and this the create_uid column is always there. # have log_access enabled so that the create_uid column is always there.
# And even with _inherits, these fields are always present in the local # And even with _inherits, these fields are always present in the local
# table too, so no need for JOINs. # table too, so no need for JOINs.
cr.execute("""SELECT distinct create_uid cr.execute("""SELECT distinct create_uid
@ -3708,9 +3725,8 @@ class BaseModel(object):
WHERE id IN %%s""" % self._table, (tuple(ids),)) WHERE id IN %%s""" % self._table, (tuple(ids),))
uids = [x[0] for x in cr.fetchall()] uids = [x[0] for x in cr.fetchall()]
if len(uids) != 1 or uids[0] != uid: if len(uids) != 1 or uids[0] != uid:
raise except_orm(_('AccessError'), '%s access is ' raise except_orm(_('Access Denied'),
'restricted to your own records for transient models ' _('For this kind of document, you may only access records you created yourself.\n\n(Document type: %s)') % (self._description,))
'(except for the super-user).' % operation.capitalize())
else: else:
where_clause, where_params, tables = self.pool.get('ir.rule').domain_get(cr, uid, self._name, operation, context=context) where_clause, where_params, tables = self.pool.get('ir.rule').domain_get(cr, uid, self._name, operation, context=context)
if where_clause: if where_clause:
@ -3719,10 +3735,7 @@ class BaseModel(object):
cr.execute('SELECT ' + self._table + '.id FROM ' + ','.join(tables) + cr.execute('SELECT ' + self._table + '.id FROM ' + ','.join(tables) +
' WHERE ' + self._table + '.id IN %s' + where_clause, ' WHERE ' + self._table + '.id IN %s' + where_clause,
[sub_ids] + where_params) [sub_ids] + where_params)
if cr.rowcount != len(sub_ids): self._check_record_rules_result_count(cr, uid, sub_ids, operation, context=context)
raise except_orm(_('AccessError'),
_('Operation prohibited by access rules, or performed on an already deleted document (Operation: %s, Document type: %s).')
% (operation, self._description))
def _workflow_trigger(self, cr, uid, ids, trigger, context=None): def _workflow_trigger(self, cr, uid, ids, trigger, context=None):
"""Call given workflow trigger as a result of a CRUD operation""" """Call given workflow trigger as a result of a CRUD operation"""