From 0efbc0b240f656a2288f566e61b6fff6f5f6fe4b Mon Sep 17 00:00:00 2001 From: Imre Farkas <ifarkas@gitlab.com> Date: Fri, 3 Jun 2022 14:58:28 +0000 Subject: [PATCH] Require authentication when enumerating users via GraqhQL Changelog: changed --- app/graphql/resolvers/user_resolver.rb | 10 ++++ app/graphql/resolvers/users_resolver.rb | 8 ++- ...require_auth_for_graphql_user_resolver.yml | 8 +++ .../visibility_and_access_controls.md | 4 +- spec/graphql/resolvers/user_resolver_spec.rb | 49 +++++++++++++++---- spec/graphql/resolvers/users_resolver_spec.rb | 30 ++++++++---- spec/graphql/types/user_type_spec.rb | 8 +-- .../user/starred_projects_query_spec.rb | 21 ++++---- 8 files changed, 101 insertions(+), 37 deletions(-) create mode 100644 config/feature_flags/development/require_auth_for_graphql_user_resolver.yml diff --git a/app/graphql/resolvers/user_resolver.rb b/app/graphql/resolvers/user_resolver.rb index 99fd0d4927dd..d2e8aa514693 100644 --- a/app/graphql/resolvers/user_resolver.rb +++ b/app/graphql/resolvers/user_resolver.rb @@ -2,6 +2,8 @@ module Resolvers class UserResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + description 'Retrieve a single user' type Types::UserType, null: true @@ -23,6 +25,8 @@ def ready?(id: nil, username: nil) end def resolve(id: nil, username: nil) + authorize! + if id GitlabSchema.object_from_id(id, expected_type: User) else @@ -39,5 +43,11 @@ def batch_load(username) end end end + + def authorize! + return unless Feature.enabled?(:require_auth_for_graphql_user_resolver) + + raise_resource_not_available_error! unless context[:current_user].present? + end end end diff --git a/app/graphql/resolvers/users_resolver.rb b/app/graphql/resolvers/users_resolver.rb index 1424c14083df..12555f4e5656 100644 --- a/app/graphql/resolvers/users_resolver.rb +++ b/app/graphql/resolvers/users_resolver.rb @@ -47,8 +47,12 @@ def ready?(**args) end def authorize!(usernames) - authorized = Ability.allowed?(context[:current_user], :read_users_list) - authorized &&= usernames.present? if context[:current_user].blank? + if Feature.enabled?(:require_auth_for_graphql_user_resolver) + authorized = context[:current_user].present? + else + authorized = Ability.allowed?(context[:current_user], :read_users_list) + authorized &&= usernames.present? if context[:current_user].blank? + end raise_resource_not_available_error! unless authorized end diff --git a/config/feature_flags/development/require_auth_for_graphql_user_resolver.yml b/config/feature_flags/development/require_auth_for_graphql_user_resolver.yml new file mode 100644 index 000000000000..7dd6c7ecd957 --- /dev/null +++ b/config/feature_flags/development/require_auth_for_graphql_user_resolver.yml @@ -0,0 +1,8 @@ +--- +name: require_auth_for_graphql_user_resolver +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/88020 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/362953 +milestone: '15.1' +type: development +group: group::authentication and authorization +default_enabled: false diff --git a/doc/user/admin_area/settings/visibility_and_access_controls.md b/doc/user/admin_area/settings/visibility_and_access_controls.md index 33b392676adb..103eae07517e 100644 --- a/doc/user/admin_area/settings/visibility_and_access_controls.md +++ b/doc/user/admin_area/settings/visibility_and_access_controls.md @@ -142,7 +142,9 @@ To restrict visibility levels for projects, snippets, and selected pages: 1. In the **Restricted visibility levels** section, select the desired visibility levels to restrict. If you restrict the **Public** level: - User profiles are only visible to logged in users via the Web interface. - - User attributes are only visible to authenticated users via the GraphQL API. + - User attributes via the GraphQL API are: + - Not visible in [GitLab 15.1 and later](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/88020). + - Only visible to authenticated users between [GitLab 13.1](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33195) and GitLab 15.0. 1. Select **Save changes**. For more details on project visibility, see diff --git a/spec/graphql/resolvers/user_resolver_spec.rb b/spec/graphql/resolvers/user_resolver_spec.rb index 446d765d3eef..32a9b1776293 100644 --- a/spec/graphql/resolvers/user_resolver_spec.rb +++ b/spec/graphql/resolvers/user_resolver_spec.rb @@ -6,8 +6,41 @@ include GraphqlHelpers describe '#resolve' do + let_it_be(:current_user) { nil } let_it_be(:user) { create(:user) } + shared_examples 'queries user' do + context 'authenticated access' do + let_it_be(:current_user) { create(:user) } + + it 'returns the correct user' do + expect( + resolve_user(args) + ).to eq(user) + end + end + + context 'unauthenticated access' do + it 'forbids search' do + expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do + resolve_user(args) + end + end + + context 'require_auth_for_graphql_user_resolver feature flag is disabled' do + before do + stub_feature_flags(require_auth_for_graphql_user_resolver: false) + end + + it 'returns the correct user' do + expect( + resolve_user(args) + ).to eq(user) + end + end + end + end + context 'when neither an ID or a username is provided' do it 'generates an ArgumentError' do expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError) do @@ -23,25 +56,21 @@ end context 'by username' do - it 'returns the correct user' do - expect( - resolve_user(username: user.username) - ).to eq(user) + include_examples "queries user" do + let(:args) { { username: user.username } } end end context 'by ID' do - it 'returns the correct user' do - expect( - resolve_user(id: user.to_global_id) - ).to eq(user) + include_examples "queries user" do + let(:args) { { id: user.to_global_id } } end end end private - def resolve_user(args = {}) - sync(resolve(described_class, args: args)) + def resolve_user(args = {}, context = { current_user: current_user }) + sync(resolve(described_class, args: args, ctx: context)) end end diff --git a/spec/graphql/resolvers/users_resolver_spec.rb b/spec/graphql/resolvers/users_resolver_spec.rb index 1ba296912a3a..5f7a096a14b8 100644 --- a/spec/graphql/resolvers/users_resolver_spec.rb +++ b/spec/graphql/resolvers/users_resolver_spec.rb @@ -14,14 +14,6 @@ end describe '#resolve' do - it 'generates an error when read_users_list is not authorized' do - expect(Ability).to receive(:allowed?).with(current_user, :read_users_list).and_return(false) - - expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do - resolve_users - end - end - context 'when no arguments are passed' do it 'returns all users' do expect(resolve_users).to contain_exactly(user1, user2, current_user) @@ -79,8 +71,26 @@ end end - it 'allows to search by username' do - expect(resolve_users(args: { usernames: [user1.username] })).to contain_exactly(user1) + it 'prohibits search by username' do + expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do + resolve_users(args: { usernames: [user1.username] }) + end + end + + context 'require_auth_for_graphql_user_resolver feature flag is disabled' do + before do + stub_feature_flags(require_auth_for_graphql_user_resolver: false) + end + + it 'prohibits search without usernames passed' do + expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do + resolve_users + end + end + + it 'allows to search by username' do + expect(resolve_users(args: { usernames: [user1.username] })).to contain_exactly(user1) + end end end end diff --git a/spec/graphql/types/user_type_spec.rb b/spec/graphql/types/user_type_spec.rb index c913a4c3662f..fec6a771640d 100644 --- a/spec/graphql/types/user_type_spec.rb +++ b/spec/graphql/types/user_type_spec.rb @@ -91,8 +91,8 @@ context 'when requester is nil' do let(:current_user) { nil } - it 'returns `****`' do - expect(user_name).to eq('****') + it 'returns nothing' do + expect(user_name).to be_nil end end @@ -134,8 +134,8 @@ context 'when requester is nil' do let(:current_user) { nil } - it 'returns `****`' do - expect(user_name).to eq('****') + it 'returns nothing' do + expect(user_name).to be_nil end end diff --git a/spec/requests/api/graphql/user/starred_projects_query_spec.rb b/spec/requests/api/graphql/user/starred_projects_query_spec.rb index 37a85b98e5f7..75a17ed34c40 100644 --- a/spec/requests/api/graphql/user/starred_projects_query_spec.rb +++ b/spec/requests/api/graphql/user/starred_projects_query_spec.rb @@ -17,7 +17,6 @@ let_it_be(:user, reload: true) { create(:user) } let(:user_fields) { 'starredProjects { nodes { id } }' } - let(:current_user) { nil } let(:starred_projects) do post_graphql(query, current_user: current_user) @@ -34,21 +33,23 @@ user.toggle_star(project_c) end - it_behaves_like 'a working graphql query' do - before do - post_graphql(query) - end - end + context 'anonymous access' do + let(:current_user) { nil } - it 'found only public project' do - expect(starred_projects).to contain_exactly( - a_graphql_entity_for(project_a) - ) + it 'returns nothing' do + expect(starred_projects).to be_nil + end end context 'the current user is the user' do let(:current_user) { user } + it_behaves_like 'a working graphql query' do + before do + post_graphql(query, current_user: current_user) + end + end + it 'found all projects' do expect(starred_projects).to contain_exactly( a_graphql_entity_for(project_a), -- GitLab