diff --git a/app/models/group.rb b/app/models/group.rb index 300b59b46..b6bf345f3 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -72,18 +72,45 @@ class Group < Principal end def user_added(user) - members.preload(:member_roles).each do |member| - next if member.project_id.nil? - # skip if the group is a member without roles in the project - next if member.member_roles.empty? - - user_member = - Member.find_or_initialize_by(:project_id => member.project_id, :user_id => user.id) - user_member.member_roles << - member.member_roles.pluck(:id, :role_id).map do |id, role_id| - MemberRole.new(:role_id => role_id, :inherited_from => id) - end - user_member.save! + group_members = members.preload(:member_roles).select do |m| + m.project_id.present? && m.member_roles.any? + end + return if group_members.empty? + + direct_project_ids = group_members.map(&:project_id) + inheriting_children = collect_inheriting_descendants(direct_project_ids) + all_project_ids = direct_project_ids + inheriting_children.values.flatten + + existing_members = Member.where(:user_id => user.id, :project_id => all_project_ids). + preload(:member_roles).index_by(&:project_id) + existing_role_keys = Set.new + existing_members.each do |project_id, m| + m.member_roles.each do |mr| + existing_role_keys << [project_id, mr.inherited_from] if mr.inherited_from + end + end + + group_members.each do |member| + group_role_data = member.member_roles.map { |mr| [mr.id, mr.role_id] } + + user_member = existing_members[member.project_id] || + Member.new(:project_id => member.project_id, :user_id => user.id) + new_roles = group_role_data.filter_map do |mr_id, role_id| + next if existing_role_keys.include?([member.project_id, mr_id]) + + build_inherited_member_role(role_id, mr_id) + end + unless new_roles.empty? + user_member.member_roles << new_roles + user_member.save! + existing_members[member.project_id] = user_member + new_roles.each { |mr| existing_role_keys << [member.project_id, mr.inherited_from] } + end + + propagate_member_roles_to_descendants( + user, new_roles, member.project_id, + inheriting_children, existing_members, existing_role_keys + ) end end @@ -112,6 +139,56 @@ class Group < Principal private + def build_inherited_member_role(role_id, inherited_from) + mr = MemberRole.new(:role_id => role_id, :inherited_from => inherited_from) + mr.skip_subproject_propagation = true + mr + end + + def collect_inheriting_descendants(root_project_ids) + result = {} + current_ids = root_project_ids + while current_ids.any? + children = Project.where(:parent_id => current_ids, :inherit_members => true).pluck(:id, :parent_id) + break if children.empty? + + children.each do |child_id, parent_id| + (result[parent_id] ||= []) << child_id + end + current_ids = children.map(&:first) + end + result + end + + def propagate_member_roles_to_descendants(user, source_member_roles, parent_project_id, + inheriting_children, existing_members, existing_role_keys) + return if source_member_roles.empty? + + child_project_ids = inheriting_children[parent_project_id] + return unless child_project_ids + + child_project_ids.each do |child_project_id| + child_member = existing_members[child_project_id] || + Member.new(:project_id => child_project_id, :user_id => user.id) + child_roles = source_member_roles.filter_map do |source_mr| + next if existing_role_keys.include?([child_project_id, source_mr.id]) + + build_inherited_member_role(source_mr.role_id, source_mr.id) + end + unless child_roles.empty? + child_member.member_roles << child_roles + child_member.save! + existing_members[child_project_id] = child_member + child_roles.each { |mr| existing_role_keys << [child_project_id, mr.inherited_from] } + end + + propagate_member_roles_to_descendants( + user, child_roles, child_project_id, + inheriting_children, existing_members, existing_role_keys + ) + end + end + # Removes references that are not handled by associations def remove_references_before_destroy return if self.id.nil? diff --git a/app/models/member_role.rb b/app/models/member_role.rb index 975c6f2b0..b8edcaea4 100644 --- a/app/models/member_role.rb +++ b/app/models/member_role.rb @@ -21,6 +21,8 @@ class MemberRole < ApplicationRecord belongs_to :member belongs_to :role + attr_accessor :skip_subproject_propagation + after_create :add_role_to_group_users, :add_role_to_subprojects after_destroy :remove_member_if_empty @@ -68,6 +70,7 @@ class MemberRole < ApplicationRecord end def add_role_to_subprojects + return if skip_subproject_propagation return if member.project.leaf? member.project.children.where(:inherit_members => true).ids.each do |subproject_id| diff --git a/test/unit/group_test.rb b/test/unit/group_test.rb index 1fd836ca3..28916f67c 100644 --- a/test/unit/group_test.rb +++ b/test/unit/group_test.rb @@ -183,4 +183,103 @@ class GroupTest < ActiveSupport::TestCase assert_nothing_raised {group.users << user} assert group.users.include?(user) end + + def test_user_added_should_propagate_roles_to_subprojects + group = Group.find(11) + user = User.find(9) + parent = Project.generate! + child = Project.generate_with_parent!(parent, :inherit_members => true) + + Member.create!(:principal => group, :project => parent, :role_ids => [1, 2]) + group.users << user + + # User should be a member of both parent and child + assert user.member_of?(parent) + assert user.member_of?(child) + + # User's roles in child should match those in parent + parent_user_member = Member.find_by(:project_id => parent.id, :user_id => user.id) + child_user_member = Member.find_by(:project_id => child.id, :user_id => user.id) + assert_not_nil child_user_member + assert_equal parent_user_member.roles.sort, child_user_member.roles.sort + + # Child user member roles should be inherited from parent user member roles + parent_user_mr_ids = parent_user_member.member_roles.select(&:inherited?).map(&:id).sort + child_user_inherited_from = child_user_member.member_roles.select { |mr| + parent_user_mr_ids.include?(mr.inherited_from) + } + assert_equal parent_user_mr_ids.size, child_user_inherited_from.size + end + + def test_user_added_should_propagate_roles_to_grandchild_projects + group = Group.find(11) + user = User.find(9) + grandparent = Project.generate! + parent = Project.generate_with_parent!(grandparent, :inherit_members => true) + child = Project.generate_with_parent!(parent, :inherit_members => true) + + Member.create!(:principal => group, :project => grandparent, :role_ids => [1, 2]) + group.users << user + + # User should be a member of all three levels + assert user.member_of?(grandparent) + assert user.member_of?(parent) + assert user.member_of?(child) + + # Verify inherited_from chain: child -> parent -> grandparent + gp_user_member = Member.find_by(:project_id => grandparent.id, :user_id => user.id) + p_user_member = Member.find_by(:project_id => parent.id, :user_id => user.id) + c_user_member = Member.find_by(:project_id => child.id, :user_id => user.id) + + assert_equal gp_user_member.roles.sort, p_user_member.roles.sort + assert_equal p_user_member.roles.sort, c_user_member.roles.sort + + # Parent's inherited MemberRoles should point to grandparent's user MemberRoles + gp_user_mr_ids = gp_user_member.member_roles.select(&:inherited?).map(&:id).sort + p_inherited_from = p_user_member.member_roles.select { |mr| + gp_user_mr_ids.include?(mr.inherited_from) + } + assert_equal gp_user_mr_ids.size, p_inherited_from.size + + # Child's inherited MemberRoles should point to parent's user MemberRoles + p_user_mr_ids = p_user_member.member_roles.select(&:inherited?).map(&:id).sort + c_inherited_from = c_user_member.member_roles.select { |mr| + p_user_mr_ids.include?(mr.inherited_from) + } + assert_equal p_user_mr_ids.size, c_inherited_from.size + end + + def test_user_added_should_propagate_roles_to_sibling_subprojects + group = Group.find(11) + user = User.find(9) + parent = Project.generate! + child1 = Project.generate_with_parent!(parent, :inherit_members => true) + child2 = Project.generate_with_parent!(parent, :inherit_members => true) + + Member.create!(:principal => group, :project => parent, :role_ids => [1, 2]) + group.users << user + + # User should be a member of parent and both siblings + assert user.member_of?(parent) + assert user.member_of?(child1) + assert user.member_of?(child2) + + # Both siblings should have the same roles as parent + parent_user_member = Member.find_by(:project_id => parent.id, :user_id => user.id) + child1_user_member = Member.find_by(:project_id => child1.id, :user_id => user.id) + child2_user_member = Member.find_by(:project_id => child2.id, :user_id => user.id) + assert_not_nil child1_user_member + assert_not_nil child2_user_member + assert_equal parent_user_member.roles.sort, child1_user_member.roles.sort + assert_equal parent_user_member.roles.sort, child2_user_member.roles.sort + + # Both siblings' member_roles should be inherited from parent's member_roles + parent_user_mr_ids = parent_user_member.member_roles.select(&:inherited?).map(&:id).sort + [child1_user_member, child2_user_member].each do |child_member| + child_inherited_from = child_member.member_roles.select { |mr| + parent_user_mr_ids.include?(mr.inherited_from) + } + assert_equal parent_user_mr_ids.size, child_inherited_from.size + end + end end