diff options
Diffstat (limited to 'lib/api')
46 files changed, 1114 insertions, 214 deletions
diff --git a/lib/api/api.rb b/lib/api/api.rb index 2be6792af5f..b37751e1b47 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -167,6 +167,7 @@ module API mount ::API::GroupVariables mount ::API::ImportBitbucketServer mount ::API::ImportGithub + mount ::API::IssueLinks mount ::API::Issues mount ::API::JobArtifacts mount ::API::Jobs @@ -193,9 +194,11 @@ module API mount ::API::NugetPackages mount ::API::PypiPackages mount ::API::ComposerPackages - mount ::API::ConanPackages + mount ::API::ConanProjectPackages + mount ::API::ConanInstancePackages mount ::API::MavenPackages mount ::API::NpmPackages + mount ::API::GenericPackages mount ::API::GoProxy mount ::API::Pages mount ::API::PagesDomains @@ -233,6 +236,7 @@ module API mount ::API::Templates mount ::API::Todos mount ::API::Triggers + mount ::API::UsageData mount ::API::UserCounts mount ::API::Users mount ::API::Variables @@ -244,6 +248,16 @@ module API mount ::API::Internal::Pages mount ::API::Internal::Kubernetes + version 'v3', using: :path do + # Although the following endpoints are kept behind V3 namespace, + # they're not deprecated neither should be removed when V3 get + # removed. They're needed as a layer to integrate with Jira + # Development Panel. + namespace '/', requirements: ::API::V3::Github::ENDPOINT_REQUIREMENTS do + mount ::API::V3::Github + end + end + route :any, '*path' do error!('404 Not Found', 404) end diff --git a/lib/api/applications.rb b/lib/api/applications.rb index 4e8d68c8d09..4f2c3ee79ef 100644 --- a/lib/api/applications.rb +++ b/lib/api/applications.rb @@ -6,6 +6,15 @@ module API before { authenticated_as_admin! } resource :applications do + helpers do + def validate_redirect_uri(value) + uri = ::URI.parse(value) + !uri.is_a?(URI::HTTP) || uri.host + rescue URI::InvalidURIError + false + end + end + desc 'Create a new application' do detail 'This feature was introduced in GitLab 10.5' success Entities::ApplicationWithSecret @@ -19,6 +28,13 @@ module API desc: 'Application will be used where the client secret is confidential' end post do + # Validate that host in uri is specified + # Please remove it when https://github.com/doorkeeper-gem/doorkeeper/pull/1440 is merged + # and the doorkeeper gem version is bumped + unless validate_redirect_uri(declared_params[:redirect_uri]) + render_api_error!({ redirect_uri: ["must be an absolute URI."] }, :bad_request) + end + application = Doorkeeper::Application.new(declared_params) if application.save diff --git a/lib/api/ci/pipelines.rb b/lib/api/ci/pipelines.rb index a010e0dd761..045f81074a7 100644 --- a/lib/api/ci/pipelines.rb +++ b/lib/api/ci/pipelines.rb @@ -15,6 +15,24 @@ module API detail 'This feature was introduced in GitLab 8.11.' success Entities::Ci::PipelineBasic end + + helpers do + params :optional_scope do + optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show', + values: ::CommitStatus::AVAILABLE_STATUSES, + coerce_with: ->(scope) { + case scope + when String + [scope] + when ::Array + scope + else + ['unknown'] + end + } + end + end + params do use :pagination optional :scope, type: String, values: %w[running pending finished branches tags], @@ -96,6 +114,64 @@ module API present pipeline, with: Entities::Ci::Pipeline end + desc 'Get pipeline jobs' do + success Entities::Ci::Job + end + params do + requires :pipeline_id, type: Integer, desc: 'The pipeline ID' + use :optional_scope + use :pagination + end + + get ':id/pipelines/:pipeline_id/jobs' do + authorize!(:read_pipeline, user_project) + + pipeline = user_project.all_pipelines.find(params[:pipeline_id]) + + if Feature.enabled?(:ci_jobs_finder_refactor) + builds = ::Ci::JobsFinder + .new(current_user: current_user, pipeline: pipeline, params: params) + .execute + else + authorize!(:read_build, pipeline) + builds = pipeline.builds + builds = filter_builds(builds, params[:scope]) + end + + builds = builds.with_preloads + + present paginate(builds), with: Entities::Ci::Job + end + + desc 'Get pipeline bridge jobs' do + success Entities::Ci::Bridge + end + params do + requires :pipeline_id, type: Integer, desc: 'The pipeline ID' + use :optional_scope + use :pagination + end + + get ':id/pipelines/:pipeline_id/bridges' do + authorize!(:read_build, user_project) + + pipeline = user_project.all_pipelines.find(params[:pipeline_id]) + + if Feature.enabled?(:ci_jobs_finder_refactor) + bridges = ::Ci::JobsFinder + .new(current_user: current_user, pipeline: pipeline, params: params, type: ::Ci::Bridge) + .execute + else + authorize!(:read_pipeline, pipeline) + bridges = pipeline.bridges + bridges = filter_builds(bridges, params[:scope]) + end + + bridges = bridges.with_preloads + + present paginate(bridges), with: Entities::Ci::Bridge + end + desc 'Gets the variables for a given pipeline' do detail 'This feature was introduced in GitLab 11.11' success Entities::Ci::Variable @@ -170,6 +246,21 @@ module API end helpers do + # NOTE: This method should be removed once the ci_jobs_finder_refactor FF is + # removed. https://gitlab.com/gitlab-org/gitlab/-/issues/245183 + # rubocop: disable CodeReuse/ActiveRecord + def filter_builds(builds, scope) + return builds if scope.nil? || scope.empty? + + available_statuses = ::CommitStatus::AVAILABLE_STATUSES + + unknown = scope - available_statuses + render_api_error!('Scope contains invalid value(s)', 400) unless unknown.empty? + + builds.where(status: scope) + end + # rubocop: enable CodeReuse/ActiveRecord + def pipeline strong_memoize(:pipeline) do user_project.all_pipelines.find(params[:pipeline_id]) @@ -178,7 +269,7 @@ module API def latest_pipeline strong_memoize(:latest_pipeline) do - user_project.latest_pipeline_for_ref(params[:ref]) + user_project.latest_pipeline(params[:ref]) end end end diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb index 31be1bb7e3e..08903dce3dc 100644 --- a/lib/api/ci/runner.rb +++ b/lib/api/ci/runner.rb @@ -159,29 +159,29 @@ module API end desc 'Updates a job' do - http_codes [[200, 'Job was updated'], [403, 'Forbidden']] + http_codes [[200, 'Job was updated'], + [202, 'Update accepted'], + [400, 'Unknown parameters'], + [403, 'Forbidden']] end params do requires :token, type: String, desc: %q(Runners's authentication token) requires :id, type: Integer, desc: %q(Job's ID) optional :trace, type: String, desc: %q(Job's full trace) optional :state, type: String, desc: %q(Job's status: success, failed) + optional :checksum, type: String, desc: %q(Job's trace CRC32 checksum) optional :failure_reason, type: String, desc: %q(Job's failure_reason) end put '/:id' do job = authenticate_job! - job.trace.set(params[:trace]) if params[:trace] - Gitlab::Metrics.add_event(:update_build) - case params[:state].to_s - when 'running' - job.touch if job.needs_touch? - when 'success' - job.success! - when 'failed' - job.drop!(params[:failure_reason] || :unknown_failure) + service = ::Ci::UpdateBuildStateService + .new(job, declared_params(include_missing: false)) + + service.execute.then do |result| + status result.status end end diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 140351c9e5c..9f5a6e87505 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -117,8 +117,10 @@ module API render_api_error!('invalid state', 400) end - MergeRequest.where(source_project: user_project, source_branch: ref) - .update_all(head_pipeline_id: pipeline.id) if pipeline.latest? + if pipeline.latest? + MergeRequest.where(source_project: user_project, source_branch: ref) + .update_all(head_pipeline_id: pipeline.id) + end present status, with: Entities::CommitStatus rescue StateMachines::InvalidTransition => e diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 3c7ed2a25a0..20877fb5c5f 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -136,7 +136,10 @@ module API if result[:status] == :success commit_detail = user_project.repository.commit(result[:result]) - Gitlab::UsageDataCounters::WebIdeCounter.increment_commits_count if find_user_from_warden + if find_user_from_warden + Gitlab::UsageDataCounters::WebIdeCounter.increment_commits_count + Gitlab::UsageDataCounters::EditorUniqueCounter.track_web_ide_edit_action(author: current_user) + end present commit_detail, with: Entities::CommitDetail, stats: params[:stats] else diff --git a/lib/api/composer_packages.rb b/lib/api/composer_packages.rb index 05887e58425..31d097c4bea 100644 --- a/lib/api/composer_packages.rb +++ b/lib/api/composer_packages.rb @@ -123,7 +123,7 @@ module API bad_request! end - track_event('push_package') + package_event('push_package') ::Packages::Composer::CreatePackageService .new(authorized_user_project, current_user, declared_params) diff --git a/lib/api/conan_instance_packages.rb b/lib/api/conan_instance_packages.rb new file mode 100644 index 00000000000..209748d79fa --- /dev/null +++ b/lib/api/conan_instance_packages.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# Conan Instance-Level Package Manager Client API +module API + class ConanInstancePackages < Grape::API::Instance + namespace 'packages/conan/v1' do + include ConanPackageEndpoints + end + end +end diff --git a/lib/api/conan_packages.rb b/lib/api/conan_package_endpoints.rb index 7f2afea9931..445447cfcd2 100644 --- a/lib/api/conan_packages.rb +++ b/lib/api/conan_package_endpoints.rb @@ -9,8 +9,8 @@ # # Technical debt: https://gitlab.com/gitlab-org/gitlab/issues/35798 module API - class ConanPackages < Grape::API::Instance - helpers ::API::Helpers::PackagesManagerClientsHelpers + module ConanPackageEndpoints + extend ActiveSupport::Concern PACKAGE_REQUIREMENTS = { package_name: API::NO_SLASH_URL_PART_REGEX, @@ -28,15 +28,19 @@ module API CONAN_FILES = (Gitlab::Regex::Packages::CONAN_RECIPE_FILES + Gitlab::Regex::Packages::CONAN_PACKAGE_FILES).freeze - before do - require_packages_enabled! + included do + helpers ::API::Helpers::PackagesManagerClientsHelpers + helpers ::API::Helpers::Packages::Conan::ApiHelpers + helpers ::API::Helpers::RelatedResourcesHelpers - # Personal access token will be extracted from Bearer or Basic authorization - # in the overridden find_personal_access_token or find_user_from_job_token helpers - authenticate! - end + before do + require_packages_enabled! + + # Personal access token will be extracted from Bearer or Basic authorization + # in the overridden find_personal_access_token or find_user_from_job_token helpers + authenticate! + end - namespace 'packages/conan/v1' do desc 'Ping the Conan API' do detail 'This feature was introduced in GitLab 12.2' end @@ -115,7 +119,7 @@ module API authorize!(:read_package, project) presenter = ::Packages::Conan::PackagePresenter.new( - recipe, + package, current_user, project, conan_package_reference: params[:conan_package_reference] @@ -133,7 +137,7 @@ module API get do authorize!(:read_package, project) - presenter = ::Packages::Conan::PackagePresenter.new(recipe, current_user, project) + presenter = ::Packages::Conan::PackagePresenter.new(package, current_user, project) present presenter, with: ::API::Entities::ConanPackage::ConanRecipeSnapshot end @@ -242,7 +246,7 @@ module API delete do authorize!(:destroy_package, project) - track_event('delete_package') + package_event('delete_package', category: 'API::ConanPackages') package.destroy end @@ -295,7 +299,7 @@ module API route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true put 'authorize' do - authorize_workhorse!(subject: project) + authorize_workhorse!(subject: project, maximum_size: project.actual_limits.conan_max_file_size) end end @@ -322,7 +326,7 @@ module API route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true put 'authorize' do - authorize_workhorse!(subject: project) + authorize_workhorse!(subject: project, maximum_size: project.actual_limits.conan_max_file_size) end desc 'Upload package files' do @@ -341,11 +345,5 @@ module API end end end - - helpers do - include Gitlab::Utils::StrongMemoize - include ::API::Helpers::RelatedResourcesHelpers - include ::API::Helpers::Packages::Conan::ApiHelpers - end end end diff --git a/lib/api/conan_project_packages.rb b/lib/api/conan_project_packages.rb new file mode 100644 index 00000000000..c51992231a7 --- /dev/null +++ b/lib/api/conan_project_packages.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# Conan Project-Level Package Manager Client API +module API + class ConanProjectPackages < Grape::API::Instance + params do + requires :id, type: Integer, desc: 'The ID of a project', regexp: %r{\A[1-9]\d*\z} + end + + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + namespace ':id/packages/conan/v1' do + include ConanPackageEndpoints + end + end + end +end diff --git a/lib/api/entities/issue_link.rb b/lib/api/entities/issue_link.rb new file mode 100644 index 00000000000..8e24b046325 --- /dev/null +++ b/lib/api/entities/issue_link.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + class IssueLink < Grape::Entity + expose :source, as: :source_issue, using: ::API::Entities::IssueBasic + expose :target, as: :target_issue, using: ::API::Entities::IssueBasic + expose :link_type + end + end +end diff --git a/lib/api/entities/milestone.rb b/lib/api/entities/milestone.rb index 5a0c222d691..b191210a234 100644 --- a/lib/api/entities/milestone.rb +++ b/lib/api/entities/milestone.rb @@ -10,6 +10,7 @@ module API expose :state, :created_at, :updated_at expose :due_date expose :start_date + expose :expired?, as: :expired expose :web_url do |milestone, _options| Gitlab::UrlBuilder.build(milestone) diff --git a/lib/api/entities/package.rb b/lib/api/entities/package.rb index 670965b225c..d903f50befa 100644 --- a/lib/api/entities/package.rb +++ b/lib/api/entities/package.rb @@ -28,7 +28,7 @@ module API expose :pipeline, if: ->(package) { package.build_info }, using: Package::Pipeline - expose :versions, using: ::API::Entities::PackageVersion + expose :versions, using: ::API::Entities::PackageVersion, unless: ->(_, opts) { opts[:collection] } private diff --git a/lib/api/entities/related_issue.rb b/lib/api/entities/related_issue.rb new file mode 100644 index 00000000000..491c606bd49 --- /dev/null +++ b/lib/api/entities/related_issue.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + class RelatedIssue < ::API::Entities::Issue + expose :issue_link_id + expose :issue_link_type, as: :link_type + end + end +end diff --git a/lib/api/generic_packages.rb b/lib/api/generic_packages.rb new file mode 100644 index 00000000000..98b8a40c7c9 --- /dev/null +++ b/lib/api/generic_packages.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module API + class GenericPackages < Grape::API::Instance + before do + require_packages_enabled! + authenticate! + + require_generic_packages_available! + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + route_setting :authentication, job_token_allowed: true + + namespace ':id/packages/generic' do + get 'ping' do + :pong + end + end + end + + helpers do + include ::API::Helpers::PackagesHelpers + + def require_generic_packages_available! + not_found! unless Feature.enabled?(:generic_packages, user_project) + end + end + end +end diff --git a/lib/api/github/entities.rb b/lib/api/github/entities.rb new file mode 100644 index 00000000000..c28a0b8eb7e --- /dev/null +++ b/lib/api/github/entities.rb @@ -0,0 +1,217 @@ +# frozen_string_literal: true + +# Simplified version of Github API entities. +# It's mainly used to mimic Github API and integrate with Jira Development Panel. +# +module API + module Github + module Entities + class Repository < Grape::Entity + expose :id + expose :owner do |project, options| + root_namespace = options[:root_namespace] || project.root_namespace + + { login: root_namespace.path } + end + expose :name do |project, options| + ::Gitlab::Jira::Dvcs.encode_project_name(project) + end + end + + class BranchCommit < Grape::Entity + expose :id, as: :sha + expose :type do |_| + 'commit' + end + end + + class RepoCommit < Grape::Entity + expose :id, as: :sha + expose :author do |commit| + { + login: commit.author&.username, + email: commit.author_email + } + end + expose :committer do |commit| + { + login: commit.author&.username, + email: commit.committer_email + } + end + expose :commit do |commit| + { + author: { + name: commit.author_name, + email: commit.author_email, + date: commit.authored_date.iso8601, + type: 'User' + }, + committer: { + name: commit.committer_name, + email: commit.committer_email, + date: commit.committed_date.iso8601, + type: 'User' + }, + message: commit.safe_message + } + end + expose :parents do |commit| + commit.parent_ids.map { |id| { sha: id } } + end + expose :files do |commit| + commit.diffs.diff_files.flat_map do |diff| + additions = diff.added_lines + deletions = diff.removed_lines + + if diff.new_file? + { + status: 'added', + filename: diff.new_path, + additions: additions, + changes: additions + } + elsif diff.deleted_file? + { + status: 'removed', + filename: diff.old_path, + deletions: deletions, + changes: deletions + } + elsif diff.renamed_file? + [ + { + status: 'removed', + filename: diff.old_path, + deletions: deletions, + changes: deletions + }, + { + status: 'added', + filename: diff.new_path, + additions: additions, + changes: additions + } + ] + else + { + status: 'modified', + filename: diff.new_path, + additions: additions, + deletions: deletions, + changes: (additions + deletions) + } + end + end + end + end + + class Branch < Grape::Entity + expose :name + + expose :commit, using: BranchCommit do |repo_branch, options| + options[:project].repository.commit(repo_branch.dereferenced_target) + end + end + + class User < Grape::Entity + expose :id + expose :username, as: :login + expose :user_url, as: :url + expose :user_url, as: :html_url + expose :avatar_url + + private + + def user_url + Gitlab::Routing.url_helpers.user_url(object) + end + end + + class NoteableComment < Grape::Entity + expose :id + expose :author, as: :user, using: User + expose :note, as: :body + expose :created_at + end + + class PullRequest < Grape::Entity + expose :title + expose :assignee, using: User do |merge_request| + merge_request.assignee + end + expose :author, as: :user, using: User + expose :created_at + expose :description, as: :body + # Since Jira service requests `/repos/-/jira/pulls` (without project + # scope), we need to make it work with ID instead IID. + expose :id, as: :number + # GitHub doesn't have a "merged" or "closed" state. It's just "open" or + # "closed". + expose :state do |merge_request| + case merge_request.state + when 'opened', 'locked' + 'open' + when 'merged' + 'closed' + else + merge_request.state + end + end + expose :merged?, as: :merged + expose :merged_at do |merge_request| + merge_request.metrics&.merged_at + end + expose :closed_at do |merge_request| + merge_request.metrics&.latest_closed_at + end + expose :updated_at + expose :html_url do |merge_request| + Gitlab::UrlBuilder.build(merge_request) + end + expose :head do + expose :source_branch, as: :label + expose :source_branch, as: :ref + expose :source_project, as: :repo, using: Repository + end + expose :base do + expose :target_branch, as: :label + expose :target_branch, as: :ref + expose :target_project, as: :repo, using: Repository + end + end + + class PullRequestPayload < Grape::Entity + expose :action do |merge_request| + case merge_request.state + when 'merged', 'closed' + 'closed' + else + 'opened' + end + end + + expose :id + expose :pull_request, using: PullRequest do |merge_request| + merge_request + end + end + + class PullRequestEvent < Grape::Entity + expose :id do |merge_request| + updated_at = merge_request.updated_at.to_i + "#{merge_request.id}-#{updated_at}" + end + expose :type do |_merge_request| + 'PullRequestEvent' + end + expose :updated_at, as: :created_at + expose :payload, using: PullRequestPayload do |merge_request| + # The merge request data is used by PullRequestPayload and PullRequest, so we just provide it + # here. Otherwise Grape::Entity would try to access a field "payload" on Merge Request. + merge_request + end + end + end + end +end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 599d5bd0baf..1912a06682e 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -532,11 +532,29 @@ module API ::Gitlab::Tracking.event(category, action.to_s, **args) rescue => error - Rails.logger.warn( # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.warn( "Tracking event failed for action: #{action}, category: #{category}, message: #{error.message}" ) end + # @param event_name [String] the event name + # @param values [Array|String] the values counted + def increment_unique_values(event_name, values) + return unless values.present? + + feature_name = "usage_data_#{event_name}" + return unless Feature.enabled?(feature_name) + return unless Gitlab::CurrentSettings.usage_ping_enabled? + + Gitlab::UsageDataCounters::HLLRedisCounter.track_event(values, event_name) + rescue => error + Gitlab::AppLogger.warn("Redis tracking event failed for event: #{event_name}, message: #{error.message}") + end + + def with_api_params(&block) + yield({ api: true, request: request }) + end + protected def project_finder_params_visibility_ce diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb index b7ce1eba3f9..69b53ea6c2f 100644 --- a/lib/api/helpers/internal_helpers.rb +++ b/lib/api/helpers/internal_helpers.rb @@ -67,7 +67,7 @@ module API result == 'PONG' rescue => e - Rails.logger.warn("GitLab: An unexpected error occurred in pinging to Redis: #{e}") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.warn("GitLab: An unexpected error occurred in pinging to Redis: #{e}") false end diff --git a/lib/api/helpers/packages/conan/api_helpers.rb b/lib/api/helpers/packages/conan/api_helpers.rb index 1161d1386bb..dcbf933a4e1 100644 --- a/lib/api/helpers/packages/conan/api_helpers.rb +++ b/lib/api/helpers/packages/conan/api_helpers.rb @@ -5,11 +5,13 @@ module API module Packages module Conan module ApiHelpers + include Gitlab::Utils::StrongMemoize + def present_download_urls(entity) authorize!(:read_package, project) presenter = ::Packages::Conan::PackagePresenter.new( - recipe, + package, current_user, project, conan_package_reference: params[:conan_package_reference] @@ -31,7 +33,7 @@ module API def recipe_upload_urls { upload_urls: Hash[ file_names.select(&method(:recipe_file?)).map do |file_name| - [file_name, recipe_file_upload_url(file_name)] + [file_name, build_recipe_file_upload_url(file_name)] end ] } end @@ -39,7 +41,7 @@ module API def package_upload_urls { upload_urls: Hash[ file_names.select(&method(:package_file?)).map do |file_name| - [file_name, package_file_upload_url(file_name)] + [file_name, build_package_file_upload_url(file_name)] end ] } end @@ -52,32 +54,58 @@ module API file_name.in?(::Packages::Conan::FileMetadatum::PACKAGE_FILES) end - def package_file_upload_url(file_name) - expose_url( - api_v4_packages_conan_v1_files_package_path( - package_name: params[:package_name], - package_version: params[:package_version], - package_username: params[:package_username], - package_channel: params[:package_channel], - recipe_revision: '0', - conan_package_reference: params[:conan_package_reference], - package_revision: '0', - file_name: file_name - ) + def build_package_file_upload_url(file_name) + options = url_options(file_name).merge( + conan_package_reference: params[:conan_package_reference], + package_revision: ::Packages::Conan::FileMetadatum::DEFAULT_PACKAGE_REVISION ) + + package_file_url(options) + end + + def build_recipe_file_upload_url(file_name) + recipe_file_url(url_options(file_name)) end - def recipe_file_upload_url(file_name) - expose_url( - api_v4_packages_conan_v1_files_export_path( - package_name: params[:package_name], - package_version: params[:package_version], - package_username: params[:package_username], - package_channel: params[:package_channel], - recipe_revision: '0', - file_name: file_name + def url_options(file_name) + { + package_name: params[:package_name], + package_version: params[:package_version], + package_username: params[:package_username], + package_channel: params[:package_channel], + file_name: file_name, + recipe_revision: ::Packages::Conan::FileMetadatum::DEFAULT_RECIPE_REVISION + } + end + + def package_file_url(options) + case package_scope + when :project + expose_url( + api_v4_projects_packages_conan_v1_files_package_path( + options.merge(id: project.id) + ) ) - ) + when :instance + expose_url( + api_v4_packages_conan_v1_files_package_path(options) + ) + end + end + + def recipe_file_url(options) + case package_scope + when :project + expose_url( + api_v4_projects_packages_conan_v1_files_export_path( + options.merge(id: project.id) + ) + ) + when :instance + expose_url( + api_v4_packages_conan_v1_files_export_path(options) + ) + end end def recipe @@ -86,16 +114,23 @@ module API def project strong_memoize(:project) do - full_path = ::Packages::Conan::Metadatum.full_path_from(package_username: params[:package_username]) - Project.find_by_full_path(full_path) + case package_scope + when :project + find_project!(params[:id]) + when :instance + full_path = ::Packages::Conan::Metadatum.full_path_from(package_username: params[:package_username]) + find_project!(full_path) + end end end def package strong_memoize(:package) do project.packages + .conan .with_name(params[:package_name]) .with_version(params[:package_version]) + .with_conan_username(params[:package_username]) .with_conan_channel(params[:package_channel]) .order_created .last @@ -123,7 +158,7 @@ module API conan_package_reference: params[:conan_package_reference] ).execute! - track_event('pull_package') if params[:file_name] == ::Packages::Conan::FileMetadatum::PACKAGE_BINARY + package_event('pull_package', category: 'API::ConanPackages') if params[:file_name] == ::Packages::Conan::FileMetadatum::PACKAGE_BINARY present_carrierwave_file!(package_file.file) end @@ -134,7 +169,7 @@ module API def track_push_package_event if params[:file_name] == ::Packages::Conan::FileMetadatum::PACKAGE_BINARY && params[:file].size > 0 # rubocop: disable Style/ZeroLengthPredicate - track_event('push_package') + package_event('push_package', category: 'API::ConanPackages') end end @@ -155,6 +190,7 @@ module API def upload_package_file(file_type) authorize_upload!(project) + bad_request!('File is too large') if project.actual_limits.exceeded?(:conan_max_file_size, params['file.size'].to_i) current_package = find_or_create_package @@ -234,6 +270,10 @@ module API token end + + def package_scope + params[:id].present? ? :project : :instance + end end end end diff --git a/lib/api/helpers/packages_helpers.rb b/lib/api/helpers/packages_helpers.rb index c6037d52de9..403f5ea3851 100644 --- a/lib/api/helpers/packages_helpers.rb +++ b/lib/api/helpers/packages_helpers.rb @@ -47,6 +47,10 @@ module API authorize_create_package!(subject) require_gitlab_workhorse! end + + def package_event(event_name, **args) + track_event(event_name, **args) + end end end end diff --git a/lib/api/helpers/packages_manager_clients_helpers.rb b/lib/api/helpers/packages_manager_clients_helpers.rb index 955d21cb44f..e7662b03577 100644 --- a/lib/api/helpers/packages_manager_clients_helpers.rb +++ b/lib/api/helpers/packages_manager_clients_helpers.rb @@ -6,18 +6,8 @@ module API extend Grape::API::Helpers include ::API::Helpers::PackagesHelpers - params :workhorse_upload_params do - optional 'file.path', type: String, desc: 'Path to locally stored body (generated by Workhorse)' - optional 'file.name', type: String, desc: 'Real filename as send in Content-Disposition (generated by Workhorse)' - optional 'file.type', type: String, desc: 'Real content type as send in Content-Type (generated by Workhorse)' - optional 'file.size', type: Integer, desc: 'Real size of file (generated by Workhorse)' - optional 'file.md5', type: String, desc: 'MD5 checksum of the file (generated by Workhorse)' - optional 'file.sha1', type: String, desc: 'SHA1 checksum of the file (generated by Workhorse)' - optional 'file.sha256', type: String, desc: 'SHA256 checksum of the file (generated by Workhorse)' - end - def find_job_from_http_basic_auth - return unless headers + return unless request.headers token = decode_token @@ -27,7 +17,7 @@ module API end def find_deploy_token_from_http_basic_auth - return unless headers + return unless request.headers token = decode_token @@ -36,16 +26,10 @@ module API DeployToken.active.find_by_token(token) end - def uploaded_package_file(param_name = :file) - uploaded_file = UploadedFile.from_params(params, param_name, ::Packages::PackageFileUploader.workhorse_local_upload_path) - bad_request!('Missing package file!') unless uploaded_file - uploaded_file - end - private def decode_token - encoded_credentials = headers['Authorization'].to_s.split('Basic ', 2).second + encoded_credentials = request.headers['Authorization'].to_s.split('Basic ', 2).second Base64.decode64(encoded_credentials || '').split(':', 2).second end end diff --git a/lib/api/helpers/search_helpers.rb b/lib/api/helpers/search_helpers.rb index 936684ea1f8..cb5f92fa62a 100644 --- a/lib/api/helpers/search_helpers.rb +++ b/lib/api/helpers/search_helpers.rb @@ -17,6 +17,10 @@ module API # This is a separate method so that EE can redefine it. %w(issues merge_requests milestones notes wiki_blobs commits blobs users) end + + def self.search_states + %w(all opened closed merged) + end end end end diff --git a/lib/api/helpers/services_helpers.rb b/lib/api/helpers/services_helpers.rb index ff938358439..4bceda51900 100644 --- a/lib/api/helpers/services_helpers.rb +++ b/lib/api/helpers/services_helpers.rb @@ -631,12 +631,26 @@ module API name: :issues_url, type: String, desc: 'The issues URL' + } + ], + 'ewm' => [ + { + required: true, + name: :new_issue_url, + type: String, + desc: 'New Issue URL' }, { - required: false, - name: :description, + required: true, + name: :project_url, + type: String, + desc: 'Project URL' + }, + { + required: true, + name: :issues_url, type: String, - desc: 'The description of the tracker' + desc: 'Issues URL' } ], 'youtrack' => [ @@ -651,12 +665,6 @@ module API name: :issues_url, type: String, desc: 'The issues URL' - }, - { - required: false, - name: :description, - type: String, - desc: 'The description of the tracker' } ], 'slack' => [ @@ -747,6 +755,7 @@ module API ::DiscordService, ::DroneCiService, ::EmailsOnPushService, + ::EwmService, ::ExternalWikiService, ::FlowdockService, ::HangoutsChatService, diff --git a/lib/api/helpers/snippets_helpers.rb b/lib/api/helpers/snippets_helpers.rb index 79367da8d1f..9224381735f 100644 --- a/lib/api/helpers/snippets_helpers.rb +++ b/lib/api/helpers/snippets_helpers.rb @@ -27,6 +27,24 @@ module API exactly_one_of :files, :content end + params :update_file_params do |options| + optional :files, type: Array, desc: 'An array of files to update' do + requires :action, type: String, + values: SnippetInputAction::ACTIONS.map(&:to_s), + desc: "The type of action to perform on the file, must be one of: #{SnippetInputAction::ACTIONS.join(", ")}" + optional :content, type: String, desc: 'The content of a snippet' + optional :file_path, file_path: true, type: String, desc: 'The file path of a snippet file' + optional :previous_path, file_path: true, type: String, desc: 'The previous path of a snippet file' + end + + mutually_exclusive :files, :content + mutually_exclusive :files, :file_name + end + + params :minimum_update_params do + at_least_one_of :content, :description, :files, :file_name, :title, :visibility + end + def content_for(snippet) if snippet.empty_repo? env['api.format'] = :txt @@ -53,10 +71,30 @@ module API end end - def process_file_args(args) - args[:snippet_actions] = args.delete(:files)&.map do |file| - file[:action] = :create - file.symbolize_keys + def process_create_params(args) + with_api_params do |api_params| + args[:snippet_actions] = args.delete(:files)&.map do |file| + file[:action] = :create + file.symbolize_keys + end + + args.merge(api_params) + end + end + + def process_update_params(args) + with_api_params do |api_params| + args[:snippet_actions] = args.delete(:files)&.map(&:symbolize_keys) + + args.merge(api_params) + end + end + + def validate_params_for_multiple_files(snippet) + return unless params[:content] || params[:file_name] + + if Feature.enabled?(:snippet_multiple_files, current_user) && snippet.multiple_files? + render_api_error!({ error: _('To update Snippets with multiple files, you must use the `files` parameter') }, 400) end end end diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb index 17599c72243..ff687a57888 100644 --- a/lib/api/internal/base.rb +++ b/lib/api/internal/base.rb @@ -241,14 +241,16 @@ module API break { success: false, message: "Invalid token expiry date: '#{params[:expires_at]}'" } end - access_token = nil + result = ::PersonalAccessTokens::CreateService.new( + user, name: params[:name], scopes: params[:scopes], expires_at: expires_at + ).execute - ::Users::UpdateService.new(current_user, user: user).execute! do |user| - access_token = user.personal_access_tokens.create!( - name: params[:name], scopes: params[:scopes], expires_at: expires_at - ) + unless result.status == :success + break { success: false, message: "Failed to create token: #{result.message}" } end + access_token = result.payload[:personal_access_token] + { success: true, token: access_token.token, scopes: access_token.scopes, expires_at: access_token.expires_at } end diff --git a/lib/api/internal/kubernetes.rb b/lib/api/internal/kubernetes.rb index 7f64fd7efe3..6d5dfd086e7 100644 --- a/lib/api/internal/kubernetes.rb +++ b/lib/api/internal/kubernetes.rb @@ -4,7 +4,16 @@ module API # Kubernetes Internal API module Internal class Kubernetes < Grape::API::Instance + before do + check_feature_enabled + authenticate_gitlab_kas_request! + end + helpers do + def authenticate_gitlab_kas_request! + unauthorized! unless Gitlab::Kas.verify_api_request(headers) + end + def agent_token @agent_token ||= cluster_agent_token_from_authorization_token end @@ -36,7 +45,7 @@ module API end def check_feature_enabled - not_found! unless Feature.enabled?(:kubernetes_agent_internal_api) + not_found! unless Feature.enabled?(:kubernetes_agent_internal_api, default_enabled: true, type: :ops) end def check_agent_token @@ -47,7 +56,6 @@ module API namespace 'internal' do namespace 'kubernetes' do before do - check_feature_enabled check_agent_token end @@ -89,6 +97,26 @@ module API } end end + + namespace 'kubernetes/usage_metrics' do + desc 'POST usage metrics' do + detail 'Updates usage metrics for agent' + end + params do + requires :gitops_sync_count, type: Integer, desc: 'The count to increment the gitops_sync metric by' + end + post '/' do + gitops_sync_count = params[:gitops_sync_count] + + if gitops_sync_count < 0 + bad_request!('gitops_sync_count must be greater than or equal to zero') + else + Gitlab::UsageDataCounters::KubernetesAgentCounter.increment_gitops_sync(gitops_sync_count) + + no_content! + end + end + end end end end diff --git a/lib/api/issue_links.rb b/lib/api/issue_links.rb new file mode 100644 index 00000000000..6cc5b344f47 --- /dev/null +++ b/lib/api/issue_links.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +module API + class IssueLinks < Grape::API::Instance + include PaginationParams + + before { authenticate! } + + params do + requires :id, type: String, desc: 'The ID of a project' + requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue' + end + resource :projects, requirements: { id: %r{[^/]+} } do + desc 'Get related issues' do + success Entities::RelatedIssue + end + get ':id/issues/:issue_iid/links' do + source_issue = find_project_issue(params[:issue_iid]) + related_issues = source_issue.related_issues(current_user) + + present related_issues, + with: Entities::RelatedIssue, + current_user: current_user, + project: user_project + end + + desc 'Relate issues' do + success Entities::IssueLink + end + params do + requires :target_project_id, type: String, desc: 'The ID of the target project' + requires :target_issue_iid, type: Integer, desc: 'The IID of the target issue' + optional :link_type, type: String, values: IssueLink.link_types.keys, + desc: 'The type of the relation' + end + # rubocop: disable CodeReuse/ActiveRecord + post ':id/issues/:issue_iid/links' do + source_issue = find_project_issue(params[:issue_iid]) + target_issue = find_project_issue(declared_params[:target_issue_iid], + declared_params[:target_project_id]) + + create_params = { target_issuable: target_issue, link_type: declared_params[:link_type] } + + result = ::IssueLinks::CreateService + .new(source_issue, current_user, create_params) + .execute + + if result[:status] == :success + issue_link = IssueLink.find_by!(source: source_issue, target: target_issue) + + present issue_link, with: Entities::IssueLink + else + render_api_error!(result[:message], result[:http_status]) + end + end + # rubocop: enable CodeReuse/ActiveRecord + + desc 'Remove issues relation' do + success Entities::IssueLink + end + params do + requires :issue_link_id, type: Integer, desc: 'The ID of an issue link' + end + delete ':id/issues/:issue_iid/links/:issue_link_id' do + issue_link = IssueLink.find(declared_params[:issue_link_id]) + + find_project_issue(params[:issue_iid]) + find_project_issue(issue_link.target.iid.to_s, issue_link.target.project_id.to_s) + + result = ::IssueLinks::DestroyService + .new(issue_link, current_user) + .execute + + if result[:status] == :success + present issue_link, with: Entities::IssueLink + else + render_api_error!(result[:message], result[:http_status]) + end + end + end + end +end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 1694a967f26..0e5b0fae6e2 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -114,6 +114,19 @@ module API present issues, options end + + desc "Get specified issue (admin only)" do + success Entities::Issue + end + params do + requires :id, type: String, desc: 'The ID of the Issue' + end + get ":id" do + authenticated_as_admin! + issue = Issue.find(params['id']) + + present issue, with: Entities::Issue, current_user: current_user, project: issue.project + end end params do diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb index 084c146abe7..ad46d948f3b 100644 --- a/lib/api/jobs.rb +++ b/lib/api/jobs.rb @@ -48,54 +48,6 @@ module API end # rubocop: enable CodeReuse/ActiveRecord - desc 'Get pipeline jobs' do - success Entities::Ci::Job - end - params do - requires :pipeline_id, type: Integer, desc: 'The pipeline ID' - use :optional_scope - use :pagination - end - # rubocop: disable CodeReuse/ActiveRecord - get ':id/pipelines/:pipeline_id/jobs' do - authorize!(:read_pipeline, user_project) - pipeline = user_project.all_pipelines.find(params[:pipeline_id]) - authorize!(:read_build, pipeline) - - builds = pipeline.builds - builds = filter_builds(builds, params[:scope]) - builds = builds.preload(:job_artifacts_archive, :job_artifacts, project: [:namespace]) - - present paginate(builds), with: Entities::Ci::Job - end - # rubocop: enable CodeReuse/ActiveRecord - - desc 'Get pipeline bridge jobs' do - success ::API::Entities::Ci::Bridge - end - params do - requires :pipeline_id, type: Integer, desc: 'The pipeline ID' - use :optional_scope - use :pagination - end - # rubocop: disable CodeReuse/ActiveRecord - get ':id/pipelines/:pipeline_id/bridges' do - authorize!(:read_build, user_project) - pipeline = user_project.ci_pipelines.find(params[:pipeline_id]) - authorize!(:read_pipeline, pipeline) - - bridges = pipeline.bridges - bridges = filter_builds(bridges, params[:scope]) - bridges = bridges.preload( - :metadata, - downstream_pipeline: [project: [:route, { namespace: :route }]], - project: [:namespace] - ) - - present paginate(bridges), with: ::API::Entities::Ci::Bridge - end - # rubocop: enable CodeReuse/ActiveRecord - desc 'Get a specific job of a project' do success Entities::Ci::Job end diff --git a/lib/api/maven_packages.rb b/lib/api/maven_packages.rb index 32a45c59cfa..e6d9a9a7c20 100644 --- a/lib/api/maven_packages.rb +++ b/lib/api/maven_packages.rb @@ -107,7 +107,7 @@ module API when 'sha1' package_file.file_sha1 else - track_event('pull_package') if jar_file?(format) + package_event('pull_package') if jar_file?(format) present_carrierwave_file_with_head_support!(package_file.file) end end @@ -145,7 +145,7 @@ module API when 'sha1' package_file.file_sha1 else - track_event('pull_package') if jar_file?(format) + package_event('pull_package') if jar_file?(format) present_carrierwave_file_with_head_support!(package_file.file) end @@ -181,7 +181,7 @@ module API when 'sha1' package_file.file_sha1 else - track_event('pull_package') if jar_file?(format) + package_event('pull_package') if jar_file?(format) present_carrierwave_file_with_head_support!(package_file.file) end @@ -200,7 +200,7 @@ module API status 200 content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE - ::Packages::PackageFileUploader.workhorse_authorize(has_length: true) + ::Packages::PackageFileUploader.workhorse_authorize(has_length: true, maximum_size: user_project.actual_limits.maven_max_file_size) end desc 'Upload the maven package file' do @@ -214,6 +214,7 @@ module API route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true put ':id/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do authorize_upload! + bad_request!('File is too large') if user_project.actual_limits.exceeded?(:maven_max_file_size, params[:file].size) file_name, format = extract_format(params[:file_name]) @@ -232,7 +233,7 @@ module API when 'md5' nil else - track_event('push_package') if jar_file?(format) + package_event('push_package') if jar_file?(format) file_params = { file: params[:file], diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 6f25df720c4..4bd72b267a9 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -29,11 +29,13 @@ module API remove_labels milestone_id remove_source_branch - state_event + allow_collaboration + allow_maintainer_to_push + squash target_branch title + state_event discussion_locked - squash ] end @@ -154,13 +156,13 @@ module API helpers do params :optional_params do - optional :description, type: String, desc: 'The description of the merge request' optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request' optional :assignee_ids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The array of user IDs to assign issue' - optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request' + optional :description, type: String, desc: 'The description of the merge request' optional :labels, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names' optional :add_labels, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names' optional :remove_labels, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names' + optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request' optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging' optional :allow_collaboration, type: Boolean, desc: 'Allow commits from members who can merge to the target branch' optional :allow_maintainer_to_push, type: Boolean, as: :allow_collaboration, desc: '[deprecated] See allow_collaboration' diff --git a/lib/api/npm_packages.rb b/lib/api/npm_packages.rb index 21ca57b7985..fca405b76b7 100644 --- a/lib/api/npm_packages.rb +++ b/lib/api/npm_packages.rb @@ -141,7 +141,7 @@ module API package_file = ::Packages::PackageFileFinder .new(package, params[:file_name]).execute! - track_event('pull_package') + package_event('pull_package') present_carrierwave_file!(package_file.file) end @@ -157,7 +157,7 @@ module API put ':id/packages/npm/:package_name', requirements: NPM_ENDPOINT_REQUIREMENTS do authorize_create_package!(user_project) - track_event('push_package') + package_event('push_package') created_package = ::Packages::Npm::CreatePackageService .new(user_project, current_user, params.merge(build: current_authenticated_job)).execute diff --git a/lib/api/nuget_packages.rb b/lib/api/nuget_packages.rb index 56c4de2071d..f84a3acbe6d 100644 --- a/lib/api/nuget_packages.rb +++ b/lib/api/nuget_packages.rb @@ -92,6 +92,7 @@ module API put do authorize_upload!(authorized_user_project) + bad_request!('File is too large') if authorized_user_project.actual_limits.exceeded?(:nuget_max_file_size, params[:package].size) file_params = params.merge( file: params[:package], @@ -104,7 +105,7 @@ module API package_file = ::Packages::CreatePackageFileService.new(package, file_params) .execute - track_event('push_package') + package_event('push_package') ::Packages::Nuget::ExtractionWorker.perform_async(package_file.id) # rubocop:disable CodeReuse/Worker @@ -118,7 +119,11 @@ module API route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true put 'authorize' do - authorize_workhorse!(subject: authorized_user_project, has_length: false) + authorize_workhorse!( + subject: authorized_user_project, + has_length: false, + maximum_size: authorized_user_project.actual_limits.nuget_max_file_size + ) end params do @@ -193,7 +198,7 @@ module API not_found!('Package') unless package_file - track_event('pull_package') + package_event('pull_package') # nuget and dotnet don't support 302 Moved status codes, supports_direct_download has to be set to false present_carrierwave_file!(package_file.file, supports_direct_download: false) @@ -228,7 +233,7 @@ module API .new(authorized_user_project, params[:q], search_options) .execute - track_event('search_package') + package_event('search_package') present ::Packages::Nuget::SearchResultsPresenter.new(search), with: ::API::Entities::Nuget::SearchResults diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index fba4c60504f..f6e87fece89 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -64,12 +64,8 @@ module API end post ":id/snippets" do authorize! :create_snippet, user_project - snippet_params = declared_params(include_missing: false).tap do |create_args| - create_args[:request] = request - create_args[:api] = true - process_file_args(create_args) - end + snippet_params = process_create_params(declared_params(include_missing: false)) service_response = ::Snippets::CreateService.new(user_project, current_user, snippet_params).execute snippet = service_response.payload[:snippet] @@ -88,14 +84,16 @@ module API end params do requires :snippet_id, type: Integer, desc: 'The ID of a project snippet' - optional :title, type: String, allow_blank: false, desc: 'The title of the snippet' - optional :file_name, type: String, desc: 'The file name of the snippet' optional :content, type: String, allow_blank: false, desc: 'The content of the snippet' optional :description, type: String, desc: 'The description of a snippet' + optional :file_name, type: String, desc: 'The file name of the snippet' + optional :title, type: String, allow_blank: false, desc: 'The title of the snippet' optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the snippet' - at_least_one_of :title, :file_name, :content, :visibility_level + + use :update_file_params + use :minimum_update_params end # rubocop: disable CodeReuse/ActiveRecord put ":id/snippets/:snippet_id" do @@ -104,8 +102,9 @@ module API authorize! :update_snippet, snippet - snippet_params = declared_params(include_missing: false) - .merge(request: request, api: true) + validate_params_for_multiple_files(snippet) + + snippet_params = process_update_params(declared_params(include_missing: false)) service_response = ::Snippets::UpdateService.new(user_project, current_user, snippet_params).execute(snippet) snippet = service_response.payload[:snippet] diff --git a/lib/api/pypi_packages.rb b/lib/api/pypi_packages.rb index 739928a61ed..c07db68f8a8 100644 --- a/lib/api/pypi_packages.rb +++ b/lib/api/pypi_packages.rb @@ -64,7 +64,7 @@ module API requires :sha256, type: String, desc: 'The PyPi package sha256 check sum' end - route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true + route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth get 'files/:sha256/*file_identifier' do project = unauthorized_user_project! @@ -72,7 +72,7 @@ module API package = packages_finder(project).by_file_name_and_sha256(filename, params[:sha256]) package_file = ::Packages::PackageFileFinder.new(package, filename, with_file_name_like: false).execute - track_event('pull_package') + package_event('pull_package') present_carrierwave_file!(package_file.file, supports_direct_download: true) end @@ -87,11 +87,11 @@ module API # An Api entry point but returns an HTML file instead of JSON. # PyPi simple API returns the package descriptor as a simple HTML file. - route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true + route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth get 'simple/*package_name', format: :txt do authorize_read_package!(authorized_user_project) - track_event('list_package') + package_event('list_package') packages = find_package_versions presenter = ::Packages::Pypi::PackagePresenter.new(packages, authorized_user_project) @@ -117,11 +117,12 @@ module API optional :sha256_digest, type: String end - route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true + route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth post do authorize_upload!(authorized_user_project) + bad_request!('File is too large') if authorized_user_project.actual_limits.exceeded?(:pypi_max_file_size, params[:content].size) - track_event('push_package') + package_event('push_package') ::Packages::Pypi::CreatePackageService .new(authorized_user_project, current_user, declared_params) @@ -134,9 +135,13 @@ module API forbidden! end - route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true + route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth post 'authorize' do - authorize_workhorse!(subject: authorized_user_project, has_length: false) + authorize_workhorse!( + subject: authorized_user_project, + has_length: false, + maximum_size: authorized_user_project.actual_limits.pypi_max_file_size + ) end end end diff --git a/lib/api/search.rb b/lib/api/search.rb index 53095e0b81a..b9c6a823f4f 100644 --- a/lib/api/search.rb +++ b/lib/api/search.rb @@ -32,6 +32,7 @@ module API search_params = { scope: params[:scope], search: params[:search], + state: params[:state], snippets: snippets?, page: params[:page], per_page: params[:per_page] @@ -79,6 +80,7 @@ module API type: String, desc: 'The scope of the search', values: Helpers::SearchHelpers.global_search_scopes + optional :state, type: String, desc: 'Filter results by state', values: Helpers::SearchHelpers.search_states use :pagination end get do @@ -100,6 +102,7 @@ module API type: String, desc: 'The scope of the search', values: Helpers::SearchHelpers.group_search_scopes + optional :state, type: String, desc: 'Filter results by state', values: Helpers::SearchHelpers.search_states use :pagination end get ':id/(-/)search' do @@ -122,6 +125,7 @@ module API desc: 'The scope of the search', values: Helpers::SearchHelpers.project_search_scopes optional :ref, type: String, desc: 'The name of a repository branch or tag. If not given, the default branch is used' + optional :state, type: String, desc: 'Filter results by state', values: Helpers::SearchHelpers.search_states use :pagination end get ':id/(-/)search' do diff --git a/lib/api/settings.rb b/lib/api/settings.rb index f2e0aaecfb9..6e5534d0c9a 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -61,6 +61,10 @@ module API end optional :email_author_in_body, type: Boolean, desc: 'Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead.' optional :enabled_git_access_protocol, type: String, values: %w[ssh http nil], desc: 'Allow only the selected protocols to be used for Git access.' + optional :gitpod_enabled, type: Boolean, desc: 'Enable Gitpod' + given gitpod_enabled: ->(val) { val } do + requires :gitpod_url, type: String, desc: 'The configured Gitpod instance URL' + end optional :gitaly_timeout_default, type: Integer, desc: 'Default Gitaly timeout, in seconds. Set to 0 to disable timeouts.' optional :gitaly_timeout_fast, type: Integer, desc: 'Gitaly fast operation timeout, in seconds. Set to 0 to disable timeouts.' optional :gitaly_timeout_medium, type: Integer, desc: 'Medium Gitaly timeout, in seconds. Set to 0 to disable timeouts.' @@ -140,11 +144,9 @@ module API end optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.' optional :usage_ping_enabled, type: Boolean, desc: 'Every week GitLab will report license usage back to GitLab, Inc.' - optional :instance_statistics_visibility_private, type: Boolean, desc: 'When set to `true` Instance statistics will only be available to admins' optional :local_markdown_version, type: Integer, desc: 'Local markdown version, increase this value when any cached markdown should be invalidated' optional :allow_local_requests_from_hooks_and_services, type: Boolean, desc: 'Deprecated: Use :allow_local_requests_from_web_hooks_and_services instead. Allow requests to the local network from hooks and services.' # support legacy names, can be removed in v5 optional :snowplow_enabled, type: Grape::API::Boolean, desc: 'Enable Snowplow tracking' - optional :snowplow_iglu_registry_url, type: String, desc: 'The Snowplow base Iglu Schema Registry URL to use for custom context and self describing events' given snowplow_enabled: ->(val) { val } do requires :snowplow_collector_hostname, type: String, desc: 'The Snowplow collector hostname' optional :snowplow_cookie_domain, type: String, desc: 'The Snowplow cookie domain' @@ -152,6 +154,7 @@ module API end optional :issues_create_limit, type: Integer, desc: "Maximum number of issue creation requests allowed per minute per user. Set to 0 for unlimited requests per minute." optional :raw_blob_request_limit, type: Integer, desc: "Maximum number of requests per minute for each raw path. Set to 0 for unlimited requests per minute." + optional :wiki_page_max_content_bytes, type: Integer, desc: "Maximum wiki page content size in bytes" ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type| optional :"#{type}_key_restriction", diff --git a/lib/api/sidekiq_metrics.rb b/lib/api/sidekiq_metrics.rb index de1373144e3..77f2b1e871e 100644 --- a/lib/api/sidekiq_metrics.rb +++ b/lib/api/sidekiq_metrics.rb @@ -17,7 +17,7 @@ module API end def process_metrics - Sidekiq::ProcessSet.new.map do |process| + Sidekiq::ProcessSet.new(false).map do |process| { hostname: process['hostname'], pid: process['pid'], diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb index 1a3283aed98..c6ef35875fc 100644 --- a/lib/api/snippets.rb +++ b/lib/api/snippets.rb @@ -76,12 +76,7 @@ module API post do authorize! :create_snippet - attrs = declared_params(include_missing: false).tap do |create_args| - create_args[:request] = request - create_args[:api] = true - - process_file_args(create_args) - end + attrs = process_create_params(declared_params(include_missing: false)) service_response = ::Snippets::CreateService.new(nil, current_user, attrs).execute snippet = service_response.payload[:snippet] @@ -99,16 +94,19 @@ module API detail 'This feature was introduced in GitLab 8.15.' success Entities::PersonalSnippet end + params do requires :id, type: Integer, desc: 'The ID of a snippet' - optional :title, type: String, allow_blank: false, desc: 'The title of a snippet' - optional :file_name, type: String, desc: 'The name of a snippet file' optional :content, type: String, allow_blank: false, desc: 'The content of a snippet' optional :description, type: String, desc: 'The description of a snippet' + optional :file_name, type: String, desc: 'The name of a snippet file' + optional :title, type: String, allow_blank: false, desc: 'The title of a snippet' optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the snippet' - at_least_one_of :title, :file_name, :content, :visibility + + use :update_file_params + use :minimum_update_params end put ':id' do snippet = snippets_for_current_user.find_by_id(params.delete(:id)) @@ -116,8 +114,12 @@ module API authorize! :update_snippet, snippet - attrs = declared_params(include_missing: false).merge(request: request, api: true) + validate_params_for_multiple_files(snippet) + + attrs = process_update_params(declared_params(include_missing: false)) + service_response = ::Snippets::UpdateService.new(nil, current_user, attrs).execute(snippet) + snippet = service_response.payload[:snippet] if service_response.success? diff --git a/lib/api/terraform/state.rb b/lib/api/terraform/state.rb index f6e966defce..7063a3d08b5 100644 --- a/lib/api/terraform/state.rb +++ b/lib/api/terraform/state.rb @@ -35,10 +35,10 @@ module API route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth get do remote_state_handler.find_with_lock do |state| - no_content! unless state.file.exists? + no_content! unless state.latest_file && state.latest_file.exists? env['api.format'] = :binary # this bypasses json serialization - body state.file.read + body state.latest_file.read status :ok end end @@ -52,8 +52,7 @@ module API no_content! if data.empty? remote_state_handler.handle_with_lock do |state| - state.file = CarrierWaveStringFile.new(data) - state.save! + state.update_file!(CarrierWaveStringFile.new(data), version: params[:serial]) status :ok end end diff --git a/lib/api/todos.rb b/lib/api/todos.rb index 4a73e3e0e94..5eae92a251e 100644 --- a/lib/api/todos.rb +++ b/lib/api/todos.rb @@ -39,8 +39,17 @@ module API resource :todos do helpers do + params :todo_filters do + optional :action, String, values: Todo::ACTION_NAMES.values.map(&:to_s) + optional :author_id, Integer + optional :state, String, values: Todo.state_machine.states.map(&:name).map(&:to_s) + optional :type, String, values: TodosFinder.todo_types + optional :project_id, Integer + optional :group_id, Integer + end + def find_todos - TodosFinder.new(current_user, params).execute + TodosFinder.new(current_user, declared_params(include_missing: false)).execute end def issuable_and_awardable?(type) @@ -72,7 +81,7 @@ module API success Entities::Todo end params do - use :pagination + use :pagination, :todo_filters end get do todos = paginate(find_todos.with_entity_associations) diff --git a/lib/api/usage_data.rb b/lib/api/usage_data.rb new file mode 100644 index 00000000000..a1512197ee1 --- /dev/null +++ b/lib/api/usage_data.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module API + class UsageData < Grape::API::Instance + before { authenticate! } + + namespace 'usage_data' do + before do + not_found! unless Feature.enabled?(:usage_data_api) + forbidden!('Invalid CSRF token is provided') unless verified_request? + end + + desc 'Track usage data events' do + detail 'This feature was introduced in GitLab 13.4.' + end + + params do + requires :event, type: String, desc: 'The event name that should be tracked' + end + + post 'increment_unique_users' do + event_name = params[:event] + + increment_unique_values(event_name, current_user.id) + + status :ok + end + end + end +end diff --git a/lib/api/users.rb b/lib/api/users.rb index 335624963aa..73bb43b88fc 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -84,6 +84,7 @@ module API optional :created_after, type: DateTime, desc: 'Return users created after the specified time' optional :created_before, type: DateTime, desc: 'Return users created before the specified time' optional :without_projects, type: Boolean, default: false, desc: 'Filters only users without projects' + optional :exclude_internal, as: :non_internal, type: Boolean, default: false, desc: 'Filters only non internal users' all_or_none_of :extern_uid, :provider use :sort_params @@ -115,6 +116,7 @@ module API entity = current_user&.admin? ? Entities::UserWithAdmin : Entities::UserBasic users = users.preload(:identities, :u2f_registrations) if entity == Entities::UserWithAdmin + users = users.preload(:identities, :webauthn_registrations) if entity == Entities::UserWithAdmin users, options = with_custom_attributes(users, { with: entity, current_user: current_user }) users = users.preload(:user_detail) @@ -217,9 +219,15 @@ module API .where.not(id: user.id).exists? user_params = declared_params(include_missing: false) + admin_making_changes_for_another_user = (current_user != user) - user_params[:password_expires_at] = Time.current if user_params[:password].present? - result = ::Users::UpdateService.new(current_user, user_params.merge(user: user)).execute + if user_params[:password].present? + user_params[:password_expires_at] = Time.current if admin_making_changes_for_another_user + end + + result = ::Users::UpdateService.new(current_user, user_params.merge(user: user)).execute do |user| + user.send_only_admin_changed_your_password_notification! if admin_making_changes_for_another_user + end if result[:status] == :success present user, with: Entities::UserWithAdmin, current_user: current_user diff --git a/lib/api/v3/github.rb b/lib/api/v3/github.rb new file mode 100644 index 00000000000..593f90460ac --- /dev/null +++ b/lib/api/v3/github.rb @@ -0,0 +1,232 @@ +# frozen_string_literal: true + +# These endpoints partially mimic Github API behavior in order to successfully +# integrate with Jira Development Panel. +# Endpoints returning an empty list were temporarily added to avoid 404's +# during Jira's DVCS integration. +# +module API + module V3 + class Github < Grape::API::Instance + NO_SLASH_URL_PART_REGEX = %r{[^/]+}.freeze + ENDPOINT_REQUIREMENTS = { + namespace: NO_SLASH_URL_PART_REGEX, + project: NO_SLASH_URL_PART_REGEX, + username: NO_SLASH_URL_PART_REGEX + }.freeze + + # Used to differentiate Jira Cloud requests from Jira Server requests + # Jira Cloud user agent format: Jira DVCS Connector Vertigo/version + # Jira Server user agent format: Jira DVCS Connector/version + JIRA_DVCS_CLOUD_USER_AGENT = 'Jira DVCS Connector Vertigo'.freeze + + include PaginationParams + + before do + authorize_jira_user_agent!(request) + authenticate! + end + + helpers do + params :project_full_path do + requires :namespace, type: String + requires :project, type: String + end + + def authorize_jira_user_agent!(request) + not_found! unless Gitlab::Jira::Middleware.jira_dvcs_connector?(request.env) + end + + def update_project_feature_usage_for(project) + # Prevent errors on GitLab Geo not allowing + # UPDATE statements to happen in GET requests. + return if Gitlab::Database.read_only? + + project.log_jira_dvcs_integration_usage(cloud: jira_cloud?) + end + + def jira_cloud? + request.env['HTTP_USER_AGENT'].include?(JIRA_DVCS_CLOUD_USER_AGENT) + end + + def find_project_with_access(params) + project = find_project!( + ::Gitlab::Jira::Dvcs.restore_full_path(params.slice(:namespace, :project).symbolize_keys) + ) + not_found! unless can?(current_user, :download_code, project) + project + end + + # rubocop: disable CodeReuse/ActiveRecord + def find_merge_requests + merge_requests = authorized_merge_requests.reorder(updated_at: :desc) + paginate(merge_requests) + end + # rubocop: enable CodeReuse/ActiveRecord + + # rubocop: disable CodeReuse/ActiveRecord + def find_merge_request_with_access(id, access_level = :read_merge_request) + merge_request = authorized_merge_requests.find_by(id: id) + not_found! unless can?(current_user, access_level, merge_request) + merge_request + end + # rubocop: enable CodeReuse/ActiveRecord + + def authorized_merge_requests + MergeRequestsFinder.new(current_user, authorized_only: !current_user.admin?).execute + end + + def authorized_merge_requests_for_project(project) + MergeRequestsFinder.new(current_user, authorized_only: !current_user.admin?, project_id: project.id).execute + end + + # rubocop: disable CodeReuse/ActiveRecord + def find_notes(noteable) + # They're not presented on Jira Dev Panel ATM. A comments count with a + # redirect link is presented. + notes = paginate(noteable.notes.user.reorder(nil)) + notes.select { |n| n.readable_by?(current_user) } + end + # rubocop: enable CodeReuse/ActiveRecord + end + + resource :orgs do + get ':namespace/repos' do + present [] + end + end + + resource :user do + get :repos do + present [] + end + end + + resource :users do + params do + use :pagination + end + + get ':namespace/repos' do + namespace = Namespace.find_by_full_path(params[:namespace]) + not_found!('Namespace') unless namespace + + projects = current_user.can_read_all_resources? ? Project.all : current_user.authorized_projects + projects = projects.in_namespace(namespace.self_and_descendants) + + projects_cte = Project.wrap_with_cte(projects) + .eager_load_namespace_and_owner + .with_route + + present paginate(projects_cte), + with: ::API::Github::Entities::Repository, + root_namespace: namespace.root_ancestor + end + + get ':username' do + forbidden! unless can?(current_user, :read_users_list) + user = UsersFinder.new(current_user, { username: params[:username] }).execute.first + not_found! unless user + present user, with: ::API::Github::Entities::User + end + end + + # Jira dev panel integration weirdly requests for "/-/jira/pulls" instead + # "/api/v3/repos/<namespace>/<project>/pulls". This forces us into + # returning _all_ Merge Requests from authorized projects (user is a member), + # instead just the authorized MRs from a project. + # Jira handles the filtering, presenting just MRs mentioning the Jira + # issue ID on the MR title / description. + resource :repos do + # Keeping for backwards compatibility with old Jira integration instructions + # so that users that do not change it will not suddenly have a broken integration + get '/-/jira/pulls' do + present find_merge_requests, with: ::API::Github::Entities::PullRequest + end + + get '/-/jira/events' do + present [] + end + + params do + use :project_full_path + end + get ':namespace/:project/pulls' do + user_project = find_project_with_access(params) + + merge_requests = authorized_merge_requests_for_project(user_project) + + present paginate(merge_requests), with: ::API::Github::Entities::PullRequest + end + + params do + use :project_full_path + end + get ':namespace/:project/pulls/:id' do + merge_request = find_merge_request_with_access(params[:id]) + + present merge_request, with: ::API::Github::Entities::PullRequest + end + + # In Github, each Merge Request is automatically also an issue. + # Therefore we return its comments here. + # It'll present _just_ the comments counting with a link to GitLab on + # Jira dev panel, not the actual note content. + get ':namespace/:project/issues/:id/comments' do + merge_request = find_merge_request_with_access(params[:id]) + + present find_notes(merge_request), with: ::API::Github::Entities::NoteableComment + end + + # This refer to "review" comments but Jira dev panel doesn't seem to + # present it accordingly. + get ':namespace/:project/pulls/:id/comments' do + present [] + end + + # Commits are not presented within "Pull Requests" modal on Jira dev + # panel. + get ':namespace/:project/pulls/:id/commits' do + present [] + end + + # Self-hosted Jira (tested on 7.11.1) requests this endpoint right + # after fetching branches. + get ':namespace/:project/events' do + user_project = find_project_with_access(params) + + merge_requests = authorized_merge_requests_for_project(user_project) + + present paginate(merge_requests), with: ::API::Github::Entities::PullRequestEvent + end + + params do + use :project_full_path + use :pagination + end + get ':namespace/:project/branches' do + user_project = find_project_with_access(params) + + update_project_feature_usage_for(user_project) + + branches = ::Kaminari.paginate_array(user_project.repository.branches.sort_by(&:name)) + + present paginate(branches), with: ::API::Github::Entities::Branch, project: user_project + end + + params do + use :project_full_path + end + get ':namespace/:project/commits/:sha' do + user_project = find_project_with_access(params) + + commit = user_project.commit(params[:sha]) + + not_found! 'Commit' unless commit + + present commit, with: ::API::Github::Entities::RepoCommit + end + end + end + end +end diff --git a/lib/api/variables.rb b/lib/api/variables.rb index cea0cb3a19c..0b3ec10f1b4 100644 --- a/lib/api/variables.rb +++ b/lib/api/variables.rb @@ -17,7 +17,6 @@ module API def find_variable(params) variables = ::Ci::VariablesFinder.new(user_project, params).execute.to_a - return variables.first unless ::Gitlab::Ci::Features.variables_api_filter_environment_scope? return variables.first unless variables.many? # rubocop: disable CodeReuse/ActiveRecord conflict!("There are multiple variables with provided parameters. Please use 'filter[environment_scope]'") diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb index 95afa36113c..4eba12157bd 100644 --- a/lib/api/wikis.rb +++ b/lib/api/wikis.rb @@ -83,11 +83,12 @@ module API put ':id/wikis/:slug' do authorize! :create_wiki, container - page = WikiPages::UpdateService + response = WikiPages::UpdateService .new(container: container, current_user: current_user, params: params) .execute(wiki_page) + page = response.payload[:page] - if page.valid? + if response.success? present page, with: Entities::WikiPage else render_validation_error!(page) @@ -101,11 +102,15 @@ module API delete ':id/wikis/:slug' do authorize! :admin_wiki, container - WikiPages::DestroyService + response = WikiPages::DestroyService .new(container: container, current_user: current_user) .execute(wiki_page) - no_content! + if response.success? + no_content! + else + render_api_error!(reponse.message) + end end desc 'Upload an attachment to the wiki repository' do |