diff --git a/ee/app/models/authz/custom_ability.rb b/ee/app/models/authz/custom_ability.rb index 0d2fe6e1be4ac2a02ad1ef84aec46aa3f0dce4ad..35b0c31f130f62bd52611d56fc7d301db78bab7e 100644 --- a/ee/app/models/authz/custom_ability.rb +++ b/ee/app/models/authz/custom_ability.rb @@ -52,17 +52,11 @@ def custom_roles_enabled?(resource) end def abilities_for_projects(user, projects) - ::Preloaders::UserMemberRolesInProjectsPreloader.new( - projects: projects, - user: user - ).execute + ::Authz::Project.new(user, scope: projects).permitted end def abilities_for_groups(user, groups) - ::Preloaders::UserMemberRolesInGroupsPreloader.new( - groups: groups, - user: user - ).execute + ::Authz::Group.new(user, scope: groups).permitted end def abilities_for(user, resource) diff --git a/ee/app/models/authz/group.rb b/ee/app/models/authz/group.rb new file mode 100644 index 0000000000000000000000000000000000000000..24febf02afd0c14efe6b09c8fd00375b0b0e1cc0 --- /dev/null +++ b/ee/app/models/authz/group.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Authz + class Group < Resource + def initialize(user, scope: user.authorized_groups) + super(user, scope) + end + + def permitted + ::Preloaders::UserMemberRolesInGroupsPreloader + .new(groups: scope, user: user) + .execute + end + end +end diff --git a/ee/app/models/authz/project.rb b/ee/app/models/authz/project.rb new file mode 100644 index 0000000000000000000000000000000000000000..5dd55f6cf361226e7f73c05b6dccb99096c69395 --- /dev/null +++ b/ee/app/models/authz/project.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Authz + class Project < Resource + def initialize(user, scope: user.authorized_projects) + super(user, scope) + end + + def permitted + ::Preloaders::UserMemberRolesInProjectsPreloader + .new(projects: scope, user: user) + .execute + end + end +end diff --git a/ee/app/models/authz/resource.rb b/ee/app/models/authz/resource.rb new file mode 100644 index 0000000000000000000000000000000000000000..d852a349a0f44f1b54e1adb66159f948b6c4c680 --- /dev/null +++ b/ee/app/models/authz/resource.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Authz + class Resource + attr_reader :user, :scope + + def initialize(user, scope) + @user = user + @scope = scope + end + + def permitted + raise NotImplementedError + end + + def permitted_to(permission, to_relation: true) + ids = permitted.filter_map do |id, permissions| + id if permission.in?(permissions || []) + end + to_relation ? scope.where(id: ids) : ids + end + end +end diff --git a/ee/app/models/ee/user.rb b/ee/app/models/ee/user.rb index 58217bf960213cc627ddfbe479609075a9e74902..fb9418cfa3957da15937e832d677dd2a8a5109c9 100644 --- a/ee/app/models/ee/user.rb +++ b/ee/app/models/ee/user.rb @@ -723,6 +723,23 @@ def contributed_epic_groups ::Group.where(id: contributed_group_ids).not_aimed_for_deletion end + override :ci_owned_runners + def ci_owned_runners + ::Ci::Runner.from_union([ + super, + ::Ci::Runner.belonging_to_group(groups_permitted_to(:admin_runners)), + ::Ci::Runner.belonging_to_project(projects_permitted_to(:admin_runners)) + ]) + end + + def groups_permitted_to(permission) + ::Authz::Group.new(self).permitted_to(permission, to_relation: false) + end + + def projects_permitted_to(permission) + ::Authz::Project.new(self).permitted_to(permission, to_relation: false) + end + protected override :password_required? diff --git a/ee/spec/factories/member_roles.rb b/ee/spec/factories/member_roles.rb index 8b0829566c4d04b3b770112bbe918cb89509c6f4..7f7b56fe76f5639fb36c75f98e909f418be09c8b 100644 --- a/ee/spec/factories/member_roles.rb +++ b/ee/spec/factories/member_roles.rb @@ -7,12 +7,12 @@ read_code { true } name { generate(:title) } - trait(:developer) { base_access_level { Gitlab::Access::DEVELOPER } } - trait(:maintainer) { base_access_level { Gitlab::Access::MAINTAINER } } - trait(:reporter) { base_access_level { Gitlab::Access::REPORTER } } - trait(:guest) { base_access_level { Gitlab::Access::GUEST } } trait(:minimal_access) { base_access_level { Gitlab::Access::MINIMAL_ACCESS } } + ::Gitlab::Access.sym_options_with_owner.each do |role, value| + trait(role) { base_access_level { value } } + end + Gitlab::CustomRoles::Definition.all.each_value do |attributes| trait attributes[:name].to_sym do send(attributes[:name].to_sym) { true } diff --git a/ee/spec/models/authz/group_spec.rb b/ee/spec/models/authz/group_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..444cd607e2861e573e57fc69f759910c7cb7ac3f --- /dev/null +++ b/ee/spec/models/authz/group_spec.rb @@ -0,0 +1,146 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Authz::Group, feature_category: :system_access do + subject(:group_authorization) { scope ? described_class.new(user, scope: scope) : described_class.new(user) } + + let(:scope) { nil } + + let_it_be(:user, reload: true) { create(:user) } + let_it_be(:root_group) { create(:group) } + let_it_be(:group) { create(:group, parent: root_group) } + let_it_be(:child_group) { create(:group, parent: group) } + + let_it_be(:admin_runners_role) { create(:member_role, :guest, :admin_runners, namespace: root_group) } + let_it_be(:admin_vulnerability_role) { create(:member_role, :guest, :admin_vulnerability, namespace: root_group) } + let_it_be(:read_dependency_role) { create(:member_role, :guest, :read_dependency, namespace: root_group) } + + before do + stub_licensed_features(custom_roles: true) + end + + describe "#permitted" do + subject(:permitted) { group_authorization.permitted } + + context 'when authorized for different permissions at different levels in the group hierarchy' do + let_it_be(:memberships) do + [ + [admin_runners_role, root_group], + [admin_vulnerability_role, group], + [read_dependency_role, child_group] + ] + end + + before_all do + memberships.each do |(role, group)| + create(:group_member, :guest, member_role: role, user: user, source: group) + end + end + + it { is_expected.to include(root_group.id => match_array([:admin_runners])) } + + it do + permissions = [:admin_runners, :admin_vulnerability, :read_vulnerability] + is_expected.to include(group.id => match_array(permissions)) + end + + it do + is_expected.to include(child_group.id => match_array([ + :admin_runners, + :admin_vulnerability, + :read_dependency, + :read_vulnerability + ])) + end + end + end + + describe "#permitted_to" do + subject(:permitted_to) { group_authorization.permitted_to(custom_ability) } + + let(:custom_ability) { :admin_runners } + + it { is_expected.to be_empty } + + context 'when authorized for a project' do + let_it_be(:project) { create(:project, group: group) } + + before_all do + create(:project_member, :guest, member_role: admin_runners_role, user: user, source: project) + end + + it { is_expected.to be_empty } + end + + context 'when authorized for root group' do + before_all do + create(:group_member, :guest, member_role: admin_runners_role, user: user, source: root_group) + end + + it { is_expected.to match_array([root_group, group, child_group]) } + end + + context 'when authorized for group' do + before_all do + create(:group_member, :guest, member_role: admin_runners_role, user: user, source: group) + end + + it { is_expected.to match_array([group, child_group]) } + end + + context 'when authorized for child group' do + before_all do + create(:group_member, :guest, member_role: admin_runners_role, user: user, source: child_group) + end + + it { is_expected.to match_array([child_group]) } + end + + context 'when authorized for different permissions at different levels in the group hierarchy' do + let_it_be(:memberships) do + [ + [admin_runners_role, root_group], + [admin_vulnerability_role, group], + [read_dependency_role, child_group] + ] + end + + before_all do + memberships.each do |(role, group)| + create(:group_member, :guest, member_role: role, user: user, source: group) + end + end + + describe ":admin_runners" do + let(:custom_ability) { :admin_runners } + + it { is_expected.to match_array([root_group, group, child_group]) } + + context 'when overriding the default scope' do + let(:scope) { ::Group.where(id: [group.id]) } + + it { is_expected.to match_array([group]) } + end + end + + describe ":admin_vulnerability" do + let(:custom_ability) { :admin_vulnerability } + + it { is_expected.to match_array([group, child_group]) } + end + + describe ":read_dependency" do + let(:custom_ability) { :read_dependency } + + it { is_expected.to match_array([child_group]) } + + context 'when overriding the default scope' do + let(:scope) { ::Group.where(id: [group.id]) } + + it { is_expected.to be_empty } + end + end + end + end +end diff --git a/ee/spec/models/authz/project_spec.rb b/ee/spec/models/authz/project_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..1650a3d21744ffc057e51b7b6d70266f41fe4858 --- /dev/null +++ b/ee/spec/models/authz/project_spec.rb @@ -0,0 +1,183 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Authz::Project, feature_category: :system_access do + subject(:project_authorization) { scope ? described_class.new(user, scope: scope) : described_class.new(user) } + + let(:scope) { nil } + + let_it_be(:user, reload: true) { create(:user) } + let_it_be(:root_group) { create(:group) } + let_it_be(:group) { create(:group, parent: root_group) } + let_it_be(:child_group) { create(:group, parent: group) } + + let_it_be(:root_project) { create(:project, group: root_group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:child_project) { create(:project, group: child_group) } + + let_it_be(:admin_runners_role) { create(:member_role, :guest, :admin_runners, namespace: root_group) } + let_it_be(:admin_vulnerability_role) { create(:member_role, :guest, :admin_vulnerability, namespace: root_group) } + let_it_be(:archive_project_role) { create(:member_role, :owner, :archive_project, namespace: root_group) } + let_it_be(:read_dependency_role) { create(:member_role, :guest, :read_dependency, namespace: root_group) } + + before do + stub_licensed_features(custom_roles: true) + end + + describe "#permitted" do + subject(:permitted) { project_authorization.permitted } + + context 'when authorized for different permissions at different levels in the group hierarchy' do + let_it_be(:memberships) do + [ + [admin_runners_role, root_group], + [admin_vulnerability_role, group], + [read_dependency_role, child_group], + [archive_project_role, child_project] + ] + end + + before_all do + memberships.each do |(role, source)| + if source.is_a?(::Group) + create(:group_member, :guest, member_role: role, user: user, source: source) + else + create(:project_member, :guest, member_role: role, user: user, source: source) + end + end + end + + it { is_expected.to include(root_project.id => include(:admin_runners)) } + it { is_expected.to include(project.id => include(:admin_runners, :admin_vulnerability)) } + + it do + is_expected.to include(child_project.id => include( + :admin_runners, + :admin_vulnerability, + :read_dependency, + :archive_project + )) + end + end + end + + describe "#permitted_to" do + subject(:permitted_to) { project_authorization.permitted_to(custom_ability) } + + let(:custom_ability) { :admin_runners } + + it { is_expected.to be_empty } + + context 'when authorized for a project' do + let_it_be(:project) { create(:project, group: group) } + + before_all do + create(:project_member, :guest, member_role: admin_runners_role, user: user, source: project) + end + + it { is_expected.to match_array([project]) } + end + + context 'when authorized for root group' do + before_all do + create(:group_member, :guest, member_role: admin_runners_role, user: user, source: root_group) + end + + it { is_expected.to match_array([root_project, project, child_project]) } + end + + context 'when authorized for group' do + before_all do + create(:group_member, :guest, member_role: admin_runners_role, user: user, source: group) + end + + it { is_expected.to match_array([project, child_project]) } + end + + context 'when authorized for child group' do + before_all do + create(:group_member, :guest, member_role: admin_runners_role, user: user, source: child_group) + end + + it { is_expected.to match_array([child_project]) } + end + + context 'when authorized for project' do + before_all do + create(:project_member, :guest, member_role: admin_runners_role, user: user, source: project) + end + + it { is_expected.to match_array([project]) } + end + + context 'when authorized for different permissions at different levels in the group hierarchy' do + let_it_be(:memberships) do + [ + [admin_runners_role, root_group], + [admin_vulnerability_role, group], + [read_dependency_role, child_group], + [archive_project_role, child_project] + ] + end + + before_all do + memberships.each do |(role, source)| + if source.is_a?(::Group) + create(:group_member, :guest, member_role: role, user: user, source: source) + else + create(:project_member, :guest, member_role: role, user: user, source: source) + end + end + end + + describe ":admin_runners" do + let(:custom_ability) { :admin_runners } + + it { is_expected.to match_array([root_project, project, child_project]) } + + context 'when overriding the default scope' do + let(:scope) { ::Project.where(id: [project.id]) } + + it { is_expected.to match_array([project]) } + end + end + + describe ":admin_vulnerability" do + let(:custom_ability) { :admin_vulnerability } + + it { is_expected.to match_array([project, child_project]) } + + describe ":read_vulnerability" do + let(:custom_ability) { :read_vulnerability } + + it { is_expected.to match_array([project, child_project]) } + end + end + + describe ":read_dependency" do + let(:custom_ability) { :read_dependency } + + it { is_expected.to match_array([child_project]) } + + context 'when overriding the default scope' do + let(:scope) { ::Project.where(id: [project.id]) } + + it { is_expected.to be_empty } + end + end + + describe ":archive_project" do + let(:custom_ability) { :archive_project } + + it { is_expected.to match_array([child_project]) } + + context 'when overriding the default scope' do + let(:scope) { ::Project.where(id: [child_project.id]) } + + it { is_expected.to match_array([child_project]) } + end + end + end + end +end diff --git a/ee/spec/models/authz/resource_spec.rb b/ee/spec/models/authz/resource_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..e5ac92c92115eef5d45f0466088160af8b99b05d --- /dev/null +++ b/ee/spec/models/authz/resource_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Authz::Resource, feature_category: :system_access do + subject(:resource_authorization) { described_class.new(user, scope) } + + let(:user) { build_stubbed(:user) } + let(:scope) { Group.none } + + describe "#permitted" do + subject(:permitted) { resource_authorization.permitted } + + it { expect { permitted }.to raise_error(NotImplementedError) } + end +end diff --git a/ee/spec/models/ee/user_spec.rb b/ee/spec/models/ee/user_spec.rb index 218bf8e3da8cc7315b68ea85e36a830ac4c0440e..71d4510d0e434ebb80aa66dff888b1f625104292 100644 --- a/ee/spec/models/ee/user_spec.rb +++ b/ee/spec/models/ee/user_spec.rb @@ -4210,4 +4210,42 @@ it { is_expected.to be_truthy } end end + + describe '#ci_owned_runners' do + subject(:runners) { user.ci_owned_runners } + + let_it_be(:user, reload: true) { create(:user) } + let_it_be(:group) { create(:group, name: "group") } + let_it_be(:subgroup) { create(:group, parent: group, name: "subgroup") } + let_it_be(:project) { create(:project, group: subgroup, name: "project") } + let_it_be(:role) { create(:member_role, :guest, :admin_runners, namespace: group) } + + let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group]) } + let_it_be(:subgroup_runner) { create(:ci_runner, :group, groups: [subgroup]) } + let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project]) } + + before do + stub_licensed_features(custom_roles: true) + end + + it { expect(runners).to be_empty } + + context 'when the user has the `admin_runners` permission via a custom role on a root group' do + let_it_be(:membership) { create(:group_member, :guest, member_role: role, user: user, source: group) } + + it { expect(runners).to match_array([group_runner, subgroup_runner, project_runner]) } + end + + context 'when the user has the `admin_runners` permission via a custom role on a sub group' do + let_it_be(:membership) { create(:group_member, :guest, member_role: role, user: user, source: subgroup) } + + it { expect(runners).to match_array([subgroup_runner, project_runner]) } + end + + context 'when the user has the `admin_runners` permission via a custom role on a project' do + let_it_be(:membership) { create(:project_member, :guest, member_role: role, user: user, source: project) } + + it { expect(runners).to match_array([project_runner]) } + end + end end diff --git a/ee/spec/requests/custom_roles/admin_runners/request_spec.rb b/ee/spec/requests/custom_roles/admin_runners/request_spec.rb index 97c2dd77f8997d91977530d123754df020bec945..b76cb0beb8a14cf625ee21f0886857cfcd5c922b 100644 --- a/ee/spec/requests/custom_roles/admin_runners/request_spec.rb +++ b/ee/spec/requests/custom_roles/admin_runners/request_spec.rb @@ -193,7 +193,7 @@ let_it_be(:membership) { create(:group_member, :guest, member_role: role, user: user, source: group) } let_it_be(:project_runner) { create(:ci_runner, :project, description: 'Project runner', projects: [project]) } - pending "GET /runners" do + it "GET /runners" do get api("/runners", user) expect(response).to have_gitlab_http_status(:ok)