diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index 1108a64c59e7e06e2c9984939a4c290941e78d15..2b93aa30c0f2ef5a645cba06d6cbba142d9dab14 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -60,6 +60,21 @@ module Routable joins(:route).where(wheres.join(' OR ')) end end + + # Builds a relation to find multiple objects that are nested under user membership + # + # Usage: + # + # Klass.member_descendants(1) + # + # Returns an ActiveRecord::Relation. + def member_descendants(user_id) + joins(:route). + joins("INNER JOIN routes r2 ON routes.path LIKE CONCAT(r2.path, '/%') + INNER JOIN members ON members.source_id = r2.source_id + AND members.source_type = r2.source_type"). + where('members.user_id = ?', user_id) + end end private diff --git a/app/models/namespace.rb b/app/models/namespace.rb index d3a4ddbb3bfdb0b59e8f8d866013648c2e300ece..eb970dddd911f437fb2ad3162d080d10283e15b2 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -194,7 +194,7 @@ class Namespace < ActiveRecord::Base paths << path end - self.class.joins(:route).where('routes.path IN (?)', paths).order_id_asc + self.class.joins(:route).where('routes.path IN (?)', paths).reorder('routes.path ASC') else self.class.none end @@ -202,7 +202,7 @@ class Namespace < ActiveRecord::Base # Scopes the model on direct and indirect children of the record def descendants - self.class.joins(:route).where('routes.path LIKE ?', "#{route.path}/%").order_id_asc + self.class.joins(:route).where('routes.path LIKE ?', "#{route.path}/%").reorder('routes.path ASC') end private diff --git a/app/models/route.rb b/app/models/route.rb index ebd18dce737521b6c0fe58f534f2fd135e498ee9..dd171fdb06934fd9fb5cf4b04ae61baf1322a6be 100644 --- a/app/models/route.rb +++ b/app/models/route.rb @@ -15,8 +15,9 @@ class Route < ActiveRecord::Base # rubocop:disable Rails/FindEach Route.where('path LIKE ?', "#{path_was}/%").each do |route| # Note that update column skips validation and callbacks. - # We need this to avoid recursive call of rename_children method + # We need this to avoid recursive call of rename_descendants method route.update_column(:path, route.path.sub(path_was, path)) end + # rubocop:enable Rails/FindEach end end diff --git a/app/models/user.rb b/app/models/user.rb index 06dd98a318858f85a3a845a73874efa0b99cd3fa..f294b9f77c1dbe3935787e0a0570fe787058b7a0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -439,6 +439,15 @@ class User < ActiveRecord::Base Group.where("namespaces.id IN (#{union.to_sql})") end + def nested_groups + Group.member_descendants(id) + end + + def nested_projects + Project.joins(:namespace).where('namespaces.parent_id IS NOT NULL'). + member_descendants(id) + end + def refresh_authorized_projects Users::RefreshAuthorizedProjectsService.new(self).execute end diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb index 21ec1bd9e6554cffa24b29a6c943df384c5ec048..84d3c1c4373d7e34d477509e1901f0db05b819d2 100644 --- a/app/services/users/refresh_authorized_projects_service.rb +++ b/app/services/users/refresh_authorized_projects_service.rb @@ -119,7 +119,8 @@ module Users user.personal_projects.select("#{user.id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"), user.groups_projects.select_for_project_authorization, user.projects.select_for_project_authorization, - user.groups.joins(:shared_projects).select_for_project_authorization + user.groups.joins(:shared_projects).select_for_project_authorization, + user.nested_projects.select_for_project_authorization ] Gitlab::SQL::Union.new(relations) diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb index b556135532f3b725e21d3531ff71a157a1b19e75..30443534ccaf6baaa6c751c2f19cb807d38428d6 100644 --- a/spec/models/concerns/routable_spec.rb +++ b/spec/models/concerns/routable_spec.rb @@ -68,4 +68,14 @@ describe Group, 'Routable' do end end end + + describe '.member_descendants' do + let!(:user) { create(:user) } + let!(:nested_group) { create(:group, parent: group) } + + before { group.add_owner(user) } + subject { described_class.member_descendants(user.id) } + + it { is_expected.to eq([nested_group]) } + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 8b20ee816140831c2dbd75bf4793412073722643..f2ed2d45dcad620eb50889b7961b2f65a4257e13 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1356,6 +1356,39 @@ describe User, models: true do end end + describe '#nested_groups' do + let!(:user) { create(:user) } + let!(:group) { create(:group) } + let!(:nested_group) { create(:group, parent: group) } + + before do + group.add_owner(user) + + # Add more data to ensure method does not include wrong groups + create(:group).add_owner(create(:user)) + end + + it { expect(user.nested_groups).to eq([nested_group]) } + end + + describe '#nested_projects' do + let!(:user) { create(:user) } + let!(:group) { create(:group) } + let!(:nested_group) { create(:group, parent: group) } + let!(:project) { create(:project, namespace: group) } + let!(:nested_project) { create(:project, namespace: nested_group) } + + before do + group.add_owner(user) + + # Add more data to ensure method does not include wrong projects + other_project = create(:project, namespace: create(:group, :nested)) + other_project.add_developer(create(:user)) + end + + it { expect(user.nested_projects).to eq([nested_project]) } + end + describe '#refresh_authorized_projects', redis: true do let(:project1) { create(:empty_project) } let(:project2) { create(:empty_project) }