diff --git a/app/graphql/resolvers/organizations/projects_resolver.rb b/app/graphql/resolvers/organizations/projects_resolver.rb index b053801d31e676c89e02dd964b3f51295f948956..a4a930acd3886c0bebc4361da7d055a7b5b5b87f 100644 --- a/app/graphql/resolvers/organizations/projects_resolver.rb +++ b/app/graphql/resolvers/organizations/projects_resolver.rb @@ -19,3 +19,5 @@ def finder_params(args) end end end + +Resolvers::Organizations::ProjectsResolver.prepend_mod diff --git a/app/models/project.rb b/app/models/project.rb index d7719e72904a84af5770259529cc22255155a466..b966ef8d246db3e94e81af60f23d41ad0c91be9e 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -646,6 +646,7 @@ def self.integration_association_name(name) scope :without_deleted, -> { where(pending_delete: false) } scope :not_hidden, -> { where(hidden: false) } scope :not_in_groups, ->(groups) { where.not(group: groups) } + scope :by_not_in_root_id, ->(root_id) { joins(:project_namespace).where('namespaces.traversal_ids[1] NOT IN (?)', root_id) } scope :not_aimed_for_deletion, -> { where(marked_for_deletion_at: nil).without_deleted } scope :with_storage_feature, ->(feature) do diff --git a/ee/app/finders/ee/projects_finder.rb b/ee/app/finders/ee/projects_finder.rb index 847cae6134cd894e9072f4c3196a9d32f118bbad..d2ce601b5f94d2e9f677730bcdd54f43cbf2d7f1 100644 --- a/ee/app/finders/ee/projects_finder.rb +++ b/ee/app/finders/ee/projects_finder.rb @@ -11,6 +11,7 @@ module EE # feature_available: string[] # aimed_for_deletion: Symbol # include_hidden: boolean + # filter_expired_saml_session_projects: boolean module ProjectsFinder extend ::Gitlab::Utils::Override @@ -23,9 +24,22 @@ def filter_projects(collection) collection = by_feature_available(collection) collection = by_hidden(collection) collection = by_marked_for_deletion_on(collection) + collection = by_saml_sso_session(collection) by_aimed_for_deletion(collection) end + def by_saml_sso_session(projects) + return projects unless filter_expired_saml_session_projects? + + projects.by_not_in_root_id(current_user.expired_sso_session_saml_providers.select(:group_id)) + end + + def filter_expired_saml_session_projects? + return false if current_user.nil? || current_user.can_read_all_resources? + + params.fetch(:filter_expired_saml_session_projects, false) + end + def by_plans(collection) if names = params[:plans].presence collection.for_plan_name(names) diff --git a/ee/app/graphql/ee/resolvers/organizations/projects_resolver.rb b/ee/app/graphql/ee/resolvers/organizations/projects_resolver.rb new file mode 100644 index 0000000000000000000000000000000000000000..046055e5f1dfe2fe076d25bc19d95245c026d891 --- /dev/null +++ b/ee/app/graphql/ee/resolvers/organizations/projects_resolver.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module EE + module Resolvers + module Organizations + module ProjectsResolver + extend ::Gitlab::Utils::Override + + override :finder_params + def finder_params(args) + super.merge(filter_expired_saml_session_projects: true) + end + end + end + end +end diff --git a/ee/spec/finders/ee/projects_finder_spec.rb b/ee/spec/finders/ee/projects_finder_spec.rb index 5a9e67907086b50fc286111677ec5525893efdc3..1836a4e76bf38636f1c4ecf7d3582638a6a62c64 100644 --- a/ee/spec/finders/ee/projects_finder_spec.rb +++ b/ee/spec/finders/ee/projects_finder_spec.rb @@ -65,6 +65,78 @@ it { is_expected.to contain_exactly(ultimate_project, ultimate_project2, premium_project, no_plan_project) } end + context 'filter by SAML SSO session' do + let(:params) { { filter_expired_saml_session_projects: true } } + let(:finder) { described_class.new(current_user: current_user, params: params) } + + let_it_be(:current_user) { user } + + let_it_be(:root_group1) do + create(:group, saml_provider: create(:saml_provider), developers: current_user) do |group| + create_saml_identity(group, current_user) + end + end + + let_it_be(:root_group2) do + create(:group, saml_provider: create(:saml_provider)) + end + + let_it_be(:private_root_group) do + create(:group, :private, saml_provider: create(:saml_provider), developers: current_user) do |group| + create_saml_identity(group, current_user) + end + end + + let_it_be(:project1) { create(:project, :public, group: root_group1) } + let_it_be(:project2) { create(:project, :public, group: root_group2) } + let_it_be(:private_project) { create(:project, :private, group: private_root_group) } + let_it_be(:all_projects) { [project1, project2, private_project] } + + subject(:projects) { finder.execute.id_in(all_projects).to_a } + + context 'when the current user is nil' do + let_it_be(:current_user) { nil } + + it 'includes public SAML projects' do + expect(projects).to contain_exactly(project1, project2) + end + end + + shared_examples 'includes all SAML projects' do + specify do + expect(projects).to match_array(all_projects) + end + end + + context 'when the current user is an admin', :enable_admin_mode do + let_it_be(:current_user) { create(:admin) } + + it_behaves_like 'includes all SAML projects' + end + + context 'when the current user has no active SAML sessions' do + it 'filters out the SAML member projects' do + expect(projects).to contain_exactly(project2) + end + end + + context 'when filter_expired_saml_session_projects param is false' do + let(:params) { { filter_expired_saml_session_projects: false } } + + it_behaves_like 'includes all SAML projects' + end + + context 'when the current user has active SAML sessions' do + before do + active_saml_sessions = { root_group1.saml_provider.id => Time.current, + private_root_group.saml_provider.id => Time.current } + allow(::Gitlab::Auth::GroupSaml::SsoState).to receive(:active_saml_sessions).and_return(active_saml_sessions) + end + + it_behaves_like 'includes all SAML projects' + end + end + context 'filter by hidden' do let_it_be(:hidden_project) { create(:project, :public, :hidden) } @@ -119,6 +191,10 @@ private + def create_saml_identity(group, current_user) + create(:group_saml_identity, saml_provider: group.saml_provider, user: current_user) + end + def create_project(plan, visibility = :public) create(:project, visibility, namespace: create(:group_with_plan, plan: plan)) end diff --git a/ee/spec/requests/ee/api/graphql/organizations/organization_query_spec.rb b/ee/spec/requests/ee/api/graphql/organizations/organization_query_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..5fda0885b4ae56dda9687ac77d938ce82d4b4af7 --- /dev/null +++ b/ee/spec/requests/ee/api/graphql/organizations/organization_query_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting organization information', feature_category: :cell do + include ::GraphqlHelpers + + let(:query) { graphql_query_for(:organization, { id: organization.to_global_id }, organization_fields) } + let(:current_user) { user } + + let_it_be(:organization_owner) { create(:organization_owner) } + let_it_be(:organization) { organization_owner.organization } + let_it_be(:user) { organization_owner.user } + let_it_be(:project) { create(:project, organization: organization, developers: user) } + + let_it_be(:saml_group) do + saml_provider = create(:saml_provider) + create(:group, organization: organization, saml_provider: saml_provider, developers: user) do |group| + create(:group_saml_identity, saml_provider: group.saml_provider, user: user) + end + end + + let_it_be(:saml_project) { create(:project, group: saml_group, organization: organization, developers: user) } + + subject(:request_organization) { post_graphql(query, current_user: current_user) } + + context 'when requesting projects' do + let(:projects) { graphql_data_at(:organization, :projects, :nodes) } + let(:organization_fields) do + <<~FIELDS + projects(first: 1) { + nodes { + id + } + } + FIELDS + end + + context 'when current user has an active SAML session' do + before do + active_saml_sessions = { saml_group.saml_provider.id => Time.current } + allow(::Gitlab::Auth::GroupSaml::SsoState).to receive(:active_saml_sessions).and_return(active_saml_sessions) + end + + it 'includes SAML project' do + request_organization + + expect(projects).to match_array(a_graphql_entity_for(saml_project)) + end + end + + context 'when current user has no active SAML session' do + it 'excludes SAML project' do + request_organization + + expect(projects).to match_array(a_graphql_entity_for(project)) + end + end + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index ad207444b6bc071095a138fce8be90cafd5d0c2e..a12dc3b02d0f8fb79edb4bed828a398c954aa78e 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2265,6 +2265,19 @@ def has_external_wiki end end + describe '.by_not_in_root_id' do + let_it_be(:group1) { create(:group) } + let_it_be(:group2) { create(:group) } + let_it_be(:group1_project) { create(:project, namespace: group1) } + let_it_be(:group2_project) { create(:project, namespace: group2) } + let_it_be(:subgroup_project) { create(:project, namespace: create(:group, parent: group1)) } + + it 'returns correct namespaces' do + expect(described_class.by_not_in_root_id(group1.id)).to contain_exactly(group2_project) + expect(described_class.by_not_in_root_id(group2.id)).to contain_exactly(group1_project, subgroup_project) + end + end + describe '.order_by_storage_size' do let_it_be(:project_1) { create(:project_statistics, repository_size: 1).project } let_it_be(:project_2) { create(:project_statistics, repository_size: 3).project }