[IMP] share,portal: improve sharing with portal group

Use a new strategy: documents are shared with a
temporary group, and when everything is setup,
the access rights are copied to the groups we are
sharing with, and the rules are transferred to them.
The temporary group is then deleted.
This works well for multiple shares, are both
access rights and rules are additive in this 
fashion.

bzr revid: odo@openerp.com-20110619170000-epst11y4aaichblh
This commit is contained in:
Olivier Dony 2011-06-19 19:00:00 +02:00
parent 981b3fa3e1
commit 5cfa1974c6
4 changed files with 140 additions and 104 deletions

View File

@ -112,52 +112,78 @@ class share_wizard_portal(osv.osv_memory):
menu_id = self.pool.get('ir.ui.menu').create(cr, UID_ROOT, menu_data)
return menu_id
def _create_share_users_groups(self, cr, uid, wizard_data, context=None):
"""Creates the appropriate shared users and groups, and populates
result_line_ids of wizard_data with one line for each user.
def _create_share_users_group(self, cr, uid, wizard_data, context=None):
# Override of super() to handle the possibly selected "existing users"
# and "existing groups".
# In both cases, we call super() to create the share group, but when
# sharing with existing groups, we will later delete it, and copy its
# access rights and rules to the selected groups.
group_id = super(share_wizard_portal,self)._create_share_users_group(cr, uid, wizard_data, context=context)
:return: group_ids (to which the shared access should be granted),
new_user_ids, and existing_user_ids.
"""
group_ids, new_ids, existing_ids = [], [], []
if wizard_data.user_type == 'groups':
group_id = None
group_ids.extend([g.id for g in wizard_data.group_ids])
# populate result lines with the users of each group
for group in wizard_data.group_ids:
for user in group.users:
new_line = {'login': user.login,
'newly_created': False}
wizard_data.write({'result_line_ids': [(0,0,new_line)]})
# get the list of portals and the related groups
# and install their menus.
# For sharing with existing groups, we don't create a share group, instead we'll
# alter the rules of the groups so they can see the shared data
if wizard_data.group_ids:
# get the list of portals and the related groups to install their menus.
Portals = self.pool.get('res.portal')
all_portals = Portals.browse(cr, UID_ROOT, Portals.search(cr, UID_ROOT, [])) #no context!
all_portal_group_ids = [p.group_id.id for p in all_portals]
# populate result lines with the users of each group and
# setup the menu for portal groups
for group in wizard_data.group_ids:
if group.id in all_portal_group_ids:
portal = all_portals[all_portal_group_ids.index(group.id)]
self._create_shared_data_menu(cr, uid, wizard_data, portal, context=context)
else:
# for other case with user_type in ('emails', 'existing'), we rely on super()
group_ids, new_ids, existing_ids = super(share_wizard_portal,self)._create_share_users_groups(cr, uid, wizard_data, context=context)
# must take care of existing users, by adding them to the new group, which is group_ids[0],
# and adding the shortcut
group_id = group_ids[0]
existing_user_ids = [x.id for x in wizard_data.user_ids] # manually selected users
if existing_user_ids:
self.pool.get('res.users').write(cr, UID_ROOT, existing_user_ids, {'groups_id': [(4,group_id)]})
self._setup_action_and_shortcut(cr, uid, wizard_data, existing_ids, make_home=False, context=context)
existing_ids.extend(existing_user_ids)
# populate the result lines for existing users too
for user in wizard_data.user_ids:
new_line = { 'login': user.login,
'newly_created': False}
for user in group.users:
new_line = {'user_id': user.id,
'newly_created': False}
wizard_data.write({'result_line_ids': [(0,0,new_line)]})
return group_ids, new_ids, existing_ids
elif wizard_data.user_ids:
# must take care of existing users, by adding them to the new group, which is group_ids[0],
# and adding the shortcut
selected_user_ids = [x.id for x in wizard_data.user_ids]
self.pool.get('res.users').write(cr, UID_ROOT, selected_user_ids, {'groups_id': [(4,group_id)]})
self._setup_action_and_shortcut(cr, uid, wizard_data, selected_user_ids, make_home=False, context=context)
# populate the result lines for existing users too
for user in wizard_data.user_ids:
new_line = { 'user_id': user.id,
'newly_created': False}
wizard_data.write({'result_line_ids': [(0,0,new_line)]})
return group_id
def copy_share_group_access_and_delete(self, cr, wizard_data, share_group_id, context=None):
# In the case of sharing with existing groups, the strategy is to copy
# access rights and rules from the share group, so that we can
if not wizard_data.group_ids: return
Groups = self.pool.get('res.groups')
Rules = self.pool.get('ir.rule')
Rights = self.pool.get('ir.model.access')
share_group = Groups.browse(cr, UID_ROOT, share_group_id)
share_rule_ids = [r.id for r in share_group.rule_groups]
for target_group in wizard_data.group_ids:
# Link the rules to the group. This is appropriate because as of
# v6.1, the algorithm for combining them will OR the rules, hence
# extending the visible data.
Rules.write(cr, UID_ROOT, share_rule_ids, {'groups': [(4,target_group.id)]})
self._logger.debug("Linked sharing rules from temporary sharing group to group %s", target_group)
# Copy the access rights. This is appropriate too because
# groups have the UNION of all permissions granted by their
# access right lines.
for access_line in share_group.model_access:
Rights.copy(cr, UID_ROOT, access_line.id, default={'group_id': target_group.id})
self._logger.debug("Copied access rights from temporary sharing group to group %s", target_group)
# finally, delete it after removing its users
Groups.write(cr, UID_ROOT, [share_group_id], {'users': [(6,0,[])]})
Groups.unlink(cr, UID_ROOT, [share_group_id])
self._logger.debug("Deleted temporary sharing group %s", share_group_id)
def _finish_result_lines(self, cr, uid, wizard_data, share_group_id, context=None):
super(share_wizard_portal,self)._finish_result_lines(cr, uid, wizard_data, share_group_id, context=context)
self.copy_share_group_access_and_delete(cr, wizard_data, share_group_id, context=context)
share_wizard_portal()

View File

@ -21,7 +21,6 @@
</field>
</group>
<group colspan="4" attrs="{'invisible':[('user_type','!=','groups')]}">
<separator colspan="4" string="Existing groups"/>
<field colspan="4" nolabel="1" name="group_ids" mode="tree">
<tree string="Existing groups">
<field name="name"/>

View File

@ -75,7 +75,7 @@ def get_column_infos(osv_model):
class share_wizard(osv.osv_memory):
__logger = logging.getLogger('share.wizard')
_logger = logging.getLogger('share.wizard')
_name = 'share.wizard'
_description = 'Share Wizard'
@ -180,7 +180,7 @@ class share_wizard(osv.osv_memory):
existing = user_obj.search(cr, UID_ROOT, [('login', '=', new_user)])
existing_ids.extend(existing)
if existing:
new_line = { 'login': new_user,
new_line = { 'user_id': existing[0],
'newly_created': False}
wizard_data.write({'result_line_ids': [(0,0,new_line)]})
continue
@ -194,7 +194,7 @@ class share_wizard(osv.osv_memory):
'share': True,
'company_id': current_user.company_id.id
})
new_line = { 'login': new_user,
new_line = { 'user_id': user_id,
'password': new_pass,
'newly_created': True}
wizard_data.write({'result_line_ids': [(0,0,new_line)]})
@ -252,7 +252,7 @@ class share_wizard(osv.osv_memory):
except Exception:
# Note: must catch all exceptions, as UnquoteEvalContext may cause many
# different exceptions, as it shadows builtins.
self.__logger.debug("Failed to cleanup action context as it does not parse server-side", exc_info=True)
self._logger.debug("Failed to cleanup action context as it does not parse server-side", exc_info=True)
result = context_str
return result
@ -260,7 +260,7 @@ class share_wizard(osv.osv_memory):
copied_action = wizard_data.action_id
action_def = {
'name': wizard_data.name,
'domain': '[]',
'domain': copied_action.domain,
'context': self._cleanup_action_context(wizard_data.action_id.context, uid),
'res_model': copied_action.res_model,
'view_mode': copied_action.view_mode,
@ -401,8 +401,8 @@ class share_wizard(osv.osv_memory):
[x.id for x in current_user.groups_id], target_model_ids, context=context)
group_access_map = self._get_access_map_for_groups_and_models(cr, uid,
[group_id], target_model_ids, context=context)
self.__logger.debug("Current user access matrix: %r", current_user_access_map)
self.__logger.debug("New group current access matrix: %r", group_access_map)
self._logger.debug("Current user access matrix: %r", current_user_access_map)
self._logger.debug("New group current access matrix: %r", group_access_map)
# Create required rights if allowed by current user rights and not
# already granted
@ -423,7 +423,7 @@ class share_wizard(osv.osv_memory):
need_creation = True
if need_creation:
model_access_obj.create(cr, UID_ROOT, values)
self.__logger.debug("Creating access right for model %s with values: %r", model.model, values)
self._logger.debug("Creating access right for model %s with values: %r", model.model, values)
def _link_or_copy_current_user_rules(self, cr, current_user, group_id, fields_relations, context=None):
rule_obj = self.pool.get('ir.rule')
@ -445,13 +445,13 @@ class share_wizard(osv.osv_memory):
'groups': [(6,0,[group_id])],
'domain_force': rule.domain, # evaluated version!
})
self.__logger.debug("Copying rule %s (%s) on model %s with domain: %s", rule.name, rule.id, model.model, rule.domain_force)
self._logger.debug("Copying rule %s (%s) on model %s with domain: %s", rule.name, rule.id, model.model, rule.domain_force)
else:
# otherwise we can simply link the rule to keep it dynamic
rule_obj.write(cr, 1, [rule.id], {
'groups': [(4,group_id)]
})
self.__logger.debug("Linking rule %s (%s) on model %s with domain: %s", rule.name, rule.id, model.model, rule.domain_force)
self._logger.debug("Linking rule %s (%s) on model %s with domain: %s", rule.name, rule.id, model.model, rule.domain_force)
def _check_personal_rule_or_duplicate(self, cr, group_id, rule, context=None):
"""Verifies that the given rule only belongs to the given group_id, otherwise
@ -470,7 +470,7 @@ class share_wizard(osv.osv_memory):
'groups': [(6,0,[group_id])],
'domain_force': rule.domain_force, # non evaluated!
})
self.__logger.debug("Duplicating rule %s (%s) (domain: %s) for modified access ", rule.name, rule.id, rule.domain_force)
self._logger.debug("Duplicating rule %s (%s) (domain: %s) for modified access ", rule.name, rule.id, rule.domain_force)
# then disconnect from group_id:
rule.write({'groups':[(3,group_id)]}) # disconnects, does not delete!
return rule_obj.browse(cr, UID_ROOT, new_id, context=context)
@ -480,7 +480,19 @@ class share_wizard(osv.osv_memory):
If ``restrict`` is True, instead of adding a rule, the domain is
combined with AND operator with all existing rules in the group, to implement
an additional restriction (as of 6.1, multiple rules in the same group are
OR'ed by default, so a restriction must alter all existing rules)"""
OR'ed by default, so a restriction must alter all existing rules)
This is necessary because the personal rules of the user that is sharing
are first copied to the new share group. Afterwards the filters used for
sharing are applied as an additional layer of rules, which are likely to
apply to the same model. The default rule algorithm would OR them (as of 6.1),
which would result in a combined set of permission that could be larger
than those of the user that is sharing! Hence we must forcefully AND the
rules at this stage.
One possibly undesirable effect can appear when sharing with a
pre-existing group, in which case altering pre-existing rules would not
be desired. This is addressed in the portal module.
"""
if rule_name is None:
rule_name = _('Sharing filter created by user %s (%s) for group %s') % \
(current_user.name, current_user.login, group_id)
@ -493,7 +505,7 @@ class share_wizard(osv.osv_memory):
if restrict:
continue
else:
self.__logger.debug("Ignoring sharing rule on model %s with domain: %s the same rule exists already", model_id, domain)
self._logger.debug("Ignoring sharing rule on model %s with domain: %s the same rule exists already", model_id, domain)
return
if restrict:
# restricting existing rules is done by adding the clause
@ -505,7 +517,7 @@ class share_wizard(osv.osv_memory):
new_clause = expression.normalize(eval(domain, eval_ctx))
combined_domain = expression.AND([new_clause, org_domain])
rule.write({'domain_force': combined_domain, 'name': rule.name + _('(Modified)')})
self.__logger.debug("Combining sharing rule %s on model %s with domain: %s", rule.id, model_id, domain)
self._logger.debug("Combining sharing rule %s on model %s with domain: %s", rule.id, model_id, domain)
if not restrict:
# Adding the new rule in the group is ok for normal cases, because rules
# in the same group and for the same model will be combined with OR
@ -516,7 +528,7 @@ class share_wizard(osv.osv_memory):
'domain_force': domain,
'groups': [(4,group_id)]
})
self.__logger.debug("Created sharing rule on model %s with domain: %s", model_id, domain)
self._logger.debug("Created sharing rule on model %s with domain: %s", model_id, domain)
def _create_indirect_sharing_rules(self, cr, current_user, wizard_data, group_id, fields_relations, context=None):
rule_name = _('Indirect sharing filter created by user %s (%s) for group %s') % \
@ -538,17 +550,18 @@ class share_wizard(osv.osv_memory):
group_id, model_id=model.id, domain=str(related_domain),
rule_name=rule_name, restrict=True, context=context)
except Exception:
self.__logger.exception('Failed to create share access')
self._logger.exception('Failed to create share access')
raise osv.except_osv(_('Sharing access could not be created'),
_('Sorry, the current screen and filter you are trying to share are not supported at the moment.\nYou may want to try a simpler filter.'))
def _finish_result_lines(self, cr, uid, wizard_data, context=None):
def _finish_result_lines(self, cr, uid, wizard_data, share_group_id, context=None):
"""Perform finalization of the share, and populate the summary"""
share_root_url = wizard_data.share_root_url
format_url = '%(dbname)s' in share_root_url
for result in wizard_data.result_line_ids:
# share_root_url may contain placeholders for dbname, login and password
share_url = share_root_url % \
{'login': result.login,
{'login': result.user_id.login,
'password': '', # kept empty for security reasons
'dbname': cr.dbname} if format_url else share_root_url
result.write({'share_url': share_url}, context=context)
@ -565,15 +578,13 @@ class share_wizard(osv.osv_memory):
_('Please indicate the emails of the persons to share with, one per line'),
context=context)
def _create_share_users_groups(self, cr, uid, wizard_data, context=None):
"""Creates the appropriate shared users and groups, and populates
result_line_ids of wizard_data with one line for each user.
def _create_share_users_group(self, cr, uid, wizard_data, context=None):
"""Creates the appropriate share group and share users, and populates
result_line_ids of wizard_data with one line for each user.
:return: group_ids (to which the shared access should be granted),
new_user_ids, and existing_user_ids.
:return: the new group id (to which the shared access should be granted)
"""
group_id = self._create_share_group(cr, uid, wizard_data, context=context)
group_ids = [group_id] # other implementations may return multiple groups
# First create any missing user, based on the email addresses provided
new_ids, existing_ids = self._create_new_share_users(cr, uid, wizard_data, group_id, context=context)
# Finally, setup the new action and shortcut for the users.
@ -587,14 +598,14 @@ class share_wizard(osv.osv_memory):
if new_ids:
# new users need a new shortcut AND a home action
self._setup_action_and_shortcut(cr, uid, wizard_data, new_ids, make_home=True, context=context)
return group_ids, new_ids, existing_ids
return group_id
def go_step_2(self, cr, uid, ids, context=None):
wizard_data = self.browse(cr, uid, ids[0], context=context)
self._check_preconditions(cr, uid, wizard_data, context=context)
# Create shared group and users
group_ids, new_user_ids, existing_user_ids = self._create_share_users_groups(cr, uid, wizard_data, context=context)
group_id = self._create_share_users_group(cr, uid, wizard_data, context=context)
current_user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
@ -615,43 +626,42 @@ class share_wizard(osv.osv_memory):
obj0, obj1, obj2, obj3 = self._get_relationship_classes(cr, uid, model, context=context)
mode = wizard_data.access_mode
for group_id in group_ids:
# Add access to [obj0] and [obj1] according to chosen mode
self._add_access_rights_for_share_group(cr, uid, group_id, mode, obj0, context=context)
self._add_access_rights_for_share_group(cr, uid, group_id, mode, obj1, context=context)
# Add access to [obj0] and [obj1] according to chosen mode
self._add_access_rights_for_share_group(cr, uid, group_id, mode, obj0, context=context)
self._add_access_rights_for_share_group(cr, uid, group_id, mode, obj1, context=context)
# Add read-only access (always) to [obj2]
self._add_access_rights_for_share_group(cr, uid, group_id, 'readonly', obj2, context=context)
# Add read-only access (always) to [obj2]
self._add_access_rights_for_share_group(cr, uid, group_id, 'readonly', obj2, context=context)
# IR.RULES
# A. On [obj0], [obj1], [obj2]: add all rules from all groups of
# the user that is sharing
# Warning: rules must be copied instead of linked if they contain a reference
# to uid or if the rule is shared with other groups (and it must be replaced correctly)
# B. On [obj0]: 1 rule with domain of shared action
# C. For each model in [obj1]: 1 rule in the form:
# many2one_rel.domain_of_obj0
# where many2one_rel is the many2one used in the definition of the
# one2many, and domain_of_obj0 is the sharing domain
# For example if [obj0] is project.project with a domain of
# ['id', 'in', [1,2]]
# then we will have project.task in [obj1] and we need to create this
# ir.rule on project.task:
# ['project_id.id', 'in', [1,2]]
# IR.RULES
# A. On [obj0], [obj1], [obj2]: add all rules from all groups of
# the user that is sharing
# Warning: rules must be copied instead of linked if they contain a reference
# to uid or if the rule is shared with other groups (and it must be replaced correctly)
# B. On [obj0]: 1 rule with domain of shared action
# C. For each model in [obj1]: 1 rule in the form:
# many2one_rel.domain_of_obj0
# where many2one_rel is the many2one used in the definition of the
# one2many, and domain_of_obj0 is the sharing domain
# For example if [obj0] is project.project with a domain of
# ['id', 'in', [1,2]]
# then we will have project.task in [obj1] and we need to create this
# ir.rule on project.task:
# ['project_id.id', 'in', [1,2]]
# A.
all_relations = obj0 + obj1 + obj2
self._link_or_copy_current_user_rules(cr, current_user, group_id, all_relations, context=context)
# B.
main_domain = wizard_data.domain if wizard_data.domain != '[]' else DOMAIN_ALL
self._create_or_combine_sharing_rule(cr, current_user, wizard_data,
group_id, model_id=model.id, domain=main_domain,
restrict=True, context=context)
# C.
self._create_indirect_sharing_rules(cr, current_user, wizard_data, group_id, obj1, context=context)
# A.
all_relations = obj0 + obj1 + obj2
self._link_or_copy_current_user_rules(cr, current_user, group_id, all_relations, context=context)
# B.
main_domain = wizard_data.domain if wizard_data.domain != '[]' else DOMAIN_ALL
self._create_or_combine_sharing_rule(cr, current_user, wizard_data,
group_id, model_id=model.id, domain=main_domain,
restrict=True, context=context)
# C.
self._create_indirect_sharing_rules(cr, current_user, wizard_data, group_id, obj1, context=context)
# so far, so good -> check summary results and return them
self._finish_result_lines(cr, uid, wizard_data, context=context)
self._finish_result_lines(cr, uid, wizard_data, group_id, context=context)
# update share URL to make it generic
generic_share_url = wizard_data.share_root_url % {'dbname': cr.dbname,
@ -679,7 +689,7 @@ class share_wizard(osv.osv_memory):
}
def send_emails(self, cr, uid, wizard_data, context=None):
self.__logger.info('Sending share notifications by email...')
self._logger.info('Sending share notifications by email...')
user = self.pool.get('res.users').browse(cr, UID_ROOT, uid)
if not user.user_email:
raise osv.except_osv(_('Email required'), _('The current user must have an email address configured in User Preferences to be able to send outgoing emails.'))
@ -687,7 +697,7 @@ class share_wizard(osv.osv_memory):
# TODO: also send an HTML version of this mail
emails_sent = 0
for result_line in wizard_data.result_line_ids:
email_to = result_line.login
email_to = result_line.user_id.user_email
subject = wizard_data.name
body = _("Hello,")
body += "\n\n"
@ -701,12 +711,12 @@ class share_wizard(osv.osv_memory):
body += "\n\n"
if result_line.newly_created:
body += _("These are your credentials to access this protected area:\n")
body += "%s: %s" % (_("Username"), result_line.login) + "\n"
body += "%s: %s" % (_("Username"), result_line.user_id.login) + "\n"
body += "%s: %s" % (_("Password"), result_line.password) + "\n"
body += "%s: %s" % (_("Database"), cr.dbname) + "\n"
else:
body += _("The documents have been automatically added to your current OpenERP documents.\n")
body += _("You may use your current login (%s) and password to view them.\n") % result_line.login
body += _("You may use your current login (%s) and password to view them.\n") % result_line.user_id.login
body += "\n\n"
body += user.signature
body += "\n\n"
@ -717,15 +727,16 @@ class share_wizard(osv.osv_memory):
if tools.email_send(user.user_email, [email_to], subject, body):
emails_sent += 1
else:
self.__logger.warning('Failed to send share notification from %s to %s, ignored', user.user_email, email_to)
self.__logger.info('%s share notification(s) successfully sent.', emails_sent)
self._logger.warning('Failed to send share notification from %s to %s, ignored', user.user_email, email_to)
self._logger.info('%s share notification(s) successfully sent.', emails_sent)
share_wizard()
class share_result_line(osv.osv_memory):
_name = 'share.wizard.result.line'
_rec_name = 'login'
_rec_name = 'user_id'
_columns = {
'login': fields.char('Username', size=64, required=True, readonly=True),
'user_id': fields.many2one('res.users', required=True, readonly=True),
'login': fields.related('user_id', 'login', string='Login', type='char', size=64, required=True, readonly=True),
'password': fields.char('Password', size=64, readonly=True),
'share_url': fields.char('Share URL', size=512, required=True),
'share_wizard_id': fields.many2one('share.wizard', 'Share Wizard', required=True),

View File

@ -31,7 +31,7 @@
<form string="Grant instant access to your documents">
<group colspan="4" name="emails_group" attrs="{'invisible':[('user_type','!=','emails')]}">
<separator colspan="4" string="Share with these people (one e-mail per line)"/>
<field colspan="4" nolabel="1" name="new_users"/>
<field colspan="4" nolabel="1" name="new_users" attrs="{'required':[('user_type','=','emails')]}"/>
</group>
<separator colspan="4" string="Optional: include a personal message"/>
<group colspan="4" col="4">