From 07bac26ab654690df3718b20cea108ec3b0a1f1f Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Tue, 11 May 2021 10:26:34 +0100 Subject: [PATCH 1/2] Expose the admin_path_locks project permission in GraphQL --- app/graphql/types/permission_types/project.rb | 2 ++ .../graphql/ee/types/permission_types/project.rb | 15 +++++++++++++++ .../types/permission_types/project_spec.rb | 13 +++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 ee/app/graphql/ee/types/permission_types/project.rb create mode 100644 ee/spec/graphql/types/permission_types/project_spec.rb diff --git a/app/graphql/types/permission_types/project.rb b/app/graphql/types/permission_types/project.rb index 5747e63d195667..f6a5563d367534 100644 --- a/app/graphql/types/permission_types/project.rb +++ b/app/graphql/types/permission_types/project.rb @@ -27,3 +27,5 @@ def create_snippet end end end + +::Types::PermissionTypes::Project.prepend_mod_with('Types::PermissionTypes::Project') diff --git a/ee/app/graphql/ee/types/permission_types/project.rb b/ee/app/graphql/ee/types/permission_types/project.rb new file mode 100644 index 00000000000000..a44ee9fd483ca0 --- /dev/null +++ b/ee/app/graphql/ee/types/permission_types/project.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module EE + module Types + module PermissionTypes + module Project + extend ActiveSupport::Concern + + prepended do + ability_field :admin_path_locks + end + end + end + end +end diff --git a/ee/spec/graphql/types/permission_types/project_spec.rb b/ee/spec/graphql/types/permission_types/project_spec.rb new file mode 100644 index 00000000000000..f1c56a9aa8cfee --- /dev/null +++ b/ee/spec/graphql/types/permission_types/project_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::PermissionTypes::Project do + specify do + expected_permissions = [:admin_path_locks] + + expected_permissions.each do |permission| + expect(described_class).to have_graphql_field(permission) + end + end +end -- GitLab From 24fd7addf00802a88c357c429563aaa30b7802aa Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Mon, 10 May 2021 16:45:10 +0100 Subject: [PATCH 2/2] Expose Path Locks in the GraphQL API Path Locks are an EE-only feature that allow a user to prevent anyone else from pushing changes to a certain path. We need to expose the locks through GraphQL to convert the file view to GraphQL. Changelog: added --- doc/api/graphql/reference/index.md | 43 ++++++++++++++++++ ee/app/graphql/ee/types/project_type.rb | 7 +++ .../graphql/resolvers/path_locks_resolver.rb | 36 +++++++++++++++ ee/app/graphql/types/path_lock_type.rb | 16 +++++++ .../unreleased/323195-path-locks-graphql.yml | 5 +++ .../resolvers/path_locks_resolver_spec.rb | 44 +++++++++++++++++++ ee/spec/graphql/types/path_lock_type_spec.rb | 11 +++++ ee/spec/graphql/types/project_type_spec.rb | 2 +- .../api/graphql/project/path_locks_spec.rb | 43 ++++++++++++++++++ 9 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 ee/app/graphql/resolvers/path_locks_resolver.rb create mode 100644 ee/app/graphql/types/path_lock_type.rb create mode 100644 ee/changelogs/unreleased/323195-path-locks-graphql.yml create mode 100644 ee/spec/graphql/resolvers/path_locks_resolver_spec.rb create mode 100644 ee/spec/graphql/types/path_lock_type_spec.rb create mode 100644 ee/spec/requests/api/graphql/project/path_locks_spec.rb diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 11a805986eccca..b2348ec3267216 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -5800,6 +5800,29 @@ The edge type for [`PackageTag`](#packagetag). | `cursor` | [`String!`](#string) | A cursor for use in pagination. | | `node` | [`PackageTag`](#packagetag) | The item at the end of the edge. | +#### `PathLockConnection` + +The connection type for [`PathLock`](#pathlock). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `edges` | [`[PathLockEdge]`](#pathlockedge) | A list of edges. | +| `nodes` | [`[PathLock]`](#pathlock) | A list of nodes. | +| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | + +#### `PathLockEdge` + +The edge type for [`PathLock`](#pathlock). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `cursor` | [`String!`](#string) | A cursor for use in pagination. | +| `node` | [`PathLock`](#pathlock) | The item at the end of the edge. | + #### `PipelineArtifactRegistryConnection` The connection type for [`PipelineArtifactRegistry`](#pipelineartifactregistry). @@ -10613,6 +10636,18 @@ Information about pagination in a connection. | `hasPreviousPage` | [`Boolean!`](#boolean) | When paginating backwards, are there more items?. | | `startCursor` | [`String`](#string) | When paginating backwards, the cursor to continue. | +### `PathLock` + +Represents a file or directory in the project repository that has been locked. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `id` | [`PathLockID!`](#pathlockid) | ID of the path lock. | +| `path` | [`String`](#string) | The locked path. | +| `user` | [`UserCore`](#usercore) | The user that has locked this path. | + ### `Pipeline` #### Fields @@ -10827,6 +10862,7 @@ Represents vulnerability finding of a security report on the pipeline. | `onlyAllowMergeIfPipelineSucceeds` | [`Boolean`](#boolean) | Indicates if merge requests of the project can only be merged with successful jobs. | | `openIssuesCount` | [`Int`](#int) | Number of open issues for the project. | | `path` | [`String!`](#string) | Path of the project. | +| `pathLocks` | [`PathLockConnection`](#pathlockconnection) | The project's path locks. (see [Connections](#connections)) | | `pipelineAnalytics` | [`PipelineAnalytics`](#pipelineanalytics) | Pipeline analytics. | | `printingMergeRequestLinkEnabled` | [`Boolean`](#boolean) | Indicates if a link to create or view a merge request should display after a push to Git repositories of the project from the command line. | | `publicJobs` | [`Boolean`](#boolean) | Indicates if there is public access to pipelines and job details of the project, including output logs and artifacts. | @@ -11641,6 +11677,7 @@ Represents a Project Membership. | Name | Type | Description | | ---- | ---- | ----------- | | `adminOperations` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_operations` on this resource. | +| `adminPathLocks` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_path_locks` on this resource. | | `adminProject` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_project` on this resource. | | `adminRemoteMirror` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_remote_mirror` on this resource. | | `adminWiki` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_wiki` on this resource. | @@ -15036,6 +15073,12 @@ A `PackagesPackageID` is a global ID. It is encoded as a string. An example `PackagesPackageID` is: `"gid://gitlab/Packages::Package/1"`. +### `PathLockID` + +A `PathLockID` is a global ID. It is encoded as a string. + +An example `PathLockID` is: `"gid://gitlab/PathLock/1"`. + ### `PayloadAlertFieldPathSegment` String or integer. diff --git a/ee/app/graphql/ee/types/project_type.rb b/ee/app/graphql/ee/types/project_type.rb index 5c3f683a4661f9..38411ce68c3722 100644 --- a/ee/app/graphql/ee/types/project_type.rb +++ b/ee/app/graphql/ee/types/project_type.rb @@ -142,6 +142,13 @@ module ProjectType null: true, description: "The project's push rules settings.", method: :push_rule + + field :path_locks, + ::Types::PathLockType.connection_type, + null: true, + description: "The project's path locks.", + extras: [:lookahead], + resolver: ::Resolvers::PathLocksResolver end def api_fuzzing_ci_configuration diff --git a/ee/app/graphql/resolvers/path_locks_resolver.rb b/ee/app/graphql/resolvers/path_locks_resolver.rb new file mode 100644 index 00000000000000..5d5ab041b4f877 --- /dev/null +++ b/ee/app/graphql/resolvers/path_locks_resolver.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Resolvers + class PathLocksResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + include LooksAhead + + authorize :download_code + + type Types::PathLockType, null: true + + alias_method :project, :object + + def resolve_with_lookahead(**args) + authorize!(project) + + return [] unless path_lock_feature_enabled? + + find_path_locks(args) + end + + private + + def preloads + { user: [:user] } + end + + def find_path_locks(args) + apply_lookahead(project.path_locks) + end + + def path_lock_feature_enabled? + project.licensed_feature_available?(:file_locks) + end + end +end diff --git a/ee/app/graphql/types/path_lock_type.rb b/ee/app/graphql/types/path_lock_type.rb new file mode 100644 index 00000000000000..ed90c16310fda5 --- /dev/null +++ b/ee/app/graphql/types/path_lock_type.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +module Types + class PathLockType < BaseObject # rubocop:disable Graphql/AuthorizeTypes + graphql_name 'PathLock' + description 'Represents a file or directory in the project repository that has been locked.' + + field :id, ::Types::GlobalIDType[PathLock], null: false, + description: 'ID of the path lock.' + + field :path, GraphQL::STRING_TYPE, null: true, + description: 'The locked path.' + + field :user, ::Types::UserType, null: true, + description: 'The user that has locked this path.' + end +end diff --git a/ee/changelogs/unreleased/323195-path-locks-graphql.yml b/ee/changelogs/unreleased/323195-path-locks-graphql.yml new file mode 100644 index 00000000000000..60a5ed61c66046 --- /dev/null +++ b/ee/changelogs/unreleased/323195-path-locks-graphql.yml @@ -0,0 +1,5 @@ +--- +title: Expose Path Locks in the GraphQL API +merge_request: 61380 +author: +type: added diff --git a/ee/spec/graphql/resolvers/path_locks_resolver_spec.rb b/ee/spec/graphql/resolvers/path_locks_resolver_spec.rb new file mode 100644 index 00000000000000..9950072258c198 --- /dev/null +++ b/ee/spec/graphql/resolvers/path_locks_resolver_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::PathLocksResolver do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:path_lock) { create(:path_lock, path: 'README.md', project: project) } + + let(:user) { project.owner } + + describe '#resolve' do + subject(:resolve_path_locks) { resolve(described_class, obj: project, lookahead: positive_lookahead, ctx: { current_user: user }) } + + context 'feature is not licensed' do + before do + stub_licensed_features(file_locks: false) + end + + it { is_expected.to be_empty } + end + + context 'feature is licensed' do + before do + stub_licensed_features(file_locks: true) + end + + it { is_expected.to contain_exactly(path_lock) } + + it 'preloads users' do + path_lock = resolve_path_locks.first + + expect(path_lock.association_cached?(:user)).to be_truthy + end + + context 'user is unauthorized' do + let(:user) { create(:user) } + + it { expect { resolve_path_locks }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) } + end + end + end +end diff --git a/ee/spec/graphql/types/path_lock_type_spec.rb b/ee/spec/graphql/types/path_lock_type_spec.rb new file mode 100644 index 00000000000000..afcd98323f75a4 --- /dev/null +++ b/ee/spec/graphql/types/path_lock_type_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['PathLock'] do + it { expect(described_class.graphql_name).to eq('PathLock') } + + it 'has the expected fields' do + expect(described_class).to have_graphql_fields(:id, :path, :user) + end +end diff --git a/ee/spec/graphql/types/project_type_spec.rb b/ee/spec/graphql/types/project_type_spec.rb index 9bc6b86aa27fbc..b4accc38792ea0 100644 --- a/ee/spec/graphql/types/project_type_spec.rb +++ b/ee/spec/graphql/types/project_type_spec.rb @@ -20,7 +20,7 @@ vulnerabilities vulnerability_scanners requirement_states_count vulnerability_severities_count packages compliance_frameworks vulnerabilities_count_by_day security_dashboard_path iterations iteration_cadences cluster_agents repository_size_excess actual_repository_size_limit - code_coverage_summary api_fuzzing_ci_configuration + code_coverage_summary api_fuzzing_ci_configuration path_locks ] expect(described_class).to include_graphql_fields(*expected_fields) diff --git a/ee/spec/requests/api/graphql/project/path_locks_spec.rb b/ee/spec/requests/api/graphql/project/path_locks_spec.rb new file mode 100644 index 00000000000000..0f88d5c94ef942 --- /dev/null +++ b/ee/spec/requests/api/graphql/project/path_locks_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.project(fullPath).pathLocks' do + using RSpec::Parameterized::TableSyntax + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, namespace: user.namespace) } + let_it_be(:path_lock) { create(:path_lock, project: project, path: 'README.md') } + + subject(:path_locks_response) do + post_graphql( + graphql_query_for( + :project, { full_path: project.full_path }, "pathLocks { nodes { #{all_graphql_fields_for('PathLock')} } }" + ), + current_user: user + ) + + graphql_data_at(:project, :pathLocks, :nodes) + end + + context 'unlicensed feature' do + before do + stub_licensed_features(file_locks: false) + end + + it { is_expected.to be_empty } + end + + context 'licensed feature' do + before do + stub_licensed_features(file_locks: true) + end + + it 'returns path locks' do + is_expected.to match_array( + a_hash_including('id' => path_lock.to_global_id.to_s, 'path' => 'README.md') + ) + end + end +end -- GitLab